uniweb 0.66.0 → 0.68.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,17 +41,17 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/kit": "^0.19.4",
45
- "@uniweb/runtime": "^0.27.4",
46
44
  "@uniweb/content-writer": "^0.3.4",
47
- "@uniweb/schemas": "^0.5.0",
48
45
  "@uniweb/core": "^0.29.8",
46
+ "@uniweb/runtime": "^0.27.4",
47
+ "@uniweb/kit": "^0.19.4",
48
+ "@uniweb/schemas": "^0.7.0",
49
49
  "@uniweb/semantic-parser": "^1.4.1"
50
50
  },
51
51
  "peerDependencies": {
52
+ "@uniweb/build": "^0.62.0",
52
53
  "@uniweb/content-reader": "^1.2.5",
53
- "@uniweb/semantic-parser": "^1.4.1",
54
- "@uniweb/build": "^0.60.0"
54
+ "@uniweb/semantic-parser": "^1.4.1"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "@uniweb/build": {
@@ -879,6 +879,25 @@ Keep those folders flat: `records/article/design-tips.md` works; `records/articl
879
879
 
880
880
  Item frontmatter conventionally uses `title`, `date`, `tags`, `image`, `description`, `author` — plus any fields your content needs (`price`, `role`, `order`). Images can sit beside the item file and be referenced with `./`. **`draft: true` keeps a record off the live site without deleting it**: it is still a record, in `records/`, and `pnpm dev` shows it so you can preview it, but a build for hosting leaves it out, and a backend you `uniweb push` it to keeps it in the site's folder without delivering it. (`published: false`, an older spelling, is refused.)
881
881
 
882
+ **A record's file follows its data schema's sections.** A schema with one section (the `fields:` form) is written flat, as above. A schema with more than one is written **by section** — each field under its section's name — and a field left at the top stops the build and a push, naming where it goes. `@std/article` has two, its card `article` and `article_body`:
883
+
884
+ ```markdown
885
+ <!-- records/std/article/hello.md -->
886
+ ---
887
+ article:
888
+ title: Hello
889
+ date: 2026-05-01
890
+ article_body:
891
+ author: Ada
892
+ ---
893
+
894
+ The body — the value of the schema's content field, `article_body.content`.
895
+ ```
896
+
897
+ **A component receives it differently from how the file writes it**, the same way on a static site and a hosted one: the brief's fields at the top (`article.title`), every other section under its name (`article.article_body.content`, `article.article_body.author`), and `$name`, the handle. Lists carry the brief alone unless the query says otherwise (`deferred:`, below). Full rules: `reference/entity-content.md`.
898
+
899
+ **A reference names the record it points at.** A `{ ref: '@/speaker' }` field is written as that record's name — `speaker: ada` for `records/speaker/ada.yml` (its file's name, or its `slug:`) — and a `many` one as a list of names. A component receives `{ entity, brief }`: the record reduced to its brief, so `talk.speaker.brief.name`. `uniweb validate` and a push report a name that matches no record; a push sends each reference to the record it names (a second pass when that record is new too), and a pull writes the name back.
900
+
882
901
  **`records/folder.yml` — optional, and only for folders.** It is the one file in `records/` that is not a record, it moves with the directory (`paths.records`), and like a pages folder's `folder.yml` it describes the folder it sits in. Every record sits at the top of the site's records folder unless `folder.yml` places it in a sub-folder. Add one only when a query needs to ask for a *slice* of the records rather than all of them — most sites never do:
883
902
 
884
903
  ```yaml
@@ -946,7 +965,7 @@ pages/blog/
946
965
 
947
966
  **Two options for bigger sets:**
948
967
 
949
- `deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
968
+ `deferred: [body]` strips heavy fields from the list payload — cards stay light, while a `[slug]` page still receives the full record automatically and other components fetch the whole record on demand with `useWholeRecord`. It names keys of the record as a component receives it — a field, or a section's name — and a schema with more than one section implies it: every section but the brief (`[article_body]` for `@std/article`), so it is rarely written. File-based records emit per-record files at `/data/<name>/<slug>.json` and need no configuration; an external query names its one-record request with `record:` instead (*Fetching from other sources* in Part 4).
950
969
 
951
970
  `queryable:` declares which fields a reader may filter on, with enough metadata for the foundation to render controls:
952
971
 
@@ -1866,11 +1885,11 @@ fetch:
1866
1885
  limit: 3
1867
1886
  ```
1868
1887
 
1869
- **Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]`. The list payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json`. (An external query declares no `deferred:` — its whole record comes from `record:`, below.) On a parametric page the record's full data is delivered automatically; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`. The hook is safe to call on any query: when the query has no separate source for the whole record it returns the record you passed in, because nothing was stripped from it.
1888
+ **Lean lists with `deferred:`.** A query over records with heavy fields (article bodies, large nested arrays) can declare `deferred: [body]` — keys of the record as a component receives it, a section's name included; a schema with more than one section implies every section but its brief. The list payload omits those fields; per-record full files are emitted at `/data/<name>/<slug>.json`. (An external query declares no `deferred:` — its whole record comes from `record:`, below.) On a parametric page the record's full data is delivered automatically; elsewhere components fetch the whole record on demand with `useWholeRecord(record, { query })`. The hook is safe to call on any query: when the query has no separate source for the whole record it returns the record you passed in, because nothing was stripped from it.
1870
1889
 
1871
1890
  **Component-side fetching.** When a component genuinely needs to fetch on its own (a search box, "load more", a lazy popover), use the kit hooks — `useFetched`, `useCacheEntry`, `useWholeRecord`. They share the framework's cache and dispatcher with declarative fetches; same-key requests dedupe automatically.
1872
1891
 
1873
- **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. A violation fails it (`--lax` only reports); it checks every record file in `records/`, and `push` / `publish` refuse the same findings before sending anything. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`), `ref`/`options`, and rich `sections`-form inputs are reported deferred.
1892
+ **Validate before shipping.** `uniweb validate` checks file-based data against your declared schemas — missing required fields, type/enum/format mismatches, nested fields. A violation fails it (`--lax` only reports); it checks every record file in `records/`, and `push` / `publish` refuse the same findings before sending anything. Distinct from `uniweb doctor` (project structure): `validate` checks your *data* against the schemas you *declared*. External queries (`url:`) are reported as deferred — their data isn't in the project; the value of a `ref`/`options` field is not checked against the entity it names.
1874
1893
 
1875
1894
  ### Fetching from other sources (`fetcher:`)
1876
1895
 
@@ -2609,7 +2628,7 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
2609
2628
 
2610
2629
  Either side can publish. Nothing about this changes how you build: the same foundation and the same site run under `uniweb dev`, `uniweb export`, or a CI deploy with no account at all.
2611
2630
 
2612
- **Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --backend <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
2631
+ **Publishing vs registering.** Foundations on Uniweb Cloud live in the catalog as `@org/name@version` — `name` from the foundation's `main.js`. When a foundation powers a single site, **don't run `uniweb register` yourself** — `uniweb publish` from the site directory releases the local foundation to the catalog (when its code changed) and goes live in one step. A registered version is immutable, so when the foundation's code changed under a version already registered, `push` and `publish` release the change under the next version and write it into the foundation's `package.json` — commit that file (`--no-release` sends the content without the code). The version it picks is always the next patch: for a change that is not backwards compatible, set a higher minor or major version in the foundation's `package.json` yourself before you push. They stop when the registry holds a version newer than yours, released from another copy: pull that change first, or pass `--bump` to release yours above it. Register deliberately only when the foundation is a product meant for multiple sites; consuming sites then pin `foundation: '@org/name@1.2.3'`. **The catalog is private and access-segregated, not a public package registry** — people see only the foundations licensed to sites they own or edit. The *site* carries the license, and it rides along with site ownership when a developer hands a site to a client. Don't describe publishing as making a foundation publicly discoverable. Schemas can also be registered on their own from a schemas-only package (`@uniweb/schemas`, any `@org/schemas`, or a bare folder of `schemas/*.{yml,json,js}`) — that's how `@std` schemas are published. Auth via `uniweb login` (`uniweb login --backend <url> --token <bearer>` without a terminal) or `UNIWEB_TOKEN`; preview with `--dry-run`.
2613
2632
 
2614
2633
  ### Staying current
2615
2634
 
@@ -20,9 +20,10 @@
20
20
  * above the registered one, written into the package's `package.json` first. ⛔ This
21
21
  * reverses the version-bump release gate of 2026-06-23, whose reasoning — a deliberate,
22
22
  * npm-style release — fits a foundation other sites use as a product; those sites pin
23
- * a catalog ref and never reach this code. (A library's own demo site, which names it
24
- * by path, does — whether such a foundation should declare its releases deliberate is
25
- * open.)
23
+ * a catalog ref and never reach this code. Its author's demo site, which names it by
24
+ * path, does — and gets the same default, by ruling [Diego, 2026-09-24]. The release is
25
+ * always the next patch, so a change that is not backwards compatible needs a higher
26
+ * version set in `package.json` by hand.
26
27
  *
27
28
  * ⛔ **A NEWER registered version still stops it.** It was released from another copy
28
29
  * of the project, whose code this one may not have, and releasing above it would make
@@ -1186,8 +1186,12 @@ export async function probeUnpushed(siteDir, { backend = null, sendAll = false }
1186
1186
  // ⚠️ Until 2026-09-23 this said the site's recorded ORG resolves it and must be
1187
1187
  // passed in — true before 2026-09-22, and read as a reason to pass it.
1188
1188
  const pkg = await comparisonEmit(siteDir, { backend, priorHashes, sendAll })
1189
+ // A record held back for a reference (`pushInPasses`) is not in either lane, and is
1190
+ // still unpushed.
1189
1191
  const changed =
1190
- (pkg.siteContent?.entityCount || 0) + (pkg.records?.entityCount || 0)
1192
+ (pkg.siteContent?.entityCount || 0) +
1193
+ (pkg.records?.entityCount || 0) +
1194
+ (pkg.waiting || []).filter((w) => w.held).length
1191
1195
  return { changed, unchanged: pkg.skipped || 0, warnings: pkg.warnings || [] }
1192
1196
  }
1193
1197
 
@@ -1846,3 +1850,78 @@ export async function pushSyncPackages({
1846
1850
  }
1847
1851
  return { exitCode: 0, boundSiteUuid, finalizedTotal, wrote }
1848
1852
  }
1853
+
1854
+ /**
1855
+ * Push, then push again while records WAIT for a reference's record — the verbs' way to
1856
+ * send a package (`push`, `publish`).
1857
+ *
1858
+ * ⭐ A reference is sent as the uuid the backend minted for the record it names, so a
1859
+ * record that points at one created by the same push cannot be completed in that push
1860
+ * (`@uniweb/build`'s `emitSyncPackages` → `waiting`). The first pass creates what it can —
1861
+ * records whose waiting references are not required go without them — and each further
1862
+ * pass, emitted afresh from the state the last one banked, sends what has become
1863
+ * nameable. A chain of required references takes one pass per link; two records that
1864
+ * each require the other can never be completed, and the push stops, naming them.
1865
+ *
1866
+ * @param {object} params
1867
+ * @param {object} params.pkg - the first pass's package (`emitSyncPackages`)
1868
+ * @param {() => Promise<object>} params.reemit - the next pass's package, emitted afresh
1869
+ * @param {Function} [params.push] - sends one package; `pushSyncPackages` (a test's seam)
1870
+ * @returns {Promise<{ exitCode: number, boundSiteUuid?: string, finalizedTotal: number, wrote: string[], passes: number }>}
1871
+ */
1872
+ export async function pushInPasses({ client, siteDir, pkg, report, reemit, maxPasses = 12, push = pushSyncPackages }) {
1873
+ const { note, error } = report
1874
+ let current = pkg
1875
+ let passes = 1
1876
+ let result = await push({ client, siteDir, pkg: current, report })
1877
+ let finalizedTotal = result.finalizedTotal || 0
1878
+ const wrote = [...(result.wrote || [])]
1879
+ while (result.exitCode === 0 && current.waiting?.length) {
1880
+ const n = current.waiting.length
1881
+ note(
1882
+ `${n} record${n === 1 ? '' : 's'} name${n === 1 ? 's' : ''} a record this push created — ` +
1883
+ `pushing again to complete ${n === 1 ? 'its references' : 'their references'}.`
1884
+ )
1885
+ let next
1886
+ try {
1887
+ next = await reemit()
1888
+ } catch (err) {
1889
+ error(`Could not build the next pass: ${err.message}`)
1890
+ return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
1891
+ }
1892
+ if (refuseUnsendableRecords(next.refusals, report)) {
1893
+ return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
1894
+ }
1895
+ // Nothing became nameable: every record still waiting waits on one that waits too.
1896
+ if (waitingKey(next.waiting) === waitingKey(current.waiting) || passes >= maxPasses) {
1897
+ error(
1898
+ 'These records could not be completed — each requires a reference to a record that ' +
1899
+ 'is itself waiting to be created:'
1900
+ )
1901
+ for (const w of next.waiting || []) {
1902
+ const refs = w.pending.map((p) => `${p.path} → "${p.name}"`).join(', ')
1903
+ note(` ${w.id} (${w.model}): ${refs}`)
1904
+ }
1905
+ note(
1906
+ 'Push one of them without that reference first — or make it optional in the schema — ' +
1907
+ 'then add it back and push again.'
1908
+ )
1909
+ return { ...result, exitCode: 1, finalizedTotal, wrote, passes }
1910
+ }
1911
+ current = next
1912
+ passes++
1913
+ result = await push({ client, siteDir, pkg: current, report })
1914
+ finalizedTotal += result.finalizedTotal || 0
1915
+ for (const w of result.wrote || []) if (!wrote.includes(w)) wrote.push(w)
1916
+ }
1917
+ return { ...result, finalizedTotal, wrote, passes }
1918
+ }
1919
+
1920
+ // Which records wait, and on what — two passes that leave it the same made no progress.
1921
+ function waitingKey(waiting) {
1922
+ return JSON.stringify(
1923
+ (waiting || [])
1924
+ .map((w) => [w.model, w.id, w.pending.map((p) => `${p.path}:${p.name}`).sort()])
1925
+ .sort()
1926
+ )
1927
+ }
@@ -90,7 +90,8 @@ import {
90
90
  refuseUnsendableRecords,
91
91
  clearRemoteSyncStateIfUnbound,
92
92
  dropSiteBoundValues,
93
- pushSyncPackages
93
+ readItemUuids,
94
+ pushInPasses
94
95
  } from '../backend/site-sync.js'
95
96
  import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
96
97
  import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
@@ -767,12 +768,6 @@ export async function publish(args = []) {
767
768
  // (It stamped `info.data_bundle` until 2026-08-18; the ball is gone and
768
769
  // collection data now lands at its serving tail, so nothing records it.)
769
770
  const priorHashes = readSyncCache(siteDir, client.origin)
770
- // publish rides the same gated push as `uniweb push`: if an app author has
771
- // edited since this clone last synced, the push is refused rather than
772
- // overwriting them, and nothing goes live. `--force` drops the precondition.
773
- const baseVersions = args.includes('--force')
774
- ? null
775
- : readBaseVersions(siteDir, client.origin)
776
771
  // Per-item identity, recovered from the backend when this clone has never seen it.
777
772
  // Without it the backend re-mints every page and section row (see readItemUuids).
778
773
  const itemUuids = await ensureItemUuids({
@@ -948,9 +943,14 @@ export async function publish(args = []) {
948
943
  say.dim('Services unchanged.')
949
944
  }
950
945
 
951
- let pkg
952
- try {
953
- pkg = await emitSyncPackages(siteDir, {
946
+ // The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
947
+ // hashes, identity and base versions that the next one must build on.
948
+ //
949
+ // publish rides the same gated push as `uniweb push`: if an app author has
950
+ // edited since this clone last synced, the push is refused rather than
951
+ // overwriting them, and nothing goes live. `--force` drops the precondition.
952
+ const forced = args.includes('--force')
953
+ const emitOptions = ({ priorHashes, itemUuids }) => ({
954
954
  backend: client.origin,
955
955
  ...(declaration.declare ? {} : { declareServices: false }),
956
956
  // Placement identity for the folder — see writeFolderItemUuids.
@@ -959,14 +959,20 @@ export async function publish(args = []) {
959
959
  resolveModel,
960
960
  priorHashes,
961
961
  itemUuids,
962
- ...(baseVersions
963
- ? { baseVersions, itemBaseVersions: readItemBaseVersions(siteDir, client.origin) }
964
- : {}),
962
+ ...(forced
963
+ ? {}
964
+ : {
965
+ baseVersions: readBaseVersions(siteDir, client.origin),
966
+ itemBaseVersions: readItemBaseVersions(siteDir, client.origin)
967
+ }),
965
968
  ...(Object.keys(injectInfo).length ? { injectInfo } : {}),
966
969
  ...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
967
970
  ...(assetRewrite ? { assetRewrite } : {}),
968
971
  ...(assetIds ? { assetIds } : {})
969
- })
972
+ })
973
+ let pkg
974
+ try {
975
+ pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, itemUuids }))
970
976
  } catch (err) {
971
977
  say.err(`Could not build the sync package: ${err.message}`)
972
978
  return { exitCode: 1 }
@@ -981,11 +987,19 @@ export async function publish(args = []) {
981
987
  error: (m) => say.err(m),
982
988
  dim: (s) => `${c.dim}${s}${c.reset}`
983
989
  }
984
- const pushResult = await pushSyncPackages({
990
+ const pushResult = await pushInPasses({
985
991
  client,
986
992
  siteDir,
987
993
  pkg,
988
- report
994
+ report,
995
+ reemit: () =>
996
+ emitSyncPackages(
997
+ siteDir,
998
+ emitOptions({
999
+ priorHashes: readSyncCache(siteDir, client.origin),
1000
+ itemUuids: readItemUuids(siteDir, client.origin)
1001
+ })
1002
+ )
989
1003
  })
990
1004
  if (pushResult.exitCode !== 0) return { exitCode: pushResult.exitCode }
991
1005
  const siteUuid = pushResult.boundSiteUuid
@@ -87,7 +87,7 @@ import {
87
87
  ensureSiteExists,
88
88
  clearRemoteSyncStateIfUnbound,
89
89
  dropSiteBoundValues,
90
- pushSyncPackages
90
+ pushInPasses
91
91
  } from '../backend/site-sync.js'
92
92
  import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
93
93
 
@@ -496,9 +496,9 @@ export async function push(args = [], deps = {}) {
496
496
  output || dryRun
497
497
  ? readItemUuids(siteDir, client.origin)
498
498
  : await ensureItemUuids({ client, siteDir, note })
499
- let pkg
500
- try {
501
- pkg = await emitSyncPackages(siteDir, {
499
+ // The emit's options, read afresh for each pass (`pushInPasses`): a pass banks
500
+ // hashes, identity and base versions that the next one must build on.
501
+ const emitOptions = ({ priorHashes, sendAll, itemUuids }) => ({
502
502
  backend: client.origin,
503
503
  // Placement identity for the folder — see writeFolderItemUuids.
504
504
  folderItemUuids: readFolderItemUuids(siteDir, client.origin),
@@ -529,7 +529,10 @@ export async function push(args = [], deps = {}) {
529
529
  }),
530
530
  ...(assetRewrite ? { assetRewrite } : {}),
531
531
  ...(assetIds ? { assetIds } : {})
532
- })
532
+ })
533
+ let pkg
534
+ try {
535
+ pkg = await emitSyncPackages(siteDir, emitOptions({ priorHashes, sendAll, itemUuids }))
533
536
  } catch (err) {
534
537
  error(`Could not build the sync package: ${err.message}`)
535
538
  return { exitCode: 2 }
@@ -595,8 +598,9 @@ export async function push(args = [], deps = {}) {
595
598
  }
596
599
 
597
600
  // Submit both lanes, back-fill the minted uuids, and persist the send-only-changed
598
- // cache. Shared with `uniweb publish` via ../backend/site-sync.js.
599
- const result = await pushSyncPackages({
601
+ // cache — again for any record whose reference names a record this push created.
602
+ // Shared with `uniweb publish` via ../backend/site-sync.js.
603
+ const result = await pushInPasses({
600
604
  client,
601
605
  siteDir,
602
606
  pkg,
@@ -605,7 +609,16 @@ export async function push(args = [], deps = {}) {
605
609
  note,
606
610
  error,
607
611
  dim: (s) => `${colors.dim}${s}${colors.reset}`
608
- }
612
+ },
613
+ reemit: () =>
614
+ emitSyncPackages(
615
+ siteDir,
616
+ emitOptions({
617
+ priorHashes: readSyncCache(siteDir, client.origin),
618
+ sendAll: false,
619
+ itemUuids: readItemUuids(siteDir, client.origin)
620
+ })
621
+ )
609
622
  })
610
623
  if (result.exitCode !== 0) return { exitCode: result.exitCode }
611
624
  success(
@@ -12,9 +12,10 @@
12
12
  * ⭐ A violation fails it (exit 1); `--lax` reports without failing. It warned by
13
13
  * default until 2026-09-24, with `--strict` to fail — a gate that passes is one
14
14
  * nobody reads, and `push` / `publish` now refuse the same findings. The live render
15
- * path stays tolerant. Dynamic (remote) inputs and entity references can't be
16
- * resolved without a running backend, so they're reported as deferred, never
17
- * silently skipped.
15
+ * path stays tolerant. Remote (`url:`) inputs are not in the project, so they're
16
+ * reported as deferred, never silently skipped. The value of an entity reference is
17
+ * not checked against the entity it names. ⛔ This said references were "reported as
18
+ * deferred" until 2026-09-24; no check ever reported them.
18
19
  */
19
20
 
20
21
  import { existsSync, readFileSync } from 'node:fs'
@@ -171,7 +172,7 @@ function printSiteHuman(result) {
171
172
  }
172
173
 
173
174
  if (deferred.length > 0) {
174
- log(` ${colors.dim}↪ deferred (not statically checkable):${colors.reset}`)
175
+ log(` ${colors.dim}↪ deferred (not checked):${colors.reset}`)
175
176
  for (const d of deferred) {
176
177
  const extra = d.url
177
178
  ? ` ${colors.dim}(${d.url})${colors.reset}`
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-24T15:29:04.711Z",
3
+ "generatedAt": "2026-09-24T21:52:18.260Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "0.6.1",
6
+ "version": "0.6.2",
7
7
  "path": "framework/api",
8
8
  "deps": [
9
9
  "@uniweb/core"
10
10
  ]
11
11
  },
12
12
  "@uniweb/build": {
13
- "version": "0.60.0",
13
+ "version": "0.62.0",
14
14
  "path": "framework/build",
15
15
  "deps": [
16
16
  "@uniweb/content-reader",
@@ -96,7 +96,7 @@
96
96
  "deps": []
97
97
  },
98
98
  "@uniweb/schemas": {
99
- "version": "0.5.0",
99
+ "version": "0.7.0",
100
100
  "path": "framework/schemas",
101
101
  "deps": []
102
102
  },
@@ -116,7 +116,7 @@
116
116
  "deps": []
117
117
  },
118
118
  "@uniweb/templates": {
119
- "version": "0.16.3",
119
+ "version": "0.17.0",
120
120
  "path": "framework/templates",
121
121
  "deps": []
122
122
  },
@@ -126,7 +126,7 @@
126
126
  "deps": []
127
127
  },
128
128
  "@uniweb/unipress": {
129
- "version": "0.10.3",
129
+ "version": "0.10.5",
130
130
  "path": "framework/unipress",
131
131
  "deps": [
132
132
  "@uniweb/build",
package/src/index.js CHANGED
@@ -1697,9 +1697,9 @@ project against framework conventions.
1697
1697
 
1698
1698
  Every record file is checked against the schema its folder names, whether or
1699
1699
  not a section reads it. A violation fails the run; \`push\` and \`publish\` refuse
1700
- the same findings. The live render path stays tolerant. Dynamic (\`url:\`)
1701
- inputs and entity references can't be resolved without a running backend, so
1702
- they're reported as deferred, never silently skipped.
1700
+ the same findings. The live render path stays tolerant. Remote (\`url:\`)
1701
+ inputs are not in the project, so they're reported as deferred, never silently
1702
+ skipped. A reference field's value is not checked against the entity it names.
1703
1703
 
1704
1704
  ${colors.bright}Options:${colors.reset}
1705
1705
  --lax Report violations without failing (exit 0)