smoodly 0.0.44 → 0.0.46

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 (241) hide show
  1. package/README.md +3 -3
  2. package/dist/addresses.d.ts +6 -7
  3. package/dist/addresses.js +6 -4
  4. package/dist/admin/PublicSans-LICENSE.txt +93 -0
  5. package/dist/admin/auth-config.d.ts +6 -0
  6. package/dist/admin/auth-config.js +1 -0
  7. package/dist/admin/browser/fetch-op.d.ts +10 -0
  8. package/dist/admin/browser/fetch-op.js +44 -0
  9. package/dist/admin/browser/main.d.ts +1 -0
  10. package/dist/admin/browser/main.js +21 -0
  11. package/dist/admin/custom-section.d.ts +1 -1
  12. package/dist/admin/drift.d.ts +1 -1
  13. package/dist/admin/editor/EditorView.js +14 -14
  14. package/dist/admin/editor/autosave.js +1 -1
  15. package/dist/admin/editor/canvas-address.d.ts +2 -0
  16. package/dist/admin/editor/canvas-address.js +8 -0
  17. package/dist/admin/editor/overlay-geometry.d.ts +1 -1
  18. package/dist/admin/editor/protocol.d.ts +18 -4
  19. package/dist/admin/editor/protocol.js +15 -1
  20. package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
  21. package/dist/admin/editor/useCanvasBridge.js +5 -6
  22. package/dist/admin/editor/useInlineSession.js +4 -2
  23. package/dist/admin/files.d.ts +11 -0
  24. package/dist/admin/files.generated.js +2 -0
  25. package/dist/admin/files.js +8 -0
  26. package/dist/admin/forms/useRefOptions.d.ts +2 -2
  27. package/dist/admin/forms/useRefOptions.js +2 -2
  28. package/dist/admin/forms/useRichtextEditor.js +4 -4
  29. package/dist/admin/handler.d.ts +40 -0
  30. package/dist/admin/handler.js +151 -0
  31. package/dist/admin/index.d.ts +1 -1
  32. package/dist/admin/ops-impl.d.ts +7 -1
  33. package/dist/admin/ops-impl.js +110 -33
  34. package/dist/admin/ops.d.ts +3 -2
  35. package/dist/admin/page-data.d.ts +21 -0
  36. package/dist/admin/page-data.js +4 -0
  37. package/dist/admin/page.d.ts +6 -0
  38. package/dist/admin/page.js +35 -0
  39. package/dist/admin/serialize.js +7 -7
  40. package/dist/admin/shell/AdminApp.js +2 -2
  41. package/dist/admin/shell/EntryForm.js +1 -1
  42. package/dist/admin/shell/router.d.ts +3 -16
  43. package/dist/admin/shell/router.js +3 -39
  44. package/dist/admin/shell/session.d.ts +2 -5
  45. package/dist/admin/supabase-auth.js +1 -1
  46. package/dist/admin/tree-ops.d.ts +1 -1
  47. package/dist/admin/ui/fonts.d.ts +12 -0
  48. package/dist/admin/ui/fonts.js +22 -0
  49. package/dist/admin/ui/theme.d.ts +13 -4
  50. package/dist/admin/ui/theme.js +28 -157
  51. package/dist/assemble.d.ts +60 -0
  52. package/dist/assemble.js +359 -0
  53. package/dist/astro/cache.d.ts +34 -0
  54. package/dist/astro/cache.js +69 -0
  55. package/dist/astro/components/Custom.astro +21 -0
  56. package/dist/astro/components/Guarded.astro +24 -0
  57. package/dist/astro/components/Head.astro +26 -0
  58. package/dist/astro/components/Image.astro +20 -0
  59. package/dist/astro/components/RichText.astro +23 -0
  60. package/dist/astro/components/Section.astro +20 -0
  61. package/dist/astro/components/Video.astro +5 -0
  62. package/dist/astro/components/Zone.astro +19 -0
  63. package/dist/astro/components.d.ts +6 -0
  64. package/dist/astro/components.js +7 -0
  65. package/dist/astro/content-loaders.d.ts +40 -0
  66. package/dist/astro/content-loaders.js +100 -0
  67. package/dist/astro/content.d.ts +9 -0
  68. package/dist/astro/content.js +17 -0
  69. package/dist/astro/custom.d.ts +1 -0
  70. package/dist/astro/custom.js +3 -0
  71. package/dist/astro/defaults.d.ts +20 -0
  72. package/dist/astro/defaults.js +21 -0
  73. package/dist/astro/env-files.d.ts +1 -0
  74. package/dist/astro/env-files.js +21 -0
  75. package/dist/astro/head.d.ts +13 -0
  76. package/dist/astro/head.js +19 -0
  77. package/dist/astro/image.d.ts +27 -0
  78. package/dist/astro/image.js +29 -0
  79. package/dist/astro/index.d.ts +43 -0
  80. package/dist/astro/index.js +111 -0
  81. package/dist/astro/locale-paths.d.ts +30 -0
  82. package/dist/astro/locale-paths.js +38 -0
  83. package/dist/astro/middleware.d.ts +1 -0
  84. package/dist/astro/middleware.js +83 -0
  85. package/dist/astro/preview-document.d.ts +1 -0
  86. package/dist/astro/preview-document.js +18 -0
  87. package/dist/astro/preview.d.ts +10 -0
  88. package/dist/astro/preview.js +72 -0
  89. package/dist/astro/richtext.d.ts +15 -0
  90. package/dist/astro/richtext.js +78 -0
  91. package/dist/astro/routes/admin.d.ts +3 -0
  92. package/dist/astro/routes/admin.js +41 -0
  93. package/dist/astro/routes/page.astro +17 -0
  94. package/dist/astro/routes/preview.astro +11 -0
  95. package/dist/astro/runtime.d.ts +32 -0
  96. package/dist/astro/runtime.js +46 -0
  97. package/dist/astro/scope.d.ts +45 -0
  98. package/dist/astro/scope.js +103 -0
  99. package/dist/astro/server.d.ts +14 -0
  100. package/dist/astro/server.js +45 -0
  101. package/dist/astro/shell.d.ts +67 -0
  102. package/dist/astro/shell.js +63 -0
  103. package/dist/astro/style.d.ts +1 -0
  104. package/dist/astro/style.js +11 -0
  105. package/dist/astro/views-check.d.ts +3 -0
  106. package/dist/astro/views-check.js +19 -0
  107. package/dist/astro/zone.d.ts +39 -0
  108. package/dist/astro/zone.js +33 -0
  109. package/dist/bind.d.ts +19 -0
  110. package/dist/bind.js +34 -0
  111. package/dist/bridge/bridge.d.ts +9 -0
  112. package/dist/bridge/bridge.js +107 -0
  113. package/dist/bridge/entry.d.ts +1 -0
  114. package/dist/bridge/entry.js +5 -0
  115. package/dist/bridge/preview-pill.d.ts +18 -0
  116. package/dist/bridge/preview-pill.js +115 -0
  117. package/dist/cli/import.d.ts +3 -0
  118. package/dist/cli/import.js +10 -1
  119. package/dist/cli/load-collections.js +4 -3
  120. package/dist/cli/load-config.d.ts +4 -0
  121. package/dist/cli/load-config.js +26 -0
  122. package/dist/cli/main.d.ts +7 -1
  123. package/dist/cli/main.js +32 -7
  124. package/dist/cli/migrate.d.ts +3 -0
  125. package/dist/cli/migrate.js +8 -1
  126. package/dist/cli/rebuild.d.ts +23 -0
  127. package/dist/cli/rebuild.js +22 -0
  128. package/dist/cli/space-io.d.ts +15 -1
  129. package/dist/cli/space-io.js +14 -0
  130. package/dist/client.js +5 -0
  131. package/dist/config.d.ts +28 -20
  132. package/dist/config.js +71 -75
  133. package/dist/content/content-api.d.ts +1 -1
  134. package/dist/content/node.d.ts +2 -3
  135. package/dist/content/node.js +2 -1
  136. package/dist/crash-message.d.ts +1 -0
  137. package/dist/crash-message.js +8 -0
  138. package/dist/css.d.ts +4 -0
  139. package/dist/css.js +5 -2
  140. package/dist/custom/CustomSection.d.ts +1 -1
  141. package/dist/custom/CustomSection.js +4 -8
  142. package/dist/custom/css.js +2 -2
  143. package/dist/custom/index.d.ts +6 -10
  144. package/dist/custom/index.js +1 -2
  145. package/dist/custom/layout.d.ts +5 -0
  146. package/dist/custom/layout.js +14 -0
  147. package/dist/entry-page.d.ts +3 -18
  148. package/dist/env.js +1 -1
  149. package/dist/fields.js +2 -1
  150. package/dist/global-id.js +2 -2
  151. package/dist/global.d.ts +5 -2
  152. package/dist/global.js +8 -5
  153. package/dist/image/next-config.d.ts +3 -0
  154. package/dist/image/next-config.js +12 -7
  155. package/dist/image/remote-pattern.d.ts +1 -0
  156. package/dist/image/remote-pattern.js +5 -0
  157. package/dist/index.d.ts +15 -17
  158. package/dist/index.js +10 -19
  159. package/dist/marks.d.ts +7 -0
  160. package/dist/marks.js +7 -0
  161. package/dist/next/admin-route.d.ts +7 -0
  162. package/dist/next/admin-route.js +28 -0
  163. package/dist/next/bridge.d.ts +1 -0
  164. package/dist/next/bridge.js +26 -0
  165. package/dist/next/canvas-boundary.d.ts +1 -1
  166. package/dist/next/canvas-slot.d.ts +1 -0
  167. package/dist/next/canvas-slot.js +4 -3
  168. package/dist/next/index.d.ts +2 -0
  169. package/dist/next/index.js +1 -0
  170. package/dist/next/page-renderer.d.ts +5 -1
  171. package/dist/next/page-renderer.js +19 -33
  172. package/dist/next/preview-pill-define.d.ts +1 -0
  173. package/dist/next/preview-pill-define.js +10 -0
  174. package/dist/next/preview-pill.d.ts +0 -3
  175. package/dist/next/preview-pill.js +7 -50
  176. package/dist/next/queries.d.ts +3 -28
  177. package/dist/next/queries.js +3 -90
  178. package/dist/next/reads.d.ts +2 -0
  179. package/dist/next/reads.js +18 -0
  180. package/dist/page-context.d.ts +89 -0
  181. package/dist/page-context.js +8 -0
  182. package/dist/pairing.d.ts +2 -22
  183. package/dist/project.d.ts +69 -0
  184. package/dist/project.js +210 -0
  185. package/dist/published.d.ts +96 -0
  186. package/dist/published.js +125 -0
  187. package/dist/react.d.ts +61 -0
  188. package/dist/react.js +17 -0
  189. package/dist/read-shapes.d.ts +1 -1
  190. package/dist/reads.d.ts +80 -0
  191. package/dist/reads.js +142 -0
  192. package/dist/refs.d.ts +1 -1
  193. package/dist/registration.d.ts +8 -0
  194. package/dist/registration.js +17 -0
  195. package/dist/render.d.ts +5 -83
  196. package/dist/render.js +5 -10
  197. package/dist/resolve.d.ts +1 -1
  198. package/dist/richtext/index.d.ts +6 -2
  199. package/dist/richtext/index.js +3 -2
  200. package/dist/richtext-href.d.ts +4 -0
  201. package/dist/richtext-href.js +17 -0
  202. package/dist/richtext-render.js +1 -14
  203. package/dist/schema-version.d.ts +1 -1
  204. package/dist/schema-version.js +1 -1
  205. package/dist/section-crash.d.ts +2 -2
  206. package/dist/section-crash.js +2 -7
  207. package/dist/section-wrapper.d.ts +3 -10
  208. package/dist/section-wrapper.js +3 -2
  209. package/dist/site.d.ts +38 -14
  210. package/dist/site.js +159 -286
  211. package/dist/smd-key.d.ts +9 -0
  212. package/dist/smd-key.js +1 -0
  213. package/dist/sql-space.d.ts +4 -0
  214. package/dist/sql-space.js +10 -5
  215. package/dist/sql.d.ts +2 -0
  216. package/dist/sql.js +117 -0
  217. package/dist/store.d.ts +1 -1
  218. package/dist/supabase-published-store.d.ts +35 -0
  219. package/dist/supabase-published-store.js +215 -0
  220. package/dist/supabase-store.d.ts +1 -1
  221. package/dist/toolset.d.ts +2 -4
  222. package/dist/toolset.js +6 -4
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/dist/view-props.d.ts +40 -0
  226. package/dist/view-props.js +1 -0
  227. package/dist/views.d.ts +36 -0
  228. package/dist/views.js +52 -0
  229. package/dist/zones.d.ts +7 -7
  230. package/dist/zones.js +7 -6
  231. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  232. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  233. package/package.json +46 -19
  234. package/dist/admin/next/bridge.d.ts +0 -2
  235. package/dist/admin/next/bridge.js +0 -129
  236. package/dist/admin/next/create-admin.d.ts +0 -19
  237. package/dist/admin/next/create-admin.js +0 -44
  238. package/dist/admin/next/index.d.ts +0 -8
  239. package/dist/admin/next/index.js +0 -8
  240. package/dist/admin/next/op-handler.d.ts +0 -18
  241. package/dist/admin/next/op-handler.js +0 -31
package/dist/site.js CHANGED
@@ -1,22 +1,16 @@
1
- import { ARRAY_FIELD_TYPES, entryFieldsIn, schemaOf } from "./entry-store.js";
2
- import { buildPageTree, chainOf, entryAlternates, entryPathRows, entrySegment, pageAlternates, pagePath, } from "./paths.js";
3
- import { resolveFields, resolveFieldSets, resolveMedia, resolveTree } from "./resolve.js";
4
- import { elementSchemas } from "./richtext-walk.js";
5
- import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
6
- import { SMD_KEY } from "./section-wrapper.js";
7
- import { resolveStyleValues } from "./styles.js";
8
- import { resolveCustom } from "./custom/index.js";
9
- import { addressMap, codeAddresses, lastPart } from "./addresses.js";
10
- import { fullPath, servedAnchor } from "./anchors.js";
1
+ import { schemaOf } from "./entry-store.js";
2
+ import { entrySegment } from "./paths.js";
3
+ import { lastPart } from "./addresses.js";
11
4
  import { withDefaults } from "./field-defaults.js";
12
5
  import { modelKey } from "./model-key.js";
13
6
  import { SMOODLY_VERSION } from "./version.js";
14
7
  import { toEntryItem, toPageItem } from "./read-shapes.js";
15
- import { entrySeo, pageSeo, SEO_KEY } from "./seo.js";
8
+ import { pageSeo } from "./seo.js";
9
+ import { checkEntryQuery, createAssembler, pageFieldsOf, siteShape } from "./assemble.js";
10
+ import { collectionOrder } from "./collections.js";
11
+ import { createProjector } from "./project.js";
16
12
  /** The `page` prop for a loaded page. `href` is the app's seam from a
17
13
  * locale-relative path to a site URL; the default keeps CMS paths as URLs. */
18
- /** Field types whose stored value is an object: not filterable (memory and SQL would disagree). */
19
- const OBJECT_FIELD_TYPES = new Set(["image", "video", "file", "richtext", "object"]);
20
14
  export function pageContextOf(hit, href = (p) => p) {
21
15
  const withUrl = (l) => ({ ...l, url: href(l.path, hit.locale) });
22
16
  return {
@@ -32,18 +26,9 @@ export function pageContextOf(hit, href = (p) => p) {
32
26
  };
33
27
  }
34
28
  export function createSmoodlySite(options) {
35
- const locales = options.locales;
36
- const homePageSlug = options.homePageSlug ?? "home";
37
- const styles = resolveStyleValues(options.styles);
38
- const custom = resolveCustom(options.custom);
39
- const elements = elementSchemas(options.elements);
40
- // Code-owned addresses from the site's own registry (spec 2026-09-29 nested paths §3): a code-owned root's path.
41
- const codeOwned = codeAddresses({ locales, pages: options.pages, collections: options.collections });
42
- const addresses = addressMap(codeOwned);
43
- const pathOpts = { locales, homePageSlug, addresses };
44
- /** The same addresses as the path index sees them: an entry's stored part joined to its collection's (spec 2026-09-29 anchored paths §2). */
45
- const anchors = { addresses, homePageSlug, defaultLocale: locales.default };
46
- const mounted = options.collections.filter((c) => c.path !== undefined);
29
+ const shape = siteShape(options);
30
+ const { locales, pageSchemas, homePageSlug, styles, custom, codeOwned } = shape;
31
+ const assembler = createAssembler(options, shape);
47
32
  // Every cached read keys on it (spec 2026-09-30 tester findings §3): an upgrade or a model change starts a fresh cache.
48
33
  const cacheKey = `${SMOODLY_VERSION}:${modelKey({
49
34
  // Whole schemas: a collection's order, tree or title field changes a read as its fields do (functions drop out in the hash).
@@ -51,245 +36,95 @@ export function createSmoodlySite(options) {
51
36
  globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, section: g.section })),
52
37
  sections: options.sections.map((x) => ("schema" in x ? x.schema : x)),
53
38
  elements: (options.elements ?? []).map((x) => ("schema" in x ? x.schema : x)),
54
- pages: options.pages.map((x) => x.schema),
39
+ pages: pageSchemas,
55
40
  locales, homePageSlug, styles, custom,
56
41
  })}`;
57
- /** The resolver's fetcher, bound to a locale: the merged fields of that
58
- * locale — published ones (a versioned collection's snapshot) on the
59
- * public path, the live rows in draft mode. */
60
- const fetcher = (draft, locale) => (collection, ids) => options.stores().entries.getMany(collection, ids, locale, draft ? undefined : { status: "published" });
61
- /** The resolver's path lookup, bound to a locale: each EntryRef's address (spec 2026-09-29 content API contract §2). */
62
- const pathLookup = (locale) => (collection, ids) => options.stores().entries.pathsIn(collection, ids, locale);
63
- /** The media fetcher (spec 2026-09-08 §9) — absent when the site runs without an asset store. */
64
- const assetFetcher = () => {
65
- const assets = options.stores().assets;
66
- return assets ? (ids) => assets.getMany(ids) : undefined;
67
- };
68
- const sectionSettings = (name) => {
69
- const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
70
- return (s && ("schema" in s ? s.schema : s).settings) ?? {};
71
- };
72
- /** Visible to the public in a locale: present there, and a folder or a
73
- * published page. Draft mode sees every present node. Presence is the
74
- * locale row — nothing falls back (spec 2026-09-06 §3). */
75
- const visible = (node, locale, draft) => {
76
- const row = node.locales[locale];
77
- return row !== undefined && (draft || node.template === null || row.status === "published");
78
- };
79
- const hasPage = (node, locale, draft) => {
80
- const row = node.locales[locale];
81
- return node.template !== null && row !== undefined && (draft || row.status === "published");
82
- };
83
- const pageFields = (template) => {
84
- if (template === null)
85
- return {};
86
- const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
87
- return p?.schema.fields ?? {};
88
- };
89
- /** The version whose fields a link shows: the draft's in draft mode, else the published one. */
90
- const versionFor = (node, locale, draft) => {
91
- const row = node.locales[locale];
92
- if (!row || node.template === null)
93
- return null;
94
- return draft ? row.draftVersionId : row.publishedVersionId;
95
- };
96
- /** Raw page fields (no asset resolution) of many nodes in ONE store read
97
- * (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
98
- async function linkFields(nodes, locale, draft) {
99
- const { pages } = options.stores();
100
- const wanted = new Map(); // node id → version id
101
- for (const n of nodes) {
102
- const v = versionFor(n, locale, draft);
103
- if (v)
104
- wanted.set(n.id, v);
105
- }
106
- const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
107
- const out = new Map();
108
- for (const n of nodes) {
109
- const v = wanted.get(n.id);
110
- out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
111
- }
112
- return out;
113
- }
114
- /** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
115
- const link = (node, byId, locale, draft, fields) => ({
116
- title: node.locales[locale].title,
117
- path: pagePath(node, byId, locale, pathOpts),
118
- hasPage: hasPage(node, locale, draft),
119
- fields: fields.get(node.id) ?? {},
120
- });
121
- async function loadPage(id, locale, path, draft, version) {
122
- const { pages } = options.stores();
123
- const record = await pages.getPage(id);
124
- if (!record || record.template === null)
125
- return null;
126
- const row = record.locales[locale];
127
- if (!row)
128
- return null;
129
- let raw;
130
- if (version !== undefined) {
131
- // History: a stored version of THIS page in THIS locale, and only inside draft mode.
132
- const stored = draft ? await pages.getVersion(version) : null;
133
- raw = stored && stored.pageId === id && stored.locale === locale ? stored.tree : null;
134
- }
135
- else {
136
- raw = draft ? await pages.getDraftTree(id, locale) : row.status === "published" ? await pages.getPublishedTree(id, locale) : null;
137
- }
138
- if (!raw)
139
- return null;
140
- const tree = await resolveTree(raw, {
141
- sections: options.sections,
142
- collections: options.collections,
143
- globals: options.globals,
144
- elements: options.elements,
145
- fetch: fetcher(draft, locale), paths: pathLookup(locale),
146
- assets: assetFetcher(),
147
- page: { fields: pageFields(record.template) },
148
- custom,
149
- });
150
- const all = await pages.listPages();
151
- const index = new Map(all.map((n) => [n.id, n]));
152
- const byId = (x) => index.get(x);
153
- const chain = chainOf(record, byId);
154
- const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
155
- const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
156
- const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
157
- const children = kids.map((n) => link(n, byId, locale, draft, fields));
158
- // Alternates advertise PUBLISHED locales only; the editor (draft) sees every present
159
- // one. Derived from the tree already listed — no index lookup (spec 2026-09-15 §2).
160
- const alternates = pageAlternates(record, byId, pathOpts, (l) => draft || record.locales[l]?.status === "published");
161
- const withSeo = (options.pages ?? []).some((x) => x.schema.kind === "page" && x.schema.name === record.template && x.schema.seo === true);
162
- return {
163
- kind: "page",
164
- locale,
165
- record,
166
- template: record.template,
167
- tree,
168
- title: row.title,
169
- path,
170
- fields: withDefaults(tree.fields, pageFields(record.template)),
171
- ancestors,
172
- children,
173
- locales: alternates,
174
- ...(withSeo ? { seo: pageSeo(tree.meta, row.title) } : {}),
175
- };
176
- }
177
- async function loadEntry(id, locale, path, draft) {
178
- const { entries } = options.stores();
179
- for (const schema of mounted) {
180
- const record = await entries.get(schema.name, id);
181
- if (!record)
182
- continue;
183
- // No row, or a draft row outside draft mode, is a 404 for THIS locale.
184
- const row = record.locales[locale];
185
- if (!row)
186
- return null;
187
- if (!draft && row.status !== "published")
188
- return null;
189
- const raw = draft
190
- ? entryFieldsIn(record, locale)
191
- : (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
192
- if (!raw)
193
- return null;
194
- const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
195
- // Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
196
- // One record is in memory (a nested entry's parents are not), so the `paths` index
197
- // supplies the paths — the same single lookup as before (spec 2026-09-15 §2).
198
- const known = await entries.pathsOf(schema.name, id);
199
- const alternates = entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", (l) => known[l] ?? null);
200
- return {
201
- kind: "entry", locale, collection: schema.name, entry: { ...record, fields }, path, locales: alternates,
202
- ...(schema.seo ? { seo: entrySeo(fields, schema) } : {}),
203
- };
204
- }
205
- return null;
206
- }
207
42
  const localeOf = (o) => o?.locale ?? locales.default;
208
- async function readEntries(collection, o = {}) {
43
+ const published = () => options.stores().published;
44
+ const projector = createProjector({
45
+ assembler,
46
+ stores: () => { const s = options.stores(); return { pages: s.pages, entries: s.entries, published: s.published }; },
47
+ collections: options.collections, globals: options.globals ?? [], sections: options.sections, elements: options.elements,
48
+ locales, model: cacheKey,
49
+ });
50
+ // Delivery (spec 2026-10-07 delivery model §3–§4): outside draft mode a read takes the stored document when it is
51
+ // current — built by this model and not marked stale — and otherwise assembles that one document, stores it and serves it.
52
+ const current = (row) => row.model === cacheKey && !row.stale;
53
+ const fresh = (row) => current(row) && row.document != null;
54
+ const itemOf = (doc) => toEntryItem(doc.entry, doc.entry.fields, { path: doc.path, locale: doc.locale, locales: doc.locales, ...(doc.seo ? { seo: doc.seo } : {}) });
55
+ /** A page row's answer: its document when current (null for a folder or a hidden node), else the healed one. */
56
+ const pageOf = (row, id, locale) => row && current(row) ? Promise.resolve(row.document) : projector.healPage(id, locale, { seen: row !== null });
57
+ /** An entry document is served at an address only when its collection has addresses. */
58
+ const served = (doc) => (doc && doc.path !== null ? doc : null);
59
+ const entryOf = async (row, id, locale) => row && fresh(row) ? row.document : projector.healEntry(id, locale, { seen: row !== null });
60
+ /** The published documents of a collection's query. Stale rows are rebuilt in one batched projection and the
61
+ * query read once more (spec §4): a rebuild that finds an entry unpublished removes its row, so it drops out. */
62
+ async function deliveredEntries(collection, o) {
209
63
  const schema = schemaOf(options.collections, collection);
64
+ checkEntryQuery(schema, collection, o);
210
65
  const locale = localeOf(o);
211
- const draft = o.draft ?? false;
212
- const { entries } = options.stores();
213
- // A filter that silently matched nothing would be worse than an error (spec 2026-09-11 §3).
214
- for (const key of Object.keys(o.where ?? {})) {
215
- if (key === "slug") {
216
- throw new Error(`smoodly: getEntries("${collection}"): "slug" is not filterable — where filters fields; find an entry by its path instead.`);
217
- }
218
- if (key === SEO_KEY)
219
- throw new Error(`smoodly: getEntries("${collection}"): "${SEO_KEY}" is reserved — the SEO panel's values are not filterable.`);
220
- if (!(key in schema.fields))
221
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" is not a field of the collection.`);
222
- // The memory adapter's `===` never matches an object while SQL's `@>` matches a subset, so the two would disagree.
223
- if (OBJECT_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
224
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" holds an object — filter on a scalar field instead.`);
66
+ const read = () => published().entries(collection, {
67
+ locale, where: o.where, order: o.order ?? collectionOrder(schema), limit: o.limit, ids: o.ids,
68
+ ...(o.parent !== undefined ? { parent: o.parent } : {}),
69
+ });
70
+ let rows = await read();
71
+ const behind = rows.filter((r) => !fresh(r)).map((r) => r.id);
72
+ if (behind.length > 0) {
73
+ // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
74
+ try {
75
+ await projector.entries(behind, { cap: Infinity });
76
+ rows = await read();
225
77
  }
226
- // Any-of reads the text form of the whole field (PostgREST's `->>`), which for
227
- // an array is its JSON — `["a"]` never matches "a" — so refuse rather than match nothing.
228
- if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
229
- throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
78
+ catch (e) {
79
+ console.error(`smoodly: rebuilding ${behind.length} ${collection} documents in the delivery tables failed; served as read.`, e);
230
80
  }
231
81
  }
232
- if (o.limit !== undefined && (!Number.isInteger(o.limit) || o.limit < 0)) {
233
- throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
234
- }
235
- // The store filters, orders and limits over the snapshot shown (spec 2026-09-29 content API contract §5): the
236
- // locale gate — presence in draft mode, published otherwise — is part of the query, so `limit` counts rows that are really there.
237
- const rows = await entries.query(collection, {
238
- locale, draft, where: o.where, order: o.order, limit: o.limit, ids: o.ids,
239
- ...(o.parent !== undefined ? { parent: o.parent } : {}),
240
- });
241
- // Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
242
- const all = schema.tree ? await entries.list(collection) : rows.map((r) => r.record);
243
- // References and media resolve for the whole list in one pass (§2, §6).
244
- const sets = rows.map((r) => ({ values: structuredClone(r.fields), descriptors: schema.fields }));
245
- await resolveFieldSets(sets, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
246
- const index = new Map(all.map((e) => [e.id, e]));
247
- const byId = (x) => index.get(x);
248
- return rows.map(({ record }, i) => {
249
- const paths = entryPathRows([record], byId, schema.name);
250
- const pathIn = (l) => {
251
- const row = paths.find((r) => r.locale === l);
252
- return row ? fullPath(anchors, row.anchor ?? "", row.path, l) : null;
253
- };
254
- const item = toEntryItem(record, sets[i].values, {
255
- path: pathIn(locale),
256
- locale,
257
- locales: entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", pathIn),
258
- ...(schema.seo ? { seo: entrySeo(sets[i].values, schema) } : {}),
259
- });
260
- return { record, item };
261
- });
82
+ return rows.flatMap((r) => (r.document != null ? [r.document] : []));
262
83
  }
263
- const getEntries = async (collection, o = {}) => (await readEntries(collection, o)).map((r) => r.item);
84
+ const getEntries = async (collection, o = {}) => o.draft ? (await assembler.entries(collection, o)).map((r) => r.item) : (await deliveredEntries(collection, o)).map(itemOf);
85
+ /** Every entry of the collection in the locale, with the record a tree and a bare slug read `parentId` from. */
86
+ const readAll = async (collection, locale, draft) => draft
87
+ ? assembler.entries(collection, { locale, draft })
88
+ : (await deliveredEntries(collection, { locale })).map((doc) => ({ record: doc.entry, item: itemOf(doc) }));
264
89
  const api = {
265
90
  async getPagesTree(o) {
266
91
  const locale = localeOf(o);
267
- const draft = o?.draft ?? false;
268
- const all = await options.stores().pages.listPages();
269
- const shown = all.filter((n) => visible(n, locale, draft));
270
- const index = new Map(shown.map((n) => [n.id, n]));
271
- const fields = await linkFields(shown, locale, draft);
272
- // Every node the walk reaches is present with a present chain. Left
273
- // out, with its subtree: a record no longer in code (no path, spec
274
- // 2026-09-29 nested paths §5), and a page where another code address
275
- // wins, which visitors never reach (anchored paths §4).
276
- const hidden = (node) => node.path === null || servedAnchor(anchors, locale, node.path) !== (chainOf(node.record, (x) => index.get(x))[0].fixed ?? "");
277
- const toItems = (nodes) => nodes.flatMap((node) => hidden(node) ? [] : [{
278
- id: node.record.id,
279
- title: node.record.locales[locale].title,
280
- path: node.path,
281
- hasPage: hasPage(node.record, locale, draft),
282
- fields: fields.get(node.record.id) ?? {},
283
- children: toItems(node.children),
284
- }]);
285
- return toItems(buildPageTree(shown, locale, pathOpts));
92
+ if (o?.draft)
93
+ return assembler.tree(locale, { draft: true });
94
+ // One light read. A row behind the model or marked stale is rebuilt before answering (spec §4), and the tree read
95
+ // again; a rebuild that finds the node gone or unpublished removes its row.
96
+ let rows = await published().tree(locale);
97
+ const behind = rows.filter((r) => !current(r)).map((r) => r.id);
98
+ if (behind.length > 0) {
99
+ // One batched projection for every row behind (one tree read in the projector), then one more tree read.
100
+ // A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
101
+ try {
102
+ await projector.pages(behind, { cap: Infinity });
103
+ rows = await published().tree(locale);
104
+ }
105
+ catch (e) {
106
+ console.error(`smoodly: rebuilding ${behind.length} page documents in the delivery tables failed; served as read.`, e);
107
+ }
108
+ }
109
+ // A node the tree hides (path null) leaves with its subtree, as does one whose parent has no row.
110
+ const build = (parentId) => rows.filter((r) => r.parentId === parentId && r.path !== null).map((r) => ({
111
+ id: r.id, title: r.title, path: r.path, hasPage: r.hasPage, fields: r.linkFields, children: build(r.id),
112
+ }));
113
+ return build(null);
286
114
  },
287
115
  async getPage(pathOrId, o) {
288
116
  const locale = localeOf(o);
117
+ if (!o?.draft) {
118
+ // The content API stays strict (§7): a redirect row, and a code-owned page with no record, are not pages.
119
+ const hit = pathOrId.startsWith("/")
120
+ ? (await delivered(locale, pathOrId, false)).hit
121
+ : await pageOf(await published().page(pathOrId, locale), pathOrId, locale);
122
+ return hit?.kind === "page" ? toPageItem(hit) : null;
123
+ }
289
124
  const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
290
125
  if (!path)
291
126
  return null;
292
- const hit = await site.resolve(locale, path, { draft: o?.draft ?? false });
127
+ const hit = await site.resolve(locale, path, { draft: true });
293
128
  return hit?.kind === "page" ? toPageItem(hit) : null;
294
129
  },
295
130
  getEntries,
@@ -298,64 +133,78 @@ export function createSmoodlySite(options) {
298
133
  const locale = localeOf(o);
299
134
  const draft = o?.draft ?? false;
300
135
  if (idOrPath.startsWith("/")) {
301
- const hit = await site.resolve(locale, idOrPath, { draft });
136
+ const hit = draft ? await site.resolve(locale, idOrPath, { draft }) : (await delivered(locale, idOrPath, false)).hit;
302
137
  return hit?.kind === "entry" && hit.collection === collection ? toEntryItem(hit.entry, hit.entry.fields, hit) : null;
303
138
  }
304
- const [byId] = await readEntries(collection, { locale, draft, ids: [idOrPath] });
305
- if (byId)
306
- return byId.item;
139
+ if (draft) {
140
+ const [byId] = await assembler.entries(collection, { locale, draft, ids: [idOrPath] });
141
+ if (byId)
142
+ return byId.item;
143
+ }
144
+ else {
145
+ const row = await published().entry(idOrPath, locale);
146
+ if (row && row.collection === collection && fresh(row))
147
+ return itemOf(row.document);
148
+ // A miss is a record no projection wrote yet, or a slug: one `get` tells them apart.
149
+ if (row?.collection === collection || (await options.stores().entries.get(collection, idOrPath))) {
150
+ const doc = await projector.healEntry(idOrPath, locale, { seen: row !== null });
151
+ if (doc)
152
+ return itemOf(doc);
153
+ }
154
+ }
307
155
  // A bare slug names a ROOT entry only (sibling-scoped slugs, spec §4):
308
156
  // match the localized slug directly (paths.ts owns slug derivation),
309
157
  // regardless of whether the collection even has a `path`.
310
- const all = await readEntries(collection, { locale, draft });
158
+ const all = await readAll(collection, locale, draft);
311
159
  return all.find((r) => r.record.parentId === null && entrySegment(r.record, locale) === idOrPath)?.item ?? null;
312
160
  },
313
161
  async getEntriesTree(collection, o) {
314
- const rows = await readEntries(collection, { locale: localeOf(o), draft: o?.draft ?? false });
162
+ const rows = await readAll(collection, localeOf(o), o?.draft ?? false);
315
163
  const build = (parentId) => rows.filter((r) => r.record.parentId === parentId).map((r) => ({ ...r.item, children: build(r.record.id) }));
316
164
  return build(null);
317
165
  },
318
166
  async getGlobal(item, o) {
319
167
  const locale = localeOf(o);
320
- const draft = o?.draft ?? false;
321
- const { entries } = options.stores();
322
- const [record] = await entries.list(item.name);
323
- if (!record)
324
- return null;
325
- const row = record.locales[locale];
326
- if (!row || (!draft && row.status !== "published"))
327
- return null;
328
- const raw = draft
329
- ? entryFieldsIn(record, locale)
330
- : (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
331
- if (!raw)
332
- return null;
333
- const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
334
- if (!item.section)
335
- return { locale, fields };
336
- const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
337
- const fetch = assetFetcher();
338
- const own = settings !== null && typeof settings === "object" ? settings : {};
339
- if (fetch)
340
- await resolveMedia(own, sectionSettings(item.section), fetch, elements);
341
- // The layout node id, the css and the values ride the styles under the reserved key (spec 2026-09-13 §1, 2026-09-14 §2, §4):
342
- // the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
343
- const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
344
- return {
345
- locale,
346
- fields: rest,
347
- settings: own,
348
- styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
349
- };
168
+ if (o?.draft)
169
+ return assembler.global(item, locale, { draft: true });
170
+ // The item's row by its collection, in one read: a global is a one-record collection.
171
+ const [row] = await published().entries(item.name, { locale, limit: 1 });
172
+ return row && fresh(row) ? row.document : projector.healGlobal(item, locale, { seen: row !== undefined });
350
173
  },
351
174
  };
175
+ /** The published read of an address; `code`: the code-owned page answers where the index has no row. */
176
+ /** `globals`: the names of global items to bring along (schema v9); `rows` is what the route carried, when it had any. */
177
+ async function delivered(locale, path, code, globals) {
178
+ const route = await published().byAddress(locale, path, globals);
179
+ const rows = route?.globals;
180
+ if (route && fresh(route.row))
181
+ return { hit: route.row.document, rows };
182
+ let healed = null;
183
+ if (route) {
184
+ // Stale: rebuilt by id. Unpublished or moved since the row was written, the address is answered as a miss.
185
+ healed = route.kind === "page" ? await projector.healPage(route.row.id, locale) : served(await projector.healEntry(route.row.id, locale));
186
+ if (healed && healed.path === path)
187
+ return { hit: healed, rows };
188
+ }
189
+ // A miss: the paths index doubles as the existence check — a record to assemble, a redirect row, or nothing.
190
+ // A record wins over the code-owned page at its address, as the renderer has it.
191
+ const hit = await site.locate(locale, path);
192
+ if (!hit)
193
+ return { hit: code ? codePage(locale, path) : null };
194
+ if (hit.redirect)
195
+ return { hit: { kind: "redirect", target: { kind: hit.kind, id: hit.id } } };
196
+ if (route && hit.id === route.row.id)
197
+ return { hit: healed, rows }; // just rebuilt
198
+ // By id: a page the tree hides has a current row the address lookup cannot reach; anything else heals there.
199
+ return { hit: await site.load(hit, locale, path), rows };
200
+ }
352
201
  const codePath = (address) => (address === homePageSlug ? "/" : `/${address}`);
353
202
  function codePage(locale, path) {
354
203
  const spec = codeOwned.find((a) => a.template !== null && a.segments[locale] !== undefined && codePath(a.segments[locale]) === path);
355
204
  if (!spec || !locales.supported.includes(locale))
356
205
  return null;
357
206
  const template = spec.template;
358
- const withSeo = options.pages.some((x) => x.schema.kind === "page" && x.schema.name === template && x.schema.seo === true);
207
+ const withSeo = pageSchemas.some((x) => x.kind === "page" && x.name === template && x.seo === true);
359
208
  return {
360
209
  kind: "page",
361
210
  locale,
@@ -365,7 +214,7 @@ export function createSmoodlySite(options) {
365
214
  tree: { zones: {} },
366
215
  title: spec.title,
367
216
  path,
368
- fields: withDefaults(undefined, pageFields(template)),
217
+ fields: withDefaults(undefined, pageFieldsOf(shape, template)),
369
218
  ancestors: [],
370
219
  children: [],
371
220
  locales: Object.fromEntries(Object.entries(spec.segments).map(([l, address]) => [l, { slug: lastPart(address), path: codePath(address) }])),
@@ -380,9 +229,32 @@ export function createSmoodlySite(options) {
380
229
  custom,
381
230
  locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
382
231
  targetPaths: (target) => options.stores().pages.targetPaths(target),
383
- load(target, locale, path, opts = {}) {
232
+ async load(target, locale, path, opts = {}) {
384
233
  const draft = opts.draft ?? false;
385
- return target.kind === "page" ? loadPage(target.id, locale, path, draft, opts.version) : loadEntry(target.id, locale, path, draft);
234
+ // Draft mode assembles from the editing tables; so does a stored version, which outside draft mode is nothing.
235
+ if (draft || opts.version !== undefined) {
236
+ return target.kind === "page" ? assembler.page(target.id, locale, path, { draft, version: opts.version }) : assembler.entry(target.id, locale, path, { draft });
237
+ }
238
+ return target.kind === "page"
239
+ ? pageOf(await published().page(target.id, locale), target.id, locale)
240
+ : served(await entryOf(await published().entry(target.id, locale), target.id, locale));
241
+ },
242
+ lookup: async (locale, path) => (await delivered(locale, path, true)).hit,
243
+ async lookupWithGlobals(locale, path, items) {
244
+ const { hit, rows } = await delivered(locale, path, true, items.map((i) => i.name));
245
+ const globals = new Map();
246
+ if (rows) {
247
+ for (const item of items) {
248
+ const row = rows.find((r) => r.collection === item.name);
249
+ // No row: unpublished here, or never projected — left out rather than healed on every read, because a
250
+ // heal of an unpublished item writes nothing and the next read would heal again; the layout's own read
251
+ // pays the call where a page renders it. A stale row heals once and is written.
252
+ if (!row)
253
+ continue;
254
+ globals.set(item.name, fresh(row) ? row.document : await projector.healGlobal(item, locale, { seen: true }));
255
+ }
256
+ }
257
+ return { hit, globals };
386
258
  },
387
259
  async resolve(locale, path, opts) {
388
260
  // The content API stays strict (§7): a content address is not a redirect.
@@ -390,6 +262,7 @@ export function createSmoodlySite(options) {
390
262
  return target && !target.redirect ? site.load(target, locale, path, opts) : null;
391
263
  },
392
264
  codePage,
265
+ projector,
393
266
  ...api,
394
267
  };
395
268
  return site;
@@ -0,0 +1,9 @@
1
+ import type { StyleValues } from "./styles.ts";
2
+ export declare const SMD_KEY = "__smd";
3
+ export type SmdInternal = {
4
+ node?: string;
5
+ css?: string;
6
+ values?: StyleValues;
7
+ /** Pre-scoped layout CSS (the custom section, spec 2026-09-14 §6), emitted before the editor's css so the box wins. */
8
+ layoutCss?: string;
9
+ };
@@ -0,0 +1 @@
1
+ export const SMD_KEY = "__smd";
@@ -6,6 +6,10 @@ export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
6
6
  export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
7
7
  export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
8
8
  export type ContentTable = (typeof CONTENT_TABLES)[number];
9
+ /** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
10
+ * space-stamped like content, but not transferred (export skips them, import
11
+ * rebuilds them) and not counted toward the row cap, since they are derived. */
12
+ export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries"];
9
13
  export declare function currentSpaceSQL(): string;
10
14
  /** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
11
15
  * the write key's insert, update and delete, each alone for its action; its
package/dist/sql-space.js CHANGED
@@ -19,6 +19,10 @@ export const CONTENT_TABLES = [
19
19
  "paths",
20
20
  "assets",
21
21
  ];
22
+ /** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
23
+ * space-stamped like content, but not transferred (export skips them, import
24
+ * rebuilds them) and not counted toward the row cap, since they are derived. */
25
+ export const DELIVERY_TABLES = ["published_pages", "published_entries"];
22
26
  export function currentSpaceSQL() {
23
27
  return `
24
28
  -- The space a request acts in: the key's claim in Smoodly Cloud, the
@@ -55,7 +59,7 @@ const WRITE = `${OWN} and (select "smoodly_key_kind"()) = 'write'`;
55
59
  * policy, which is also their only select. */
56
60
  export function contentPoliciesSQL() {
57
61
  const statements = [];
58
- for (const table of [...CONTENT_TABLES, "members"]) {
62
+ for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
59
63
  if (table === "refs" || table === "members") {
60
64
  statements.push(`drop policy if exists "smoodly_write" on "${table}";\n` +
61
65
  `create policy "smoodly_write" on "${table}" for all\n` +
@@ -190,7 +194,7 @@ $$;`.trim());
190
194
  statements.push(`create index if not exists "${table}_space_idx" on "${table}" ("space_id");`);
191
195
  }
192
196
  // Trigger names sort a_ before b_: same-event triggers fire alphabetically.
193
- for (const table of [...CONTENT_TABLES, "members"]) {
197
+ for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
194
198
  statements.push(`drop trigger if exists "smoodly_a_set_space" on "${table}";\n` +
195
199
  `create trigger "smoodly_a_set_space" before insert on "${table}"\n` +
196
200
  ` for each row execute function "smoodly_set_space"();`);
@@ -270,9 +274,10 @@ export function mediaPoliciesSQL() {
270
274
  * public. `smoodly_current_space` is NOT here — the stores read it through
271
275
  * rpc(). Last in the schema, after every definition exists. */
272
276
  export function grantsSQL() {
273
- // smoodly_list_entries is callable by every role on purpose: invoker
274
- // rights and the read policies decide what a key sees (spec 2026-09-29
275
- // content API contract §5), so there is nothing to revoke.
277
+ // smoodly_list_entries and smoodly_list_published are callable by every
278
+ // role on purpose: invoker rights and the read policies decide what a key
279
+ // sees (spec 2026-09-29 content API contract §5), so there is nothing to
280
+ // revoke; the published_routes view is security_invoker for the same reason.
276
281
  return [
277
282
  `revoke execute on function "smoodly_reorder"(text, uuid[]) from public, anon;`,
278
283
  `revoke execute on function "smoodly_reorder_pages"(uuid[]) from public, anon;`,
package/dist/sql.d.ts CHANGED
@@ -10,4 +10,6 @@ export declare function foreignKeyIndexesSQL(): string;
10
10
  * §5). Invoker rights, so RLS decides what a key sees. Defined after the
11
11
  * space layer, whose `smoodly_key_kind` it calls. */
12
12
  export declare function listEntriesSQL(): string;
13
+ /** The delivery model (spec 2026-10-07 §1): two derived tables, a list function over the raw snapshot, a routes view. */
14
+ export declare function publishedTablesSQL(): string;
13
15
  export declare function coreTablesSQL(): string;