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

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 +89 -13
  2. package/dist/index.js +200 -110
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,26 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
2
 
3
+ type FieldKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "union" | "unknown";
4
+ interface FieldNode {
5
+ /** Dotted path from the schema root, with `[]` for array members: `cta.label`, `items[].title`. */
6
+ path: string;
7
+ kind: FieldKind;
8
+ optional: boolean;
9
+ /** Present only for `enum`. */
10
+ members?: readonly string[];
11
+ }
12
+ interface SchemaAdapter {
13
+ describe(schema: unknown): FieldNode[];
14
+ }
15
+
16
+ /**
17
+ * Reads a zod schema for the studio and hint resolution, entirely through the Standard JSON
18
+ * Schema converter the schema itself carries — `core` imports nothing from zod to do it.
19
+ * Validation never runs against this projection; it always runs `schema["~standard"].validate()`
20
+ * on the real schema.
21
+ */
22
+ declare const zodAdapter: SchemaAdapter;
23
+
3
24
  type UnknownProps = Record<string, unknown>;
4
25
  /**
5
26
  * A block's props, derived from its schema. The whole of invariant 1 in one type: there is no
@@ -148,6 +169,41 @@ declare class CompileError extends Error {
148
169
  constructor(issues: readonly CompileIssue[]);
149
170
  }
150
171
 
172
+ /** One route pointer and the artifact it names, as an adapter read them. */
173
+ interface LiveRoute {
174
+ pointer: RoutePointer;
175
+ /** `null` when the store holds no artifact at the pointer's hash — a pointer into nothing. */
176
+ artifact: Artifact | null;
177
+ }
178
+ /** One block's version delta between what a live artifact needs and what is registered now. */
179
+ interface BlockDrift {
180
+ block: string;
181
+ /** The version the artifact was compiled against. */
182
+ live: number;
183
+ /** The version registered now — `null` when the registry no longer holds the block at all. */
184
+ registered: number | null;
185
+ }
186
+ /**
187
+ * Why one live route would not render. `unreadable-artifact` is a pointer whose hash the store
188
+ * cannot resolve, which breaks the route without any registry change at all.
189
+ */
190
+ type RouteIncompatibility = {
191
+ route: string;
192
+ hash: string;
193
+ reason: "unreadable-artifact";
194
+ } | {
195
+ route: string;
196
+ hash: string;
197
+ reason: "block-drift";
198
+ drifted: BlockDrift[];
199
+ };
200
+ /** The verdict over every live pointer. `checked` is reported so a run over nothing cannot read as a pass. */
201
+ interface CompatibilityReport {
202
+ checked: number;
203
+ compatible: boolean;
204
+ incompatible: RouteIncompatibility[];
205
+ }
206
+
151
207
  interface Registry {
152
208
  get(name: string): Block | undefined;
153
209
  names(): string[];
@@ -155,6 +211,17 @@ interface Registry {
155
211
  fingerprint(): string;
156
212
  }
157
213
 
214
+ /**
215
+ * The guardrail's whole question: would this registry fail to render an artifact a live route
216
+ * pointer currently references? `checkRollback` decides each route — one artifact against the
217
+ * registry is the same comparison whether a pointer is moving or a registry is — and this walks
218
+ * every pointer the caller read, so publishing and merging are held to one rule.
219
+ *
220
+ * Pure, and synchronous: the caller reads the store and hands over what it found, so this runs
221
+ * in CI, in a worker, or in a browser studio unchanged.
222
+ */
223
+ declare function checkCompatibility(live: readonly LiveRoute[], registry: Registry): CompatibilityReport;
224
+
158
225
  type RollbackCheck = {
159
226
  compatible: true;
160
227
  } | {
@@ -200,18 +267,12 @@ declare function defineBlock<Schema extends StandardSchemaV1, Component>(block:
200
267
  */
201
268
  declare function defineCatalog(entries: Record<string, CatalogEntry>): Catalog;
202
269
 
203
- type FieldKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "union" | "unknown";
204
- interface FieldNode {
205
- /** Dotted path from the schema root, with `[]` for array members: `cta.label`, `items[].title`. */
206
- path: string;
207
- kind: FieldKind;
208
- optional: boolean;
209
- /** Present only for `enum`. */
210
- members?: readonly string[];
211
- }
212
- interface SchemaAdapter {
213
- describe(schema: unknown): FieldNode[];
214
- }
270
+ /**
271
+ * The report as a CI log reads it. `checked` leads every line, including the passing one: a run
272
+ * that found no pointers and a run that cleared eight are the same word otherwise, and the first
273
+ * of those is a gate certifying nothing.
274
+ */
275
+ declare function formatCompatibilityReport(report: CompatibilityReport): string;
215
276
 
216
277
  /**
217
278
  * matchKind is parsed from the route at publish, never caller-supplied. It lives in core so
@@ -219,4 +280,19 @@ interface SchemaAdapter {
219
280
  */
220
281
  declare function parseMatchKind(route: string): RoutePointer["matchKind"];
221
282
 
222
- export { type Artifact, type ArtifactNode, type ArtifactStore, type Block, type BlockDocs, type BlockUi, type Catalog, type CatalogEntry, CompileError, type CompileIssue, type CompileIssueCode, type DocumentMeta, type DocumentVersion, type FieldHint, type FieldHintData, type FieldKind, type FieldNode, type Holes, type InferProps, type Manifest, type Node, type Registry, type RollbackCheck, type RoutePointer, type SchemaAdapter, type SlotConstraint, type UnknownProps, checkRollback, compile, createRegistry, defineBlock, defineCatalog, parseMatchKind };
283
+ /** Copy-on-write down one dotted path. Paths address object fields only; `[]` has no single target. */
284
+ declare function setAtPath(target: Record<string, unknown>, path: string, value: unknown): Record<string, unknown>;
285
+
286
+ /**
287
+ * The first document operation: a new `DocumentVersion` with one prop set on one node,
288
+ * copy-on-write, every other node untouched by reference. It lives beside `compile` so any
289
+ * caller — an editor, a script, an agent — writes a document the same way
290
+ * ([#134](https://github.com/effekt/nubbin/issues/134)).
291
+ *
292
+ * It does not validate the value (that is `compile`'s job at the next compile) and does not
293
+ * bump `version` — appending a version is the authoring store's concern
294
+ * ([#11](https://github.com/effekt/nubbin/issues/11)), not a property of one edit.
295
+ */
296
+ declare function setNodeProp(version: DocumentVersion, nodeId: string, path: string, value: unknown): DocumentVersion;
297
+
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 };
package/dist/index.js CHANGED
@@ -1,3 +1,111 @@
1
+ // src/adapters/isStandardJsonSchemaCapable.ts
2
+ function isStandardJsonSchemaCapable(value) {
3
+ if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
4
+ const props = value["~standard"];
5
+ if (typeof props !== "object" || props === null || !("jsonSchema" in props)) return false;
6
+ const converter = props.jsonSchema;
7
+ if (typeof converter !== "object" || converter === null || !("input" in converter)) return false;
8
+ return typeof converter.input === "function";
9
+ }
10
+
11
+ // src/adapters/projectJsonSchema.ts
12
+ var OPTIONS = {
13
+ target: "draft-2020-12",
14
+ /** zod's option name: a type JSON Schema cannot express throws here, at registration. */
15
+ libraryOptions: { unrepresentable: "throw" }
16
+ };
17
+ function projectJsonSchema(schema) {
18
+ if (!isStandardJsonSchemaCapable(schema)) {
19
+ throw new Error("Schema does not expose the Standard JSON Schema converter (spec >= 1.1)");
20
+ }
21
+ return schema["~standard"].jsonSchema.input(OPTIONS);
22
+ }
23
+
24
+ // src/adapters/kindOfJsonSchema.ts
25
+ function kindOfJsonSchema(node) {
26
+ if (Array.isArray(node.enum)) return "enum";
27
+ if (Array.isArray(node.oneOf) || Array.isArray(node.anyOf)) return "union";
28
+ switch (node.type) {
29
+ case "string":
30
+ return "string";
31
+ case "number":
32
+ case "integer":
33
+ return "number";
34
+ case "boolean":
35
+ return "boolean";
36
+ case "array":
37
+ return "array";
38
+ case "object":
39
+ return "object";
40
+ default:
41
+ return "unknown";
42
+ }
43
+ }
44
+
45
+ // src/adapters/fieldNodeAt.ts
46
+ function fieldNodeAt(path, node, optional) {
47
+ const kind = kindOfJsonSchema(node);
48
+ if (kind === "enum" && Array.isArray(node.enum)) {
49
+ return { path, kind, optional, members: node.enum.map(String) };
50
+ }
51
+ return { path, kind, optional };
52
+ }
53
+
54
+ // src/adapters/isJsonSchemaNode.ts
55
+ function isJsonSchemaNode(value) {
56
+ return typeof value === "object" && value !== null && !Array.isArray(value);
57
+ }
58
+
59
+ // src/adapters/walkArrayItems.ts
60
+ function walkArrayItems(arrayNode, path, descend) {
61
+ if (!isJsonSchemaNode(arrayNode.items)) return [];
62
+ const itemPath = `${path}[]`;
63
+ return [fieldNodeAt(itemPath, arrayNode.items, false), ...descend(arrayNode.items, itemPath)];
64
+ }
65
+
66
+ // src/adapters/walkObjectProperties.ts
67
+ function walkObjectProperties(objectNode, basePath, descend) {
68
+ const { properties, required } = objectNode;
69
+ if (!isJsonSchemaNode(properties)) return [];
70
+ const requiredNames = Array.isArray(required) ? required : [];
71
+ const fields = [];
72
+ for (const [name, child] of Object.entries(properties)) {
73
+ if (!isJsonSchemaNode(child)) continue;
74
+ const path = basePath === "" ? name : `${basePath}.${name}`;
75
+ fields.push(fieldNodeAt(path, child, !requiredNames.includes(name)));
76
+ fields.push(...descend(child, path));
77
+ }
78
+ return fields;
79
+ }
80
+
81
+ // src/adapters/walkUnionBranches.ts
82
+ function walkUnionBranches(unionNode, path, descend) {
83
+ const branches = [unionNode.oneOf, unionNode.anyOf].filter(Array.isArray).flat();
84
+ return branches.flatMap((branch) => isJsonSchemaNode(branch) ? descend(branch, path) : []);
85
+ }
86
+
87
+ // src/adapters/walkJsonSchema.ts
88
+ function walkJsonSchema(node, basePath) {
89
+ const kind = kindOfJsonSchema(node);
90
+ if (kind === "object") return walkObjectProperties(node, basePath, walkJsonSchema);
91
+ if (kind === "array") return walkArrayItems(node, basePath, walkJsonSchema);
92
+ if (kind === "union") return walkUnionBranches(node, basePath, walkJsonSchema);
93
+ return [];
94
+ }
95
+
96
+ // src/adapters/zodAdapter.ts
97
+ var zodAdapter = {
98
+ describe(schema) {
99
+ const fields = walkJsonSchema(projectJsonSchema(schema), "");
100
+ const seen = /* @__PURE__ */ new Set();
101
+ return fields.filter((field) => {
102
+ if (seen.has(field.path)) return false;
103
+ seen.add(field.path);
104
+ return true;
105
+ });
106
+ }
107
+ };
108
+
1
109
  // src/CompileError.ts
2
110
  var CompileError = class extends Error {
3
111
  issues;
@@ -16,6 +124,34 @@ function checkRollback(artifact, registry) {
16
124
  return drifted.length === 0 ? { compatible: true } : { compatible: false, drifted };
17
125
  }
18
126
 
127
+ // src/describeDrift.ts
128
+ function describeDrift(artifact, registry, drifted) {
129
+ return drifted.flatMap((block) => {
130
+ const live = artifact.blockVersions[block];
131
+ return live === void 0 ? [] : [{ block, live, registered: registry.get(block)?.version ?? null }];
132
+ });
133
+ }
134
+
135
+ // src/checkCompatibility.ts
136
+ function checkCompatibility(live, registry) {
137
+ const incompatible = live.flatMap(({ pointer, artifact }) => {
138
+ const { route, hash } = pointer;
139
+ if (artifact === null) {
140
+ return [{ route, hash, reason: "unreadable-artifact" }];
141
+ }
142
+ const check = checkRollback(artifact, registry);
143
+ return check.compatible ? [] : [
144
+ {
145
+ route,
146
+ hash,
147
+ reason: "block-drift",
148
+ drifted: describeDrift(artifact, registry, check.drifted)
149
+ }
150
+ ];
151
+ });
152
+ return { checked: live.length, compatible: incompatible.length === 0, incompatible };
153
+ }
154
+
19
155
  // src/artifactNodeOf.ts
20
156
  function artifactNodeOf(node, resolve) {
21
157
  const { props, holes } = resolve(node);
@@ -401,7 +537,7 @@ function validateStructure(version, registry) {
401
537
  }
402
538
 
403
539
  // src/version.constants.ts
404
- var NUBBIN_VERSION = "0.1.0-rc.4";
540
+ var NUBBIN_VERSION = "0.1.0-rc.5";
405
541
 
406
542
  // src/compile.ts
407
543
  function compile(version, catalog, registry, route) {
@@ -497,114 +633,6 @@ function defineBlock(block) {
497
633
  return block;
498
634
  }
499
635
 
500
- // src/adapters/isStandardJsonSchemaCapable.ts
501
- function isStandardJsonSchemaCapable(value) {
502
- if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
503
- const props = value["~standard"];
504
- if (typeof props !== "object" || props === null || !("jsonSchema" in props)) return false;
505
- const converter = props.jsonSchema;
506
- if (typeof converter !== "object" || converter === null || !("input" in converter)) return false;
507
- return typeof converter.input === "function";
508
- }
509
-
510
- // src/adapters/projectJsonSchema.ts
511
- var OPTIONS = {
512
- target: "draft-2020-12",
513
- /** zod's option name: a type JSON Schema cannot express throws here, at registration. */
514
- libraryOptions: { unrepresentable: "throw" }
515
- };
516
- function projectJsonSchema(schema) {
517
- if (!isStandardJsonSchemaCapable(schema)) {
518
- throw new Error("Schema does not expose the Standard JSON Schema converter (spec >= 1.1)");
519
- }
520
- return schema["~standard"].jsonSchema.input(OPTIONS);
521
- }
522
-
523
- // src/adapters/kindOfJsonSchema.ts
524
- function kindOfJsonSchema(node) {
525
- if (Array.isArray(node.enum)) return "enum";
526
- if (Array.isArray(node.oneOf) || Array.isArray(node.anyOf)) return "union";
527
- switch (node.type) {
528
- case "string":
529
- return "string";
530
- case "number":
531
- case "integer":
532
- return "number";
533
- case "boolean":
534
- return "boolean";
535
- case "array":
536
- return "array";
537
- case "object":
538
- return "object";
539
- default:
540
- return "unknown";
541
- }
542
- }
543
-
544
- // src/adapters/fieldNodeAt.ts
545
- function fieldNodeAt(path, node, optional) {
546
- const kind = kindOfJsonSchema(node);
547
- if (kind === "enum" && Array.isArray(node.enum)) {
548
- return { path, kind, optional, members: node.enum.map(String) };
549
- }
550
- return { path, kind, optional };
551
- }
552
-
553
- // src/adapters/isJsonSchemaNode.ts
554
- function isJsonSchemaNode(value) {
555
- return typeof value === "object" && value !== null && !Array.isArray(value);
556
- }
557
-
558
- // src/adapters/walkArrayItems.ts
559
- function walkArrayItems(arrayNode, path, descend) {
560
- if (!isJsonSchemaNode(arrayNode.items)) return [];
561
- const itemPath = `${path}[]`;
562
- return [fieldNodeAt(itemPath, arrayNode.items, false), ...descend(arrayNode.items, itemPath)];
563
- }
564
-
565
- // src/adapters/walkObjectProperties.ts
566
- function walkObjectProperties(objectNode, basePath, descend) {
567
- const { properties, required } = objectNode;
568
- if (!isJsonSchemaNode(properties)) return [];
569
- const requiredNames = Array.isArray(required) ? required : [];
570
- const fields = [];
571
- for (const [name, child] of Object.entries(properties)) {
572
- if (!isJsonSchemaNode(child)) continue;
573
- const path = basePath === "" ? name : `${basePath}.${name}`;
574
- fields.push(fieldNodeAt(path, child, !requiredNames.includes(name)));
575
- fields.push(...descend(child, path));
576
- }
577
- return fields;
578
- }
579
-
580
- // src/adapters/walkUnionBranches.ts
581
- function walkUnionBranches(unionNode, path, descend) {
582
- const branches = [unionNode.oneOf, unionNode.anyOf].filter(Array.isArray).flat();
583
- return branches.flatMap((branch) => isJsonSchemaNode(branch) ? descend(branch, path) : []);
584
- }
585
-
586
- // src/adapters/walkJsonSchema.ts
587
- function walkJsonSchema(node, basePath) {
588
- const kind = kindOfJsonSchema(node);
589
- if (kind === "object") return walkObjectProperties(node, basePath, walkJsonSchema);
590
- if (kind === "array") return walkArrayItems(node, basePath, walkJsonSchema);
591
- if (kind === "union") return walkUnionBranches(node, basePath, walkJsonSchema);
592
- return [];
593
- }
594
-
595
- // src/adapters/zodAdapter.ts
596
- var zodAdapter = {
597
- describe(schema) {
598
- const fields = walkJsonSchema(projectJsonSchema(schema), "");
599
- const seen = /* @__PURE__ */ new Set();
600
- return fields.filter((field) => {
601
- if (seen.has(field.path)) return false;
602
- seen.add(field.path);
603
- return true;
604
- });
605
- }
606
- };
607
-
608
636
  // src/adapters/resolveHintPaths.ts
609
637
  function resolveHintPaths(blockName, schema, fields) {
610
638
  const known = new Set(zodAdapter.describe(schema).map((field) => field.path));
@@ -616,6 +644,17 @@ function resolveHintPaths(blockName, schema, fields) {
616
644
  }
617
645
  }
618
646
 
647
+ // src/assertDataHintAddressable.ts
648
+ function assertDataHintAddressable(blockName, fields) {
649
+ for (const [path, hint] of Object.entries(fields)) {
650
+ if (hint.data !== void 0 && path.includes("[]")) {
651
+ throw new Error(
652
+ `${blockName}: ui.fields["${path}"] sets \`data\`, but a hole cannot address an array member \u2014 "[]" has no single target`
653
+ );
654
+ }
655
+ }
656
+ }
657
+
619
658
  // src/assertValidDefaults.ts
620
659
  function assertValidDefaults(blockName, schema, defaults) {
621
660
  const result = standardValidate(schema, defaults);
@@ -629,6 +668,7 @@ function defineCatalog(entries) {
629
668
  for (const [blockName, entry] of Object.entries(entries)) {
630
669
  if (entry.ui?.fields !== void 0) {
631
670
  resolveHintPaths(blockName, entry.schema, entry.ui.fields);
671
+ assertDataHintAddressable(blockName, entry.ui.fields);
632
672
  }
633
673
  if (entry.defaults !== void 0) {
634
674
  assertValidDefaults(blockName, entry.schema, entry.defaults);
@@ -637,6 +677,27 @@ function defineCatalog(entries) {
637
677
  return entries;
638
678
  }
639
679
 
680
+ // src/formatBlockDrift.ts
681
+ function formatBlockDrift(drift) {
682
+ return drift.registered === null ? `${drift.block}: page needs v${drift.live}, no longer in the registry` : `${drift.block}: page needs v${drift.live}, registry has v${drift.registered}`;
683
+ }
684
+
685
+ // src/formatRouteIncompatibility.ts
686
+ function formatRouteIncompatibility(incompatibility) {
687
+ const heading = ` ${incompatibility.route} (artifact ${incompatibility.hash})`;
688
+ const reasons = incompatibility.reason === "unreadable-artifact" ? ["the store holds no artifact at this hash"] : incompatibility.drifted.map(formatBlockDrift);
689
+ return [heading, ...reasons.map((reason) => ` ${reason}`)].join("\n");
690
+ }
691
+
692
+ // src/formatCompatibilityReport.ts
693
+ function formatCompatibilityReport(report) {
694
+ if (report.compatible) {
695
+ return `${report.checked} live route pointer(s) checked; every one is compatible with this registry.`;
696
+ }
697
+ const summary = `${report.incompatible.length} of ${report.checked} live route pointer(s) are incompatible with this registry:`;
698
+ return [summary, ...report.incompatible.map(formatRouteIncompatibility)].join("\n");
699
+ }
700
+
640
701
  // src/parseMatchKind.ts
641
702
  function parseMatchKind(route) {
642
703
  if (route.endsWith("/*")) {
@@ -647,12 +708,41 @@ function parseMatchKind(route) {
647
708
  }
648
709
  return "exact";
649
710
  }
711
+
712
+ // src/setAtPath.ts
713
+ 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) {
719
+ return { ...target, [head]: value };
720
+ }
721
+ const child = target[head];
722
+ const base = typeof child === "object" && child !== null ? child : {};
723
+ return { ...target, [head]: setAtPath(base, rest.join("."), value) };
724
+ }
725
+
726
+ // src/setNodeProp.ts
727
+ function setNodeProp(version, nodeId, path, value) {
728
+ const node = version.elements[nodeId];
729
+ if (node === void 0) {
730
+ throw new Error(`no node "${nodeId}" in document "${version.documentId}"`);
731
+ }
732
+ const edited = { ...node, props: setAtPath(node.props, path, value) };
733
+ return { ...version, elements: { ...version.elements, [nodeId]: edited } };
734
+ }
650
735
  export {
651
736
  CompileError,
737
+ checkCompatibility,
652
738
  checkRollback,
653
739
  compile,
654
740
  createRegistry,
655
741
  defineBlock,
656
742
  defineCatalog,
657
- parseMatchKind
743
+ formatCompatibilityReport,
744
+ parseMatchKind,
745
+ setAtPath,
746
+ setNodeProp,
747
+ zodAdapter
658
748
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubbin/core",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.5",
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",