kibi-cli 0.12.7 → 0.14.0

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 (114) hide show
  1. package/dist/cli.js +1 -0
  2. package/dist/commands/check.d.ts +1 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +130 -82
  5. package/dist/commands/discovery-shared.d.ts +1 -0
  6. package/dist/commands/discovery-shared.d.ts.map +1 -1
  7. package/dist/commands/discovery-shared.js +2 -191
  8. package/dist/commands/discovery-table.d.ts +3 -0
  9. package/dist/commands/discovery-table.d.ts.map +1 -0
  10. package/dist/commands/discovery-table.js +204 -0
  11. package/dist/commands/sync/discovery.d.ts.map +1 -1
  12. package/dist/commands/sync/discovery.js +8 -1
  13. package/dist/commands/sync/extraction.d.ts +6 -1
  14. package/dist/commands/sync/extraction.d.ts.map +1 -1
  15. package/dist/commands/sync/extraction.js +7 -3
  16. package/dist/commands/sync/persistence.d.ts.map +1 -1
  17. package/dist/commands/sync/persistence.js +39 -5
  18. package/dist/extractors/manifest.d.ts.map +1 -1
  19. package/dist/extractors/manifest.js +18 -0
  20. package/dist/extractors/markdown.d.ts +6 -0
  21. package/dist/extractors/markdown.d.ts.map +1 -1
  22. package/dist/extractors/symbols-coordinator.d.ts +1 -1
  23. package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
  24. package/dist/extractors/symbols-ts.js +62 -0
  25. package/dist/prolog.d.ts.map +1 -1
  26. package/dist/prolog.js +11 -7
  27. package/dist/public/impact/analyzer.d.ts +3 -0
  28. package/dist/public/impact/analyzer.d.ts.map +1 -0
  29. package/dist/public/impact/analyzer.js +54 -0
  30. package/dist/public/impact/coverage-depth-quality.d.ts +4 -0
  31. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -0
  32. package/dist/public/impact/coverage-depth-quality.js +179 -0
  33. package/dist/public/impact/diagnostics.d.ts +7 -0
  34. package/dist/public/impact/diagnostics.d.ts.map +1 -0
  35. package/dist/public/impact/diagnostics.js +122 -0
  36. package/dist/public/impact/diff-meaning.d.ts +2 -0
  37. package/dist/public/impact/diff-meaning.d.ts.map +1 -0
  38. package/dist/public/impact/diff-meaning.js +101 -0
  39. package/dist/public/impact/full-kb-quality.d.ts +10 -0
  40. package/dist/public/impact/full-kb-quality.d.ts.map +1 -0
  41. package/dist/public/impact/full-kb-quality.js +158 -0
  42. package/dist/public/impact/manifest.d.ts +5 -0
  43. package/dist/public/impact/manifest.d.ts.map +1 -0
  44. package/dist/public/impact/manifest.js +43 -0
  45. package/dist/public/impact/requirement-quality.d.ts +4 -0
  46. package/dist/public/impact/requirement-quality.d.ts.map +1 -0
  47. package/dist/public/impact/requirement-quality.js +216 -0
  48. package/dist/public/impact/source-changes.d.ts +5 -0
  49. package/dist/public/impact/source-changes.d.ts.map +1 -0
  50. package/dist/public/impact/source-changes.js +129 -0
  51. package/dist/public/impact/summaries.d.ts +6 -0
  52. package/dist/public/impact/summaries.d.ts.map +1 -0
  53. package/dist/public/impact/summaries.js +68 -0
  54. package/dist/public/impact/symbol-quality-model.d.ts +21 -0
  55. package/dist/public/impact/symbol-quality-model.d.ts.map +1 -0
  56. package/dist/public/impact/symbol-quality-model.js +116 -0
  57. package/dist/public/impact/symbol-quality.d.ts +4 -0
  58. package/dist/public/impact/symbol-quality.d.ts.map +1 -0
  59. package/dist/public/impact/symbol-quality.js +159 -0
  60. package/dist/public/impact/types.d.ts +79 -0
  61. package/dist/public/impact/types.d.ts.map +1 -0
  62. package/dist/public/impact/types.js +1 -0
  63. package/dist/public/impact-diagnostics.d.ts +7 -0
  64. package/dist/public/impact-diagnostics.d.ts.map +1 -0
  65. package/dist/public/impact-diagnostics.js +5 -0
  66. package/dist/public/schemas/entity.d.ts.map +1 -1
  67. package/dist/public/schemas/entity.js +21 -0
  68. package/dist/public/skills/kibi-usage/SKILL.md +20 -5
  69. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  70. package/dist/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
  71. package/dist/public/skills/kibi-usage/resources/workflows.md +14 -0
  72. package/dist/public/symbol-granularity.d.ts +1 -1
  73. package/dist/public/symbol-granularity.d.ts.map +1 -1
  74. package/dist/public/symbol-granularity.js +2 -0
  75. package/dist/traceability/evidence-model.d.ts +1 -1
  76. package/dist/traceability/evidence-model.d.ts.map +1 -1
  77. package/dist/traceability/evidence-model.js +1 -1
  78. package/dist/traceability/staged-diagnostics.d.ts +6 -6
  79. package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
  80. package/dist/traceability/staged-diagnostics.js +12 -6
  81. package/dist/traceability/staged-impact-contract.js +1 -1
  82. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  83. package/dist/traceability/staged-symbols-manifest.js +23 -1
  84. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  85. package/dist/traceability/symbol-extract.js +41 -0
  86. package/dist/traceability/temp-kb.d.ts.map +1 -1
  87. package/dist/types/entities.d.ts +5 -1
  88. package/dist/types/entities.d.ts.map +1 -1
  89. package/dist/utils/prolog-query-plan-safety.d.ts +8 -0
  90. package/dist/utils/prolog-query-plan-safety.d.ts.map +1 -0
  91. package/dist/utils/prolog-query-plan-safety.js +47 -0
  92. package/dist/utils/rule-registry.d.ts.map +1 -1
  93. package/dist/utils/rule-registry.js +12 -0
  94. package/package.json +7 -9
  95. package/schema/entities.pl +4 -0
  96. package/src/public/impact/analyzer.ts +90 -0
  97. package/src/public/impact/coverage-depth-quality.ts +268 -0
  98. package/src/public/impact/diagnostics.ts +192 -0
  99. package/src/public/impact/diff-meaning.ts +121 -0
  100. package/src/public/impact/full-kb-quality.ts +203 -0
  101. package/src/public/impact/manifest.ts +57 -0
  102. package/src/public/impact/requirement-quality.ts +282 -0
  103. package/src/public/impact/source-changes.ts +180 -0
  104. package/src/public/impact/summaries.ts +100 -0
  105. package/src/public/impact/symbol-quality-model.ts +171 -0
  106. package/src/public/impact/symbol-quality.ts +207 -0
  107. package/src/public/impact/types.ts +99 -0
  108. package/src/public/impact-diagnostics.ts +22 -0
  109. package/src/public/schemas/entity.ts +23 -0
  110. package/src/public/skills/kibi-usage/SKILL.md +20 -5
  111. package/src/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  112. package/src/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
  113. package/src/public/skills/kibi-usage/resources/workflows.md +14 -0
  114. package/src/public/symbol-granularity.ts +4 -0
@@ -143,9 +143,31 @@ function normalizeAuthoredManifestSymbolsForSourceFile(records, sourceFile) {
143
143
  priority: typeof record.priority === "string" ? record.priority : null,
144
144
  severity: typeof record.severity === "string" ? record.severity : null,
145
145
  textRef: typeof record.text_ref === "string" ? record.text_ref : null,
146
+ granularityReason: typeof record.granularity_reason === "string"
147
+ ? record.granularity_reason
148
+ : null,
146
149
  }))
147
150
  .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
148
151
  }
152
+ function getAuthoredSymbolComparisonKey(symbol) {
153
+ return symbol.id ?? `${symbol.sourceFile}:${symbol.title}`;
154
+ }
155
+ function getChangedAuthoredEntityIds(headSymbols, stagedSymbols) {
156
+ const headSignatures = new Map(headSymbols.map((symbol) => [
157
+ getAuthoredSymbolComparisonKey(symbol),
158
+ JSON.stringify(symbol),
159
+ ]));
160
+ const changedIds = stagedSymbols.flatMap((symbol) => {
161
+ if (symbol.id === null)
162
+ return [];
163
+ const stagedSignature = JSON.stringify(symbol);
164
+ return headSignatures.get(getAuthoredSymbolComparisonKey(symbol)) ===
165
+ stagedSignature
166
+ ? []
167
+ : [symbol.id];
168
+ });
169
+ return uniqueSorted(changedIds);
170
+ }
149
171
  function normalizeExpectedSymbolsForStagedFile(stagedFile) {
150
172
  const analysis = analyzeSourceText(stagedFile.path, stagedFile.content ?? "");
151
173
  return analysis.symbols
@@ -268,7 +290,7 @@ export function collectStagedAuthoredSymbolsManifestEvidence(options) {
268
290
  }
269
291
  entries.push({
270
292
  sourcePath: sourceFile.path,
271
- entityIds: getEntityIdsForSourceFile(stagedManifestRecords, sourceFile.path),
293
+ entityIds: getChangedAuthoredEntityIds(headSymbols, stagedSymbols),
272
294
  });
273
295
  }
274
296
  return { path: paths.symbolsPath, entries };
@@ -1 +1 @@
1
- {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EAEhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,KAAK,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAU7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE9D,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG5E;AAmOD,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CAyOnB"}
1
+ {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EAEhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,KAAK,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAU7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE9D,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG5E;AAmOD,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CAwSnB"}
@@ -255,6 +255,47 @@ stagedFile, manifestLookup) {
255
255
  void stagedFile.path;
256
256
  }
257
257
  }
258
+ for (const property of typeof cls.getProperties === "function"
259
+ ? cls.getProperties()
260
+ : []) {
261
+ try {
262
+ const name = formatMethodSymbolName(cls.getName(), property.getName());
263
+ const start = property.getNameNode()?.getStart() ?? property.getStart();
264
+ const end = property.getEnd();
265
+ const span = getSpan(start, end);
266
+ const reqLinks = parseReqDirectives(`${property.getFullText()}\n${property
267
+ .getJsDocs()
268
+ .map((d) => d.getFullText())
269
+ .join("\n")}`);
270
+ results.push(buildSymbolResult(stagedFile, name, "property", span, reqLinks, manifestLookup));
271
+ }
272
+ catch {
273
+ void stagedFile.path;
274
+ }
275
+ }
276
+ for (const accessor of [
277
+ ...(typeof cls.getGetAccessors === "function"
278
+ ? cls.getGetAccessors()
279
+ : []),
280
+ ...(typeof cls.getSetAccessors === "function"
281
+ ? cls.getSetAccessors()
282
+ : []),
283
+ ]) {
284
+ try {
285
+ const name = formatMethodSymbolName(cls.getName(), accessor.getName());
286
+ const start = accessor.getNameNode()?.getStart() ?? accessor.getStart();
287
+ const end = accessor.getEnd();
288
+ const span = getSpan(start, end);
289
+ const reqLinks = parseReqDirectives(`${accessor.getFullText()}\n${accessor
290
+ .getJsDocs()
291
+ .map((d) => d.getFullText())
292
+ .join("\n")}`);
293
+ results.push(buildSymbolResult(stagedFile, name, "accessor", span, reqLinks, manifestLookup));
294
+ }
295
+ catch {
296
+ void stagedFile.path;
297
+ }
298
+ }
258
299
  }
259
300
  // Enums
260
301
  for (const en of sf.getEnums()) {
@@ -1 +1 @@
1
- {"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CASvC;AAOD;;;GAGG;AACH,wBAAgB,iBAAiB,CAE/B,OAAO,EAAE,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,aAAa,GACpD,IAAI,CAEN;AA4ID,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAG1B,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,gBAAgB,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6C7E;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
1
+ {"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CASvC;AAOD;;;GAGG;AACH,wBAAgB,iBAAiB,CAE/B,OAAO,EAAE,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,aAAa,GACpD,IAAI,CAEN;AA4ID,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAG1B,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,gBAAgB,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6C7E;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
@@ -39,13 +39,17 @@ export interface FactFields {
39
39
  examples?: string[];
40
40
  predicate_args?: string[];
41
41
  }
42
+ export interface TestVerificationFields {
43
+ verification_scope?: "unit" | "integration" | "end_to_end";
44
+ verification_perspective?: "internal" | "consumer";
45
+ }
42
46
  export type Requirement = BaseEntity & {
43
47
  type: "req";
44
48
  };
45
49
  export type Scenario = BaseEntity & {
46
50
  type: "scenario";
47
51
  };
48
- export type TestEntity = BaseEntity & {
52
+ export type TestEntity = BaseEntity & TestVerificationFields & {
49
53
  type: "test";
50
54
  };
51
55
  export type ADR = BaseEntity & {
@@ -1 +1 @@
1
- {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/types/entities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/types/entities.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EACN,SAAS,GACT,gBAAgB,GAChB,aAAa,GACb,MAAM,GACN,kBAAkB,GAClB,WAAW,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IACtD,UAAU,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,YAAY,CAAC;IAC3D,wBAAwB,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;CACpD;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,sBAAsB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAChF,MAAM,MAAM,GAAG,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AACnD,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,MAAM,GACd,WAAW,GACX,QAAQ,GACR,UAAU,GACV,GAAG,GACH,IAAI,GACJ,KAAK,GACL,MAAM,GACN,IAAI,CAAC"}
@@ -0,0 +1,8 @@
1
+ export interface PrologQueryPlanSafetyViolation {
2
+ readonly predicate: string;
3
+ readonly line: number;
4
+ readonly description: string;
5
+ readonly suggestion: string;
6
+ }
7
+ export declare function analyzePrologQueryPlanSafety(source: string): PrologQueryPlanSafetyViolation[];
8
+ //# sourceMappingURL=prolog-query-plan-safety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prolog-query-plan-safety.d.ts","sourceRoot":"","sources":["../../src/utils/prolog-query-plan-safety.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAMD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,GACb,8BAA8B,EAAE,CAyBlC"}
@@ -0,0 +1,47 @@
1
+ const GENERATOR_PATTERN = /\b(?:kb_entity|kb_relationship|member|memberchk|findall|setof|bagof)\s*\(/;
2
+ const NEGATION_PATTERN = /\\\+\s*/;
3
+ export function analyzePrologQueryPlanSafety(source) {
4
+ const lines = source.split(/\r?\n/);
5
+ const violations = [];
6
+ let clauseStart = 0;
7
+ let predicate = "unknown";
8
+ let clauseLines = [];
9
+ for (let index = 0; index < lines.length; index += 1) {
10
+ const line = lines[index] ?? "";
11
+ if (clauseLines.length === 0 && line.trim().length === 0) {
12
+ continue;
13
+ }
14
+ if (clauseLines.length === 0) {
15
+ clauseStart = index + 1;
16
+ predicate = predicateNameFrom(line) ?? "unknown";
17
+ }
18
+ clauseLines.push(line);
19
+ if (line.trim().endsWith(".")) {
20
+ const violation = analyzeClause(predicate, clauseStart, clauseLines);
21
+ if (violation)
22
+ violations.push(violation);
23
+ clauseLines = [];
24
+ }
25
+ }
26
+ return violations;
27
+ }
28
+ function predicateNameFrom(line) {
29
+ const match = line.match(/^\s*([a-z][a-zA-Z0-9_]*)\s*\(/);
30
+ return match?.[1] ?? null;
31
+ }
32
+ function analyzeClause(predicate, startLine, lines) {
33
+ const negationIndex = lines.findIndex((line) => NEGATION_PATTERN.test(line));
34
+ if (negationIndex < 0)
35
+ return null;
36
+ const hasLaterGenerator = lines
37
+ .slice(negationIndex + 1)
38
+ .some((line) => GENERATOR_PATTERN.test(line));
39
+ if (!hasLaterGenerator)
40
+ return null;
41
+ return {
42
+ predicate,
43
+ line: startLine + negationIndex,
44
+ description: "Negation appears before later generator calls in the same clause.",
45
+ suggestion: "Move kb_entity/kb_relationship/member/findall generators before \\+/1 so variables are bound before negation.",
46
+ };
47
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EA4EjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CAsBb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
1
+ {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EA0FjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CAsBb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
@@ -87,6 +87,18 @@ export const RULES = [
87
87
  defaultEnabled: false,
88
88
  category: "integrity",
89
89
  },
90
+ {
91
+ name: "predicate-verifiability",
92
+ description: "Detect requires_predicate links that do not target ground fact_kind: predicate facts",
93
+ defaultEnabled: false,
94
+ category: "integrity",
95
+ },
96
+ {
97
+ name: "query-plan-safety",
98
+ description: "Detect Prolog validation clauses that place negation before generator calls",
99
+ defaultEnabled: true,
100
+ category: "integrity",
101
+ },
90
102
  ];
91
103
  /**
92
104
  * Set of all rule names for quick lookups.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.12.7",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -11,13 +11,7 @@
11
11
  "bin": {
12
12
  "kibi": "bin/kibi"
13
13
  },
14
- "files": [
15
- "dist",
16
- "bin",
17
- "schema",
18
- "src/schemas",
19
- "src/public"
20
- ],
14
+ "files": ["dist", "bin", "schema", "src/schemas", "src/public"],
21
15
  "scripts": {
22
16
  "build": "tsc -p tsconfig.json && mkdir -p dist/public/skills && cp -r src/public/skills/. dist/public/skills/",
23
17
  "prepack": "npm run build"
@@ -75,6 +69,10 @@
75
69
  "types": "./dist/public/check-types.d.ts",
76
70
  "default": "./dist/public/check-types.js"
77
71
  },
72
+ "./public/impact-diagnostics": {
73
+ "types": "./dist/public/impact-diagnostics.d.ts",
74
+ "default": "./dist/public/impact-diagnostics.js"
75
+ },
78
76
  "./public/symbol-granularity": {
79
77
  "types": "./dist/public/symbol-granularity.d.ts",
80
78
  "default": "./dist/public/symbol-granularity.js"
@@ -105,7 +103,7 @@
105
103
  "fast-glob": "^3.2.12",
106
104
  "gray-matter": "^4.0.3",
107
105
  "js-yaml": "^4.1.0",
108
- "kibi-core": "^0.6.2",
106
+ "kibi-core": "^0.7.0",
109
107
  "ts-morph": "^23.0.0",
110
108
  "ignore": "^5.3.0"
111
109
  },
@@ -48,6 +48,10 @@ entity_property(fact, valid_from, datetime).
48
48
  entity_property(fact, valid_to, datetime).
49
49
  entity_property(fact, canonical_key, string).
50
50
 
51
+ % Typed symbol metadata fields - only valid for symbol entities
52
+ entity_property(symbol, symbol_role, atom).
53
+ entity_property(symbol, granularity_reason, atom).
54
+
51
55
  % Required properties for all entity types
52
56
  required_property(Type, id) :- entity_type(Type).
53
57
  required_property(Type, title) :- entity_type(Type).
@@ -0,0 +1,90 @@
1
+ import * as path from "node:path";
2
+ import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
3
+ import { extractSymbolsFromStagedFile } from "../../traceability/symbol-extract.js";
4
+ import {
5
+ createSemanticReviewDiagnostics,
6
+ createSymbolGranularityDiagnostics,
7
+ } from "./diagnostics.js";
8
+ import {
9
+ createImpactManifestLookup,
10
+ readImpactManifestResults,
11
+ } from "./manifest.js";
12
+ import { collectSourceChanges, uniqueSorted } from "./source-changes.js";
13
+ import {
14
+ buildNextActions,
15
+ collectLinkedEntities,
16
+ formatExtractedSymbols,
17
+ } from "./summaries.js";
18
+ import { createSymbolQualityDiagnostics } from "./symbol-quality.js";
19
+ import type {
20
+ ChangedFileImpactOptions,
21
+ ChangedFileImpactResult,
22
+ } from "./types.js";
23
+
24
+ export function analyzeChangedFileImpact(
25
+ options: ChangedFileImpactOptions,
26
+ ): ChangedFileImpactResult {
27
+ const workspaceRoot = path.resolve(options.workspaceRoot);
28
+ const sourceChanges = collectSourceChanges({ ...options, workspaceRoot });
29
+ const sourceFiles = uniqueSorted(sourceChanges.map((change) => change.file));
30
+ const manifestResults = readImpactManifestResults(workspaceRoot);
31
+ const manifestLookup = createImpactManifestLookup(manifestResults);
32
+ const symbolsByFile = new Map<string, ExtractedSymbol[]>();
33
+ const sourceContentByFile = new Map<string, string>();
34
+
35
+ for (const change of sourceChanges) {
36
+ sourceContentByFile.set(change.file, change.content);
37
+ symbolsByFile.set(
38
+ change.file,
39
+ extractSymbolsFromStagedFile(
40
+ {
41
+ path: change.file,
42
+ status: change.status,
43
+ hunkRanges: [...change.hunkRanges],
44
+ content: change.content,
45
+ },
46
+ manifestLookup,
47
+ ).filter((symbol) => symbol.hunkRanges.length > 0),
48
+ );
49
+ }
50
+
51
+ const activeSourceFiles = new Set(sourceFiles);
52
+ const activeManifestResults = manifestResults.filter(
53
+ (result) =>
54
+ result.sourceFile !== undefined &&
55
+ activeSourceFiles.has(result.sourceFile),
56
+ );
57
+ const impactDiagnostics =
58
+ options.includeImpactDiagnostics === false
59
+ ? []
60
+ : [
61
+ ...createSymbolGranularityDiagnostics({
62
+ manifestResults: activeManifestResults,
63
+ symbolsByFile,
64
+ sourceContentByFile,
65
+ workspaceRoot,
66
+ }),
67
+ ...createSymbolQualityDiagnostics({
68
+ manifestResults: activeManifestResults,
69
+ symbolsByFile,
70
+ }),
71
+ ...createSemanticReviewDiagnostics({ symbolsByFile }),
72
+ ];
73
+ const maxDiagnostics = options.maxDiagnostics;
74
+ const cappedDiagnostics =
75
+ maxDiagnostics !== undefined && maxDiagnostics >= 0
76
+ ? impactDiagnostics.slice(0, maxDiagnostics)
77
+ : impactDiagnostics;
78
+
79
+ return {
80
+ impactDiagnostics: cappedDiagnostics,
81
+ sourceFiles,
82
+ extractedSymbols: formatExtractedSymbols(symbolsByFile),
83
+ linkedEntities: collectLinkedEntities(
84
+ symbolsByFile,
85
+ manifestResults,
86
+ activeSourceFiles,
87
+ ),
88
+ nextActions: buildNextActions(sourceFiles),
89
+ };
90
+ }
@@ -0,0 +1,268 @@
1
+ import type {
2
+ ExtractedEntity,
3
+ ExtractionResult,
4
+ } from "../../extractors/markdown.js";
5
+ import type { QualityDiagnostic } from "./types.js";
6
+
7
+ type ReviewableCoverageDepth =
8
+ | "unit_only"
9
+ | "open_or_nonpassing_tests_only"
10
+ | "scenario_only_no_test"
11
+ | "no_test_evidence";
12
+
13
+ type CoverageDepth =
14
+ | ReviewableCoverageDepth
15
+ | "direct_passing_integration"
16
+ | "scenario_passing_integration"
17
+ | "direct_passing_e2e"
18
+ | "scenario_passing_e2e";
19
+
20
+ type RequirementCoverage = {
21
+ readonly requirement: ExtractedEntity;
22
+ readonly scenarioCount: number;
23
+ readonly directTests: readonly ExtractedEntity[];
24
+ readonly scenarioTests: readonly ExtractedEntity[];
25
+ };
26
+
27
+ type RelationshipLookup = {
28
+ readonly result: ExtractionResult;
29
+ readonly relationshipType: string;
30
+ readonly type: string;
31
+ readonly entities: ReadonlyMap<string, ExtractedEntity>;
32
+ };
33
+
34
+ type IncomingLookup = {
35
+ readonly manifestResults: readonly ExtractionResult[];
36
+ readonly targetId: string;
37
+ readonly relationshipType: string;
38
+ readonly type: string;
39
+ };
40
+
41
+ function entitiesById(
42
+ manifestResults: readonly ExtractionResult[],
43
+ ): ReadonlyMap<string, ExtractedEntity> {
44
+ return new Map(
45
+ manifestResults.map((result) => [result.entity.id, result.entity]),
46
+ );
47
+ }
48
+
49
+ function outgoingTargets(
50
+ lookup: RelationshipLookup,
51
+ ): readonly ExtractedEntity[] {
52
+ return lookup.result.relationships.flatMap((relationship) => {
53
+ if (relationship.type !== lookup.relationshipType) return [];
54
+ const target = lookup.entities.get(relationship.to);
55
+ return target?.type === lookup.type ? [target] : [];
56
+ });
57
+ }
58
+
59
+ function incomingSources(lookup: IncomingLookup): readonly ExtractedEntity[] {
60
+ return lookup.manifestResults.flatMap((result) => {
61
+ if (result.entity.type !== lookup.type) return [];
62
+ return result.relationships.some(
63
+ (relationship) =>
64
+ relationship.type === lookup.relationshipType &&
65
+ relationship.to === lookup.targetId,
66
+ )
67
+ ? [result.entity]
68
+ : [];
69
+ });
70
+ }
71
+
72
+ function uniqueEntities(
73
+ entities: readonly ExtractedEntity[],
74
+ ): readonly ExtractedEntity[] {
75
+ return [
76
+ ...new Map(entities.map((entity) => [entity.id, entity])).values(),
77
+ ].toSorted((left, right) => left.id.localeCompare(right.id));
78
+ }
79
+
80
+ function testScope(test: ExtractedEntity): string {
81
+ if (test.verification_scope !== undefined) return test.verification_scope;
82
+ if (test.tags?.some((tag) => tag.toLowerCase() === "e2e") === true) {
83
+ return "end_to_end";
84
+ }
85
+ return test.source.toLowerCase().includes("e2e") ? "end_to_end" : "unknown";
86
+ }
87
+
88
+ function passingTests(
89
+ tests: readonly ExtractedEntity[],
90
+ ): readonly ExtractedEntity[] {
91
+ return tests.filter((test) => test.status === "passing");
92
+ }
93
+
94
+ function classifyCoverageDepth(coverage: RequirementCoverage): CoverageDepth {
95
+ const allTests = uniqueEntities([
96
+ ...coverage.directTests,
97
+ ...coverage.scenarioTests,
98
+ ]);
99
+ const directPassingTests = passingTests(coverage.directTests);
100
+ if (directPassingTests.some((test) => testScope(test) === "end_to_end")) {
101
+ return "direct_passing_e2e";
102
+ }
103
+ if (directPassingTests.some((test) => testScope(test) === "integration")) {
104
+ return "direct_passing_integration";
105
+ }
106
+
107
+ const scenarioPassingTests = passingTests(coverage.scenarioTests);
108
+ if (scenarioPassingTests.some((test) => testScope(test) === "end_to_end")) {
109
+ return "scenario_passing_e2e";
110
+ }
111
+ if (scenarioPassingTests.some((test) => testScope(test) === "integration")) {
112
+ return "scenario_passing_integration";
113
+ }
114
+
115
+ const allPassingTests = passingTests(allTests);
116
+ if (
117
+ allPassingTests.length > 0 &&
118
+ allPassingTests.every((test) => testScope(test) === "unit")
119
+ ) {
120
+ return "unit_only";
121
+ }
122
+
123
+ if (allTests.length > 0) return "open_or_nonpassing_tests_only";
124
+ if (coverage.scenarioCount > 0) return "scenario_only_no_test";
125
+ return "no_test_evidence";
126
+ }
127
+
128
+ function isReviewableCoverageDepth(
129
+ coverageDepth: CoverageDepth,
130
+ ): coverageDepth is ReviewableCoverageDepth {
131
+ return (
132
+ coverageDepth === "unit_only" ||
133
+ coverageDepth === "open_or_nonpassing_tests_only" ||
134
+ coverageDepth === "scenario_only_no_test" ||
135
+ coverageDepth === "no_test_evidence"
136
+ );
137
+ }
138
+
139
+ function uniqueStrings(values: readonly string[]): readonly string[] {
140
+ return [...new Set(values)].toSorted((left, right) =>
141
+ left.localeCompare(right),
142
+ );
143
+ }
144
+
145
+ function coverageDepthSuggestion(
146
+ coverageDepth: ReviewableCoverageDepth,
147
+ ): string {
148
+ if (coverageDepth === "unit_only") {
149
+ return "Keep unit coverage where appropriate; add or link a passing end-to-end or scenario-backed test when this behavior warrants e2e confidence.";
150
+ }
151
+ return "Add or link passing tests, preferably end-to-end or scenario-backed when this behavior warrants e2e confidence.";
152
+ }
153
+
154
+ function createDiagnostic(
155
+ coverage: RequirementCoverage,
156
+ ): QualityDiagnostic | undefined {
157
+ const coverageDepth = classifyCoverageDepth(coverage);
158
+ if (!isReviewableCoverageDepth(coverageDepth)) return undefined;
159
+
160
+ const allTests = uniqueEntities([
161
+ ...coverage.directTests,
162
+ ...coverage.scenarioTests,
163
+ ]);
164
+ return {
165
+ id: "coverage_depth_review",
166
+ severity: "review",
167
+ blocking: false,
168
+ category: "coverage",
169
+ entityId: coverage.requirement.id,
170
+ source: coverage.requirement.source,
171
+ files: [coverage.requirement.source],
172
+ message: `Requirement ${coverage.requirement.id} (${coverage.requirement.title}) has ${coverageDepth} coverage depth and may need stronger behavior-level evidence.`,
173
+ suggestion: coverageDepthSuggestion(coverageDepth),
174
+ evidence: {
175
+ requirementId: coverage.requirement.id,
176
+ title: coverage.requirement.title,
177
+ source: coverage.requirement.source,
178
+ coverageDepth,
179
+ directTests: coverage.directTests.map((test) => test.id),
180
+ scenarioTests: coverage.scenarioTests.map((test) => test.id),
181
+ testStatuses: uniqueStrings(allTests.map((test) => test.status)),
182
+ verificationScopes: uniqueStrings(allTests.map(testScope)),
183
+ },
184
+ };
185
+ }
186
+
187
+ function coverageForRequirement(
188
+ result: ExtractionResult,
189
+ manifestResults: readonly ExtractionResult[],
190
+ entities: ReadonlyMap<string, ExtractedEntity>,
191
+ ): RequirementCoverage {
192
+ const resultsById = new Map(
193
+ manifestResults.map((manifestResult) => [
194
+ manifestResult.entity.id,
195
+ manifestResult,
196
+ ]),
197
+ );
198
+ const scenarios = outgoingTargets({
199
+ result,
200
+ relationshipType: "specified_by",
201
+ type: "scenario",
202
+ entities,
203
+ });
204
+ const directTests = uniqueEntities([
205
+ ...outgoingTargets({
206
+ result,
207
+ relationshipType: "verified_by",
208
+ type: "test",
209
+ entities,
210
+ }),
211
+ ...outgoingTargets({
212
+ result,
213
+ relationshipType: "covered_by",
214
+ type: "test",
215
+ entities,
216
+ }),
217
+ ...incomingSources({
218
+ manifestResults,
219
+ targetId: result.entity.id,
220
+ relationshipType: "validates",
221
+ type: "test",
222
+ }),
223
+ ]);
224
+ const scenarioTests = uniqueEntities(
225
+ scenarios.flatMap((scenario) => {
226
+ const scenarioResult = resultsById.get(scenario.id);
227
+ return [
228
+ ...incomingSources({
229
+ manifestResults,
230
+ targetId: scenario.id,
231
+ relationshipType: "validates",
232
+ type: "test",
233
+ }),
234
+ ...(scenarioResult === undefined
235
+ ? []
236
+ : outgoingTargets({
237
+ result: scenarioResult,
238
+ relationshipType: "verified_by",
239
+ type: "test",
240
+ entities,
241
+ })),
242
+ ];
243
+ }),
244
+ );
245
+ return {
246
+ requirement: result.entity,
247
+ scenarioCount: scenarios.length,
248
+ directTests,
249
+ scenarioTests,
250
+ };
251
+ }
252
+
253
+ export function createCoverageDepthQualityDiagnostics(
254
+ manifestResults: readonly ExtractionResult[],
255
+ ): readonly QualityDiagnostic[] {
256
+ const entities = entitiesById(manifestResults);
257
+ return manifestResults
258
+ .filter((result) => result.entity.type === "req")
259
+ .flatMap((result) => {
260
+ const diagnostic = createDiagnostic(
261
+ coverageForRequirement(result, manifestResults, entities),
262
+ );
263
+ return diagnostic === undefined ? [] : [diagnostic];
264
+ })
265
+ .toSorted((left, right) =>
266
+ (left.entityId ?? "").localeCompare(right.entityId ?? ""),
267
+ );
268
+ }