mongo-query-normalizer 0.1.0 → 0.2.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 (242) hide show
  1. package/README.md +369 -39
  2. package/README.zh-CN.md +363 -44
  3. package/dist/ast/types.d.ts +4 -0
  4. package/dist/ast/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/normalize-context.d.ts +5 -1
  8. package/dist/normalize-context.d.ts.map +1 -1
  9. package/dist/normalize-context.js +3 -0
  10. package/dist/normalize-context.js.map +1 -1
  11. package/dist/normalize.d.ts +1 -1
  12. package/dist/normalize.d.ts.map +1 -1
  13. package/dist/normalize.js +105 -52
  14. package/dist/normalize.js.map +1 -1
  15. package/dist/options/constants.d.ts.map +1 -1
  16. package/dist/options/constants.js +7 -16
  17. package/dist/options/constants.js.map +1 -1
  18. package/dist/options/resolve.d.ts.map +1 -1
  19. package/dist/options/resolve.js +32 -1
  20. package/dist/options/resolve.js.map +1 -1
  21. package/dist/options/types.d.ts +1 -1
  22. package/dist/options/types.d.ts.map +1 -1
  23. package/dist/passes/normalize-predicate.d.ts.map +1 -1
  24. package/dist/passes/normalize-predicate.js +93 -14
  25. package/dist/passes/normalize-predicate.js.map +1 -1
  26. package/dist/predicate/analysis/detect-array-sensitive.d.ts +3 -0
  27. package/dist/predicate/analysis/detect-array-sensitive.d.ts.map +1 -0
  28. package/dist/predicate/analysis/detect-array-sensitive.js +33 -0
  29. package/dist/predicate/analysis/detect-array-sensitive.js.map +1 -0
  30. package/dist/predicate/analysis/detect-null-sensitive.d.ts +3 -0
  31. package/dist/predicate/analysis/detect-null-sensitive.d.ts.map +1 -0
  32. package/dist/predicate/analysis/detect-null-sensitive.js +24 -0
  33. package/dist/predicate/analysis/detect-null-sensitive.js.map +1 -0
  34. package/dist/predicate/analysis/detect-opaque-mix.d.ts +3 -0
  35. package/dist/predicate/analysis/detect-opaque-mix.d.ts.map +1 -0
  36. package/dist/predicate/analysis/detect-opaque-mix.js +9 -0
  37. package/dist/predicate/analysis/detect-opaque-mix.js.map +1 -0
  38. package/dist/predicate/analysis/detect-path-conflict-risk.d.ts +3 -0
  39. package/dist/predicate/analysis/detect-path-conflict-risk.d.ts.map +1 -0
  40. package/dist/predicate/analysis/detect-path-conflict-risk.js +14 -0
  41. package/dist/predicate/analysis/detect-path-conflict-risk.js.map +1 -0
  42. package/dist/predicate/analysis/merge-range-bounds.d.ts +27 -0
  43. package/dist/predicate/analysis/merge-range-bounds.d.ts.map +1 -0
  44. package/dist/predicate/analysis/merge-range-bounds.js +150 -0
  45. package/dist/predicate/analysis/merge-range-bounds.js.map +1 -0
  46. package/dist/predicate/capabilities/eq/eq-eq.d.ts +3 -0
  47. package/dist/predicate/capabilities/eq/eq-eq.d.ts.map +1 -0
  48. package/dist/predicate/capabilities/eq/eq-eq.js +53 -0
  49. package/dist/predicate/capabilities/eq/eq-eq.js.map +1 -0
  50. package/dist/predicate/capabilities/eq/eq-in.d.ts +3 -0
  51. package/dist/predicate/capabilities/eq/eq-in.d.ts.map +1 -0
  52. package/dist/predicate/capabilities/eq/eq-in.js +117 -0
  53. package/dist/predicate/capabilities/eq/eq-in.js.map +1 -0
  54. package/dist/predicate/capabilities/eq/eq-ne.d.ts +3 -0
  55. package/dist/predicate/capabilities/eq/eq-ne.d.ts.map +1 -0
  56. package/dist/predicate/capabilities/eq/eq-ne.js +37 -0
  57. package/dist/predicate/capabilities/eq/eq-ne.js.map +1 -0
  58. package/dist/predicate/capabilities/eq/eq-range.d.ts +3 -0
  59. package/dist/predicate/capabilities/eq/eq-range.d.ts.map +1 -0
  60. package/dist/predicate/capabilities/eq/eq-range.js +138 -0
  61. package/dist/predicate/capabilities/eq/eq-range.js.map +1 -0
  62. package/dist/predicate/capabilities/ne/ne-ne.d.ts +3 -0
  63. package/dist/predicate/capabilities/ne/ne-ne.d.ts.map +1 -0
  64. package/dist/predicate/capabilities/ne/ne-ne.js +44 -0
  65. package/dist/predicate/capabilities/ne/ne-ne.js.map +1 -0
  66. package/dist/predicate/capabilities/nin/nin-nin.d.ts +3 -0
  67. package/dist/predicate/capabilities/nin/nin-nin.d.ts.map +1 -0
  68. package/dist/predicate/capabilities/nin/nin-nin.js +45 -0
  69. package/dist/predicate/capabilities/nin/nin-nin.js.map +1 -0
  70. package/dist/predicate/capabilities/range/range-range.d.ts +3 -0
  71. package/dist/predicate/capabilities/range/range-range.d.ts.map +1 -0
  72. package/dist/predicate/capabilities/range/range-range.js +154 -0
  73. package/dist/predicate/capabilities/range/range-range.js.map +1 -0
  74. package/dist/predicate/capabilities/shared/capability-types.d.ts +11 -0
  75. package/dist/predicate/capabilities/shared/capability-types.d.ts.map +1 -0
  76. package/dist/predicate/capabilities/shared/capability-types.js +3 -0
  77. package/dist/predicate/capabilities/shared/capability-types.js.map +1 -0
  78. package/dist/predicate/capabilities/shared/relation-context.d.ts +13 -0
  79. package/dist/predicate/capabilities/shared/relation-context.d.ts.map +1 -0
  80. package/dist/predicate/capabilities/shared/relation-context.js +3 -0
  81. package/dist/predicate/capabilities/shared/relation-context.js.map +1 -0
  82. package/dist/predicate/capabilities/shared/relation-result.d.ts +12 -0
  83. package/dist/predicate/capabilities/shared/relation-result.d.ts.map +1 -0
  84. package/dist/predicate/capabilities/shared/relation-result.js +14 -0
  85. package/dist/predicate/capabilities/shared/relation-result.js.map +1 -0
  86. package/dist/predicate/index.d.ts +12 -0
  87. package/dist/predicate/index.d.ts.map +1 -0
  88. package/dist/predicate/index.js +22 -0
  89. package/dist/predicate/index.js.map +1 -0
  90. package/dist/predicate/ir/build-field-bundle.d.ts +7 -0
  91. package/dist/predicate/ir/build-field-bundle.d.ts.map +1 -0
  92. package/dist/predicate/ir/build-field-bundle.js +88 -0
  93. package/dist/predicate/ir/build-field-bundle.js.map +1 -0
  94. package/dist/predicate/ir/compile-field-bundle.d.ts +4 -0
  95. package/dist/predicate/ir/compile-field-bundle.d.ts.map +1 -0
  96. package/dist/predicate/ir/compile-field-bundle.js +57 -0
  97. package/dist/predicate/ir/compile-field-bundle.js.map +1 -0
  98. package/dist/predicate/ir/dedupe-atoms.d.ts +6 -0
  99. package/dist/predicate/ir/dedupe-atoms.d.ts.map +1 -0
  100. package/dist/predicate/ir/dedupe-atoms.js +42 -0
  101. package/dist/predicate/ir/dedupe-atoms.js.map +1 -0
  102. package/dist/predicate/ir/field-predicate-bundle.d.ts +16 -0
  103. package/dist/predicate/ir/field-predicate-bundle.d.ts.map +1 -0
  104. package/dist/predicate/ir/field-predicate-bundle.js +3 -0
  105. package/dist/predicate/ir/field-predicate-bundle.js.map +1 -0
  106. package/dist/predicate/ir/predicate-atom.d.ts +33 -0
  107. package/dist/predicate/ir/predicate-atom.d.ts.map +1 -0
  108. package/dist/predicate/ir/predicate-atom.js +3 -0
  109. package/dist/predicate/ir/predicate-atom.js.map +1 -0
  110. package/dist/predicate/local-normalize-result.d.ts +17 -0
  111. package/dist/predicate/local-normalize-result.d.ts.map +1 -0
  112. package/dist/predicate/local-normalize-result.js +3 -0
  113. package/dist/predicate/local-normalize-result.js.map +1 -0
  114. package/dist/predicate/meta/collect-predicate-meta.d.ts +16 -0
  115. package/dist/predicate/meta/collect-predicate-meta.d.ts.map +1 -0
  116. package/dist/predicate/meta/collect-predicate-meta.js +13 -0
  117. package/dist/predicate/meta/collect-predicate-meta.js.map +1 -0
  118. package/dist/predicate/normalize-field-predicate-bundle.d.ts +18 -0
  119. package/dist/predicate/normalize-field-predicate-bundle.d.ts.map +1 -0
  120. package/dist/predicate/normalize-field-predicate-bundle.js +126 -0
  121. package/dist/predicate/normalize-field-predicate-bundle.js.map +1 -0
  122. package/dist/predicate/normalize-predicate.d.ts +5 -0
  123. package/dist/predicate/normalize-predicate.d.ts.map +1 -0
  124. package/dist/predicate/normalize-predicate.js +10 -0
  125. package/dist/predicate/normalize-predicate.js.map +1 -0
  126. package/dist/predicate/planner/capability-selector.d.ts +8 -0
  127. package/dist/predicate/planner/capability-selector.d.ts.map +1 -0
  128. package/dist/predicate/planner/capability-selector.js +23 -0
  129. package/dist/predicate/planner/capability-selector.js.map +1 -0
  130. package/dist/predicate/planner/relation-plan.d.ts +10 -0
  131. package/dist/predicate/planner/relation-plan.d.ts.map +1 -0
  132. package/dist/predicate/planner/relation-plan.js +3 -0
  133. package/dist/predicate/planner/relation-plan.js.map +1 -0
  134. package/dist/predicate/planner/relation-planner.d.ts +5 -0
  135. package/dist/predicate/planner/relation-planner.d.ts.map +1 -0
  136. package/dist/predicate/planner/relation-planner.js +62 -0
  137. package/dist/predicate/planner/relation-planner.js.map +1 -0
  138. package/dist/predicate/registry/predicate-capability-registry.d.ts +3 -0
  139. package/dist/predicate/registry/predicate-capability-registry.d.ts.map +1 -0
  140. package/dist/predicate/registry/predicate-capability-registry.js +23 -0
  141. package/dist/predicate/registry/predicate-capability-registry.js.map +1 -0
  142. package/dist/predicate/safety/predicate-safety-policy.d.ts +9 -0
  143. package/dist/predicate/safety/predicate-safety-policy.d.ts.map +1 -0
  144. package/dist/predicate/safety/predicate-safety-policy.js +11 -0
  145. package/dist/predicate/safety/predicate-safety-policy.js.map +1 -0
  146. package/dist/predicate/utils/bson-compare.d.ts +2 -0
  147. package/dist/predicate/utils/bson-compare.d.ts.map +1 -0
  148. package/dist/predicate/utils/bson-compare.js +7 -0
  149. package/dist/predicate/utils/bson-compare.js.map +1 -0
  150. package/dist/predicate/utils/intersect-in-lists.d.ts +5 -0
  151. package/dist/predicate/utils/intersect-in-lists.d.ts.map +1 -0
  152. package/dist/predicate/utils/intersect-in-lists.js +15 -0
  153. package/dist/predicate/utils/intersect-in-lists.js.map +1 -0
  154. package/dist/predicate/utils/set-ops.d.ts +3 -0
  155. package/dist/predicate/utils/set-ops.d.ts.map +1 -0
  156. package/dist/predicate/utils/set-ops.js +18 -0
  157. package/dist/predicate/utils/set-ops.js.map +1 -0
  158. package/dist/predicate/utils/value-equality.d.ts +2 -0
  159. package/dist/predicate/utils/value-equality.d.ts.map +1 -0
  160. package/dist/predicate/utils/value-equality.js +8 -0
  161. package/dist/predicate/utils/value-equality.js.map +1 -0
  162. package/dist/rules/or-common-predicate/detect-common-predicates-in-or.d.ts +6 -0
  163. package/dist/rules/or-common-predicate/detect-common-predicates-in-or.d.ts.map +1 -0
  164. package/dist/rules/or-common-predicate/detect-common-predicates-in-or.js +83 -0
  165. package/dist/rules/or-common-predicate/detect-common-predicates-in-or.js.map +1 -0
  166. package/dist/rules/shape/collapse-single-child-logical.js +1 -1
  167. package/dist/rules/shape/collapse-single-child-logical.js.map +1 -1
  168. package/dist/rules/shape/dedupe-logical-children.js +1 -1
  169. package/dist/rules/shape/dedupe-logical-children.js.map +1 -1
  170. package/dist/rules/shape/flatten-logical.js +2 -2
  171. package/dist/rules/shape/flatten-logical.js.map +1 -1
  172. package/dist/rules/shape/remove-empty-logical.js +2 -2
  173. package/dist/rules/shape/remove-empty-logical.js.map +1 -1
  174. package/dist/scope/analysis/analyze-branch-coverage.d.ts +5 -0
  175. package/dist/scope/analysis/analyze-branch-coverage.d.ts.map +1 -0
  176. package/dist/scope/analysis/analyze-branch-coverage.js +8 -0
  177. package/dist/scope/analysis/analyze-branch-coverage.js.map +1 -0
  178. package/dist/scope/analysis/analyze-branch-satisfiability.d.ts +9 -0
  179. package/dist/scope/analysis/analyze-branch-satisfiability.d.ts.map +1 -0
  180. package/dist/scope/analysis/analyze-branch-satisfiability.js +33 -0
  181. package/dist/scope/analysis/analyze-branch-satisfiability.js.map +1 -0
  182. package/dist/scope/context/build-inherited-constraints.d.ts +6 -0
  183. package/dist/scope/context/build-inherited-constraints.d.ts.map +1 -0
  184. package/dist/scope/context/build-inherited-constraints.js +119 -0
  185. package/dist/scope/context/build-inherited-constraints.js.map +1 -0
  186. package/dist/scope/context/constraint-set.d.ts +19 -0
  187. package/dist/scope/context/constraint-set.d.ts.map +1 -0
  188. package/dist/scope/context/constraint-set.js +26 -0
  189. package/dist/scope/context/constraint-set.js.map +1 -0
  190. package/dist/scope/context/merge-constraint-sources.d.ts +4 -0
  191. package/dist/scope/context/merge-constraint-sources.d.ts.map +1 -0
  192. package/dist/scope/context/merge-constraint-sources.js +28 -0
  193. package/dist/scope/context/merge-constraint-sources.js.map +1 -0
  194. package/dist/scope/index.d.ts +8 -0
  195. package/dist/scope/index.d.ts.map +1 -0
  196. package/dist/scope/index.js +19 -0
  197. package/dist/scope/index.js.map +1 -0
  198. package/dist/scope/meta/collect-scope-meta.d.ts +5 -0
  199. package/dist/scope/meta/collect-scope-meta.d.ts.map +1 -0
  200. package/dist/scope/meta/collect-scope-meta.js +7 -0
  201. package/dist/scope/meta/collect-scope-meta.js.map +1 -0
  202. package/dist/scope/normalize-scope.d.ts +4 -0
  203. package/dist/scope/normalize-scope.d.ts.map +1 -0
  204. package/dist/scope/normalize-scope.js +149 -0
  205. package/dist/scope/normalize-scope.js.map +1 -0
  206. package/dist/scope/planner/scope-plan.d.ts +6 -0
  207. package/dist/scope/planner/scope-plan.d.ts.map +1 -0
  208. package/dist/scope/planner/scope-plan.js +9 -0
  209. package/dist/scope/planner/scope-plan.js.map +1 -0
  210. package/dist/scope/planner/scope-rewrite-planner.d.ts +4 -0
  211. package/dist/scope/planner/scope-rewrite-planner.d.ts.map +1 -0
  212. package/dist/scope/planner/scope-rewrite-planner.js +8 -0
  213. package/dist/scope/planner/scope-rewrite-planner.js.map +1 -0
  214. package/dist/scope/propagation/create-branch-local-bundle.d.ts +5 -0
  215. package/dist/scope/propagation/create-branch-local-bundle.d.ts.map +1 -0
  216. package/dist/scope/propagation/create-branch-local-bundle.js +16 -0
  217. package/dist/scope/propagation/create-branch-local-bundle.js.map +1 -0
  218. package/dist/scope/propagation/propagate-constraints-to-children.d.ts +6 -0
  219. package/dist/scope/propagation/propagate-constraints-to-children.d.ts.map +1 -0
  220. package/dist/scope/propagation/propagate-constraints-to-children.js +9 -0
  221. package/dist/scope/propagation/propagate-constraints-to-children.js.map +1 -0
  222. package/dist/scope/rewrite/collapse-single-branch.d.ts +5 -0
  223. package/dist/scope/rewrite/collapse-single-branch.d.ts.map +1 -0
  224. package/dist/scope/rewrite/collapse-single-branch.js +28 -0
  225. package/dist/scope/rewrite/collapse-single-branch.js.map +1 -0
  226. package/dist/scope/rewrite/prune-impossible-branches.d.ts +6 -0
  227. package/dist/scope/rewrite/prune-impossible-branches.d.ts.map +1 -0
  228. package/dist/scope/rewrite/prune-impossible-branches.js +32 -0
  229. package/dist/scope/rewrite/prune-impossible-branches.js.map +1 -0
  230. package/dist/scope/rewrite/remove-covered-local-constraints.d.ts +6 -0
  231. package/dist/scope/rewrite/remove-covered-local-constraints.d.ts.map +1 -0
  232. package/dist/scope/rewrite/remove-covered-local-constraints.js +76 -0
  233. package/dist/scope/rewrite/remove-covered-local-constraints.js.map +1 -0
  234. package/dist/scope/safety/scope-safety-policy.d.ts +10 -0
  235. package/dist/scope/safety/scope-safety-policy.d.ts.map +1 -0
  236. package/dist/scope/safety/scope-safety-policy.js +12 -0
  237. package/dist/scope/safety/scope-safety-policy.js.map +1 -0
  238. package/dist/types.d.ts +70 -2
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +2 -1
  241. package/dist/types.js.map +1 -1
  242. package/package.json +6 -4
package/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  **English** | [中文](README.zh-CN.md)
4
4
 
5
- An **observable, level-based** normalizer for MongoDB query objects. It stabilizes query **shape** at the conservative default, offers **preview** higher levels for analysis and experiments, and returns **predictable** output plus **metadata**—not a MongoDB planner optimizer.
5
+ An **observable, level-based** normalizer for MongoDB query objects. It stabilizes query **shape** at the conservative default, and adds **`predicate`** and **`scope`** levels with **documented, test-backed contracts** (see [SPEC.md](SPEC.md) and [docs/normalization-matrix.md](docs/normalization-matrix.md) / [中文](docs/normalization-matrix.zh-CN.md)). It returns **predictable** output plus **metadata**—not a MongoDB planner optimizer.
6
6
 
7
- > **v0.1.0 — production scope:** For **general production** traffic, use **`shape` only**—it is the **sole** level we recommend for that role in this release. **`predicate`**, **`logical`**, and **`experimental`** are **preview / experimental** surfaces; they fit **offline analysis**, **replay testing**, **semantic validation**, and **targeted experiments** better than a blanket default for all online requests.
7
+ > **Default posture:** **`shape`** is the smallest, structural-only pass and the recommended default for the widest production use. **`predicate`** and **`scope`** apply additional conservative rewrites under explicit contracts; adopt them when you need those transforms and accept their modeled-operator scope (opaque operators stay preserved).
8
+ >
9
+ > **As of `v0.2.0`:** predicate rewrites are intentionally narrowed to an explicitly validated surface (`eq.eq`, `eq.ne`, `eq.in`, `eq.range`, `range.range`). High-risk combinations (for example null-vs-missing, array-sensitive semantics, `$exists`/`$nin`, object-vs-dotted-path mixes, opaque mixes) remain conservative by design.
8
10
 
9
11
  ---
10
12
 
@@ -20,8 +22,8 @@ This library does **not** promise to make queries faster or to pick optimal inde
20
22
 
21
23
  ## Features
22
24
 
23
- - **Level-based** normalization (`shape` → `predicate` → `logical` → `experimental`)
24
- - **Conservative default**: `shape` only out of the box (the **only** level we recommend for general production in v0.1.0)
25
+ - **Level-based** normalization (`shape` → `predicate` → `scope`)
26
+ - **Conservative default**: `shape` only out of the box (lowest-risk structural pass)
25
27
  - **Observable** `meta`: changed flags, applied/skipped rules, warnings, hashes, optional stats
26
28
  - **Stable / idempotent** output when rules apply (same options)
27
29
  - **Opaque fallback** for unsupported operators (passthrough, not semantically rewritten)
@@ -51,19 +53,158 @@ console.log(result.meta);
51
53
 
52
54
  ---
53
55
 
56
+ ## Complete usage guide
57
+
58
+ ### 1) Minimal usage (recommended default)
59
+
60
+ ```ts
61
+ import { normalizeQuery } from "mongo-query-normalizer";
62
+
63
+ const { query: normalizedQuery, meta } = normalizeQuery(inputQuery);
64
+ ```
65
+
66
+ - Without `options`, default behavior is `level: "shape"`.
67
+ - Best for low-risk structural stabilization: logging, cache-key normalization, query diff alignment.
68
+
69
+ ### 2) Pick a level explicitly
70
+
71
+ ```ts
72
+ normalizeQuery(inputQuery, { level: "shape" }); // structural only (default)
73
+ normalizeQuery(inputQuery, { level: "predicate" }); // modeled predicate cleanup
74
+ normalizeQuery(inputQuery, { level: "scope" }); // scope propagation / conservative pruning
75
+ ```
76
+
77
+ - `shape`: safest structural normalization.
78
+ - `predicate`: dedupe / merge / contradiction collapse for modeled operators.
79
+ - `scope`: adds inherited-constraint propagation and conservative branch decisions on top of `predicate`.
80
+
81
+ ### 3) Full `options` example
82
+
83
+ ```ts
84
+ import { normalizeQuery } from "mongo-query-normalizer";
85
+
86
+ const result = normalizeQuery(inputQuery, {
87
+ level: "scope",
88
+ rules: {
89
+ // shape-related
90
+ flattenLogical: true,
91
+ removeEmptyLogical: true,
92
+ collapseSingleChildLogical: true,
93
+ dedupeLogicalChildren: true,
94
+ // predicate-related
95
+ dedupeSameFieldPredicates: true,
96
+ mergeComparablePredicates: true,
97
+ collapseContradictions: true,
98
+ // ordering-related
99
+ sortLogicalChildren: true,
100
+ sortFieldPredicates: true,
101
+ // scope observe-only rule (no structural hoist)
102
+ detectCommonPredicatesInOr: true,
103
+ },
104
+ safety: {
105
+ maxNormalizeDepth: 32,
106
+ maxNodeGrowthRatio: 1.5,
107
+ },
108
+ observe: {
109
+ collectWarnings: true,
110
+ collectMetrics: false,
111
+ collectPredicateTraces: false,
112
+ collectScopeTraces: false,
113
+ },
114
+ predicate: {
115
+ safetyPolicy: {
116
+ // override only fields you care about
117
+ },
118
+ },
119
+ scope: {
120
+ safetyPolicy: {
121
+ // override only fields you care about
122
+ },
123
+ },
124
+ });
125
+ ```
126
+
127
+ ### 4) Inspect resolved runtime options
128
+
129
+ ```ts
130
+ import { resolveNormalizeOptions } from "mongo-query-normalizer";
131
+
132
+ const resolvedOptions = resolveNormalizeOptions({
133
+ level: "predicate",
134
+ observe: { collectMetrics: true },
135
+ });
136
+
137
+ console.log(resolvedOptions);
138
+ ```
139
+
140
+ - Useful for debugging why a rule is enabled/disabled.
141
+ - Useful for logging a startup-time normalization config snapshot.
142
+
143
+ ### 5) Consume `query` and `meta`
144
+
145
+ ```ts
146
+ const { query: normalizedQuery, meta } = normalizeQuery(inputQuery, options);
147
+
148
+ if (meta.bailedOut) {
149
+ logger.warn({ reason: meta.bailoutReason }, "normalization bailed out");
150
+ }
151
+
152
+ if (meta.changed) {
153
+ logger.info(
154
+ {
155
+ level: meta.level,
156
+ beforeHash: meta.beforeHash,
157
+ afterHash: meta.afterHash,
158
+ appliedRules: meta.appliedRules,
159
+ },
160
+ "query normalized"
161
+ );
162
+ }
163
+ ```
164
+
165
+ - `query`: normalized query object.
166
+ - `meta`: observability data (changed flag, rule traces, warnings, hashes, optional stats/traces).
167
+
168
+ ### 6) Typical integration patterns
169
+
170
+ ```ts
171
+ // A. Normalize centrally in data-access layer
172
+ export function normalizeForFind(rawFilter) {
173
+ return normalizeQuery(rawFilter, { level: "shape" }).query;
174
+ }
175
+
176
+ // B. Use stronger convergence in offline paths
177
+ export function normalizeForBatch(rawFilter) {
178
+ return normalizeQuery(rawFilter, { level: "predicate" }).query;
179
+ }
180
+ ```
181
+
182
+ - Prefer `shape` for online request paths.
183
+ - Enable `predicate` / `scope` when there is clear benefit plus test coverage.
184
+
185
+ ### 7) Errors and boundaries
186
+
187
+ - Invalid `level` throws an error (for example, typos).
188
+ - Unsupported or unknown operators are generally preserved as opaque; semantic merge behavior is not guaranteed for them.
189
+ - The library target is stability and observability, not query planning optimization.
190
+
191
+ ---
192
+
54
193
  ## Default behavior
55
194
 
56
195
  - **Default `level` is `"shape"`** (see `resolveNormalizeOptions()`).
57
- - By default there is **no** aggressive predicate merge or logical hoisting.
196
+ - By default there is **no** predicate merge at `shape`. At **`scope`**, core work is inherited-constraint propagation and conservative branch decisions; **`detectCommonPredicatesInOr`** is an **optional, observe-only** rule (warnings / traces)—never a structural hoist.
58
197
  - The goal is **stability and observability**, not “smart optimization.”
59
198
 
60
199
  ---
61
200
 
62
- ## Production guidance (v0.1.0)
201
+ ## Choosing a level
202
+
203
+ - Use **`shape`** when you only need structural stabilization (flatten, dedupe children, ordering, etc.).
204
+ - Use **`predicate`** when you need same-field dedupe, modeled comparable merges, and contradiction collapse on **modeled** operators; opaque subtrees stay preserved.
205
+ - Use **`scope`** when you need inherited-constraint propagation, conservative pruning, and narrow coverage elimination as described in the spec and matrix. **`detectCommonPredicatesInOr`** (when enabled) is **observe-only** and does not rewrite structure.
63
206
 
64
- - Use **`shape`** for **general production** traffic. It is the **only** level recommended for that purpose in v0.1.0.
65
- - Levels above `shape` (`predicate`, `logical`, `experimental`) are **preview / unstable** surfaces. Use them for **offline analysis**, **replay testing**, and **targeted experiments** when you explicitly accept preview semantics—not as a default for all online requests.
66
- - If you opt into a non-`shape` level, **`meta.warnings` includes a boundary notice** for that call. In **non-production** runs (`NODE_ENV !== "production"`), the library also prints a **matching `console.warn` once per level per process** so local development surfaces the same guidance without spamming repeated logs.
207
+ Authoritative behavior boundaries are in **[SPEC.md](SPEC.md)**, **[docs/normalization-matrix.md](docs/normalization-matrix.md)**, and contract tests under **`test/contracts/`**—not informal README prose alone.
67
208
 
68
209
  ---
69
210
 
@@ -71,32 +212,29 @@ console.log(result.meta);
71
212
 
72
213
  ### `shape` (default)
73
214
 
74
- **Recommended for production hot paths** (the only v0.1.0 level recommended for general production). Safe structural normalization only, for example:
215
+ **Recommended default** for the lowest-risk path. Safe structural normalization only, for example:
75
216
 
76
- - flatten logical nodes
77
- - remove empty logical nodes
78
- - collapse single-child logical nodes
79
- - dedupe logical children
217
+ - flatten compound (`$and` / `$or`) nodes
218
+ - remove empty compound nodes
219
+ - collapse single-child compound nodes
220
+ - dedupe compound children
80
221
  - canonical ordering
81
222
 
82
223
  ### `predicate`
83
224
 
84
- **Preview / not recommended for general production** in v0.1.0. On top of `shape`, conservative **predicate** cleanup:
225
+ On top of `shape`, conservative **predicate** cleanup on **modeled** operators:
85
226
 
86
227
  - dedupe same-field predicates
87
228
  - merge comparable predicates where modeled
88
229
  - collapse clear contradictions to an unsatisfiable filter
89
230
  - merge **direct** `$and` children that share the same field name before further predicate work (so contradictions like `{ $and: [{ a: 1 }, { a: 2 }] }` can be detected)
90
231
 
91
- ### `logical`
232
+ ### `scope`
92
233
 
93
- **Preview / not recommended for general production** in v0.1.0. On top of `predicate`:
234
+ On top of `predicate`:
94
235
 
95
- - **detect** common predicates inside `$or` (detection / metadata; **no** default hoisting)
96
-
97
- ### `experimental`
98
-
99
- **Preview / not recommended for general production** in v0.1.0. May **hoist** common predicates from `$or` when the corresponding rule is enabled—**not** for blanket production rollout.
236
+ - **Inherited constraint propagation** (phase-1 allowlist) and **conservative branch pruning**; **coverage elimination** only in narrow, tested cases when policy allows
237
+ - Optional **`detectCommonPredicatesInOr`**: observe-only (warnings / traces); **no** structural rewrite
100
238
 
101
239
  ---
102
240
 
@@ -107,11 +245,13 @@ console.log(result.meta);
107
245
  | `changed` | Structural/predicate output differs from input (hash-based) |
108
246
  | `level` | Resolved normalization level |
109
247
  | `appliedRules` / `skippedRules` | Rule tracing |
110
- | `warnings` | Non-fatal issues when observation is enabled, plus a **v0.1.0 boundary warning** whenever the resolved level is not `shape` (always present for that case, independent of `observe.collectWarnings`) |
248
+ | `warnings` | Non-fatal issues when `observe.collectWarnings` is enabled (rule notices, detection text, etc.) |
111
249
  | `bailedOut` | Safety stop; output reverts to pre-pass parse for that call |
112
250
  | `bailoutReason` | Why bailout happened, if any |
113
251
  | `beforeHash` / `afterHash` | Stable hashes for diffing |
114
252
  | `stats` | Optional before/after tree metrics (`observe.collectMetrics`) |
253
+ | `predicateTraces` | When `observe.collectPredicateTraces`: per-field planner / skip / contradiction signals |
254
+ | `scopeTrace` | When `observe.collectScopeTraces`: constraint extraction rejections + scope decision events |
115
255
 
116
256
  ---
117
257
 
@@ -136,24 +276,22 @@ The **public contract** is:
136
276
  ## Principles (explicit)
137
277
 
138
278
  1. Default level is **`shape`**.
139
- 2. At the default **`shape`** level, the API is **intended for general production use** in v0.1.0.
140
- 3. **`predicate`** and above may change structure while aiming for **semantic equivalence** on modeled operators.
141
- 4. **`experimental`** is for experiments or offline replay—**not** default online traffic.
142
- 5. **Opaque** nodes are not rewritten semantically.
143
- 6. Output should be **idempotent** under the same options when no bailout occurs.
144
- 7. This library is **not** the MongoDB query planner or an optimizer.
279
+ 2. **`predicate`** / **`scope`** may change structure while aiming for **semantic equivalence** on **modeled** operators.
280
+ 3. **Opaque** nodes are not rewritten semantically.
281
+ 4. Output should be **idempotent** under the same options when no bailout occurs.
282
+ 5. This library is **not** the MongoDB query planner or an optimizer.
145
283
 
146
284
  ---
147
285
 
148
286
  ## Example scenarios
149
287
 
150
- **Online main path** — use default (`shape`); this is the supported production default in v0.1.0:
288
+ **Online main path** — use default (`shape`); this remains the most production-safe baseline in `v0.2.0`:
151
289
 
152
290
  ```ts
153
291
  normalizeQuery(query);
154
292
  ```
155
293
 
156
- **Offline analysis / replay / experiments** — opt into higher levels only when you accept preview semantics and non-`shape` boundary warnings (and optional dev console hints):
294
+ **Predicate or scope** — pass `level` explicitly; review [SPEC.md](SPEC.md) and contract tests for supported vs preserved patterns:
157
295
 
158
296
  ```ts
159
297
  normalizeQuery(query, { level: "predicate" });
@@ -168,22 +306,214 @@ normalizeQuery(query, options?) => { query, meta }
168
306
  resolveNormalizeOptions(options?) => ResolvedNormalizeOptions
169
307
  ```
170
308
 
171
- Types: `NormalizeLevel`, `NormalizeOptions`, `NormalizeRules`, `NormalizeSafety`, `NormalizeObserve`, `ResolvedNormalizeOptions`, `NormalizeResult`, `NormalizeStats`.
309
+ Types: `NormalizeLevel`, `NormalizeOptions`, `NormalizeRules`, `NormalizeSafety`, `NormalizeObserve`, `ResolvedNormalizeOptions`, `NormalizeResult`, `NormalizeStats`, `PredicateSafetyPolicy`, `ScopeSafetyPolicy`, trace-related types (see package exports).
310
+
311
+ ---
312
+
313
+ ## Testing
314
+
315
+ ### Test layout
316
+
317
+ This repository organizes tests by **API surface**, **normalization level**, and **cross-level contracts**, while preserving deeper semantic and regression suites.
318
+
319
+ ### Directory responsibilities
320
+
321
+ #### `test/api/`
322
+
323
+ Tests the public API and configuration surface.
324
+
325
+ Put tests here when they verify:
326
+
327
+ * `normalizeQuery` return shape and top-level behavior
328
+ * `resolveNormalizeOptions`
329
+ * package exports
330
+
331
+ Do **not** put level-specific normalization behavior here.
332
+
333
+ ---
334
+
335
+ #### `test/levels/`
336
+
337
+ Tests the behavior boundary of each `NormalizeLevel`.
338
+
339
+ Current levels:
340
+
341
+ * `shape`
342
+ * `predicate`
343
+ * `scope`
344
+
345
+ Each level test file should focus on four things:
346
+
347
+ 1. positive capabilities of that level
348
+ 2. behavior explicitly not enabled at that level
349
+ 3. contrast with the adjacent level(s)
350
+ 4. a small number of representative contracts for that level
351
+
352
+ Prefer asserting:
353
+
354
+ * normalized query structure
355
+ * observable cross-level differences
356
+ * stable public metadata
357
+
358
+ Avoid overfitting to:
359
+
360
+ * exact warning text
361
+ * exact internal rule IDs
362
+ * fixed child ordering unless ordering itself is part of the contract
363
+
364
+ ---
365
+
366
+ #### `test/contracts/`
367
+
368
+ Tests contracts that should hold across levels, or default behavior that is separate from any single level.
369
+
370
+ Put tests here when they verify:
371
+
372
+ * default level behavior
373
+ * idempotency across all levels
374
+ * output invariants across all levels
375
+ * opaque subtree preservation across all levels
376
+ * formal **`predicate` / `scope`** contracts (supported merges, opaque preservation, scope policy guards, rule toggles)—see `test/contracts/predicate-scope-stable-contract.test.js`
377
+
378
+ Use `test/helpers/level-contract-runner.js` for all-level suites.
379
+
380
+ ---
381
+
382
+ #### `test/semantic/`
383
+
384
+ Tests semantic equivalence against execution behavior.
385
+ These tests validate that normalization preserves meaning.
386
+
387
+ This directory is intentionally separate from `levels/` and `contracts/`.
388
+
389
+ ---
390
+
391
+ #### `test/property/`
392
+
393
+ Tests property-based and metamorphic behavior.
394
+
395
+ Use this directory for:
396
+
397
+ * randomized semantic checks
398
+ * metamorphic invariants
399
+ * broad input-space validation
400
+
401
+ Do not use it as the primary place to express level boundaries.
172
402
 
173
403
  ---
174
404
 
175
- ## Semantic tests (property-based)
405
+ #### `test/regression/`
406
+
407
+ Tests known historical failures and hand-crafted regression cases.
408
+
409
+ Add a regression test here when fixing a bug that should stay fixed.
410
+
411
+ ---
412
+
413
+ #### `test/performance/`
414
+
415
+ Tests performance guards or complexity-sensitive behavior.
416
+
417
+ These tests should stay focused on performance-related expectations, not general normalization structure.
418
+
419
+ ---
420
+
421
+ ### Helper files
422
+
423
+ #### `test/helpers/level-runner.js`
424
+
425
+ Shared helper for running a query at a specific level.
426
+
427
+ #### `test/helpers/level-cases.js`
428
+
429
+ Shared fixed inputs used across level tests.
430
+ Prefer adding reusable representative cases here instead of duplicating inline fixtures.
431
+
432
+ #### `test/helpers/level-contract-runner.js`
433
+
434
+ Shared `LEVELS` list and helpers for all-level contract suites.
435
+
436
+ ---
437
+
438
+ ### Rules for adding new tests
439
+
440
+ #### When adding a new normalization rule
441
+
442
+ Ask first:
443
+
444
+ * Is this a public API behavior?
445
+
446
+ * Add to `test/api/`
447
+ * Is this enabled only at a specific level?
448
+
449
+ * Add to `test/levels/`
450
+ * Should this hold for all levels?
451
+
452
+ * Add to `test/contracts/`
453
+ * Is this about semantic preservation or randomized validation?
454
+
455
+ * Add to `test/semantic/` or `test/property/`
456
+ * Is this a bug fix for a previously broken case?
457
+
458
+ * Add to `test/regression/`
459
+
460
+ ---
461
+
462
+ #### When adding a new level
463
+
464
+ At minimum, update all of the following:
465
+
466
+ 1. add a new `test/levels/<level>-level.test.js`
467
+ 2. register the level in `test/helpers/level-contract-runner.js`
468
+ 3. ensure all-level contract suites cover it
469
+ 4. add at least one contrast case against the adjacent level
470
+
471
+ ---
472
+
473
+ ### Testing style guidance
474
+
475
+ Prefer:
476
+
477
+ * example-based tests for level boundaries
478
+ * query-shape assertions
479
+ * contrast tests between adjacent levels
480
+ * shared fixtures for representative cases
481
+
482
+ Avoid:
483
+
484
+ * coupling level tests to unstable implementation details
485
+ * repeating the same fixture with only superficial assertion changes
486
+ * putting default-level behavior inside a specific level test
487
+ * mixing exports/API tests with normalization behavior tests
488
+
489
+ ---
490
+
491
+ ### Practical rule of thumb
492
+
493
+ * `api/` answers: **how the library is used**
494
+ * `levels/` answers: **what each level does and does not do**
495
+ * `contracts/` answers: **what must always remain true**
496
+ * `semantic/property/regression/performance` answer: **whether the system remains correct, robust, and efficient**
497
+
498
+ ---
499
+
500
+ ### npm scripts and property-test tooling
501
+
502
+ Randomized semantic tests use **`mongodb-memory-server`** + **`fast-check`** to compare **real** `find` results (same `sort` / `skip` / `limit`, projection `{ _id: 1 }`) before and after `normalizeQuery` on a **fixed document schema** and a **restricted operator set** (see `test/helpers/arbitraries.js`). They assert matching **`_id` order**, **idempotency** of the returned `query`, and (for opaque operators) **non-crash / stable second pass** only. **`FC_SEED` / `FC_RUNS` defaults are centralized in `test/helpers/fc-config.js`** (also re-exported from `arbitraries.js`).
176
503
 
177
- Randomized tests use **`mongodb-memory-server`** + **`fast-check`** to compare **real** `find` results (same `sort` / `skip` / `limit`, projection `{ _id: 1 }`) before and after `normalizeQuery` on a **fixed document schema** and a **restricted operator set** (see `test/helpers/arbitraries.js`). They assert matching **`_id` order**, **idempotency** of the returned `query`, and (for opaque operators) **non-crash / stable second pass** only. **`FC_SEED` / `FC_RUNS` defaults are centralized in `test/helpers/fc-config.js`** (also re-exported from `arbitraries.js`).
504
+ To **avoid downloading** a MongoDB binary, set one of **`MONGODB_BINARY`**, **`MONGOD_BINARY`**, or **`MONGOMS_SYSTEM_BINARY`** to your local `mongod` path before running semantic tests (see `test/helpers/mongo-fixture.js`).
178
505
 
179
- - **`npm run test:unit`** — unit tests (excludes `test/semantic/**`, `test/regression/**`, `test/property/**`; includes `test/contracts/**`, `test/invariants/**`, `test/performance/**`).
180
- - **`npm run test:semantic`** — semantic + regression + property folders (defaults when env unset: see `fc-config.js`).
181
- - **`npm run test:semantic:quick`** — lower **`FC_RUNS`** (script sets `45`) + **`FC_SEED=42`**, still runs `test/regression/**` and `test/property/**`.
182
- - **`npm run test:semantic:ci`** — CI-oriented env (`FC_RUNS=200`, `FC_SEED=42` in script).
506
+ * **`npm run test`** — build, then `test:unit`, then `test:semantic`.
507
+ * **`npm run test:api`** — `test/api/**/*.test.js` only.
508
+ * **`npm run test:levels`** — `test/levels/**/*.test.js` and `test/contracts/*.test.js`.
509
+ * **`npm run test:unit`** — all `test/**/*.test.js` except `test/semantic/**`, `test/regression/**`, and `test/property/**` (includes `test/api/**`, `test/levels/**`, `test/contracts/**`, `test/performance/**`, and other unit tests).
510
+ * **`npm run test:semantic`** — semantic + regression + property folders (defaults when env unset: see `fc-config.js`).
511
+ * **`npm run test:semantic:quick`** — lower **`FC_RUNS`** (script sets `45`) + **`FC_SEED=42`**, still runs `test/regression/**` and `test/property/**`.
512
+ * **`npm run test:semantic:ci`** — CI-oriented env (`FC_RUNS=200`, `FC_SEED=42` in script).
183
513
 
184
514
  Override property-test parameters: **`FC_SEED`**, **`FC_RUNS`**, optional **`FC_QUICK=1`** (see `fc-config.js`). How to reproduce failures and when to add a fixed regression case: **`test/REGRESSION.md`**.
185
515
 
186
- Full-text, geo, heavy **`$expr`**, **`$where`**, aggregation, collation, etc. stay **out** of the main semantic equivalence generator; opaque contracts live in **`test/contracts/opaque-operators.test.js`**.
516
+ Full-text, geo, heavy **`$expr`**, **`$where`**, aggregation, collation, etc. stay **out** of the main semantic equivalence generator; opaque contracts live in **`test/contracts/opaque-operators.all-levels.test.js`**.
187
517
 
188
518
  ---
189
519