`resolution` leaves the input's required list, so the generated type makes it
optional. The editor already sent null for an unresolved question; nothing in
the frontend changes behaviour.
Two things an author could not control from Studio.
The profile's "주요 관심 주제" was four strings in the JSX. Creating or removing
a topic in Studio changed nothing, and correcting the list meant a rebuild and
a redeploy. It now renders the published topic list. The old literal opened
with "Backend Architecture", which no record in the catalogue actually carries
— the profile was advertising a topic that did not exist, and nothing could
have caught that while the list lived in the markup.
The working-copy list gained a delete control. It routes by kind because the
contract and the storage both do: Case and Reference share one table split by
type, Question is its own. Decision has no delete — its lifecycle is accept,
reject, supersede, which records what happened rather than erasing it — so the
control does not appear for it.
The list summary carries no version, so deletion reads the working copy first
and uses the version it finds. A stale version from a list left open should
fail as a conflict, not delete whatever is there now.
The public site shipped a Releases page and a footer link to a release, and
neither could ever have content: the read path existed, the write path did not.
This adds the seven release operations to the contract contribution and the
gateway, and a Studio screen that can actually write one.
The editor is six markdown fields rather than one, because that is what the
contract models and what a release note is — why, what, what a reader notices,
what it leaves in the code, how it was verified, what is still open. Publishing
is separate from saving: a draft saves in any state, but the public query keys
on PUBLISHED alone, so publish is where completeness is demanded.
No new CSS. The screen reuses the document editor's field classes and the
working-copy list's row classes, so it inherits Studio's spacing and type
instead of introducing a second look.
The footer previously linked `/releases/0.1.0` — a version that did not exist,
so the link 404'd, and one that would have gone stale at 0.2.0 anyway. It now
points at the changelog index, which is the only place that knows what the
latest release is and which reads correctly when there are none.
Route inventory, navigation order, message catalog, manual accessibility
evidence, artifact baseline, and the pinned gate-shape digest all move with the
new route. The digest was recomputed by first reproducing the previous constant
from the previous gates.json, so the computation is known to be the one it was
pinned under.
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.
Only the recorded source revision moves (55a9599 -> b98eaf9). The vendored
specs and the generated types are byte-identical, which is the useful part:
the envelope redefinition landed on a merge commit, not a schema change, so
nothing downstream has to move with it.
The public surface — 17 of the 28 registered routes — reads from a 29KB
TypeScript fixture and never touches the backend. `TECH_LOG_STUDIO_SOURCE`
only ever switched the Studio gateways; `publicContent` was wired to the
static adapter unconditionally, so no configuration could make the public
site show published content. This is the first half of closing that: the
contract and the switch, with the adapter still to come.
The generator now vendors both canonical contracts instead of one. They are
independent — different services on different schedules — so each carries
its own digest and operation list, and updating one leaves the other's drift
gate quiet.
`TECH_LOG_PUBLIC_SOURCE` is deliberately a second flag rather than a rename
of the Studio one. The combination that matters right now is exactly the one
a single flag cannot express: the authoring backend is live while the public
read API does not exist yet. production stays on MOCK for that reason —
pointing it at HTTP today would empty the live site — and moves when the
backend serves /api/v1/public.
Also records the compatibility evidence the registry gate wanted for the
Studio access change in fff5e6f. That gate has been failing since, which is
on me: the change was real and breaking, and it shipped without the note
explaining that route ids and schemas are untouched and only the access
classification moves.
canonical studio-v1.yaml(tech-log-design-package b20d7a2)이 VALIDATION_FAILED를
DOCUMENT_VALIDATION_FAILED로 개명했다 — 스켈레톤 전역 OperationalError.
VALIDATION_FAILED(400)와 code 문자열이 충돌해 같은 code가 두 HTTP status를
갖던 문제를 해소한다.
- generate:tech-log-contract로 vendor된 계약·생성 타입·canonical-source.json 재생성
- STUDIO_ERROR_CODES(손수 유지되는 계약 미러)의 해당 항목 개명
계약 밖 코드는 STUDIO_UNAVAILABLE로 접히므로 이 배열이 계약과 어긋나면 안 된다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
design-package 6a85d81이 ResponseMeta.page를 순수 null 타입에서
[object, null]로 넓혔다(openapi-generator가 순수 null 타입을 다루지
못해 생긴 계약 결함 수정, wire 의미는 그대로 — page는 여전히 항상
null). page의 생성 타입이 `null`에서 `{ [key: string]: unknown } | null`로
넓어졌다 — 의도된 변경이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
studio-v1.yaml v3.0.0(ADR-006)에 맞춰 계약을 재생성하고, 성공은
{success,data,meta}, 실패는 {success,error,meta} 봉투를 전송 경계에서
언랩하는 envelopeData/envelopeError validator를 도입한다. 앱·도메인
계층은 기존과 같은 payload/ProblemDetails 모양을 계속 받고,
StudioGateway 포트 시그니처는 무변경이다.
- tech-log-studio-contract-contribution.ts: envelopeData/envelopeError
도입, 18개 operation의 outputValidator를 passthrough에서 envelopeData로
교체
- studio-error-mapping.ts: 봉투 오류의 status(항상 0)를
outcome.metadata.status로 덮는다. SafeResponseMetadata.status가
실제 필드명이며(httpStatus 아님) PROBLEM outcome에서 필수 필드다
- contract.ts: 삭제된 ProblemDetails 생성 스키마를 손으로 유지 — 앱
계층·mock 게이트웨이가 그 모양을 계속 소비한다
- asset-upload-transport.ts: multipart 업로드는 일반 계약 런타임을
거치지 않는 별도 seam이지만 같은 wire 봉투를 쓴다 — envelopeData/
envelopeError를 재사용해 이 경로도 언랩한다 (브리프 파일 목록 밖의
발견, report에 기록)
- 테스트: 신규 studio-envelope-unwrap.test.ts(TDD) + 봉투 뼈대를 직접
만드는 기존 테스트(asset-upload-transport, studio-csrf-composition,
contract-generation)를 봉투 형태로 갱신
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registers the TECH_LOG_STUDIO_SESSION SAME_ORIGIN_COOKIE auth profile and
declares the tech-log-studio-http-v1 contribution covering all 18 JSON
Studio operations (everything except the multipart uploadStudioAsset),
built from two shared builders (safeOperation/keyedOperation) so every
KEYED command gets IDEMPOTENCY_REPLAY recovery and a zero retry budget,
and every SAFE read gets a plain retry budget, without repeating the
declaration shape 18 times.
Rescopes the canonical package identity from "tech-log-studio-contract"
to "@tech-log/studio-contract" (generator, canonical-source.json,
contract-generation.test.ts) because the platform's contribution
composer requires an npm-scoped packageId; the unscoped form failed
composition. Updates the release-manifest test fixture, which hardcoded
an empty expected contract set, to derive its expected packages from the
real installed set now that TechLog is always installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vendors the canonical studio-v1.yaml, generates types via an isolated
`pnpm dlx` toolchain (openapi-typescript needs TypeScript 5's classic
compiler API; this repo pins TypeScript 7.0.2 per VD-01, whose root
export has none), and adds an offline drift gate that checks the
vendored yaml/generated types/canonical-source.json against each
other without touching the sibling design-package repo or the network.
Regenerating from canonical surfaces real, new required fields on
existing schemas (WorkingCopyDetail.nextAction, PreviewDetail/PublicPreview
.dependencyRevision, StudioDashboard.totals.needsValidation,
PublicationSnapshot.contentFormatVersion/rendererContractVersion) and a
new required EvidenceFigureBlock.asset. The mock gateway and fixtures
are updated to satisfy the former; the latter exposes a real authoring-
vs-rendering conflation in the content-format parser (it declared its
output as the server's fully-resolved PublicRenderModel type, which it
has no asset catalog to satisfy). Split that boundary: the parser now
produces an authoring block type omitting the resolved asset, and each
of its three consumers (the mock gateway, the Studio instant preview,
and the static Case demo page) attaches the resolved descriptor from
its own asset source through a shared, pure domain-level resolver.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>