@omgbase/core 0.1.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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/eval-matcher.d.ts +13 -0
  3. package/dist/src/cli/eval-matcher.js +36 -0
  4. package/dist/src/core/attach.d.ts +5 -0
  5. package/dist/src/core/attach.js +38 -0
  6. package/dist/src/core/cursor.d.ts +11 -0
  7. package/dist/src/core/cursor.js +35 -0
  8. package/dist/src/core/hash.d.ts +23 -0
  9. package/dist/src/core/hash.js +82 -0
  10. package/dist/src/core/ids.d.ts +6 -0
  11. package/dist/src/core/ids.js +26 -0
  12. package/dist/src/core/index.d.ts +1 -0
  13. package/dist/src/core/index.js +2 -0
  14. package/dist/src/core/ingest.d.ts +43 -0
  15. package/dist/src/core/ingest.js +287 -0
  16. package/dist/src/core/order-key.d.ts +8 -0
  17. package/dist/src/core/order-key.js +72 -0
  18. package/dist/src/core/parse/parse.d.ts +3 -0
  19. package/dist/src/core/parse/parse.js +104 -0
  20. package/dist/src/core/parse/render.d.ts +3 -0
  21. package/dist/src/core/parse/render.js +20 -0
  22. package/dist/src/core/parse/tree.d.ts +8 -0
  23. package/dist/src/core/parse/tree.js +43 -0
  24. package/dist/src/core/parse/types.d.ts +52 -0
  25. package/dist/src/core/parse/types.js +9 -0
  26. package/dist/src/core/read/document.d.ts +107 -0
  27. package/dist/src/core/read/document.js +191 -0
  28. package/dist/src/core/read/nodes.d.ts +47 -0
  29. package/dist/src/core/read/nodes.js +102 -0
  30. package/dist/src/core/read/outline.d.ts +12 -0
  31. package/dist/src/core/read/outline.js +76 -0
  32. package/dist/src/core/read/reader.d.ts +125 -0
  33. package/dist/src/core/read/reader.js +206 -0
  34. package/dist/src/core/read/refs.d.ts +15 -0
  35. package/dist/src/core/read/refs.js +51 -0
  36. package/dist/src/core/store/edges.d.ts +26 -0
  37. package/dist/src/core/store/edges.js +101 -0
  38. package/dist/src/core/store/fts.d.ts +10 -0
  39. package/dist/src/core/store/fts.js +22 -0
  40. package/dist/src/core/store/gc.d.ts +18 -0
  41. package/dist/src/core/store/gc.js +69 -0
  42. package/dist/src/core/store/nodes.d.ts +5 -0
  43. package/dist/src/core/store/nodes.js +57 -0
  44. package/dist/src/core/store/properties.d.ts +38 -0
  45. package/dist/src/core/store/properties.js +177 -0
  46. package/dist/src/core/store/rebuild.d.ts +3 -0
  47. package/dist/src/core/store/rebuild.js +29 -0
  48. package/dist/src/core/store/schema.d.ts +9 -0
  49. package/dist/src/core/store/schema.js +386 -0
  50. package/dist/src/core/store/sections.d.ts +2 -0
  51. package/dist/src/core/store/sections.js +29 -0
  52. package/dist/src/core/store/store.d.ts +24 -0
  53. package/dist/src/core/store/store.js +164 -0
  54. package/dist/src/core/store/writers.d.ts +52 -0
  55. package/dist/src/core/store/writers.js +89 -0
  56. package/dist/src/core/vec.d.ts +10 -0
  57. package/dist/src/core/vec.js +33 -0
  58. package/dist/src/format/adapter.d.ts +97 -0
  59. package/dist/src/format/adapter.js +14 -0
  60. package/dist/src/format/index.d.ts +6 -0
  61. package/dist/src/format/index.js +15 -0
  62. package/dist/src/format/json.d.ts +3 -0
  63. package/dist/src/format/json.js +324 -0
  64. package/dist/src/format/markdown.d.ts +5 -0
  65. package/dist/src/format/markdown.js +217 -0
  66. package/dist/src/format/registry.d.ts +6 -0
  67. package/dist/src/format/registry.js +24 -0
  68. package/dist/src/format/yaml.d.ts +5 -0
  69. package/dist/src/format/yaml.js +413 -0
  70. package/dist/src/graph/extract.d.ts +38 -0
  71. package/dist/src/graph/extract.js +242 -0
  72. package/dist/src/graph/history.d.ts +103 -0
  73. package/dist/src/graph/history.js +210 -0
  74. package/dist/src/graph/inbound-links.d.ts +33 -0
  75. package/dist/src/graph/inbound-links.js +130 -0
  76. package/dist/src/graph/link-destinations.d.ts +22 -0
  77. package/dist/src/graph/link-destinations.js +84 -0
  78. package/dist/src/graph/link-health.d.ts +79 -0
  79. package/dist/src/graph/link-health.js +146 -0
  80. package/dist/src/graph/links.d.ts +23 -0
  81. package/dist/src/graph/links.js +42 -0
  82. package/dist/src/index.d.ts +60 -0
  83. package/dist/src/index.js +70 -0
  84. package/dist/src/mcp/errors.d.ts +17 -0
  85. package/dist/src/mcp/errors.js +18 -0
  86. package/dist/src/mcp/graph.d.ts +63 -0
  87. package/dist/src/mcp/graph.js +212 -0
  88. package/dist/src/mcp/reference.d.ts +1 -0
  89. package/dist/src/mcp/reference.js +227 -0
  90. package/dist/src/mcp/server.d.ts +30 -0
  91. package/dist/src/mcp/server.js +1076 -0
  92. package/dist/src/mcp/stdio.d.ts +13 -0
  93. package/dist/src/mcp/stdio.js +24 -0
  94. package/dist/src/migrate/mrplex.d.ts +27 -0
  95. package/dist/src/migrate/mrplex.js +29 -0
  96. package/dist/src/mutate/apply.d.ts +83 -0
  97. package/dist/src/mutate/apply.js +302 -0
  98. package/dist/src/mutate/doc-store.d.ts +49 -0
  99. package/dist/src/mutate/doc-store.js +71 -0
  100. package/dist/src/mutate/docs.d.ts +60 -0
  101. package/dist/src/mutate/docs.js +232 -0
  102. package/dist/src/mutate/known-ids.d.ts +12 -0
  103. package/dist/src/mutate/known-ids.js +154 -0
  104. package/dist/src/mutate/load.d.ts +3 -0
  105. package/dist/src/mutate/load.js +66 -0
  106. package/dist/src/mutate/lower.d.ts +32 -0
  107. package/dist/src/mutate/lower.js +232 -0
  108. package/dist/src/mutate/macros.d.ts +91 -0
  109. package/dist/src/mutate/macros.js +230 -0
  110. package/dist/src/mutate/ops.d.ts +40 -0
  111. package/dist/src/mutate/ops.js +513 -0
  112. package/dist/src/mutate/opset.d.ts +92 -0
  113. package/dist/src/mutate/opset.js +79 -0
  114. package/dist/src/mutate/plan-update.d.ts +51 -0
  115. package/dist/src/mutate/plan-update.js +187 -0
  116. package/dist/src/mutate/tree.d.ts +46 -0
  117. package/dist/src/mutate/tree.js +167 -0
  118. package/dist/src/oqx/run.d.ts +1 -0
  119. package/dist/src/oqx/run.js +6 -0
  120. package/dist/src/oqx-js/context.d.ts +23 -0
  121. package/dist/src/oqx-js/context.js +564 -0
  122. package/dist/src/oqx-js/planner.d.ts +10 -0
  123. package/dist/src/oqx-js/planner.js +91 -0
  124. package/dist/src/oqx-js/run.d.ts +41 -0
  125. package/dist/src/oqx-js/run.js +332 -0
  126. package/dist/src/oqx-js/sql/translate.d.ts +27 -0
  127. package/dist/src/oqx-js/sql/translate.js +290 -0
  128. package/dist/src/reconcile/crossdoc.d.ts +34 -0
  129. package/dist/src/reconcile/crossdoc.js +76 -0
  130. package/dist/src/reconcile/eval/generator.d.ts +21 -0
  131. package/dist/src/reconcile/eval/generator.js +129 -0
  132. package/dist/src/reconcile/eval/metrics.d.ts +32 -0
  133. package/dist/src/reconcile/eval/metrics.js +74 -0
  134. package/dist/src/reconcile/flatten.d.ts +12 -0
  135. package/dist/src/reconcile/flatten.js +37 -0
  136. package/dist/src/reconcile/phase5.d.ts +8 -0
  137. package/dist/src/reconcile/phase5.js +167 -0
  138. package/dist/src/reconcile/phase6a.d.ts +2 -0
  139. package/dist/src/reconcile/phase6a.js +159 -0
  140. package/dist/src/reconcile/phases.d.ts +28 -0
  141. package/dist/src/reconcile/phases.js +0 -0
  142. package/dist/src/reconcile/reconcile.d.ts +21 -0
  143. package/dist/src/reconcile/reconcile.js +115 -0
  144. package/dist/src/reconcile/similarity.d.ts +7 -0
  145. package/dist/src/reconcile/similarity.js +41 -0
  146. package/dist/src/reconcile/types.d.ts +46 -0
  147. package/dist/src/reconcile/types.js +19 -0
  148. package/dist/src/search/cel/ast.d.ts +74 -0
  149. package/dist/src/search/cel/ast.js +3 -0
  150. package/dist/src/search/cel/compile.d.ts +60 -0
  151. package/dist/src/search/cel/compile.js +836 -0
  152. package/dist/src/search/cel/lexer.d.ts +9 -0
  153. package/dist/src/search/cel/lexer.js +139 -0
  154. package/dist/src/search/cel/parser.d.ts +7 -0
  155. package/dist/src/search/cel/parser.js +219 -0
  156. package/dist/src/search/drain.d.ts +39 -0
  157. package/dist/src/search/drain.js +97 -0
  158. package/dist/src/search/embeddings.d.ts +131 -0
  159. package/dist/src/search/embeddings.js +263 -0
  160. package/dist/src/search/external.d.ts +13 -0
  161. package/dist/src/search/external.js +177 -0
  162. package/dist/src/search/fts-query.d.ts +1 -0
  163. package/dist/src/search/fts-query.js +64 -0
  164. package/dist/src/search/provider.d.ts +17 -0
  165. package/dist/src/search/provider.js +13 -0
  166. package/dist/src/search/resolve.d.ts +18 -0
  167. package/dist/src/search/resolve.js +23 -0
  168. package/dist/src/search/rrf.d.ts +36 -0
  169. package/dist/src/search/rrf.js +84 -0
  170. package/dist/src/search/tasks.d.ts +12 -0
  171. package/dist/src/search/tasks.js +131 -0
  172. package/dist/src/search/text.d.ts +16 -0
  173. package/dist/src/search/text.js +21 -0
  174. package/dist/src/search/vector.d.ts +18 -0
  175. package/dist/src/search/vector.js +49 -0
  176. package/dist/src/sync/admin.d.ts +59 -0
  177. package/dist/src/sync/admin.js +53 -0
  178. package/dist/src/sync/attach.d.ts +13 -0
  179. package/dist/src/sync/attach.js +29 -0
  180. package/dist/src/sync/checkpoint.d.ts +21 -0
  181. package/dist/src/sync/checkpoint.js +52 -0
  182. package/dist/src/sync/driver.d.ts +25 -0
  183. package/dist/src/sync/driver.js +80 -0
  184. package/dist/src/sync/external-source.d.ts +14 -0
  185. package/dist/src/sync/external-source.js +137 -0
  186. package/dist/src/sync/freshness.d.ts +42 -0
  187. package/dist/src/sync/freshness.js +152 -0
  188. package/dist/src/sync/fs-util.d.ts +12 -0
  189. package/dist/src/sync/fs-util.js +55 -0
  190. package/dist/src/sync/git-heuristics.d.ts +6 -0
  191. package/dist/src/sync/git-heuristics.js +16 -0
  192. package/dist/src/sync/observe.d.ts +63 -0
  193. package/dist/src/sync/observe.js +84 -0
  194. package/dist/src/sync/plugin.d.ts +54 -0
  195. package/dist/src/sync/plugin.js +12 -0
  196. package/dist/src/sync/reconciling-ingest.d.ts +12 -0
  197. package/dist/src/sync/reconciling-ingest.js +209 -0
  198. package/dist/src/sync/recovery.d.ts +6 -0
  199. package/dist/src/sync/recovery.js +34 -0
  200. package/dist/src/sync/settings.d.ts +18 -0
  201. package/dist/src/sync/settings.js +58 -0
  202. package/dist/src/sync/sources.d.ts +37 -0
  203. package/dist/src/sync/sources.js +75 -0
  204. package/dist/src/sync/tombstone.d.ts +9 -0
  205. package/dist/src/sync/tombstone.js +55 -0
  206. package/dist/src/sync/watch-lease.d.ts +14 -0
  207. package/dist/src/sync/watch-lease.js +91 -0
  208. package/dist/src/sync/watcher.d.ts +19 -0
  209. package/dist/src/sync/watcher.js +34 -0
  210. package/dist/src/sync/workspace.d.ts +34 -0
  211. package/dist/src/sync/workspace.js +120 -0
  212. package/dist/src/sync/writer-lock.d.ts +16 -0
  213. package/dist/src/sync/writer-lock.js +121 -0
  214. package/package.json +80 -0
@@ -0,0 +1,37 @@
1
+ import { sha256, normalizeVisibleText } from "../core/hash.js";
2
+ /** Adapt a parsed RawBlock-ish tree (no ids) to FlatSource. */
3
+ export function fromInput(blocks) {
4
+ return blocks.map((b) => ({
5
+ ...(b.blockId ? { blockId: b.blockId } : {}),
6
+ type: b.type,
7
+ raw: b.raw,
8
+ children: fromInput(b.children),
9
+ }));
10
+ }
11
+ export function flatten(blocks) {
12
+ const out = [];
13
+ const walk = (list, parentKey) => {
14
+ list.forEach((b, index) => {
15
+ const key = `${parentKey ?? ""}/${index}`;
16
+ const text = normalizeVisibleText(b.raw, b.type);
17
+ const mb = {
18
+ type: b.type,
19
+ rawHashHex: sha256(b.raw).toString("hex"),
20
+ normHashHex: sha256(text).toString("hex"),
21
+ text,
22
+ anchors: b.anchors ?? [],
23
+ parentKey,
24
+ index,
25
+ key,
26
+ };
27
+ if (b.blockId)
28
+ mb.blockId = b.blockId;
29
+ out.push(mb);
30
+ if (b.children.length > 0)
31
+ walk(b.children, key);
32
+ });
33
+ };
34
+ walk(blocks, null);
35
+ return out;
36
+ }
37
+ //# sourceMappingURL=flatten.js.map
@@ -0,0 +1,8 @@
1
+ import type { PhaseState } from "./phases.js";
2
+ /**
3
+ * Phase 5 — scored assignment. Greedy-by-score with R3 order constraints:
4
+ * accept the highest-scoring candidate, discard conflicting candidates
5
+ * (same old or new, or order-crossing with an already-accepted same-parent
6
+ * pair), repeat. Accept while score ≥ θ (θ_small for tiny blocks).
7
+ */
8
+ export declare function phase5Scored(state: PhaseState): void;
@@ -0,0 +1,167 @@
1
+ import { shingles, dice, tokenCount } from "./similarity.js";
2
+ // score(o,n) = 0.55 text_sim + 0.15 neighbor_ctx + 0.10 parent_match
3
+ // + 0.10 position_prior + 0.10 anchor_evidence
4
+ function scorePair(o, n, state, oldCount, newCount) {
5
+ const textSimVal = dice(shingles(o.text), shingles(n.text));
6
+ // neighbor_ctx: fraction of {prev,next} siblings that are matched pairs.
7
+ const neighborCtx = neighborContext(o, n, state);
8
+ // parent_match: 1 if parents are a matched pair (or both roots).
9
+ const parentMatch = parentsMatched(o, n, state) ? 1 : 0;
10
+ // position_prior: 1 - |rel_pos(o) - rel_pos(n)|
11
+ const relO = oldCount > 1 ? o.index / (oldCount - 1) : 0;
12
+ const relN = newCount > 1 ? n.index / (newCount - 1) : 0;
13
+ const positionPrior = 1 - Math.abs(relO - relN);
14
+ // anchor_evidence: shared anchors / code-fence info / heading prefix.
15
+ const anchorEvidence = sharedAnchorEvidence(o, n);
16
+ return (0.55 * textSimVal +
17
+ 0.15 * neighborCtx +
18
+ 0.1 * parentMatch +
19
+ 0.1 * positionPrior +
20
+ 0.1 * anchorEvidence);
21
+ }
22
+ function siblingAt(blocks, parentKey, index) {
23
+ return blocks.find((b) => b.parentKey === parentKey && b.index === index);
24
+ }
25
+ function neighborContext(o, n, state) {
26
+ let matched = 0;
27
+ let total = 0;
28
+ for (const delta of [-1, 1]) {
29
+ const oldSib = siblingAt(state.old, o.parentKey, o.index + delta);
30
+ const newSib = siblingAt(state.neu, n.parentKey, n.index + delta);
31
+ if (oldSib || newSib) {
32
+ total++;
33
+ // matched pair if the old sibling's id is assigned to the new sibling's key
34
+ if (oldSib && newSib && state.matched.get(newSib.key) === oldSib.blockId)
35
+ matched++;
36
+ }
37
+ }
38
+ return total === 0 ? 0 : matched / total;
39
+ }
40
+ function parentsMatched(o, n, state) {
41
+ if (o.parentKey === null && n.parentKey === null)
42
+ return true;
43
+ if (o.parentKey === null || n.parentKey === null)
44
+ return false;
45
+ // old parent block whose key === o.parentKey; check its id maps to n.parentKey
46
+ const oldParent = state.old.find((b) => b.key === o.parentKey);
47
+ if (!oldParent?.blockId)
48
+ return false;
49
+ return state.matched.get(n.parentKey) === oldParent.blockId;
50
+ }
51
+ function sharedAnchorEvidence(o, n) {
52
+ if (o.anchors.length > 0 && n.anchors.some((a) => o.anchors.includes(a)))
53
+ return 1;
54
+ return 0;
55
+ }
56
+ // Candidate pruning (03 §5): |token_count diff| ≤ 3× and a shared 3-gram; cap
57
+ // 12 candidates per block by shingle overlap.
58
+ function pruneCandidates(olds, news) {
59
+ const oldShingles = new Map();
60
+ for (const o of olds)
61
+ oldShingles.set(o, shingles(o.text));
62
+ const candidates = [];
63
+ for (const n of news) {
64
+ const ns = shingles(n.text);
65
+ const nCount = tokenCount(n.text);
66
+ const perBlock = [];
67
+ for (const o of olds) {
68
+ if (o.type !== n.type)
69
+ continue;
70
+ const oCount = tokenCount(o.text);
71
+ const ratio = Math.max(oCount, nCount) / Math.max(1, Math.min(oCount, nCount));
72
+ if (ratio > 3)
73
+ continue;
74
+ const os = oldShingles.get(o);
75
+ let shares = false;
76
+ for (const s of ns)
77
+ if (os.has(s)) {
78
+ shares = true;
79
+ break;
80
+ }
81
+ if (!shares && ns.size > 0 && os.size > 0)
82
+ continue;
83
+ perBlock.push({ old: o, neu: n, score: 0 });
84
+ }
85
+ // cap at 12 by shingle overlap
86
+ perBlock.sort((a, b) => dice(oldShingles.get(b.old), ns) - dice(oldShingles.get(a.old), ns));
87
+ candidates.push(...perBlock.slice(0, 12));
88
+ }
89
+ return candidates;
90
+ }
91
+ function unmatchedOld(state) {
92
+ return state.old.filter((b) => !state.usedOld.has(b.blockId));
93
+ }
94
+ function unmatchedNew(state) {
95
+ return state.neu.filter((b) => !state.usedNew.has(b.key));
96
+ }
97
+ /**
98
+ * Phase 5 — scored assignment. Greedy-by-score with R3 order constraints:
99
+ * accept the highest-scoring candidate, discard conflicting candidates
100
+ * (same old or new, or order-crossing with an already-accepted same-parent
101
+ * pair), repeat. Accept while score ≥ θ (θ_small for tiny blocks).
102
+ */
103
+ export function phase5Scored(state) {
104
+ const olds = unmatchedOld(state);
105
+ const news = unmatchedNew(state);
106
+ if (olds.length === 0 || news.length === 0)
107
+ return;
108
+ // Bulk path guard: too many unmatched ⇒ skip (handled by give-up logic 2.5).
109
+ if (olds.length + news.length > state.config.maxScoredBlocks * 2)
110
+ return;
111
+ const oldCount = state.old.length;
112
+ const newCount = state.neu.length;
113
+ const candidates = pruneCandidates(olds, news);
114
+ for (const c of candidates)
115
+ c.score = scorePair(c.old, c.neu, state, oldCount, newCount);
116
+ // Deterministic order: score desc, then old key, then new key.
117
+ candidates.sort((a, b) => b.score - a.score || cmp(a.old.key, b.old.key) || cmp(a.neu.key, b.neu.key));
118
+ // All scored candidates per new key, for near-miss recording (R5).
119
+ const byNewKey = new Map();
120
+ for (const c of candidates)
121
+ (byNewKey.get(c.neu.key) ?? byNewKey.set(c.neu.key, []).get(c.neu.key)).push(c);
122
+ // accepted pairs per parent for the order (R3) check
123
+ const acceptedByParent = new Map();
124
+ for (const c of candidates) {
125
+ if (state.usedOld.has(c.old.blockId) || state.usedNew.has(c.neu.key))
126
+ continue;
127
+ const threshold = tokenCount(c.neu.text) < state.config.smallBlockTokens ? state.config.thetaSmall : state.config.thetaAccept;
128
+ if (c.score < threshold)
129
+ break; // sorted desc: nothing below will qualify either
130
+ // R3: within the same new parent, accepted pairs must not cross in order.
131
+ if (c.old.parentKey === c.neu.parentKey) {
132
+ const list = acceptedByParent.get(c.neu.parentKey) ?? [];
133
+ const crosses = list.some((p) => (p.oldIndex - c.old.index) * (p.newIndex - c.neu.index) < 0);
134
+ if (crosses)
135
+ continue;
136
+ }
137
+ // Near-misses (R5): other candidates for this new block whose score fell
138
+ // within 0.1 below the acceptance threshold. Recorded on the winner.
139
+ const nearMisses = (byNewKey.get(c.neu.key) ?? [])
140
+ .filter((o) => o.old.blockId !== c.old.blockId && o.score < threshold && o.score >= threshold - 0.1)
141
+ .map((o) => ({ blockId: o.old.blockId, score: round(o.score) }));
142
+ // accept
143
+ state.matched.set(c.neu.key, c.old.blockId);
144
+ state.usedOld.add(c.old.blockId);
145
+ state.usedNew.add(c.neu.key);
146
+ const moved = c.old.parentKey !== c.neu.parentKey || c.old.index !== c.neu.index;
147
+ const kind = moved ? "edited_moved" : "edited";
148
+ state.dispositions.push({
149
+ blockId: c.old.blockId,
150
+ kind,
151
+ confidence: c.score,
152
+ reason: "scored",
153
+ matcherV: state.config.matcherV,
154
+ detail: nearMisses.length > 0 ? { near_misses: nearMisses } : {},
155
+ });
156
+ const list = acceptedByParent.get(c.neu.parentKey) ?? [];
157
+ list.push({ oldIndex: c.old.index, newIndex: c.neu.index });
158
+ acceptedByParent.set(c.neu.parentKey, list);
159
+ }
160
+ }
161
+ function round(n) {
162
+ return Math.round(n * 1000) / 1000;
163
+ }
164
+ function cmp(a, b) {
165
+ return a < b ? -1 : a > b ? 1 : 0;
166
+ }
167
+ //# sourceMappingURL=phase5.js.map
@@ -0,0 +1,2 @@
1
+ import type { PhaseState } from "./phases.js";
2
+ export declare function phase6aCompound(state: PhaseState): void;
@@ -0,0 +1,159 @@
1
+ import { shingles, dice, tokenCount, tokenize } from "./similarity.js";
2
+ // Phase 6a — compound classification (03 §4): split, merge, copy over the
3
+ // still-unmatched. Split/merge use dominant-fragment inheritance (tunable via
4
+ // split.dominant_share; set to 1.01 to disable). Copies never steal identity.
5
+ function unmatchedOld(state) {
6
+ return state.old.filter((b) => !state.usedOld.has(b.blockId));
7
+ }
8
+ function unmatchedNew(state) {
9
+ return state.neu.filter((b) => !state.usedNew.has(b.key));
10
+ }
11
+ // coverage(concat(N), O): how much of O's shingles the concatenation covers.
12
+ function coverage(concatText, oText) {
13
+ const os = shingles(oText);
14
+ if (os.size === 0)
15
+ return 0;
16
+ const cs = shingles(concatText);
17
+ let covered = 0;
18
+ for (const s of os)
19
+ if (cs.has(s))
20
+ covered++;
21
+ return covered / os.size;
22
+ }
23
+ function carryTo(state, oldId, newB, kind, confidence, detail) {
24
+ state.matched.set(newB.key, oldId);
25
+ state.usedOld.add(oldId);
26
+ state.usedNew.add(newB.key);
27
+ state.dispositions.push({ blockId: oldId, kind, confidence, reason: "scored", matcherV: state.config.matcherV, detail });
28
+ }
29
+ // Record a mint-with-lineage disposition against a still-minted new block. The
30
+ // new block gets a fresh id later (assignment step); here we note the lineage
31
+ // keyed by the new block's positional key in detail.
32
+ function noteLineage(state, newB, kind, counterpartOldId) {
33
+ state.usedNew.add(newB.key);
34
+ state.dispositions.push({
35
+ blockId: `NEW:${newB.key}`,
36
+ kind,
37
+ confidence: null,
38
+ reason: "scored",
39
+ matcherV: state.config.matcherV,
40
+ detail: { counterpart: counterpartOldId, newKey: newB.key },
41
+ });
42
+ }
43
+ /** Detect splits: one old block O covered by a run of ≥2 adjacent new blocks. */
44
+ function detectSplits(state) {
45
+ const { splitCoverage, splitDominantShare } = state.config;
46
+ for (const o of unmatchedOld(state)) {
47
+ const news = unmatchedNew(state)
48
+ .filter((n) => n.parentKey === o.parentKey && n.type === o.type)
49
+ .sort((a, b) => a.index - b.index);
50
+ // find a contiguous run (by index) of length ≥2 covering O
51
+ for (let start = 0; start < news.length; start++) {
52
+ for (let end = news.length; end > start + 1; end--) {
53
+ const run = news.slice(start, end);
54
+ if (!isContiguous(run))
55
+ continue;
56
+ const concat = run.map((n) => n.text).join(" ");
57
+ const cov = coverage(concat, o.text);
58
+ if (cov < splitCoverage)
59
+ continue;
60
+ // leftover < 0.2 of the run's content is novel? approximate via reverse coverage.
61
+ const leftover = 1 - coverage(o.text, concat);
62
+ if (leftover >= 0.2)
63
+ continue;
64
+ // dominant fragment: first fragment holding ≥ share of O's tokens?
65
+ const oTokens = tokenCount(o.text) || 1;
66
+ const first = run[0];
67
+ const firstShare = sharedTokenFraction(first.text, o.text, oTokens);
68
+ if (firstShare >= splitDominantShare) {
69
+ carryTo(state, o.blockId, first, "edited", 0.8 * cov, { split: run.map((n) => n.key), dominant: first.key });
70
+ for (const n of run.slice(1))
71
+ noteLineage(state, n, "split_from", o.blockId);
72
+ }
73
+ else {
74
+ state.usedOld.add(o.blockId);
75
+ state.dispositions.push({ blockId: o.blockId, kind: "deleted", confidence: null, reason: "tombstone", matcherV: state.config.matcherV, detail: { splitInto: run.map((n) => n.key) } });
76
+ for (const n of run)
77
+ noteLineage(state, n, "split_from", o.blockId);
78
+ }
79
+ return; // one split per pass (conservative)
80
+ }
81
+ }
82
+ }
83
+ }
84
+ /** Detect merges: a run of ≥2 adjacent old blocks covered by one new block. */
85
+ function detectMerges(state) {
86
+ const { splitCoverage, splitDominantShare } = state.config;
87
+ for (const n of unmatchedNew(state)) {
88
+ const olds = unmatchedOld(state)
89
+ .filter((o) => o.parentKey === n.parentKey && o.type === n.type)
90
+ .sort((a, b) => a.index - b.index);
91
+ for (let start = 0; start < olds.length; start++) {
92
+ for (let end = olds.length; end > start + 1; end--) {
93
+ const run = olds.slice(start, end);
94
+ if (!isContiguous(run))
95
+ continue;
96
+ const concat = run.map((o) => o.text).join(" ");
97
+ const cov = coverage(n.text, concat);
98
+ if (cov < splitCoverage)
99
+ continue;
100
+ const nTokens = tokenCount(n.text) || 1;
101
+ // dominant contributor: first old block holding ≥ share of N's tokens.
102
+ const first = run[0];
103
+ const firstShare = sharedTokenFraction(first.text, n.text, nTokens);
104
+ if (firstShare >= splitDominantShare) {
105
+ carryTo(state, first.blockId, n, "edited", 0.8 * cov, { merge: run.map((o) => o.blockId), dominant: first.blockId });
106
+ for (const o of run.slice(1)) {
107
+ state.usedOld.add(o.blockId);
108
+ state.dispositions.push({ blockId: o.blockId, kind: "merged_into", confidence: 0.8 * cov, reason: "scored", matcherV: state.config.matcherV, detail: { into: first.blockId } });
109
+ }
110
+ }
111
+ else {
112
+ state.usedNew.add(n.key);
113
+ for (const o of run) {
114
+ state.usedOld.add(o.blockId);
115
+ state.dispositions.push({ blockId: o.blockId, kind: "merged_into", confidence: null, reason: "scored", matcherV: state.config.matcherV, detail: { mergedKey: n.key } });
116
+ }
117
+ noteLineage(state, n, "merged_into", first.blockId);
118
+ }
119
+ return;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ /** Detect copies: unmatched new block ≥ copy.sim to a MATCHED (present) old block. */
125
+ function detectCopies(state) {
126
+ const matchedOldIds = new Set(state.matched.values());
127
+ const matchedOld = state.old.filter((o) => matchedOldIds.has(o.blockId));
128
+ for (const n of unmatchedNew(state)) {
129
+ for (const o of matchedOld) {
130
+ if (o.type !== n.type)
131
+ continue;
132
+ if (dice(shingles(o.text), shingles(n.text)) >= state.config.copySim) {
133
+ noteLineage(state, n, "copied_from", o.blockId);
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ export function phase6aCompound(state) {
140
+ detectSplits(state);
141
+ detectMerges(state);
142
+ detectCopies(state);
143
+ }
144
+ function isContiguous(run) {
145
+ for (let i = 1; i < run.length; i++)
146
+ if (run[i].index !== run[i - 1].index + 1)
147
+ return false;
148
+ return true;
149
+ }
150
+ function sharedTokenFraction(fragment, whole, wholeTokenCount) {
151
+ const fragTokens = new Set(tokenize(fragment));
152
+ const wholeTokens = tokenize(whole);
153
+ let shared = 0;
154
+ for (const t of wholeTokens)
155
+ if (fragTokens.has(t))
156
+ shared++;
157
+ return shared / wholeTokenCount;
158
+ }
159
+ //# sourceMappingURL=phase6a.js.map
@@ -0,0 +1,28 @@
1
+ import type { MatchBlock, Disposition, ReconcileConfig } from "./types.js";
2
+ export interface PhaseState {
3
+ old: MatchBlock[];
4
+ neu: MatchBlock[];
5
+ /** new key → old id */
6
+ matched: Map<string, string>;
7
+ /** old ids already consumed */
8
+ usedOld: Set<string>;
9
+ /** new keys already matched */
10
+ usedNew: Set<string>;
11
+ dispositions: Disposition[];
12
+ config: ReconcileConfig;
13
+ }
14
+ /** Phase 1 — exact raw-hash lock. */
15
+ export declare function phase1Exact(state: PhaseState): void;
16
+ /** Phase 2 — normalized-hash lock. */
17
+ export declare function phase2Normalized(state: PhaseState): void;
18
+ /** Phase 3 — anchor lock: same authored ^ref, unique on both sides. */
19
+ export declare function phase3Anchor(state: PhaseState): void;
20
+ /**
21
+ * Phase 4 — context propagation (03 §4). Matched siblings vouch for the
22
+ * stranger between them: within a gap bounded by matched blocks, if exactly one
23
+ * unmatched OLD child remains, pair it with its best-matching unmatched NEW
24
+ * child in that same gap (text_sim ≥ floor, unique best). Insertions in the gap
25
+ * do not block the carry — they simply lose to the better-matching candidate,
26
+ * which is why an inserted block never steals an edited block's identity.
27
+ */
28
+ export declare function phase4Context(state: PhaseState): void;
Binary file
@@ -0,0 +1,21 @@
1
+ import type { MatchBlock, ReconcileConfig, MatchResult } from "./types.js";
2
+ export interface ResurrectionCandidate {
3
+ blockId: string;
4
+ rawHashHex: string;
5
+ normHashHex: string;
6
+ type: string;
7
+ }
8
+ export interface ReconcileOptions {
9
+ config?: ReconcileConfig;
10
+ /** resurrection pool (cross-checkpoint) matched by raw/norm hash only. */
11
+ pool?: ResurrectionCandidate[];
12
+ }
13
+ export interface DocReconcileResult extends MatchResult {
14
+ /** pool ids consumed by resurrection this run */
15
+ consumedPool: string[];
16
+ }
17
+ /**
18
+ * Reconcile one document. `oldBlocks` carry ids; `newBlocks` do not. Returns the
19
+ * assignment (new key → id), dispositions, and deleted old ids.
20
+ */
21
+ export declare function reconcileDocument(oldBlocks: MatchBlock[], newBlocks: MatchBlock[], opts?: ReconcileOptions): DocReconcileResult;
@@ -0,0 +1,115 @@
1
+ import { mintId } from "../core/ids.js";
2
+ import { DEFAULT_CONFIG } from "./types.js";
3
+ import { phase1Exact, phase2Normalized, phase3Anchor, phase4Context } from "./phases.js";
4
+ import { phase5Scored } from "./phase5.js";
5
+ import { phase6aCompound } from "./phase6a.js";
6
+ function newState(old, neu, config) {
7
+ return { old, neu, matched: new Map(), usedOld: new Set(), usedNew: new Set(), dispositions: [], config };
8
+ }
9
+ // Bulk-rewrite give-up (03 §6): after phase 2, if a large doc is mostly
10
+ // unmatched with low mean similarity, mint everything + one bulk_rewrite.
11
+ function shouldBulkRewrite(state) {
12
+ const total = state.neu.length;
13
+ if (total < state.config.bulkMinBlocks)
14
+ return false;
15
+ const unmatchedFrac = (state.neu.length - state.usedNew.size) / total;
16
+ return unmatchedFrac > state.config.bulkUnmatchedFrac;
17
+ }
18
+ /** Resurrection (03 §4 phase 6b, cross-checkpoint): match remaining new blocks
19
+ * against a pool by exact raw/norm hash only. Consumes matched pool rows. */
20
+ function phase6bResurrection(state, pool, consumed) {
21
+ const byRaw = new Map();
22
+ const byNorm = new Map();
23
+ for (const c of pool) {
24
+ byRaw.set(`${c.type} ${c.rawHashHex}`, c);
25
+ byNorm.set(`${c.type} ${c.normHashHex}`, c);
26
+ }
27
+ for (const n of state.neu) {
28
+ if (state.usedNew.has(n.key))
29
+ continue;
30
+ const hit = byRaw.get(`${n.type} ${n.rawHashHex}`) ?? byNorm.get(`${n.type} ${n.normHashHex}`);
31
+ if (hit && !consumed.has(hit.blockId)) {
32
+ consumed.add(hit.blockId);
33
+ state.matched.set(n.key, hit.blockId);
34
+ state.usedNew.add(n.key);
35
+ state.dispositions.push({ blockId: hit.blockId, kind: "resurrected", confidence: 0.99, reason: "exact_hash", matcherV: state.config.matcherV, detail: {} });
36
+ }
37
+ }
38
+ }
39
+ /**
40
+ * Reconcile one document. `oldBlocks` carry ids; `newBlocks` do not. Returns the
41
+ * assignment (new key → id), dispositions, and deleted old ids.
42
+ */
43
+ export function reconcileDocument(oldBlocks, newBlocks, opts = {}) {
44
+ const config = opts.config ?? DEFAULT_CONFIG;
45
+ const state = newState(oldBlocks, newBlocks, config);
46
+ const consumedPool = new Set();
47
+ phase1Exact(state);
48
+ phase2Normalized(state);
49
+ if (shouldBulkRewrite(state)) {
50
+ return bulkRewrite(state);
51
+ }
52
+ phase3Anchor(state);
53
+ phase4Context(state);
54
+ phase5Scored(state);
55
+ phase6aCompound(state);
56
+ if (opts.pool && opts.pool.length > 0)
57
+ phase6bResurrection(state, opts.pool, consumedPool);
58
+ return finalize(state, [...consumedPool]);
59
+ }
60
+ // Phase 7 defaults + assignment resolution. Every new block gets a final id:
61
+ // carried (from state.matched), lineage-minted (NEW:<key> dispositions), or a
62
+ // plain minted 'inserted'. Remaining old blocks are 'deleted'.
63
+ function finalize(state, consumedPool) {
64
+ const assignment = new Map();
65
+ const dispositions = [...state.dispositions];
66
+ // Carried assignments.
67
+ for (const [newKey, oldId] of state.matched)
68
+ assignment.set(newKey, oldId);
69
+ // Lineage placeholders (split_from/merged_into/copied_from on NEW:<key>):
70
+ // mint a fresh id and rewrite the disposition to the minted id.
71
+ for (const d of dispositions) {
72
+ if (d.blockId.startsWith("NEW:")) {
73
+ const newKey = d.detail.newKey;
74
+ const minted = assignment.get(newKey) ?? mintId("b");
75
+ assignment.set(newKey, minted);
76
+ d.blockId = minted;
77
+ }
78
+ }
79
+ // Remaining new blocks with no assignment → minted 'inserted'.
80
+ for (const n of state.neu) {
81
+ if (!assignment.has(n.key)) {
82
+ const minted = mintId("b");
83
+ assignment.set(n.key, minted);
84
+ dispositions.push({ blockId: minted, kind: "inserted", confidence: null, reason: null, matcherV: state.config.matcherV, detail: {} });
85
+ }
86
+ }
87
+ // Remaining old blocks (not carried) → deleted.
88
+ const deleted = [];
89
+ for (const o of state.old) {
90
+ if (!state.usedOld.has(o.blockId)) {
91
+ deleted.push(o.blockId);
92
+ dispositions.push({ blockId: o.blockId, kind: "deleted", confidence: null, reason: "tombstone", matcherV: state.config.matcherV, detail: {} });
93
+ }
94
+ }
95
+ return { assignment, dispositions, deleted, consumedPool };
96
+ }
97
+ function bulkRewrite(state) {
98
+ // Mint everything; emit one doc-scoped bulk_rewrite + deleted for all old.
99
+ const assignment = new Map();
100
+ const dispositions = [
101
+ { blockId: "DOC", kind: "bulk_rewrite", confidence: null, reason: null, matcherV: state.config.matcherV, detail: { unmatchedFrac: (state.neu.length - state.usedNew.size) / state.neu.length } },
102
+ ];
103
+ for (const n of state.neu) {
104
+ const minted = mintId("b");
105
+ assignment.set(n.key, minted);
106
+ dispositions.push({ blockId: minted, kind: "inserted", confidence: null, reason: null, matcherV: state.config.matcherV, detail: {} });
107
+ }
108
+ const deleted = [];
109
+ for (const o of state.old) {
110
+ deleted.push(o.blockId);
111
+ dispositions.push({ blockId: o.blockId, kind: "deleted", confidence: null, reason: "tombstone", matcherV: state.config.matcherV, detail: {} });
112
+ }
113
+ return { assignment, dispositions, deleted, consumedPool: [] };
114
+ }
115
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1,7 @@
1
+ export declare function tokenize(text: string): string[];
2
+ /** Token 3-gram shingles (padded so short texts still produce shingles). */
3
+ export declare function shingles(text: string, n?: number): Set<string>;
4
+ /** Dice coefficient over two shingle sets: 2|A∩B| / (|A|+|B|). */
5
+ export declare function dice(a: Set<string>, b: Set<string>): number;
6
+ export declare function textSim(a: string, b: string): number;
7
+ export declare function tokenCount(text: string): number;
@@ -0,0 +1,41 @@
1
+ // Text similarity (03 §5): token 3-gram shingle Dice coefficient over
2
+ // normalized text. Deterministic; no clock/RNG.
3
+ export function tokenize(text) {
4
+ return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
5
+ }
6
+ /** Token 3-gram shingles (padded so short texts still produce shingles). */
7
+ export function shingles(text, n = 3) {
8
+ const tokens = tokenize(text);
9
+ const out = new Set();
10
+ if (tokens.length === 0)
11
+ return out;
12
+ if (tokens.length < n) {
13
+ out.add(tokens.join(" "));
14
+ return out;
15
+ }
16
+ for (let i = 0; i + n <= tokens.length; i++) {
17
+ out.add(tokens.slice(i, i + n).join(" "));
18
+ }
19
+ return out;
20
+ }
21
+ /** Dice coefficient over two shingle sets: 2|A∩B| / (|A|+|B|). */
22
+ export function dice(a, b) {
23
+ if (a.size === 0 && b.size === 0)
24
+ return 1;
25
+ if (a.size === 0 || b.size === 0)
26
+ return 0;
27
+ let inter = 0;
28
+ for (const s of a)
29
+ if (b.has(s))
30
+ inter++;
31
+ return (2 * inter) / (a.size + b.size);
32
+ }
33
+ export function textSim(a, b) {
34
+ if (a === b)
35
+ return 1;
36
+ return dice(shingles(a), shingles(b));
37
+ }
38
+ export function tokenCount(text) {
39
+ return tokenize(text).length;
40
+ }
41
+ //# sourceMappingURL=similarity.js.map
@@ -0,0 +1,46 @@
1
+ export type DispositionKind = "same" | "edited" | "moved" | "edited_moved" | "inserted" | "deleted" | "split_from" | "merged_into" | "copied_from" | "resurrected" | "bulk_rewrite";
2
+ export type Reason = "exact_hash" | "normalized_hash" | "anchor" | "context_unique" | "scored" | "tombstone" | "api";
3
+ export interface MatchBlock {
4
+ /** present on old blocks; undefined on new blocks until assigned */
5
+ blockId?: string;
6
+ type: string;
7
+ rawHashHex: string;
8
+ normHashHex: string;
9
+ text: string;
10
+ anchors: string[];
11
+ parentKey: string | null;
12
+ index: number;
13
+ /** stable positional key: parentKey + '/' + index */
14
+ key: string;
15
+ }
16
+ export interface Disposition {
17
+ blockId: string;
18
+ kind: DispositionKind;
19
+ confidence: number | null;
20
+ reason: Reason | null;
21
+ matcherV: string;
22
+ detail: Record<string, unknown>;
23
+ }
24
+ export interface ReconcileConfig {
25
+ matcherV: string;
26
+ thetaAccept: number;
27
+ thetaSmall: number;
28
+ thetaXdoc: number;
29
+ splitCoverage: number;
30
+ splitDominantShare: number;
31
+ copySim: number;
32
+ bulkUnmatchedFrac: number;
33
+ bulkMinBlocks: number;
34
+ maxScoredBlocks: number;
35
+ smallBlockTokens: number;
36
+ contextSimFloor: number;
37
+ }
38
+ export declare const DEFAULT_CONFIG: ReconcileConfig;
39
+ /** The result of matching one document's old tree against new blocks. */
40
+ export interface MatchResult {
41
+ /** new block key → assigned id (carried) or minted id */
42
+ assignment: Map<string, string>;
43
+ dispositions: Disposition[];
44
+ /** old block ids that were not carried (deleted) */
45
+ deleted: string[];
46
+ }
@@ -0,0 +1,19 @@
1
+ // Reconciliation types (reconciliation-spec). The matcher takes the last
2
+ // persisted block tree (blocks carrying ids) and the newly parsed tree (blocks
3
+ // without ids) and produces an assignment: each new block either CARRIES an
4
+ // existing id or is MINTED, plus a disposition per carry/mint.
5
+ export const DEFAULT_CONFIG = {
6
+ matcherV: "m1.0",
7
+ thetaAccept: 0.62,
8
+ thetaSmall: 0.8,
9
+ thetaXdoc: 0.8,
10
+ splitCoverage: 0.8,
11
+ splitDominantShare: 0.7,
12
+ copySim: 0.95,
13
+ bulkUnmatchedFrac: 0.45,
14
+ bulkMinBlocks: 100,
15
+ maxScoredBlocks: 2000,
16
+ smallBlockTokens: 8,
17
+ contextSimFloor: 0.35,
18
+ };
19
+ //# sourceMappingURL=types.js.map