0eb3c86839501db152f7e99a9810cda29a7339f6
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
11c2713139 |
feat: let Studio create the topics and projects publishing requires
Publishing needs a topic and nothing could create one. The backend now owns that surface; this is its consumer — the management contract vendored, a gateway over its nine operations, and one Studio screen that lists, creates, and deletes topics and projects. The screen adds no CSS. It reuses the classes the working-copy list already uses, so it inherits Studio's spacing, type, and colour rather than growing a second visual vocabulary beside them. Scope stops at list/create/delete: renaming, phase changes, and visibility are implemented in the backend and declared in the contract, but their screens are a separate design. Two real defects surfaced while making the public port async, and both would have shipped: The search page and the header search dialog shared a query key. With an empty query, `["tech-log","search",""]` was identical for both, so react-query handed one surface the other's cache — different shapes — and the page died reading a field that was not there. Keys now name the surface. The explore filter's selects are uncontrolled and read `defaultValue`, which React applies once. Their options arrive later now, so the first render had nothing to match and the value stayed empty: a topic in the URL no longer showed as selected. The form key includes whether the catalog has arrived, so it remounts with the options present. Controlled inputs would be the other answer, but this form submits to build a URL — the URL owns the value. The route brought its own bookkeeping: a build chunk, a manual accessibility evidence file, and the CI artifact baseline that counts them. The gate pins a digest of its own shape precisely so a new route cannot slip in without that count being reviewed. Test harnesses that render public screens now assemble the query providers and await the settled paint, because the screens they render became async. |
||
|
|
d84b57bb3f |
fix: let the mock's dependency revision observe the Asset store
`createMockStudioGateway`'s default `dependencyRevision.current()` returned a literal constant and never consulted `dependencies.assets`, so the staleness guards in `createStudioPreview` and `publishStudioDocument` could not fire for an asset-store mutation between validate and preview. The live case: a document references an evidence key with `alt=""` and the store holds only a `decorative: true` Asset for it, so validation is correctly VALID with zero issues. A newer `decorative: false` Asset then wins that key. Preview succeeds, the figure resolves to `decorative: false, alt: ""`, and publish snapshots it verbatim -- a meaningful image with no accessible name, validated clean, with nothing anywhere reporting an error. The default now folds the Asset store into the revision. Each Asset is reduced to the fields the mock's own validation and projection read -- identity and resolution order (`id`, `assetKey`, `updatedAt`), resolvability (`managementStatus`, `publicPath`), the alt rule (`decorative`, `altText`), and what the published `ResolvedAsset` carries (`mediaType`, `width`, `height`) -- canonicalized with `stableStringify`, sorted, and folded into a 128-bit FNV-1a digest. Sorting the canonical strings is what makes it order-independent, which this mock's reproducibility across the suite depends on. It is a projection rather than the whole record because the excluded fields cost sensitivity without buying any. `usageCount` is the clearest: it counts referencing documents, so on a real backend publishing any document that uses an Asset would invalidate every other author's in-flight validation, while changing nothing the validator or renderer reads. An empty store still reports the bare catalog constant -- that is the world the seeded fixtures were validated against, and `fixtures.ts` now shares the one definition rather than retyping the literal. `findResolvableAsset` is untouched: it is a single-point-in-time predicate and is correct as it stands. Seeing a change *between* two points is the revision's job. A caller-supplied `dependencyRevision` still wins outright. The asset-picker test that reached `failureOf`'s `ContentFormatError` branch did so only because the revision could not move; it now pins its own revision to keep reaching the projection, and asserts the problem detail so the two 409 paths cannot be confused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b2f577ef49 |
test: stop DOM-element assertions from OOM-killing the worker
`node:assert` builds its `AssertionError` eagerly, running `util.inspect` over both operands with `depth: 1000`, `getters: true` and `maxArrayLength: Infinity`. A React-rendered DOM element carries `__reactFiber$*` / `__reactProps$*` as own enumerable properties, and that fiber graph re-expands once per traversal path, so inspecting a single rendered element allocates without bound. Measured on the Asset Library heading: depth 6 = 1.3MB, 8 = 7.8MB, 10 = 36MB, 12 = 135MB -- at Node's depth 1000 the worker dies before any `AssertionError` exists. The damage is not the crash, it is the disguise. Equality assertions only inspect their operands on failure, so these sites stayed invisible while green and detonated exactly when the behaviour they guard regressed -- reporting as `worker exited unexpectedly` with a truncated count (`8 passed (13)`) and no failing test named. Breaking the delete-path focus restoration in `asset-library.tsx` reproduced it: 29.5GB anon-rss and the system OOM killer, or a V8 heap abort in 3s under a 512MB cap. The same regression now fails in 1.15s with `expect(element).toHaveFocus()` naming both the expected heading and the `<body>` that took focus instead. `expect` is not affected -- vitest prints and diffs DOM nodes through pretty-format's DOM plugin, which reads tag/attributes/children and never touches the fiber -- so every unsafe site converts to a matcher: `toHaveFocus()` for the three focus comparisons, `not.toBeInTheDocument()` for the sixteen `assert.equal(queryBy..., null)` absence checks, which are equally lethal (proved separately: element-vs-null inspects the element). Three layers so this cannot come back: - the 20 live sites in asset-library/asset-picker now use matchers; - `test-assertion-boundary/no-element-operand-equality` fails `pnpm lint` when a DOM-element expression reaches `node:assert` equality, resolving local bindings and exempting the forms that cannot fail with an element in hand (`assert.notEqual(el, null)`, `el.textContent`); - a 2048MB worker old-space ceiling in `vitest.config.ts` bounds any future runaway to a legible `Reached heap limit` abort in seconds instead of an OOM-killed machine (heaviest suite peaks near 1.3GB RSS). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9950cb9d6b |
fix: clear the decorative flag when the upload dialog is handed a different file
The twin of the previous commit, with a sharper consequence. `decorative` does not merely describe the previous image, it *exempts* it: `validate-working-copy.ts`'s EVIDENCE_ALT_REQUIRED reads `Asset.decorative`, so a flag inherited from a discarded divider lets a meaningful diagram publish with no accessible name at all -- the check passes rather than catching it. Stale alt text ships a wrong description; stale `decorative` ships none. Same terminal-state retry path: tick 장식용 for `divider.png`, have it rejected, pick `sequence.png`, upload -- and `sequence.png` shipped as decorative with `altText: undefined`. Resetting it also re-enables the alt input, so the post-selection focus call no longer has to ask whether it would land on a disabled control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1f2cba79e9 |
fix: clear alt text when the upload dialog is handed a different file
The file input's `onChange` reset `state` to `IDLE` and left `altText` untouched. On success that is harmless -- the dialog unmounts. But REJECTED, QUARANTINED and TRANSPORT_FAILED all leave it mounted with the file input re-enabled, and that is precisely the retry path: upload `db-schema.png` described as "DB 스키마", have it quarantined, pick `sequence.png`, upload -- and `sequence.png` shipped described as "DB 스키마", passing EVIDENCE_ALT_REQUIRED and publishing with a caption about a different image. Cleared on every file selection rather than only after a failure: alt text describes one image, and "which file is this describing" has one honest answer per selection. `submit()`'s existing ALT_REQUIRED refusal turns the emptied field into a stop rather than a silent omission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f69edb633d |
feat: search the server from the Asset Picker without shrinking preview's catalog
The Picker asked for `{ managementStatus: "READY", limit: 50 }` and
ignored `nextCursor`, so the 51st-oldest READY asset onward could not be
inserted at all. It sits inside the editing flow, where scrolling a long
list is the wrong interaction, so it gets search rather than a "더 보기"
control -- and it still loads a first page, because an empty panel until
you type is hostile to an author reaching for the asset they uploaded a
minute ago.
The trap this creates is the substance of the change. The editor screen's
Asset array feeds two consumers with opposite needs: the Picker's
*displayed* list, which a search must narrow, and Instant Preview's
*resolution catalog*, which a search must never narrow -- its gate
rejects any key no loaded asset backs. Handing search results straight to
the screen's `setAssets` would blank previously-inserted evidence figures
the moment the author typed a query.
They are kept apart by making the screen's callback additive by
construction rather than by convention: `mergeAssetCatalog` (domain,
beside `findResolvableAsset`) can only grow the set, and both writers --
observed pages and fresh uploads -- go through it. The prop is renamed
`onAssetsObserved` so the contract reads as "what the Picker saw", not
"what to show"; the replacing version was one `setAssets` reference away
and looked correct.
A key backed by neither the first page nor the current results is still
unresolvable. That is the known deferred limitation, not this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6085af51b6 |
fix: collect alt text and a decorative flag when uploading a Studio Asset
The upload dialog sent `{ file, kind }` only, though `UploadAssetForm` and
`asset-upload-transport.ts` both carry `altText`/`decorative`. Every asset
uploaded through the flagship authoring flow therefore landed as
`altText: null, decorative: false`, and the directive `case-fields.tsx` and
`asset-picker.tsx` build from `asset.decorative ? "" : (asset.altText ?? "")`
could only ever be `alt=""`. The document parsed and previewed correctly and
then failed publish validation with EVIDENCE_ALT_REQUIRED, recoverable only
by hand-editing raw Markdown -- the exact thing the Picker exists to prevent.
The dialog now stages the file instead of uploading on selection, and carries
a decorative checkbox plus an alt-text field (focused as soon as a file is
chosen, submitting on Enter). A decorative asset never demands alt text; a
meaningful one is refused with a stated reason rather than a disabled button.
Insertion is unchanged: both call sites already read the Asset, so they now
insert what it actually carries.
Three new tests drive the whole loop -- upload through the real MOCK
composition, auto-insert, save, validate, preview -- and assert the result is
publishable. The pre-existing loop test no longer needs its hand-edit
workaround.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
073fda87eb |
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> |
||
|
|
783e9b2cf1 |
fix: rebuild gate 1 from the resolver's own predicate, not a catalog lookup
Fix round 3 (review of
|
||
|
|
7ff9728a5c |
fix: un-collapse evidence gate 1 from gate 2 to close a raw-Error escape
Fix round 2 (review of
|
||
|
|
98649585e6 |
fix: generate fresh upload idempotency keys and reconcile mock preview/validation on evidence keys
Fix round 1 (review of
|
||
|
|
54d9bf9120 |
feat: insert evidence directives from the TechLog asset picker
Adds an Asset Picker and upload dialog to the CASE editor so authors can insert `:::evidence` directives that reference backend assets, and opens projectWorkingCopy's two evidence gates so Instant Preview accepts a key backed by a freshly loaded READY asset instead of only the one hardcoded legacy key. The editor screen now owns the loaded Asset list so the Picker, the upload dialog, and Instant Preview all read the same array, and a freshly uploaded asset appears in the preview without a refetch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |