@stalfh233/omc-cli 0.4.102 → 0.5.1

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 (225) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +359 -359
  3. package/coverage/m1-coverage-manifest-v1.json +1414 -1414
  4. package/dist/approval-flow.js +0 -0
  5. package/dist/approval-token.js +102 -0
  6. package/dist/args.js +0 -0
  7. package/dist/artifacts.js +0 -0
  8. package/dist/batch-call.js +0 -0
  9. package/dist/bench/call-face-eval.js +0 -0
  10. package/dist/bench/context-attribution.js +0 -0
  11. package/dist/bench/discovery-cost-eval.js +2 -0
  12. package/dist/bench/driver.js +0 -0
  13. package/dist/bench/evals.js +0 -0
  14. package/dist/bench/fake-http-server.js +65 -0
  15. package/dist/bench/instrument.js +0 -0
  16. package/dist/bench/intent-face-eval.js +40 -40
  17. package/dist/bench/run.js +0 -0
  18. package/dist/bench/scenario.js +0 -0
  19. package/dist/bench/types.js +0 -0
  20. package/dist/bi-wire.js +0 -0
  21. package/dist/bizservice-config.js +0 -0
  22. package/dist/call.js +0 -0
  23. package/dist/capability-absences.js +0 -0
  24. package/dist/capability-overview.js +0 -0
  25. package/dist/capability-shape.js +0 -0
  26. package/dist/cli-contract.js +38 -1
  27. package/dist/cli-output.js +8 -2
  28. package/dist/cli.js +147 -5
  29. package/dist/closest-match.js +0 -0
  30. package/dist/code-rules.js +0 -0
  31. package/dist/command-transport.js +0 -0
  32. package/dist/compound-writes.js +0 -0
  33. package/dist/compute.js +0 -0
  34. package/dist/config-store.js +0 -0
  35. package/dist/context.js +0 -0
  36. package/dist/contract-consistency.js +0 -0
  37. package/dist/contract-resources.js +0 -0
  38. package/dist/coverage-consistency.js +0 -0
  39. package/dist/coverage-registry.js +0 -0
  40. package/dist/coverage.js +0 -0
  41. package/dist/data-list-filter.js +0 -0
  42. package/dist/describe-cache.js +0 -0
  43. package/dist/discovery.js +0 -0
  44. package/dist/endpoints.js +0 -0
  45. package/dist/environment-policy.js +0 -0
  46. package/dist/execution-metadata.js +0 -0
  47. package/dist/fake/app.js +45 -0
  48. package/dist/fake/b2-registration.js +594 -0
  49. package/dist/fake/businessrule.js +242 -0
  50. package/dist/fake/datarule.js +72 -0
  51. package/dist/fake/dictionary.js +108 -0
  52. package/dist/fake/environment.js +30 -0
  53. package/dist/fake/field.js +167 -0
  54. package/dist/fake/form.js +101 -0
  55. package/dist/fake/index.js +121 -0
  56. package/dist/fake/list-view.js +289 -0
  57. package/dist/fake/model.js +136 -0
  58. package/dist/fake/online-js.js +18 -0
  59. package/dist/fake/report.js +253 -0
  60. package/dist/fake/routes.js +47 -0
  61. package/dist/fake/rule-lifecycle.js +37 -0
  62. package/dist/fake/runtime-data.js +367 -0
  63. package/dist/fake/state.js +67 -0
  64. package/dist/fake/workflow.js +364 -0
  65. package/dist/field-change.js +86 -2
  66. package/dist/field-families.js +59 -9
  67. package/dist/form-default.js +0 -0
  68. package/dist/form-layout.js +0 -0
  69. package/dist/form-support.js +0 -0
  70. package/dist/formula-lint.js +0 -0
  71. package/dist/goal-routes.js +16 -16
  72. package/dist/governed-execution.js +0 -0
  73. package/dist/graph.js +734 -0
  74. package/dist/human-summary.js +0 -0
  75. package/dist/identity.js +0 -0
  76. package/dist/intent/baseline.js +0 -0
  77. package/dist/intent/capabilities/bizservice.js +0 -0
  78. package/dist/intent/capabilities/businessrule.js +0 -0
  79. package/dist/intent/capabilities/datarule.js +0 -0
  80. package/dist/intent/capabilities/field.js +0 -0
  81. package/dist/intent/capabilities/form.js +0 -0
  82. package/dist/intent/capabilities/index.js +0 -0
  83. package/dist/intent/capabilities/listview.js +0 -0
  84. package/dist/intent/capabilities/model.js +0 -0
  85. package/dist/intent/capabilities/onlinejs.js +0 -0
  86. package/dist/intent/capabilities/report.js +0 -0
  87. package/dist/intent/capabilities/workflow.js +0 -0
  88. package/dist/intent/capability.js +0 -0
  89. package/dist/intent/cli.js +0 -0
  90. package/dist/intent/compare.js +0 -0
  91. package/dist/intent/compiler.js +0 -0
  92. package/dist/intent/dsl.js +0 -0
  93. package/dist/intent/plan-apply.js +0 -0
  94. package/dist/intent/plan-file.js +0 -0
  95. package/dist/intent/readback.js +0 -0
  96. package/dist/intent/schema.js +0 -0
  97. package/dist/intent/validation.js +0 -0
  98. package/dist/intent/yaml.js +0 -0
  99. package/dist/journal.js +0 -0
  100. package/dist/json-column.js +0 -0
  101. package/dist/lanes/app-contract.js +0 -0
  102. package/dist/lanes/app-coverage.js +0 -0
  103. package/dist/lanes/app.js +0 -0
  104. package/dist/lanes/apply-changes.js +0 -0
  105. package/dist/lanes/b2-registration-contract.js +0 -0
  106. package/dist/lanes/b2-registration-coverage.js +0 -0
  107. package/dist/lanes/b2-registration.js +0 -0
  108. package/dist/lanes/businessrule-contract.js +0 -0
  109. package/dist/lanes/businessrule-coverage.js +0 -0
  110. package/dist/lanes/businessrule.js +7 -1
  111. package/dist/lanes/contract-support.js +0 -0
  112. package/dist/lanes/coverage-declaration.js +0 -0
  113. package/dist/lanes/custom-page.js +0 -0
  114. package/dist/lanes/datarule-contract.js +0 -0
  115. package/dist/lanes/datarule-coverage.js +0 -0
  116. package/dist/lanes/datarule-protocol.js +0 -0
  117. package/dist/lanes/datarule.js +0 -0
  118. package/dist/lanes/dictionary-contract.js +0 -0
  119. package/dist/lanes/dictionary-coverage.js +0 -0
  120. package/dist/lanes/dictionary.js +0 -0
  121. package/dist/lanes/environment-contract.js +0 -0
  122. package/dist/lanes/environment-coverage.js +0 -0
  123. package/dist/lanes/environment.js +0 -0
  124. package/dist/lanes/field-contract.js +0 -0
  125. package/dist/lanes/field-coverage.js +0 -0
  126. package/dist/lanes/field.js +6 -1
  127. package/dist/lanes/folder-contract.js +0 -0
  128. package/dist/lanes/folder-coverage.js +0 -0
  129. package/dist/lanes/folder.js +0 -0
  130. package/dist/lanes/form-contract.js +0 -0
  131. package/dist/lanes/form-coverage.js +0 -0
  132. package/dist/lanes/form.js +0 -0
  133. package/dist/lanes/lane-ids.js +0 -0
  134. package/dist/lanes/list-view-contract.js +0 -0
  135. package/dist/lanes/list-view-coverage.js +0 -0
  136. package/dist/lanes/list-view-shapes.js +0 -0
  137. package/dist/lanes/list-view.js +0 -0
  138. package/dist/lanes/meta-contract.js +0 -0
  139. package/dist/lanes/meta.js +6 -0
  140. package/dist/lanes/model-contract.js +0 -0
  141. package/dist/lanes/model-coverage.js +0 -0
  142. package/dist/lanes/model.js +4 -2
  143. package/dist/lanes/online-js-contract.js +0 -0
  144. package/dist/lanes/online-js-coverage.js +0 -0
  145. package/dist/lanes/online-js.js +0 -0
  146. package/dist/lanes/org-tree.js +0 -0
  147. package/dist/lanes/organization.js +0 -0
  148. package/dist/lanes/report-contract.js +0 -0
  149. package/dist/lanes/report-coverage.js +0 -0
  150. package/dist/lanes/report.js +0 -0
  151. package/dist/lanes/rule-graph.js +0 -0
  152. package/dist/lanes/rule-lifecycle-contract.js +0 -0
  153. package/dist/lanes/rule-lifecycle-coverage.js +0 -0
  154. package/dist/lanes/rule-lifecycle.js +0 -0
  155. package/dist/lanes/runtime-data-contract.js +0 -0
  156. package/dist/lanes/runtime-data-coverage.js +0 -0
  157. package/dist/lanes/runtime-data.js +0 -0
  158. package/dist/lanes/workflow-contract.js +0 -0
  159. package/dist/lanes/workflow-coverage.js +0 -0
  160. package/dist/lanes/workflow.js +0 -0
  161. package/dist/model-purge.js +0 -0
  162. package/dist/model-scaffold.js +0 -0
  163. package/dist/omc-core-lazy.js +0 -0
  164. package/dist/online-js-layout.js +0 -0
  165. package/dist/online-js-source.js +28 -28
  166. package/dist/package-tool.js +0 -0
  167. package/dist/package.js +0 -0
  168. package/dist/path-projection.js +0 -0
  169. package/dist/plan.js +0 -0
  170. package/dist/read.js +0 -0
  171. package/dist/recipes/b2-registration.js +0 -0
  172. package/dist/recipes/businessrule.js +0 -0
  173. package/dist/recipes/datarule.js +0 -0
  174. package/dist/recipes/dictionary.js +0 -0
  175. package/dist/recipes/field.js +55 -1
  176. package/dist/recipes/form.js +0 -0
  177. package/dist/recipes/list-view.js +0 -0
  178. package/dist/recipes/model.js +0 -0
  179. package/dist/recipes/online-js.js +0 -0
  180. package/dist/recipes/runtime-data.js +0 -0
  181. package/dist/recipes/shared.js +0 -0
  182. package/dist/recipes/workflow.js +0 -0
  183. package/dist/redact.js +0 -0
  184. package/dist/refusal-guidance.js +0 -0
  185. package/dist/rollback-spec.js +0 -0
  186. package/dist/rollback.js +0 -0
  187. package/dist/rule-support.js +0 -0
  188. package/dist/rule-when.js +0 -0
  189. package/dist/runtime-support.js +0 -0
  190. package/dist/script.js +0 -0
  191. package/dist/server.js +92 -0
  192. package/dist/service-doctor.js +0 -0
  193. package/dist/session-manager.js +0 -0
  194. package/dist/session.js +12 -0
  195. package/dist/skills.js +12 -2
  196. package/dist/sqlite-warning-filter.js +0 -0
  197. package/dist/support.js +0 -0
  198. package/dist/token-cache.js +0 -0
  199. package/dist/tool-types.js +0 -0
  200. package/dist/tools.js +0 -0
  201. package/dist/usage-log.js +0 -0
  202. package/dist/wire.js +0 -0
  203. package/dist/workflow-support.js +0 -0
  204. package/dist/write-lease.js +0 -0
  205. package/dist/write-lock.js +0 -0
  206. package/dist/write.js +0 -0
  207. package/dist/zip.js +0 -0
  208. package/docs/images/omc-nixi/README.md +41 -41
  209. package/docs/omc-introduction.md +851 -851
  210. package/docs/tool-surface-map.md +42 -42
  211. package/package.json +7 -5
  212. package/skills/omc-acceptance-criteria.md +50 -50
  213. package/skills/omc-business-configuration.md +334 -334
  214. package/skills/omc-capabilities.md +214 -214
  215. package/skills/omc-capability-scouting.md +55 -55
  216. package/skills/omc-config-draft-review.md +175 -175
  217. package/skills/omc-five-piece-flow.md +35 -35
  218. package/skills/omc-glossary.md +86 -86
  219. package/skills/omc-refusals.md +127 -127
  220. package/skills/omc-requirement-analysis.md +161 -161
  221. package/skills/omc-requirement-vocabulary.md +61 -61
  222. package/skills/omc-start-here.md +106 -106
  223. package/skills/omc-tool-selection.md +119 -119
  224. package/skills/omc-write-hazards.md +200 -199
  225. package/dist/lanes/organization-coverage.js +0 -27
package/dist/graph.js ADDED
@@ -0,0 +1,734 @@
1
+ import { createRequire } from "node:module";
2
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { joinConfigRoot } from "./command-transport.js";
5
+ import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
6
+ import { ENDPOINTS } from "./endpoints.js";
7
+ import { flattenTreeRows, resolveApplicationCode, sha256 } from "./support.js";
8
+ import { wireCall, wireRows } from "./wire.js";
9
+ /**
10
+ * `node:sqlite` must stay OUT of the CLI's startup module graph: a static
11
+ * import emits the ExperimentalWarning during ESM linking, before any user
12
+ * code (including the stderr filter cli.ts installs as its first import) can
13
+ * run — that is why omc-core is lazily loaded too (sqlite-warning-filter.ts /
14
+ * omc-core-lazy.ts). Load it on first use, inside a command.
15
+ */
16
+ let sqliteModule;
17
+ function nodeSqlite() {
18
+ if (sqliteModule === undefined)
19
+ sqliteModule = createRequire(import.meta.url)("node:sqlite");
20
+ return sqliteModule;
21
+ }
22
+ /**
23
+ * ADR-0008 — the local derived rule-graph index.
24
+ *
25
+ * `rule.references` treats the r23 enumeration storm (171 rule.get calls) but
26
+ * re-scans the whole platform on every invocation and swallows per-model read
27
+ * failures, so a repeat question pays full wire cost again and its zero-hit
28
+ * answer can be a silent false negative. This module materializes the same
29
+ * walk ONCE into a local SQLite index:
30
+ *
31
+ * - scope: current-effective business rule graphs only (model.list → rule.list
32
+ * rows → businessRuleGet per row id — the exact wire reads rule.references
33
+ * makes; rows from the list face are current, so no stale-row double-read);
34
+ * - edges are api-fact only, extracted with rule.references' matching
35
+ * semantics (service/model/field — see extractReferenceEdges);
36
+ * - fail-closed completeness: every swallowed read/parse failure is a recorded
37
+ * gap; a zero-hit answer over a gapped index never claims "no references";
38
+ * - derived locator, never authority: results are candidate pointers; plan /
39
+ * apply / readback keep reading the live baseline;
40
+ * - no raw payloads persist: whitelisted rule columns + graph sha256 + edges;
41
+ * - identity-bound: meta carries the canonical environment identity
42
+ * fingerprint (which includes baseUrl/enterpriseId/applicationCode by
43
+ * construction — it is NOT a config-content fingerprint and never certifies
44
+ * "config unchanged"; freshness is complete|partial|unknown only);
45
+ * - each pull rebuilds wholesale into a staging DB and publishes atomically;
46
+ * staleness is handled by reporting pulledAt + identity, refreshed by an
47
+ * explicit re-pull (no incremental watermarks, no write-back hooks).
48
+ */
49
+ const GRAPH_SCHEMA_VERSION = "4";
50
+ const STALE_LOCK_MS = 15 * 60 * 1000;
51
+ export const DEFAULT_PULL_CONCURRENCY = 16;
52
+ export const MAX_PULL_CONCURRENCY = 32;
53
+ export class GraphIndexAbsentError extends Error {
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "GraphIndexAbsentError";
57
+ }
58
+ }
59
+ export class GraphPullLockHeldError extends Error {
60
+ constructor(message) {
61
+ super(message);
62
+ this.name = "GraphPullLockHeldError";
63
+ }
64
+ }
65
+ export function graphDbPath() {
66
+ return join(joinConfigRoot(), ".omc", "graph", "graph.db");
67
+ }
68
+ /**
69
+ * Extract api-fact reference edges from one rule's stored node graph. The
70
+ * matching mirrors `rule.references` exactly (businessrule.ts) so the index and
71
+ * the live scan answer the same question; anything rule.references cannot see
72
+ * (condition leaves, datarules — its v1 boundary) is equally invisible here.
73
+ */
74
+ export function extractReferenceEdges(schemaCode, ruleCode, nodes) {
75
+ const edges = [];
76
+ const push = (kind, code, node, via) => {
77
+ if (code === "")
78
+ return;
79
+ edges.push({
80
+ kind,
81
+ code,
82
+ schemaCode,
83
+ ruleCode,
84
+ nodeCode: String(node.nodeCode ?? ""),
85
+ nodeType: String(node.nodeType ?? ""),
86
+ via,
87
+ });
88
+ };
89
+ for (const node of nodes) {
90
+ const mapping = node.methodMapping;
91
+ if (node.nodeType === "BIZ_ACTION" && mapping) {
92
+ push("service", String(mapping.serviceCode ?? ""), node, "methodMapping.serviceCode");
93
+ }
94
+ if (["CREATE", "UPDATE", "DELETE"].includes(String(node.nodeType ?? ""))) {
95
+ push("model", String(node.targetObjectCode ?? ""), node, "targetObjectCode");
96
+ if (Array.isArray(node.dataActions))
97
+ for (const action of node.dataActions)
98
+ push("field", String(action.targetDataItem ?? ""), node, "dataActions.targetDataItem");
99
+ }
100
+ if (mapping) {
101
+ push("model", String(mapping.schemaCode ?? ""), node, "methodMapping.schemaCode");
102
+ for (const [key, label] of [["inputMappings", "inputMappings.bizCode"], ["outputMappings", "outputMappings.bizCode"]]) {
103
+ if (Array.isArray(mapping[key]))
104
+ for (const row of mapping[key])
105
+ if (String(row.bizCode ?? "") !== "" && (row.codeType === undefined || row.codeType === 1))
106
+ push("field", String(row.bizCode), node, label);
107
+ }
108
+ }
109
+ }
110
+ return edges;
111
+ }
112
+ /**
113
+ * datarule face (phase 2): `dataRuleList` is one call per model and the LIST
114
+ * rows already carry the target fields (`propertyCodes`) and the stored
115
+ * `options` (formulas/conditions with `{field}` tokens) — no per-rule get.
116
+ * Target codes are api-fact field edges; `{token}` inside serialized options
117
+ * is the same class of stored-config reference (the braces are the platform's
118
+ * own field-reference syntax, cf. formulaFromEditorText).
119
+ */
120
+ export function extractDataRuleEdges(schemaCode, row) {
121
+ const targets = Array.isArray(row.propertyCodes) ? row.propertyCodes.map((code) => String(code)) : typeof row.propertyCode === "string" && row.propertyCode !== "" ? [row.propertyCode] : [];
122
+ const edges = [];
123
+ const locator = targets.join("+");
124
+ for (const code of targets)
125
+ edges.push({ kind: "field", code, schemaCode, ruleCode: "", nodeCode: locator, nodeType: "datarule", via: "datarule.targets" });
126
+ const tokens = new Set();
127
+ try {
128
+ for (const token of JSON.stringify(row.options ?? {}).matchAll(/\{([A-Za-z0-9_.-]+)\}/g))
129
+ tokens.add(token[1]);
130
+ }
131
+ catch {
132
+ // options stringify never throws; guard anyway
133
+ }
134
+ for (const code of tokens)
135
+ edges.push({ kind: "field", code, schemaCode, ruleCode: "", nodeCode: locator, nodeType: "datarule", via: "datarule.options-token" });
136
+ return edges;
137
+ }
138
+ /**
139
+ * listview face (phase 2b): `listQueryList` is one call per model but carries
140
+ * only code/name — the column/sort/condition field references live in
141
+ * `listQueryGet` per view (queryColumns/querySorts/queryConditions
142
+ * propertyCode; the same projector listview.get reads back). Views are
143
+ * always-live (no draft lifecycle), so a get is the current state.
144
+ */
145
+ export function extractListViewEdges(schemaCode, viewCode, model) {
146
+ const edges = [];
147
+ const push = (code, via) => {
148
+ if (typeof code === "string" && code !== "")
149
+ edges.push({ kind: "field", code, schemaCode, ruleCode: "", nodeCode: viewCode, nodeType: "listview", via });
150
+ };
151
+ for (const column of Array.isArray(model.queryColumns) ? model.queryColumns : [])
152
+ push(column.propertyCode, "listview.columns");
153
+ for (const sort of Array.isArray(model.querySorts) ? model.querySorts : [])
154
+ push(sort.propertyCode, "listview.sorts");
155
+ for (const condition of Array.isArray(model.queryConditions) ? model.queryConditions : [])
156
+ push(condition.propertyCode, "listview.conditions");
157
+ return edges;
158
+ }
159
+ /**
160
+ * The full pull walk, PIPELINED: one shared bounded worker pool executes both
161
+ * task kinds — per-model face reads (rule.list + dataruleList) and per-rule
162
+ * graph reads. A model face completing submits its rule reads straight into
163
+ * the pool, so wall time ≈ max(modelFaces, ruleReads) / concurrency instead of
164
+ * the phased sum. Read failures are gaps, never silent, and per FACE: a model
165
+ * whose datarule list fails still contributes its business rules (gap kind
166
+ * `datarule-list`) and vice versa (`rule-list`). Output is sorted so the
167
+ * published DB is byte-stable regardless of completion order.
168
+ */
169
+ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRENCY) {
170
+ const identity = session.environmentFingerprint();
171
+ const pulledAtIso = new Date().toISOString();
172
+ const totalStart = Date.now();
173
+ let listMs = 0;
174
+ let readMs = 0;
175
+ // Model enumeration mirrors the governed `model.list` tool exactly: the
176
+ // application tree flattened to BizModel rows. The bare `modelList` endpoint
177
+ // returns EMPTY data on a real platform (works only against the fake face)
178
+ // — using it here made the first live pull enumerate zero models and (pre-
179
+ // zero-guard) wedge the scheduler.
180
+ const applicationCode = await resolveApplicationCode(session, undefined).catch(() => "");
181
+ const schemaCodes = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data))
182
+ .filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== "")
183
+ .map((row) => String(row.code))
184
+ .sort();
185
+ const rules = [];
186
+ const edges = [];
187
+ const gaps = [];
188
+ let parseFailures = 0;
189
+ let dataruleCount = 0;
190
+ let viewCount = 0;
191
+ // --- shared scheduler: bounded starts, dynamic task submission ---
192
+ class Semaphore {
193
+ // Handoff semantics: a released slot is handed DIRECTLY to the head
194
+ // waiter (free is NOT incremented). The naive `free += 1; wake()` lets a
195
+ // task submitted in the microtask gap steal the slot, driving free
196
+ // negative — a waiter is then never woken, the pull promise never
197
+ // settles, and Node exits 0 SILENTLY when the event loop drains (no
198
+ // envelope, lock left behind). Direct handoff keeps the invariant.
199
+ free = concurrency;
200
+ waiters = [];
201
+ async acquire() {
202
+ if (this.free > 0) {
203
+ this.free -= 1;
204
+ return;
205
+ }
206
+ await new Promise((resolve) => this.waiters.push(resolve));
207
+ }
208
+ release() {
209
+ const waiter = this.waiters.shift();
210
+ if (waiter !== undefined) {
211
+ waiter();
212
+ return;
213
+ }
214
+ this.free += 1;
215
+ }
216
+ }
217
+ const semaphore = new Semaphore();
218
+ let inflight = 0;
219
+ let resolveAll = () => { };
220
+ const allDone = new Promise((resolve) => {
221
+ resolveAll = resolve;
222
+ });
223
+ const maybeDone = () => {
224
+ if (inflight === 0)
225
+ resolveAll();
226
+ };
227
+ const submit = (task) => {
228
+ inflight += 1;
229
+ void (async () => {
230
+ await semaphore.acquire();
231
+ try {
232
+ await task();
233
+ }
234
+ catch (error) {
235
+ gaps.push({ kind: "pull-task", ref: "internal", note: error instanceof Error ? error.message : String(error) });
236
+ }
237
+ finally {
238
+ semaphore.release();
239
+ inflight -= 1;
240
+ maybeDone();
241
+ }
242
+ })();
243
+ };
244
+ // --- task kinds ---
245
+ const runModelFace = async (schemaCode) => {
246
+ const faceStart = Date.now();
247
+ // Independent per-face reads: one face failing must not drop the other's
248
+ // rows (review: Promise.all here mis-filed datarule failures as rule gaps).
249
+ let rows = [];
250
+ let ruleListOk = true;
251
+ try {
252
+ rows = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
253
+ }
254
+ catch (error) {
255
+ ruleListOk = false;
256
+ gaps.push({ kind: "rule-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
257
+ }
258
+ let dataruleRows = [];
259
+ try {
260
+ dataruleRows = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
261
+ dataruleCount += dataruleRows.length;
262
+ for (const row of dataruleRows)
263
+ edges.push(...extractDataRuleEdges(schemaCode, row));
264
+ }
265
+ catch (error) {
266
+ gaps.push({ kind: "datarule-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
267
+ }
268
+ try {
269
+ const views = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode, clientType: "PC" } })).data);
270
+ for (const view of views) {
271
+ const viewCode = typeof view.code === "string" ? view.code : "";
272
+ if (viewCode === "")
273
+ continue;
274
+ submit(() => runListViewRead(schemaCode, viewCode));
275
+ }
276
+ }
277
+ catch (error) {
278
+ gaps.push({ kind: "listview-list", ref: schemaCode, note: error instanceof Error ? error.message : String(error) });
279
+ }
280
+ listMs += Date.now() - faceStart;
281
+ if (!ruleListOk)
282
+ return;
283
+ const readSubmit = Date.now();
284
+ let queued = 0;
285
+ for (const row of rows) {
286
+ const ruleCode = String(row.code ?? "");
287
+ const ruleId = row.id;
288
+ if (ruleCode === "" || typeof ruleId !== "string" || ruleId === "") {
289
+ gaps.push({ kind: "rule-row-id", ref: `${schemaCode}/${ruleCode || "?"}` });
290
+ continue;
291
+ }
292
+ queued += 1;
293
+ submit(() => runRuleRead(schemaCode, ruleCode, ruleId));
294
+ }
295
+ if (queued > 0)
296
+ readMs += Date.now() - readSubmit;
297
+ };
298
+ const runRuleRead = async (schemaCode, ruleCode, ruleId) => {
299
+ const start = Date.now();
300
+ try {
301
+ const record = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: ruleId } })).data;
302
+ if (!record) {
303
+ gaps.push({ kind: "rule-get", ref: `${schemaCode}/${ruleCode}`, note: "empty readback" });
304
+ return;
305
+ }
306
+ const nodes = decodeJsonColumn(record.node, JSON_COLUMN_NULL_COALESCE);
307
+ if (!Array.isArray(nodes)) {
308
+ parseFailures += 1;
309
+ gaps.push({ kind: "rule-graph-parse", ref: `${schemaCode}/${ruleCode}` });
310
+ return;
311
+ }
312
+ rules.push({
313
+ schemaCode,
314
+ ruleCode,
315
+ name: String(record.name ?? ruleCode),
316
+ enabled: typeof record.enabled === "boolean" ? record.enabled : null,
317
+ status: typeof record.status === "string" ? record.status : null,
318
+ defaultRule: typeof record.defaultRule === "boolean" ? record.defaultRule : null,
319
+ rowId: ruleId,
320
+ graphSha256: sha256({ nodes, routes: decodeJsonColumn(record.route, JSON_COLUMN_NULL_COALESCE) }),
321
+ });
322
+ edges.push(...extractReferenceEdges(schemaCode, ruleCode, nodes));
323
+ }
324
+ catch (error) {
325
+ gaps.push({ kind: "rule-get", ref: `${schemaCode}/${ruleCode}`, note: error instanceof Error ? error.message : String(error) });
326
+ }
327
+ finally {
328
+ readMs += Date.now() - start;
329
+ }
330
+ };
331
+ const runListViewRead = async (schemaCode, viewCode) => {
332
+ try {
333
+ const got = (await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode, code: viewCode, clientType: "PC" } })).data;
334
+ if (got === null || !got.id) {
335
+ gaps.push({ kind: "listview-get", ref: `${schemaCode}/${viewCode}`, note: "empty readback" });
336
+ return;
337
+ }
338
+ viewCount += 1;
339
+ edges.push(...extractListViewEdges(schemaCode, viewCode, got));
340
+ }
341
+ catch (error) {
342
+ gaps.push({ kind: "listview-get", ref: `${schemaCode}/${viewCode}`, note: error instanceof Error ? error.message : String(error) });
343
+ }
344
+ };
345
+ for (const schemaCode of schemaCodes)
346
+ submit(() => runModelFace(schemaCode));
347
+ // Zero-scope guard: with no models there are no tasks, so no finally ever
348
+ // calls maybeDone — awaiting allDone would wedge the pull until the event
349
+ // loop drained and Node exited 0 silently. Resolve explicitly.
350
+ maybeDone();
351
+ await allDone;
352
+ rules.sort((a, b) => (a.schemaCode === b.schemaCode ? a.ruleCode.localeCompare(b.ruleCode) : a.schemaCode.localeCompare(b.schemaCode)));
353
+ edges.sort((a, b) => a.kind === b.kind
354
+ ? a.code === b.code
355
+ ? a.schemaCode === b.schemaCode
356
+ ? a.ruleCode === b.ruleCode
357
+ ? a.nodeCode === b.nodeCode
358
+ ? a.via.localeCompare(b.via)
359
+ : a.nodeCode.localeCompare(b.nodeCode)
360
+ : a.ruleCode.localeCompare(b.ruleCode)
361
+ : a.schemaCode.localeCompare(b.schemaCode)
362
+ : a.code.localeCompare(b.code)
363
+ : a.kind.localeCompare(b.kind));
364
+ gaps.sort((a, b) => (a.kind === b.kind ? a.ref.localeCompare(b.ref) : a.kind.localeCompare(b.kind)));
365
+ return {
366
+ meta: {
367
+ schemaVersion: GRAPH_SCHEMA_VERSION,
368
+ scope: "businessrule",
369
+ identityFingerprint: identity,
370
+ environmentName: session.environmentName,
371
+ applicationCode: session.profile.applicationCode,
372
+ pulledAtIso,
373
+ },
374
+ rules,
375
+ edges,
376
+ gaps,
377
+ scanned: { models: schemaCodes.length, rules: rules.length, datarules: dataruleCount, views: viewCount, parseFailures },
378
+ timings: { listMs, readMs, totalMs: Date.now() - totalStart },
379
+ };
380
+ }
381
+ function ensureGraphDir() {
382
+ const dir = dirname(graphDbPath());
383
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
384
+ return dir;
385
+ }
386
+ /**
387
+ * Cross-process pull lock: O_EXCL create; a lock older than STALE_LOCK_MS is
388
+ * considered abandoned and taken over. This is a LOCAL index lock — pull never
389
+ * writes the environment, so it must not touch the environment write lock.
390
+ */
391
+ async function withGraphPullLock(run) {
392
+ ensureGraphDir();
393
+ const lockPath = `${graphDbPath()}.lock`;
394
+ const owner = `${process.pid} ${new Date().toISOString()}`;
395
+ let fd;
396
+ try {
397
+ try {
398
+ fd = openSync(lockPath, "wx");
399
+ writeFileSync(fd, owner);
400
+ }
401
+ catch (error) {
402
+ if (error.code !== "EEXIST")
403
+ throw error;
404
+ let stale = false;
405
+ try {
406
+ stale = Date.now() - statSync(lockPath).mtimeMs > STALE_LOCK_MS;
407
+ }
408
+ catch {
409
+ stale = false;
410
+ }
411
+ if (!stale)
412
+ throw new GraphPullLockHeldError("graph pull 已在进行(本地索引锁被占用);稍后重试,或确认无进行中拉取后删除 <配置根>/.omc/graph/graph.db.lock");
413
+ rmSync(lockPath, { force: true });
414
+ try {
415
+ fd = openSync(lockPath, "wx");
416
+ writeFileSync(fd, owner);
417
+ }
418
+ catch (error) {
419
+ // Lost the stale-takeover race to another pull — still a lock refusal
420
+ // (exit 12), never a generic thrown error (exit 10).
421
+ if (error.code === "EEXIST")
422
+ throw new GraphPullLockHeldError("graph pull 已在进行(陈旧锁接管竞争失败);稍后重试");
423
+ throw error;
424
+ }
425
+ }
426
+ return await run();
427
+ }
428
+ finally {
429
+ if (fd !== undefined)
430
+ closeSync(fd);
431
+ try {
432
+ unlinkSync(lockPath);
433
+ }
434
+ catch {
435
+ // already gone — another stale takeover won the race
436
+ }
437
+ }
438
+ }
439
+ /** Wholesalerebuild + atomic publish (staging file + rename), 0600. */
440
+ export async function publishGraphPull(session, concurrency = DEFAULT_PULL_CONCURRENCY) {
441
+ try {
442
+ return await withGraphPullLock(async () => {
443
+ const data = await pullRuleGraph(session, concurrency);
444
+ const staging = `${graphDbPath()}.staging-${process.pid}`;
445
+ rmSync(staging, { force: true });
446
+ const DatabaseSyncCtor = nodeSqlite().DatabaseSync;
447
+ const db = new DatabaseSyncCtor(staging);
448
+ try {
449
+ // The index is a rebuildable cache: crash durability is worthless here,
450
+ // and the default per-transaction journal fsync costs MINUTES on a
451
+ // network filesystem (observed 165s wall for a 2s pull on NFS /vol1 —
452
+ // D-state in folio_wait_bit_common). One transaction + memory journal
453
+ // + synchronous=OFF makes the build effectively free.
454
+ db.exec("PRAGMA journal_mode=MEMORY; PRAGMA synchronous=OFF;");
455
+ db.exec(`BEGIN;
456
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
457
+ CREATE TABLE rules (
458
+ schema_code TEXT NOT NULL,
459
+ rule_code TEXT NOT NULL,
460
+ name TEXT,
461
+ enabled INTEGER,
462
+ status TEXT,
463
+ default_rule INTEGER,
464
+ row_id TEXT NOT NULL,
465
+ graph_sha256 TEXT NOT NULL,
466
+ PRIMARY KEY (schema_code, rule_code)
467
+ );
468
+ CREATE TABLE edges (
469
+ kind TEXT NOT NULL,
470
+ code TEXT NOT NULL,
471
+ schema_code TEXT NOT NULL,
472
+ rule_code TEXT NOT NULL,
473
+ node_code TEXT,
474
+ node_type TEXT,
475
+ via TEXT NOT NULL
476
+ );
477
+ CREATE INDEX edges_lookup ON edges(kind, code);
478
+ CREATE TABLE gaps (kind TEXT NOT NULL, ref TEXT NOT NULL, note TEXT);
479
+ `);
480
+ const insertMeta = db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)");
481
+ for (const [key, value] of Object.entries(data.meta))
482
+ insertMeta.run(key, String(value));
483
+ const insertRule = db.prepare("INSERT INTO rules (schema_code, rule_code, name, enabled, status, default_rule, row_id, graph_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
484
+ for (const rule of data.rules)
485
+ insertRule.run(rule.schemaCode, rule.ruleCode, rule.name, rule.enabled === null ? null : rule.enabled ? 1 : 0, rule.status, rule.defaultRule === null ? null : rule.defaultRule ? 1 : 0, rule.rowId, rule.graphSha256);
486
+ const insertEdge = db.prepare("INSERT INTO edges (kind, code, schema_code, rule_code, node_code, node_type, via) VALUES (?, ?, ?, ?, ?, ?, ?)");
487
+ for (const edge of data.edges)
488
+ insertEdge.run(edge.kind, edge.code, edge.schemaCode, edge.ruleCode, edge.nodeCode, edge.nodeType, edge.via);
489
+ const insertGap = db.prepare("INSERT INTO gaps (kind, ref, note) VALUES (?, ?, ?)");
490
+ for (const gap of data.gaps)
491
+ insertGap.run(gap.kind, gap.ref, gap.note ?? null);
492
+ // FTS5 mirrors rules (code+name) for `graph search`; one rebuild after
493
+ // the bulk insert is cheaper than per-row triggers on a wholesale rebuild.
494
+ db.exec(`
495
+ CREATE VIRTUAL TABLE rules_fts USING fts5(rule_code, name, content='rules', content_rowid='rowid');
496
+ INSERT INTO rules_fts(rules_fts) VALUES('rebuild');
497
+ `);
498
+ db.exec("COMMIT;");
499
+ }
500
+ finally {
501
+ db.close();
502
+ }
503
+ chmodSync(staging, 0o600);
504
+ const dbBytes = statSync(staging).size;
505
+ renameSync(staging, graphDbPath());
506
+ return { ...data, dbBytes };
507
+ });
508
+ }
509
+ catch (error) {
510
+ // A failed build must not leave staging garbage next to the index.
511
+ rmSync(`${graphDbPath()}.staging-${process.pid}`, { force: true });
512
+ throw error;
513
+ }
514
+ }
515
+ function openGraphIndex() {
516
+ const dbPath = graphDbPath();
517
+ if (!existsSync(dbPath))
518
+ throw new GraphIndexAbsentError("没有本地规则图索引(<配置根>/.omc/graph/graph.db);先运行 omc graph pull --env <env> 全量重建");
519
+ let db;
520
+ try {
521
+ const DatabaseSyncCtor = nodeSqlite().DatabaseSync;
522
+ db = new DatabaseSyncCtor(dbPath, { readOnly: true });
523
+ const meta = {};
524
+ for (const row of db.prepare("SELECT key, value FROM meta").all())
525
+ meta[row.key] = row.value;
526
+ if (meta.schemaVersion !== GRAPH_SCHEMA_VERSION)
527
+ throw new GraphIndexAbsentError(`索引 schema 版本不匹配(${meta.schemaVersion} ≠ ${GRAPH_SCHEMA_VERSION});重跑 omc graph pull`);
528
+ const ruleCount = db.prepare("SELECT COUNT(*) AS n FROM rules").get().n;
529
+ const modelCount = db.prepare("SELECT COUNT(DISTINCT schema_code) AS n FROM rules").get().n;
530
+ const edgeRows = db.prepare("SELECT kind, COUNT(*) AS n FROM edges GROUP BY kind").all();
531
+ const edges = {};
532
+ for (const row of edgeRows)
533
+ edges[row.kind] = row.n;
534
+ const gapRows = db.prepare("SELECT kind, COUNT(*) AS n FROM gaps GROUP BY kind").all();
535
+ const byKind = {};
536
+ let gapCount = 0;
537
+ for (const row of gapRows) {
538
+ byKind[row.kind] = row.n;
539
+ gapCount += row.n;
540
+ }
541
+ return {
542
+ db,
543
+ summary: {
544
+ environmentName: meta.environmentName ?? "",
545
+ applicationCode: meta.applicationCode ?? "",
546
+ identityFingerprint: meta.identityFingerprint ?? "",
547
+ pulledAtIso: meta.pulledAtIso ?? "",
548
+ freshness: gapCount === 0 ? "complete" : "partial",
549
+ models: modelCount,
550
+ rules: ruleCount,
551
+ edges,
552
+ gaps: { count: gapCount, byKind },
553
+ },
554
+ };
555
+ }
556
+ catch (error) {
557
+ db?.close();
558
+ throw error instanceof GraphIndexAbsentError ? error : new GraphIndexAbsentError(`索引不可读(schema 不匹配或文件损坏):${error instanceof Error ? error.message : String(error)};重跑 omc graph pull`);
559
+ }
560
+ }
561
+ export function queryGraphReferences(kind, code) {
562
+ const { db, summary } = openGraphIndex();
563
+ try {
564
+ const rows = db
565
+ .prepare("SELECT schema_code, rule_code, node_code, node_type, via FROM edges WHERE kind = ? AND code = ? ORDER BY schema_code, rule_code, node_code, via")
566
+ .all(kind, code);
567
+ const matches = rows.map((row) => ({
568
+ schemaCode: row.schema_code,
569
+ ruleCode: row.rule_code,
570
+ nodeCode: row.node_code ?? "",
571
+ nodeType: row.node_type ?? "",
572
+ via: row.via,
573
+ }));
574
+ const gapped = summary.gaps.count > 0;
575
+ return {
576
+ kind,
577
+ code,
578
+ matches,
579
+ index: summary,
580
+ ...(matches.length === 0
581
+ ? {
582
+ note: gapped
583
+ ? "零命中,但索引存在扫描缺口——不可作为「无引用」证明;先 omc graph pull 重建再查"
584
+ : "零命中=索引覆盖范围内没有引用它(规则面:service→methodMapping.serviceCode;model→targetObjectCode/methodMapping.schemaCode;field→dataActions.targetDataItem/映射 bizCode;数据规则面:propertyCodes 目标与 options {字段} 引用;视图面:列/排序/条件的 propertyCode。不含规则条件叶子/流程绑定)",
585
+ }
586
+ : {}),
587
+ ...(gapped
588
+ ? { gapWarning: `索引有 ${summary.gaps.count} 个扫描缺口(${Object.entries(summary.gaps.byKind).map(([k, v]) => `${k}:${v}`).join(", ")});命中列表可能不完整` }
589
+ : {}),
590
+ };
591
+ }
592
+ finally {
593
+ db.close();
594
+ }
595
+ }
596
+ export function readGraphStatus() {
597
+ const { db, summary } = openGraphIndex();
598
+ try {
599
+ const gaps = db.prepare("SELECT kind, ref, note FROM gaps ORDER BY kind, ref LIMIT 50").all();
600
+ return { index: summary, dbBytes: statSync(graphDbPath()).size, gaps };
601
+ }
602
+ finally {
603
+ db.close();
604
+ }
605
+ }
606
+ /**
607
+ * Bounded FTS5 lookup over rule code + name — the "find the rule about X"
608
+ * entry point (references/impact need an exact code; search gets you the
609
+ * code). Tokens are quoted (no FTS syntax injection); the LAST token becomes
610
+ * a prefix match (`引用*`), which is also what makes CJK usable: unicode61
611
+ * stores a CJK run as one token, so a full-run query hits only by prefix.
612
+ */
613
+ export function searchGraphRules(term, limit = 20) {
614
+ const { db, summary } = openGraphIndex();
615
+ try {
616
+ const tokens = term.trim().split(/\s+/).filter((token) => token !== "");
617
+ let matches = [];
618
+ if (tokens.length > 0) {
619
+ const quoted = tokens.map((token, index) => `"${token.replaceAll('"', '""')}"${index === tokens.length - 1 ? "*" : ""}`).join(" ");
620
+ const rows = db
621
+ .prepare(`SELECT r.schema_code, r.rule_code, r.name, r.enabled, r.status
622
+ FROM rules_fts JOIN rules r ON r.rowid = rules_fts.rowid
623
+ WHERE rules_fts MATCH ? ORDER BY bm25(rules_fts), r.schema_code, r.rule_code LIMIT ?`)
624
+ .all(quoted, limit);
625
+ const refsFor = db.prepare("SELECT kind, code FROM edges WHERE schema_code = ? AND rule_code = ? ORDER BY kind, code");
626
+ matches = rows.map((row) => {
627
+ const refs = { service: [], model: [], field: [] };
628
+ for (const edge of refsFor.all(row.schema_code, row.rule_code))
629
+ if (edge.kind in refs)
630
+ refs[edge.kind].push(edge.code);
631
+ return {
632
+ schemaCode: row.schema_code,
633
+ ruleCode: row.rule_code,
634
+ name: row.name ?? "",
635
+ enabled: row.enabled === null ? null : row.enabled === 1,
636
+ status: row.status,
637
+ refs,
638
+ };
639
+ });
640
+ }
641
+ const gapped = summary.gaps.count > 0;
642
+ return {
643
+ term,
644
+ rules: matches,
645
+ index: summary,
646
+ ...(matches.length === 0
647
+ ? { note: tokens.length === 0 ? "检索词为空" : "没有命中规则(code+name 全文;引用检索用 references/impact <kind> <code>)" }
648
+ : {}),
649
+ ...(gapped ? { gapWarning: `索引有 ${summary.gaps.count} 个扫描缺口;命中列表可能不完整` } : {}),
650
+ };
651
+ }
652
+ finally {
653
+ db.close();
654
+ }
655
+ }
656
+ /**
657
+ * Reverse blast radius over the reference edges: hop 1 = rules referencing the
658
+ * given code (what `references` answers), hop 2 = everything those rules touch
659
+ * (services/models/fields). "Change field X → these rules fire, and they also
660
+ * read/write these codes — verify those too." Bounded: 50 rules, 20 codes per
661
+ * kind; the same fail-closed gap semantics as references apply.
662
+ */
663
+ export function impactGraph(kind, code) {
664
+ const { db, summary } = openGraphIndex();
665
+ try {
666
+ const hop1 = db
667
+ .prepare("SELECT DISTINCT schema_code, rule_code FROM edges WHERE kind = ? AND code = ? AND node_type NOT IN ('datarule', 'listview') ORDER BY schema_code, rule_code LIMIT 51")
668
+ .all(kind, code);
669
+ const dataruleHits = db
670
+ .prepare("SELECT DISTINCT schema_code, node_code AS targets, via FROM edges WHERE kind = ? AND code = ? AND node_type = 'datarule' ORDER BY schema_code, node_code, via LIMIT 21")
671
+ .all(kind, code);
672
+ const listViewHits = db
673
+ .prepare("SELECT DISTINCT schema_code, node_code AS view_code, via FROM edges WHERE kind = ? AND code = ? AND node_type = 'listview' ORDER BY schema_code, node_code, via LIMIT 21")
674
+ .all(kind, code);
675
+ const capped = hop1.length > 50;
676
+ const refsFor = db.prepare("SELECT kind, code, via FROM edges WHERE schema_code = ? AND rule_code = ? AND node_type <> 'datarule' ORDER BY kind, code, via");
677
+ const ruleName = db.prepare("SELECT name FROM rules WHERE schema_code = ? AND rule_code = ?");
678
+ const services = new Set();
679
+ const models = new Set();
680
+ const fields = new Set();
681
+ const rules = hop1.slice(0, 50).map((row) => {
682
+ const via = [];
683
+ const touches = { service: [], model: [], field: [] };
684
+ for (const edge of refsFor.all(row.schema_code, row.rule_code)) {
685
+ if (edge.kind === kind && edge.code === code) {
686
+ if (!via.includes(edge.via))
687
+ via.push(edge.via);
688
+ continue; // the queried code itself is not "additional blast radius"
689
+ }
690
+ const bucket = touches[edge.kind];
691
+ if (bucket && !bucket.includes(edge.code))
692
+ bucket.push(edge.code);
693
+ if (edge.kind === "service")
694
+ services.add(edge.code);
695
+ else if (edge.kind === "model")
696
+ models.add(edge.code);
697
+ else if (edge.kind === "field")
698
+ fields.add(edge.code);
699
+ }
700
+ for (const key of ["service", "model", "field"]) {
701
+ const bucket = touches[key];
702
+ if (bucket.length > 20)
703
+ touches[key] = bucket.slice(0, 20);
704
+ }
705
+ const name = ruleName.get(row.schema_code, row.rule_code)?.name ?? "";
706
+ return { schemaCode: row.schema_code, ruleCode: row.rule_code, name, matchedVia: via, touches };
707
+ });
708
+ const gapped = summary.gaps.count > 0;
709
+ return {
710
+ kind,
711
+ code,
712
+ rules,
713
+ datarules: dataruleHits.slice(0, 20).map((hit) => ({ schemaCode: hit.schema_code, targets: hit.targets ?? "", via: hit.via })),
714
+ listviews: listViewHits.slice(0, 20).map((hit) => ({ schemaCode: hit.schema_code, viewCode: hit.view_code ?? "", via: hit.via })),
715
+ totals: { rules: hop1.length, datarules: dataruleHits.length, listviews: listViewHits.length, touchedServices: services.size, touchedModels: models.size, touchedFields: fields.size },
716
+ index: summary,
717
+ ...(hop1.length === 0 && dataruleHits.length === 0 && listViewHits.length === 0
718
+ ? {
719
+ note: gapped
720
+ ? "零命中,但索引存在扫描缺口——不可作为「无影响」证明;先 omc graph pull 重建再查"
721
+ : "零命中=索引覆盖范围内没有引用它(规则面+数据规则面+视图列/排序/条件面;流程绑定暂不在索引内)",
722
+ }
723
+ : {}),
724
+ ...(capped || dataruleHits.length > 20 || listViewHits.length > 20
725
+ ? { truncated: true, note: `命中超过上限已截断(规则 50/实际 ${hop1.length},数据规则 20/实际 ${dataruleHits.length},视图 20/实际 ${listViewHits.length});完整规则清单用 references` }
726
+ : {}),
727
+ ...(gapped ? { gapWarning: `索引有 ${summary.gaps.count} 个扫描缺口(${Object.entries(summary.gaps.byKind).map(([k, v]) => `${k}:${v}`).join(", ")});影响面可能不完整` } : {}),
728
+ };
729
+ }
730
+ finally {
731
+ db.close();
732
+ }
733
+ }
734
+ //# sourceMappingURL=graph.js.map