fix: collapse the evidence-key gate and resolver into one decision
Three fix rounds each rebuilt the gate as a separate expression that merely agreed with the resolver on the inputs that round's tests used. Different expressions cannot agree in general, so the defect class stayed open while each reported instance closed. `findResolvableAsset(assets, key)` is now the single place that decides which Asset an evidence key resolves to. Every gate is `Boolean(findResolvableAsset(...)) || legacyKey(key)` via one shared composition, and every resolver returns what it returns: - validate-working-copy: the key gate and the decorative lookup (a last-wins Map against the resolver's first-wins find, so alt could be judged against a different Asset than the one rendered) - adapters/mock/project-public-render-model: gate and resolver - instant-preview: gate and descriptor resolver - createAssetCatalogResolver: the pixels, a fourth expression nobody had listed -- one Asset's caption could sit over another Asset's image Duplicate assetKeys are a contract violation but reachable through a paged list, so the choice is total and order-independent: newest updatedAt wins, tie-broken by id. InstantPreview's gate is no longer looser than the others. The un-loaded-asset case it was loosened for blanks either way; all the looseness bought was catalog-only keys rendering an empty gap with no message while validation said EVIDENCE_UNSUPPORTED. The test that pinned that divergence now asserts the consistent behaviour, and the false comment claiming a fix that did not exist is gone. idempotent() now maps a deterministic content failure to VALIDATION_STALE/409 instead of offering a retry that fails identically, and no longer caches uncharacterized internal failures -- reporting one as retryable while freezing it in the ledger meant the retry could never re-run. Adds tests/features/tech-log/evidence-key-agreement.test.ts: 144 adversarial (asset list, key) combinations asserting the agreement itself rather than examples. It reported 53 disagreements against the previous code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
783e9b2cf1
commit
073fda87eb
@@ -1,4 +1,5 @@
|
||||
import type { Asset } from "../../../contracts/studio/contract.ts";
|
||||
import { findResolvableAsset } from "../../../domain/content-format/asset-evidence-catalog.ts";
|
||||
import type {
|
||||
EvidenceAsset,
|
||||
ResolveEvidenceAsset,
|
||||
@@ -92,21 +93,22 @@ function resolveWith(
|
||||
return legacy ?? MISSING_EVIDENCE_ASSET;
|
||||
}
|
||||
|
||||
/** Instant Preview: resolves against the Asset list the editor has loaded. */
|
||||
/**
|
||||
* Instant Preview: resolves against the Asset list the editor has loaded.
|
||||
* `findResolvableAsset` is the single decision -- the same one the preview's
|
||||
* key gate and its block-descriptor resolver make -- so the image shown here
|
||||
* is always the Asset the gate accepted. Building a local `Map` here instead
|
||||
* was last-wins where the descriptor resolver was first-wins, which meant two
|
||||
* READY Assets sharing one `assetKey` could put one Asset's caption over
|
||||
* another Asset's image.
|
||||
*/
|
||||
export function createAssetCatalogResolver(
|
||||
assets: readonly Asset[],
|
||||
): ResolveEvidenceAsset {
|
||||
const byKey = new Map<string, Asset>();
|
||||
for (const asset of assets) {
|
||||
// QUARANTINED/REJECTED must never render on any surface.
|
||||
if (asset.managementStatus !== "READY") continue;
|
||||
byKey.set(asset.assetKey, asset);
|
||||
}
|
||||
|
||||
return (key: string) =>
|
||||
resolveWith(key, (lookupKey) => {
|
||||
const asset = byKey.get(lookupKey);
|
||||
if (!asset || asset.publicPath === null) return undefined;
|
||||
const asset = findResolvableAsset(assets, lookupKey);
|
||||
if (!asset) return undefined;
|
||||
return {
|
||||
assetId: asset.id,
|
||||
assetKey: asset.assetKey,
|
||||
|
||||
Reference in New Issue
Block a user