@jini-ai/cms 0.5.2 → 0.5.3

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 (205) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +14 -8
  3. package/dist/comments/errors.d.ts +23 -0
  4. package/dist/comments/errors.js +25 -0
  5. package/dist/comments/express/index.d.ts +2 -0
  6. package/dist/comments/express/index.js +2 -0
  7. package/dist/comments/express/submit-handler.d.ts +30 -0
  8. package/dist/comments/express/submit-handler.js +85 -0
  9. package/dist/comments/hooks.d.ts +39 -0
  10. package/dist/comments/hooks.js +41 -0
  11. package/dist/comments/index.d.ts +11 -0
  12. package/dist/comments/index.js +11 -0
  13. package/dist/comments/ingress.d.ts +57 -0
  14. package/dist/comments/ingress.js +173 -0
  15. package/dist/comments/module.d.ts +94 -0
  16. package/dist/comments/module.js +73 -0
  17. package/dist/comments/ports.d.ts +184 -0
  18. package/dist/comments/ports.js +2 -0
  19. package/dist/comments/repo.memory.d.ts +70 -0
  20. package/dist/comments/repo.memory.js +120 -0
  21. package/dist/comments/sanitize.d.ts +15 -0
  22. package/dist/comments/sanitize.js +31 -0
  23. package/dist/comments/spam.heuristic.d.ts +8 -0
  24. package/dist/comments/spam.heuristic.js +54 -0
  25. package/dist/comments/sql/index.d.ts +3 -0
  26. package/dist/comments/sql/index.js +3 -0
  27. package/dist/comments/sql/repo.d.ts +77 -0
  28. package/dist/comments/sql/repo.js +190 -0
  29. package/dist/comments/sql/repo.rows.d.ts +49 -0
  30. package/dist/comments/sql/repo.rows.js +71 -0
  31. package/dist/comments/types.d.ts +187 -0
  32. package/dist/comments/types.js +39 -0
  33. package/dist/comments/write-service.d.ts +121 -0
  34. package/dist/comments/write-service.js +144 -0
  35. package/dist/core/commands/command.d.ts +2 -9
  36. package/dist/core/commands/command.js +2 -9
  37. package/dist/core/index.d.ts +2 -1
  38. package/dist/core/index.js +1 -0
  39. package/dist/core/keyset-cursor.d.ts +25 -0
  40. package/dist/core/keyset-cursor.js +20 -0
  41. package/dist/core/ports.d.ts +30 -0
  42. package/dist/core/ports.js +0 -1
  43. package/dist/core/tools/args.d.ts +1 -46
  44. package/dist/core/tools/args.js +1 -44
  45. package/dist/media/index.d.ts +0 -3
  46. package/dist/media/index.js +1 -4
  47. package/dist/media/node.d.ts +5 -0
  48. package/dist/media/node.js +6 -0
  49. package/dist/redirects/capture.d.ts +80 -0
  50. package/dist/redirects/capture.js +66 -0
  51. package/dist/redirects/express/index.d.ts +27 -0
  52. package/dist/redirects/express/index.js +40 -0
  53. package/dist/redirects/hit-sink.d.ts +56 -0
  54. package/dist/redirects/hit-sink.js +44 -0
  55. package/dist/redirects/host-config/index.d.ts +40 -0
  56. package/dist/redirects/host-config/index.js +145 -0
  57. package/dist/redirects/index.d.ts +14 -0
  58. package/dist/redirects/index.js +14 -0
  59. package/dist/redirects/matcher.d.ts +58 -0
  60. package/dist/redirects/matcher.js +144 -0
  61. package/dist/redirects/order.d.ts +30 -0
  62. package/dist/redirects/order.js +26 -0
  63. package/dist/redirects/ports.d.ts +242 -0
  64. package/dist/redirects/ports.internal.d.ts +57 -0
  65. package/dist/redirects/ports.internal.js +14 -0
  66. package/dist/redirects/ports.js +2 -0
  67. package/dist/redirects/redirects.d.ts +226 -0
  68. package/dist/redirects/redirects.js +590 -0
  69. package/dist/redirects/referrer-alias.d.ts +31 -0
  70. package/dist/redirects/referrer-alias.js +33 -0
  71. package/dist/redirects/repo.memory.d.ts +51 -0
  72. package/dist/redirects/repo.memory.js +98 -0
  73. package/dist/redirects/reserved-destination.d.ts +58 -0
  74. package/dist/redirects/reserved-destination.js +59 -0
  75. package/dist/redirects/resolver.d.ts +100 -0
  76. package/dist/redirects/resolver.js +87 -0
  77. package/dist/redirects/spa/index.d.ts +30 -0
  78. package/dist/redirects/spa/index.js +27 -0
  79. package/dist/redirects/sql/index.d.ts +4 -0
  80. package/dist/redirects/sql/index.js +4 -0
  81. package/dist/redirects/sql/repo.d.ts +82 -0
  82. package/dist/redirects/sql/repo.js +143 -0
  83. package/dist/redirects/sql/repo.rows.d.ts +49 -0
  84. package/dist/redirects/sql/repo.rows.js +79 -0
  85. package/dist/redirects/sql/row-types.d.ts +34 -0
  86. package/dist/redirects/sql/row-types.js +2 -0
  87. package/dist/redirects/target-gate.d.ts +30 -0
  88. package/dist/redirects/target-gate.js +85 -0
  89. package/dist/redirects/types.d.ts +213 -0
  90. package/dist/redirects/types.js +42 -0
  91. package/dist/seo/absolute-url.d.ts +58 -0
  92. package/dist/seo/absolute-url.js +117 -0
  93. package/dist/seo/dom/index.d.ts +14 -0
  94. package/dist/seo/dom/index.js +52 -0
  95. package/dist/seo/errors.d.ts +35 -0
  96. package/dist/seo/errors.js +35 -0
  97. package/dist/seo/feed.d.ts +49 -0
  98. package/dist/seo/feed.js +61 -0
  99. package/dist/seo/index.d.ts +13 -0
  100. package/dist/seo/index.js +13 -0
  101. package/dist/seo/media.d.ts +57 -0
  102. package/dist/seo/media.js +134 -0
  103. package/dist/seo/page-head.d.ts +18 -0
  104. package/dist/seo/page-head.js +114 -0
  105. package/dist/seo/ports.d.ts +221 -0
  106. package/dist/seo/ports.js +27 -0
  107. package/dist/seo/seo.d.ts +73 -0
  108. package/dist/seo/seo.js +230 -0
  109. package/dist/seo/sitemap.d.ts +139 -0
  110. package/dist/seo/sitemap.js +217 -0
  111. package/dist/seo/types.d.ts +278 -0
  112. package/dist/seo/types.js +2 -0
  113. package/dist/seo/write-service.d.ts +79 -0
  114. package/dist/seo/write-service.js +273 -0
  115. package/dist/seo/xml-escape.d.ts +16 -0
  116. package/dist/seo/xml-escape.js +19 -0
  117. package/dist/settings/express/cms-adapter.d.ts +20 -0
  118. package/dist/settings/express/cms-adapter.js +124 -0
  119. package/dist/settings/express/contracts.d.ts +135 -0
  120. package/dist/settings/express/contracts.js +2 -0
  121. package/dist/settings/express/http.d.ts +10 -0
  122. package/dist/settings/express/http.js +211 -0
  123. package/dist/settings/express/index.d.ts +5 -0
  124. package/dist/settings/express/index.js +5 -0
  125. package/dist/settings/express/shared.d.ts +68 -0
  126. package/dist/settings/express/shared.js +54 -0
  127. package/dist/settings/express/stream.d.ts +24 -0
  128. package/dist/settings/express/stream.js +147 -0
  129. package/dist/settings/index.d.ts +3 -38
  130. package/dist/settings/index.js +3 -35
  131. package/dist/taxonomy/write-service.d.ts +2 -30
  132. package/dist/widgets/authorize-helper.d.ts +42 -0
  133. package/dist/widgets/authorize-helper.js +16 -0
  134. package/dist/widgets/concurrency.d.ts +12 -0
  135. package/dist/widgets/concurrency.js +44 -0
  136. package/dist/widgets/config-validation.d.ts +34 -0
  137. package/dist/widgets/config-validation.js +125 -0
  138. package/dist/widgets/embed-service.d.ts +133 -0
  139. package/dist/widgets/embed-service.js +460 -0
  140. package/dist/widgets/embed-validation.d.ts +23 -0
  141. package/dist/widgets/embed-validation.js +38 -0
  142. package/dist/widgets/entry-payload.d.ts +103 -0
  143. package/dist/widgets/entry-payload.js +160 -0
  144. package/dist/widgets/errors.d.ts +136 -0
  145. package/dist/widgets/errors.js +136 -0
  146. package/dist/widgets/host-ports.d.ts +149 -0
  147. package/dist/widgets/host-ports.js +2 -0
  148. package/dist/widgets/html/html-embeds.d.ts +247 -0
  149. package/dist/widgets/html/html-embeds.js +408 -0
  150. package/dist/widgets/html/index.d.ts +5 -0
  151. package/dist/widgets/html/index.js +4 -0
  152. package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
  153. package/dist/widgets/html/legacy-widget-markers.js +45 -0
  154. package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
  155. package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
  156. package/dist/widgets/index.d.ts +20 -0
  157. package/dist/widgets/index.js +19 -0
  158. package/dist/widgets/markers/html-attributes.d.ts +98 -0
  159. package/dist/widgets/markers/html-attributes.js +216 -0
  160. package/dist/widgets/markers/index.d.ts +3 -0
  161. package/dist/widgets/markers/index.js +3 -0
  162. package/dist/widgets/markers/marker.d.ts +496 -0
  163. package/dist/widgets/markers/marker.js +780 -0
  164. package/dist/widgets/page-embed-types.d.ts +42 -0
  165. package/dist/widgets/page-embed-types.js +42 -0
  166. package/dist/widgets/ports.d.ts +142 -0
  167. package/dist/widgets/ports.js +2 -0
  168. package/dist/widgets/read-service.d.ts +81 -0
  169. package/dist/widgets/read-service.js +79 -0
  170. package/dist/widgets/region-area-service.d.ts +110 -0
  171. package/dist/widgets/region-area-service.js +180 -0
  172. package/dist/widgets/registry.d.ts +217 -0
  173. package/dist/widgets/registry.js +172 -0
  174. package/dist/widgets/repo.memory.d.ts +41 -0
  175. package/dist/widgets/repo.memory.js +48 -0
  176. package/dist/widgets/resolver-service.d.ts +209 -0
  177. package/dist/widgets/resolver-service.js +909 -0
  178. package/dist/widgets/resolvers/contact-form.d.ts +26 -0
  179. package/dist/widgets/resolvers/contact-form.js +54 -0
  180. package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
  181. package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
  182. package/dist/widgets/resolvers/index.d.ts +65 -0
  183. package/dist/widgets/resolvers/index.js +155 -0
  184. package/dist/widgets/resolvers/menu.d.ts +42 -0
  185. package/dist/widgets/resolvers/menu.js +43 -0
  186. package/dist/widgets/resolvers/ports.d.ts +82 -0
  187. package/dist/widgets/resolvers/ports.js +2 -0
  188. package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
  189. package/dist/widgets/resolvers/recent-entries.js +165 -0
  190. package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
  191. package/dist/widgets/resolvers/taxonomy.js +34 -0
  192. package/dist/widgets/sql/index.d.ts +3 -0
  193. package/dist/widgets/sql/index.js +3 -0
  194. package/dist/widgets/sql/repo.d.ts +41 -0
  195. package/dist/widgets/sql/repo.js +82 -0
  196. package/dist/widgets/sql/tables.d.ts +13 -0
  197. package/dist/widgets/sql/tables.js +2 -0
  198. package/dist/widgets/types.d.ts +213 -0
  199. package/dist/widgets/types.js +17 -0
  200. package/dist/widgets/where-used.d.ts +41 -0
  201. package/dist/widgets/where-used.js +11 -0
  202. package/dist/widgets/write-service.d.ts +204 -0
  203. package/dist/widgets/write-service.js +332 -0
  204. package/dist/workspace/delete.d.ts +1 -1
  205. package/package.json +220 -11
@@ -0,0 +1,165 @@
1
+ import { getWidgetTypeRegistration } from "../registry.js";
2
+ const NO_DISPLAY_FIELDS = [];
3
+ /** Prefers `publishedAt`; falls back to `updatedAt` for the rare row with no `publishedAt` yet
4
+ * (draft rows never reach either path — both queries filter to `status: 'published'` — but a
5
+ * defensive fallback costs nothing). @complexity O(1). */
6
+ function entryDateIso(entry) {
7
+ return entry.publishedAt ?? entry.updatedAt;
8
+ }
9
+ /** Pure display formatting for an entry's date (e.g. "Sep 3, 2026") — the same short en-US
10
+ * convention `pages.ts`'s `formatPostPreviewDate` already established for post-previews/collection
11
+ * cards. Duplicated rather than imported: that function is file-private and this widgets/route-layer
12
+ * boundary has no barrel re-export for it, the same accepted-duplication precedent `pages.ts`'s own
13
+ * `collectionEntryFieldValue` doc already documents for this exact kind of small formatting helper.
14
+ * A malformed timestamp degrades to the raw ISO string rather than "Invalid Date".
15
+ * @complexity O(1). */
16
+ function formatEntryDate(iso) {
17
+ const parsed = new Date(iso);
18
+ if (Number.isNaN(parsed.getTime()))
19
+ return iso;
20
+ return parsed.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
21
+ }
22
+ /** Legacy (no-`collection`) path: no single content type is in scope, so there is no field list to
23
+ * display — matches today's rendering exactly (title only). @complexity O(1). */
24
+ function toLegacyItemProps(entry, collectionList) {
25
+ const dateIso = entryDateIso(entry);
26
+ return { title: entry.title, href: collectionList.entryPublicHref(entry.type, entry.slug), dateIso, dateLabel: formatEntryDate(dateIso), fields: NO_DISPLAY_FIELDS };
27
+ }
28
+ /** Route-layer twin of `repo.sqlite.ts`'s `json_extract($.ext.site.<name>)` / `trash-aware-memory-
29
+ * repo.ts`'s own private `siteFieldValue` — duplicated for the same module-boundary reason
30
+ * `pages.ts`'s `collectionEntryFieldValue` already documents (no barrel re-export of either
31
+ * internal helper). @complexity O(1). */
32
+ function entryFieldValue(entry, name) {
33
+ const fields = entry.fieldsJson;
34
+ return fields?.ext?.site?.[name];
35
+ }
36
+ /** `collection`-path item: the content type's resolved `display.fields` (already validated/ordered
37
+ * by `parseCollectionListConfig`) become this entry's shown field values.
38
+ * @complexity O(f) over the displayed field count. */
39
+ function toCollectionItemProps(entry, fields, collectionList) {
40
+ const dateIso = entryDateIso(entry);
41
+ return {
42
+ title: entry.title,
43
+ href: collectionList.entryPublicHref(entry.type, entry.slug),
44
+ dateIso,
45
+ dateLabel: formatEntryDate(dateIso),
46
+ fields: fields.map((f) => ({ name: f.name, label: collectionList.humanizeFieldName(f.name), kind: f.kind, value: entryFieldValue(entry, f.name) })),
47
+ };
48
+ }
49
+ const LEGACY_DEFAULT_LAYOUT = "list";
50
+ const LEGACY_MIN_COLUMNS = 1;
51
+ const LEGACY_MAX_COLUMNS = 6;
52
+ const LEGACY_DEFAULT_COLUMNS = 3;
53
+ /** Display-only, unvalidated-against-any-content-type knobs — new, additive keys an old config never
54
+ * set, so absence falls back to the pre-existing visual shape (D7). `widgetLayout` applies on BOTH
55
+ * paths: the widget's layout default is "list" even with `collection` set (D7), unlike the
56
+ * collection marker's own "cards" default that `parseCollectionListConfig` applies.
57
+ * @complexity O(1). */
58
+ function widgetLayout(config, collectionList) {
59
+ return collectionList.isCollectionListLayout(config.layout) ? config.layout : LEGACY_DEFAULT_LAYOUT;
60
+ }
61
+ function legacyColumns(config) {
62
+ const raw = config.columns;
63
+ if (typeof raw !== "number" || !Number.isFinite(raw))
64
+ return LEGACY_DEFAULT_COLUMNS;
65
+ return Math.min(Math.max(Math.floor(raw), LEGACY_MIN_COLUMNS), LEGACY_MAX_COLUMNS);
66
+ }
67
+ function toItemIr(props) {
68
+ // Cast: RecentEntryItemProps is a plain, JSON-serializable shape with no index signature of its
69
+ // own — same precedent as menu.ts's toMenuItemProps for ResolvedNavItem.
70
+ return { componentId: "entry-list-item", props: props };
71
+ }
72
+ function collectionKey(config) {
73
+ return typeof config.collection === "string" && config.collection.length > 0 ? config.collection : undefined;
74
+ }
75
+ /** Resolves the shared legacy-path batch query once, then slices/maps it per instance — REQ-24's
76
+ * "one query per batch" contract, unchanged by this plan. @complexity O(n) over the shared result
77
+ * per instance, n bounded by `registryMax`. */
78
+ async function resolveLegacyInstances(deps, instances, workspaceId, registryMax, results) {
79
+ if (instances.length === 0)
80
+ return;
81
+ // Review fix 3b: SYSTEM_CONTENT_TYPES is excluded IN the query (via
82
+ // listByWorkspaceExcludingTypes), not filtered out of an already limit-bounded result — a batch
83
+ // of system rows newer than real content used to be able to fill the bounded query and leave
84
+ // nothing (or fewer than asked for) once filtered afterward.
85
+ const visible = await deps.entryList.listByWorkspaceExcludingTypes({
86
+ workspaceId,
87
+ excludeTypes: deps.collectionList.systemContentTypes,
88
+ status: "published",
89
+ orderBy: "updatedAt",
90
+ orderDirection: "desc",
91
+ limit: registryMax,
92
+ });
93
+ for (const instance of instances) {
94
+ const configuredMax = typeof instance.config.maxItems === "number" ? instance.config.maxItems : registryMax;
95
+ const max = Math.max(0, Math.min(configuredMax, registryMax));
96
+ const entries = visible.slice(0, max);
97
+ results.set(instance.id, {
98
+ ok: true,
99
+ ir: {
100
+ componentId: "recent-entries",
101
+ props: { layout: widgetLayout(instance.config, deps.collectionList), columns: legacyColumns(instance.config), typeKey: "recent-entries" },
102
+ children: entries.map((entry) => toItemIr(toLegacyItemProps(entry, deps.collectionList))),
103
+ },
104
+ dependencyKeys: entries.map((entry) => entry.id),
105
+ });
106
+ }
107
+ }
108
+ /** Resolves one `collection`-configured instance independently — each may name a different content
109
+ * type/filter/sort, so (unlike the legacy path) there is no single shared query to batch across
110
+ * instances. @complexity One `contentTypes.findByKey` plus, only once that and the config both
111
+ * resolve, one bounded `listPublishedForDisplay` query (never an unbounded scan). */
112
+ async function resolveCollectionInstance(deps, instance, workspaceId, registryMax, typeKey) {
113
+ const contentType = await deps.contentTypes.findByKey({ workspaceId, key: typeKey });
114
+ if (contentType === null) {
115
+ return { ok: false, reason: "target-disabled" };
116
+ }
117
+ const parsed = deps.collectionList.parseCollectionListConfig(instance.config, contentType, { limitKey: "maxItems", defaultSort: "updated" });
118
+ if (!parsed.ok) {
119
+ return { ok: false, reason: "invalid-config" };
120
+ }
121
+ // REQ-25 defense in depth: the parser's own ceiling (24) is wider than this registration's (20).
122
+ const boundedQuery = { ...parsed.query, limit: Math.min(parsed.query.limit, registryMax) };
123
+ const rows = await deps.entryList.listPublishedForDisplay({ workspaceId, query: boundedQuery });
124
+ return {
125
+ ok: true,
126
+ ir: {
127
+ componentId: "recent-entries",
128
+ // D7: the widget's own layout default is "list" — `parsed.display.layout` carries the
129
+ // collection MARKER's "cards" default for an absent key, so it is not used here.
130
+ // `structuredData` only here, never on the legacy path: that one lists mixed types, so it can
131
+ // never truthfully claim to be an FAQ (AW-7).
132
+ props: {
133
+ layout: widgetLayout(instance.config, deps.collectionList),
134
+ columns: parsed.display.columns,
135
+ typeKey,
136
+ ...(parsed.display.structuredData === undefined ? {} : { structuredData: parsed.display.structuredData }),
137
+ },
138
+ children: rows.map((row) => toItemIr(toCollectionItemProps(row, parsed.display.fields, deps.collectionList))),
139
+ },
140
+ dependencyKeys: rows.map((row) => row.id),
141
+ };
142
+ }
143
+ export function createRecentEntriesResolver(deps, _optional = {}) {
144
+ return {
145
+ async resolveMany(instances, context) {
146
+ // `"recent-entries"` is a literal, so `getWidgetTypeRegistration` returns THIS registration's
147
+ // own literal `clamps.maxItems: number` here, never the shared interface's optional form.
148
+ const registryMax = getWidgetTypeRegistration({ typeKey: "recent-entries" }).clamps.maxItems;
149
+ const results = new Map();
150
+ const legacyInstances = instances.filter((instance) => collectionKey(instance.config) === undefined);
151
+ const collectionInstances = instances.filter((instance) => collectionKey(instance.config) !== undefined);
152
+ await resolveLegacyInstances(deps, legacyInstances, context.workspaceId, registryMax, results);
153
+ // Each collection-configured instance may name a different content type/filter/sort — unlike
154
+ // the legacy batch above, there is no single shared query, so these resolve independently and
155
+ // concurrently (never one query per instance run serially).
156
+ await Promise.all(collectionInstances.map(async (instance) => {
157
+ // Safe: filtered by `collectionKey(instance.config) !== undefined` above.
158
+ const typeKey = collectionKey(instance.config);
159
+ results.set(instance.id, await resolveCollectionInstance(deps, instance, context.workspaceId, registryMax, typeKey));
160
+ }));
161
+ return results;
162
+ },
163
+ };
164
+ }
165
+ //# sourceMappingURL=recent-entries.js.map
@@ -0,0 +1,12 @@
1
+ import type { TaxonomyListPort, TermListPort } from "../../taxonomy/index.js";
2
+ import type { PageHtmlEmbedRef } from "../html/html-embeds.js";
3
+ import type { WidgetRenderIR } from "../types.js";
4
+ /** Workspace-scoped read ports are supplied by the host, like the other HTML embed resolvers. */
5
+ export declare function resolveTaxonomyEmbeds({ refs, deps }: {
6
+ refs: readonly PageHtmlEmbedRef[];
7
+ deps: {
8
+ taxonomyRepo?: TaxonomyListPort;
9
+ termRepo?: TermListPort;
10
+ };
11
+ }, _optional?: {}): Promise<ReadonlyMap<string, WidgetRenderIR>>;
12
+ //# sourceMappingURL=taxonomy.d.ts.map
@@ -0,0 +1,34 @@
1
+ /** Workspace-scoped read ports are supplied by the host, like the other HTML embed resolvers. */
2
+ export async function resolveTaxonomyEmbeds({ refs, deps }, _optional = {}) {
3
+ const resolved = new Map();
4
+ if (!deps.taxonomyRepo || !deps.termRepo)
5
+ return resolved;
6
+ const taxonomies = (await deps.taxonomyRepo.list()).filter((taxonomy) => taxonomy.status !== "trash" && taxonomy.status !== "purged");
7
+ const termsById = new Map();
8
+ for (const ref of refs) {
9
+ const key = ref.id;
10
+ if (!key)
11
+ continue;
12
+ // Names are human-readable targets, but unlike slugs are not guaranteed unique.
13
+ // An ambiguous name is unresolved, never an arbitrary choice; real ids win.
14
+ const matches = taxonomies.filter((taxonomy) => taxonomy.name === key);
15
+ const taxonomy = taxonomies.find((candidate) => candidate.id === key) ?? (matches.length === 1 ? matches[0] : undefined);
16
+ if (!taxonomy)
17
+ continue;
18
+ let terms = termsById.get(taxonomy.id);
19
+ if (!terms) {
20
+ terms = await deps.termRepo.listByTaxonomy({ taxonomyId: taxonomy.id });
21
+ termsById.set(taxonomy.id, terms);
22
+ }
23
+ resolved.set(key, {
24
+ componentId: "taxonomy-list",
25
+ props: {
26
+ id: taxonomy.id, name: taxonomy.name, hierarchical: taxonomy.hierarchical,
27
+ terms: terms.filter((term) => term.taxonomyId === taxonomy.id && term.status !== "trash" && term.status !== "purged")
28
+ .map((term) => ({ id: term.id, name: term.name, parentId: term.parentId })),
29
+ },
30
+ });
31
+ }
32
+ return resolved;
33
+ }
34
+ //# sourceMappingURL=taxonomy.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./tables.js";
2
+ export * from "./repo.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./tables.js";
2
+ export * from "./repo.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,41 @@
1
+ import type { StorageKernel } from "@jini-ai/db/kernel";
2
+ import type { WidgetsDatabase, WidgetsTables } from "./tables.js";
3
+ import type { UUID } from "@jini-ai/core/primitives";
4
+ import type { WidgetRegionBindingRepoPort } from "../ports.js";
5
+ import type { WidgetRegionBindingRow, WidgetRegionKey } from "../types.js";
6
+ export declare class SqlWidgetRegionBindingRepo implements WidgetRegionBindingRepoPort {
7
+ protected readonly kernel: StorageKernel<WidgetsDatabase>;
8
+ protected readonly tables: WidgetsTables;
9
+ constructor({ kernel, tables }: {
10
+ kernel: StorageKernel<WidgetsDatabase>;
11
+ tables: WidgetsTables;
12
+ }, _optional?: Record<string, never>);
13
+ findByRegion(required: {
14
+ workspaceId: UUID;
15
+ regionKey: WidgetRegionKey;
16
+ }): Promise<WidgetRegionBindingRow | null>;
17
+ listByWorkspace(required: {
18
+ workspaceId: UUID;
19
+ }): Promise<WidgetRegionBindingRow[]>;
20
+ upsert(required: {
21
+ workspaceId: UUID;
22
+ regionKey: WidgetRegionKey;
23
+ areaEntryId: UUID;
24
+ updatedAt: string;
25
+ }): Promise<WidgetRegionBindingRow>;
26
+ markInactive(required: {
27
+ workspaceId: UUID;
28
+ regionKey: WidgetRegionKey;
29
+ }): Promise<void>;
30
+ /** Delete-then-insert in one kernel transaction, so a reader never sees the workspace half rebuilt. */
31
+ rebuildForWorkspace(required: {
32
+ workspaceId: UUID;
33
+ bindings: readonly WidgetRegionBindingRow[];
34
+ }): Promise<void>;
35
+ }
36
+ /** Borrow the host kernel through this repository's narrow SQL schema view; no connection work. */
37
+ export declare function widgetRegionBindingRepoFor<DB>({ kernel, tables }: {
38
+ kernel: StorageKernel<DB>;
39
+ tables: WidgetsTables;
40
+ }, _optional?: Record<string, never>): SqlWidgetRegionBindingRepo;
41
+ //# sourceMappingURL=repo.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file THE durable `WidgetRegionBindingRepoPort`: one Kysely query body for every dialect the
3
+ * storage kernel drives (storage plan §4, ADR-066). ADR-006 rule-of-two "second adapter" half —
4
+ * `repo.memory.ts`'s `InMemoryWidgetRegionBindingRepo` is the first; `repo.sqlite.ts` is the thin
5
+ * subclass the composition root builds from the content db handle. `upsert` targets the composite
6
+ * `UNIQUE(workspace_id, region_key)` index (a real strengthening of INV-02 over the in-memory
7
+ * adapter's single-process-only guarantee).
8
+ *
9
+ * Architectural role:
10
+ * Infrastructure adapter. No feature logic — `region-area-service.ts` owns every write decision;
11
+ * this class only persists what it's told.
12
+ */
13
+ function toRecord(row) {
14
+ return {
15
+ workspaceId: row.workspace_id,
16
+ regionKey: row.region_key,
17
+ areaEntryId: row.area_entry_id,
18
+ updatedAt: row.updated_at,
19
+ };
20
+ }
21
+ function toRow(binding) {
22
+ return {
23
+ workspace_id: binding.workspaceId,
24
+ region_key: binding.regionKey,
25
+ area_entry_id: binding.areaEntryId,
26
+ updated_at: binding.updatedAt,
27
+ };
28
+ }
29
+ export class SqlWidgetRegionBindingRepo {
30
+ kernel;
31
+ tables;
32
+ constructor({ kernel, tables }, _optional = {}) { this.kernel = kernel; this.tables = tables; }
33
+ async findByRegion(required) {
34
+ const row = await this.kernel.run((db) => db
35
+ .selectFrom(this.tables.regionBindings)
36
+ .selectAll()
37
+ .where("workspace_id", "=", required.workspaceId)
38
+ .where("region_key", "=", required.regionKey)
39
+ .limit(1)
40
+ .executeTakeFirst());
41
+ return row ? toRecord(row) : null;
42
+ }
43
+ async listByWorkspace(required) {
44
+ const rows = await this.kernel.run((db) => db.selectFrom(this.tables.regionBindings).selectAll().where("workspace_id", "=", required.workspaceId).execute());
45
+ return rows.map(toRecord);
46
+ }
47
+ async upsert(required) {
48
+ const row = {
49
+ workspaceId: required.workspaceId,
50
+ regionKey: required.regionKey,
51
+ areaEntryId: required.areaEntryId,
52
+ updatedAt: required.updatedAt,
53
+ };
54
+ await this.kernel.run((db) => db
55
+ .insertInto(this.tables.regionBindings)
56
+ .values(toRow(row))
57
+ .onConflict((oc) => oc.columns(["workspace_id", "region_key"]).doUpdateSet({ area_entry_id: row.areaEntryId, updated_at: row.updatedAt }))
58
+ .execute());
59
+ return row;
60
+ }
61
+ async markInactive(required) {
62
+ await this.kernel.run((db) => db
63
+ .deleteFrom(this.tables.regionBindings)
64
+ .where("workspace_id", "=", required.workspaceId)
65
+ .where("region_key", "=", required.regionKey)
66
+ .execute());
67
+ }
68
+ /** Delete-then-insert in one kernel transaction, so a reader never sees the workspace half rebuilt. */
69
+ async rebuildForWorkspace(required) {
70
+ await this.kernel.transaction(async () => {
71
+ await this.kernel.run((db) => db.deleteFrom(this.tables.regionBindings).where("workspace_id", "=", required.workspaceId).execute());
72
+ if (required.bindings.length === 0)
73
+ return;
74
+ await this.kernel.run((db) => db.insertInto(this.tables.regionBindings).values(required.bindings.map(toRow)).execute());
75
+ });
76
+ }
77
+ }
78
+ /** Borrow the host kernel through this repository's narrow SQL schema view; no connection work. */
79
+ export function widgetRegionBindingRepoFor({ kernel, tables }, _optional = {}) {
80
+ return new SqlWidgetRegionBindingRepo({ kernel: kernel, tables });
81
+ }
82
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1,13 @@
1
+ /** Region-binding table names are a required host choice. */
2
+ export interface WidgetsTables {
3
+ regionBindings: string;
4
+ }
5
+ /** Local narrow SQL row; widgets never imports a host-generated database schema. */
6
+ export interface WidgetRegionBindingsTable {
7
+ workspace_id: string;
8
+ region_key: string;
9
+ area_entry_id: string;
10
+ updated_at: string;
11
+ }
12
+ export type WidgetsDatabase = Record<string, WidgetRegionBindingsTable>;
13
+ //# sourceMappingURL=tables.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @file Core type definitions for the Tovu `widgets` library (ADR-047, SPEC-043).
3
+ *
4
+ * Purpose:
5
+ * Widgets are placeable, reusable, individually-configured components (a contact
6
+ * form, a recent-posts list, a text block, social links, a menu) placed either
7
+ * into a theme-declared region (an ordered list, backed by a seeded
8
+ * `widget_area` entry) or embedded inline inside a page's rich-text body (a
9
+ * `widgetEmbed` TipTap node). A widget instance is a seeded ADR-022 content
10
+ * entry (type `widget`), exactly as a menu is (ADR-029) — this file mirrors
11
+ * `navigation/types.ts` deliberately, per the ADR-047 debate's finding that the
12
+ * region-binding mechanism must structurally match `nav_location_bindings`
13
+ * (source-of-truth-on-the-entry + a derived, reconciled binding index), not
14
+ * merely resemble it.
15
+ *
16
+ * How it relates to the project:
17
+ * - `core/ports.ts` supplies the shared primitives (`UUID`, `JsonObject`, …).
18
+ * - `widgets/ports.ts` declares the one rule-of-two port this library adds
19
+ * (`WidgetRegionBindingRepoPort`, mirroring `NavLocationBindingRepoPort`).
20
+ * - `widgets/registry.ts` declares the v1 widget-type registry as plain data —
21
+ * see that file for why type *registration* and type *behavior* are split.
22
+ * - The theme renderer never resolves a widget reference itself (ADR-020 §7 /
23
+ * ADR-029 §7's "theme receives resolved data" boundary, unchanged here).
24
+ *
25
+ * Architectural role:
26
+ * INTERFACES + TYPES ONLY (no feature logic). This is the design-frozen shape
27
+ * ADR-047 (as amended by its 2026-07-21 debate fold-in) introduces; the entries
28
+ * repo, revisions, and the command gateway are reused unchanged from ADR-022 /
29
+ * ADR-008.
30
+ */
31
+ import type { ISODateTime, JsonObject, UUID } from "@jini-ai/core/primitives";
32
+ /** `widget` ships as a seeded content-type registry row (ADR-022 §1), like `post`/`page`/`menu`. */
33
+ export declare const WIDGET_CONTENT_TYPE: "widget";
34
+ /**
35
+ * `widget_area` ships as a seeded, **system-managed** content-type registry
36
+ * row — never publicly routable, never itself placeable as a widget, never a
37
+ * valid `widgetEmbed` target (REQ-17 — no recursion into a region from inside
38
+ * a region).
39
+ */
40
+ export declare const WIDGET_AREA_CONTENT_TYPE: "widget_area";
41
+ /** The extension-field owner namespace for widget-level fields (`fields.ext.widget.*`). */
42
+ export declare const WIDGET_FIELD_NAMESPACE: "widget";
43
+ /** The extension-field owner namespace for a `widget_area` entry's region assignment. */
44
+ export declare const WIDGET_AREA_FIELD_NAMESPACE: "widgets";
45
+ /**
46
+ * The v1 widget-type keys (SPEC-043 REQ-09). Kept as a closed union rather than
47
+ * a bare `string` so a typo in a registration or a call site is a compile error,
48
+ * not a runtime `unknown-type` surprise.
49
+ */
50
+ export type WidgetTypeKey = "text" | "social-links" | "recent-entries" | "menu" | "contact-form";
51
+ /**
52
+ * A widget type's declared capability class (ADR-047 Debate Fold-In Amendment
53
+ * 3). `static` types render validated config directly with no resolver;
54
+ * `query`/`form`/`entry-reference` types have a registered resolver in the
55
+ * closed `CORE_RESOLVERS` map (`resolvers/index.ts`).
56
+ */
57
+ export type WidgetCapability = "static" | "query" | "form" | "entry-reference";
58
+ /** Where a widget type may legally be placed. */
59
+ export type WidgetPlacementContext = "region" | "inline";
60
+ /**
61
+ * A widget type's **registration** — plain, JSON-serializable data (SPEC-043
62
+ * REQ-07). Never imports or references executable behavior; the only pointer
63
+ * to code this record carries is `resolverId`, which must resolve *only*
64
+ * through the closed `CORE_RESOLVERS` map (REQ-08) — never a module path,
65
+ * function name, query string, or expression.
66
+ */
67
+ export interface WidgetTypeRegistration {
68
+ readonly typeKey: WidgetTypeKey;
69
+ readonly capability: WidgetCapability;
70
+ /** JSON-Schema-shaped description of `fields.ext.widget.*`'s valid shape for this type. */
71
+ readonly configSchema: JsonObject;
72
+ readonly placementContexts: readonly WidgetPlacementContext[];
73
+ /** Cost clamps enforced by core at the orchestration layer, never by the resolver's own discipline (REQ-25). */
74
+ readonly clamps: {
75
+ readonly maxItems?: number;
76
+ readonly timeoutMs: number;
77
+ };
78
+ /** `undefined` for `static`-capability types (REQ-10). Indexes into `CORE_RESOLVERS` only. */
79
+ readonly resolverId?: string;
80
+ }
81
+ /**
82
+ * `purged` is observably distinct from `trash` (REQ-43): both are treated identically everywhere a
83
+ * widget instance's availability is checked (resolution, placement validation — see
84
+ * `resolver-service.ts`/`region-area-service.ts`), but only `purged` records that the instance was
85
+ * force-deleted past a known reference (`purgeWidgetInstance({ force: true })`), vs. an ordinary
86
+ * `trash` that may still be restored with no prior reference conflict. No real hard-delete exists in
87
+ * this codebase for any content type (`EntryRepoPort` has no delete method) — this status is the
88
+ * cheap, in-model way to keep that distinction visible without one.
89
+ *
90
+ * No `draft` member (Fable adversarial-review fix, 2026-07-21, Finding F) — `write-service.ts`
91
+ * creates every widget instance directly as `active`; nothing anywhere in this codebase ever writes
92
+ * `draft`. A prior version of this union carried it anyway, which was a live landmine: the
93
+ * resolver/placement-validation skip lists above only ever checked for `trash`/`purged`, so a
94
+ * `draft` status — had anything ever produced one — would have rendered publicly instead of being
95
+ * held back. Removed rather than defensively added to the skip lists, since there is no real
96
+ * "draft" concept for widget instances to guard.
97
+ */
98
+ export type WidgetInstanceStatus = "active" | "trash" | "purged";
99
+ /**
100
+ * A typed read model over a `type='widget'` entries row (ADR-022 §2 universal
101
+ * columns) plus the parsed `fields.ext.widget.*` config bag. A projection, not
102
+ * a new table — widget instances are entries (REQ-01).
103
+ */
104
+ export interface WidgetInstanceEntry {
105
+ readonly id: UUID;
106
+ readonly workspaceId: UUID;
107
+ readonly slug: string;
108
+ readonly title: string;
109
+ readonly status: WidgetInstanceStatus;
110
+ readonly widgetType: WidgetTypeKey;
111
+ /** Validated against the type's registered `configSchema` on every write (REQ-02). */
112
+ readonly config: JsonObject;
113
+ readonly updatedAt: ISODateTime;
114
+ readonly version: number;
115
+ }
116
+ /** A theme-registered region key (e.g. `header`, `footer`, `sidebar`). */
117
+ export type WidgetRegionKey = string;
118
+ /** One entry in a `widget_area`'s ordered placement list (lives in the area entry's `bodyJson`). */
119
+ export interface WidgetPlacementNode {
120
+ /** Stable ULID, minted once, preserved across reorders — for diagnostics and `entry_refs` locators. */
121
+ readonly placementId: UUID;
122
+ readonly widgetEntryId: UUID;
123
+ readonly enabled: boolean;
124
+ }
125
+ /** The full region-composition document stored in a `widget_area` entry's `bodyJson`. */
126
+ export interface WidgetAreaDoc {
127
+ readonly schemaVersion: number;
128
+ readonly placements: readonly WidgetPlacementNode[];
129
+ }
130
+ /**
131
+ * A typed read model over a `type='widget_area'` entries row. `regionKey` is
132
+ * the **source of truth** for which region this area fills
133
+ * (`fields.ext.widgets.regionKey`) — mirrors `NavMenuEntry.locations` exactly
134
+ * (ADR-047 Debate Fold-In Amendment 1).
135
+ */
136
+ export interface WidgetAreaEntry {
137
+ readonly id: UUID;
138
+ readonly workspaceId: UUID;
139
+ readonly regionKey: WidgetRegionKey;
140
+ readonly doc: WidgetAreaDoc;
141
+ readonly updatedAt: ISODateTime;
142
+ readonly version: number;
143
+ }
144
+ /**
145
+ * A row of the derived `widget_region_bindings` index. **Derived + rebuildable**
146
+ * from `WidgetAreaEntry.regionKey` at the write chokepoint (same species as
147
+ * `nav_location_bindings` and ADR-022 §5 `entry_refs`), NOT a source of truth
148
+ * and NOT revision-generating (INV-02). Reconciled by exactly one code path
149
+ * (`region-area-service.ts`'s `reconcileWidgetRegionBindings`) — never
150
+ * hand-authored.
151
+ */
152
+ export interface WidgetRegionBindingRow {
153
+ readonly workspaceId: UUID;
154
+ readonly regionKey: WidgetRegionKey;
155
+ readonly areaEntryId: UUID;
156
+ readonly updatedAt: ISODateTime;
157
+ }
158
+ /** A theme/plugin-registered widget region (mirrors `NavLocationDescriptor`). */
159
+ export interface WidgetRegionDescriptor {
160
+ readonly key: WidgetRegionKey;
161
+ readonly label: string;
162
+ readonly registeredBy: string;
163
+ readonly description?: string;
164
+ }
165
+ /**
166
+ * The `widgetEmbed` node's persisted shape. A block-level atom (REQ-18) — never
167
+ * nested inside a widget instance's own `bodyJson` (REQ-19, INV-04), enforced
168
+ * by `embed-validation.ts`'s `validateWidgetEmbedMutation`, the single
169
+ * validator both the live-editor path and the server-side/AI path call.
170
+ */
171
+ export interface WidgetEmbedNode {
172
+ readonly type: "widgetEmbed";
173
+ readonly placementId: UUID;
174
+ readonly widgetEntryId: UUID;
175
+ }
176
+ /** The internal render IR node shape every resolved widget produces (ADR-020's canonical render IR). */
177
+ export interface WidgetRenderIR {
178
+ readonly componentId: string;
179
+ readonly props: JsonObject;
180
+ readonly children?: readonly WidgetRenderIR[];
181
+ }
182
+ /** Context passed to a resolver for one page render (REQ-23). */
183
+ export interface WidgetResolveContext {
184
+ readonly workspaceId: UUID;
185
+ readonly locale?: string;
186
+ readonly preview: boolean;
187
+ }
188
+ /** A minimal, read-only view of a widget instance passed into a resolver — never a raw DB handle (SPEC-043's REQ-24 batching contract). */
189
+ export interface WidgetInstanceView {
190
+ readonly id: UUID;
191
+ readonly widgetType: WidgetTypeKey;
192
+ readonly config: JsonObject;
193
+ }
194
+ /** Why a widget resolution failed (SPEC-043 REQ-27's closed failure taxonomy). */
195
+ export type WidgetResolveFailureReason = "unknown-type" | "invalid-config" | "target-disabled" | "timeout" | "resolver-error";
196
+ export type WidgetResolveResult = {
197
+ readonly ok: true;
198
+ readonly ir: WidgetRenderIR;
199
+ readonly dependencyKeys: readonly string[];
200
+ } | {
201
+ readonly ok: false;
202
+ readonly reason: WidgetResolveFailureReason;
203
+ };
204
+ /**
205
+ * The typed resolver contract every dynamic (non-`static`) widget type
206
+ * implements (REQ-23/24). **Batch-first by design** — `resolveMany` is called
207
+ * at most once per type per page render (REQ-24), never once per placement, so
208
+ * N+1 query cost is a contract violation, not an optimization opportunity.
209
+ */
210
+ export interface WidgetResolver {
211
+ resolveMany(instances: readonly WidgetInstanceView[], context: WidgetResolveContext): Promise<ReadonlyMap<UUID, WidgetResolveResult>>;
212
+ }
213
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,17 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Content-type identity
3
+ // ---------------------------------------------------------------------------
4
+ /** `widget` ships as a seeded content-type registry row (ADR-022 §1), like `post`/`page`/`menu`. */
5
+ export const WIDGET_CONTENT_TYPE = "widget";
6
+ /**
7
+ * `widget_area` ships as a seeded, **system-managed** content-type registry
8
+ * row — never publicly routable, never itself placeable as a widget, never a
9
+ * valid `widgetEmbed` target (REQ-17 — no recursion into a region from inside
10
+ * a region).
11
+ */
12
+ export const WIDGET_AREA_CONTENT_TYPE = "widget_area";
13
+ /** The extension-field owner namespace for widget-level fields (`fields.ext.widget.*`). */
14
+ export const WIDGET_FIELD_NAMESPACE = "widget";
15
+ /** The extension-field owner namespace for a `widget_area` entry's region assignment. */
16
+ export const WIDGET_AREA_FIELD_NAMESPACE = "widgets";
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @file REQ-34's "where used" projection for widgets — the model-facing shape every caller that
3
+ * surfaces reference data renders (the widget-editor banner, the purge-blocked 409 body, the
4
+ * `widgets.diagnose` AI tool), so no consumer has to special-case where the data came from.
5
+ *
6
+ * Architectural role:
7
+ * This is a response SHAPER, not transport. It maps `entry_refs` rows — this domain's own
8
+ * reference-integrity index (`core/entry-refs`) — onto the disclosure vocabulary widgets speaks
9
+ * (`region` | `embed`). It touches no `Response`, no status code, and no error mapping; those stay
10
+ * in the HTTP layer.
11
+ *
12
+ * Why it lives here rather than under the HTTP admin layer, where it used to:
13
+ * it was the single import edge that made `widgets` depend on the composition root, and `server/`
14
+ * transitively reaches the whole tree — that one edge inflated this domain's transitive closure
15
+ * from 47 files / 7 modules to 162 files / 31 modules, which is the entire reason widgets looked
16
+ * unextractable. A host importing a domain's projection is the correct direction; a domain
17
+ * importing its host's transport module is not. Same misplacement species as
18
+ * `core/rate-limit/rate-limit.ts` and `features/plugin-runtime/admin-response.ts`, both already
19
+ * relocated. `server/http/site/page-head.ts` is the same species but NOT relocated — its own doc
20
+ * comment records a deliberate ADR-032 decision that the render layer, not `seo`, owns this seam,
21
+ * which the 2026-08-02 module-graph analysis's "page-head.ts -> seo/" suggestion did not account
22
+ * for; that one needs an architecture decision, not a mechanical move (2026-08-12 Refactor pass).
23
+ * The HTTP layer re-exports this symbol so its own consumers keep their existing import site.
24
+ */
25
+ import type { EntryRefRow } from "./ports.js";
26
+ /** One disclosed reference: which widgets-level location holds it, and where inside that source. */
27
+ export interface WhereUsedReference {
28
+ /** `entry_refs` distinguishes several source kinds; widgets discloses them as exactly two. */
29
+ kind: "region" | "embed";
30
+ sourceEntryId: string;
31
+ fieldPath: string;
32
+ }
33
+ /** REQ-34's where-used disclosure shape. */
34
+ export interface WhereUsedResponse {
35
+ count: number;
36
+ references: WhereUsedReference[];
37
+ }
38
+ export declare function toWhereUsedResponse({ refs }: {
39
+ refs: readonly EntryRefRow[];
40
+ }, _optional?: Record<string, never>): WhereUsedResponse;
41
+ //# sourceMappingURL=where-used.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function toWhereUsedResponse({ refs }, _optional = {}) {
2
+ return {
3
+ count: refs.length,
4
+ references: refs.map((ref) => ({
5
+ kind: ref.sourceKind === "widget-embed" ? "embed" : "region",
6
+ sourceEntryId: ref.sourceEntryId,
7
+ fieldPath: ref.fieldPath,
8
+ })),
9
+ };
10
+ }
11
+ //# sourceMappingURL=where-used.js.map