@iterant/site-runtime 3.4.0 → 3.5.0

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.
@@ -50,7 +50,7 @@ runtime and says so.
50
50
 
51
51
  <!-- generated: available libraries -->
52
52
 
53
- _Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.4.0._
53
+ _Generated from package.json by scripts/generate-kit-table.mjs. Runtime 3.5.0._
54
54
 
55
55
  **Toolchain** (this package owns the version; do NOT declare these):
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iterant/site-runtime",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "The platform layer every Iterant brand site runs on: content grammar, collection schemas, SEO head and JSON-LD, layout core, Astro config preset, dev integrations and the verify gates.",
6
6
  "scripts": {
@@ -58,6 +58,31 @@ export type DatabaseQuery = {
58
58
 
59
59
  export type DatabaseRow = Record<string, string | number | null>;
60
60
 
61
+ /**
62
+ * Where a rendered row came from, carried on the row so the visual editor can
63
+ * find the element again (epic DBX, decision DBX-D20). It rides a NON-enumerable
64
+ * `__db` slot: invisible to `JSON.stringify`, to `Object.keys`, and to every
65
+ * template that spreads or iterates the row, so nothing accidentally renders it.
66
+ */
67
+ export type DbProvenance = {
68
+ table: string;
69
+ id: string;
70
+ /** Whether the published site shows this row. The preview channel projects it
71
+ * per row (`_live`); a draft reads `false`. */
72
+ live: boolean;
73
+ };
74
+
75
+ /** The property that carries {@link DbProvenance}, hidden from enumeration. */
76
+ const PROVENANCE_KEY = "__db";
77
+
78
+ /** The attributes {@link dbAttrs} writes onto a DB-bound element. */
79
+ export type DbAttrs = {
80
+ "data-db-table": string;
81
+ "data-db-row": string;
82
+ "data-db-col": string;
83
+ "data-db-draft"?: "";
84
+ };
85
+
61
86
  export type DatabaseResult = {
62
87
  rows: DatabaseRow[];
63
88
  /** Every row matching the query, not just this page. */
@@ -151,5 +176,72 @@ export async function readDatabase(
151
176
  if (!Array.isArray(page.rows)) {
152
177
  return warn(`"${table}" answered without rows`, body);
153
178
  }
154
- return { rows: page.rows, total: Number(page.total ?? page.rows.length) };
179
+ return {
180
+ rows: page.rows.map((row) => withProvenance(row, table)),
181
+ total: Number(page.total ?? page.rows.length),
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Attach {@link DbProvenance} to a row that carries a system `_id`.
187
+ *
188
+ * The preview-read channel returns `_id` and a computed `_live` flag; the public
189
+ * production read (C6) is published-only and carries neither, so this leaves a
190
+ * production row untouched. The `_live` flag is lifted OFF the row into the
191
+ * hidden slot so no template can render it; `_id` stays where a `systemColumns`
192
+ * grant may legitimately want it. The slot is non-enumerable, so a row that
193
+ * spreads or serializes looks exactly as it did before.
194
+ */
195
+ function withProvenance(row: DatabaseRow, table: string): DatabaseRow {
196
+ const id = row._id;
197
+ if (id === undefined || id === null) return row;
198
+ const { _live, ...rest } = row;
199
+ const provenance: DbProvenance = {
200
+ table,
201
+ id: String(id),
202
+ live: _live === undefined ? true : _live !== 0,
203
+ };
204
+ Object.defineProperty(rest, PROVENANCE_KEY, {
205
+ value: provenance,
206
+ enumerable: false,
207
+ writable: false,
208
+ configurable: true,
209
+ });
210
+ return rest;
211
+ }
212
+
213
+ function provenanceOf(row: DatabaseRow): DbProvenance | undefined {
214
+ return (row as { [PROVENANCE_KEY]?: DbProvenance })[PROVENANCE_KEY];
215
+ }
216
+
217
+ /**
218
+ * The `data-db-*` attributes that make a DB-bound element self-describing, spread
219
+ * onto the element where the value is interpolated:
220
+ *
221
+ * ```astro
222
+ * <h1 {...dbAttrs(article, "title")}>{article.title}</h1>
223
+ * ```
224
+ *
225
+ * The visual editor reads these to map a click back to a table, row, and column
226
+ * (dbx-31). A draft row (its `_live` is false) also gets `data-db-draft`, which
227
+ * the preview marks so a screenshot cannot be mistaken for the live page.
228
+ *
229
+ * Returns `{}` in a production build (`import.meta.env.PROD`) and when the row
230
+ * carries no provenance, so published HTML ships with no annotations and no row
231
+ * ids. Preview only, by default (fork F1).
232
+ */
233
+ export function dbAttrs(
234
+ row: DatabaseRow,
235
+ column: string,
236
+ ): DbAttrs | Record<string, never> {
237
+ if (import.meta.env.PROD) return {};
238
+ const provenance = provenanceOf(row);
239
+ if (!provenance) return {};
240
+ const attrs: DbAttrs = {
241
+ "data-db-table": provenance.table,
242
+ "data-db-row": provenance.id,
243
+ "data-db-col": column,
244
+ };
245
+ if (provenance.live === false) attrs["data-db-draft"] = "";
246
+ return attrs;
155
247
  }
@@ -25,9 +25,11 @@ export const DEFAULT_LOCALE = "en";
25
25
  * `chrome.pt-br` → {base:"chrome", locale:"pt-br"}. Base names never contain
26
26
  * dots, so any well-formed lowercased locale suffix IS a sibling id.
27
27
  *
28
- * KEEP IN SYNC with `apps/agent-mvp/src/locales.ts#parseEntryId` the agent
29
- * writes the sibling ids the site reads back (out-of-workspace duplication,
30
- * same pattern as the special-input mirrors).
28
+ * The agent-side counterpart is
29
+ * `apps/agent-flue/src/product/localization.ts#parseEntryId`: the agent writes
30
+ * the sibling ids the site reads back (out-of-workspace duplication). KEEP THE
31
+ * LOCALE SEGMENT GRAMMAR IN SYNC with its LOCALE_SEGMENT_PATTERN; the two
32
+ * differ only on ids whose suffix fails that grammar.
31
33
  */
32
34
  export function parseEntryId(id: string): { base: string; locale?: string } {
33
35
  const dot = id.indexOf(".");
@@ -42,8 +44,8 @@ export function parseEntryId(id: string): { base: string; locale?: string } {
42
44
  * Canonical BCP-47 casing for a locale (`pt-br` → `pt-BR`, `ES` → `es`):
43
45
  * language subtag lowercased, 4-alpha script subtags Titlecased, other
44
46
  * subtags uppercased. Files/routes use the lowercased segment; this casing is
45
- * for the `<html lang>` attribute and hreflang values (starter 2.9.0). KEEP IN
46
- * SYNC with `apps/agent-mvp/src/locales.ts#normalizeBcp47`.
47
+ * for the `<html lang>` attribute and hreflang values (starter 2.9.0). This is
48
+ * the single implementation; the agent side never renders lang attributes.
47
49
  */
48
50
  export function normalizeBcp47(locale: string): string {
49
51
  const raw = locale.trim().replace(/_/g, "-");