@nubbin/core 0.1.0-rc.6 → 0.1.0-rc.7

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 (3) hide show
  1. package/dist/index.d.ts +184 -56
  2. package/dist/index.js +369 -100
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -45,41 +45,9 @@ interface Block<Schema extends StandardSchemaV1 = StandardSchemaV1, Component =
45
45
  component: Component;
46
46
  /** Bumped when the schema changes incompatibly. */
47
47
  version: number;
48
- /** A deprecated block still resolves; the studio hides it from the palette. */
49
- status?: "active" | "deprecated";
50
48
  slots: Record<string, SlotConstraint>;
51
49
  }
52
50
 
53
- /** How a field's value resolves at render. Absent means static — the value freezes into props. */
54
- type FieldHintData = "request" | {
55
- revalidate: number;
56
- };
57
- /**
58
- * Studio treatment for one schema path. Open by design: control resolution ranks testers over
59
- * these hints, so a consumer can carry keys core does not read.
60
- */
61
- interface FieldHint {
62
- label?: string;
63
- control?: string;
64
- data?: FieldHintData;
65
- }
66
- interface BlockUi {
67
- /** Keyed by schema path (`title`, `cta.label`, `items[].icon`). Unresolvable paths fail registration. */
68
- fields?: Record<string, FieldHint>;
69
- }
70
- interface BlockDocs {
71
- summary?: string;
72
- usage?: string;
73
- }
74
- /** Serializable data only — what the studio and CI read. Components live in the registry. */
75
- interface CatalogEntry {
76
- schema: unknown;
77
- ui?: BlockUi;
78
- defaults?: UnknownProps;
79
- docs?: BlockDocs;
80
- }
81
- type Catalog = Record<string, CatalogEntry>;
82
-
83
51
  interface DocumentMeta {
84
52
  title: string;
85
53
  description?: string;
@@ -108,6 +76,46 @@ interface DocumentVersion {
108
76
  createdBy: string;
109
77
  }
110
78
 
79
+ /**
80
+ * Places a node in a parent's slot: a new `DocumentVersion` with the node registered and the
81
+ * slot's order rewritten, copy-on-write, every other node untouched by reference.
82
+ *
83
+ * `node.id` arrives already minted. `core` runs in a browser, a worker and a build step, and a
84
+ * generator inside it would make the same composition produce a different document every time —
85
+ * so the caller mints, and the two things this can still check are the parent it names and the
86
+ * id it brings.
87
+ *
88
+ * Slot legality is not one of them. Whether the block belongs in that slot, and whether the slot
89
+ * is now over its `max`, is `compile`'s judgment — the same division `setNodeProp` follows for a
90
+ * prop's validity, and what lets a document be illegal between two edits that end legal.
91
+ *
92
+ * `index` inserts; its absence appends.
93
+ */
94
+ declare function addNode(version: DocumentVersion, parentId: string, slot: string, node: Node, index?: number): DocumentVersion;
95
+
96
+ /** How a field's value resolves at render. Absent means static — the value freezes into props. */
97
+ type FieldHintData = {
98
+ revalidate: number;
99
+ };
100
+ /**
101
+ * Studio treatment for one schema path. Open by design: a consumer may carry keys core does not
102
+ * read, and core reads exactly one — how the field resolves.
103
+ */
104
+ interface FieldHint {
105
+ data?: FieldHintData;
106
+ }
107
+ interface BlockUi {
108
+ /** Keyed by schema path (`title`, `cta.label`, `items[].icon`). Unresolvable paths fail registration. */
109
+ fields?: Record<string, FieldHint>;
110
+ }
111
+ /** Serializable data only — what the studio and CI read. Components live in the registry. */
112
+ interface CatalogEntry {
113
+ schema: unknown;
114
+ ui?: BlockUi;
115
+ defaults?: UnknownProps;
116
+ }
117
+ type Catalog = Record<string, CatalogEntry>;
118
+
111
119
  /** Field path → how the field resolves at render. */
112
120
  type Holes = Record<string, FieldHintData>;
113
121
  /** Resolved — no lookups, no dangling references possible. */
@@ -126,7 +134,6 @@ interface Artifact {
126
134
  route: string;
127
135
  documentId: string;
128
136
  documentVersion: number;
129
- registryFingerprint: string;
130
137
  /** What this was compiled against — only the blocks the document uses. */
131
138
  blockVersions: Record<string, number>;
132
139
  tree: ArtifactNode[];
@@ -156,21 +163,6 @@ interface ArtifactStore {
156
163
  unpublish(route: string): Promise<void>;
157
164
  }
158
165
 
159
- type CompileIssueCode = "no-roots" | "unknown-block" | "dangling-child" | "cycle" | "unreachable" | "slot-not-allowed" | "slot-min" | "slot-max" | "invalid-props";
160
- interface CompileIssue {
161
- nodeId: string;
162
- /** Where in the node the problem sits: `block`, `slots.items`, or a dotted prop path. */
163
- path: string;
164
- code: CompileIssueCode;
165
- message: string;
166
- }
167
-
168
- /** Carries every issue found in one pass, so an author fixing six problems sees six. */
169
- declare class CompileError extends Error {
170
- readonly issues: readonly CompileIssue[];
171
- constructor(issues: readonly CompileIssue[]);
172
- }
173
-
174
166
  /** One route pointer and the artifact it names, as an adapter read them. */
175
167
  interface LiveRoute {
176
168
  pointer: RoutePointer;
@@ -209,8 +201,6 @@ interface CompatibilityReport {
209
201
  interface Registry {
210
202
  get(name: string): Block | undefined;
211
203
  names(): string[];
212
- /** Hash of every block name and version. Nothing else. */
213
- fingerprint(): string;
214
204
  }
215
205
 
216
206
  /**
@@ -238,17 +228,96 @@ type RollbackCheck = {
238
228
  */
239
229
  declare function checkRollback(artifact: Artifact, registry: Registry): RollbackCheck;
240
230
 
231
+ /**
232
+ * Every reason Nubbin refuses something, as a value a consumer can key off rather than a string
233
+ * they have to match. Each member's value is its own name in kebab-case, so a serialized issue
234
+ * reads the same in a log as it does in code.
235
+ *
236
+ * A const object rather than a TypeScript `enum`: it yields the same two things — a value to
237
+ * branch on and a type to check against — without the runtime object an `enum` emits, and `core`
238
+ * has to run unchanged in a browser, a worker and a build step.
239
+ *
240
+ * Closed on purpose. A new refusal is a new member here, which is what makes the reference page
241
+ * listing them checkable against this file.
242
+ */
243
+ declare const NubbinIssueCode: {
244
+ readonly BlockVersion: "block-version";
245
+ readonly SlotBounds: "slot-bounds";
246
+ readonly SlotAllowUnknown: "slot-allow-unknown";
247
+ readonly DuplicateBlockName: "duplicate-block-name";
248
+ readonly InvalidDefaults: "invalid-defaults";
249
+ readonly HintPathUnresolvable: "hint-path-unresolvable";
250
+ readonly HintNotAddressable: "hint-not-addressable";
251
+ readonly NotStandardSchema: "not-standard-schema";
252
+ readonly NoJsonSchema: "no-json-schema";
253
+ readonly NoRoots: "no-roots";
254
+ readonly UnknownBlock: "unknown-block";
255
+ readonly DanglingChild: "dangling-child";
256
+ readonly Cycle: "cycle";
257
+ readonly Unreachable: "unreachable";
258
+ readonly SlotNotAllowed: "slot-not-allowed";
259
+ readonly SlotMin: "slot-min";
260
+ readonly SlotMax: "slot-max";
261
+ readonly InvalidProps: "invalid-props";
262
+ readonly UnknownProp: "unknown-prop";
263
+ readonly NoSuchNode: "no-such-node";
264
+ readonly DuplicateNodeId: "duplicate-node-id";
265
+ readonly PathNotAddressable: "path-not-addressable";
266
+ readonly InvalidRoute: "invalid-route";
267
+ readonly BlockNotLoaded: "block-not-loaded";
268
+ readonly NoHoleResolver: "no-hole-resolver";
269
+ readonly NotOneHostElement: "not-one-host-element";
270
+ readonly ArtifactNotStored: "artifact-not-stored";
271
+ };
272
+ /** The value of any member, for a consumer narrowing on `issue.code`. */
273
+ type NubbinIssueCode = (typeof NubbinIssueCode)[keyof typeof NubbinIssueCode];
274
+
275
+ /**
276
+ * One reason Nubbin refused something. The shape every refusal takes, whether it was thrown or
277
+ * returned, so a consumer writes one handler and serializes one thing.
278
+ *
279
+ * `code` is for branching and `message` is for reading — the prose says what a person needs and
280
+ * the code says what a program can act on, and neither is asked to do the other's job.
281
+ *
282
+ * `at` and `path` are two coordinates rather than one string because an editing surface needs
283
+ * both: `at` names the thing to select — a node, a block, a route — and `path` names where
284
+ * inside it to highlight. Joining them into prose would leave a studio parsing its own error
285
+ * messages to find the field an author has to fix.
286
+ */
287
+ interface NubbinIssue {
288
+ code: NubbinIssueCode;
289
+ message: string;
290
+ /** What it concerns: a node id, a block name, or a route. */
291
+ at?: string;
292
+ /** Where within that: a dotted prop path, `slots.items`, or `block`. */
293
+ path?: string;
294
+ }
295
+
296
+ /**
297
+ * What `compile` produces: the artifact, and everything it has to say about the document that
298
+ * did not stop it producing one.
299
+ *
300
+ * The two travel together because the split is not severity, it is whether an artifact exists.
301
+ * A document the schema refuses has none, and `compile` throws; a document carrying a key the
302
+ * schema did not declare has a perfectly good one, and what became of that key is the consumer's
303
+ * to log, ship, or ignore.
304
+ */
305
+ interface CompileResult {
306
+ artifact: Artifact;
307
+ issues: readonly NubbinIssue[];
308
+ }
309
+
241
310
  /**
242
311
  * Orchestration only. Structure first, and stop there if it failed — prop validation on a
243
312
  * document with dangling references produces cascading noise that buries the real cause.
313
+ *
314
+ * The route is judged before any of it, because it is baked into the artifact and into the
315
+ * content address: an unaddressable route would otherwise compile, hash, and store cleanly, and
316
+ * only fail to match a request once it was live.
244
317
  */
245
- declare function compile(version: DocumentVersion, catalog: Catalog, registry: Registry, route: string): Artifact;
318
+ declare function compile(version: DocumentVersion, catalog: Catalog, registry: Registry, route: string): CompileResult;
246
319
 
247
320
  /**
248
- * Sorted by name so registration order cannot change the fingerprint, and built from name and
249
- * version alone so unrelated edits — a slot constraint, a deprecation — do not invalidate every
250
- * artifact compiled before them.
251
- *
252
321
  * Slot `allow` lists resolve once the whole array is ingested, so a block may name a sibling
253
322
  * registered after it.
254
323
  */
@@ -275,12 +344,71 @@ declare function defineCatalog(entries: Record<string, CatalogEntry>): Catalog;
275
344
  */
276
345
  declare function formatCompatibilityReport(report: CompatibilityReport): string;
277
346
 
347
+ /**
348
+ * Moves a node into a slot: a new `DocumentVersion` with the reference taken out of whatever held
349
+ * it — a slot or the roots — and placed in the target, the node itself untouched by reference.
350
+ *
351
+ * `index` names a position in the target slot *as it stands after the node is taken out*, which
352
+ * is the only reading under which "move it to the end" is the slot's length. Its absence appends.
353
+ *
354
+ * It does not refuse a move into the node's own subtree. That makes a cycle and detaches the node
355
+ * from the roots, and `compile` refuses both by name — judging it here would be a second opinion
356
+ * on one question, which is the division every operation in this package follows.
357
+ */
358
+ declare function moveNode(version: DocumentVersion, nodeId: string, toParentId: string, toSlot: string, index?: number): DocumentVersion;
359
+
360
+ /**
361
+ * Every refusal Nubbin throws, carrying its causes as data rather than only as prose. One class
362
+ * for the whole surface: a consumer writes one `catch`, reads `code` or `issues`, and ships that
363
+ * shape to whatever tooling they keep — the package neither logs nor decides what a refusal
364
+ * means to them.
365
+ *
366
+ * It extends `Error`, so a handler that only ever read `.message` keeps working.
367
+ */
368
+ declare class NubbinError extends Error {
369
+ /**
370
+ * The first issue's code, so the common case reads as
371
+ * `error.code === NubbinIssueCode.UnknownProp` with no array to index and no string to typo.
372
+ * Every refusal but `compile`'s carries exactly one issue; read `issues` for the rest.
373
+ */
374
+ readonly code: NubbinIssue["code"];
375
+ readonly issues: readonly NubbinIssue[];
376
+ constructor(issues: readonly NubbinIssue[]);
377
+ }
378
+
278
379
  /**
279
380
  * matchKind is parsed from the route at publish, never caller-supplied. It lives in core so
280
381
  * every adapter derives it from one implementation — a second parser is free to disagree.
382
+ *
383
+ * The route is judged before it is classified: this is the last point before a pointer is
384
+ * written, so an adapter that never called `compile` still cannot publish an unaddressable one.
281
385
  */
282
386
  declare function parseMatchKind(route: string): RoutePointer["matchKind"];
283
387
 
388
+ /**
389
+ * Throws the one-cause refusal that almost every surface raises, so a site says what it refuses
390
+ * and why in one line rather than assembling an issue array around it.
391
+ *
392
+ * `compile` is the exception and builds its own `NubbinError`, because it collects: an author
393
+ * fixing six problems should see six, not the first.
394
+ *
395
+ * Returns `never`, so a caller need not `return` after it for the narrowing to hold.
396
+ */
397
+ declare function refuse(code: NubbinIssue["code"], message: string, at?: string): never;
398
+
399
+ /**
400
+ * Removes a node and everything beneath it: a new `DocumentVersion` with the subtree gone, the
401
+ * reference to it dropped from whatever held it, and every other node untouched by reference.
402
+ *
403
+ * It cascades because removing a section means removing what was in it. Left behind, the children
404
+ * would be unreachable — which `compile` already refuses — so the alternative is a document that
405
+ * cannot compile until an author deletes each orphan by hand.
406
+ *
407
+ * Whether the slot it emptied is now below its `min` is `compile`'s judgment, as it is for every
408
+ * other operation here.
409
+ */
410
+ declare function removeNode(version: DocumentVersion, nodeId: string): DocumentVersion;
411
+
284
412
  /**
285
413
  * The inline emphasis a span may carry. Semantic, never stylistic, and closed: a construct
286
414
  * outside this set is added as a member deliberately, not smuggled in as markup.
@@ -343,4 +471,4 @@ declare function setAtPath(target: Record<string, unknown>, path: string, value:
343
471
  */
344
472
  declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
345
473
 
346
- export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDocs, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, CompileError, type CompileIssue, type CompileIssueCode, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, type Registry, type RichText, type RichTextBlock, type RichTextBlockKind, type RichTextMark, type RichTextSpan, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type StandardDataSchema, type UnknownProps, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, parseMatchKind, richText, setAtPath, setNodeProp, zodAdapter };
474
+ export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDrift, type BlockUi, type Catalog, type CatalogEntry, type CompatibilityReport, type CompileResult, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type LiveRoute, type Manifest, type Node, NubbinError, type NubbinIssue, NubbinIssueCode, type Registry, type RichText, type RichTextBlock, type RichTextBlockKind, type RichTextMark, type RichTextSpan, type RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type StandardDataSchema, type UnknownProps, addNode, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, moveNode, parseMatchKind, refuse, removeNode, richText, setAtPath, setNodeProp, zodAdapter };
package/dist/index.js CHANGED
@@ -1,3 +1,80 @@
1
+ // src/NubbinIssueCode.ts
2
+ var NubbinIssueCode = {
3
+ // Registration — a block, catalog or registry the developer wrote cannot be used as written.
4
+ BlockVersion: "block-version",
5
+ SlotBounds: "slot-bounds",
6
+ SlotAllowUnknown: "slot-allow-unknown",
7
+ DuplicateBlockName: "duplicate-block-name",
8
+ InvalidDefaults: "invalid-defaults",
9
+ HintPathUnresolvable: "hint-path-unresolvable",
10
+ HintNotAddressable: "hint-not-addressable",
11
+ // Schema — what the consumer brought does not answer the one door core reads a schema through.
12
+ NotStandardSchema: "not-standard-schema",
13
+ NoJsonSchema: "no-json-schema",
14
+ // Structure — the document's graph cannot become a tree.
15
+ NoRoots: "no-roots",
16
+ UnknownBlock: "unknown-block",
17
+ DanglingChild: "dangling-child",
18
+ Cycle: "cycle",
19
+ Unreachable: "unreachable",
20
+ SlotNotAllowed: "slot-not-allowed",
21
+ SlotMin: "slot-min",
22
+ SlotMax: "slot-max",
23
+ // Props — the values a node carries, judged against the block's schema.
24
+ InvalidProps: "invalid-props",
25
+ UnknownProp: "unknown-prop",
26
+ // Document operations — the caller named something the document does not hold.
27
+ NoSuchNode: "no-such-node",
28
+ DuplicateNodeId: "duplicate-node-id",
29
+ PathNotAddressable: "path-not-addressable",
30
+ InvalidRoute: "invalid-route",
31
+ // Render — the artifact and the registry serving it disagree.
32
+ BlockNotLoaded: "block-not-loaded",
33
+ NoHoleResolver: "no-hole-resolver",
34
+ NotOneHostElement: "not-one-host-element",
35
+ // Store — a write the store cannot honour.
36
+ ArtifactNotStored: "artifact-not-stored"
37
+ };
38
+
39
+ // src/summariseIssues.ts
40
+ function summariseIssues(issues) {
41
+ const lines = issues.map(
42
+ (issue) => issue.at === void 0 ? `${issue.message} [${issue.code}]` : `${issue.at} [${issue.code}]: ${issue.message}`
43
+ );
44
+ const only = lines[0];
45
+ if (lines.length === 1 && only !== void 0) {
46
+ return only;
47
+ }
48
+ return `Refused with ${issues.length} issue(s):
49
+ ${lines.join("\n")}`;
50
+ }
51
+
52
+ // src/NubbinError.ts
53
+ var NubbinError = class extends Error {
54
+ /**
55
+ * The first issue's code, so the common case reads as
56
+ * `error.code === NubbinIssueCode.UnknownProp` with no array to index and no string to typo.
57
+ * Every refusal but `compile`'s carries exactly one issue; read `issues` for the rest.
58
+ */
59
+ code;
60
+ issues;
61
+ constructor(issues) {
62
+ const first = issues[0];
63
+ if (first === void 0) {
64
+ throw new Error("NubbinError needs an issue \u2014 a refusal with no cause names nothing");
65
+ }
66
+ super(summariseIssues(issues));
67
+ this.name = "NubbinError";
68
+ this.code = first.code;
69
+ this.issues = issues;
70
+ }
71
+ };
72
+
73
+ // src/refuse.ts
74
+ function refuse(code, message, at) {
75
+ throw new NubbinError([at === void 0 ? { code, message } : { code, message, at }]);
76
+ }
77
+
1
78
  // src/adapters/isStandardJsonSchemaCapable.ts
2
79
  function isStandardJsonSchemaCapable(value) {
3
80
  if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
@@ -16,7 +93,10 @@ var OPTIONS = {
16
93
  };
17
94
  function projectJsonSchema(schema) {
18
95
  if (!isStandardJsonSchemaCapable(schema)) {
19
- throw new Error("Schema does not expose the Standard JSON Schema converter (spec >= 1.1)");
96
+ refuse(
97
+ NubbinIssueCode.NoJsonSchema,
98
+ "Schema does not expose the Standard JSON Schema converter (spec >= 1.1)"
99
+ );
20
100
  }
21
101
  return schema["~standard"].jsonSchema.input(OPTIONS);
22
102
  }
@@ -106,17 +186,47 @@ var zodAdapter = {
106
186
  }
107
187
  };
108
188
 
109
- // src/CompileError.ts
110
- var CompileError = class extends Error {
111
- issues;
112
- constructor(issues) {
113
- const summary = issues.map((issue) => `${issue.nodeId} ${issue.path} [${issue.code}]: ${issue.message}`).join("\n");
114
- super(`Compile failed with ${issues.length} issue(s):
115
- ${summary}`);
116
- this.name = "CompileError";
117
- this.issues = issues;
189
+ // src/requireNode.ts
190
+ function requireNode(version, nodeId) {
191
+ const node = version.elements[nodeId];
192
+ if (node === void 0) {
193
+ refuse(
194
+ NubbinIssueCode.NoSuchNode,
195
+ `no node "${nodeId}" in document "${version.documentId}"`,
196
+ nodeId
197
+ );
118
198
  }
119
- };
199
+ return node;
200
+ }
201
+
202
+ // src/withElements.ts
203
+ function withElements(version, ...nodes) {
204
+ const elements = { ...version.elements };
205
+ for (const node of nodes) {
206
+ elements[node.id] = node;
207
+ }
208
+ return { ...version, elements };
209
+ }
210
+
211
+ // src/withSlotChild.ts
212
+ function withSlotChild(node, slot, childId, index) {
213
+ const children = [...node.slots?.[slot] ?? []];
214
+ children.splice(index ?? children.length, 0, childId);
215
+ return { ...node, slots: { ...node.slots, [slot]: children } };
216
+ }
217
+
218
+ // src/addNode.ts
219
+ function addNode(version, parentId, slot, node, index) {
220
+ const parent = requireNode(version, parentId);
221
+ if (version.elements[node.id] !== void 0) {
222
+ refuse(
223
+ NubbinIssueCode.DuplicateNodeId,
224
+ `document "${version.documentId}" already holds a node "${node.id}"`,
225
+ node.id
226
+ );
227
+ }
228
+ return withElements(version, withSlotChild(parent, slot, node.id, index), node);
229
+ }
120
230
 
121
231
  // src/checkRollback.ts
122
232
  function checkRollback(artifact, registry) {
@@ -152,6 +262,43 @@ function checkCompatibility(live, registry) {
152
262
  return { checked: live.length, compatible: incompatible.length === 0, incompatible };
153
263
  }
154
264
 
265
+ // src/routeSegmentIssue.ts
266
+ var SEGMENT = /^[A-Za-z0-9\-._~%!$&'()+,;=:@]+$/;
267
+ var PARAM = /^\[[A-Za-z0-9_-]+\]$/;
268
+ function routeSegmentIssue(segment, isLast) {
269
+ if (segment === "") {
270
+ return "a segment names nothing";
271
+ }
272
+ if (segment === "*") {
273
+ return isLast ? void 0 : "a prefix star is only the final segment";
274
+ }
275
+ if (segment.startsWith("[") || segment.endsWith("]")) {
276
+ return PARAM.test(segment) ? void 0 : `"${segment}" is not a param segment`;
277
+ }
278
+ return SEGMENT.test(segment) ? void 0 : `"${segment}" carries a character a route cannot`;
279
+ }
280
+
281
+ // src/assertValidRoute.ts
282
+ function assertValidRoute(route) {
283
+ const unaddressable = (why) => refuse(NubbinIssueCode.InvalidRoute, `route "${route}" is not addressable: ${why}`, route);
284
+ if (!route.startsWith("/")) {
285
+ unaddressable("a route starts at the root, with a slash");
286
+ }
287
+ if (route === "/") {
288
+ return;
289
+ }
290
+ if (route.endsWith("/")) {
291
+ unaddressable("a trailing slash would key a second pointer to one page");
292
+ }
293
+ const segments = route.slice(1).split("/");
294
+ for (const [index, segment] of segments.entries()) {
295
+ const issue = routeSegmentIssue(segment, index === segments.length - 1);
296
+ if (issue !== void 0) {
297
+ unaddressable(issue);
298
+ }
299
+ }
300
+ }
301
+
155
302
  // src/artifactNodeOf.ts
156
303
  function artifactNodeOf(node, resolve) {
157
304
  const { props, holes } = resolve(node);
@@ -211,17 +358,18 @@ function denormalize(version, resolve) {
211
358
  }
212
359
 
213
360
  // src/fnv1a.ts
214
- var OFFSET_BASIS = 2166136261;
215
- var PRIME = 16777619;
361
+ var OFFSET_BASIS = 14695981039346656037n;
362
+ var PRIME = 1099511628211n;
363
+ var MASK = 0xffffffffffffffffn;
216
364
  var HEX_RADIX = 16;
217
- var HEX_WIDTH = 8;
365
+ var HEX_WIDTH = 16;
218
366
  function fnv1a(input) {
219
367
  let hash = OFFSET_BASIS;
220
368
  for (let index = 0; index < input.length; index += 1) {
221
- hash ^= input.charCodeAt(index);
222
- hash = Math.imul(hash, PRIME);
369
+ hash ^= BigInt(input.charCodeAt(index));
370
+ hash = hash * PRIME & MASK;
223
371
  }
224
- return (hash >>> 0).toString(HEX_RADIX).padStart(HEX_WIDTH, "0");
372
+ return hash.toString(HEX_RADIX).padStart(HEX_WIDTH, "0");
225
373
  }
226
374
 
227
375
  // src/hashArtifact.ts
@@ -234,11 +382,34 @@ function hashArtifact(artifact) {
234
382
  return fnv1a(JSON.stringify(artifact, sortKeys));
235
383
  }
236
384
 
385
+ // src/isUnknownProps.ts
386
+ function isUnknownProps(value) {
387
+ if (typeof value !== "object") return false;
388
+ return value !== null && !Array.isArray(value);
389
+ }
390
+
391
+ // src/droppedKeyPaths.ts
392
+ function droppedKeyPaths(written, parsed, prefix = "") {
393
+ const dropped = [];
394
+ for (const [key, value] of Object.entries(written)) {
395
+ const path = prefix === "" ? key : `${prefix}.${key}`;
396
+ if (!Object.hasOwn(parsed, key)) {
397
+ dropped.push(path);
398
+ continue;
399
+ }
400
+ const kept = parsed[key];
401
+ if (isUnknownProps(value) && isUnknownProps(kept)) {
402
+ dropped.push(...droppedKeyPaths(value, kept, path));
403
+ }
404
+ }
405
+ return dropped;
406
+ }
407
+
237
408
  // src/splitPath.ts
238
409
  function splitPath(path) {
239
410
  const [head, ...tail] = path.split(".");
240
411
  if (head === void 0 || head === "" || head.includes("[]")) {
241
- throw new Error(`path "${path}" is not addressable`);
412
+ refuse(NubbinIssueCode.PathNotAddressable, `path "${path}" is not addressable`, path);
242
413
  }
243
414
  return { head, tail };
244
415
  }
@@ -283,12 +454,6 @@ function formatIssuePath(path) {
283
454
  ).join(".");
284
455
  }
285
456
 
286
- // src/isUnknownProps.ts
287
- function isUnknownProps(value) {
288
- if (typeof value !== "object") return false;
289
- return value !== null && !Array.isArray(value);
290
- }
291
-
292
457
  // src/isStandardSchema.ts
293
458
  function isStandardSchema(value) {
294
459
  if (typeof value !== "object" || value === null) return false;
@@ -301,11 +466,15 @@ function isStandardSchema(value) {
301
466
  // src/standardValidate.ts
302
467
  function standardValidate(schema, value) {
303
468
  if (!isStandardSchema(schema)) {
304
- throw new Error("Schema does not implement Standard Schema (`~standard.validate`)");
469
+ refuse(
470
+ NubbinIssueCode.NotStandardSchema,
471
+ "Schema does not implement Standard Schema (`~standard.validate`)"
472
+ );
305
473
  }
306
474
  const result = schema["~standard"].validate(value);
307
475
  if (result instanceof Promise) {
308
- throw new Error(
476
+ refuse(
477
+ NubbinIssueCode.NotStandardSchema,
309
478
  "Schema validates asynchronously; compile and registration require synchronous validation"
310
479
  );
311
480
  }
@@ -317,9 +486,9 @@ function validateNodeProps(node, schema) {
317
486
  const result = standardValidate(schema, node.props);
318
487
  if (result.issues !== void 0) {
319
488
  const issues = result.issues.map((issue) => ({
320
- nodeId: node.id,
489
+ at: node.id,
321
490
  path: formatIssuePath(issue.path),
322
- code: "invalid-props",
491
+ code: NubbinIssueCode.InvalidProps,
323
492
  message: issue.message
324
493
  }));
325
494
  return { issues };
@@ -328,9 +497,9 @@ function validateNodeProps(node, schema) {
328
497
  return {
329
498
  issues: [
330
499
  {
331
- nodeId: node.id,
500
+ at: node.id,
332
501
  path: "",
333
- code: "invalid-props",
502
+ code: NubbinIssueCode.InvalidProps,
334
503
  message: "block props must parse to an object"
335
504
  }
336
505
  ]
@@ -343,20 +512,29 @@ function validateNodeProps(node, schema) {
343
512
  function resolveAllProps(version, catalog) {
344
513
  const resolved = /* @__PURE__ */ new Map();
345
514
  const issues = [];
515
+ const reported = [];
346
516
  for (const node of Object.values(version.elements)) {
347
517
  const entry = catalog[node.block];
348
518
  if (entry === void 0) {
349
519
  const message = `"${node.block}" has no catalog entry, so its props cannot be validated`;
350
- issues.push({ code: "unknown-block", message, nodeId: node.id, path: "block" });
520
+ issues.push({ code: NubbinIssueCode.UnknownBlock, message, at: node.id, path: "block" });
351
521
  continue;
352
522
  }
353
523
  const { value, issues: propIssues } = validateNodeProps(node, entry.schema);
354
524
  issues.push(...propIssues);
355
525
  if (value !== void 0) {
526
+ for (const path of droppedKeyPaths(node.props, value)) {
527
+ reported.push({
528
+ code: NubbinIssueCode.UnknownProp,
529
+ message: `"${path}" is not a field of ${node.block}, so the schema did not keep it`,
530
+ at: node.id,
531
+ path
532
+ });
533
+ }
356
534
  resolved.set(node.id, partitionProps(value, entry.ui));
357
535
  }
358
536
  }
359
- return { resolved, issues };
537
+ return { resolved, issues, reported };
360
538
  }
361
539
 
362
540
  // src/usedBlockVersions.ts
@@ -379,6 +557,16 @@ function pushCycleFrame(stack, state, version, id) {
379
557
  stack.push({ id, edges: slotEdges(node), next: 0 });
380
558
  }
381
559
 
560
+ // src/toIssue.ts
561
+ function toIssue(code, message, at, path) {
562
+ return {
563
+ code,
564
+ message,
565
+ ...at === void 0 ? {} : { at },
566
+ ...path === void 0 ? {} : { path }
567
+ };
568
+ }
569
+
382
570
  // src/findCyclesFrom.ts
383
571
  function findCyclesFrom(version, root, state) {
384
572
  const stack = [];
@@ -395,12 +583,14 @@ function findCyclesFrom(version, root, state) {
395
583
  }
396
584
  frame.next += 1;
397
585
  if (state.get(edge.childId) === "visiting") {
398
- issues.push({
399
- nodeId: frame.id,
400
- path: `slots.${edge.slot}`,
401
- code: "cycle",
402
- message: `"${frame.id}" reaches back to "${edge.childId}", so the graph cannot flatten into a tree`
403
- });
586
+ issues.push(
587
+ toIssue(
588
+ NubbinIssueCode.Cycle,
589
+ `"${frame.id}" reaches back to "${edge.childId}", so the graph cannot flatten into a tree`,
590
+ frame.id,
591
+ `slots.${edge.slot}`
592
+ )
593
+ );
404
594
  } else if (!state.has(edge.childId)) {
405
595
  pushCycleFrame(stack, state, version, edge.childId);
406
596
  }
@@ -422,12 +612,14 @@ function findDanglingChildren(version) {
422
612
  for (const node of Object.values(version.elements)) {
423
613
  for (const edge of slotEdges(node)) {
424
614
  if (version.elements[edge.childId] === void 0) {
425
- issues.push({
426
- nodeId: node.id,
427
- path: `slots.${edge.slot}`,
428
- code: "dangling-child",
429
- message: `child "${edge.childId}" has no matching element`
430
- });
615
+ issues.push(
616
+ toIssue(
617
+ NubbinIssueCode.DanglingChild,
618
+ `child "${edge.childId}" has no matching element`,
619
+ node.id,
620
+ `slots.${edge.slot}`
621
+ )
622
+ );
431
623
  }
432
624
  }
433
625
  }
@@ -439,9 +631,9 @@ function findRootIssues(version) {
439
631
  if (version.roots.length === 0) {
440
632
  return [
441
633
  {
442
- nodeId: "",
634
+ at: "",
443
635
  path: "roots",
444
- code: "no-roots",
636
+ code: NubbinIssueCode.NoRoots,
445
637
  message: "a document needs at least one root, and this one names none"
446
638
  }
447
639
  ];
@@ -449,9 +641,9 @@ function findRootIssues(version) {
449
641
  return version.roots.flatMap(
450
642
  (root) => version.elements[root] === void 0 ? [
451
643
  {
452
- nodeId: root,
644
+ at: root,
453
645
  path: "roots",
454
- code: "dangling-child",
646
+ code: NubbinIssueCode.DanglingChild,
455
647
  message: `root "${root}" has no matching element`
456
648
  }
457
649
  ] : []
@@ -466,9 +658,9 @@ function disallowedChildren(parent, path, childIds, allow, version) {
466
658
  const child = version.elements[childId];
467
659
  if (child === void 0 || allow.includes(child.block)) continue;
468
660
  issues.push({
469
- nodeId: childId,
661
+ at: childId,
470
662
  path,
471
- code: "slot-not-allowed",
663
+ code: NubbinIssueCode.SlotNotAllowed,
472
664
  message: `"${child.block}" is not allowed in ${path} of "${parent.block}"; allowed: ${allow.join(", ")}`
473
665
  });
474
666
  }
@@ -480,20 +672,20 @@ function slotBoundIssues(parentId, path, count, constraint) {
480
672
  const { min, max } = constraint;
481
673
  const bounds = [
482
674
  {
483
- code: "slot-min",
675
+ code: NubbinIssueCode.SlotMin,
484
676
  limit: min,
485
677
  breached: min !== void 0 && count < min,
486
678
  sense: "at least"
487
679
  },
488
680
  {
489
- code: "slot-max",
681
+ code: NubbinIssueCode.SlotMax,
490
682
  limit: max,
491
683
  breached: max !== void 0 && count > max,
492
684
  sense: "at most"
493
685
  }
494
686
  ];
495
687
  return bounds.filter((bound) => bound.breached).map((bound) => ({
496
- nodeId: parentId,
688
+ at: parentId,
497
689
  path,
498
690
  code: bound.code,
499
691
  message: `${path} holds ${count} of ${bound.sense} ${bound.limit}`
@@ -506,9 +698,9 @@ function slotIssuesAt(parent, slotName, childIds, constraint, version) {
506
698
  if (constraint === void 0) {
507
699
  return [
508
700
  {
509
- nodeId: parent.id,
701
+ at: parent.id,
510
702
  path,
511
- code: "slot-not-allowed",
703
+ code: NubbinIssueCode.SlotNotAllowed,
512
704
  message: `"${parent.block}" declares no slot "${slotName}"`
513
705
  }
514
706
  ];
@@ -535,28 +727,41 @@ function findSlotViolations(version, registry) {
535
727
  // src/findUnknownBlocks.ts
536
728
  function findUnknownBlocks(version, registry) {
537
729
  return Object.values(version.elements).filter((node) => registry.get(node.block) === void 0).map((node) => ({
538
- nodeId: node.id,
730
+ at: node.id,
539
731
  path: "block",
540
- code: "unknown-block",
732
+ code: NubbinIssueCode.UnknownBlock,
541
733
  message: `"${node.block}" is not a registered block`
542
734
  }));
543
735
  }
544
736
 
545
- // src/reachableIds.ts
546
- function reachableIds(version) {
547
- const seen = new Set(version.roots);
548
- const queue = [...version.roots];
549
- while (queue.length > 0) {
550
- const id = queue.pop();
551
- const node = id === void 0 ? void 0 : version.elements[id];
552
- if (node === void 0) continue;
737
+ // src/idsReachableFrom.ts
738
+ function idsReachableFrom(elements, seeds) {
739
+ const found = /* @__PURE__ */ new Set();
740
+ const pending = [...seeds];
741
+ while (pending.length > 0) {
742
+ const id = pending.pop();
743
+ const node = id === void 0 ? void 0 : elements[id];
744
+ if (node === void 0) {
745
+ continue;
746
+ }
553
747
  for (const edge of slotEdges(node)) {
554
- if (seen.has(edge.childId)) continue;
555
- seen.add(edge.childId);
556
- queue.push(edge.childId);
748
+ if (found.has(edge.childId)) {
749
+ continue;
750
+ }
751
+ found.add(edge.childId);
752
+ pending.push(edge.childId);
557
753
  }
558
754
  }
559
- return seen;
755
+ return found;
756
+ }
757
+
758
+ // src/reachableIds.ts
759
+ function reachableIds(version) {
760
+ const reached = idsReachableFrom(version.elements, version.roots);
761
+ for (const root of version.roots) {
762
+ reached.add(root);
763
+ }
764
+ return reached;
560
765
  }
561
766
 
562
767
  // src/findUnreachable.ts
@@ -566,9 +771,9 @@ function findUnreachable(version) {
566
771
  for (const node of Object.values(version.elements)) {
567
772
  if (reached.has(node.id)) continue;
568
773
  issues.push({
569
- nodeId: node.id,
774
+ at: node.id,
570
775
  path: "",
571
- code: "unreachable",
776
+ code: NubbinIssueCode.Unreachable,
572
777
  message: `no slot reaches "${node.id}" from any root`
573
778
  });
574
779
  }
@@ -590,26 +795,26 @@ function validateStructure(version, registry) {
590
795
  }
591
796
 
592
797
  // src/version.constants.ts
593
- var NUBBIN_VERSION = "0.1.0-rc.6";
798
+ var NUBBIN_VERSION = "0.1.0-rc.7";
594
799
 
595
800
  // src/compile.ts
596
801
  function compile(version, catalog, registry, route) {
802
+ assertValidRoute(route);
597
803
  const structural = validateStructure(version, registry);
598
- if (structural.length > 0) throw new CompileError(structural);
599
- const { resolved, issues } = resolveAllProps(version, catalog);
600
- if (issues.length > 0) throw new CompileError(issues);
804
+ if (structural.length > 0) throw new NubbinError(structural);
805
+ const { resolved, issues, reported } = resolveAllProps(version, catalog);
806
+ if (issues.length > 0) throw new NubbinError(issues);
601
807
  const tree = denormalize(version, (node) => resolved.get(node.id) ?? { props: {}, holes: {} });
602
808
  const content = {
603
809
  route,
604
810
  documentId: version.documentId,
605
811
  documentVersion: version.version,
606
- registryFingerprint: registry.fingerprint(),
607
812
  blockVersions: usedBlockVersions(version, registry),
608
813
  tree,
609
814
  meta: version.meta,
610
815
  compiledWith: NUBBIN_VERSION
611
816
  };
612
- return { ...content, hash: hashArtifact(content) };
817
+ return { artifact: { ...content, hash: hashArtifact(content) }, issues: reported };
613
818
  }
614
819
 
615
820
  // src/unknownAllowEntries.ts
@@ -624,7 +829,8 @@ function assertSlotAllows(blocks) {
624
829
  const known = new Set(blocks.map((block) => block.name));
625
830
  const unresolved = blocks.flatMap((block) => unknownAllowEntries(block, known));
626
831
  if (unresolved.length > 0) {
627
- throw new Error(
832
+ refuse(
833
+ NubbinIssueCode.SlotAllowUnknown,
628
834
  `Slot allow lists name ${unresolved.join(", ")}, which no registered block defines. Registered blocks: ${[...known].sort().join(", ")}`
629
835
  );
630
836
  }
@@ -635,26 +841,25 @@ function createRegistry(blocks) {
635
841
  const byName = /* @__PURE__ */ new Map();
636
842
  for (const block of blocks) {
637
843
  if (byName.has(block.name)) {
638
- throw new Error(
639
- `Duplicate block name "${block.name}" \u2014 names are the identity nodes resolve through`
844
+ refuse(
845
+ NubbinIssueCode.DuplicateBlockName,
846
+ `Duplicate block name "${block.name}" \u2014 names are the identity nodes resolve through`,
847
+ block.name
640
848
  );
641
849
  }
642
850
  byName.set(block.name, block);
643
851
  }
644
852
  assertSlotAllows([...byName.values()]);
645
- const signature = [...byName.values()].map((block) => `${block.name}@${block.version}`).sort().join("\n");
646
- const fingerprint = fnv1a(signature);
647
853
  return {
648
854
  get: (name) => byName.get(name),
649
- names: () => [...byName.keys()],
650
- fingerprint: () => fingerprint
855
+ names: () => [...byName.keys()]
651
856
  };
652
857
  }
653
858
 
654
859
  // src/assertBlockVersion.ts
655
860
  function assertBlockVersion(name, version) {
656
861
  if (!Number.isInteger(version) || version < 1) {
657
- throw new Error(`${name}: version must be an integer of 1 or more`);
862
+ refuse(NubbinIssueCode.BlockVersion, "version must be an integer of 1 or more", name);
658
863
  }
659
864
  }
660
865
 
@@ -662,7 +867,7 @@ function assertBlockVersion(name, version) {
662
867
  function assertSlotBounds(name, slots) {
663
868
  for (const [slot, { min, max }] of Object.entries(slots)) {
664
869
  if (min !== void 0 && max !== void 0 && min > max) {
665
- throw new Error(`${name}: slot "${slot}" has min ${min} above max ${max}`);
870
+ refuse(NubbinIssueCode.SlotBounds, `min ${min} is above max ${max}`, `${name} slots.${slot}`);
666
871
  }
667
872
  }
668
873
  }
@@ -679,8 +884,10 @@ function resolveHintPaths(blockName, schema, fields) {
679
884
  const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
680
885
  const unresolved = Object.keys(fields).filter((path) => !known.has(path));
681
886
  if (unresolved.length > 0) {
682
- throw new Error(
683
- `${blockName}: ui.fields references ${unresolved.map((p) => `"${p}"`).join(", ")}, which the schema does not define. Known paths: ${[...known].join(", ")}`
887
+ refuse(
888
+ NubbinIssueCode.HintPathUnresolvable,
889
+ `ui.fields references ${unresolved.map((p) => `"${p}"`).join(", ")}, which the schema does not define. Known paths: ${[...known].join(", ")}`,
890
+ blockName
684
891
  );
685
892
  }
686
893
  }
@@ -691,16 +898,20 @@ function assertDataHintAddressable(blockName, fields) {
691
898
  for (const [path, hint] of Object.entries(fields)) {
692
899
  if (hint.data === void 0) continue;
693
900
  if (path.includes("[]")) {
694
- throw new Error(
695
- `${blockName}: ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`
901
+ refuse(
902
+ NubbinIssueCode.HintNotAddressable,
903
+ `ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`,
904
+ blockName
696
905
  );
697
906
  }
698
907
  const nested = seen.find(
699
908
  (other) => path.startsWith(`${other}.`) || other.startsWith(`${path}.`)
700
909
  );
701
910
  if (nested !== void 0) {
702
- throw new Error(
703
- `${blockName}: ui.fields["${nested}"] and ui.fields["${path}"] both set \`data\`, but their paths overlap \u2014 two holes over one value have no defined order of application`
911
+ refuse(
912
+ NubbinIssueCode.HintNotAddressable,
913
+ `ui.fields["${nested}"] and ui.fields["${path}"] both set \`data\`, but their paths overlap \u2014 two holes over one value have no defined order of application`,
914
+ blockName
704
915
  );
705
916
  }
706
917
  seen.push(path);
@@ -712,7 +923,11 @@ function assertValidDefaults(blockName, schema, defaults) {
712
923
  const result = standardValidate(schema, defaults);
713
924
  if (result.issues === void 0) return;
714
925
  const detail = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
715
- throw new Error(`${blockName}: defaults do not satisfy the schema \u2014 ${detail}`);
926
+ refuse(
927
+ NubbinIssueCode.InvalidDefaults,
928
+ `defaults do not satisfy the schema \u2014 ${detail}`,
929
+ blockName
930
+ );
716
931
  }
717
932
 
718
933
  // src/defineCatalog.ts
@@ -750,8 +965,40 @@ function formatCompatibilityReport(report) {
750
965
  return [summary, ...report.incompatible.map(formatRouteIncompatibility)].join("\n");
751
966
  }
752
967
 
968
+ // src/withoutSlotChildren.ts
969
+ function withoutSlotChildren(node, removed) {
970
+ const entries = Object.entries(node.slots ?? {});
971
+ const holdsRemoved = entries.some(([, children]) => children.some((id) => removed.has(id)));
972
+ if (!holdsRemoved) {
973
+ return node;
974
+ }
975
+ const slots = Object.fromEntries(
976
+ entries.map(([slot, children]) => [slot, children.filter((id) => !removed.has(id))])
977
+ );
978
+ return { ...node, slots };
979
+ }
980
+
981
+ // src/detachIds.ts
982
+ function detachIds(version, ids) {
983
+ const elements = {};
984
+ for (const [id, node] of Object.entries(version.elements)) {
985
+ elements[id] = withoutSlotChildren(node, ids);
986
+ }
987
+ return { ...version, roots: version.roots.filter((id) => !ids.has(id)), elements };
988
+ }
989
+
990
+ // src/moveNode.ts
991
+ function moveNode(version, nodeId, toParentId, toSlot, index) {
992
+ requireNode(version, nodeId);
993
+ requireNode(version, toParentId);
994
+ const detached = detachIds(version, /* @__PURE__ */ new Set([nodeId]));
995
+ const parent = requireNode(detached, toParentId);
996
+ return withElements(detached, withSlotChild(parent, toSlot, nodeId, index));
997
+ }
998
+
753
999
  // src/parseMatchKind.ts
754
1000
  function parseMatchKind(route) {
1001
+ assertValidRoute(route);
755
1002
  if (route.endsWith("/*")) {
756
1003
  return "prefix";
757
1004
  }
@@ -761,6 +1008,20 @@ function parseMatchKind(route) {
761
1008
  return "exact";
762
1009
  }
763
1010
 
1011
+ // src/removeNode.ts
1012
+ function removeNode(version, nodeId) {
1013
+ requireNode(version, nodeId);
1014
+ const removed = idsReachableFrom(version.elements, [nodeId]).add(nodeId);
1015
+ const detached = detachIds(version, removed);
1016
+ const elements = {};
1017
+ for (const [id, node] of Object.entries(detached.elements)) {
1018
+ if (!removed.has(id)) {
1019
+ elements[id] = node;
1020
+ }
1021
+ }
1022
+ return { ...detached, elements };
1023
+ }
1024
+
764
1025
  // src/defineStandardSchema.ts
765
1026
  var STANDARD_SCHEMA_VERSION = 1;
766
1027
  function defineStandardSchema(issuesOf, jsonSchemaOf) {
@@ -914,21 +1175,26 @@ function setAtPath(target, path, value) {
914
1175
  return { ...target, [head]: value };
915
1176
  }
916
1177
  const child = target[head];
1178
+ if (Array.isArray(child)) {
1179
+ refuse(
1180
+ NubbinIssueCode.PathNotAddressable,
1181
+ `path "${path}" descends into an array at "${head}", which addresses no field`,
1182
+ path
1183
+ );
1184
+ }
917
1185
  const base = typeof child === "object" && child !== null ? child : {};
918
1186
  return { ...target, [head]: setAtPath(base, tail.join("."), value) };
919
1187
  }
920
1188
 
921
1189
  // src/setNodeProp.ts
922
1190
  function setNodeProp(version, nodeId, path, value) {
923
- const node = version.elements[nodeId];
924
- if (node === void 0) {
925
- throw new Error(`no node "${nodeId}" in document "${version.documentId}"`);
926
- }
927
- const edited = { ...node, props: setAtPath(node.props, path, value) };
928
- return { ...version, elements: { ...version.elements, [nodeId]: edited } };
1191
+ const node = requireNode(version, nodeId);
1192
+ return withElements(version, { ...node, props: setAtPath(node.props, path, value) });
929
1193
  }
930
1194
  export {
931
- CompileError,
1195
+ NubbinError,
1196
+ NubbinIssueCode,
1197
+ addNode,
932
1198
  checkCompatibility,
933
1199
  checkRollback,
934
1200
  compile,
@@ -936,7 +1202,10 @@ export {
936
1202
  defineBlock,
937
1203
  defineCatalog,
938
1204
  formatCompatibilityReport,
1205
+ moveNode,
939
1206
  parseMatchKind,
1207
+ refuse,
1208
+ removeNode,
940
1209
  richText,
941
1210
  setAtPath,
942
1211
  setNodeProp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubbin/core",
3
- "version": "0.1.0-rc.6",
3
+ "version": "0.1.0-rc.7",
4
4
  "description": "The Nubbin contract: define blocks, split catalog from registry, and compile a page document into an immutable artifact.",
5
5
  "keywords": [
6
6
  "nubbin",