arborkit 1.0.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 (94) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +134 -0
  4. package/dist/addressing.d.ts +22 -0
  5. package/dist/addressing.js +56 -0
  6. package/dist/addressing.js.map +1 -0
  7. package/dist/ag-ui.d.ts +45 -0
  8. package/dist/ag-ui.js +50 -0
  9. package/dist/ag-ui.js.map +1 -0
  10. package/dist/arbor.d.ts +73 -0
  11. package/dist/arbor.js +1542 -0
  12. package/dist/arbor.js.map +1 -0
  13. package/dist/artifact-tree.d.ts +66 -0
  14. package/dist/artifact-tree.js +285 -0
  15. package/dist/artifact-tree.js.map +1 -0
  16. package/dist/clock.d.ts +15 -0
  17. package/dist/clock.js +23 -0
  18. package/dist/clock.js.map +1 -0
  19. package/dist/decompose.d.ts +18 -0
  20. package/dist/decompose.js +22 -0
  21. package/dist/decompose.js.map +1 -0
  22. package/dist/delta-storage.d.ts +55 -0
  23. package/dist/delta-storage.js +106 -0
  24. package/dist/delta-storage.js.map +1 -0
  25. package/dist/delta.d.ts +55 -0
  26. package/dist/delta.js +740 -0
  27. package/dist/delta.js.map +1 -0
  28. package/dist/embedding-port.d.ts +17 -0
  29. package/dist/embedding-port.js +21 -0
  30. package/dist/embedding-port.js.map +1 -0
  31. package/dist/embedding-text.d.ts +14 -0
  32. package/dist/embedding-text.js +21 -0
  33. package/dist/embedding-text.js.map +1 -0
  34. package/dist/errors.d.ts +37 -0
  35. package/dist/errors.js +59 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/event-log.d.ts +75 -0
  38. package/dist/event-log.js +82 -0
  39. package/dist/event-log.js.map +1 -0
  40. package/dist/file-storage.d.ts +22 -0
  41. package/dist/file-storage.js +42 -0
  42. package/dist/file-storage.js.map +1 -0
  43. package/dist/ids.d.ts +17 -0
  44. package/dist/ids.js +22 -0
  45. package/dist/ids.js.map +1 -0
  46. package/dist/index.d.ts +29 -0
  47. package/dist/index.js +1826 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/json-edit.d.ts +18 -0
  50. package/dist/json-edit.js +85 -0
  51. package/dist/json-edit.js.map +1 -0
  52. package/dist/jsonpointer.d.ts +14 -0
  53. package/dist/jsonpointer.js +33 -0
  54. package/dist/jsonpointer.js.map +1 -0
  55. package/dist/mutator.d.ts +59 -0
  56. package/dist/mutator.js +244 -0
  57. package/dist/mutator.js.map +1 -0
  58. package/dist/navigator.d.ts +85 -0
  59. package/dist/navigator.js +192 -0
  60. package/dist/navigator.js.map +1 -0
  61. package/dist/path-glob.d.ts +13 -0
  62. package/dist/path-glob.js +40 -0
  63. package/dist/path-glob.js.map +1 -0
  64. package/dist/registry-validator.d.ts +15 -0
  65. package/dist/registry-validator.js +11 -0
  66. package/dist/registry-validator.js.map +1 -0
  67. package/dist/replay.d.ts +38 -0
  68. package/dist/replay.js +183 -0
  69. package/dist/replay.js.map +1 -0
  70. package/dist/semantic-index.d.ts +88 -0
  71. package/dist/semantic-index.js +226 -0
  72. package/dist/semantic-index.js.map +1 -0
  73. package/dist/storage.d.ts +39 -0
  74. package/dist/storage.js +378 -0
  75. package/dist/storage.js.map +1 -0
  76. package/dist/toolset.d.ts +78 -0
  77. package/dist/toolset.js +306 -0
  78. package/dist/toolset.js.map +1 -0
  79. package/dist/type-aware-decision.d.ts +8 -0
  80. package/dist/type-aware-decision.js +17 -0
  81. package/dist/type-aware-decision.js.map +1 -0
  82. package/dist/type-registry.d.ts +20 -0
  83. package/dist/type-registry.js +17 -0
  84. package/dist/type-registry.js.map +1 -0
  85. package/dist/types.d.ts +28 -0
  86. package/dist/types.js +1 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/vector-index-port.d.ts +34 -0
  89. package/dist/vector-index-port.js +49 -0
  90. package/dist/vector-index-port.js.map +1 -0
  91. package/dist/zod-adapter.d.ts +13 -0
  92. package/dist/zod-adapter.js +34 -0
  93. package/dist/zod-adapter.js.map +1 -0
  94. package/package.json +47 -0
@@ -0,0 +1,244 @@
1
+ // src/errors.ts
2
+ var ArborError = class extends Error {
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = new.target.name;
7
+ }
8
+ code;
9
+ };
10
+ var NodeNotFoundError = class extends ArborError {
11
+ constructor(ref) {
12
+ super("NODE_NOT_FOUND", `Node not found: ${JSON.stringify(ref)}`);
13
+ this.ref = ref;
14
+ }
15
+ ref;
16
+ };
17
+ var ScopeViolationError = class extends ArborError {
18
+ constructor(targetPath, scope) {
19
+ super("SCOPE_VIOLATION", `Access outside scope: ${targetPath} (scope: ${scope})`);
20
+ this.targetPath = targetPath;
21
+ this.scope = scope;
22
+ }
23
+ targetPath;
24
+ scope;
25
+ };
26
+ var StaleVersionError = class extends ArborError {
27
+ constructor(id, expected, actual) {
28
+ super("STALE_VERSION", `Stale version for ${id}: expected ${expected}, actual ${actual}`);
29
+ this.id = id;
30
+ this.expected = expected;
31
+ this.actual = actual;
32
+ }
33
+ id;
34
+ expected;
35
+ actual;
36
+ };
37
+ var InvalidOpError = class extends ArborError {
38
+ constructor(message) {
39
+ super("INVALID_OP", message);
40
+ }
41
+ };
42
+
43
+ // src/jsonpointer.ts
44
+ function isWithin(path, scope) {
45
+ return scope === void 0 || path === scope || path.startsWith(scope + "/");
46
+ }
47
+
48
+ // src/mutator.ts
49
+ var Mutator = class {
50
+ constructor(tree, addressing, log, deps) {
51
+ this.tree = tree;
52
+ this.addressing = addressing;
53
+ this.log = log;
54
+ this.deps = deps;
55
+ }
56
+ tree;
57
+ addressing;
58
+ log;
59
+ deps;
60
+ resolve(ref) {
61
+ const node = "id" in ref ? this.addressing.byId(ref.id) : this.addressing.byPath(ref.path);
62
+ if (!node) throw new NodeNotFoundError(ref);
63
+ return node;
64
+ }
65
+ checkScope(node, writeScope) {
66
+ if (writeScope === void 0) return;
67
+ const path = this.addressing.pathOf(node.id);
68
+ if (!isWithin(path, writeScope)) throw new ScopeViolationError(path, writeScope);
69
+ }
70
+ checkVersion(node, ifVersion) {
71
+ if (ifVersion !== void 0 && node.meta.version !== ifVersion) {
72
+ throw new StaleVersionError(node.id, ifVersion, node.meta.version);
73
+ }
74
+ }
75
+ bump(node, owner) {
76
+ node.meta.version += 1;
77
+ node.meta.updatedAt = this.deps.clock.now();
78
+ if (owner !== void 0) node.meta.owner = owner;
79
+ }
80
+ set(ref, value, opts = {}) {
81
+ const node = this.resolve(ref);
82
+ this.checkScope(node, opts.writeScope);
83
+ this.checkVersion(node, opts.ifVersion);
84
+ const cloned = structuredClone(value);
85
+ const clearType = opts.type === null;
86
+ const type = clearType ? void 0 : opts.type ?? node.type;
87
+ this.deps.validate?.({ node, proposed: cloned, type, op: "set" });
88
+ const before = this.tree.toJson(node.id);
89
+ const typeBefore = node.type;
90
+ const tagsBefore = [...node.tags ?? []];
91
+ const orphaned = this.tree.descendantIds(node.id);
92
+ this.tree.replaceValue(node.id, cloned, type, clearType);
93
+ if (opts.tags !== void 0) node.tags = [...opts.tags];
94
+ this.bump(node, opts.owner);
95
+ this.deps.onChange?.(node);
96
+ if (this.deps.onChange) {
97
+ for (const id of this.tree.descendantIds(node.id)) {
98
+ const child = this.tree.get(id);
99
+ if (child) this.deps.onChange(child);
100
+ }
101
+ }
102
+ if (this.deps.onRemove) {
103
+ for (const id of orphaned) this.deps.onRemove(id);
104
+ }
105
+ this.log.append({
106
+ kind: "set",
107
+ targetId: node.id,
108
+ parentId: node.parentId,
109
+ key: node.key,
110
+ path: this.addressing.pathOf(node.id),
111
+ before,
112
+ after: cloned,
113
+ nodeTypeBefore: typeBefore ?? null,
114
+ nodeType: type ?? null,
115
+ tagsBefore,
116
+ tags: [...node.tags ?? []],
117
+ actor: opts.owner,
118
+ ts: this.deps.clock.now()
119
+ });
120
+ }
121
+ insert(parentRef, keyOrIndex, value, opts = {}) {
122
+ const parent = this.resolve(parentRef);
123
+ this.checkScope(parent, opts.writeScope);
124
+ this.checkVersion(parent, opts.ifVersion);
125
+ const cloned = structuredClone(value);
126
+ const type = opts.type === null ? void 0 : opts.type;
127
+ this.deps.validate?.({ node: null, proposed: cloned, type, op: "insert" });
128
+ const newId = this.tree.insertChild(parent.id, keyOrIndex, cloned, type);
129
+ const child = this.tree.get(newId);
130
+ if (opts.tags !== void 0) child.tags = [...opts.tags];
131
+ this.bump(parent, opts.owner);
132
+ this.deps.onChange?.(child);
133
+ if (this.deps.onChange) {
134
+ for (const id of this.tree.descendantIds(newId)) {
135
+ const desc = this.tree.get(id);
136
+ if (desc) this.deps.onChange(desc);
137
+ }
138
+ }
139
+ this.log.append({
140
+ kind: "insert",
141
+ targetId: newId,
142
+ parentId: parent.id,
143
+ key: child.key,
144
+ path: this.addressing.pathOf(newId),
145
+ after: cloned,
146
+ nodeType: child.type ?? null,
147
+ tags: [...child.tags ?? []],
148
+ actor: opts.owner,
149
+ ts: this.deps.clock.now()
150
+ });
151
+ return newId;
152
+ }
153
+ remove(ref, opts = {}) {
154
+ const node = this.resolve(ref);
155
+ if (node.parentId === null) throw new InvalidOpError("cannot remove the root");
156
+ this.checkScope(node, opts.writeScope);
157
+ this.checkVersion(node, opts.ifVersion);
158
+ const before = this.tree.toJson(node.id);
159
+ const path = this.addressing.pathOf(node.id);
160
+ const removedIds = [node.id, ...this.tree.descendantIds(node.id)];
161
+ const parent = this.tree.get(node.parentId);
162
+ const removedKey = node.key;
163
+ this.tree.removeChild(node.parentId, node.id);
164
+ this.bump(parent, opts.owner);
165
+ if (this.deps.onRemove) {
166
+ for (const id of removedIds) this.deps.onRemove(id);
167
+ }
168
+ this.deps.onChange?.(parent);
169
+ this.log.append({
170
+ kind: "remove",
171
+ targetId: node.id,
172
+ parentId: parent.id,
173
+ key: removedKey,
174
+ path,
175
+ before,
176
+ nodeTypeBefore: node.type ?? null,
177
+ tagsBefore: [...node.tags ?? []],
178
+ actor: opts.owner,
179
+ ts: this.deps.clock.now()
180
+ });
181
+ }
182
+ move(ref, toParentRef, keyOrIndex, opts = {}) {
183
+ const node = this.resolve(ref);
184
+ if (node.parentId === null) throw new InvalidOpError("cannot move the root");
185
+ const toParent = this.resolve(toParentRef);
186
+ this.checkScope(node, opts.writeScope);
187
+ this.checkScope(toParent, opts.writeScope);
188
+ this.checkVersion(node, opts.ifVersion);
189
+ const oldParentId = node.parentId;
190
+ const from = { parentId: node.parentId, key: node.key };
191
+ const fromPath = this.addressing.pathOf(node.id);
192
+ this.tree.moveNode(node.id, toParent.id, keyOrIndex);
193
+ const toPath = this.addressing.pathOf(node.id);
194
+ const bumped = /* @__PURE__ */ new Set();
195
+ for (const id of [node.id, oldParentId, toParent.id]) {
196
+ if (id !== null && !bumped.has(id)) {
197
+ const n = this.tree.get(id);
198
+ if (n) this.bump(n, opts.owner);
199
+ bumped.add(id);
200
+ }
201
+ }
202
+ if (this.deps.onChange) {
203
+ this.deps.onChange(node);
204
+ for (const id of this.tree.descendantIds(node.id)) {
205
+ const d = this.tree.get(id);
206
+ if (d) this.deps.onChange(d);
207
+ }
208
+ const oldP = oldParentId !== null ? this.tree.get(oldParentId) : void 0;
209
+ if (oldP) this.deps.onChange(oldP);
210
+ const newP = this.tree.get(toParent.id);
211
+ if (newP) this.deps.onChange(newP);
212
+ }
213
+ this.log.append({
214
+ kind: "move",
215
+ targetId: node.id,
216
+ parentId: toParent.id,
217
+ key: node.key,
218
+ from,
219
+ to: { parentId: toParent.id, key: node.key },
220
+ fromPath,
221
+ toPath,
222
+ actor: opts.owner,
223
+ ts: this.deps.clock.now()
224
+ });
225
+ }
226
+ /** Run `fn` atomically: if it throws, the tree, log, and any hooked index state are restored. */
227
+ transaction(fn) {
228
+ const snap = this.tree.snapshot();
229
+ const logLen = this.log.length();
230
+ const hookSnap = this.deps.onTxSnapshot?.();
231
+ try {
232
+ fn();
233
+ } catch (err) {
234
+ this.tree.restore(snap);
235
+ this.log.truncateTo(logLen);
236
+ if (this.deps.onTxRestore) this.deps.onTxRestore(hookSnap);
237
+ throw err;
238
+ }
239
+ }
240
+ };
241
+ export {
242
+ Mutator
243
+ };
244
+ //# sourceMappingURL=mutator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/jsonpointer.ts","../src/mutator.ts"],"sourcesContent":["import type { NodeId } from \"./types\";\r\n\r\n/** A reference to a node: by stable id or by JSON Pointer path. */\r\nexport type Ref = { id: NodeId } | { path: string };\r\n\r\nexport class ArborError extends Error {\r\n constructor(\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = new.target.name;\r\n }\r\n}\r\n\r\nexport class NodeNotFoundError extends ArborError {\r\n constructor(public readonly ref: Ref) {\r\n super(\"NODE_NOT_FOUND\", `Node not found: ${JSON.stringify(ref)}`);\r\n }\r\n}\r\n\r\nexport class ScopeViolationError extends ArborError {\r\n constructor(\r\n public readonly targetPath: string,\r\n public readonly scope: string,\r\n ) {\r\n super(\"SCOPE_VIOLATION\", `Access outside scope: ${targetPath} (scope: ${scope})`);\r\n }\r\n}\r\n\r\nexport class StaleVersionError extends ArborError {\r\n constructor(\r\n public readonly id: NodeId,\r\n public readonly expected: number,\r\n public readonly actual: number,\r\n ) {\r\n super(\"STALE_VERSION\", `Stale version for ${id}: expected ${expected}, actual ${actual}`);\r\n }\r\n}\r\n\r\nexport class InvalidOpError extends ArborError {\r\n constructor(message: string) {\r\n super(\"INVALID_OP\", message);\r\n }\r\n}\r\n\r\nexport class ValidationError extends ArborError {\r\n constructor(\r\n public readonly type: string | undefined,\r\n public readonly details: string,\r\n ) {\r\n super(\"VALIDATION_ERROR\", `Validation failed${type ? ` for type ${type}` : \"\"}: ${details}`);\r\n }\r\n}\r\n","/** Escape a single reference token per RFC 6901: ~ -> ~0, / -> ~1. */\r\nexport function encodeSegment(s: string): string {\r\n return s.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\r\n}\r\n\r\n/** Unescape a single reference token per RFC 6901: ~1 -> /, then ~0 -> ~. */\r\nexport function decodeSegment(s: string): string {\r\n return s.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\r\n}\r\n\r\n/** Build a JSON Pointer string. Empty segment list => \"\" (root). */\r\nexport function buildPointer(segments: ReadonlyArray<string | number>): string {\r\n if (segments.length === 0) return \"\";\r\n return \"/\" + segments.map((s) => encodeSegment(String(s))).join(\"/\");\r\n}\r\n\r\n/** Append one child key to a parent pointer, escaping the key exactly as `buildPointer` does. */\r\nexport function appendPointer(parent: string, key: string | number): string {\r\n return parent + \"/\" + encodeSegment(String(key));\r\n}\r\n\r\n/** True when `path` is at or under `scope` (JSON Pointer prefix). Undefined scope = everywhere. */\r\nexport function isWithin(path: string, scope: string | undefined): boolean {\r\n return scope === undefined || path === scope || path.startsWith(scope + \"/\");\r\n}\r\n\r\n/** Parse a JSON Pointer into decoded segments. \"\" => [] (root). */\r\nexport function parsePointer(pointer: string): string[] {\r\n if (pointer === \"\") return [];\r\n if (!pointer.startsWith(\"/\")) {\r\n throw new Error(`Invalid JSON Pointer (must be \"\" or start with \"/\"): ${pointer}`);\r\n }\r\n return pointer.slice(1).split(\"/\").map(decodeSegment);\r\n}\r\n","import type { ArbNode, Json, NodeId } from \"./types\";\r\nimport type { Clock } from \"./clock\";\r\nimport type { ArtifactTree } from \"./artifact-tree\";\r\nimport type { Addressing } from \"./addressing\";\r\nimport type { EventLog, OpKind } from \"./event-log\";\r\nimport { type Ref, NodeNotFoundError, ScopeViolationError, StaleVersionError, InvalidOpError } from \"./errors\";\r\nimport { isWithin } from \"./jsonpointer\";\r\n\r\n/** Optional validation hook. Throws to reject a mutation. M3 plugs Zod in here. */\r\nexport type Validator = (input: { node: ArbNode | null; proposed: Json; type?: string; op: OpKind }) => void;\r\n\r\nexport interface MutatorDeps {\r\n clock: Clock;\r\n validate?: Validator;\r\n /** Called after a node's content changes (set/insert) — e.g. to mark a semantic index stale. */\r\n onChange?: (node: ArbNode) => void;\r\n /** Called after a node is removed — e.g. to drop it from a semantic index. */\r\n onRemove?: (nodeId: NodeId) => void;\r\n /** Called at transaction start; the returned snapshot is passed to `onTxRestore` on rollback. */\r\n onTxSnapshot?: () => unknown;\r\n /** Called on transaction rollback with the snapshot from `onTxSnapshot`. */\r\n onTxRestore?: (snapshot: unknown) => void;\r\n}\r\n\r\nexport interface MutateOpts {\r\n owner?: string;\r\n /** JSON Pointer prefix; the target must be at or under it. */\r\n writeScope?: string;\r\n /** Optimistic concurrency: reject unless the target's current version equals this. */\r\n ifVersion?: number;\r\n /** Register/override the node's type (drives validation and the decompose override).\r\n * `null` explicitly CLEARS the type (validation is skipped) — used by type-aware revert. */\r\n type?: string | null;\r\n /** Replace the node's tags (identity labels for exact `find` by tag). */\r\n tags?: string[];\r\n}\r\n\r\nexport class Mutator {\r\n constructor(\r\n private readonly tree: ArtifactTree,\r\n private readonly addressing: Addressing,\r\n private readonly log: EventLog,\r\n private readonly deps: MutatorDeps,\r\n ) {}\r\n\r\n private resolve(ref: Ref): ArbNode {\r\n const node = \"id\" in ref ? this.addressing.byId(ref.id) : this.addressing.byPath(ref.path);\r\n if (!node) throw new NodeNotFoundError(ref);\r\n return node;\r\n }\r\n\r\n private checkScope(node: ArbNode, writeScope?: string): void {\r\n if (writeScope === undefined) return;\r\n const path = this.addressing.pathOf(node.id);\r\n if (!isWithin(path, writeScope)) throw new ScopeViolationError(path, writeScope);\r\n }\r\n\r\n private checkVersion(node: ArbNode, ifVersion?: number): void {\r\n if (ifVersion !== undefined && node.meta.version !== ifVersion) {\r\n throw new StaleVersionError(node.id, ifVersion, node.meta.version);\r\n }\r\n }\r\n\r\n private bump(node: ArbNode, owner?: string): void {\r\n node.meta.version += 1;\r\n node.meta.updatedAt = this.deps.clock.now();\r\n if (owner !== undefined) node.meta.owner = owner;\r\n }\r\n\r\n set(ref: Ref, value: Json, opts: MutateOpts = {}): void {\r\n const node = this.resolve(ref);\r\n this.checkScope(node, opts.writeScope);\r\n this.checkVersion(node, opts.ifVersion);\r\n // Defensive copy: neither the tree nor the event log may alias the caller's\r\n // live object — post-call mutation would silently rewrite state AND history.\r\n const cloned = structuredClone(value);\r\n const clearType = opts.type === null;\r\n const type = clearType ? undefined : (opts.type ?? node.type);\r\n this.deps.validate?.({ node, proposed: cloned, type, op: \"set\" });\r\n const before = this.tree.toJson(node.id);\r\n const typeBefore = node.type;\r\n const tagsBefore = [...(node.tags ?? [])];\r\n const orphaned = this.tree.descendantIds(node.id);\r\n this.tree.replaceValue(node.id, cloned, type, clearType);\r\n if (opts.tags !== undefined) node.tags = [...opts.tags];\r\n this.bump(node, opts.owner);\r\n this.deps.onChange?.(node);\r\n if (this.deps.onChange) {\r\n // replaceValue rebuilt the subtree: every descendant is a NEW node and\r\n // must be announced too (text leaves are what the semantic index embeds).\r\n for (const id of this.tree.descendantIds(node.id)) {\r\n const child = this.tree.get(id);\r\n if (child) this.deps.onChange(child);\r\n }\r\n }\r\n if (this.deps.onRemove) {\r\n for (const id of orphaned) this.deps.onRemove(id);\r\n }\r\n this.log.append({\r\n kind: \"set\",\r\n targetId: node.id,\r\n parentId: node.parentId,\r\n key: node.key,\r\n path: this.addressing.pathOf(node.id),\r\n before,\r\n after: cloned,\r\n nodeTypeBefore: typeBefore ?? null,\r\n nodeType: type ?? null,\r\n tagsBefore,\r\n tags: [...(node.tags ?? [])],\r\n actor: opts.owner,\r\n ts: this.deps.clock.now(),\r\n });\r\n }\r\n\r\n insert(parentRef: Ref, keyOrIndex: string | number, value: Json, opts: MutateOpts = {}): NodeId {\r\n const parent = this.resolve(parentRef);\r\n this.checkScope(parent, opts.writeScope);\r\n this.checkVersion(parent, opts.ifVersion);\r\n // Defensive copy: neither the tree nor the event log may alias the caller's\r\n // live object — post-call mutation would silently rewrite state AND history.\r\n const cloned = structuredClone(value);\r\n const type = opts.type === null ? undefined : opts.type;\r\n this.deps.validate?.({ node: null, proposed: cloned, type, op: \"insert\" });\r\n const newId = this.tree.insertChild(parent.id, keyOrIndex, cloned, type);\r\n const child = this.tree.get(newId)!;\r\n if (opts.tags !== undefined) child.tags = [...opts.tags];\r\n this.bump(parent, opts.owner);\r\n this.deps.onChange?.(child);\r\n if (this.deps.onChange) {\r\n for (const id of this.tree.descendantIds(newId)) {\r\n const desc = this.tree.get(id);\r\n if (desc) this.deps.onChange(desc);\r\n }\r\n }\r\n this.log.append({\r\n kind: \"insert\",\r\n targetId: newId,\r\n parentId: parent.id,\r\n key: child.key,\r\n path: this.addressing.pathOf(newId),\r\n after: cloned,\r\n nodeType: child.type ?? null,\r\n tags: [...(child.tags ?? [])],\r\n actor: opts.owner,\r\n ts: this.deps.clock.now(),\r\n });\r\n return newId;\r\n }\r\n\r\n remove(ref: Ref, opts: MutateOpts = {}): void {\r\n const node = this.resolve(ref);\r\n if (node.parentId === null) throw new InvalidOpError(\"cannot remove the root\");\r\n this.checkScope(node, opts.writeScope);\r\n this.checkVersion(node, opts.ifVersion);\r\n const before = this.tree.toJson(node.id);\r\n const path = this.addressing.pathOf(node.id);\r\n const removedIds = [node.id, ...this.tree.descendantIds(node.id)];\r\n const parent = this.tree.get(node.parentId)!;\r\n const removedKey = node.key;\r\n this.tree.removeChild(node.parentId, node.id);\r\n this.bump(parent, opts.owner);\r\n if (this.deps.onRemove) {\r\n for (const id of removedIds) this.deps.onRemove(id);\r\n }\r\n // The parent's semantic unit (if any) lost content — re-hash it.\r\n this.deps.onChange?.(parent);\r\n this.log.append({\r\n kind: \"remove\",\r\n targetId: node.id,\r\n parentId: parent.id,\r\n key: removedKey,\r\n path,\r\n before,\r\n nodeTypeBefore: node.type ?? null,\r\n tagsBefore: [...(node.tags ?? [])],\r\n actor: opts.owner,\r\n ts: this.deps.clock.now(),\r\n });\r\n }\r\n\r\n move(ref: Ref, toParentRef: Ref, keyOrIndex: string | number, opts: MutateOpts = {}): void {\r\n const node = this.resolve(ref);\r\n if (node.parentId === null) throw new InvalidOpError(\"cannot move the root\");\r\n const toParent = this.resolve(toParentRef);\r\n this.checkScope(node, opts.writeScope);\r\n this.checkScope(toParent, opts.writeScope);\r\n this.checkVersion(node, opts.ifVersion);\r\n const oldParentId = node.parentId;\r\n const from = { parentId: node.parentId, key: node.key };\r\n const fromPath = this.addressing.pathOf(node.id);\r\n this.tree.moveNode(node.id, toParent.id, keyOrIndex);\r\n const toPath = this.addressing.pathOf(node.id);\r\n // bump moved node + both parents (dedupe if old === new parent)\r\n const bumped = new Set<NodeId>();\r\n for (const id of [node.id, oldParentId, toParent.id]) {\r\n if (id !== null && !bumped.has(id)) {\r\n const n = this.tree.get(id);\r\n if (n) this.bump(n, opts.owner);\r\n bumped.add(id);\r\n }\r\n }\r\n if (this.deps.onChange) {\r\n // The moved subtree's ancestry changed (suppression status may flip), and\r\n // both old and new locations' semantic units changed content.\r\n this.deps.onChange(node);\r\n for (const id of this.tree.descendantIds(node.id)) {\r\n const d = this.tree.get(id);\r\n if (d) this.deps.onChange(d);\r\n }\r\n const oldP = oldParentId !== null ? this.tree.get(oldParentId) : undefined;\r\n if (oldP) this.deps.onChange(oldP);\r\n const newP = this.tree.get(toParent.id);\r\n if (newP) this.deps.onChange(newP);\r\n }\r\n this.log.append({\r\n kind: \"move\",\r\n targetId: node.id,\r\n parentId: toParent.id,\r\n key: node.key,\r\n from,\r\n to: { parentId: toParent.id, key: node.key },\r\n fromPath,\r\n toPath,\r\n actor: opts.owner,\r\n ts: this.deps.clock.now(),\r\n });\r\n }\r\n\r\n /** Run `fn` atomically: if it throws, the tree, log, and any hooked index state are restored. */\r\n transaction(fn: () => void): void {\r\n const snap = this.tree.snapshot();\r\n const logLen = this.log.length();\r\n const hookSnap = this.deps.onTxSnapshot?.();\r\n try {\r\n fn();\r\n } catch (err) {\r\n this.tree.restore(snap);\r\n this.log.truncateTo(logLen);\r\n if (this.deps.onTxRestore) this.deps.onTxRestore(hookSnap);\r\n throw err;\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,85 @@
1
+ import { NodeId, NodeKind, Json, NodeMeta, ArbNode } from './types.js';
2
+ import { ArtifactTree } from './artifact-tree.js';
3
+ import { Addressing } from './addressing.js';
4
+ import { Ref } from './errors.js';
5
+ import './ids.js';
6
+ import './clock.js';
7
+ import './decompose.js';
8
+
9
+ interface NodeSummary {
10
+ id: NodeId;
11
+ path: string;
12
+ key: string | number | null;
13
+ kind: NodeKind;
14
+ type?: string;
15
+ }
16
+ interface ChildSummary {
17
+ id: NodeId;
18
+ key: string | number | null;
19
+ kind: NodeKind;
20
+ type?: string;
21
+ hasChildren: boolean;
22
+ size: number;
23
+ preview: string;
24
+ }
25
+ interface DescribeOpts {
26
+ offset?: number;
27
+ limit?: number;
28
+ }
29
+ interface DescribeResult {
30
+ node: NodeSummary;
31
+ children: ChildSummary[];
32
+ truncated?: {
33
+ shown: number;
34
+ total: number;
35
+ nextOffset: number;
36
+ };
37
+ }
38
+ interface GetOpts {
39
+ maxDepth?: number;
40
+ }
41
+ interface GetResult {
42
+ id: NodeId;
43
+ path: string;
44
+ type?: string;
45
+ content: Json;
46
+ meta: NodeMeta;
47
+ truncated?: boolean;
48
+ }
49
+ interface FindSelector {
50
+ type?: string;
51
+ tag?: string;
52
+ pathPattern?: string;
53
+ }
54
+ interface FindOpts {
55
+ limit?: number;
56
+ /** JSON Pointer prefix: only nodes at/under it are hits — checked BEFORE the
57
+ * limit is consumed, so out-of-scope matches never eat the budget. */
58
+ within?: string;
59
+ }
60
+ interface FindHit {
61
+ id: NodeId;
62
+ path: string;
63
+ type?: string;
64
+ }
65
+ interface FindResult {
66
+ hits: FindHit[];
67
+ /** True when the walk stopped at `limit` — there MAY be more matches (an exact-fit
68
+ * final hit also reports true). */
69
+ truncated: boolean;
70
+ }
71
+ /** Read-only navigation over the artifact tree: describe (cheap listing) and get (bounded content). */
72
+ declare class Navigator {
73
+ private readonly tree;
74
+ private readonly addressing;
75
+ constructor(tree: ArtifactTree, addressing: Addressing);
76
+ protected resolve(ref: Ref): ArbNode;
77
+ private summarize;
78
+ describe(ref?: Ref, opts?: DescribeOpts): DescribeResult;
79
+ get(ref: Ref, opts?: GetOpts): GetResult;
80
+ private matches;
81
+ /** Find nodes matching ALL provided selector fields (exact `type`, `tag` membership, glob `pathPattern`). */
82
+ find(selector: FindSelector, opts?: FindOpts): FindResult;
83
+ }
84
+
85
+ export { type ChildSummary, type DescribeOpts, type DescribeResult, type FindHit, type FindOpts, type FindResult, type FindSelector, type GetOpts, type GetResult, Navigator, type NodeSummary };
@@ -0,0 +1,192 @@
1
+ // src/errors.ts
2
+ var ArborError = class extends Error {
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = new.target.name;
7
+ }
8
+ code;
9
+ };
10
+ var NodeNotFoundError = class extends ArborError {
11
+ constructor(ref) {
12
+ super("NODE_NOT_FOUND", `Node not found: ${JSON.stringify(ref)}`);
13
+ this.ref = ref;
14
+ }
15
+ ref;
16
+ };
17
+
18
+ // src/decompose.ts
19
+ function byteSize(value) {
20
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
21
+ }
22
+
23
+ // src/jsonpointer.ts
24
+ function encodeSegment(s) {
25
+ return s.replace(/~/g, "~0").replace(/\//g, "~1");
26
+ }
27
+ function decodeSegment(s) {
28
+ return s.replace(/~1/g, "/").replace(/~0/g, "~");
29
+ }
30
+ function appendPointer(parent, key) {
31
+ return parent + "/" + encodeSegment(String(key));
32
+ }
33
+ function isWithin(path, scope) {
34
+ return scope === void 0 || path === scope || path.startsWith(scope + "/");
35
+ }
36
+ function parsePointer(pointer) {
37
+ if (pointer === "") return [];
38
+ if (!pointer.startsWith("/")) {
39
+ throw new Error(`Invalid JSON Pointer (must be "" or start with "/"): ${pointer}`);
40
+ }
41
+ return pointer.slice(1).split("/").map(decodeSegment);
42
+ }
43
+
44
+ // src/path-glob.ts
45
+ function matchSegments(pattern, path) {
46
+ if (pattern.length === 0) return path.length === 0;
47
+ const [head, ...rest] = pattern;
48
+ if (head === "**") {
49
+ for (let i = 0; i <= path.length; i++) {
50
+ if (matchSegments(rest, path.slice(i))) return true;
51
+ }
52
+ return false;
53
+ }
54
+ if (path.length === 0) return false;
55
+ if (head === "*" || head === path[0]) {
56
+ return matchSegments(rest, path.slice(1));
57
+ }
58
+ return false;
59
+ }
60
+ function compileGlob(pattern) {
61
+ const patternSegs = parsePointer(pattern);
62
+ return (path) => matchSegments(patternSegs, parsePointer(path));
63
+ }
64
+
65
+ // src/navigator.ts
66
+ var DEFAULT_LIMIT = 100;
67
+ var PREVIEW_MAX = 50;
68
+ function previewOf(node) {
69
+ if (node.kind === "leaf") {
70
+ const s = JSON.stringify(node.content);
71
+ return s.length <= PREVIEW_MAX ? s : s.slice(0, PREVIEW_MAX) + "\u2026";
72
+ }
73
+ return node.kind === "array" ? `[${node.childIds.length} items]` : `{${node.childIds.length} keys}`;
74
+ }
75
+ var Navigator = class {
76
+ constructor(tree, addressing) {
77
+ this.tree = tree;
78
+ this.addressing = addressing;
79
+ }
80
+ tree;
81
+ addressing;
82
+ resolve(ref) {
83
+ const node = "id" in ref ? this.addressing.byId(ref.id) : this.addressing.byPath(ref.path);
84
+ if (!node) throw new NodeNotFoundError(ref);
85
+ return node;
86
+ }
87
+ summarize(node) {
88
+ return {
89
+ id: node.id,
90
+ key: node.key,
91
+ kind: node.kind,
92
+ type: node.type,
93
+ hasChildren: node.childIds.length > 0,
94
+ size: node.kind === "leaf" ? byteSize(node.content) : node.childIds.length,
95
+ preview: previewOf(node)
96
+ };
97
+ }
98
+ describe(ref = { path: "" }, opts = {}) {
99
+ const node = this.resolve(ref);
100
+ const all = this.tree.children(node.id);
101
+ const total = all.length;
102
+ const offset = opts.offset ?? 0;
103
+ const limit = opts.limit ?? DEFAULT_LIMIT;
104
+ const page = all.slice(offset, offset + limit);
105
+ const result = {
106
+ node: {
107
+ id: node.id,
108
+ path: this.addressing.pathOf(node.id),
109
+ key: node.key,
110
+ kind: node.kind,
111
+ type: node.type
112
+ },
113
+ children: page.map((c) => this.summarize(c))
114
+ };
115
+ const nextOffset = offset + page.length;
116
+ if (nextOffset < total) {
117
+ result.truncated = { shown: page.length, total, nextOffset };
118
+ }
119
+ return result;
120
+ }
121
+ get(ref, opts = {}) {
122
+ const node = this.resolve(ref);
123
+ const maxDepth = opts.maxDepth;
124
+ let truncated = false;
125
+ const reconstruct = (id, depth) => {
126
+ const n = this.tree.get(id);
127
+ if (n.kind === "leaf") return n.content;
128
+ if (maxDepth !== void 0 && depth >= maxDepth) {
129
+ truncated = true;
130
+ const label = n.kind === "array" ? `${n.childIds.length} items` : `${n.childIds.length} keys`;
131
+ return `[truncated: ${label}]`;
132
+ }
133
+ if (n.kind === "array") return n.childIds.map((cid) => reconstruct(cid, depth + 1));
134
+ const obj = {};
135
+ for (const cid of n.childIds) {
136
+ const c = this.tree.get(cid);
137
+ obj[String(c.key)] = reconstruct(cid, depth + 1);
138
+ }
139
+ return obj;
140
+ };
141
+ const content = reconstruct(node.id, 0);
142
+ const result = {
143
+ id: node.id,
144
+ path: this.addressing.pathOf(node.id),
145
+ type: node.type,
146
+ content,
147
+ meta: node.meta
148
+ };
149
+ if (truncated) result.truncated = true;
150
+ return result;
151
+ }
152
+ matches(node, sel, path, glob) {
153
+ if (sel.type !== void 0 && node.type !== sel.type) return false;
154
+ if (sel.tag !== void 0 && !(node.tags?.includes(sel.tag) ?? false)) return false;
155
+ if (glob !== void 0 && !glob(path)) return false;
156
+ return true;
157
+ }
158
+ /** Find nodes matching ALL provided selector fields (exact `type`, `tag` membership, glob `pathPattern`). */
159
+ find(selector, opts = {}) {
160
+ const limit = opts.limit ?? DEFAULT_LIMIT;
161
+ const within = opts.within;
162
+ const glob = selector.pathPattern !== void 0 ? compileGlob(selector.pathPattern) : void 0;
163
+ const hits = [];
164
+ let truncated = false;
165
+ const visit = (id, path) => {
166
+ if (hits.length >= limit) {
167
+ truncated = true;
168
+ return;
169
+ }
170
+ const node = this.tree.get(id);
171
+ if (this.matches(node, selector, path, glob)) {
172
+ if (isWithin(path, within)) {
173
+ hits.push({ id: node.id, path, type: node.type });
174
+ }
175
+ }
176
+ for (const cid of node.childIds) {
177
+ if (hits.length >= limit) {
178
+ truncated = true;
179
+ break;
180
+ }
181
+ const child = this.tree.get(cid);
182
+ visit(cid, appendPointer(path, child.key));
183
+ }
184
+ };
185
+ visit(this.tree.rootIdValue(), "");
186
+ return { hits, truncated };
187
+ }
188
+ };
189
+ export {
190
+ Navigator
191
+ };
192
+ //# sourceMappingURL=navigator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/decompose.ts","../src/jsonpointer.ts","../src/path-glob.ts","../src/navigator.ts"],"sourcesContent":["import type { NodeId } from \"./types\";\r\n\r\n/** A reference to a node: by stable id or by JSON Pointer path. */\r\nexport type Ref = { id: NodeId } | { path: string };\r\n\r\nexport class ArborError extends Error {\r\n constructor(\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = new.target.name;\r\n }\r\n}\r\n\r\nexport class NodeNotFoundError extends ArborError {\r\n constructor(public readonly ref: Ref) {\r\n super(\"NODE_NOT_FOUND\", `Node not found: ${JSON.stringify(ref)}`);\r\n }\r\n}\r\n\r\nexport class ScopeViolationError extends ArborError {\r\n constructor(\r\n public readonly targetPath: string,\r\n public readonly scope: string,\r\n ) {\r\n super(\"SCOPE_VIOLATION\", `Access outside scope: ${targetPath} (scope: ${scope})`);\r\n }\r\n}\r\n\r\nexport class StaleVersionError extends ArborError {\r\n constructor(\r\n public readonly id: NodeId,\r\n public readonly expected: number,\r\n public readonly actual: number,\r\n ) {\r\n super(\"STALE_VERSION\", `Stale version for ${id}: expected ${expected}, actual ${actual}`);\r\n }\r\n}\r\n\r\nexport class InvalidOpError extends ArborError {\r\n constructor(message: string) {\r\n super(\"INVALID_OP\", message);\r\n }\r\n}\r\n\r\nexport class ValidationError extends ArborError {\r\n constructor(\r\n public readonly type: string | undefined,\r\n public readonly details: string,\r\n ) {\r\n super(\"VALIDATION_ERROR\", `Validation failed${type ? ` for type ${type}` : \"\"}: ${details}`);\r\n }\r\n}\r\n","import type { Json, NodeKind } from \"./types\";\r\n\r\n/** Policy deciding whether a value is stored whole (opaque leaf) or split into child nodes. */\r\nexport interface DecomposeDecision {\r\n /** `type` is the optional registered node type (used by the by-type override in a later milestone). */\r\n isOpaque(value: Json, type?: string): boolean;\r\n}\r\n\r\n/** UTF-8 byte length of the JSON serialization of a value. */\r\nexport function byteSize(value: Json): number {\r\n return Buffer.byteLength(JSON.stringify(value), \"utf8\");\r\n}\r\n\r\n/** Structural kind of a value given whether it is being stored opaquely. */\r\nexport function kindOf(value: Json, opaque: boolean): NodeKind {\r\n if (opaque) return \"leaf\";\r\n return Array.isArray(value) ? \"array\" : \"object\";\r\n}\r\n\r\n/**\r\n * Default policy: scalars are always opaque leaves; containers stay opaque\r\n * while their serialized size is within `maxOpaqueBytes`, otherwise they split.\r\n */\r\nexport function sizeBasedDecision(maxOpaqueBytes: number): DecomposeDecision {\r\n return {\r\n isOpaque(value: Json): boolean {\r\n if (value === null || typeof value !== \"object\") return true;\r\n return byteSize(value) <= maxOpaqueBytes;\r\n },\r\n };\r\n}\r\n","/** Escape a single reference token per RFC 6901: ~ -> ~0, / -> ~1. */\r\nexport function encodeSegment(s: string): string {\r\n return s.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\r\n}\r\n\r\n/** Unescape a single reference token per RFC 6901: ~1 -> /, then ~0 -> ~. */\r\nexport function decodeSegment(s: string): string {\r\n return s.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\r\n}\r\n\r\n/** Build a JSON Pointer string. Empty segment list => \"\" (root). */\r\nexport function buildPointer(segments: ReadonlyArray<string | number>): string {\r\n if (segments.length === 0) return \"\";\r\n return \"/\" + segments.map((s) => encodeSegment(String(s))).join(\"/\");\r\n}\r\n\r\n/** Append one child key to a parent pointer, escaping the key exactly as `buildPointer` does. */\r\nexport function appendPointer(parent: string, key: string | number): string {\r\n return parent + \"/\" + encodeSegment(String(key));\r\n}\r\n\r\n/** True when `path` is at or under `scope` (JSON Pointer prefix). Undefined scope = everywhere. */\r\nexport function isWithin(path: string, scope: string | undefined): boolean {\r\n return scope === undefined || path === scope || path.startsWith(scope + \"/\");\r\n}\r\n\r\n/** Parse a JSON Pointer into decoded segments. \"\" => [] (root). */\r\nexport function parsePointer(pointer: string): string[] {\r\n if (pointer === \"\") return [];\r\n if (!pointer.startsWith(\"/\")) {\r\n throw new Error(`Invalid JSON Pointer (must be \"\" or start with \"/\"): ${pointer}`);\r\n }\r\n return pointer.slice(1).split(\"/\").map(decodeSegment);\r\n}\r\n","import { parsePointer } from \"./jsonpointer\";\r\n\r\n/** Match parsed segment arrays. `*` = one segment; `**` = zero or more segments. */\r\nfunction matchSegments(pattern: string[], path: string[]): boolean {\r\n if (pattern.length === 0) return path.length === 0;\r\n const [head, ...rest] = pattern;\r\n if (head === \"**\") {\r\n for (let i = 0; i <= path.length; i++) {\r\n if (matchSegments(rest, path.slice(i))) return true;\r\n }\r\n return false;\r\n }\r\n if (path.length === 0) return false;\r\n if (head === \"*\" || head === path[0]) {\r\n return matchSegments(rest, path.slice(1));\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Parse a glob `pattern` ONCE and return a reusable matcher over JSON Pointer paths.\r\n * `*` matches exactly one path segment; `**` matches zero or more segments.\r\n */\r\nexport function compileGlob(pattern: string): (path: string) => boolean {\r\n const patternSegs = parsePointer(pattern);\r\n return (path: string) => matchSegments(patternSegs, parsePointer(path));\r\n}\r\n\r\n/**\r\n * Glob-match a JSON Pointer `path` against a `pattern`.\r\n * `*` matches exactly one path segment; `**` matches zero or more segments.\r\n * One-shot convenience over `compileGlob` — prefer compiling when matching many paths.\r\n */\r\nexport function matchGlob(pattern: string, path: string): boolean {\r\n return compileGlob(pattern)(path);\r\n}\r\n","import type { ArbNode, Json, NodeId, NodeKind, NodeMeta } from \"./types\";\r\nimport type { ArtifactTree } from \"./artifact-tree\";\r\nimport type { Addressing } from \"./addressing\";\r\nimport { type Ref, NodeNotFoundError } from \"./errors\";\r\nimport { byteSize } from \"./decompose\";\r\nimport { compileGlob } from \"./path-glob\";\r\nimport { isWithin, appendPointer } from \"./jsonpointer\";\r\n\r\nconst DEFAULT_LIMIT = 100;\r\nconst PREVIEW_MAX = 50;\r\n\r\nexport interface NodeSummary {\r\n id: NodeId;\r\n path: string;\r\n key: string | number | null;\r\n kind: NodeKind;\r\n type?: string;\r\n}\r\n\r\nexport interface ChildSummary {\r\n id: NodeId;\r\n key: string | number | null;\r\n kind: NodeKind;\r\n type?: string;\r\n hasChildren: boolean;\r\n size: number;\r\n preview: string;\r\n}\r\n\r\nexport interface DescribeOpts {\r\n offset?: number;\r\n limit?: number;\r\n}\r\n\r\nexport interface DescribeResult {\r\n node: NodeSummary;\r\n children: ChildSummary[];\r\n truncated?: { shown: number; total: number; nextOffset: number };\r\n}\r\n\r\nexport interface GetOpts {\r\n maxDepth?: number;\r\n}\r\n\r\nexport interface GetResult {\r\n id: NodeId;\r\n path: string;\r\n type?: string;\r\n content: Json;\r\n meta: NodeMeta;\r\n truncated?: boolean;\r\n}\r\n\r\nexport interface FindSelector {\r\n type?: string;\r\n tag?: string;\r\n pathPattern?: string;\r\n}\r\n\r\nexport interface FindOpts {\r\n limit?: number;\r\n /** JSON Pointer prefix: only nodes at/under it are hits — checked BEFORE the\r\n * limit is consumed, so out-of-scope matches never eat the budget. */\r\n within?: string;\r\n}\r\n\r\nexport interface FindHit {\r\n id: NodeId;\r\n path: string;\r\n type?: string;\r\n}\r\n\r\nexport interface FindResult {\r\n hits: FindHit[];\r\n /** True when the walk stopped at `limit` — there MAY be more matches (an exact-fit\r\n * final hit also reports true). */\r\n truncated: boolean;\r\n}\r\n\r\nfunction previewOf(node: ArbNode): string {\r\n if (node.kind === \"leaf\") {\r\n const s = JSON.stringify(node.content);\r\n return s.length <= PREVIEW_MAX ? s : s.slice(0, PREVIEW_MAX) + \"…\";\r\n }\r\n return node.kind === \"array\" ? `[${node.childIds.length} items]` : `{${node.childIds.length} keys}`;\r\n}\r\n\r\n/** Read-only navigation over the artifact tree: describe (cheap listing) and get (bounded content). */\r\nexport class Navigator {\r\n constructor(\r\n private readonly tree: ArtifactTree,\r\n private readonly addressing: Addressing,\r\n ) {}\r\n\r\n protected resolve(ref: Ref): ArbNode {\r\n const node = \"id\" in ref ? this.addressing.byId(ref.id) : this.addressing.byPath(ref.path);\r\n if (!node) throw new NodeNotFoundError(ref);\r\n return node;\r\n }\r\n\r\n private summarize(node: ArbNode): ChildSummary {\r\n return {\r\n id: node.id,\r\n key: node.key,\r\n kind: node.kind,\r\n type: node.type,\r\n hasChildren: node.childIds.length > 0,\r\n size: node.kind === \"leaf\" ? byteSize(node.content) : node.childIds.length,\r\n preview: previewOf(node),\r\n };\r\n }\r\n\r\n describe(ref: Ref = { path: \"\" }, opts: DescribeOpts = {}): DescribeResult {\r\n const node = this.resolve(ref);\r\n const all = this.tree.children(node.id);\r\n const total = all.length;\r\n const offset = opts.offset ?? 0;\r\n const limit = opts.limit ?? DEFAULT_LIMIT;\r\n const page = all.slice(offset, offset + limit);\r\n const result: DescribeResult = {\r\n node: {\r\n id: node.id,\r\n path: this.addressing.pathOf(node.id),\r\n key: node.key,\r\n kind: node.kind,\r\n type: node.type,\r\n },\r\n children: page.map((c) => this.summarize(c)),\r\n };\r\n const nextOffset = offset + page.length;\r\n if (nextOffset < total) {\r\n result.truncated = { shown: page.length, total, nextOffset };\r\n }\r\n return result;\r\n }\r\n\r\n get(ref: Ref, opts: GetOpts = {}): GetResult {\r\n const node = this.resolve(ref);\r\n const maxDepth = opts.maxDepth;\r\n let truncated = false;\r\n const reconstruct = (id: NodeId, depth: number): Json => {\r\n const n = this.tree.get(id)!;\r\n if (n.kind === \"leaf\") return n.content;\r\n if (maxDepth !== undefined && depth >= maxDepth) {\r\n truncated = true;\r\n const label = n.kind === \"array\" ? `${n.childIds.length} items` : `${n.childIds.length} keys`;\r\n return `[truncated: ${label}]`;\r\n }\r\n if (n.kind === \"array\") return n.childIds.map((cid) => reconstruct(cid, depth + 1));\r\n const obj: Record<string, Json> = {};\r\n for (const cid of n.childIds) {\r\n const c = this.tree.get(cid)!;\r\n obj[String(c.key)] = reconstruct(cid, depth + 1);\r\n }\r\n return obj;\r\n };\r\n const content = reconstruct(node.id, 0);\r\n const result: GetResult = {\r\n id: node.id,\r\n path: this.addressing.pathOf(node.id),\r\n type: node.type,\r\n content,\r\n meta: node.meta,\r\n };\r\n if (truncated) result.truncated = true;\r\n return result;\r\n }\r\n\r\n private matches(node: ArbNode, sel: FindSelector, path: string, glob?: (path: string) => boolean): boolean {\r\n if (sel.type !== undefined && node.type !== sel.type) return false;\r\n if (sel.tag !== undefined && !(node.tags?.includes(sel.tag) ?? false)) return false;\r\n if (glob !== undefined && !glob(path)) return false;\r\n return true;\r\n }\r\n\r\n /** Find nodes matching ALL provided selector fields (exact `type`, `tag` membership, glob `pathPattern`). */\r\n find(selector: FindSelector, opts: FindOpts = {}): FindResult {\r\n const limit = opts.limit ?? DEFAULT_LIMIT;\r\n const within = opts.within;\r\n // The pattern is parsed ONCE per find; each node's path is built incrementally\r\n // down the DFS (no per-node pathOf walk to the root).\r\n const glob = selector.pathPattern !== undefined ? compileGlob(selector.pathPattern) : undefined;\r\n const hits: FindHit[] = [];\r\n let truncated = false;\r\n const visit = (id: NodeId, path: string): void => {\r\n if (hits.length >= limit) {\r\n truncated = true;\r\n return;\r\n }\r\n const node = this.tree.get(id)!;\r\n if (this.matches(node, selector, path, glob)) {\r\n if (isWithin(path, within)) {\r\n hits.push({ id: node.id, path, type: node.type });\r\n }\r\n }\r\n for (const cid of node.childIds) {\r\n if (hits.length >= limit) {\r\n truncated = true;\r\n break;\r\n }\r\n const child = this.tree.get(cid)!;\r\n visit(cid, appendPointer(path, child.key as string | number));\r\n }\r\n };\r\n visit(this.tree.rootIdValue(), \"\");\r\n return { hits, truncated };\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parse a glob `pattern` ONCE and return a reusable matcher over JSON Pointer paths.
3
+ * `*` matches exactly one path segment; `**` matches zero or more segments.
4
+ */
5
+ declare function compileGlob(pattern: string): (path: string) => boolean;
6
+ /**
7
+ * Glob-match a JSON Pointer `path` against a `pattern`.
8
+ * `*` matches exactly one path segment; `**` matches zero or more segments.
9
+ * One-shot convenience over `compileGlob` — prefer compiling when matching many paths.
10
+ */
11
+ declare function matchGlob(pattern: string, path: string): boolean;
12
+
13
+ export { compileGlob, matchGlob };
@@ -0,0 +1,40 @@
1
+ // src/jsonpointer.ts
2
+ function decodeSegment(s) {
3
+ return s.replace(/~1/g, "/").replace(/~0/g, "~");
4
+ }
5
+ function parsePointer(pointer) {
6
+ if (pointer === "") return [];
7
+ if (!pointer.startsWith("/")) {
8
+ throw new Error(`Invalid JSON Pointer (must be "" or start with "/"): ${pointer}`);
9
+ }
10
+ return pointer.slice(1).split("/").map(decodeSegment);
11
+ }
12
+
13
+ // src/path-glob.ts
14
+ function matchSegments(pattern, path) {
15
+ if (pattern.length === 0) return path.length === 0;
16
+ const [head, ...rest] = pattern;
17
+ if (head === "**") {
18
+ for (let i = 0; i <= path.length; i++) {
19
+ if (matchSegments(rest, path.slice(i))) return true;
20
+ }
21
+ return false;
22
+ }
23
+ if (path.length === 0) return false;
24
+ if (head === "*" || head === path[0]) {
25
+ return matchSegments(rest, path.slice(1));
26
+ }
27
+ return false;
28
+ }
29
+ function compileGlob(pattern) {
30
+ const patternSegs = parsePointer(pattern);
31
+ return (path) => matchSegments(patternSegs, parsePointer(path));
32
+ }
33
+ function matchGlob(pattern, path) {
34
+ return compileGlob(pattern)(path);
35
+ }
36
+ export {
37
+ compileGlob,
38
+ matchGlob
39
+ };
40
+ //# sourceMappingURL=path-glob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jsonpointer.ts","../src/path-glob.ts"],"sourcesContent":["/** Escape a single reference token per RFC 6901: ~ -> ~0, / -> ~1. */\r\nexport function encodeSegment(s: string): string {\r\n return s.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\r\n}\r\n\r\n/** Unescape a single reference token per RFC 6901: ~1 -> /, then ~0 -> ~. */\r\nexport function decodeSegment(s: string): string {\r\n return s.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\r\n}\r\n\r\n/** Build a JSON Pointer string. Empty segment list => \"\" (root). */\r\nexport function buildPointer(segments: ReadonlyArray<string | number>): string {\r\n if (segments.length === 0) return \"\";\r\n return \"/\" + segments.map((s) => encodeSegment(String(s))).join(\"/\");\r\n}\r\n\r\n/** Append one child key to a parent pointer, escaping the key exactly as `buildPointer` does. */\r\nexport function appendPointer(parent: string, key: string | number): string {\r\n return parent + \"/\" + encodeSegment(String(key));\r\n}\r\n\r\n/** True when `path` is at or under `scope` (JSON Pointer prefix). Undefined scope = everywhere. */\r\nexport function isWithin(path: string, scope: string | undefined): boolean {\r\n return scope === undefined || path === scope || path.startsWith(scope + \"/\");\r\n}\r\n\r\n/** Parse a JSON Pointer into decoded segments. \"\" => [] (root). */\r\nexport function parsePointer(pointer: string): string[] {\r\n if (pointer === \"\") return [];\r\n if (!pointer.startsWith(\"/\")) {\r\n throw new Error(`Invalid JSON Pointer (must be \"\" or start with \"/\"): ${pointer}`);\r\n }\r\n return pointer.slice(1).split(\"/\").map(decodeSegment);\r\n}\r\n","import { parsePointer } from \"./jsonpointer\";\r\n\r\n/** Match parsed segment arrays. `*` = one segment; `**` = zero or more segments. */\r\nfunction matchSegments(pattern: string[], path: string[]): boolean {\r\n if (pattern.length === 0) return path.length === 0;\r\n const [head, ...rest] = pattern;\r\n if (head === \"**\") {\r\n for (let i = 0; i <= path.length; i++) {\r\n if (matchSegments(rest, path.slice(i))) return true;\r\n }\r\n return false;\r\n }\r\n if (path.length === 0) return false;\r\n if (head === \"*\" || head === path[0]) {\r\n return matchSegments(rest, path.slice(1));\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Parse a glob `pattern` ONCE and return a reusable matcher over JSON Pointer paths.\r\n * `*` matches exactly one path segment; `**` matches zero or more segments.\r\n */\r\nexport function compileGlob(pattern: string): (path: string) => boolean {\r\n const patternSegs = parsePointer(pattern);\r\n return (path: string) => matchSegments(patternSegs, parsePointer(path));\r\n}\r\n\r\n/**\r\n * Glob-match a JSON Pointer `path` against a `pattern`.\r\n * `*` matches exactly one path segment; `**` matches zero or more segments.\r\n * One-shot convenience over `compileGlob` — prefer compiling when matching many paths.\r\n */\r\nexport function matchGlob(pattern: string, path: string): boolean {\r\n return compileGlob(pattern)(path);\r\n}\r\n"],"mappings":";AAMO,SAAS,cAAc,GAAmB;AAC/C,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AACjD;AAmBO,SAAS,aAAa,SAA2B;AACtD,MAAI,YAAY,GAAI,QAAO,CAAC;AAC5B,MAAI,CAAC,QAAQ,WAAW,GAAG,GAAG;AAC5B,UAAM,IAAI,MAAM,wDAAwD,OAAO,EAAE;AAAA,EACnF;AACA,SAAO,QAAQ,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,aAAa;AACtD;;;AC9BA,SAAS,cAAc,SAAmB,MAAyB;AACjE,MAAI,QAAQ,WAAW,EAAG,QAAO,KAAK,WAAW;AACjD,QAAM,CAAC,MAAM,GAAG,IAAI,IAAI;AACxB,MAAI,SAAS,MAAM;AACjB,aAAS,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK;AACrC,UAAI,cAAc,MAAM,KAAK,MAAM,CAAC,CAAC,EAAG,QAAO;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AACA,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,MAAI,SAAS,OAAO,SAAS,KAAK,CAAC,GAAG;AACpC,WAAO,cAAc,MAAM,KAAK,MAAM,CAAC,CAAC;AAAA,EAC1C;AACA,SAAO;AACT;AAMO,SAAS,YAAY,SAA4C;AACtE,QAAM,cAAc,aAAa,OAAO;AACxC,SAAO,CAAC,SAAiB,cAAc,aAAa,aAAa,IAAI,CAAC;AACxE;AAOO,SAAS,UAAU,SAAiB,MAAuB;AAChE,SAAO,YAAY,OAAO,EAAE,IAAI;AAClC;","names":[]}