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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StandardSchemaV1 } from '@standard-schema/spec';
1
+ import { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';
2
2
 
3
3
  type FieldKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "union" | "unknown";
4
4
  interface FieldNode {
@@ -48,8 +48,6 @@ interface Block<Schema extends StandardSchemaV1 = StandardSchemaV1, Component =
48
48
  /** A deprecated block still resolves; the studio hides it from the palette. */
49
49
  status?: "active" | "deprecated";
50
50
  slots: Record<string, SlotConstraint>;
51
- /** Same-node prop reshaping only. It cannot touch slots, or split or delete a block. */
52
- migrate?: Record<number, (props: UnknownProps) => UnknownProps>;
53
51
  }
54
52
 
55
53
  /** How a field's value resolves at render. Absent means static — the value freezes into props. */
@@ -99,7 +97,11 @@ interface Node {
99
97
  interface DocumentVersion {
100
98
  documentId: string;
101
99
  version: number;
102
- root: string;
100
+ /**
101
+ * Ordered entry elements — the artifact's tree is these, denormalized, in this order. See
102
+ * [A document has many roots](../../../docs/decisions/a-document-has-many-roots.md).
103
+ */
104
+ roots: readonly string[];
103
105
  elements: Record<string, Node>;
104
106
  meta: DocumentMeta;
105
107
  createdAt: string;
@@ -154,7 +156,7 @@ interface ArtifactStore {
154
156
  unpublish(route: string): Promise<void>;
155
157
  }
156
158
 
157
- type CompileIssueCode = "unknown-block" | "dangling-child" | "cycle" | "unreachable" | "slot-not-allowed" | "slot-min" | "slot-max" | "invalid-props";
159
+ type CompileIssueCode = "no-roots" | "unknown-block" | "dangling-child" | "cycle" | "unreachable" | "slot-not-allowed" | "slot-min" | "slot-max" | "invalid-props";
158
160
  interface CompileIssue {
159
161
  nodeId: string;
160
162
  /** Where in the node the problem sits: `block`, `slots.items`, or a dotted prop path. */
@@ -255,8 +257,7 @@ declare function createRegistry(blocks: readonly Block[]): Registry;
255
257
  /**
256
258
  * Identity at runtime; its job is to fix the generic parameters at the call site so props are
257
259
  * inferred from the schema rather than declared beside it. The checks here are the ones the
258
- * type system cannot make — a slot that no composition could satisfy, or a migration keyed to
259
- * a version this block never reaches.
260
+ * type system cannot make — a version below 1, or a slot that no composition could satisfy.
260
261
  */
261
262
  declare function defineBlock<Schema extends StandardSchemaV1, Component>(block: Block<Schema, Component>): Block<Schema, Component>;
262
263
 
@@ -280,6 +281,53 @@ declare function formatCompatibilityReport(report: CompatibilityReport): string;
280
281
  */
281
282
  declare function parseMatchKind(route: string): RoutePointer["matchKind"];
282
283
 
284
+ /**
285
+ * The inline emphasis a span may carry. Semantic, never stylistic, and closed: a construct
286
+ * outside this set is added as a member deliberately, not smuggled in as markup.
287
+ */
288
+ type RichTextMark = "strong" | "em" | "code";
289
+ /** The block kinds rich text is built from. Closed for the same reason the marks are. */
290
+ type RichTextBlockKind = "paragraph" | "listItem";
291
+ /** A run of text and what is true of it. Inert: nothing here is parsed or evaluated at render. */
292
+ interface RichTextSpan {
293
+ text: string;
294
+ marks?: readonly RichTextMark[];
295
+ href?: string;
296
+ }
297
+ /** One block of a rich-text value: its kind, and the ordered spans it reads as. */
298
+ interface RichTextBlock {
299
+ kind: RichTextBlockKind;
300
+ spans: readonly RichTextSpan[];
301
+ }
302
+ /** An ordered array of blocks — the whole value a `richText()` field holds. */
303
+ type RichText = readonly RichTextBlock[];
304
+
305
+ /**
306
+ * A schema `core` hand-writes rather than one a validator brings. Narrower than
307
+ * `StandardSchemaV1` in the two ways that matter to a consumer hosting it: `validate` is
308
+ * synchronous, which compile requires anyway, and the JSON Schema converter is always there,
309
+ * so the studio can read the field tree without testing for it.
310
+ */
311
+ interface StandardDataSchema<Value> {
312
+ readonly "~standard": {
313
+ readonly version: 1;
314
+ readonly vendor: string;
315
+ readonly validate: (value: unknown) => StandardSchemaV1.Result<Value>;
316
+ readonly jsonSchema: StandardJSONSchemaV1.Converter;
317
+ };
318
+ }
319
+
320
+ /**
321
+ * The rich-text field type: an ordered array of blocks, each an ordered array of inert spans,
322
+ * over closed mark and kind sets. Nothing in the value is parsed or evaluated at render, so an
323
+ * artifact carrying it is as inert as one carrying a string.
324
+ *
325
+ * A call rather than a bare value, so a field declaration reads the way the rest of a block's
326
+ * schema does and so options can arrive without changing call sites that pass none. A scan for
327
+ * this call finds every rich-text field in a registry.
328
+ */
329
+ declare function richText(): StandardDataSchema<RichText>;
330
+
283
331
  /** Copy-on-write down one dotted path. Paths address object fields only; `[]` has no single target. */
284
332
  declare function setAtPath(target: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
285
333
 
@@ -295,4 +343,4 @@ declare function setAtPath(target: Record<string, unknown>, path: string, value:
295
343
  */
296
344
  declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
297
345
 
298
- 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 RollbackCheck, type RouteIncompatibility, type RoutePointer, type SchemaAdapter, type SlotConstraint, type UnknownProps, checkCompatibility, checkRollback, compile, createRegistry, defineBlock, defineCatalog, formatCompatibilityReport, parseMatchKind, setAtPath, setNodeProp, zodAdapter };
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 };
package/dist/index.js CHANGED
@@ -191,7 +191,7 @@ function wireSlots(version, built) {
191
191
 
192
192
  // src/denormalize.ts
193
193
  function denormalize(version, resolve) {
194
- const pending = [version.root];
194
+ const pending = [...version.roots];
195
195
  const built = /* @__PURE__ */ new Map();
196
196
  while (pending.length > 0) {
197
197
  const id = pending.pop();
@@ -204,8 +204,10 @@ function denormalize(version, resolve) {
204
204
  }
205
205
  }
206
206
  wireSlots(version, built);
207
- const root = built.get(version.root);
208
- return root === void 0 ? [] : [root];
207
+ return version.roots.flatMap((id) => {
208
+ const root = built.get(id);
209
+ return root === void 0 ? [] : [root];
210
+ });
209
211
  }
210
212
 
211
213
  // src/fnv1a.ts
@@ -232,18 +234,43 @@ function hashArtifact(artifact) {
232
234
  return fnv1a(JSON.stringify(artifact, sortKeys));
233
235
  }
234
236
 
237
+ // src/splitPath.ts
238
+ function splitPath(path) {
239
+ const [head, ...tail] = path.split(".");
240
+ if (head === void 0 || head === "" || head.includes("[]")) {
241
+ throw new Error(`path "${path}" is not addressable`);
242
+ }
243
+ return { head, tail };
244
+ }
245
+
246
+ // src/takeAtPath.ts
247
+ function takeAtPath(target, path) {
248
+ const { head, tail } = splitPath(path);
249
+ if (!Object.hasOwn(target, head)) return { rest: target, taken: false };
250
+ if (tail.length === 0) {
251
+ const remaining = { ...target };
252
+ delete remaining[head];
253
+ return { rest: remaining, taken: true };
254
+ }
255
+ const child = target[head];
256
+ if (typeof child !== "object" || child === null || Array.isArray(child)) {
257
+ return { rest: target, taken: false };
258
+ }
259
+ const inner = takeAtPath(child, tail.join("."));
260
+ if (!inner.taken) return { rest: target, taken: false };
261
+ return { rest: { ...target, [head]: inner.rest }, taken: true };
262
+ }
263
+
235
264
  // src/partitionProps.ts
236
265
  function partitionProps(validated, hints) {
237
- const props = {};
266
+ let props = { ...validated };
238
267
  const holes = {};
239
- const fields = hints?.fields ?? {};
240
- for (const [key, value] of Object.entries(validated)) {
241
- const data = fields[key]?.data;
242
- if (data === void 0) {
243
- props[key] = value;
244
- } else {
245
- holes[key] = data;
246
- }
268
+ for (const [path, hint] of Object.entries(hints?.fields ?? {})) {
269
+ if (hint.data === void 0) continue;
270
+ const { rest, taken } = takeAtPath(props, path);
271
+ if (!taken) continue;
272
+ props = rest;
273
+ holes[path] = hint.data;
247
274
  }
248
275
  return { props, holes };
249
276
  }
@@ -352,12 +379,11 @@ function pushCycleFrame(stack, state, version, id) {
352
379
  stack.push({ id, edges: slotEdges(node), next: 0 });
353
380
  }
354
381
 
355
- // src/findCycles.ts
356
- function findCycles(version) {
357
- const state = /* @__PURE__ */ new Map();
382
+ // src/findCyclesFrom.ts
383
+ function findCyclesFrom(version, root, state) {
358
384
  const stack = [];
359
385
  const issues = [];
360
- pushCycleFrame(stack, state, version, version.root);
386
+ pushCycleFrame(stack, state, version, root);
361
387
  while (stack.length > 0) {
362
388
  const frame = stack.at(-1);
363
389
  if (frame === void 0) break;
@@ -382,17 +408,17 @@ function findCycles(version) {
382
408
  return issues;
383
409
  }
384
410
 
411
+ // src/findCycles.ts
412
+ function findCycles(version) {
413
+ const state = /* @__PURE__ */ new Map();
414
+ return version.roots.flatMap(
415
+ (root) => state.has(root) ? [] : findCyclesFrom(version, root, state)
416
+ );
417
+ }
418
+
385
419
  // src/findDanglingChildren.ts
386
420
  function findDanglingChildren(version) {
387
421
  const issues = [];
388
- if (version.elements[version.root] === void 0) {
389
- issues.push({
390
- nodeId: version.root,
391
- path: "root",
392
- code: "dangling-child",
393
- message: `root "${version.root}" has no matching element`
394
- });
395
- }
396
422
  for (const node of Object.values(version.elements)) {
397
423
  for (const edge of slotEdges(node)) {
398
424
  if (version.elements[edge.childId] === void 0) {
@@ -408,6 +434,30 @@ function findDanglingChildren(version) {
408
434
  return issues;
409
435
  }
410
436
 
437
+ // src/findRootIssues.ts
438
+ function findRootIssues(version) {
439
+ if (version.roots.length === 0) {
440
+ return [
441
+ {
442
+ nodeId: "",
443
+ path: "roots",
444
+ code: "no-roots",
445
+ message: "a document needs at least one root, and this one names none"
446
+ }
447
+ ];
448
+ }
449
+ return version.roots.flatMap(
450
+ (root) => version.elements[root] === void 0 ? [
451
+ {
452
+ nodeId: root,
453
+ path: "roots",
454
+ code: "dangling-child",
455
+ message: `root "${root}" has no matching element`
456
+ }
457
+ ] : []
458
+ );
459
+ }
460
+
411
461
  // src/disallowedChildren.ts
412
462
  function disallowedChildren(parent, path, childIds, allow, version) {
413
463
  if (allow === void 0) return [];
@@ -494,8 +544,8 @@ function findUnknownBlocks(version, registry) {
494
544
 
495
545
  // src/reachableIds.ts
496
546
  function reachableIds(version) {
497
- const seen = /* @__PURE__ */ new Set([version.root]);
498
- const queue = [version.root];
547
+ const seen = new Set(version.roots);
548
+ const queue = [...version.roots];
499
549
  while (queue.length > 0) {
500
550
  const id = queue.pop();
501
551
  const node = id === void 0 ? void 0 : version.elements[id];
@@ -519,7 +569,7 @@ function findUnreachable(version) {
519
569
  nodeId: node.id,
520
570
  path: "",
521
571
  code: "unreachable",
522
- message: `no slot reaches "${node.id}" from the root`
572
+ message: `no slot reaches "${node.id}" from any root`
523
573
  });
524
574
  }
525
575
  return issues;
@@ -527,8 +577,11 @@ function findUnreachable(version) {
527
577
 
528
578
  // src/validateStructure.ts
529
579
  function validateStructure(version, registry) {
580
+ const rootIssues = findRootIssues(version);
581
+ if (version.roots.length === 0) return rootIssues;
530
582
  return [
531
583
  ...findUnknownBlocks(version, registry),
584
+ ...rootIssues,
532
585
  ...findDanglingChildren(version),
533
586
  ...findCycles(version),
534
587
  ...findUnreachable(version),
@@ -537,7 +590,7 @@ function validateStructure(version, registry) {
537
590
  }
538
591
 
539
592
  // src/version.constants.ts
540
- var NUBBIN_VERSION = "0.1.0-rc.5";
593
+ var NUBBIN_VERSION = "0.1.0-rc.6";
541
594
 
542
595
  // src/compile.ts
543
596
  function compile(version, catalog, registry, route) {
@@ -605,17 +658,6 @@ function assertBlockVersion(name, version) {
605
658
  }
606
659
  }
607
660
 
608
- // src/assertMigrateKeys.ts
609
- var FIRST_MIGRATABLE_VERSION = 2;
610
- function assertMigrateKeys(name, version, migrate) {
611
- for (const key of Object.keys(migrate ?? {})) {
612
- const target = Number(key);
613
- if (target < FIRST_MIGRATABLE_VERSION || target > version) {
614
- throw new Error(`${name}: migrate key ${key} is outside the reachable range 2..${version}`);
615
- }
616
- }
617
- }
618
-
619
661
  // src/assertSlotBounds.ts
620
662
  function assertSlotBounds(name, slots) {
621
663
  for (const [slot, { min, max }] of Object.entries(slots)) {
@@ -629,7 +671,6 @@ function assertSlotBounds(name, slots) {
629
671
  function defineBlock(block) {
630
672
  assertBlockVersion(block.name, block.version);
631
673
  assertSlotBounds(block.name, block.slots);
632
- assertMigrateKeys(block.name, block.version, block.migrate);
633
674
  return block;
634
675
  }
635
676
 
@@ -646,12 +687,23 @@ function resolveHintPaths(blockName, schema, fields) {
646
687
 
647
688
  // src/assertDataHintAddressable.ts
648
689
  function assertDataHintAddressable(blockName, fields) {
690
+ const seen = [];
649
691
  for (const [path, hint] of Object.entries(fields)) {
650
- if (hint.data !== void 0 && path.includes("[]")) {
692
+ if (hint.data === void 0) continue;
693
+ if (path.includes("[]")) {
651
694
  throw new Error(
652
695
  `${blockName}: ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`
653
696
  );
654
697
  }
698
+ const nested = seen.find(
699
+ (other) => path.startsWith(`${other}.`) || other.startsWith(`${path}.`)
700
+ );
701
+ 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`
704
+ );
705
+ }
706
+ seen.push(path);
655
707
  }
656
708
  }
657
709
 
@@ -709,18 +761,161 @@ function parseMatchKind(route) {
709
761
  return "exact";
710
762
  }
711
763
 
764
+ // src/defineStandardSchema.ts
765
+ var STANDARD_SCHEMA_VERSION = 1;
766
+ function defineStandardSchema(issuesOf, jsonSchemaOf) {
767
+ return {
768
+ "~standard": {
769
+ version: STANDARD_SCHEMA_VERSION,
770
+ vendor: "nubbin",
771
+ validate: (value) => {
772
+ const issues = issuesOf(value);
773
+ return issues.length > 0 ? { issues } : { value };
774
+ },
775
+ jsonSchema: { input: jsonSchemaOf, output: jsonSchemaOf }
776
+ }
777
+ };
778
+ }
779
+
780
+ // src/richText.constants.ts
781
+ var RICH_TEXT_MARKS = ["strong", "em", "code"];
782
+ var RICH_TEXT_BLOCK_KINDS = ["paragraph", "listItem"];
783
+ var RICH_TEXT_SPAN_KEYS = ["text", "marks", "href"];
784
+ var RICH_TEXT_BLOCK_KEYS = ["kind", "spans"];
785
+
786
+ // src/isRichTextBlockKind.ts
787
+ function isRichTextBlockKind(value) {
788
+ return RICH_TEXT_BLOCK_KINDS.some((kind) => kind === value);
789
+ }
790
+
791
+ // src/nestedSchemaIssues.ts
792
+ function nestedSchemaIssues(schema, value, prefix) {
793
+ const result = standardValidate(schema, value);
794
+ if (result.issues === void 0) return [];
795
+ return result.issues.map((issue) => ({
796
+ message: issue.message,
797
+ path: [...prefix, ...issue.path ?? []]
798
+ }));
799
+ }
800
+
801
+ // src/isRichTextMark.ts
802
+ function isRichTextMark(value) {
803
+ return RICH_TEXT_MARKS.some((mark) => mark === value);
804
+ }
805
+
806
+ // src/richTextMarkIssues.ts
807
+ function richTextMarkIssues(marks) {
808
+ if (marks === void 0) return [];
809
+ if (!Array.isArray(marks)) return [{ message: "marks must be an array", path: ["marks"] }];
810
+ return marks.flatMap(
811
+ (mark, index) => isRichTextMark(mark) ? [] : [
812
+ {
813
+ message: `unknown mark ${JSON.stringify(mark)}; expected one of ${RICH_TEXT_MARKS.join(", ")}`,
814
+ path: ["marks", index]
815
+ }
816
+ ]
817
+ );
818
+ }
819
+
820
+ // src/unexpectedKeyIssues.ts
821
+ function unexpectedKeyIssues(value, allowed) {
822
+ return Object.keys(value).filter((key) => !allowed.includes(key)).map((key) => ({ message: `unexpected key "${key}"`, path: [key] }));
823
+ }
824
+
825
+ // src/richTextSpanIssues.ts
826
+ function richTextSpanIssues(value) {
827
+ if (!isUnknownProps(value)) return [{ message: "a span must be an object", path: [] }];
828
+ const issues = [];
829
+ if (typeof value.text !== "string") {
830
+ issues.push({ message: "text must be a string", path: ["text"] });
831
+ }
832
+ issues.push(...richTextMarkIssues(value.marks));
833
+ if (value.href !== void 0 && typeof value.href !== "string") {
834
+ issues.push({ message: "href must be a string", path: ["href"] });
835
+ }
836
+ issues.push(...unexpectedKeyIssues(value, RICH_TEXT_SPAN_KEYS));
837
+ return issues;
838
+ }
839
+
840
+ // src/richTextSpanSchema.ts
841
+ var richTextSpanSchema = defineStandardSchema(richTextSpanIssues, () => ({
842
+ type: "object",
843
+ properties: {
844
+ text: { type: "string" },
845
+ marks: { type: "array", items: { type: "string", enum: [...RICH_TEXT_MARKS] } },
846
+ href: { type: "string" }
847
+ },
848
+ required: ["text"],
849
+ additionalProperties: false
850
+ }));
851
+
852
+ // src/richTextBlockIssues.ts
853
+ function richTextBlockIssues(value) {
854
+ if (!isUnknownProps(value)) return [{ message: "a block must be an object", path: [] }];
855
+ const issues = [];
856
+ if (!isRichTextBlockKind(value.kind)) {
857
+ issues.push({
858
+ message: `unknown kind ${JSON.stringify(value.kind)}; expected one of ${RICH_TEXT_BLOCK_KINDS.join(", ")}`,
859
+ path: ["kind"]
860
+ });
861
+ }
862
+ if (Array.isArray(value.spans)) {
863
+ issues.push(
864
+ ...value.spans.flatMap(
865
+ (span, index) => nestedSchemaIssues(richTextSpanSchema, span, ["spans", index])
866
+ )
867
+ );
868
+ } else {
869
+ issues.push({ message: "spans must be an array", path: ["spans"] });
870
+ }
871
+ issues.push(...unexpectedKeyIssues(value, RICH_TEXT_BLOCK_KEYS));
872
+ return issues;
873
+ }
874
+
875
+ // src/richTextBlockSchema.ts
876
+ var richTextBlockSchema = defineStandardSchema(
877
+ richTextBlockIssues,
878
+ (options) => ({
879
+ type: "object",
880
+ properties: {
881
+ kind: { type: "string", enum: [...RICH_TEXT_BLOCK_KINDS] },
882
+ spans: { type: "array", items: richTextSpanSchema["~standard"].jsonSchema.input(options) }
883
+ },
884
+ required: ["kind", "spans"],
885
+ additionalProperties: false
886
+ })
887
+ );
888
+
889
+ // src/richTextIssues.ts
890
+ function richTextIssues(value) {
891
+ if (!Array.isArray(value)) {
892
+ return [{ message: "rich text must be an array of blocks", path: [] }];
893
+ }
894
+ return value.flatMap(
895
+ (block, index) => nestedSchemaIssues(richTextBlockSchema, block, [index])
896
+ );
897
+ }
898
+
899
+ // src/richTextSchema.ts
900
+ var richTextSchema = defineStandardSchema(richTextIssues, (options) => ({
901
+ type: "array",
902
+ items: richTextBlockSchema["~standard"].jsonSchema.input(options)
903
+ }));
904
+
905
+ // src/richText.ts
906
+ function richText() {
907
+ return richTextSchema;
908
+ }
909
+
712
910
  // src/setAtPath.ts
713
911
  function setAtPath(target, path, value) {
714
- const [head, ...rest] = path.split(".");
715
- if (head === void 0 || head === "" || head.includes("[]")) {
716
- throw new Error(`path "${path}" is not addressable`);
717
- }
718
- if (rest.length === 0) {
912
+ const { head, tail } = splitPath(path);
913
+ if (tail.length === 0) {
719
914
  return { ...target, [head]: value };
720
915
  }
721
916
  const child = target[head];
722
917
  const base = typeof child === "object" && child !== null ? child : {};
723
- return { ...target, [head]: setAtPath(base, rest.join("."), value) };
918
+ return { ...target, [head]: setAtPath(base, tail.join("."), value) };
724
919
  }
725
920
 
726
921
  // src/setNodeProp.ts
@@ -742,6 +937,7 @@ export {
742
937
  defineCatalog,
743
938
  formatCompatibilityReport,
744
939
  parseMatchKind,
940
+ richText,
745
941
  setAtPath,
746
942
  setNodeProp,
747
943
  zodAdapter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubbin/core",
3
- "version": "0.1.0-rc.5",
3
+ "version": "0.1.0-rc.6",
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",