kibi-cli 2.4.0 → 2.5.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 (352) hide show
  1. package/dist/cli-json-command.d.ts +6 -0
  2. package/dist/cli-json-command.d.ts.map +1 -1
  3. package/dist/cli-json-command.js +37 -9
  4. package/dist/cli-operation-loader.d.ts.map +1 -1
  5. package/dist/cli-operation-loader.js +7 -0
  6. package/dist/cli-operation-metadata.d.ts +12 -4
  7. package/dist/cli-operation-metadata.d.ts.map +1 -1
  8. package/dist/cli-operation-metadata.js +14 -4
  9. package/dist/cli-protocol.d.ts +14 -0
  10. package/dist/cli-protocol.d.ts.map +1 -1
  11. package/dist/cli-protocol.js +31 -2
  12. package/dist/cli-register-proof.d.ts.map +1 -1
  13. package/dist/cli-register-proof.js +13 -0
  14. package/dist/commands/check.d.ts.map +1 -1
  15. package/dist/commands/check.js +11 -2
  16. package/dist/commands/discovery-shared.d.ts.map +1 -1
  17. package/dist/commands/discovery-shared.js +8 -2
  18. package/dist/commands/engine.d.ts.map +1 -1
  19. package/dist/commands/engine.js +4 -2
  20. package/dist/commands/gc.d.ts.map +1 -1
  21. package/dist/commands/gc.js +7 -0
  22. package/dist/commands/init-helpers.d.ts.map +1 -1
  23. package/dist/commands/init-helpers.js +0 -1
  24. package/dist/commands/migrate.d.ts.map +1 -1
  25. package/dist/commands/migrate.js +281 -14
  26. package/dist/commands/proof-compact.d.ts +9 -0
  27. package/dist/commands/proof-compact.d.ts.map +1 -0
  28. package/dist/commands/proof-compact.js +58 -0
  29. package/dist/commands/prove.d.ts +38 -0
  30. package/dist/commands/prove.d.ts.map +1 -1
  31. package/dist/commands/prove.js +222 -47
  32. package/dist/commands/sync/persistence.d.ts.map +1 -1
  33. package/dist/commands/sync/persistence.js +61 -16
  34. package/dist/commands/sync.d.ts +8 -0
  35. package/dist/commands/sync.d.ts.map +1 -1
  36. package/dist/commands/sync.js +42 -16
  37. package/dist/diagnostics.js +1 -1
  38. package/dist/engine-limits.d.ts +74 -0
  39. package/dist/engine-limits.d.ts.map +1 -0
  40. package/dist/engine-limits.js +201 -0
  41. package/dist/engine.d.ts +10 -0
  42. package/dist/engine.d.ts.map +1 -1
  43. package/dist/engine.js +57 -9
  44. package/dist/extractors/manifest.d.ts +8 -0
  45. package/dist/extractors/manifest.d.ts.map +1 -1
  46. package/dist/extractors/manifest.js +34 -5
  47. package/dist/extractors/markdown.d.ts +8 -0
  48. package/dist/extractors/markdown.d.ts.map +1 -1
  49. package/dist/extractors/markdown.js +57 -0
  50. package/dist/extractors/relationships.d.ts.map +1 -1
  51. package/dist/extractors/relationships.js +2 -0
  52. package/dist/intent-search.d.ts +4 -1
  53. package/dist/intent-search.d.ts.map +1 -1
  54. package/dist/intent-search.js +186 -33
  55. package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
  56. package/dist/operations/bootstrap/candidate-helpers.js +8 -1
  57. package/dist/operations/bootstrap/candidates.d.ts.map +1 -1
  58. package/dist/operations/bootstrap/candidates.js +54 -38
  59. package/dist/operations/bootstrap/generate.d.ts +2 -0
  60. package/dist/operations/bootstrap/generate.d.ts.map +1 -1
  61. package/dist/operations/bootstrap/generate.js +87 -33
  62. package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
  63. package/dist/operations/bootstrap/intent-claims.js +12 -4
  64. package/dist/operations/bootstrap/markdown-candidates.d.ts +2 -0
  65. package/dist/operations/bootstrap/markdown-candidates.d.ts.map +1 -1
  66. package/dist/operations/bootstrap/markdown-candidates.js +32 -10
  67. package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
  68. package/dist/operations/bootstrap/presentation.js +6 -2
  69. package/dist/operations/bootstrap/requirement-claims.d.ts +1 -0
  70. package/dist/operations/bootstrap/requirement-claims.d.ts.map +1 -1
  71. package/dist/operations/bootstrap/requirement-claims.js +13 -1
  72. package/dist/operations/bootstrap/validation.d.ts +4 -0
  73. package/dist/operations/bootstrap/validation.d.ts.map +1 -0
  74. package/dist/operations/bootstrap/validation.js +20 -0
  75. package/dist/operations/migration/inventory-rederive.d.ts +81 -0
  76. package/dist/operations/migration/inventory-rederive.d.ts.map +1 -0
  77. package/dist/operations/migration/inventory-rederive.js +484 -0
  78. package/dist/operations/migration/kb-sources.d.ts +53 -0
  79. package/dist/operations/migration/kb-sources.d.ts.map +1 -0
  80. package/dist/operations/migration/kb-sources.js +233 -0
  81. package/dist/operations/migration/origin-backfill.d.ts +39 -0
  82. package/dist/operations/migration/origin-backfill.d.ts.map +1 -0
  83. package/dist/operations/migration/origin-backfill.js +164 -0
  84. package/dist/operations/migration/polarity-values.d.ts +10 -0
  85. package/dist/operations/migration/polarity-values.d.ts.map +1 -0
  86. package/dist/operations/migration/polarity-values.js +60 -0
  87. package/dist/operations/migration/schema6.d.ts +59 -0
  88. package/dist/operations/migration/schema6.d.ts.map +1 -0
  89. package/dist/operations/migration/schema6.js +379 -0
  90. package/dist/operations/migration/source-paths.d.ts +131 -0
  91. package/dist/operations/migration/source-paths.d.ts.map +1 -0
  92. package/dist/operations/migration/source-paths.js +308 -0
  93. package/dist/operations/migration/superseded-closure.d.ts +54 -0
  94. package/dist/operations/migration/superseded-closure.d.ts.map +1 -0
  95. package/dist/operations/migration/superseded-closure.js +187 -0
  96. package/dist/operations/modeling/logic-modeling.d.ts.map +1 -1
  97. package/dist/operations/modeling/logic-modeling.js +3 -5
  98. package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
  99. package/dist/operations/modeling/model-requirement.js +225 -67
  100. package/dist/operations/mutation/contradictions.d.ts +16 -0
  101. package/dist/operations/mutation/contradictions.d.ts.map +1 -1
  102. package/dist/operations/mutation/contradictions.js +32 -0
  103. package/dist/operations/mutation/delete.d.ts.map +1 -1
  104. package/dist/operations/mutation/delete.js +15 -7
  105. package/dist/operations/mutation/origin.d.ts +21 -0
  106. package/dist/operations/mutation/origin.d.ts.map +1 -0
  107. package/dist/operations/mutation/origin.js +78 -0
  108. package/dist/operations/mutation/plan-apply-journal.d.ts +182 -0
  109. package/dist/operations/mutation/plan-apply-journal.d.ts.map +1 -0
  110. package/dist/operations/mutation/plan-apply-journal.js +613 -0
  111. package/dist/operations/mutation/predicate-vocabulary-guard.d.ts +3 -1
  112. package/dist/operations/mutation/predicate-vocabulary-guard.d.ts.map +1 -1
  113. package/dist/operations/mutation/predicate-vocabulary-guard.js +13 -2
  114. package/dist/operations/mutation/relationships.d.ts +5 -5
  115. package/dist/operations/mutation/relationships.d.ts.map +1 -1
  116. package/dist/operations/mutation/relationships.js +31 -11
  117. package/dist/operations/mutation/serialization.d.ts.map +1 -1
  118. package/dist/operations/mutation/serialization.js +3 -1
  119. package/dist/operations/mutation/source-authoring.d.ts +6 -0
  120. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  121. package/dist/operations/mutation/source-authoring.js +7 -1
  122. package/dist/operations/mutation/types.d.ts +14 -0
  123. package/dist/operations/mutation/types.d.ts.map +1 -1
  124. package/dist/operations/mutation/upsert.d.ts +30 -3
  125. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  126. package/dist/operations/mutation/upsert.js +105 -35
  127. package/dist/operations/mutation/validate-upsert.d.ts +6 -0
  128. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  129. package/dist/operations/mutation/validate-upsert.js +34 -30
  130. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  131. package/dist/operations/planning/apply-plan.js +751 -144
  132. package/dist/operations/planning/compile-intent.d.ts +34 -1
  133. package/dist/operations/planning/compile-intent.d.ts.map +1 -1
  134. package/dist/operations/planning/compile-intent.js +314 -98
  135. package/dist/operations/proof/code-scope.d.ts +28 -0
  136. package/dist/operations/proof/code-scope.d.ts.map +1 -1
  137. package/dist/operations/proof/code-scope.js +72 -0
  138. package/dist/operations/proof/compact-receipts.d.ts +49 -0
  139. package/dist/operations/proof/compact-receipts.d.ts.map +1 -0
  140. package/dist/operations/proof/compact-receipts.js +160 -0
  141. package/dist/operations/proof/ingest-proof.d.ts +2 -0
  142. package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
  143. package/dist/operations/proof/ingest-proof.js +44 -23
  144. package/dist/operations/proof/prune-receipts.d.ts.map +1 -1
  145. package/dist/operations/proof/prune-receipts.js +5 -9
  146. package/dist/operations/proof/receipt-compaction.d.ts +48 -0
  147. package/dist/operations/proof/receipt-compaction.d.ts.map +1 -0
  148. package/dist/operations/proof/receipt-compaction.js +121 -0
  149. package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
  150. package/dist/operations/semantic-advisor/analysis-receipt.js +12 -1
  151. package/dist/operations/semantic-advisor/analyze-prose.d.ts +2 -1
  152. package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
  153. package/dist/operations/semantic-advisor/analyze-prose.js +51 -5
  154. package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
  155. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  156. package/dist/operations/semantic-advisor/clauses.js +11 -1
  157. package/dist/operations/semantic-advisor/conditional-rules.d.ts +33 -0
  158. package/dist/operations/semantic-advisor/conditional-rules.d.ts.map +1 -0
  159. package/dist/operations/semantic-advisor/conditional-rules.js +262 -0
  160. package/dist/operations/semantic-advisor/ingestion-boundary.d.ts +3 -1
  161. package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
  162. package/dist/operations/semantic-advisor/ingestion-boundary.js +14 -6
  163. package/dist/operations/semantic-advisor/plugin-orchestration.js +1 -1
  164. package/dist/prolog/codec.d.ts.map +1 -1
  165. package/dist/prolog/codec.js +2 -1
  166. package/dist/prolog.d.ts +9 -0
  167. package/dist/prolog.d.ts.map +1 -1
  168. package/dist/prolog.js +56 -10
  169. package/dist/proof/command-test-report.d.ts +74 -0
  170. package/dist/proof/command-test-report.d.ts.map +1 -0
  171. package/dist/proof/command-test-report.js +206 -0
  172. package/dist/public/branch-resolver.d.ts +1 -0
  173. package/dist/public/branch-resolver.d.ts.map +1 -1
  174. package/dist/public/branch-resolver.js +1 -0
  175. package/dist/public/entity-origin.d.ts +69 -0
  176. package/dist/public/entity-origin.d.ts.map +1 -0
  177. package/dist/public/entity-origin.js +142 -0
  178. package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
  179. package/dist/public/impact/full-kb-quality.js +1 -0
  180. package/dist/public/impact/requirement-quality.js +1 -1
  181. package/dist/public/operations/catalog.d.ts +130 -19
  182. package/dist/public/operations/catalog.d.ts.map +1 -1
  183. package/dist/public/operations/catalog.js +3 -0
  184. package/dist/public/operations/check-executor.d.ts.map +1 -1
  185. package/dist/public/operations/check-executor.js +8 -0
  186. package/dist/public/operations/contracts.d.ts +0 -5
  187. package/dist/public/operations/contracts.d.ts.map +1 -1
  188. package/dist/public/operations/contracts.js +63 -14
  189. package/dist/public/operations/discovery-entities.d.ts +15 -0
  190. package/dist/public/operations/discovery-entities.d.ts.map +1 -1
  191. package/dist/public/operations/discovery-entities.js +59 -0
  192. package/dist/public/operations/discovery-executors.d.ts +6 -1
  193. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  194. package/dist/public/operations/discovery-executors.js +65 -6
  195. package/dist/public/operations/index.d.ts +1 -0
  196. package/dist/public/operations/index.d.ts.map +1 -1
  197. package/dist/public/operations/index.js +1 -0
  198. package/dist/public/operations/migration-plan.d.ts +5 -0
  199. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  200. package/dist/public/operations/migration-plan.js +26 -1
  201. package/dist/public/operations/node-ports.d.ts.map +1 -1
  202. package/dist/public/operations/node-ports.js +24 -3
  203. package/dist/public/operations/origin-review.d.ts +51 -0
  204. package/dist/public/operations/origin-review.d.ts.map +1 -0
  205. package/dist/public/operations/origin-review.js +307 -0
  206. package/dist/public/operations/predicate-schema-conformance.js +1 -1
  207. package/dist/public/operations/repair-plan.d.ts.map +1 -1
  208. package/dist/public/operations/repair-plan.js +4 -0
  209. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  210. package/dist/public/operations/result-envelope.js +17 -5
  211. package/dist/public/operations/runtime-types.d.ts +9 -0
  212. package/dist/public/operations/runtime-types.d.ts.map +1 -1
  213. package/dist/public/operations/schema6-check-actions.d.ts +127 -0
  214. package/dist/public/operations/schema6-check-actions.d.ts.map +1 -0
  215. package/dist/public/operations/schema6-check-actions.js +575 -0
  216. package/dist/public/operations/source-path-dangling.d.ts +17 -0
  217. package/dist/public/operations/source-path-dangling.d.ts.map +1 -0
  218. package/dist/public/operations/source-path-dangling.js +66 -0
  219. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  220. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  221. package/dist/public/operations/specs/check-rules.generated.js +15 -1
  222. package/dist/public/operations/specs/check.d.ts +2 -2
  223. package/dist/public/operations/specs/check.js +1 -1
  224. package/dist/public/operations/specs/composite.d.ts +351 -0
  225. package/dist/public/operations/specs/composite.d.ts.map +1 -0
  226. package/dist/public/operations/specs/composite.js +96 -0
  227. package/dist/public/operations/specs/discovery.d.ts +8 -3
  228. package/dist/public/operations/specs/discovery.d.ts.map +1 -1
  229. package/dist/public/operations/specs/discovery.js +8 -3
  230. package/dist/public/operations/specs/modeling.d.ts +2 -2
  231. package/dist/public/operations/specs/modeling.js +2 -2
  232. package/dist/public/operations/specs/mutation-schemas.d.ts +73 -8
  233. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  234. package/dist/public/operations/specs/mutation-schemas.js +43 -7
  235. package/dist/public/operations/specs/mutation.d.ts +1 -1
  236. package/dist/public/operations/specs/mutation.js +1 -1
  237. package/dist/public/operations/specs/planning.d.ts +2 -2
  238. package/dist/public/operations/specs/planning.js +4 -4
  239. package/dist/public/operations/specs/reporting.d.ts.map +1 -1
  240. package/dist/public/operations/specs/reporting.js +13 -26
  241. package/dist/public/operations/specs/upsert.d.ts +79 -9
  242. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  243. package/dist/public/operations/specs/upsert.js +6 -1
  244. package/dist/public/operations/types.d.ts +1 -1
  245. package/dist/public/operations/types.d.ts.map +1 -1
  246. package/dist/public/proof-receipt.d.ts +1 -0
  247. package/dist/public/proof-receipt.d.ts.map +1 -1
  248. package/dist/public/schemas/entity.d.ts.map +1 -1
  249. package/dist/public/schemas/entity.js +52 -0
  250. package/dist/public/schemas/relationship.d.ts.map +1 -1
  251. package/dist/public/schemas/relationship.js +2 -0
  252. package/dist/public/skills/kibi-bootstrap/SKILL.md +6 -1
  253. package/dist/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -0
  254. package/dist/public/skills/kibi-usage/SKILL.md +68 -18
  255. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  256. package/dist/public/skills/kibi-usage/resources/kb-improvement.md +1 -1
  257. package/dist/public/skills/kibi-usage/resources/logic-ir.md +2 -2
  258. package/dist/public/skills/kibi-usage/resources/operation-access.md +25 -1
  259. package/dist/public/skills/kibi-usage/resources/ui-requirements.md +4 -4
  260. package/dist/public/skills/kibi-usage/resources/workflows.md +42 -25
  261. package/dist/public/telemetry-acceptance.d.ts +58 -2
  262. package/dist/public/telemetry-acceptance.d.ts.map +1 -1
  263. package/dist/public/telemetry-acceptance.js +136 -10
  264. package/dist/public/telemetry-remediation.d.ts.map +1 -1
  265. package/dist/public/telemetry-remediation.js +26 -10
  266. package/dist/relationships/shards.d.ts +13 -0
  267. package/dist/relationships/shards.d.ts.map +1 -1
  268. package/dist/relationships/shards.js +54 -28
  269. package/dist/runtime/cli-runtime.d.ts.map +1 -1
  270. package/dist/runtime/cli-runtime.js +44 -3
  271. package/dist/runtime/detached-snapshot.d.ts +38 -0
  272. package/dist/runtime/detached-snapshot.d.ts.map +1 -0
  273. package/dist/runtime/detached-snapshot.js +92 -0
  274. package/dist/schemas/entity.schema.json +122 -0
  275. package/dist/search-answer.d.ts +157 -0
  276. package/dist/search-answer.d.ts.map +1 -0
  277. package/dist/search-answer.js +846 -0
  278. package/dist/traceability/impact-knowledge.d.ts.map +1 -1
  279. package/dist/traceability/impact-knowledge.js +7 -0
  280. package/dist/traceability/symbol-extract.js +3 -3
  281. package/dist/traceability/temp-kb.d.ts.map +1 -1
  282. package/dist/traceability/temp-kb.js +12 -0
  283. package/dist/types/entities.d.ts +13 -0
  284. package/dist/types/entities.d.ts.map +1 -1
  285. package/dist/types/relationships.d.ts +1 -1
  286. package/dist/types/relationships.d.ts.map +1 -1
  287. package/dist/utils/branch-resolver.d.ts +37 -1
  288. package/dist/utils/branch-resolver.d.ts.map +1 -1
  289. package/dist/utils/branch-resolver.js +130 -3
  290. package/dist/utils/repo-relative-path.d.ts +18 -0
  291. package/dist/utils/repo-relative-path.d.ts.map +1 -0
  292. package/dist/utils/repo-relative-path.js +58 -0
  293. package/dist/utils/rule-registry.generated.d.ts +75 -3
  294. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  295. package/dist/utils/rule-registry.generated.js +89 -3
  296. package/dist/utils/schema-version.d.ts +1 -1
  297. package/dist/utils/schema-version.d.ts.map +1 -1
  298. package/dist/utils/schema-version.js +4 -3
  299. package/dist/utils/strict-modeling.d.ts +1 -1
  300. package/dist/utils/strict-modeling.d.ts.map +1 -1
  301. package/dist/utils/strict-modeling.js +23 -3
  302. package/package.json +3 -3
  303. package/schema/entities.pl +22 -0
  304. package/schema/relationships.pl +14 -0
  305. package/schema/validation.pl +45 -1
  306. package/src/public/branch-resolver.ts +1 -0
  307. package/src/public/entity-origin.ts +175 -0
  308. package/src/public/impact/full-kb-quality.ts +1 -0
  309. package/src/public/impact/requirement-quality.ts +1 -1
  310. package/src/public/operations/catalog.ts +3 -0
  311. package/src/public/operations/check-executor.ts +15 -0
  312. package/src/public/operations/contracts.ts +75 -15
  313. package/src/public/operations/discovery-entities.ts +74 -0
  314. package/src/public/operations/discovery-executors.ts +88 -10
  315. package/src/public/operations/index.ts +1 -0
  316. package/src/public/operations/migration-plan.ts +30 -1
  317. package/src/public/operations/node-ports.ts +24 -3
  318. package/src/public/operations/origin-review.ts +375 -0
  319. package/src/public/operations/predicate-schema-conformance.ts +1 -1
  320. package/src/public/operations/repair-plan.ts +4 -0
  321. package/src/public/operations/result-envelope.ts +18 -6
  322. package/src/public/operations/runtime-types.ts +9 -0
  323. package/src/public/operations/schema6-check-actions.ts +699 -0
  324. package/src/public/operations/source-path-dangling.ts +81 -0
  325. package/src/public/operations/specs/check-rules.generated.ts +15 -1
  326. package/src/public/operations/specs/check.ts +1 -1
  327. package/src/public/operations/specs/composite.ts +141 -0
  328. package/src/public/operations/specs/discovery.ts +9 -3
  329. package/src/public/operations/specs/modeling.ts +2 -2
  330. package/src/public/operations/specs/mutation-schemas.ts +49 -7
  331. package/src/public/operations/specs/mutation.ts +1 -1
  332. package/src/public/operations/specs/planning.ts +4 -4
  333. package/src/public/operations/specs/reporting.ts +13 -38
  334. package/src/public/operations/specs/upsert.ts +7 -1
  335. package/src/public/operations/types.ts +2 -0
  336. package/src/public/proof-receipt.ts +1 -0
  337. package/src/public/schemas/entity.ts +57 -0
  338. package/src/public/schemas/relationship.ts +2 -0
  339. package/src/public/skills/kibi-bootstrap/SKILL.md +6 -1
  340. package/src/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -0
  341. package/src/public/skills/kibi-usage/SKILL.md +68 -18
  342. package/src/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  343. package/src/public/skills/kibi-usage/resources/kb-improvement.md +1 -1
  344. package/src/public/skills/kibi-usage/resources/logic-ir.md +2 -2
  345. package/src/public/skills/kibi-usage/resources/operation-access.md +25 -1
  346. package/src/public/skills/kibi-usage/resources/ui-requirements.md +4 -4
  347. package/src/public/skills/kibi-usage/resources/workflows.md +42 -25
  348. package/src/public/telemetry-acceptance-types.d.ts +4 -1
  349. package/src/public/telemetry-acceptance.ts +204 -10
  350. package/src/public/telemetry-remediation.ts +39 -10
  351. package/src/schemas/entity.schema.json +122 -0
  352. package/src/schemas/relationship.schema.json +2 -0
@@ -0,0 +1,846 @@
1
+ import { escapeAtom, normalizeEntityId, parseEntityFromList, parseListOfLists, parseTriples, } from "./prolog/codec.js";
2
+ import { entityRowsGoal } from "./public/operations/discovery-entities.js";
3
+ import { runOperationJsonQuery } from "./public/operations/prolog-json.js";
4
+ import { loadMarkdownBody } from "./search-ranking.js";
5
+ const CURRENT_REQ_STATUSES = new Set([
6
+ "open",
7
+ "in_progress",
8
+ "closed",
9
+ "active",
10
+ "approved",
11
+ ]);
12
+ const SUPERSEDED_ADR_STATUSES = new Set([
13
+ "superseded",
14
+ "deprecated",
15
+ "rejected",
16
+ ]);
17
+ const ANSWER_RELATIONSHIPS = [
18
+ "specified_by",
19
+ "verified_by",
20
+ "validates",
21
+ "implements",
22
+ "constrains",
23
+ "requires_property",
24
+ "requires_predicate",
25
+ "requires_rule",
26
+ "supersedes",
27
+ "relates_to",
28
+ "exempts",
29
+ ];
30
+ const FACT_LINKS = new Set([
31
+ "constrains",
32
+ "requires_property",
33
+ "requires_predicate",
34
+ "requires_rule",
35
+ ]);
36
+ /** Properties the answer reads from a linked entity. */
37
+ const ROW_KEYS = [
38
+ "id",
39
+ "title",
40
+ "status",
41
+ "fact_kind",
42
+ "approved_by",
43
+ "source",
44
+ "sourceFile",
45
+ ];
46
+ // implements REQ-kibi-search-answer-layer-v2
47
+ export const SEARCH_ANSWER_LIMITS = {
48
+ seeds: 10,
49
+ governing: 5,
50
+ perRequirement: 4,
51
+ rationale: 3,
52
+ notGoverning: 5,
53
+ observations: 3,
54
+ /** Entity rows loaded per answer, the matched seeds included. */
55
+ entityLoads: 70,
56
+ maxBytes: 16_384,
57
+ /** Title length (characters) kept when an answer must shrink to fit. */
58
+ titleChars: 160,
59
+ /** Characters of an ADR decision kept as its excerpt. */
60
+ excerptChars: 280,
61
+ /** Characters of a witness or unknown detail kept. */
62
+ detailChars: 240,
63
+ };
64
+ function text(value) {
65
+ return typeof value === "string" ? value : value == null ? "" : String(value);
66
+ }
67
+ function unique(ids) {
68
+ return [...new Set(ids.filter((id) => id.length > 0))];
69
+ }
70
+ function brief(entity) {
71
+ const status = text(entity.status);
72
+ return {
73
+ id: text(entity.id),
74
+ title: text(entity.title),
75
+ ...(status ? { status } : {}),
76
+ };
77
+ }
78
+ function edgesGoal(ids) {
79
+ const idList = ids.map((id) => `'${escapeAtom(id)}'`).join(",");
80
+ return `findall([Rel,From,To], (member(Rel, [${ANSWER_RELATIONSHIPS.join(",")}]), kb_relationship(Rel, From, To), (member(From, [${idList}]); member(To, [${idList}]))), Edges)`;
81
+ }
82
+ /**
83
+ * The part of the KB graph the answer reads, fetched in batches: each
84
+ * `fetch` is one engine round trip for every id whose edges or row are not
85
+ * known yet. Answering used to cost one round trip per edge hop and per
86
+ * linked entity; on a journaled engine each costs a polling interval, which
87
+ * made a warm search take seconds.
88
+ */
89
+ class AnswerGraph {
90
+ prolog;
91
+ rowBudget;
92
+ edgesById = new Map();
93
+ rows = new Map();
94
+ rowLoads = 0;
95
+ truncated = false;
96
+ constructor(prolog, rowBudget) {
97
+ this.prolog = prolog;
98
+ this.rowBudget = rowBudget;
99
+ }
100
+ seed(entity) {
101
+ const id = text(entity.id);
102
+ if (id)
103
+ this.rows.set(id, { ...entity });
104
+ }
105
+ row(id) {
106
+ return this.rows.get(id) ?? null;
107
+ }
108
+ /** Edges touching any of `ids` that were fetched, without duplicates. */
109
+ edgesOf(ids) {
110
+ const seen = new Set();
111
+ const edges = [];
112
+ for (const id of ids) {
113
+ for (const edge of this.edgesById.get(id) ?? []) {
114
+ const key = `${edge.rel}|${edge.from}|${edge.to}`;
115
+ if (seen.has(key))
116
+ continue;
117
+ seen.add(key);
118
+ edges.push(edge);
119
+ }
120
+ }
121
+ return edges;
122
+ }
123
+ /**
124
+ * Fetch missing edges and rows in one query. `alsoGoal` is conjoined so a
125
+ * further lookup shares the round trip; its bindings are returned.
126
+ */
127
+ async fetch(request) {
128
+ const edgeIds = unique(request.edges ?? []).filter((id) => !this.edgesById.has(id));
129
+ const wanted = unique(request.rows ?? []).filter((id) => !this.rows.has(id));
130
+ const room = Math.max(0, this.rowBudget - this.rowLoads);
131
+ if (wanted.length > room)
132
+ this.truncated = true;
133
+ const rowIds = wanted.slice(0, room);
134
+ const goals = [
135
+ ...(edgeIds.length > 0 ? [edgesGoal(edgeIds)] : []),
136
+ ...(rowIds.length > 0 ? [entityRowsGoal(rowIds, ROW_KEYS)] : []),
137
+ ...(request.alsoGoal ? [request.alsoGoal] : []),
138
+ ];
139
+ if (goals.length === 0)
140
+ return {};
141
+ const result = await this.prolog.query(goals.join(", "));
142
+ if (!result.success) {
143
+ // A failed edge lookup reads as no links, as before batching; a
144
+ // failed entity lookup must not silently drop requirements.
145
+ if (rowIds.length > 0 || request.alsoGoal) {
146
+ throw new Error(result.error || "Search answer graph query failed");
147
+ }
148
+ for (const id of edgeIds)
149
+ this.edgesById.set(id, []);
150
+ return {};
151
+ }
152
+ if (edgeIds.length > 0) {
153
+ for (const id of edgeIds)
154
+ this.edgesById.set(id, []);
155
+ for (const [rel, from, to] of parseTriples(result.bindings.Edges ?? "[]")) {
156
+ const edge = {
157
+ rel,
158
+ from: normalizeEntityId(from),
159
+ to: normalizeEntityId(to),
160
+ };
161
+ this.edgesById.get(edge.from)?.push(edge);
162
+ if (edge.to !== edge.from)
163
+ this.edgesById.get(edge.to)?.push(edge);
164
+ }
165
+ }
166
+ if (rowIds.length > 0) {
167
+ this.rowLoads += rowIds.length;
168
+ for (const id of rowIds)
169
+ this.rows.set(id, null);
170
+ for (const row of parseListOfLists(result.bindings.Results ?? "[]").map(parseEntityFromList))
171
+ this.rows.set(normalizeEntityId(text(row.id)), row);
172
+ }
173
+ return result.bindings;
174
+ }
175
+ }
176
+ /** Tests that verify a scenario: `verified_by` from it or `validates` into it. */
177
+ function scenarioTests(scenarioId, edges) {
178
+ const tests = [];
179
+ for (const edge of edges) {
180
+ if (edge.rel === "verified_by" && edge.from === scenarioId)
181
+ tests.push(edge.to);
182
+ if (edge.rel === "validates" && edge.to === scenarioId)
183
+ tests.push(edge.from);
184
+ }
185
+ return tests;
186
+ }
187
+ /** Requirement ids a non-requirement entity points back to. */
188
+ function owningRequirements(id, type, edges) {
189
+ const owners = [];
190
+ for (const edge of edges) {
191
+ if (type === "scenario" && edge.rel === "specified_by" && edge.to === id)
192
+ owners.push(edge.from);
193
+ if (type === "test" && edge.rel === "verified_by" && edge.to === id)
194
+ owners.push(edge.from);
195
+ if (type === "test" && edge.rel === "validates" && edge.from === id)
196
+ owners.push(edge.to);
197
+ if (type === "symbol" && edge.rel === "implements" && edge.from === id)
198
+ owners.push(edge.to);
199
+ if (type === "fact" && FACT_LINKS.has(edge.rel) && edge.to === id)
200
+ owners.push(edge.from);
201
+ }
202
+ return owners;
203
+ }
204
+ /** Shorten to at most `maxChars` code points, ending with an ellipsis. */
205
+ function clip(value, maxChars) {
206
+ const chars = Array.from(value);
207
+ if (chars.length <= maxChars)
208
+ return value;
209
+ return `${chars.slice(0, Math.max(0, maxChars - 1)).join("")}…`;
210
+ }
211
+ /**
212
+ * The opening sentences of an ADR's Decision section (falling back to
213
+ * Rationale, then to the first prose paragraph), as plain text of at most
214
+ * `maxChars` characters.
215
+ */
216
+ // implements REQ-kibi-search-answer-layer-v2
217
+ export function adrExcerpt(body, maxChars = SEARCH_ANSWER_LIMITS.excerptChars) {
218
+ const sections = new Map();
219
+ const preamble = [];
220
+ let current = preamble;
221
+ let fenced = false;
222
+ for (const line of body.split(/\r?\n/)) {
223
+ if (/^\s*(```|~~~)/.test(line)) {
224
+ fenced = !fenced;
225
+ continue;
226
+ }
227
+ if (fenced)
228
+ continue;
229
+ const heading = /^\s{0,3}#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
230
+ if (heading) {
231
+ const name = text(heading[1]).toLowerCase();
232
+ current = [];
233
+ if (!sections.has(name))
234
+ sections.set(name, current);
235
+ continue;
236
+ }
237
+ current.push(line);
238
+ }
239
+ const named = (pattern) => [...sections.entries()].find(([name, lines]) => pattern.test(name) && lines.join("").trim() !== "")?.[1];
240
+ const lines = named(/^decision\b/) ??
241
+ named(/^rationale\b/) ??
242
+ (preamble.join("").trim() !== ""
243
+ ? preamble
244
+ : [...sections.values()].find((section) => section.join("").trim()));
245
+ if (!lines)
246
+ return undefined;
247
+ const prose = lines
248
+ .map((line) => line
249
+ .replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "")
250
+ .replace(/^\s*>\s?/, "")
251
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
252
+ .replace(/\*\*|__|`/g, "")
253
+ .trim())
254
+ .filter((line) => line !== "" && !/^\|/.test(line))
255
+ .join(" ")
256
+ .replace(/\s+/g, " ")
257
+ .trim();
258
+ if (!prose)
259
+ return undefined;
260
+ const sentences = prose.split(/(?<=[.!?])\s+/);
261
+ let excerpt = "";
262
+ for (const sentence of sentences) {
263
+ const next = `${excerpt} ${sentence.trim()}`.trim();
264
+ if (excerpt !== "" && Array.from(next).length > maxChars)
265
+ break;
266
+ excerpt = next;
267
+ }
268
+ return clip(excerpt, maxChars);
269
+ }
270
+ async function rationaleOf(row, workspaceRoot, bodies) {
271
+ const source = text(row.sourceFile) || text(row.source);
272
+ if (!source)
273
+ return brief(row);
274
+ let excerpt;
275
+ if (workspaceRoot !== undefined) {
276
+ let body = bodies.get(source);
277
+ if (body === undefined) {
278
+ body = loadMarkdownBody(source, workspaceRoot);
279
+ bodies.set(source, body);
280
+ }
281
+ const markdown = await body;
282
+ excerpt = markdown ? adrExcerpt(markdown) : undefined;
283
+ }
284
+ return {
285
+ ...brief(row),
286
+ source: source.split("#", 1)[0] ?? source,
287
+ ...(excerpt ? { excerpt } : {}),
288
+ };
289
+ }
290
+ function records(value) {
291
+ return Array.isArray(value)
292
+ ? value.filter((item) => item !== null && typeof item === "object" && !Array.isArray(item))
293
+ : [];
294
+ }
295
+ function strings(value) {
296
+ return Array.isArray(value)
297
+ ? value.map((item) => normalizeEntityId(text(item))).filter(Boolean)
298
+ : [];
299
+ }
300
+ const FEASIBILITY_UNKNOWN_TEXT = {
301
+ no_assumptions: "expects success but assumes nothing, so its feasibility against current requirements is unknown",
302
+ unmatched_assumption: "expects success but an assumption is not constrained by any current requirement, so its feasibility is unknown",
303
+ incomparable_assumption: "expects success but an assumption cannot be compared with the requirement constraint (type or unit differs), so its feasibility is unknown",
304
+ contradictory_assumptions: "expects success but its assumptions contradict each other, so the scenario can never happen",
305
+ conflicting_requirements: "expects success but the requirements constraining it conflict with each other, so its feasibility is unknown",
306
+ undecided_rule: "expects success but a rule governing it can be neither satisfied nor refuted from its assumptions, so its feasibility is unknown",
307
+ undetermined_validity: "expects success but would conflict only with constraints whose validity window may not cover the scenario's time, so its feasibility is unknown",
308
+ };
309
+ /** Project the Prolog verdict of one requirement onto the answer fields. */
310
+ // implements REQ-kibi-search-answer-layer-v2
311
+ export function projectVerdict(raw, perRequirement = SEARCH_ANSWER_LIMITS.perRequirement) {
312
+ const detail = (value) => clip(text(value), SEARCH_ANSWER_LIMITS.detailChars);
313
+ const witnesses = [];
314
+ const unknowns = [];
315
+ if (raw === undefined || raw.error !== undefined) {
316
+ unknowns.push({
317
+ kind: "verdict_unavailable",
318
+ detail: detail(raw?.error !== undefined
319
+ ? `The checks could not be evaluated for this requirement: ${text(raw.error)}`
320
+ : "The checks could not be evaluated for this requirement."),
321
+ entities: [],
322
+ });
323
+ }
324
+ else {
325
+ for (const row of records(raw.contradictions)) {
326
+ const other = normalizeEntityId(text(row.with));
327
+ if (text(row.status) === "contradiction") {
328
+ witnesses.push({
329
+ check: "domain-contradictions",
330
+ status: "contradiction",
331
+ ...(other ? { with: other } : {}),
332
+ facts: strings(row.facts),
333
+ detail: detail(row.reason),
334
+ });
335
+ }
336
+ else {
337
+ unknowns.push({
338
+ kind: "contradiction_unresolved",
339
+ detail: detail(row.reason),
340
+ entities: [other, ...strings(row.facts)].filter(Boolean),
341
+ });
342
+ }
343
+ }
344
+ for (const row of records(raw.scenarios)) {
345
+ const scenario = normalizeEntityId(text(row.scenario));
346
+ const outcome = text(row.outcome);
347
+ if (outcome === "infeasible") {
348
+ const forbidding = normalizeEntityId(text(row.requirement));
349
+ witnesses.push({
350
+ check: "scenario-feasibility",
351
+ status: "infeasible",
352
+ ...(forbidding ? { with: forbidding } : {}),
353
+ scenario,
354
+ facts: [...strings(row.assumed), ...strings(row.fact)],
355
+ detail: detail(row.reason),
356
+ });
357
+ }
358
+ else if (outcome === "unknown") {
359
+ const reason = text(row.reason);
360
+ unknowns.push({
361
+ kind: "feasibility_unknown",
362
+ detail: detail(`${scenario} ${FEASIBILITY_UNKNOWN_TEXT[reason] ?? `has unknown feasibility (${reason})`}`),
363
+ entities: [scenario, ...strings(row.facts)],
364
+ });
365
+ }
366
+ }
367
+ for (const row of records(raw.forbids)) {
368
+ witnesses.push({
369
+ check: "scenario-feasibility",
370
+ status: "infeasible",
371
+ scenario: normalizeEntityId(text(row.scenario)),
372
+ facts: [...strings(row.assumed), ...strings(row.fact)],
373
+ detail: detail(row.reason),
374
+ });
375
+ }
376
+ const inventory = records([raw.inventory])[0];
377
+ if (inventory !== undefined) {
378
+ for (const claim of records(inventory.unresolved)) {
379
+ unknowns.push({
380
+ kind: "unresolved_proposition",
381
+ detail: detail(`${text(claim.status)}: ${text(claim.claim)}`),
382
+ entities: [],
383
+ });
384
+ }
385
+ if (text(inventory.status) === "missing") {
386
+ unknowns.push({
387
+ kind: "analysis_incomplete",
388
+ detail: "The requirement's clause ledger (semantic inventory) is missing or incomplete, so contradiction analysis cannot cover all of its clauses.",
389
+ entities: [],
390
+ });
391
+ }
392
+ }
393
+ }
394
+ const status = witnesses.some((witness) => witness.status === "contradiction")
395
+ ? "contradiction"
396
+ : witnesses.length > 0
397
+ ? "infeasible"
398
+ : unknowns.length > 0
399
+ ? "unknown"
400
+ : "none";
401
+ // Contradictions first: they block regardless of scenarios.
402
+ witnesses.sort((left, right) => left.status === right.status ? 0 : left.status === "contradiction" ? -1 : 1);
403
+ return {
404
+ verdict: { status, witnesses: witnesses.slice(0, perRequirement) },
405
+ unknowns: unknowns.slice(0, perRequirement),
406
+ truncated: witnesses.length > perRequirement || unknowns.length > perRequirement,
407
+ };
408
+ }
409
+ const UNKNOWN_PROCEDURE = /existence_error|[Uu]nknown procedure|does not exist/;
410
+ function decodeJsonBinding(raw) {
411
+ if (raw === undefined)
412
+ return undefined;
413
+ let parsed = JSON.parse(raw);
414
+ if (typeof parsed === "string")
415
+ parsed = JSON.parse(parsed);
416
+ return parsed;
417
+ }
418
+ /**
419
+ * Rows the answer still needs plus the verdicts of the governing
420
+ * requirements, sharing one round trip while the discovery module is loaded;
421
+ * otherwise the module is loaded first. A verdict that cannot be computed
422
+ * leaves the answer standing with `verdict_unavailable` unknowns.
423
+ */
424
+ async function fetchVerdicts(prolog, graph, rowIds, reqIds) {
425
+ const goal = `discovery:search_answer_verdicts_json([${reqIds.map((id) => `'${escapeAtom(id)}'`).join(",")}], JsonString)`;
426
+ try {
427
+ const bindings = await graph.fetch({ rows: rowIds, alsoGoal: goal });
428
+ return decodeJsonBinding(bindings.JsonString);
429
+ }
430
+ catch (error) {
431
+ const message = error instanceof Error ? error.message : String(error);
432
+ await graph.fetch({ rows: rowIds });
433
+ if (!UNKNOWN_PROCEDURE.test(message))
434
+ return undefined;
435
+ }
436
+ try {
437
+ return await runOperationJsonQuery(prolog, "discovery.pl", goal, "Search answer verdicts");
438
+ }
439
+ catch {
440
+ return undefined;
441
+ }
442
+ }
443
+ export async function buildSearchAnswer(prolog, matches, options = {}) {
444
+ const limits = SEARCH_ANSWER_LIMITS;
445
+ const graph = new AnswerGraph(prolog, limits.entityLoads);
446
+ const seeds = matches.slice(0, limits.seeds);
447
+ let truncated = matches.length > limits.seeds;
448
+ const seedIds = seeds.map((match) => text(match.entity.id));
449
+ // Search rows are projected without fact_kind or approved_by, so the
450
+ // matched entities' own rows are loaded with their edges: notes are told
451
+ // apart from rules, and a matched exception keeps its approver. A match
452
+ // the store no longer holds keeps its search row.
453
+ await graph.fetch({ edges: seedIds, rows: seedIds });
454
+ for (const match of seeds)
455
+ if (graph.row(text(match.entity.id)) === null)
456
+ graph.seed(match.entity);
457
+ const seedEdges = graph.edgesOf(seedIds);
458
+ // Candidate requirements: matched requirements first, then requirements
459
+ // that own a matched scenario, test, symbol or fact.
460
+ const candidates = new Map();
461
+ const propose = (owner, score, via) => {
462
+ const previous = candidates.get(owner);
463
+ if (!previous || previous.score < score)
464
+ candidates.set(owner, { score, via });
465
+ };
466
+ const observations = [];
467
+ const rationaleRows = new Map();
468
+ // A matched test may verify a scenario rather than a requirement; its
469
+ // targets are resolved to their governing requirements after the loop.
470
+ const testTargets = [];
471
+ for (const match of seeds) {
472
+ const id = text(match.entity.id);
473
+ const entity = graph.row(id) ?? match.entity;
474
+ const type = text(entity.type || match.entity.type);
475
+ if (type === "req") {
476
+ if (!candidates.has(id))
477
+ candidates.set(id, { score: match.score, via: "matched" });
478
+ continue;
479
+ }
480
+ if (type === "adr") {
481
+ if (!SUPERSEDED_ADR_STATUSES.has(text(entity.status)))
482
+ rationaleRows.set(id, entity);
483
+ continue;
484
+ }
485
+ if (type === "fact") {
486
+ const kind = text(entity.fact_kind);
487
+ if (kind === "observation" || kind === "meta") {
488
+ observations.push(brief(entity));
489
+ continue;
490
+ }
491
+ }
492
+ const owners = owningRequirements(id, type, seedEdges);
493
+ if (type === "test") {
494
+ testTargets.push({ id, score: match.score, targets: owners });
495
+ continue;
496
+ }
497
+ for (const owner of owners)
498
+ propose(owner, match.score * 0.9, `${type} ${id}`);
499
+ }
500
+ const byScore = () => [...candidates.entries()]
501
+ .sort(([, left], [, right]) => right.score - left.score)
502
+ .map(([id]) => id);
503
+ const targetIds = unique(testTargets.flatMap((test) => test.targets));
504
+ // Test targets and the candidates found so far share one round trip.
505
+ await graph.fetch({
506
+ edges: [...targetIds, ...byScore()],
507
+ rows: byScore(),
508
+ });
509
+ const targetEdges = graph.edgesOf(targetIds);
510
+ for (const test of testTargets) {
511
+ for (const target of test.targets) {
512
+ // A target that some requirement specifies is a scenario: lift the
513
+ // test to that requirement. Anything else is a directly verified
514
+ // requirement (non-requirements are filtered when loaded).
515
+ const scenarioOwners = targetEdges
516
+ .filter((edge) => edge.rel === "specified_by" && edge.to === target)
517
+ .map((edge) => edge.from);
518
+ if (scenarioOwners.length === 0) {
519
+ propose(target, test.score * 0.9, `test ${test.id}`);
520
+ continue;
521
+ }
522
+ for (const owner of scenarioOwners)
523
+ propose(owner, test.score * 0.85, `test ${test.id} via ${target}`);
524
+ }
525
+ }
526
+ const supersededBy = new Map();
527
+ for (const edge of seedEdges)
528
+ if (edge.rel === "supersedes")
529
+ supersededBy.set(edge.to, edge.from);
530
+ // A matched requirement that was superseded points at what governs now:
531
+ // follow the chain so the replacement is a candidate too.
532
+ let frontier = byScore();
533
+ for (let hop = 0; hop < 3 && frontier.length > 0; hop += 1) {
534
+ await graph.fetch({ edges: frontier, rows: frontier });
535
+ for (const edge of graph.edgesOf(frontier))
536
+ if (edge.rel === "supersedes")
537
+ supersededBy.set(edge.to, edge.from);
538
+ const next = [];
539
+ for (const id of frontier) {
540
+ const replacement = supersededBy.get(id);
541
+ const source = candidates.get(id);
542
+ if (replacement === undefined || source === undefined)
543
+ continue;
544
+ if (!candidates.has(replacement)) {
545
+ candidates.set(replacement, {
546
+ score: source.score * 0.95,
547
+ via: `supersedes ${id}`,
548
+ });
549
+ next.push(replacement);
550
+ }
551
+ }
552
+ frontier = next;
553
+ }
554
+ // A replacement found on the last hop still needs its row and links, and
555
+ // a replacement that was itself superseded must not read as current.
556
+ if (frontier.length > 0) {
557
+ await graph.fetch({ edges: frontier, rows: frontier });
558
+ for (const edge of graph.edgesOf(frontier))
559
+ if (edge.rel === "supersedes")
560
+ supersededBy.set(edge.to, edge.from);
561
+ }
562
+ const chosen = [];
563
+ const notGoverning = [];
564
+ const ordered = [...candidates.entries()].sort(([leftId, left], [rightId, right]) => right.score - left.score || leftId.localeCompare(rightId));
565
+ for (const [id, candidate] of ordered) {
566
+ const entity = graph.row(id);
567
+ if (!entity || text(entity.type || "req") !== "req")
568
+ continue;
569
+ const replacement = supersededBy.get(id);
570
+ if (replacement !== undefined ||
571
+ !CURRENT_REQ_STATUSES.has(text(entity.status))) {
572
+ if (notGoverning.length < limits.notGoverning)
573
+ notGoverning.push({
574
+ ...brief(entity),
575
+ ...(replacement !== undefined ? { supersededBy: replacement } : {}),
576
+ });
577
+ else
578
+ truncated = true;
579
+ continue;
580
+ }
581
+ if (chosen.length >= limits.governing) {
582
+ truncated = true;
583
+ continue;
584
+ }
585
+ chosen.push({ id, entity, ...candidate });
586
+ }
587
+ // What each governing requirement links to, from the edges fetched above.
588
+ const firstOf = (ids) => {
589
+ const distinct = unique(ids);
590
+ if (distinct.length > limits.perRequirement)
591
+ truncated = true;
592
+ return distinct.slice(0, limits.perRequirement);
593
+ };
594
+ const plans = chosen.map((req) => {
595
+ const edges = graph.edgesOf([req.id]);
596
+ const from = (rel) => edges
597
+ .filter((edge) => edge.rel === rel && edge.from === req.id)
598
+ .map((edge) => edge.to);
599
+ const into = (rel) => edges
600
+ .filter((edge) => edge.rel === rel && edge.to === req.id)
601
+ .map((edge) => edge.from);
602
+ const scenarioIds = unique(from("specified_by"));
603
+ const traversed = firstOf(scenarioIds);
604
+ return {
605
+ req,
606
+ facts: firstOf(edges
607
+ .filter((edge) => FACT_LINKS.has(edge.rel) && edge.from === req.id)
608
+ .map((edge) => edge.to)),
609
+ scenarios: traversed,
610
+ directTests: unique([...from("verified_by"), ...into("validates")]),
611
+ related: firstOf([...from("relates_to"), ...into("relates_to")]),
612
+ exceptions: firstOf(into("exempts")),
613
+ };
614
+ });
615
+ await graph.fetch({
616
+ edges: plans.flatMap((plan) => plan.scenarios),
617
+ rows: plans.flatMap((plan) => [
618
+ ...plan.facts,
619
+ ...plan.scenarios,
620
+ ...plan.related,
621
+ ...plan.exceptions,
622
+ ...plan.directTests.slice(0, limits.perRequirement),
623
+ ]),
624
+ });
625
+ // Tests linked to the requirement itself come first, then tests that
626
+ // verify one of its scenarios; a test reached both ways counts as direct.
627
+ const testVias = plans.map((plan) => {
628
+ const testVia = new Map();
629
+ for (const testId of plan.directTests)
630
+ if (!testVia.has(testId))
631
+ testVia.set(testId, "direct");
632
+ const verification = graph.edgesOf(plan.scenarios);
633
+ for (const scenarioId of plan.scenarios)
634
+ for (const testId of scenarioTests(scenarioId, verification))
635
+ if (!testVia.has(testId))
636
+ testVia.set(testId, scenarioId);
637
+ return testVia;
638
+ });
639
+ const testIds = testVias.map((testVia) => firstOf([...testVia.keys()]));
640
+ const verdicts = await fetchVerdicts(prolog, graph, testIds.flat(), chosen.map((req) => req.id));
641
+ const verdictById = new Map(records(verdicts?.requirements).map((row) => [
642
+ normalizeEntityId(text(row.id)),
643
+ row,
644
+ ]));
645
+ const bodies = new Map();
646
+ const rowsOf = (ids) => ids.map((id) => graph.row(id)).filter((row) => row !== null);
647
+ const governing = [];
648
+ for (const [index, plan] of plans.entries()) {
649
+ const { req } = plan;
650
+ const adrRows = rowsOf(plan.related).filter((row) => text(row.type) === "adr" &&
651
+ !SUPERSEDED_ADR_STATUSES.has(text(row.status)));
652
+ for (const adr of adrRows)
653
+ rationaleRows.set(text(adr.id), adr);
654
+ const projected = projectVerdict(verdicts === undefined ? undefined : verdictById.get(req.id));
655
+ if (projected.truncated)
656
+ truncated = true;
657
+ const testVia = testVias[index] ?? new Map();
658
+ governing.push({
659
+ ...brief(req.entity),
660
+ score: req.score,
661
+ via: req.via,
662
+ facts: rowsOf(plan.facts).map((fact) => {
663
+ const factKind = text(fact.fact_kind);
664
+ return { ...brief(fact), ...(factKind ? { factKind } : {}) };
665
+ }),
666
+ scenarios: rowsOf(plan.scenarios).map(brief),
667
+ tests: rowsOf(testIds[index] ?? []).map((row) => ({
668
+ ...brief(row),
669
+ via: testVia.get(text(row.id)) ?? "direct",
670
+ })),
671
+ adrs: adrRows.map(brief),
672
+ verdict: projected.verdict,
673
+ exceptions: rowsOf(plan.exceptions).map((row) => {
674
+ const approvedBy = text(row.approved_by).trim();
675
+ return { ...brief(row), ...(approvedBy ? { approvedBy } : {}) };
676
+ }),
677
+ unknowns: projected.unknowns,
678
+ });
679
+ }
680
+ const rationaleList = [...rationaleRows.values()];
681
+ if (rationaleList.length > limits.rationale)
682
+ truncated = true;
683
+ if (observations.length > limits.observations)
684
+ truncated = true;
685
+ const rationale = await Promise.all(rationaleList
686
+ .slice(0, limits.rationale)
687
+ .map((row) => rationaleOf(row, options.workspaceRoot, bodies)));
688
+ const scope = {
689
+ branch: options.branch ?? null,
690
+ snapshotId: text(verdicts?.scope?.snapshotId) || "unknown",
691
+ syncedAt: typeof verdicts?.scope?.syncedAt === "string"
692
+ ? verdicts.scope.syncedAt
693
+ : null,
694
+ };
695
+ const answer = {
696
+ version: "kibi.search-answer.v1",
697
+ governing,
698
+ rationale,
699
+ notGoverning,
700
+ observations: observations.slice(0, limits.observations),
701
+ scope,
702
+ truncated: truncated || graph.truncated,
703
+ note: governing.length === 0
704
+ ? "No current requirement matched or owns a matched entity. Absence here is not evidence that nothing governs the change; refine the query or pass sourceLocations."
705
+ : "Governing requirements are current requirements matched by the query or linked to a matched entity. Links are discovery, not proof. A verdict lists the contradiction and scenario-feasibility witnesses that name the requirement; none means no check named it, not that it is proven. Use kb_check and kb_coverage for consistency and proof status.",
706
+ };
707
+ return fitSearchAnswer(answer, limits.maxBytes);
708
+ }
709
+ function byteSize(value) {
710
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
711
+ }
712
+ function clipTitle(entity, chars) {
713
+ return { ...entity, title: clip(entity.title, chars) };
714
+ }
715
+ function clipRationale(entity, chars) {
716
+ const clipped = clipTitle(entity, chars);
717
+ return entity.excerpt === undefined
718
+ ? clipped
719
+ : {
720
+ ...clipped,
721
+ excerpt: clip(entity.excerpt, SEARCH_ANSWER_LIMITS.excerptChars),
722
+ };
723
+ }
724
+ function withoutExcerpt(entity) {
725
+ if (entity.excerpt === undefined)
726
+ return entity;
727
+ const { excerpt: _excerpt, ...rest } = entity;
728
+ return rest;
729
+ }
730
+ const ANSWER_TRIM_ORDER = [
731
+ "observations",
732
+ "notGoverning",
733
+ "rationale",
734
+ ];
735
+ const REQUIREMENT_TRIM_ORDER = [
736
+ "unknowns",
737
+ "exceptions",
738
+ "adrs",
739
+ "tests",
740
+ "scenarios",
741
+ "facts",
742
+ ];
743
+ /**
744
+ * Hold the whole answer under `maxBytes`, marking it truncated when anything
745
+ * is cut. In order: long titles, excerpts and details are clipped; ADR
746
+ * excerpts are dropped, last rationale entry first; the least relevant
747
+ * governing requirements are dropped;
748
+ * list items are dropped (notes and side lists before the remaining
749
+ * requirement's own lists, its verdict witnesses last); then the remaining
750
+ * requirement's title and `via` are clipped. A verdict keeps its status when
751
+ * its witnesses are dropped. If even that cannot fit, the requirement itself
752
+ * is dropped.
753
+ */
754
+ export function fitSearchAnswer(answer, maxBytes = SEARCH_ANSWER_LIMITS.maxBytes) {
755
+ let current = answer;
756
+ const over = () => byteSize(current) > maxBytes;
757
+ if (!over())
758
+ return current;
759
+ const titleChars = SEARCH_ANSWER_LIMITS.titleChars;
760
+ const detailChars = SEARCH_ANSWER_LIMITS.detailChars;
761
+ const clipAll = (rows) => rows.map((row) => clipTitle(row, titleChars));
762
+ current = {
763
+ ...current,
764
+ truncated: true,
765
+ governing: current.governing.map((req) => ({
766
+ ...clipTitle(req, titleChars),
767
+ facts: clipAll(req.facts),
768
+ scenarios: clipAll(req.scenarios),
769
+ tests: clipAll(req.tests),
770
+ adrs: clipAll(req.adrs),
771
+ verdict: {
772
+ ...req.verdict,
773
+ witnesses: req.verdict.witnesses.map((witness) => ({
774
+ ...witness,
775
+ detail: clip(witness.detail, detailChars),
776
+ })),
777
+ },
778
+ exceptions: clipAll(req.exceptions),
779
+ unknowns: req.unknowns.map((unknown) => ({
780
+ ...unknown,
781
+ detail: clip(unknown.detail, detailChars),
782
+ })),
783
+ })),
784
+ rationale: current.rationale.map((adr) => clipRationale(adr, titleChars)),
785
+ notGoverning: clipAll(current.notGoverning),
786
+ observations: clipAll(current.observations),
787
+ };
788
+ // Excerpts are the most expendable text: drop them before any entry.
789
+ for (let index = current.rationale.length - 1; index >= 0 && over(); index -= 1) {
790
+ const rationale = [...current.rationale];
791
+ rationale[index] = withoutExcerpt(rationale[index]);
792
+ current = { ...current, rationale };
793
+ }
794
+ while (over() && current.governing.length > 1)
795
+ current = { ...current, governing: current.governing.slice(0, -1) };
796
+ while (over()) {
797
+ const side = ANSWER_TRIM_ORDER.find((key) => current[key].length > 0);
798
+ if (side) {
799
+ current = { ...current, [side]: current[side].slice(0, -1) };
800
+ continue;
801
+ }
802
+ const [req] = current.governing;
803
+ if (!req)
804
+ break;
805
+ const own = REQUIREMENT_TRIM_ORDER.find((key) => req[key].length > 0);
806
+ if (own) {
807
+ current = {
808
+ ...current,
809
+ governing: [{ ...req, [own]: req[own].slice(0, -1) }],
810
+ };
811
+ continue;
812
+ }
813
+ if (req.verdict.witnesses.length === 0)
814
+ break;
815
+ current = {
816
+ ...current,
817
+ governing: [
818
+ {
819
+ ...req,
820
+ verdict: {
821
+ ...req.verdict,
822
+ witnesses: req.verdict.witnesses.slice(0, -1),
823
+ },
824
+ },
825
+ ],
826
+ };
827
+ }
828
+ for (const field of ["title", "via"]) {
829
+ while (over()) {
830
+ const [req] = current.governing;
831
+ const chars = req ? Array.from(req[field]).length : 0;
832
+ if (!req || chars <= 1)
833
+ break;
834
+ // Each removed code point frees at least one byte; one more makes room
835
+ // for the ellipsis.
836
+ const keep = Math.max(1, chars - (byteSize(current) - maxBytes) - 2);
837
+ current = {
838
+ ...current,
839
+ governing: [{ ...req, [field]: clip(req[field], keep) }],
840
+ };
841
+ }
842
+ }
843
+ if (over())
844
+ current = { ...current, governing: [] };
845
+ return current;
846
+ }