@omgbase/core 0.1.0 → 0.2.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.
- package/README.md +172 -0
- package/dist/src/cli/eval-matcher.d.ts +6 -1
- package/dist/src/cli/eval-matcher.js +14 -2
- package/dist/src/core/hash.d.ts +25 -1
- package/dist/src/core/hash.js +110 -10
- package/dist/src/core/ids.d.ts +12 -0
- package/dist/src/core/ids.js +43 -1
- package/dist/src/core/ingest.d.ts +22 -2
- package/dist/src/core/ingest.js +92 -18
- package/dist/src/core/parse/parse.d.ts +1 -1
- package/dist/src/core/parse/parse.js +31 -9
- package/dist/src/core/store/nodes.d.ts +13 -0
- package/dist/src/core/store/nodes.js +24 -0
- package/dist/src/core/utf8.d.ts +29 -0
- package/dist/src/core/utf8.js +96 -0
- package/dist/src/format/adapter.d.ts +7 -1
- package/dist/src/format/markdown.js +9 -18
- package/dist/src/graph/history.d.ts +6 -0
- package/dist/src/graph/history.js +13 -2
- package/dist/src/index.d.ts +8 -5
- package/dist/src/index.js +8 -5
- package/dist/src/mcp/graph.js +8 -4
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +17 -6
- package/dist/src/mcp/server.js +12 -7
- package/dist/src/mutate/apply.d.ts +7 -0
- package/dist/src/mutate/apply.js +17 -7
- package/dist/src/mutate/docs.d.ts +2 -0
- package/dist/src/mutate/docs.js +10 -6
- package/dist/src/mutate/known-ids.js +9 -1
- package/dist/src/mutate/macros.js +9 -2
- package/dist/src/mutate/ops.d.ts +2 -1
- package/dist/src/mutate/ops.js +24 -2
- package/dist/src/mutate/plan-update.d.ts +7 -1
- package/dist/src/mutate/plan-update.js +3 -1
- package/dist/src/oqx-js/context.js +43 -6
- package/dist/src/reconcile/eval/generator.d.ts +24 -1
- package/dist/src/reconcile/eval/generator.js +248 -0
- package/dist/src/reconcile/eval/metrics.d.ts +33 -9
- package/dist/src/reconcile/eval/metrics.js +55 -32
- package/dist/src/reconcile/flatten.js +7 -5
- package/dist/src/reconcile/phase5.d.ts +2 -1
- package/dist/src/reconcile/phase5.js +27 -10
- package/dist/src/reconcile/phase6a.js +17 -5
- package/dist/src/reconcile/phases.d.ts +44 -6
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.js +16 -9
- package/dist/src/reconcile/types.d.ts +4 -1
- package/dist/src/reconcile/types.js +11 -2
- package/dist/src/search/external.d.ts +10 -0
- package/dist/src/search/external.js +18 -1
- package/dist/src/search/provider.d.ts +7 -3
- package/dist/src/search/provider.js +8 -7
- package/dist/src/search/rrf.js +3 -0
- package/dist/src/search/vector.js +21 -6
- package/dist/src/sync/attach.d.ts +7 -2
- package/dist/src/sync/attach.js +10 -8
- package/dist/src/sync/checkpoint.d.ts +18 -1
- package/dist/src/sync/checkpoint.js +31 -29
- package/dist/src/sync/driver.d.ts +15 -4
- package/dist/src/sync/driver.js +18 -43
- package/dist/src/sync/external-source.js +7 -1
- package/dist/src/sync/freshness.d.ts +65 -14
- package/dist/src/sync/freshness.js +107 -96
- package/dist/src/sync/fs-util.d.ts +19 -0
- package/dist/src/sync/fs-util.js +24 -1
- package/dist/src/sync/observe.d.ts +53 -5
- package/dist/src/sync/observe.js +162 -21
- package/dist/src/sync/reconciling-ingest.d.ts +42 -2
- package/dist/src/sync/reconciling-ingest.js +82 -10
- package/dist/src/sync/recovery.d.ts +5 -1
- package/dist/src/sync/recovery.js +8 -8
- package/dist/src/sync/workspace.d.ts +11 -3
- package/dist/src/sync/workspace.js +39 -30
- package/dist/src/sync/writer-lock.js +16 -26
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
import { type ReconcileConfig } from "../reconcile/types.js";
|
|
3
3
|
import { type ApplyResult } from "./apply.js";
|
|
4
|
+
import type { DocStore } from "./doc-store.js";
|
|
4
5
|
import { type Opset } from "./opset.js";
|
|
5
6
|
export interface PlanUpdateOptions {
|
|
6
7
|
config?: ReconcileConfig;
|
|
@@ -13,7 +14,10 @@ export interface PlanUpdateOptions {
|
|
|
13
14
|
export declare function planUpdate(store: Store, repoId: string, rootPath: string, docRef: string, content: string, opts?: PlanUpdateOptions): Opset;
|
|
14
15
|
export interface ApplyOpsetRequest {
|
|
15
16
|
repoId: string;
|
|
16
|
-
|
|
17
|
+
/** Working-tree root for the default filesystem write target; omit only with an explicit `docStore`. */
|
|
18
|
+
rootPath?: string;
|
|
19
|
+
/** Write target (ADR-014 §5); defaults to a filesystem store at `rootPath`. */
|
|
20
|
+
docStore?: DocStore;
|
|
17
21
|
opset: Opset;
|
|
18
22
|
origin: {
|
|
19
23
|
actor: string;
|
|
@@ -21,6 +25,8 @@ export interface ApplyOpsetRequest {
|
|
|
21
25
|
};
|
|
22
26
|
dryRun?: boolean;
|
|
23
27
|
omgbaseDir?: string;
|
|
28
|
+
/** Commit timestamp (RFC 3339 UTC); defaults to now (see `ApplyRequest.ts`). */
|
|
29
|
+
ts?: string;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* Apply an opset through the kernel write path. Validates the plan's
|
|
@@ -159,11 +159,13 @@ export function applyOpset(store, req) {
|
|
|
159
159
|
}
|
|
160
160
|
return apply(store, {
|
|
161
161
|
repoId: req.repoId,
|
|
162
|
-
rootPath: req.rootPath,
|
|
162
|
+
...(req.rootPath !== undefined ? { rootPath: req.rootPath } : {}),
|
|
163
|
+
...(req.docStore !== undefined ? { docStore: req.docStore } : {}),
|
|
163
164
|
ops: opsetKernelOps(opset),
|
|
164
165
|
origin: req.origin,
|
|
165
166
|
...(req.dryRun !== undefined ? { dryRun: req.dryRun } : {}),
|
|
166
167
|
...(req.omgbaseDir !== undefined ? { omgbaseDir: req.omgbaseDir } : {}),
|
|
168
|
+
...(req.ts !== undefined ? { ts: req.ts } : {}),
|
|
167
169
|
...(opset.frontmatter ? { setFrontmatter: [{ doc: opset.precondition.doc, raw: opset.frontmatter.raw }] } : {}),
|
|
168
170
|
});
|
|
169
171
|
}
|
|
@@ -159,11 +159,49 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
159
159
|
const r = one(`SELECT ordinal FROM blocks WHERE doc_id = ? AND block_id = ?`, block.doc_id, first);
|
|
160
160
|
return r?.ordinal ?? block.ordinal;
|
|
161
161
|
};
|
|
162
|
+
// ---- a document's live blocks in DOCUMENT order (spec/surface §1.2) --------
|
|
163
|
+
// Pre-order over the containment forest (children by parent_block, siblings by
|
|
164
|
+
// (ordinal, block_id)); each row is paired with its top-level ancestor's
|
|
165
|
+
// ordinal, which is what section ranges are expressed in.
|
|
166
|
+
const docBlocksPreorder = (docId, path) => {
|
|
167
|
+
const rows = all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.doc_id = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, path, docId);
|
|
168
|
+
const ids = new Set(rows.map((r) => r.block_id));
|
|
169
|
+
const byParent = new Map();
|
|
170
|
+
for (const r of rows) {
|
|
171
|
+
const parent = r.parent_block != null && ids.has(r.parent_block) ? r.parent_block : null;
|
|
172
|
+
let list = byParent.get(parent);
|
|
173
|
+
if (!list)
|
|
174
|
+
byParent.set(parent, (list = []));
|
|
175
|
+
list.push(r);
|
|
176
|
+
}
|
|
177
|
+
const out = [];
|
|
178
|
+
const walk = (parent, top) => {
|
|
179
|
+
for (const r of byParent.get(parent) ?? []) {
|
|
180
|
+
const t = top ?? r.ordinal;
|
|
181
|
+
out.push({ row: r, top: t });
|
|
182
|
+
walk(r.block_id, t);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
walk(null, null);
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
// A document's nodes in DOCUMENT order: by the owning block's pre-order rank
|
|
189
|
+
// (block-less nodes first), then span_start, then node_id. Node ids are content
|
|
190
|
+
// hashes over random doc ids, so an id order would differ per ingest.
|
|
191
|
+
const docNodesInOrder = (docId, path) => {
|
|
192
|
+
const rank = new Map();
|
|
193
|
+
docBlocksPreorder(docId, path).forEach((x, i) => rank.set(x.row.block_id, i));
|
|
194
|
+
const rows = all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.doc_id = ?`, path, docId);
|
|
195
|
+
const pos = (n) => (n.block_id == null ? -1 : rank.get(n.block_id) ?? Number.MAX_SAFE_INTEGER);
|
|
196
|
+
const span = (n) => (typeof n.span_start === "number" ? n.span_start : -1);
|
|
197
|
+
rows.sort((a, b) => pos(a) - pos(b) || span(a) - span(b) || (a.node_id < b.node_id ? -1 : a.node_id === b.node_id ? 0 : 1));
|
|
198
|
+
return tagAll(rows, "nodes");
|
|
199
|
+
};
|
|
162
200
|
// ---- relations (return row arrays) ----------------------------------------
|
|
163
201
|
const rel = {
|
|
164
202
|
docs: {
|
|
165
|
-
nodes: (r) =>
|
|
166
|
-
blocks: (r) => tagAll(
|
|
203
|
+
nodes: (r) => docNodesInOrder(r.doc_id, r.path),
|
|
204
|
+
blocks: (r) => tagAll(docBlocksPreorder(r.doc_id, r.path).map((x) => x.row), "blocks"),
|
|
167
205
|
out: (r) => tagAll(all(`SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
|
|
168
206
|
WHERE e.src_doc = ? AND e.to_commit IS NULL AND d2.repo_id = ? AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id`, r.doc_id, repoId), "docs"),
|
|
169
207
|
in: (r) => tagAll(all(`SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
|
|
@@ -174,7 +212,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
174
212
|
},
|
|
175
213
|
blocks: {
|
|
176
214
|
children: (r) => tagAll(all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.parent_block = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, r.__path, r.block_id), "blocks"),
|
|
177
|
-
nodes: (r) => tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.block_id =
|
|
215
|
+
nodes: (r) => tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.block_id = ? ORDER BY n.span_start, n.node_id`, r.__path, r.block_id), "nodes"),
|
|
178
216
|
out_edges: (r) => tagAll(all(`SELECT e.*, ? AS __path FROM edges e WHERE e.src_block = ? AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.__path, r.block_id), "edges"),
|
|
179
217
|
// enclosing section node(s): md:section whose range contains this block's top ordinal.
|
|
180
218
|
section: (r) => {
|
|
@@ -190,9 +228,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
190
228
|
const f = jattr(r, "first_ordinal"), l = jattr(r, "last_ordinal");
|
|
191
229
|
if (f == null || l == null)
|
|
192
230
|
return [];
|
|
193
|
-
// blocks whose top-level ordinal falls in the section range.
|
|
194
|
-
const
|
|
195
|
-
const kept = rows.filter((b) => { const t = topOrdinal(b); return t >= f && t <= l; });
|
|
231
|
+
// blocks whose top-level ancestor's ordinal falls in the section range, in document order.
|
|
232
|
+
const kept = docBlocksPreorder(r.doc_id, r.__path).filter((x) => x.top >= f && x.top <= l).map((x) => x.row);
|
|
196
233
|
return tagAll(kept, "blocks");
|
|
197
234
|
},
|
|
198
235
|
subsections: (r) => {
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { MatchBlock } from "../types.js";
|
|
2
2
|
export type EditClass = "edit" | "insert" | "delete" | "move" | "reorder" | "split" | "merge" | "copy";
|
|
3
|
+
/** Structured-mode classes (list-item edits; see generateStructuredCase). */
|
|
4
|
+
export type StructuredEditClass = "item-edit-mid" | "item-edit-last" | "item-insert" | "item-delete" | "item-move" | "item-gains-nested-list" | "item-reorder"
|
|
5
|
+
/** an item deleted and an unrelated item inserted in its slot (the new item must mint; m2.3 singleton-rule cost) */
|
|
6
|
+
| "item-replace"
|
|
7
|
+
/** the list container whose items were edited (must carry) */
|
|
8
|
+
| "list"
|
|
9
|
+
/** any block the script did not touch (must carry) */
|
|
10
|
+
| "same"
|
|
11
|
+
/** the paragraph / nested list / nested items minted under an item that gained a nested list */
|
|
12
|
+
| "nested-new";
|
|
3
13
|
export interface GroundTruth {
|
|
4
14
|
/** new block key → old id it should carry (absent ⇒ should be minted) */
|
|
5
15
|
carries: Map<string, string>;
|
|
6
16
|
/** old ids expected to be deleted */
|
|
7
17
|
deleted: Set<string>;
|
|
8
18
|
/** per new key, the edit class that produced it (for per-class metrics) */
|
|
9
|
-
classOf: Map<string, EditClass>;
|
|
19
|
+
classOf: Map<string, EditClass | StructuredEditClass>;
|
|
10
20
|
}
|
|
11
21
|
export interface GeneratedCase {
|
|
12
22
|
old: MatchBlock[];
|
|
@@ -19,3 +29,16 @@ export declare function generateCase(seed: number, opts?: {
|
|
|
19
29
|
size?: number;
|
|
20
30
|
intensity?: number;
|
|
21
31
|
}): GeneratedCase;
|
|
32
|
+
export interface StructuredOpts {
|
|
33
|
+
/** top-level blocks in the base document (default 10) */
|
|
34
|
+
size?: number;
|
|
35
|
+
/** edits per case ≈ size × intensity (default 0.3 ⇒ 3) */
|
|
36
|
+
intensity?: number;
|
|
37
|
+
/** words per list item; a number fixes it, absent ⇒ 2–6 per item */
|
|
38
|
+
itemWords?: number;
|
|
39
|
+
}
|
|
40
|
+
type Cls = StructuredEditClass;
|
|
41
|
+
export declare const STRUCTURED_ITEM_CLASSES: readonly Cls[];
|
|
42
|
+
/** Generate one structured case (see the mode comment above). */
|
|
43
|
+
export declare function generateStructuredCase(seed: number, opts?: StructuredOpts): GeneratedCase;
|
|
44
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sha256, normalizeVisibleText } from "../../core/hash.js";
|
|
2
|
+
import { flatten } from "../flatten.js";
|
|
2
3
|
// Small deterministic PRNG (mulberry32).
|
|
3
4
|
export function rng(seed) {
|
|
4
5
|
let a = seed >>> 0;
|
|
@@ -126,4 +127,251 @@ export function generateCase(seed, opts = {}) {
|
|
|
126
127
|
deleted.add(b.id);
|
|
127
128
|
return { old, neu, truth: { carries, deleted, classOf } };
|
|
128
129
|
}
|
|
130
|
+
// A larger vocabulary than the flat mode's so short items stay unique within a
|
|
131
|
+
// document (ground truth needs distinguishable items; duplicates would make the
|
|
132
|
+
// "right" id a coin toss the matcher cannot be blamed for).
|
|
133
|
+
const ITEM_WORDS = ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa " +
|
|
134
|
+
"quebec romeo sierra tango uniform victor whiskey xray yankee zulu apple banana cherry damson elder " +
|
|
135
|
+
"fig grape hazel iris jasmine kale lemon mango nectar olive peach quince radish sage thyme umber " +
|
|
136
|
+
"violet walnut yarrow zest amber birch cedar dune ember flint gorse heath ivy jade kelp lotus moss " +
|
|
137
|
+
"nickel onyx pearl quartz reed slate tundra vale willow").split(" ");
|
|
138
|
+
function itemToSource(it, withId) {
|
|
139
|
+
const own = it.words.join(" ");
|
|
140
|
+
if (it.nested === null) {
|
|
141
|
+
const s = { type: "list_item", raw: `- ${own}`, children: [] };
|
|
142
|
+
if (withId && it.id)
|
|
143
|
+
s.blockId = it.id;
|
|
144
|
+
return s;
|
|
145
|
+
}
|
|
146
|
+
// Parser shape: item raw keeps the nested lines; children are the folded
|
|
147
|
+
// paragraph and the nested list; the nested list's first line starts at content.
|
|
148
|
+
const nestedItemRaws = it.nested.map((w) => `- ${w}`);
|
|
149
|
+
const nestedList = {
|
|
150
|
+
type: "list",
|
|
151
|
+
raw: nestedItemRaws.join("\n "),
|
|
152
|
+
children: nestedItemRaws.map((raw) => ({ type: "list_item", raw, children: [] })),
|
|
153
|
+
};
|
|
154
|
+
const s = {
|
|
155
|
+
type: "list_item",
|
|
156
|
+
raw: `- ${own}\n ${nestedItemRaws.join("\n ")}`,
|
|
157
|
+
children: [{ type: "paragraph", raw: own, children: [] }, nestedList],
|
|
158
|
+
};
|
|
159
|
+
if (withId && it.id)
|
|
160
|
+
s.blockId = it.id;
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
function blockToSource(b, withId) {
|
|
164
|
+
if (b.kind === "list") {
|
|
165
|
+
const children = b.items.map((it) => itemToSource(it, withId));
|
|
166
|
+
const s = { type: "list", raw: children.map((c) => c.raw).join("\n"), children };
|
|
167
|
+
if (withId)
|
|
168
|
+
s.blockId = b.id;
|
|
169
|
+
return s;
|
|
170
|
+
}
|
|
171
|
+
const s = { type: b.kind, raw: b.raw, children: [] };
|
|
172
|
+
if (withId)
|
|
173
|
+
s.blockId = b.id;
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
/** Walk a FlatSource tree in flatten()'s order, yielding the same positional keys. */
|
|
177
|
+
function walkKeys(list, parentKey, fn) {
|
|
178
|
+
list.forEach((b, index) => {
|
|
179
|
+
const key = `${parentKey ?? ""}/${index}`;
|
|
180
|
+
fn(b, key);
|
|
181
|
+
walkKeys(b.children, key, fn);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export const STRUCTURED_ITEM_CLASSES = [
|
|
185
|
+
"item-edit-mid", "item-edit-last", "item-insert", "item-delete", "item-move", "item-gains-nested-list", "item-reorder",
|
|
186
|
+
"item-replace",
|
|
187
|
+
];
|
|
188
|
+
/** Generate one structured case (see the mode comment above). */
|
|
189
|
+
export function generateStructuredCase(seed, opts = {}) {
|
|
190
|
+
// Offset the seed space so structured cases never share a PRNG stream with flat ones.
|
|
191
|
+
const r = rng((seed + 0x5a5a0000) >>> 0);
|
|
192
|
+
const size = opts.size ?? 10;
|
|
193
|
+
const intensity = opts.intensity ?? 0.3;
|
|
194
|
+
const pick = (arr) => arr[Math.floor(r() * arr.length)];
|
|
195
|
+
const wordCount = () => opts.itemWords ?? 2 + Math.floor(r() * 5);
|
|
196
|
+
let nextId = 0;
|
|
197
|
+
const mint = () => `b_${nextId++}`;
|
|
198
|
+
const used = new Set(); // item texts, unique across the document
|
|
199
|
+
const freshWords = (n) => {
|
|
200
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
201
|
+
const w = Array.from({ length: n }, () => pick(ITEM_WORDS));
|
|
202
|
+
const key = w.join(" ");
|
|
203
|
+
if (!used.has(key)) {
|
|
204
|
+
used.add(key);
|
|
205
|
+
return w;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Vocabulary exhausted for this length (only plausible for 1-word items): accept a repeat.
|
|
209
|
+
return Array.from({ length: n }, () => pick(ITEM_WORDS));
|
|
210
|
+
};
|
|
211
|
+
const replaceWord = (words, at) => {
|
|
212
|
+
// Replace so the resulting item text is new to the document (bounded retries).
|
|
213
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
214
|
+
const w = pick(ITEM_WORDS);
|
|
215
|
+
if (w === words[at])
|
|
216
|
+
continue;
|
|
217
|
+
const trial = [...words];
|
|
218
|
+
trial[at] = w;
|
|
219
|
+
const key = trial.join(" ");
|
|
220
|
+
if (!used.has(key)) {
|
|
221
|
+
used.add(key);
|
|
222
|
+
words[at] = w;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
words[at] = pick(ITEM_WORDS.filter((w) => w !== words[at]));
|
|
227
|
+
};
|
|
228
|
+
// Base document: ids are assigned in document (pre-order) order.
|
|
229
|
+
const blocks = [];
|
|
230
|
+
for (let i = 0; i < size; i++) {
|
|
231
|
+
const roll = r();
|
|
232
|
+
// Guarantee two lists so there is always something to edit.
|
|
233
|
+
const wantList = roll < 0.5 || (i >= size - 2 && blocks.filter((b) => b.kind === "list").length < 2);
|
|
234
|
+
if (wantList) {
|
|
235
|
+
const id = mint();
|
|
236
|
+
const n = 3 + Math.floor(r() * 5); // 3–7 items
|
|
237
|
+
const items = Array.from({ length: n }, () => ({ id: mint(), words: freshWords(wordCount()), cls: null, nested: null }));
|
|
238
|
+
blocks.push({ kind: "list", id, items, cls: null });
|
|
239
|
+
}
|
|
240
|
+
else if (roll < 0.65) {
|
|
241
|
+
blocks.push({ kind: "heading", id: mint(), raw: `${"#".repeat(1 + Math.floor(r() * 3))} ${sentence(r, 2 + Math.floor(r() * 3))}` });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
blocks.push({ kind: "paragraph", id: mint(), raw: sentence(r, 8 + Math.floor(r() * 8)) });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const oldTree = blocks.map((b) => blockToSource(b, true));
|
|
248
|
+
const old = flatten(oldTree);
|
|
249
|
+
// Scripted item edits.
|
|
250
|
+
const deleted = new Set();
|
|
251
|
+
const lists = blocks.filter((b) => b.kind === "list");
|
|
252
|
+
const nEdits = Math.max(1, Math.floor(size * intensity));
|
|
253
|
+
for (let e = 0; e < nEdits; e++) {
|
|
254
|
+
const list = pick(lists);
|
|
255
|
+
const cls = pick(STRUCTURED_ITEM_CLASSES);
|
|
256
|
+
const untouched = list.items.map((it, i) => [it, i]).filter(([it]) => it.id !== null && it.cls === null);
|
|
257
|
+
if (untouched.length === 0)
|
|
258
|
+
continue;
|
|
259
|
+
const [it, idx] = pick(untouched);
|
|
260
|
+
switch (cls) {
|
|
261
|
+
case "item-edit-mid": {
|
|
262
|
+
// change 1–2 words, none of them the last one
|
|
263
|
+
const positions = it.words.length - 1;
|
|
264
|
+
if (positions < 1)
|
|
265
|
+
continue;
|
|
266
|
+
const changes = Math.min(positions, 1 + Math.floor(r() * 2));
|
|
267
|
+
const chosen = new Set();
|
|
268
|
+
while (chosen.size < changes)
|
|
269
|
+
chosen.add(Math.floor(r() * positions));
|
|
270
|
+
used.delete(it.words.join(" "));
|
|
271
|
+
for (const at of chosen)
|
|
272
|
+
replaceWord(it.words, at);
|
|
273
|
+
it.cls = cls;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "item-edit-last": {
|
|
277
|
+
used.delete(it.words.join(" "));
|
|
278
|
+
replaceWord(it.words, it.words.length - 1);
|
|
279
|
+
it.cls = cls;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case "item-insert": {
|
|
283
|
+
const at = Math.floor(r() * (list.items.length + 1));
|
|
284
|
+
list.items.splice(at, 0, { id: null, words: freshWords(wordCount()), cls, nested: null });
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "item-delete": {
|
|
288
|
+
if (list.items.length < 3)
|
|
289
|
+
continue;
|
|
290
|
+
deleted.add(it.id);
|
|
291
|
+
list.items.splice(idx, 1);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "item-move": {
|
|
295
|
+
if (list.items.length < 3)
|
|
296
|
+
continue;
|
|
297
|
+
list.items.splice(idx, 1);
|
|
298
|
+
let dest = Math.floor(r() * list.items.length);
|
|
299
|
+
if (dest >= idx)
|
|
300
|
+
dest++; // any position other than the original
|
|
301
|
+
list.items.splice(Math.min(dest, list.items.length), 0, it);
|
|
302
|
+
it.cls = cls;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case "item-reorder": {
|
|
306
|
+
const j = idx + 1 < list.items.length ? idx + 1 : idx - 1;
|
|
307
|
+
if (j < 0)
|
|
308
|
+
continue;
|
|
309
|
+
const other = list.items[j];
|
|
310
|
+
if (other.id === null || other.cls !== null)
|
|
311
|
+
continue;
|
|
312
|
+
list.items[idx] = other;
|
|
313
|
+
list.items[j] = it;
|
|
314
|
+
it.cls = cls;
|
|
315
|
+
other.cls = cls;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case "item-replace": {
|
|
319
|
+
// Delete the item and put an unrelated fresh item in its slot: the same
|
|
320
|
+
// shape the singleton rule (spec/reconcile §5 phase 4a step 5) reads as
|
|
321
|
+
// an edit, so this class measures its cost (the new item must mint).
|
|
322
|
+
deleted.add(it.id);
|
|
323
|
+
list.items[idx] = { id: null, words: freshWords(wordCount()), cls, nested: null };
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "item-gains-nested-list": {
|
|
327
|
+
it.nested = Array.from({ length: 2 + Math.floor(r() * 2) }, () => freshWords(1)[0]);
|
|
328
|
+
it.cls = cls;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
default:
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
list.cls = "list";
|
|
335
|
+
}
|
|
336
|
+
// New tree: built with the truth ids attached, harvested, then stripped.
|
|
337
|
+
const newTree = blocks.map((b) => blockToSource(b, true));
|
|
338
|
+
const carries = new Map();
|
|
339
|
+
const classOf = new Map();
|
|
340
|
+
const clsByKey = new Map();
|
|
341
|
+
{
|
|
342
|
+
// Classes by key: mirror blockToSource's shape.
|
|
343
|
+
const listIdx = new Map();
|
|
344
|
+
blocks.forEach((b, i) => listIdx.set(b, i));
|
|
345
|
+
for (const b of blocks) {
|
|
346
|
+
const bk = `/${listIdx.get(b)}`;
|
|
347
|
+
if (b.kind !== "list") {
|
|
348
|
+
clsByKey.set(bk, "same");
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
clsByKey.set(bk, b.cls ?? "same");
|
|
352
|
+
b.items.forEach((it, i) => {
|
|
353
|
+
const ik = `${bk}/${i}`;
|
|
354
|
+
clsByKey.set(ik, it.cls ?? (it.id === null ? "item-insert" : "same"));
|
|
355
|
+
if (it.nested !== null) {
|
|
356
|
+
clsByKey.set(`${ik}/0`, "nested-new");
|
|
357
|
+
clsByKey.set(`${ik}/1`, "nested-new");
|
|
358
|
+
it.nested.forEach((_, j) => clsByKey.set(`${ik}/1/${j}`, "nested-new"));
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
walkKeys(newTree, null, (b, key) => {
|
|
364
|
+
const cls = clsByKey.get(key) ?? "same";
|
|
365
|
+
classOf.set(key, cls);
|
|
366
|
+
if (b.blockId && !deleted.has(b.blockId))
|
|
367
|
+
carries.set(key, b.blockId);
|
|
368
|
+
delete b.blockId;
|
|
369
|
+
});
|
|
370
|
+
const neu = flatten(newTree);
|
|
371
|
+
const carriedIds = new Set(carries.values());
|
|
372
|
+
for (const b of old)
|
|
373
|
+
if (!carriedIds.has(b.blockId))
|
|
374
|
+
deleted.add(b.blockId);
|
|
375
|
+
return { old, neu, truth: { carries, deleted, classOf } };
|
|
376
|
+
}
|
|
129
377
|
//# sourceMappingURL=generator.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type EditClass, type GeneratedCase } from "./generator.js";
|
|
1
|
+
import { type ReconcileConfig } from "../types.js";
|
|
2
|
+
import { type EditClass, type GeneratedCase, type StructuredEditClass } from "./generator.js";
|
|
3
3
|
export interface ClassMetrics {
|
|
4
|
+
/** new blocks of this class (carried or minted in truth) */
|
|
5
|
+
newBlocks: number;
|
|
4
6
|
truePairs: number;
|
|
5
7
|
carriedPairs: number;
|
|
6
8
|
correctCarries: number;
|
|
@@ -9,24 +11,46 @@ export interface ClassMetrics {
|
|
|
9
11
|
}
|
|
10
12
|
export interface EvalReport {
|
|
11
13
|
cases: number;
|
|
14
|
+
mode: EvalMode;
|
|
12
15
|
overall: ClassMetrics;
|
|
13
16
|
perClass: Record<string, ClassMetrics>;
|
|
17
|
+
/** old ids the truth deletes, and how many of them the matcher carried anyway */
|
|
18
|
+
deleted: {
|
|
19
|
+
expected: number;
|
|
20
|
+
wronglyCarried: number;
|
|
21
|
+
};
|
|
22
|
+
/** the effective config the suite ran under */
|
|
14
23
|
config: ReconcileConfig;
|
|
15
24
|
}
|
|
16
|
-
|
|
25
|
+
export type EvalMode = "flat" | "structured";
|
|
26
|
+
type Agg = {
|
|
27
|
+
newBlocks: number;
|
|
17
28
|
truePairs: number;
|
|
18
29
|
carriedPairs: number;
|
|
19
30
|
correctCarries: number;
|
|
20
31
|
};
|
|
21
32
|
export declare function evaluateCase(c: GeneratedCase, config?: ReconcileConfig): {
|
|
22
|
-
overall:
|
|
23
|
-
perClass: Map<string,
|
|
33
|
+
overall: Agg;
|
|
34
|
+
perClass: Map<string, Agg>;
|
|
35
|
+
deleted: {
|
|
36
|
+
expected: number;
|
|
37
|
+
wronglyCarried: number;
|
|
38
|
+
};
|
|
24
39
|
};
|
|
25
|
-
|
|
26
|
-
export declare function runEval(opts?: {
|
|
40
|
+
export interface RunEvalOptions {
|
|
27
41
|
n?: number;
|
|
42
|
+
/** flat: top-level paragraphs per doc (12); structured: top-level blocks per doc (10) */
|
|
28
43
|
size?: number;
|
|
29
44
|
intensity?: number;
|
|
45
|
+
/** structured mode only: words per list item (absent ⇒ 2–6 per item) */
|
|
46
|
+
itemWords?: number;
|
|
47
|
+
/** "flat" (default; the release-gate suite) or "structured" (lists of short items) */
|
|
48
|
+
mode?: EvalMode;
|
|
49
|
+
/** full config; defaults to DEFAULT_CONFIG */
|
|
30
50
|
config?: ReconcileConfig;
|
|
31
|
-
})
|
|
32
|
-
|
|
51
|
+
/** fields to override on top of `config` (e.g. `{ contextSimFloor: 0.3 }`) */
|
|
52
|
+
configOverrides?: Partial<ReconcileConfig>;
|
|
53
|
+
}
|
|
54
|
+
/** Run the synthetic suite over `n` seeded cases and produce the report. */
|
|
55
|
+
export declare function runEval(opts?: RunEvalOptions): EvalReport;
|
|
56
|
+
export type { EditClass, StructuredEditClass };
|
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { reconcileDocument } from "../reconcile.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_CONFIG } from "../types.js";
|
|
3
|
+
import { generateCase, generateStructuredCase, } from "./generator.js";
|
|
3
4
|
function blank() {
|
|
4
|
-
return { truePairs: 0, carriedPairs: 0, correctCarries: 0 };
|
|
5
|
+
return { newBlocks: 0, truePairs: 0, carriedPairs: 0, correctCarries: 0 };
|
|
6
|
+
}
|
|
7
|
+
function add(into, from) {
|
|
8
|
+
into.newBlocks += from.newBlocks;
|
|
9
|
+
into.truePairs += from.truePairs;
|
|
10
|
+
into.carriedPairs += from.carriedPairs;
|
|
11
|
+
into.correctCarries += from.correctCarries;
|
|
5
12
|
}
|
|
6
13
|
export function evaluateCase(c, config) {
|
|
7
14
|
const res = reconcileDocument(c.old, c.neu, config ? { config } : {});
|
|
8
15
|
const overall = blank();
|
|
9
16
|
const perClass = new Map();
|
|
10
17
|
const bucket = (cls) => perClass.get(cls) ?? perClass.set(cls, blank()).get(cls);
|
|
18
|
+
// Flat mode leaves untouched blocks unclassified and they count as "edit"
|
|
19
|
+
// (kept: the release gates were tuned against that bucketing).
|
|
20
|
+
const classOf = (key) => c.truth.classOf.get(key) ?? "edit";
|
|
21
|
+
for (const b of c.neu) {
|
|
22
|
+
overall.newBlocks++;
|
|
23
|
+
bucket(classOf(b.key)).newBlocks++;
|
|
24
|
+
}
|
|
11
25
|
// True pairs: ground-truth carries.
|
|
12
26
|
for (const [key, oldId] of c.truth.carries) {
|
|
13
27
|
overall.truePairs++;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const got = res.assignment.get(key);
|
|
17
|
-
if (got === oldId) {
|
|
28
|
+
bucket(classOf(key)).truePairs++;
|
|
29
|
+
if (res.assignment.get(key) === oldId) {
|
|
18
30
|
overall.correctCarries++;
|
|
19
|
-
bucket(
|
|
31
|
+
bucket(classOf(key)).correctCarries++;
|
|
20
32
|
}
|
|
21
33
|
}
|
|
22
34
|
// Carried pairs: assignments where the matcher reused an OLD id (not minted).
|
|
23
35
|
const oldIds = new Set(c.old.map((b) => b.blockId));
|
|
24
|
-
const
|
|
36
|
+
const carriedOld = new Set();
|
|
25
37
|
for (const [key, id] of res.assignment) {
|
|
26
38
|
if (oldIds.has(id)) {
|
|
27
39
|
overall.carriedPairs++;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// correctness already tallied above when it matches truth; recount for
|
|
31
|
-
// precision denominators handled via correctCarries computed there.
|
|
32
|
-
void truthByKey;
|
|
40
|
+
bucket(classOf(key)).carriedPairs++;
|
|
41
|
+
carriedOld.add(id);
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
|
-
|
|
44
|
+
let wronglyCarried = 0;
|
|
45
|
+
for (const id of c.truth.deleted)
|
|
46
|
+
if (carriedOld.has(id))
|
|
47
|
+
wronglyCarried++;
|
|
48
|
+
return { overall, perClass, deleted: { expected: c.truth.deleted.size, wronglyCarried } };
|
|
36
49
|
}
|
|
37
50
|
function finalize(agg) {
|
|
38
51
|
return {
|
|
@@ -44,31 +57,41 @@ function finalize(agg) {
|
|
|
44
57
|
/** Run the synthetic suite over `n` seeded cases and produce the report. */
|
|
45
58
|
export function runEval(opts = {}) {
|
|
46
59
|
const n = opts.n ?? 200;
|
|
60
|
+
const mode = opts.mode ?? "flat";
|
|
61
|
+
const config = { ...(opts.config ?? DEFAULT_CONFIG), ...(opts.configOverrides ?? {}) };
|
|
47
62
|
const overall = blank();
|
|
48
63
|
const perClass = new Map();
|
|
64
|
+
const deleted = { expected: 0, wronglyCarried: 0 };
|
|
49
65
|
for (let seed = 1; seed <= n; seed++) {
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
genOpts
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
let c;
|
|
67
|
+
if (mode === "structured") {
|
|
68
|
+
const genOpts = {};
|
|
69
|
+
if (opts.size !== undefined)
|
|
70
|
+
genOpts.size = opts.size;
|
|
71
|
+
if (opts.intensity !== undefined)
|
|
72
|
+
genOpts.intensity = opts.intensity;
|
|
73
|
+
if (opts.itemWords !== undefined)
|
|
74
|
+
genOpts.itemWords = opts.itemWords;
|
|
75
|
+
c = generateStructuredCase(seed, genOpts);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const genOpts = {};
|
|
79
|
+
if (opts.size !== undefined)
|
|
80
|
+
genOpts.size = opts.size;
|
|
81
|
+
if (opts.intensity !== undefined)
|
|
82
|
+
genOpts.intensity = opts.intensity;
|
|
83
|
+
c = generateCase(seed, genOpts);
|
|
65
84
|
}
|
|
85
|
+
const r = evaluateCase(c, config);
|
|
86
|
+
add(overall, r.overall);
|
|
87
|
+
for (const [cls, agg] of r.perClass)
|
|
88
|
+
add(perClass.get(cls) ?? perClass.set(cls, blank()).get(cls), agg);
|
|
89
|
+
deleted.expected += r.deleted.expected;
|
|
90
|
+
deleted.wronglyCarried += r.deleted.wronglyCarried;
|
|
66
91
|
}
|
|
67
92
|
const perClassOut = {};
|
|
68
93
|
for (const [cls, agg] of perClass)
|
|
69
94
|
perClassOut[cls] = finalize(agg);
|
|
70
|
-
|
|
71
|
-
const cfg = opts.config ?? undefined;
|
|
72
|
-
return { cases: n, overall: finalize(overall), perClass: perClassOut, config: cfg };
|
|
95
|
+
return { cases: n, mode, overall: finalize(overall), perClass: perClassOut, deleted, config };
|
|
73
96
|
}
|
|
74
97
|
//# sourceMappingURL=metrics.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sha256,
|
|
1
|
+
import { sha256, childQuoteDepth, visibleText } from "../core/hash.js";
|
|
2
2
|
/** Adapt a parsed RawBlock-ish tree (no ids) to FlatSource. */
|
|
3
3
|
export function fromInput(blocks) {
|
|
4
4
|
return blocks.map((b) => ({
|
|
@@ -10,10 +10,12 @@ export function fromInput(blocks) {
|
|
|
10
10
|
}
|
|
11
11
|
export function flatten(blocks) {
|
|
12
12
|
const out = [];
|
|
13
|
-
|
|
13
|
+
// quoteDepth = blockquote ancestors; text follows spec/format §4.1 (containers
|
|
14
|
+
// compose from children, nested raws lose their `> ` prefixes).
|
|
15
|
+
const walk = (list, parentKey, quoteDepth) => {
|
|
14
16
|
list.forEach((b, index) => {
|
|
15
17
|
const key = `${parentKey ?? ""}/${index}`;
|
|
16
|
-
const text =
|
|
18
|
+
const text = visibleText(b, quoteDepth);
|
|
17
19
|
const mb = {
|
|
18
20
|
type: b.type,
|
|
19
21
|
rawHashHex: sha256(b.raw).toString("hex"),
|
|
@@ -28,10 +30,10 @@ export function flatten(blocks) {
|
|
|
28
30
|
mb.blockId = b.blockId;
|
|
29
31
|
out.push(mb);
|
|
30
32
|
if (b.children.length > 0)
|
|
31
|
-
walk(b.children, key);
|
|
33
|
+
walk(b.children, key, childQuoteDepth(b.type, quoteDepth));
|
|
32
34
|
});
|
|
33
35
|
};
|
|
34
|
-
walk(blocks, null);
|
|
36
|
+
walk(blocks, null, 0);
|
|
35
37
|
return out;
|
|
36
38
|
}
|
|
37
39
|
//# sourceMappingURL=flatten.js.map
|
|
@@ -3,6 +3,7 @@ import type { PhaseState } from "./phases.js";
|
|
|
3
3
|
* Phase 5 — scored assignment. Greedy-by-score with R3 order constraints:
|
|
4
4
|
* accept the highest-scoring candidate, discard conflicting candidates
|
|
5
5
|
* (same old or new, or order-crossing with an already-accepted same-parent
|
|
6
|
-
* pair), repeat. Accept
|
|
6
|
+
* pair), repeat. Accept a candidate when score ≥ θ (θ_small for tiny blocks);
|
|
7
|
+
* a candidate below its θ is skipped, not a stopping point.
|
|
7
8
|
*/
|
|
8
9
|
export declare function phase5Scored(state: PhaseState): void;
|