uniweb 0.25.3 → 0.25.4

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.25.3",
3
+ "version": "0.25.4",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,12 +42,12 @@
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
44
  "@uniweb/core": "^0.10.1",
45
- "@uniweb/kit": "^0.12.3",
45
+ "@uniweb/semantic-parser": "^1.2.3",
46
46
  "@uniweb/runtime": "^0.12.2",
47
- "@uniweb/semantic-parser": "^1.2.3"
47
+ "@uniweb/kit": "^0.12.3"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "^0.24.3",
50
+ "@uniweb/build": "^0.24.4",
51
51
  "@uniweb/content-reader": "^1.2.3",
52
52
  "@uniweb/semantic-parser": "^1.2.3"
53
53
  },
@@ -24,7 +24,8 @@ import {
24
24
  diffSiteUnits,
25
25
  describeSiteDiff,
26
26
  computeUnitHashes,
27
- collectUnitUuids
27
+ collectUnitUuids,
28
+ readAssetMap
28
29
  } from '@uniweb/build/uwx'
29
30
 
30
31
  // First entity `$`-document out of a `.uwx` we produced or the backend served.
@@ -181,11 +182,11 @@ function readSyncCacheFile(siteDir) {
181
182
  return {} // missing / unreadable → treat everything as changed
182
183
  }
183
184
  }
184
- // The cache holds three maps written on DIFFERENT events — content hashes on a
185
- // successful push, base versions on push AND pull, unit hashes on push and pull —
186
- // so every writer must preserve the ones it isn't touching. One merge point rather
187
- // than three hand-rolled preserves, because getting that wrong silently disarms
188
- // whichever map got clobbered.
185
+ // The cache holds several maps written on DIFFERENT events — content hashes and the
186
+ // injections that produced them on a successful push, base versions on push AND
187
+ // pull, unit hashes on push and pull — so every writer must preserve the ones it
188
+ // isn't touching. One merge point rather than a hand-rolled preserve per writer,
189
+ // because getting that wrong silently disarms whichever map got clobbered.
189
190
  function updateSyncCache(siteDir, patch) {
190
191
  const p = syncCachePath(siteDir)
191
192
  mkdirSync(dirname(p), { recursive: true })
@@ -203,9 +204,10 @@ const readMap = (siteDir, key) => {
203
204
  * Drop every cache map that describes a BACKEND site, when this clone is bound to
204
205
  * none. Returns the names dropped (empty when there was nothing to do).
205
206
  *
206
- * `.uniweb/sync-cache.json` holds four maps keyed by *unit path* — item uuids,
207
- * content hashes, entity base versions, unit bases and a unit path (`site.yml`,
208
- * `pages/about/about.md`) is the same string for every site. So the cache does not
207
+ * `.uniweb/sync-cache.json` holds five maps keyed by *unit path* — item uuids,
208
+ * content hashes, the injections those hashes were taken over, entity base versions,
209
+ * unit bases — and a unit path (`site.yml`, `pages/about/about.md`) is the same
210
+ * string for every site. So the cache does not
209
211
  * self-invalidate when `site.yml::$uuid` goes away: it keeps describing the site
210
212
  * this folder used to be.
211
213
  *
@@ -228,21 +230,29 @@ const readMap = (siteDir, key) => {
228
230
  * the clone look bound before the check runs.
229
231
  */
230
232
  /**
231
- * Drop the four remote-derived maps unconditionally, stamping the site they now
233
+ * Drop the five remote-derived maps unconditionally, stamping the site they now
232
234
  * describe (or clearing the stamp when there is none). Shared by the pre-flight
233
235
  * guard and the `item_uuid_conflict` recovery — the guard decides WHETHER, this
234
236
  * decides WHAT, and they must not drift.
235
237
  */
236
238
  export function clearRemoteSyncState(siteDir, siteUuid = null) {
237
239
  const prior = readSyncCacheFile(siteDir)
238
- const dropped = ['itemUuids', 'hashes', 'baseVersions', 'unitBases'].filter(
239
- (k) => prior[k] && Object.keys(prior[k]).length
240
- )
240
+ const dropped = [
241
+ 'itemUuids',
242
+ 'hashes',
243
+ 'baseVersions',
244
+ 'unitBases',
245
+ 'applied'
246
+ ].filter((k) => prior[k] && Object.keys(prior[k]).length)
241
247
  updateSyncCache(siteDir, {
242
248
  itemUuids: {},
243
249
  hashes: {},
244
250
  baseVersions: {},
245
251
  unitBases: {},
252
+ // Remote-derived like the rest, and doubly so: it holds the OLD site's asset
253
+ // serve URLs. Surviving the drop, it would rewrite the new site's media to
254
+ // bytes owned by the site this folder used to be.
255
+ applied: {},
246
256
  siteUuid: siteUuid || null
247
257
  })
248
258
  return dropped
@@ -258,7 +268,13 @@ export function clearRemoteSyncStateIfUnbound(siteDir) {
258
268
  }
259
269
 
260
270
  const prior = readSyncCacheFile(siteDir)
261
- const REMOTE_MAPS = ['itemUuids', 'hashes', 'baseVersions', 'unitBases']
271
+ const REMOTE_MAPS = [
272
+ 'itemUuids',
273
+ 'hashes',
274
+ 'baseVersions',
275
+ 'unitBases',
276
+ 'applied'
277
+ ]
262
278
  const populated = REMOTE_MAPS.filter(
263
279
  (k) => prior[k] && Object.keys(prior[k]).length
264
280
  )
@@ -302,8 +318,53 @@ export function clearRemoteSyncStateIfUnbound(siteDir) {
302
318
  export function readSyncCache(siteDir) {
303
319
  return readMap(siteDir, 'hashes')
304
320
  }
305
- export function writeSyncCache(siteDir, hashes) {
306
- updateSyncCache(siteDir, { hashes })
321
+
322
+ /**
323
+ * The hash-affecting injections the emit that produced those hashes applied, in the
324
+ * exact shape `emitSyncPackages` takes back as opts.
325
+ *
326
+ * ⛔ **Only the ones nothing else records.** `assetIds` is deliberately NOT banked
327
+ * here even though the emit applies it: `assets.json` is COMMITTED project state
328
+ * holding exactly that map (local ref → `{id, ext}`), written by the same push, and
329
+ * a gitignored second copy would be a second thing to disagree — the reason that
330
+ * file itself refuses to hold a serve URL. It also has the worse lifetime of the
331
+ * two: `clearRemoteSyncState` wipes this cache, and the committed map correctly
332
+ * survives. ⇒ Bank what a reader cannot re-derive; re-derive the rest.
333
+ *
334
+ * ⛔ Read this whenever you re-emit to COMPARE against `hashes`. A push hashes the
335
+ * DELIVERED document — local `/images/x.png` rewritten to the backend serve URL it
336
+ * just uploaded to, `info.foundation` replaced by the version-pinned ref — and banks
337
+ * that. An offline re-emit produces the AUTHORED document, which is a different
338
+ * document, so it matches nothing and every entity reads as changed forever. That is
339
+ * not hypothetical: it is what `uniweb status` did on any site with one local image
340
+ * (backend-framework-787e, 2026-08-19) — `push` said "1 entity unchanged" and
341
+ * `status --json` said `changed: 1`, from the same cache, seconds apart.
342
+ *
343
+ * ⭐ What is left is what only a backend round-trip produces — an asset **serve URL**
344
+ * and a released foundation version — and `status` is offline by design (measured at
345
+ * zero HTTP requests, a property the cross-client flows rely on). ⚠️ Note the serve
346
+ * URL is REPLAYED, never composed: we re-use the string the host handed us, which is
347
+ * a different act from reconstructing one, and the distinction is the same one that
348
+ * keeps `assets.json` id-only. An asset genuinely new to the site has no recorded
349
+ * mapping and still reads as changed, which is correct.
350
+ */
351
+ export function readAppliedInjections(siteDir) {
352
+ return readMap(siteDir, 'applied')
353
+ }
354
+
355
+ /**
356
+ * Bank the content hashes and the injections that produced them. ⛔ ONE call, both
357
+ * maps: they describe the same document, so writing either alone leaves the cache
358
+ * self-inconsistent — and the failure is silent, since a hash never says which
359
+ * document it is of. `applied` is written even when empty, so it can never be a
360
+ * leftover from an earlier push describing hashes it no longer matches.
361
+ *
362
+ * `assetIds` is dropped rather than stored — see readAppliedInjections: it has a
363
+ * committed source of truth in `assets.json`, and the reader re-derives it there.
364
+ */
365
+ export function writeSyncCache(siteDir, hashes, applied) {
366
+ const { assetIds: _inAssetsJson, ...bankable } = applied || {}
367
+ updateSyncCache(siteDir, { hashes, applied: bankable })
307
368
  }
308
369
 
309
370
  /**
@@ -835,10 +896,24 @@ export function writeUnitBases(siteDir, patch) {
835
896
  */
836
897
  export async function probeUnpushed(siteDir, { sendAll = false } = {}) {
837
898
  const priorHashes = readSyncCache(siteDir)
899
+ // Re-emit the document the last push HASHED, not the one the author wrote — see
900
+ // readAppliedInjections. Two sources, on purpose:
901
+ // · BANKED — the serve URLs and pinned refs only a round-trip produces. Empty
902
+ // for a cache written before this was banked (and for a never-pushed site),
903
+ // which is the pre-fix behaviour and self-heals on the next push. It can never
904
+ // point at the wrong document: it is written with the hashes it belongs to.
905
+ // · RE-DERIVED — asset identity, from the COMMITTED `assets.json` the same push
906
+ // wrote. Reading the live file rather than a snapshot is what makes a moved
907
+ // map (a teammate's push, a pull) read as changed instead of matching a copy
908
+ // of itself.
909
+ const applied = readAppliedInjections(siteDir)
910
+ const assetIds = readAssetMap(siteDir)
838
911
  const pkg = await emitSyncPackages(siteDir, {
839
912
  resolveModel: makeModelResolver({ client: null, offline: true }),
840
913
  priorHashes,
841
- sendAll
914
+ sendAll,
915
+ ...applied,
916
+ ...(Object.keys(assetIds).length ? { assetIds } : {})
842
917
  })
843
918
  const changed =
844
919
  (pkg.siteContent?.entityCount || 0) + (pkg.collections?.entityCount || 0)
@@ -867,7 +942,7 @@ export async function pushSyncPackages({
867
942
  asOrg,
868
943
  report
869
944
  }) {
870
- const { siteContent, collections, siteContentUuid, hashes } = pkg
945
+ const { siteContent, collections, siteContentUuid, hashes, applied } = pkg
871
946
  const { info, note, error } = report
872
947
  const dim = report.dim || ((s) => s)
873
948
 
@@ -1200,7 +1275,7 @@ export async function pushSyncPackages({
1200
1275
  // then bank the post-write tokens so the NEXT push carries a current base.
1201
1276
  // Entities absent from finalized[] (skipped, or not editable) keep their cached
1202
1277
  // value — absence is not invalidation.
1203
- writeSyncCache(siteDir, hashes)
1278
+ writeSyncCache(siteDir, hashes, applied)
1204
1279
  mergeHarvested()
1205
1280
  // Re-base the page attribution: our emitted document and the backend's post-write
1206
1281
  // copy of it are the two sides' new agreed state. Only when the site-content lane
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-18T22:18:28.572Z",
3
+ "generatedAt": "2026-08-19T03:59:24.190Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.24.3",
6
+ "version": "0.24.4",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -39,7 +39,15 @@
39
39
  * an upload landed.
40
40
  *
41
41
  * Contract ratified 2026-08-18; see `kb/framework/build/data-ball-retirement.md`.
42
- * ⚠️ **The endpoint is not built yet** this is unwired until it is.
42
+ * **Wired into `publish.js` and shipped in `uniweb` 0.25.3.** Both arms of the
43
+ * endpoint exist (presigned landed 2026-08-18, per Diego).
44
+ *
45
+ * ⚠️ **The presigned arm has never been exercised against a real backend from
46
+ * here.** The branch is one line and unit-tested both ways, but a stub is not a
47
+ * presigning deployment — three claims diverged that way in a single day while
48
+ * this lane was being built. A local `uniwebd` answers `direct`, so proving it
49
+ * needs a presigning deployment and one real publish. **Believed correct, not
50
+ * demonstrated.**
43
51
  */
44
52
 
45
53
  import { createHash } from 'node:crypto'