kibi-cli 2.36.0 → 2.39.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 (224) hide show
  1. package/dist/cli-json-command.d.ts.map +1 -1
  2. package/dist/cli-json-command.js +4 -0
  3. package/dist/cli-operation-metadata.d.ts +1 -1
  4. package/dist/cli-operation-metadata.js +1 -1
  5. package/dist/cli-register-foundation.d.ts.map +1 -1
  6. package/dist/cli-register-foundation.js +2 -1
  7. package/dist/cli-register-maintenance.d.ts.map +1 -1
  8. package/dist/cli-register-maintenance.js +5 -0
  9. package/dist/cli-register-reporting.d.ts.map +1 -1
  10. package/dist/cli-register-reporting.js +1 -0
  11. package/dist/cli-validate.d.ts.map +1 -1
  12. package/dist/cli-validate.js +21 -2
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +19 -1
  15. package/dist/commands/aggregated-checks.d.ts.map +1 -1
  16. package/dist/commands/aggregated-checks.js +89 -16
  17. package/dist/commands/coverage.d.ts +1 -0
  18. package/dist/commands/coverage.d.ts.map +1 -1
  19. package/dist/commands/coverage.js +40 -0
  20. package/dist/commands/doctor.d.ts +5 -0
  21. package/dist/commands/doctor.d.ts.map +1 -1
  22. package/dist/commands/doctor.js +70 -35
  23. package/dist/commands/github-init.d.ts.map +1 -1
  24. package/dist/commands/github-init.js +4 -2
  25. package/dist/commands/hook-managers.d.ts +43 -0
  26. package/dist/commands/hook-managers.d.ts.map +1 -0
  27. package/dist/commands/hook-managers.js +426 -0
  28. package/dist/commands/init.d.ts +1 -0
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +40 -5
  31. package/dist/commands/migrate.d.ts.map +1 -1
  32. package/dist/commands/migrate.js +9 -3
  33. package/dist/commands/sync/persistence.d.ts.map +1 -1
  34. package/dist/commands/sync/persistence.js +49 -1
  35. package/dist/commands/telemetry.d.ts +7 -0
  36. package/dist/commands/telemetry.d.ts.map +1 -0
  37. package/dist/commands/telemetry.js +23 -0
  38. package/dist/decision-firmness.d.ts.map +1 -1
  39. package/dist/decision-firmness.js +11 -14
  40. package/dist/engine-limits.d.ts.map +1 -1
  41. package/dist/engine-limits.js +8 -10
  42. package/dist/entity-body-context.d.ts.map +1 -1
  43. package/dist/entity-body-context.js +39 -36
  44. package/dist/extractors/markdown.d.ts +6 -0
  45. package/dist/extractors/markdown.d.ts.map +1 -1
  46. package/dist/extractors/markdown.js +21 -0
  47. package/dist/extractors/symbol-coordinates.d.ts.map +1 -1
  48. package/dist/extractors/symbol-coordinates.js +23 -4
  49. package/dist/firm-decision-edit.d.ts.map +1 -1
  50. package/dist/firm-decision-edit.js +1 -3
  51. package/dist/intent-search.d.ts +5 -1
  52. package/dist/intent-search.d.ts.map +1 -1
  53. package/dist/intent-search.js +118 -33
  54. package/dist/logic/ir.d.ts.map +1 -1
  55. package/dist/logic/ir.js +82 -108
  56. package/dist/operations/bootstrap/discovery-evidence.d.ts.map +1 -1
  57. package/dist/operations/bootstrap/discovery-evidence.js +44 -2
  58. package/dist/operations/bootstrap/discovery.d.ts.map +1 -1
  59. package/dist/operations/bootstrap/discovery.js +1 -0
  60. package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
  61. package/dist/operations/bootstrap/guidance-actions.js +2 -1
  62. package/dist/operations/bootstrap/plugin-offers.d.ts +11 -2
  63. package/dist/operations/bootstrap/plugin-offers.d.ts.map +1 -1
  64. package/dist/operations/bootstrap/plugin-offers.js +48 -9
  65. package/dist/operations/proof/claim-bindings.d.ts.map +1 -1
  66. package/dist/operations/proof/claim-bindings.js +1 -0
  67. package/dist/operations/proof/code-certificates.d.ts +2 -0
  68. package/dist/operations/proof/code-certificates.d.ts.map +1 -1
  69. package/dist/operations/proof/code-certificates.js +34 -14
  70. package/dist/operations/proof/code-models.d.ts.map +1 -1
  71. package/dist/operations/proof/code-models.js +19 -5
  72. package/dist/operations/proof/code-runner.d.ts +16 -0
  73. package/dist/operations/proof/code-runner.d.ts.map +1 -1
  74. package/dist/operations/proof/code-runner.js +42 -6
  75. package/dist/operations/proof/explain.d.ts +63 -0
  76. package/dist/operations/proof/explain.d.ts.map +1 -1
  77. package/dist/operations/proof/explain.js +162 -0
  78. package/dist/operations/semantic-advisor/analyze-prose.d.ts +9 -1
  79. package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
  80. package/dist/operations/semantic-advisor/analyze-prose.js +21 -0
  81. package/dist/operations/semantic-advisor/case-preview.d.ts +24 -0
  82. package/dist/operations/semantic-advisor/case-preview.d.ts.map +1 -0
  83. package/dist/operations/semantic-advisor/case-preview.js +216 -0
  84. package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
  85. package/dist/operations/semantic-advisor/shared.js +3 -0
  86. package/dist/operations/semantic-advisor/types.d.ts +23 -0
  87. package/dist/operations/semantic-advisor/types.d.ts.map +1 -1
  88. package/dist/plugins/approved-source-analyzers.generated.d.ts +11 -8
  89. package/dist/plugins/approved-source-analyzers.generated.d.ts.map +1 -1
  90. package/dist/plugins/approved-source-analyzers.generated.js +11 -8
  91. package/dist/plugins/compose-search-hints.d.ts +29 -0
  92. package/dist/plugins/compose-search-hints.d.ts.map +1 -0
  93. package/dist/plugins/compose-search-hints.js +94 -0
  94. package/dist/plugins/external-allowlist.d.ts +7 -0
  95. package/dist/plugins/external-allowlist.d.ts.map +1 -1
  96. package/dist/plugins/external-allowlist.js +11 -0
  97. package/dist/plugins/registry.d.ts +8 -1
  98. package/dist/plugins/registry.d.ts.map +1 -1
  99. package/dist/plugins/registry.js +22 -3
  100. package/dist/plugins/source-classification.d.ts +2 -2
  101. package/dist/plugins/source-classification.d.ts.map +1 -1
  102. package/dist/plugins/source-classification.js +25 -1
  103. package/dist/prolog/codec.d.ts.map +1 -1
  104. package/dist/prolog/codec.js +10 -20
  105. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
  106. package/dist/public/impact/coverage-depth-quality.js +32 -16
  107. package/dist/public/impact/full-kb-quality.d.ts +6 -0
  108. package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
  109. package/dist/public/impact/full-kb-quality.js +8 -3
  110. package/dist/public/impact/requirement-quality.d.ts.map +1 -1
  111. package/dist/public/impact/requirement-quality.js +56 -29
  112. package/dist/public/operations/catalog.d.ts +44 -4
  113. package/dist/public/operations/catalog.d.ts.map +1 -1
  114. package/dist/public/operations/check-executor.d.ts +6 -0
  115. package/dist/public/operations/check-executor.d.ts.map +1 -1
  116. package/dist/public/operations/check-executor.js +34 -2
  117. package/dist/public/operations/discovery-executors.d.ts +5 -1
  118. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  119. package/dist/public/operations/discovery-executors.js +65 -4
  120. package/dist/public/operations/evidence-floor.d.ts +8 -1
  121. package/dist/public/operations/evidence-floor.d.ts.map +1 -1
  122. package/dist/public/operations/evidence-floor.js +3 -4
  123. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  124. package/dist/public/operations/migration-plan.js +41 -10
  125. package/dist/public/operations/node-ports.d.ts.map +1 -1
  126. package/dist/public/operations/node-ports.js +3 -1
  127. package/dist/public/operations/repair-plan.d.ts +46 -0
  128. package/dist/public/operations/repair-plan.d.ts.map +1 -1
  129. package/dist/public/operations/repair-plan.js +110 -1
  130. package/dist/public/operations/schema6-check-actions.js +1 -1
  131. package/dist/public/operations/source-analysis-cache.d.ts +22 -0
  132. package/dist/public/operations/source-analysis-cache.d.ts.map +1 -0
  133. package/dist/public/operations/source-analysis-cache.js +157 -0
  134. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  135. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  136. package/dist/public/operations/specs/check-rules.generated.js +2 -1
  137. package/dist/public/operations/specs/check.d.ts +1 -1
  138. package/dist/public/operations/specs/discovery.d.ts +7 -2
  139. package/dist/public/operations/specs/discovery.d.ts.map +1 -1
  140. package/dist/public/operations/specs/discovery.js +7 -2
  141. package/dist/public/operations/specs/mutation-schemas.d.ts +36 -1
  142. package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
  143. package/dist/public/operations/specs/mutation-schemas.js +30 -0
  144. package/dist/public/operations/specs/upsert.d.ts +36 -1
  145. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  146. package/dist/public/operations/symbol-granularity-check.d.ts +3 -1
  147. package/dist/public/operations/symbol-granularity-check.d.ts.map +1 -1
  148. package/dist/public/operations/symbol-granularity-check.js +21 -7
  149. package/dist/public/proof-protocol.d.ts +6 -1
  150. package/dist/public/proof-protocol.d.ts.map +1 -1
  151. package/dist/public/proof-protocol.js +12 -3
  152. package/dist/public/proof-sources.d.ts +2 -0
  153. package/dist/public/proof-sources.d.ts.map +1 -1
  154. package/dist/public/proof-sources.js +32 -6
  155. package/dist/public/schemas/entity.d.ts.map +1 -1
  156. package/dist/public/schemas/entity.js +59 -0
  157. package/dist/public/skills/kibi-bootstrap/SKILL.md +2 -2
  158. package/dist/public/skills/kibi-commit-review/SKILL.md +6 -2
  159. package/dist/public/skills/kibi-decisions/SKILL.md +7 -2
  160. package/dist/public/skills/kibi-decisions/resources/signoff-views.md +3 -1
  161. package/dist/public/skills/kibi-usage/SKILL.md +50 -3
  162. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +34 -0
  163. package/dist/public/skills/kibi-usage/resources/proof.md +13 -5
  164. package/dist/public/skills/kibi-usage/resources/workflows.md +10 -0
  165. package/dist/relationships/shards.d.ts +2 -0
  166. package/dist/relationships/shards.d.ts.map +1 -1
  167. package/dist/relationships/shards.js +25 -1
  168. package/dist/schemas/entity.schema.json +98 -1
  169. package/dist/search-answer.d.ts +3 -2
  170. package/dist/search-answer.d.ts.map +1 -1
  171. package/dist/search-answer.js +57 -4
  172. package/dist/search-hints.d.ts +58 -0
  173. package/dist/search-hints.d.ts.map +1 -0
  174. package/dist/search-hints.js +187 -0
  175. package/dist/search-ranking.d.ts +7 -0
  176. package/dist/search-ranking.d.ts.map +1 -1
  177. package/dist/search-ranking.js +54 -56
  178. package/dist/search-token-cache.d.ts +30 -0
  179. package/dist/search-token-cache.d.ts.map +1 -0
  180. package/dist/search-token-cache.js +86 -0
  181. package/dist/traceability/impact-knowledge.d.ts.map +1 -1
  182. package/dist/traceability/impact-knowledge.js +2 -0
  183. package/dist/traceability/temp-kb.d.ts.map +1 -1
  184. package/dist/traceability/temp-kb.js +11 -1
  185. package/dist/types/entities.d.ts +12 -0
  186. package/dist/types/entities.d.ts.map +1 -1
  187. package/dist/usage-telemetry.d.ts +20 -0
  188. package/dist/usage-telemetry.d.ts.map +1 -0
  189. package/dist/usage-telemetry.js +93 -0
  190. package/dist/utils/branch-resolver.d.ts.map +1 -1
  191. package/dist/utils/branch-resolver.js +1 -0
  192. package/dist/utils/rule-registry.generated.d.ts +5 -0
  193. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  194. package/dist/utils/rule-registry.generated.js +6 -0
  195. package/package.json +5 -4
  196. package/schema/entities.pl +12 -0
  197. package/src/public/impact/coverage-depth-quality.ts +59 -21
  198. package/src/public/impact/full-kb-quality.ts +15 -1
  199. package/src/public/impact/requirement-quality.ts +95 -38
  200. package/src/public/operations/check-executor.ts +41 -1
  201. package/src/public/operations/discovery-executors.ts +92 -4
  202. package/src/public/operations/evidence-floor.ts +15 -3
  203. package/src/public/operations/migration-plan.ts +41 -9
  204. package/src/public/operations/node-ports.ts +3 -1
  205. package/src/public/operations/repair-plan.ts +152 -1
  206. package/src/public/operations/schema6-check-actions.ts +1 -1
  207. package/src/public/operations/source-analysis-cache.ts +196 -0
  208. package/src/public/operations/specs/check-rules.generated.ts +2 -1
  209. package/src/public/operations/specs/discovery.ts +8 -2
  210. package/src/public/operations/specs/mutation-schemas.ts +34 -0
  211. package/src/public/operations/symbol-granularity-check.ts +25 -5
  212. package/src/public/proof-protocol-types.d.ts +1 -1
  213. package/src/public/proof-protocol.ts +18 -4
  214. package/src/public/proof-sources.ts +37 -6
  215. package/src/public/schemas/entity.ts +65 -0
  216. package/src/public/skills/kibi-bootstrap/SKILL.md +2 -2
  217. package/src/public/skills/kibi-commit-review/SKILL.md +6 -2
  218. package/src/public/skills/kibi-decisions/SKILL.md +7 -2
  219. package/src/public/skills/kibi-decisions/resources/signoff-views.md +3 -1
  220. package/src/public/skills/kibi-usage/SKILL.md +50 -3
  221. package/src/public/skills/kibi-usage/resources/fact-lanes.md +34 -0
  222. package/src/public/skills/kibi-usage/resources/proof.md +13 -5
  223. package/src/public/skills/kibi-usage/resources/workflows.md +10 -0
  224. package/src/schemas/entity.schema.json +98 -1
@@ -10,6 +10,7 @@ import {
10
10
  type IntentSearchAnalysis,
11
11
  type IntentSearchFacets,
12
12
  type IntentSearchMatch,
13
+ type MarkdownBodyCache,
13
14
  type SourceLocation,
14
15
  executeIntentSearch,
15
16
  questionPaths,
@@ -18,6 +19,12 @@ import {
18
19
  import { classifyActivation } from "../../operations/bootstrap/activation.js";
19
20
  import { SwiplResolutionError } from "../../prolog/swipl-resolver.js";
20
21
  import { type SearchAnswer, buildSearchAnswer } from "../../search-answer.js";
22
+ import {
23
+ type SearchHintsAnalysis,
24
+ buildSearchHints,
25
+ searchHintsNote,
26
+ searchHintsTrigger,
27
+ } from "../../search-hints.js";
21
28
  import { rankEntities } from "../../search-ranking.js";
22
29
  import type { SearchMatch } from "../../search-ranking.js";
23
30
  import { resolveBranchAttachment } from "../../utils/branch-resolver.js";
@@ -51,7 +58,11 @@ import {
51
58
  runOperationJsonQuery,
52
59
  toPrologAtom,
53
60
  } from "./prolog-json.js";
54
- import type { OperationContext, PrologPort } from "./runtime-types.js";
61
+ import type {
62
+ CapabilityRegistry,
63
+ OperationContext,
64
+ PrologPort,
65
+ } from "./runtime-types.js";
55
66
  import type { OperationResult } from "./types.js";
56
67
  import { readWorkspaceSnapshot } from "./workspace-snapshot.js";
57
68
 
@@ -176,6 +187,7 @@ export type SearchInput = {
176
187
  readonly minScore?: number;
177
188
  readonly fields?: "summary" | "full";
178
189
  readonly answer?: boolean;
190
+ readonly hints?: boolean;
179
191
  };
180
192
 
181
193
  export type SearchPayload = {
@@ -185,7 +197,9 @@ export type SearchPayload = {
185
197
  /** Rows in `results`: the page this call returned. */
186
198
  readonly returned: number;
187
199
  readonly truncated?: boolean;
188
- readonly queryAnalysis?: IntentSearchAnalysis;
200
+ readonly queryAnalysis?: IntentSearchAnalysis & {
201
+ readonly hints?: SearchHintsAnalysis;
202
+ };
189
203
  readonly answer?: SearchAnswer;
190
204
  /** The Kibi branch whose store was searched, when the runtime attached one. */
191
205
  readonly branch?: string | null;
@@ -912,6 +926,51 @@ function questionPathHint(
912
926
  : ` The question names ${list}; their words were left out of the topic. Pass them as sourceLocations to also see the requirements linked to those files.`;
913
927
  }
914
928
 
929
+ /**
930
+ * Hints from the workspace's kibi.search-hints.v1 provider. A host without a
931
+ * plugin runtime, or a registry that fails to load, gives no hints rather than
932
+ * failing the search.
933
+ */
934
+ // implements REQ-kibi-search-hints-host
935
+ async function searchHintsFor(
936
+ context: OperationContext,
937
+ options: Omit<
938
+ Parameters<typeof buildSearchHints>[0],
939
+ "resolution" | "workspaceRoot"
940
+ >,
941
+ ): Promise<SearchHintsAnalysis | undefined> {
942
+ if (!context.ensurePlugins) return undefined;
943
+ let resolution: Awaited<ReturnType<CapabilityRegistry["resolveSearchHints"]>>;
944
+ try {
945
+ resolution = await (await context.ensurePlugins()).resolveSearchHints();
946
+ } catch (error) {
947
+ // A configured provider that cannot load is reported, not thrown: the
948
+ // lexical answer stands on its own.
949
+ const trigger = searchHintsTrigger(options.analysis);
950
+ return trigger === null
951
+ ? undefined
952
+ : {
953
+ trigger,
954
+ provider: null,
955
+ items: [],
956
+ diagnostics: [
957
+ {
958
+ pluginId: "unresolved",
959
+ packageName: null,
960
+ mode: "replace",
961
+ code: "provider_unavailable",
962
+ message: error instanceof Error ? error.message : String(error),
963
+ },
964
+ ],
965
+ };
966
+ }
967
+ return buildSearchHints({
968
+ ...options,
969
+ resolution,
970
+ workspaceRoot: context.workspaceRoot,
971
+ });
972
+ }
973
+
915
974
  export async function executeSearch(
916
975
  input: SearchInput,
917
976
  context: OperationContext,
@@ -928,6 +987,7 @@ export async function executeSearch(
928
987
  minScore,
929
988
  fields = "summary",
930
989
  answer,
990
+ hints: hintsWanted = true,
931
991
  } = input;
932
992
  const trimmedQuery = query.trim();
933
993
  if (!trimmedQuery) {
@@ -960,10 +1020,19 @@ export async function executeSearch(
960
1020
  ...(minScore !== undefined ? { minScore } : {}),
961
1021
  } as const;
962
1022
  validateIntentSearchInput(intentInput);
1023
+ let corpus:
1024
+ | {
1025
+ entities: readonly Record<string, unknown>[];
1026
+ bodies: MarkdownBodyCache;
1027
+ }
1028
+ | undefined;
963
1029
  const intentResult = await executeIntentSearch(
964
1030
  intentInput,
965
1031
  prolog,
966
1032
  context.workspaceRoot,
1033
+ (entities, bodies) => {
1034
+ corpus = { entities, bodies };
1035
+ },
967
1036
  );
968
1037
  const paginated = paginateResults(intentResult.matches, limit, offset);
969
1038
  // The answer layer summarizes the whole ranking, so it belongs to the
@@ -982,6 +1051,22 @@ export async function executeSearch(
982
1051
  const topUngoverned = answerLayer?.ungoverned.find(
983
1052
  (entry) => entry.rank === 1,
984
1053
  );
1054
+ // Hints come after the lexical answer and never change it; a strong
1055
+ // answer never reaches a provider.
1056
+ const hints =
1057
+ hintsWanted &&
1058
+ offset === 0 &&
1059
+ corpus !== undefined &&
1060
+ searchHintsTrigger(intentResult.analysis) !== null
1061
+ ? await searchHintsFor(context, {
1062
+ query: trimmedQuery,
1063
+ analysis: intentResult.analysis,
1064
+ entities: corpus.entities,
1065
+ bodies: corpus.bodies,
1066
+ governingIds: (answerLayer?.governing ?? []).map((req) => req.id),
1067
+ prolog,
1068
+ })
1069
+ : undefined;
985
1070
  const text = `${
986
1071
  intentResult.matches.length === 0
987
1072
  ? `No intent search results for '${trimmedQuery}' (abstained). No entity shares enough of the question's words: ask again in the words and language the KB is written in, or pass sourceLocations.`
@@ -991,7 +1076,7 @@ export async function executeSearch(
991
1076
  .join(
992
1077
  ", ",
993
1078
  )}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}${topUngoverned ? ` (the top match ${topUngoverned.id} is linked to no requirement)` : ""}.`
994
- }${questionPathHint(trimmedQuery, sourceLocations)}${CONTRADICTION_QUESTION.test(trimmedQuery) ? CONTRADICTION_HINT : ""}${storeNote}`;
1079
+ }${searchHintsNote(hints)}${questionPathHint(trimmedQuery, sourceLocations)}${CONTRADICTION_QUESTION.test(trimmedQuery) ? CONTRADICTION_HINT : ""}${storeNote}`;
995
1080
  return {
996
1081
  content: [{ type: "text", text }],
997
1082
  structuredContent: {
@@ -999,7 +1084,10 @@ export async function executeSearch(
999
1084
  count: intentResult.matches.length,
1000
1085
  returned: paginated.length,
1001
1086
  truncated: offset + paginated.length < intentResult.matches.length,
1002
- queryAnalysis: intentResult.analysis,
1087
+ queryAnalysis:
1088
+ hints !== undefined
1089
+ ? { ...intentResult.analysis, hints }
1090
+ : intentResult.analysis,
1003
1091
  ...(answerLayer ? { answer: answerLayer } : {}),
1004
1092
  ...branchFields,
1005
1093
  },
@@ -60,6 +60,13 @@ export type EvidenceFloorOptions = Readonly<{
60
60
  * `kibi prove --all`.
61
61
  */
62
62
  freshEvidence?: boolean;
63
+ /**
64
+ * The instant and workspace snapshot the whole check judges receipts at,
65
+ * so the floor reads the same requirement proofs the coverage evidence
66
+ * read instead of computing them again.
67
+ */
68
+ checkedAt?: string;
69
+ snapshotEvidence?: Awaited<ReturnType<typeof readWorkspaceSnapshot>>;
63
70
  }>;
64
71
 
65
72
  function floorSource(row: EvidenceFloorRow): string {
@@ -145,6 +152,7 @@ export function evidenceFloorViolations(
145
152
  export async function readEvidenceFloorRows(
146
153
  context: OperationContext,
147
154
  snapshotEvidence?: Awaited<ReturnType<typeof readWorkspaceSnapshot>>,
155
+ checkedAt: string = context.clock().toISOString(),
148
156
  ): Promise<readonly EvidenceFloorRow[]> {
149
157
  const prolog = context.prolog ?? (await context.ensureProlog?.());
150
158
  if (prolog === undefined) return [];
@@ -152,7 +160,6 @@ export async function readEvidenceFloorRows(
152
160
  const snapshot = evidence.available ? evidence.snapshot.hash : "unknown";
153
161
  const { perContractTestBindings } = await import("./specs/reporting.js");
154
162
  const bindings = await perContractTestBindings({ ...context, prolog });
155
- const checkedAt = context.clock().toISOString();
156
163
  const goal = `discovery:evidence_floor_json(${bindings === null ? "strict_snapshot, _{}" : `per_contract, ${bindings}`}, ${toPrologAtom(snapshot)}, ${toPrologAtom(checkedAt)}, ${PROOF_RECEIPT_MAX_AGE_SECONDS}, JsonString)`;
157
164
  const payload = await runOperationJsonQuery<EvidenceFloorPayload>(
158
165
  prolog,
@@ -182,8 +189,13 @@ export async function collectEvidenceFloorViolations(
182
189
  schemaVersion < EVIDENCE_FLOOR_SCHEMA_VERSION
183
190
  )
184
191
  return [];
185
- const snapshotEvidence = await readWorkspaceSnapshot(context);
186
- const rows = await readEvidenceFloorRows(context, snapshotEvidence);
192
+ const snapshotEvidence =
193
+ options.snapshotEvidence ?? (await readWorkspaceSnapshot(context));
194
+ const rows = await readEvidenceFloorRows(
195
+ context,
196
+ snapshotEvidence,
197
+ options.checkedAt,
198
+ );
187
199
  const violations = evidenceFloorViolations(rows, options);
188
200
  if (options.freshEvidence === true) {
189
201
  const clean =
@@ -169,20 +169,46 @@ function dependencyOrder(
169
169
  new Set(action.dependsOn.filter((dependency) => byId.has(dependency))),
170
170
  ]),
171
171
  );
172
+ // Dependents of each action in action order, so an emitted action frees
173
+ // its dependents in the order a scan over every action would.
174
+ const dependents = new Map<string, string[]>();
175
+ for (const action of actions) {
176
+ for (const dependency of remaining.get(action.id) as Set<string>) {
177
+ const list = dependents.get(dependency);
178
+ if (list === undefined) dependents.set(dependency, [action.id]);
179
+ else list.push(action.id);
180
+ }
181
+ }
172
182
  const ordered: MigrationAction[] = [];
173
183
  const ready = actions
174
184
  .filter((action) => (remaining.get(action.id)?.size ?? 0) === 0)
175
185
  .sort(actionSort)
176
186
  .map((action) => action.id);
177
187
  const queued = new Set(ready);
178
- while (ready.length > 0) {
179
- const id = ready.shift();
180
- if (id === undefined) break;
188
+ const compareReady = (left: string, right: string): number =>
189
+ compareReadyActionIds(left, right, byId);
190
+ // `ready` stays sorted; a freed action is inserted after its equals, where
191
+ // a stable sort of the pushed list would have put it.
192
+ const enqueue = (id: string, head: number): void => {
193
+ let low = head;
194
+ let high = ready.length;
195
+ while (low < high) {
196
+ const middle = (low + high) >>> 1;
197
+ if (compareReady(ready[middle] as string, id) <= 0) low = middle + 1;
198
+ else high = middle;
199
+ }
200
+ ready.splice(low, 0, id);
201
+ };
202
+ let head = 0;
203
+ while (head < ready.length) {
204
+ const id = ready[head] as string;
205
+ head += 1;
181
206
  queued.delete(id);
182
- const action = byId.get(id);
183
- if (action === undefined) continue;
207
+ // Only ids of `actions` are ever queued or listed as dependents.
208
+ const action = byId.get(id) as MigrationAction;
184
209
  ordered.push(action);
185
- for (const [candidateId, dependencies] of remaining) {
210
+ for (const candidateId of dependents.get(id) ?? []) {
211
+ const dependencies = remaining.get(candidateId) as Set<string>;
186
212
  if (
187
213
  !dependencies.delete(id) ||
188
214
  dependencies.size !== 0 ||
@@ -190,9 +216,8 @@ function dependencyOrder(
190
216
  ) {
191
217
  continue;
192
218
  }
193
- ready.push(candidateId);
194
219
  queued.add(candidateId);
195
- ready.sort((left, right) => compareReadyActionIds(left, right, byId));
220
+ enqueue(candidateId, head);
196
221
  }
197
222
  }
198
223
  // Preserve cyclic/missing-dependency actions in canonical order; validation will block them.
@@ -228,7 +253,14 @@ function summarize(actions: readonly MigrationAction[]) {
228
253
 
229
254
  // implements REQ-agent-guided-migration-orchestration
230
255
  export function buildMigrationPlan(input: PlanInput = {}): MigrationPlan {
231
- const actions = dependencyOrder([...(input.actions ?? [])].sort(actionSort));
256
+ // An id names one action, as in a merged plan: a later definition of the
257
+ // same id replaces the earlier one instead of being ordered twice.
258
+ const uniqueActions = [
259
+ ...new Map(
260
+ (input.actions ?? []).map((action) => [action.id, action]),
261
+ ).values(),
262
+ ];
263
+ const actions = dependencyOrder(uniqueActions.sort(actionSort));
232
264
  const evaluatedDomains = uniqueSorted(input.evaluatedDomains);
233
265
  const incompleteDomains = uniqueSorted(input.incompleteDomains);
234
266
  const expected = {
@@ -118,11 +118,13 @@ async function workspaceSnapshot(workspaceRoot: string) {
118
118
  );
119
119
  const digest = createHash("sha256");
120
120
  digest.update("kibi.workspace-snapshot.v2\0");
121
+ // Synchronous reads: one awaited read per file paid a file-handle round
122
+ // trip each, about eight times the time of reading the same files directly.
121
123
  for (const { raw, key: relativePath } of paths) {
122
124
  digest.update(relativePath);
123
125
  digest.update("\0");
124
126
  try {
125
- const content = await fs.readFile(path.join(workspaceRoot, raw));
127
+ const content = fsSync.readFileSync(path.join(workspaceRoot, raw));
126
128
  digest.update(snapshotFileContent(relativePath, content));
127
129
  } catch (error) {
128
130
  const code =
@@ -110,6 +110,40 @@ const REPAIR_PHASES = {
110
110
  validationRules: ["must-priority-coverage", "no-dangling-refs"],
111
111
  writePolicy: "sequential_upsert",
112
112
  },
113
+ dimension_grounding: {
114
+ order: 64,
115
+ objective:
116
+ "Ground each quantified claim (each, every, all, or a cardinality bound) on what varies: an integer count or bound, a set of members, a rule over a variable, or a state machine. If the quantifier ranges over nothing, record a dimension_waiver on the grounding fact for a person to approve, and record their approval in the same pull request.",
117
+ workflowSteps: [
118
+ "kb_model_requirement",
119
+ "kb_validate_upsert",
120
+ "kb_upsert",
121
+ "kb_check",
122
+ ],
123
+ validationRules: ["no-dangling-refs"],
124
+ writePolicy: "sequential_upsert",
125
+ },
126
+ case_scenarios: {
127
+ order: 66,
128
+ objective:
129
+ "Cover each uncovered rejection case: add a scenario whose assumes fact states the case's assumption (a value past the bound, a value outside the set), expects rejection or error, is linked with specified_by and is verified by a passing end-to-end test. Each repair lists caseSkeletons, upsert-ready draft scenarios and their assumed facts: fill in the placeholders, verify the scenario by a test, then set its status to active (a draft never covers a case). If a person accepts the cases as covered another way, record a case_waiver on the grounding fact for them to approve, and record their approval in the same pull request.",
130
+ workflowSteps: [
131
+ "kb_coverage",
132
+ "kb_validate_upsert",
133
+ "kb_upsert",
134
+ "kb_check",
135
+ ],
136
+ validationRules: ["scenario-outcome-mismatch", "no-dangling-refs"],
137
+ writePolicy: "sequential_upsert",
138
+ },
139
+ negative_scenarios: {
140
+ order: 65,
141
+ objective:
142
+ "Add a scenario that expects rejection or error and assumes a value the requirement forbids (see suggestedNegatives), link it with specified_by and verify it with a passing end-to-end test.",
143
+ workflowSteps: ["kb_query", "kb_validate_upsert", "kb_upsert", "kb_check"],
144
+ validationRules: ["scenario-outcome-mismatch", "no-dangling-refs"],
145
+ writePolicy: "sequential_upsert",
146
+ },
113
147
  scenario_tests: {
114
148
  order: 70,
115
149
  objective:
@@ -206,6 +240,9 @@ const GAP_PHASE: Readonly<Record<string, RepairPhase>> = {
206
240
  missing_scenario: "scenario_endpoints",
207
241
  infeasible_scenario: "scenario_endpoints",
208
242
  unknown_scenario_feasibility: "scenario_endpoints",
243
+ dimension_unrepresented: "dimension_grounding",
244
+ happy_path_only: "negative_scenarios",
245
+ uncovered_case: "case_scenarios",
209
246
  missing_scenario_test: "scenario_tests",
210
247
  proof_snapshot_unavailable: "snapshot_runtime",
211
248
  missing_passing_e2e: "proof_evidence",
@@ -235,6 +272,9 @@ const GAP_STAGE: Readonly<Record<string, string>> = {
235
272
  missing_scenario: "scenarios",
236
273
  infeasible_scenario: "scenarios",
237
274
  unknown_scenario_feasibility: "scenarios",
275
+ dimension_unrepresented: "dimension",
276
+ happy_path_only: "negative_path",
277
+ uncovered_case: "cases",
238
278
  missing_scenario_test: "scenario_tests",
239
279
  missing_passing_e2e: "passing_e2e",
240
280
  missing_proof_receipt: "passing_e2e",
@@ -255,6 +295,9 @@ const PROOF_STAGE_KEY: Readonly<Record<string, string>> = {
255
295
  contradictions: "contradictions",
256
296
  scenarios: "scenarios",
257
297
  scenario_tests: "scenarioTests",
298
+ negative_path: "negativePath",
299
+ dimension: "dimension",
300
+ cases: "cases",
258
301
  passing_e2e: "passingE2e",
259
302
  executable_symbols: "executableSymbols",
260
303
  production_symbols: "productionSymbols",
@@ -268,6 +311,34 @@ export type RepairPlanRepair = {
268
311
  readonly proofStage: string;
269
312
  readonly action: string;
270
313
  readonly evidence: Readonly<Record<string, unknown>>;
314
+ /** Draft scenarios for an uncovered_case repair, one per uncovered case. */
315
+ readonly caseSkeletons?: readonly CaseSkeleton[];
316
+ };
317
+
318
+ type SkeletonEntity = {
319
+ readonly type: "fact" | "scenario";
320
+ readonly id: string;
321
+ readonly properties: Readonly<Record<string, unknown>>;
322
+ readonly relationships: readonly {
323
+ readonly type: string;
324
+ readonly from: string;
325
+ readonly to: string;
326
+ }[];
327
+ };
328
+
329
+ // implements REQ-kibi-case-coverage
330
+ /**
331
+ * An upsert-ready draft scenario for one uncovered case: the fact it assumes
332
+ * (the case's value class) and the scenario that expects the predicted
333
+ * outcome. Drafts never count as coverage, so writing one changes no gap
334
+ * until it is verified and made active. placeholder is true when the value
335
+ * must be chosen by the author (a value outside a set).
336
+ */
337
+ export type CaseSkeleton = {
338
+ readonly caseId: string;
339
+ readonly constraint: string;
340
+ readonly placeholder: boolean;
341
+ readonly entities: readonly [SkeletonEntity, SkeletonEntity];
271
342
  };
272
343
 
273
344
  export type RepairPlanBatch = {
@@ -379,6 +450,82 @@ function stageEvidence(
379
450
  return isRecord(stable) ? stable : {};
380
451
  }
381
452
 
453
+ const VALUE_FIELD: Readonly<Record<string, string>> = {
454
+ int: "value_int",
455
+ number: "value_number",
456
+ set: "value_set",
457
+ string: "value_string",
458
+ };
459
+
460
+ function caseSkeleton(
461
+ requirementId: string,
462
+ row: Readonly<Record<string, unknown>>,
463
+ ): CaseSkeleton | undefined {
464
+ const caseId = typeof row.caseId === "string" ? row.caseId : undefined;
465
+ const assumption = isRecord(row.assumption) ? row.assumption : undefined;
466
+ const valueType =
467
+ typeof assumption?.value_type === "string" ? assumption.value_type : "";
468
+ const valueField = VALUE_FIELD[valueType];
469
+ if (caseId === undefined || assumption === undefined || !valueField)
470
+ return undefined;
471
+ const constraint = typeof row.constraint === "string" ? row.constraint : "";
472
+ const property =
473
+ typeof assumption.property_key === "string" ? assumption.property_key : "";
474
+ const expects = row.predicted === "rejection" ? "rejection" : "success";
475
+ const placeholder = JSON.stringify(assumption.value).includes("<");
476
+ const suffix = caseId.replace(/^CASE-/, "");
477
+ const factId = `FACT-CASE-${suffix}`;
478
+ const scenarioId = `SCEN-CASE-${suffix}`;
479
+ return {
480
+ caseId,
481
+ constraint,
482
+ placeholder,
483
+ entities: [
484
+ {
485
+ type: "fact",
486
+ id: factId,
487
+ properties: {
488
+ title: `${property} ${constraint}`,
489
+ status: "active",
490
+ fact_kind: "property_value",
491
+ subject_key: assumption.subject_key,
492
+ property_key: property,
493
+ operator: assumption.operator,
494
+ value_type: valueType,
495
+ [valueField]: assumption.value,
496
+ },
497
+ relationships: [],
498
+ },
499
+ {
500
+ type: "scenario",
501
+ id: scenarioId,
502
+ properties: {
503
+ title: `${property} ${constraint} is ${expects === "rejection" ? "refused" : "accepted"}`,
504
+ status: "draft",
505
+ expects,
506
+ },
507
+ relationships: [
508
+ { type: "assumes", from: scenarioId, to: factId },
509
+ { type: "specified_by", from: requirementId, to: scenarioId },
510
+ ],
511
+ },
512
+ ],
513
+ };
514
+ }
515
+
516
+ function caseSkeletons(
517
+ requirementId: string,
518
+ evidence: Readonly<Record<string, unknown>>,
519
+ ): CaseSkeleton[] {
520
+ const rows = Array.isArray(evidence.uncoveredCases)
521
+ ? evidence.uncoveredCases.filter(isRecord)
522
+ : [];
523
+ return rows.flatMap((row) => {
524
+ const skeleton = caseSkeleton(requirementId, row);
525
+ return skeleton === undefined ? [] : [skeleton];
526
+ });
527
+ }
528
+
382
529
  function batchId(requirementId: string, phase: RepairPhase): string {
383
530
  const order = String(REPAIR_PHASES[phase].order).padStart(3, "0");
384
531
  const safeRequirementId = requirementId.replace(/[^A-Za-z0-9_-]/g, "-");
@@ -408,6 +555,7 @@ function buildRequirementBatches(
408
555
  typeof sourceRepair?.stage === "string"
409
556
  ? sourceRepair.stage
410
557
  : (GAP_STAGE[gap] ?? "manual_review");
558
+ const evidence = stageEvidence(row, proofStage);
411
559
  const repair: RepairPlanRepair = {
412
560
  requirementId,
413
561
  gap,
@@ -417,7 +565,10 @@ function buildRequirementBatches(
417
565
  ),
418
566
  proofStage,
419
567
  action: actionForRepair(sourceRepair, phase),
420
- evidence: stageEvidence(row, proofStage),
568
+ evidence,
569
+ ...(gap === "uncovered_case"
570
+ ? { caseSkeletons: caseSkeletons(requirementId, evidence) }
571
+ : {}),
421
572
  };
422
573
  const phaseRepairs = byPhase.get(phase) ?? [];
423
574
  phaseRepairs.push(repair);
@@ -728,7 +728,7 @@ export function schema6ReviewActionsFromCheck(
728
728
  safety: "review",
729
729
  invocation: {
730
730
  kind: "review",
731
- instruction: `${total} agent-authored requirement(s) have no human approval${total > listed.length ? `; these ${listed.length} come first` : ""}: ${listed.join(", ")}. Have a person read each one; when it states the intended behavior, kb_upsert it with origin.approved_by set to the reviewer (keep kind: agent and the rest of origin). Rerun kb_check for the next page.`,
731
+ instruction: `${total} agent-authored requirement(s) have no human approval${total > listed.length ? `; these ${listed.length} come first` : ""}: ${listed.join(", ")}. Have a person read each one; when it states the intended behavior, kb_upsert it with origin.approved_by set to the reviewer (keep kind: agent and the rest of origin), in the same change and pull request as the requirement, not a separate one. Rerun kb_check for the next page.`,
732
732
  },
733
733
  affectedEntityIds: listed,
734
734
  affectedFiles: [...new Set(agentRequirementFiles)].sort(),