Skip to content

Commit e55fdb5

Browse files
MajorTalclaude
andcommitted
fix(astro): bump CACHE_SCHEMA_VERSION to 2 so v1.54 AssetRef fields propagate
The build cache at node_modules/.run402/assetMap.json stored AssetRefs verbatim by source SHA. When the gateway started emitting the v1.54 shape-contract fields (blurhash_data_url + asset_schema), existing caches kept returning pre-v1.54 AssetRefs on hit — every build with unchanged source bytes was silently serializing manifests that looked right (legacy fields populated) but lacked the two new fields. Reproduced against silver-pines.kychon.com: manifest built 24 min after the gateway fix shipped + live tests confirmed the fields on api.run402.com, but the deployed manifest still had both fields missing. Cache hit path at uploader.ts:404 returned the stale ref; buildManifest (vite-plugin.ts:392) JSON.stringified it verbatim into the output. Three coordinated changes plus the discipline note that prevents this class of bug going forward: 1. astro/src/cache.ts — CACHE_SCHEMA_VERSION 1 → 2. Bumping is non-destructive: stale entries are dropped silently and the next build re-uploads (or, more commonly, hits the gateway's CAS dedupe and re-receives a fresh, fully-populated AssetRef). New file header documents the "bump on every AssetRef field add" discipline plus the history of why the version was bumped. 2. astro/src/types.ts — AssetRef widened to mirror the current SDK shape: v1.50 fields (metadata, image_format, image_info, image_exif, image_exif_policy) plus v1.54 fields (blurhash_data_url, asset_schema). Plus AssetMetadata + ExifPolicy supporting exports. Strictly additive — every existing consumer continues to type-check. CacheFile.version literal bumped to 2 in lockstep with cache.ts. 3. astro/src/cache.test.ts — existing version-on-disk assertion bumped to 2 with a comment pointing at the bump discipline. Adds two new tests: a v1 → v2 schema-migration regression that asserts v1 entries are discarded on load (this is the actual bug), and a v1.54-field roundtrip guard that catches accidental field stripping in flush()/load() (the class of bug v2 prevents) by writing then reading an AssetRef carrying blurhash_data_url + asset_schema. 4. astro/CHANGELOG.md — new "Unreleased" section above the existing 1.0.0-alpha.1 entry documenting the fix + the AssetRef widening + the internal cache discipline doc. 223/223 astro unit tests pass; tsc build clean. Reproducer for downstream consumers: rm -f node_modules/.run402/assetMap.json && npm run build, then jq '.assets[<key>] | {blurhash_data_url, asset_schema}' dist/_assets-manifest.json populates both fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6851332 commit e55fdb5

4 files changed

Lines changed: 161 additions & 8 deletions

File tree

astro/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to `@run402/astro`.
44

5+
## Unreleased
6+
7+
### Fixed
8+
9+
- **Stale `dist/_assets-manifest.json` entries missing v1.54 AssetRef fields.** The build cache at `node_modules/.run402/assetMap.json` stores AssetRefs verbatim by source SHA; when the gateway started emitting `blurhash_data_url` + `asset_schema` (v1.54), existing caches kept returning pre-v1.54 AssetRefs on hit, silently producing manifests that looked correct (legacy fields populated) but lacked the v1.54 additions. Bumped `CACHE_SCHEMA_VERSION` from `1` to `2` so existing caches invalidate on first run after upgrade. Reproducer: `rm -f node_modules/.run402/assetMap.json && npm run build` then `jq '.assets[<key>] | {blurhash_data_url, asset_schema}' dist/_assets-manifest.json` populates both fields.
10+
11+
### Changed
12+
13+
- **`AssetRef` widened to mirror the current SDK shape.** Adds the v1.50 metadata + EXIF fields (`metadata`, `image_format`, `image_info`, `image_exif`, `image_exif_policy`) and v1.54 shape-contract fields (`blurhash_data_url`, `asset_schema`) the runtime already passed through. Plus the supporting `AssetMetadata` + `ExifPolicy` exports. Strictly additive — pre-existing consumers continue to type-check unchanged, but `as any` casts at field-access sites can now be removed.
14+
15+
### Internal
16+
17+
- New cache.ts header documents the AssetRef-field-add → cache-version-bump discipline that prevents future occurrences of this bug; `cache.test.ts` gains a v1 → v2 migration regression test and a v1.54-field roundtrip guard.
18+
519
## 1.0.0-alpha.1 — unreleased
620

721
The agent-DX-locked default-export preset (Finding 5 in the design-review consultation). One-line `astro.config.mjs`:

astro/src/cache.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ describe("BuildCache", () => {
6262
const expected = join(root, "node_modules", ".run402", "assetMap.json");
6363
assert.ok(existsSync(expected), "cache file should exist");
6464
const parsed = JSON.parse(readFileSync(expected, "utf-8"));
65-
assert.equal(parsed.version, 1);
65+
// Cache schema is currently v2 (v1.50 + v1.54 AssetRef additions).
66+
// Bumping CACHE_SCHEMA_VERSION should also bump this assertion AND
67+
// add a new "drops a v<previous> cache file" test to the migration
68+
// suite below — see cache.ts header for the rationale.
69+
assert.equal(parsed.version, 2);
6670
assert.ok(parsed.entries["/abs/hero.jpg"]);
6771
});
6872

@@ -115,4 +119,63 @@ describe("BuildCache", () => {
115119
const cache = new BuildCache(root);
116120
assert.equal(cache.size(), 0);
117121
});
122+
123+
// Regression: pre-v2 cache files (written before v1.54 AssetRef shape
124+
// added `blurhash_data_url` + `asset_schema`) were silently returned
125+
// verbatim on cache hit, dropping the new fields from
126+
// `dist/_assets-manifest.json`. Bumping CACHE_SCHEMA_VERSION to 2
127+
// invalidates those caches and forces a fresh AssetRef on the next
128+
// build. If this test fails after a future schema bump, also bump
129+
// CACHE_SCHEMA_VERSION here AND add a new test asserting v2 caches
130+
// are dropped at the new version.
131+
it("drops a v1 cache file on load (schema migration v1 → v2)", async () => {
132+
const cacheDir = join(root, "node_modules", ".run402");
133+
const { mkdirSync: mkdir, writeFileSync: writeFile } = await import("node:fs");
134+
mkdir(cacheDir, { recursive: true });
135+
const v1File = {
136+
version: 1,
137+
entries: {
138+
"/abs/legacy.jpg": {
139+
sha256: sampleRef.sha256,
140+
assetRef: sampleRef,
141+
cachedAt: Date.now() - 86_400_000,
142+
},
143+
},
144+
};
145+
writeFile(join(cacheDir, "assetMap.json"), JSON.stringify(v1File), "utf-8");
146+
const cache = new BuildCache(root);
147+
assert.equal(cache.size(), 0, "v1 entries should be discarded");
148+
assert.equal(
149+
cache.get("/abs/legacy.jpg", sampleRef.sha256),
150+
null,
151+
"lookup against the pre-bump entry must miss so the uploader re-fetches a fresh AssetRef",
152+
);
153+
});
154+
155+
it("round-trips v1.54 fields (blurhash_data_url + asset_schema) through set/get", () => {
156+
const v154Ref: AssetRef = {
157+
...sampleRef,
158+
metadata: { tag: "hero" },
159+
image_format: "jpeg",
160+
image_info: { has_alpha: false, color_space: "srgb" },
161+
image_exif: null,
162+
image_exif_policy: "strip",
163+
blurhash_data_url: "data:image/png;base64,iVBORw0KGgoAAAA",
164+
asset_schema: "v1.54",
165+
};
166+
const cache = new BuildCache(root);
167+
cache.set("/abs/hero.jpg", v154Ref.sha256, v154Ref);
168+
169+
// Same-process read.
170+
const same = cache.get("/abs/hero.jpg", v154Ref.sha256);
171+
assert.deepEqual(same, v154Ref);
172+
173+
// Roundtrip via the on-disk file (catches accidental field stripping
174+
// in flush()/load(), which is the actual class of bug v2 prevents).
175+
const fresh = new BuildCache(root);
176+
const reloaded = fresh.get("/abs/hero.jpg", v154Ref.sha256);
177+
assert.deepEqual(reloaded, v154Ref);
178+
assert.equal(reloaded?.blurhash_data_url, v154Ref.blurhash_data_url);
179+
assert.equal(reloaded?.asset_schema, "v1.54");
180+
});
118181
});

astro/src/cache.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@
1010
* to match the cached entry. A rename invalidates the OLD path's entry on
1111
* the next build (when no `<Image>` reference points at it anymore); a content
1212
* change at the same path invalidates the entry via the sha mismatch.
13+
*
14+
* **Bumping `CACHE_SCHEMA_VERSION` — required discipline whenever AssetRef
15+
* gains a field.** The cache stores AssetRef objects verbatim by source SHA.
16+
* If the SDK / gateway starts returning a new field but the cache version
17+
* doesn't change, every build with unchanged source bytes is a cache HIT and
18+
* the new field never makes it into the cached AssetRef — silently producing
19+
* stale manifests that look correct (legacy fields populate) but lack the
20+
* additions. This was the v1.54 `blurhash_data_url` / `asset_schema` bug.
21+
*
22+
* Bump on:
23+
* - any new AssetRef field the gateway can emit (even an optional one)
24+
* - any new derived field the SDK or this package adds at adapt time
25+
* - any change to the `CacheEntry` / `CacheFile` shape
26+
*
27+
* Bumping is non-destructive: stale entries are dropped silently and the
28+
* next build re-uploads (or, more commonly, gets a CAS dedupe at the
29+
* gateway and re-receives a fresh, fully-populated AssetRef).
1330
*/
1431

1532
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -18,7 +35,15 @@ import type { AssetRef, CacheEntry, CacheFile } from "./types.js";
1835

1936
const CACHE_DIR_REL = "node_modules/.run402";
2037
const CACHE_FILE_REL = "node_modules/.run402/assetMap.json";
21-
const CACHE_SCHEMA_VERSION = 1;
38+
/**
39+
* Bump whenever AssetRef gains a field (see file header for full discipline).
40+
* History:
41+
* - 1 — initial v1.49 AssetRef shape.
42+
* - 2 — v1.54 AssetRef additions (`blurhash_data_url`, `asset_schema`)
43+
* plus the v1.50 fields (`metadata`, `image_format`, `image_info`,
44+
* `image_exif`, `image_exif_policy`) the local type now mirrors.
45+
*/
46+
const CACHE_SCHEMA_VERSION = 2;
2247
const GITIGNORE_LINE = "node_modules/.run402/";
2348

2449
export class BuildCache {

astro/src/types.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
/**
22
* Shared types for `@run402/astro`.
33
*
4-
* Mirrors the v1.49 AssetRef shape so the package doesn't have to deep-import
4+
* Mirrors the current AssetRef shape (v1.49 base + v1.50 metadata/EXIF +
5+
* v1.54 shape-contract fields) so the package doesn't have to deep-import
56
* SDK internals (the SDK doesn't currently export `AssetRef` from `./node`,
67
* only via `./` — but we want to stay self-contained so the package still
78
* type-checks if the SDK refactors its public surface).
89
*
9-
* Keep this file in lockstep with the v1.49 `internal.blob_image_variants`
10-
* shape and `services/asset-slice.ts` `ResolvedAssetRef`.
10+
* **Lockstep + cache discipline:** keep this file in lockstep with the
11+
* gateway's `services/asset-slice.ts` `ResolvedAssetRef` shape. When you
12+
* add a field here, ALSO bump `CACHE_SCHEMA_VERSION` in `./cache.ts` —
13+
* the build cache stores AssetRefs verbatim by source SHA, so a forgotten
14+
* version bump means stale builds silently drop the new field (see the
15+
* cache.ts header for the full story).
1116
*/
1217

1318
/** A single pre-encoded variant entry returned by the gateway. */
@@ -20,7 +25,26 @@ export interface AssetVariant {
2025
sha256: string;
2126
}
2227

23-
/** The full v1.49 AssetRef including image-intrinsic fields. */
28+
/**
29+
* The full AssetRef returned by `r.assets.put` and stored verbatim in the
30+
* build cache + emitted into `dist/_assets-manifest.json`.
31+
*
32+
* Field generations:
33+
* - core (v1.45): `key`, `sha256`, `size_bytes`, `content_type`, `url`,
34+
* `cdn_url`, plus `immutable_url`, `cdn_immutable_url`, `etag`, `sri`.
35+
* - v1.49 image-intrinsic: `width_px`, `height_px`, `blurhash`,
36+
* `variant_spec_version`, `display_url`, `display_immutable_url`,
37+
* `variants`.
38+
* - v1.50 metadata + EXIF: `metadata`, `image_format`, `image_info`,
39+
* `image_exif`, `image_exif_policy` — `null` on non-image uploads
40+
* (the wire shape is widen-to-null, not omit), `null` for fields the
41+
* pipeline couldn't compute.
42+
* - v1.54 shape contract: `blurhash_data_url`, `asset_schema` — omitted
43+
* entirely (not `null`) on pre-v1.54 uploads; the omit pattern is what
44+
* `<Run402Image>` strict-mode keys off to skip legacy rows.
45+
*
46+
* Adding a field here? Bump `CACHE_SCHEMA_VERSION` in `./cache.ts`.
47+
*/
2448
export interface AssetRef {
2549
key: string;
2650
sha256: string;
@@ -48,8 +72,33 @@ export interface AssetRef {
4872
display_jpeg?: AssetVariant;
4973
}
5074
| undefined;
75+
76+
// v1.50 metadata + EXIF policy + image intrinsics. Wire shape is
77+
// widen-to-null on non-image uploads (NOT omit) — keeps the JSON inventory
78+
// wire-shape stable.
79+
metadata?: AssetMetadata | null;
80+
image_format?: string | null;
81+
image_info?: Record<string, unknown> | null;
82+
image_exif?: Record<string, unknown> | null;
83+
image_exif_policy?: ExifPolicy | null;
84+
85+
// v1.54 shape-contract fields (omitted entirely — NOT null — on pre-v1.54
86+
// uploads). Surfaced so `<Run402Image>` placeholder rendering +
87+
// schema-filtered strict-mode work without a DB roundtrip.
88+
blurhash_data_url?: string | null;
89+
asset_schema?: "v1.49" | "v1.50" | "v1.54" | null;
5190
}
5291

92+
/** Caller-supplied metadata block. ≤4 KB serialized; leaf values may be
93+
* `string | number | boolean | string[]`. The gateway echoes this back
94+
* verbatim on AssetRef. */
95+
export type AssetMetadata = Record<string, string | number | boolean | string[]>;
96+
97+
/** EXIF retention policy applied to an image upload. `"strip"` removes
98+
* the EXIF block at upload time; `"preserve"` keeps it; `"redact"` keeps
99+
* the structural keys but blanks GPS / serial-number / lens-info fields. */
100+
export type ExifPolicy = "strip" | "preserve" | "redact";
101+
53102
/** Options accepted by the `run402()` integration factory. */
54103
export interface Run402AstroOptions {
55104
/** Run402 project ID. Defaults to `process.env.RUN402_PROJECT_ID`. */
@@ -173,8 +222,10 @@ export interface CacheEntry {
173222

174223
/** Shape of the on-disk cache file. */
175224
export interface CacheFile {
176-
/** Cache schema version — bump on incompatible AssetRef shape change. */
177-
version: 1;
225+
/** Cache schema version. Bump in lockstep with `CACHE_SCHEMA_VERSION` in
226+
* `./cache.ts` whenever `AssetRef` gains a field — see that file's header
227+
* for the full discipline. v1 → v2 bump: v1.50 + v1.54 AssetRef fields. */
228+
version: 2;
178229
entries: { [absolutePath: string]: CacheEntry };
179230
}
180231

0 commit comments

Comments
 (0)