Conversation
Types are generated from the worker's schema rather than hand-written a third time, and the vocabulary is fetched at runtime so the editor form cannot offer an option the validator rejects. A runtime guard makes the server-only rule a guarantee instead of a convention: DEFINITIONS_WRITE_TOKEN cannot reach a browser bundle without throwing. The guard is testable both ways -- definitions.test.ts runs under @jest-environment node, and definitionsServerOnly.test.ts asserts the module refuses to load under jsdom.
Trims as columns, attributes as rows, and the two contract invariants enforced in the model rather than left to the UI: an attribute lives on the template or on the trims and never both, and clearing removes the key rather than storing an empty value. Editing one trim's value for a shared attribute pushes it down first, which is what makes divergence expressible at all. Tested against the real ten-trim Camry, including the regression that powertrain_type and fuel_tank_capacity_gal are the divergent rows -- the pair the production record blended.
Every difference between what someone types and what gets stored produces a Normalisation the editor shows before save -- a reparsed number, trimmed whitespace, a cleared attribute that removes the key. The validation mirror uses the worker's own error strings so one fault is never described two ways; it is explicitly not authoritative, and the save path still surfaces the worker's 422 verbatim. Fixtures move to src/utils/__fixtures__/. Jest's default testMatch treats everything under a __tests__ directory as a suite, so a fixture module there fails as a suite with no tests.
Trims as columns, attributes as rows, agreement muted and difference at full contrast -- the inverse of normal form emphasis, so the pair of attributes the production Camry record blended is the pair the eye lands on. The divergence rail offers Move to shared at one distinct value because that is the correct action under the template-or-trim rule, and a trim with no effective selector is marked in the header rather than discovered at save.
Three states, all real. A definition with no template says so rather than erroring -- the import has not run in production and 5,152 of the emitted set carry no attributes. An id the schema cannot accept says that instead, because 137 of them are live today and subaru_tribeca-(ny/nj)_2008 is one; those are never asked about, since the answer would be a misleading 404.
The worker has one shared WRITE_TOKEN and takes author on trust. This route holds the token server-side, identifies the caller from the session Console already has, stamps author from it, and refuses a body that tries to name its own author or version rather than stripping it quietly. Entitlement follows risk: creating a template harms nobody, editing one that 4,212 vehicles resolve to re-describes all of them. hardwareTemplateId is curator-only at every tier. The If-Match forwarded is the client's, never the version this route just read -- the latter would rebase a stale editor onto whatever landed while it was open and return 200.
…anner Three places the editor leaves a trace instead of acting quietly: what it stored that differs from what was typed, why a selector-less trim will be refused before save rather than at it, and what the caller is not allowed to do and why.
Publish is disabled while the draft would fail the worker, and the fault is named in the worker's own words. The precondition is the version that was loaded, held apart from the draft so it cannot move underneath it. A conflict explains what happened and offers a reload -- it never retries over the top of somebody else's version.
Publishes immediately and attributed, because nothing points at a template nobody has created. If-None-Match makes an id that already exists a conflict with a link to it rather than a silent overwrite.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The contract has three generated mirrors (Console TS, the worker's TS, dd-api's Go). Generating them without a guard is hand-writing them one step removed: the copies still drift and nothing says so. The worker already fails `npm test` on drift; Console had the generator and no check. Two changes make that possible. The generator now reads a vendored copy of template.schema.json rather than fetching the deployed one: a generator whose input can change underneath it produces output nobody can reproduce, and drift is only reviewable when it lands in a diff. `--from <url|path>` rewrites the vendored schema alongside the types, so a contract change shows both files moving in one commit. `--check` then fails when the committed output is stale, wired into `prebuild` so a Vercel build refuses to ship stale types, and covered by a test that also asserts the guard fails on a perturbed file -- a check that cannot fail is the defect it was written to prevent.
…emplate The worker takes hardwareTemplateId on each trim as well as at the top level (TRIM_KEYS carries it), and templateGrid.toPayload forwards trim overrides in the PUT body. The curator-only gate compared only the top-level value, so a non-curator whose body left that untouched and set trims[i].hardwareTemplateId was let through with a 200 and the worker stored it. The route test covered only the top-level case. hardwareTemplateIdChanged in templateEntitlement now compares the top-level value and the multiset of (trim name, hardwareTemplateId) pairs between the submitted body and the stored template. For a non-curator any difference is refused with the existing 403: a changed trim value, a value on a trim the stored template does not carry one for, a stored value that goes away with its field or with its trim, and any value at all on a create, where there is no stored template to compare against. Trims that come, go or are renamed without a hardwareTemplateId are still vehicle description and pass. Pairs are compared per occurrence so a duplicated trim name cannot hide a value. The PUT route is the only write route; a create is a PUT against an id with no stored template, which the same comparison covers.
…t question identity-api reports a GraphQL failure as HTTP 200 with `errors` set and `data` null. identity() checked only resp.ok and returned `json.data ?? null`, so countMintedVehicles read the failure as zero vehicles. Zero vehicles on a template with no author is the open tier: during an identity blip any signed-in account could publish over toyota_camry_2020 and its 4,212 minted vehicles. `errors` was never inspected. identity() now throws an IdentityError when the response is not ok, when `errors` is non-empty, or when `data` is missing, and countMintedVehicles throws when the count is not a number. resolveEntitlement catches a failed count, and a failed Manufacturer NFT holder lookup, and returns a new 'unavailable' kind: canPublish false, canSetHardwareTemplateId false, and a reason that says to try again. A curator gets the same answer. A create is unaffected, since nothing can point at a template that does not exist yet. The PUT route answers 'unavailable' with 503, not 403, because nothing was refused and the client should retry. entitlementFor reports it to Sentry, since a returned state never reaches captureException. The GET route now serves the editor read only during an outage instead of failing with a 502. The banner titles the state "Access could not be verified".
Per-trim cells rendered only for a row already scoped to a trim, and nothing in the UI moved an attribute into that scope. The divergence rail offered "Move to shared" and nothing the other way, so scope could only ever travel trim -> shared: pushDownToTrims was correct and reachable only from inside setTrimValue, which itself had no call site outside the per-trim branch, so it could never fire. A curator could preserve a divergence the extraction found but never author or correct one -- the capability this migration exists to add. The rail gains the inverse offer, "Set per trim", in the same place and the same pill as its opposite, gated by canSplit on the same rule canLift uses: a single-trim template says the same thing either way, so the offer would be noise there. On a shared row it brings the value down onto every trim, which is what the worker's "on both the template and trim" rule requires before one trim can disagree. On a row nothing sets, scope has no data to live in yet, so the grid holds the opened rows in view state until the first value is typed.
…tract
Every create was a guaranteed 422. The create form builds
`manufacturer: { slug, name }`, and worker 7bbc0db made tokenId a
required positive integer -- a field the form has no input for and must
not have, since it is the Manufacturer NFT id and identity owns it. The
drift guard could not see it either: gen-template-types.mjs --check
compares the generated types against the VENDORED schema, never against
the worker, and the vendored copy was two worker commits stale
(733272b, 2026-08-30), so CI stayed green on a body the worker refuses.
The PUT route now resolves the slug through manufacturerOwner() -- which
it already imported -- and stamps the token id where `author` is
stamped, overruling any value the body carries rather than refusing it,
because the editor round-trips the manufacturer object it loaded. A slug
identity does not know is answered here, naming the slug, instead of as
the worker's message about a field the form does not have; an identity
outage is a 503, the same "nothing was decided" the entitlement tiers
use, not a stamped guess.
Re-vendored src/types/generated/template.schema.json from
definitions-worker fb81596 (schema last changed at 97d364d) and
regenerated the types: manufacturer.tokenId becomes required, and
selectors.vinPattern picks up minLength 1 and the RE2 rules. A new test
reads the vendored schema directly and fails if the token id stops being
required, since the types check alone cannot see a stale vendor.
When the stored template read as null the route discarded the client's If-Match and sent a create-only precondition instead. A save against a template someone had deleted -- through the worker's DELETE, dd-api or dimo-admin -- therefore committed as a create: checkPrecondition(null, null) passes, the stale draft resurrected the deleted template, and the editor was told 200. That is the lost update the worker's CAS exists to make impossible, and the comment above the branch already promised the precondition always comes from the client. The header is now read once, before the stored template is consulted. An If-Match stays an If-Match whatever the read returned, so the worker answers 412 and the editor learns the template is gone; only a client that sent no If-Match at all gets the create-only precondition. An If-Match that is not a version is refused on both branches rather than silently becoming a create. The test that pinned the forwarding promise exercised only the non-null branch; it now covers the null one, and the create case says out loud that it is about a client that sent no header.
…nt template The `template === null` branch failed open. It returned canPublish for any signed-in caller and reported mintedVehicles: 0 as fact without ever calling countMintedVehicles -- while the field's own doc says an unknown count is not zero. Template absence is not definition absence: the production extraction has never been run, Console's own search renders those rows as 'missing', and identity serves toyota_camry_2020 to around 300k vehicles with no t/toyota_camry_2020.json behind it. Any signed-in developer could write what all of them resolve to, and the same hole reopened for every template deleted through the worker. The count now runs first, for every caller and both branches, and an absent template goes through the same tiers as a stored one: unavailable when identity cannot answer, curator, create only when the count is genuinely zero, manufacturer for the NFT holder, proposal-required for everyone else. With no stored document to read the manufacturer slug from, it comes off the make segment of the id -- which the worker already requires to equal manufacturer.slug, so it is the same value the document would have carried. resolveEntitlement takes the definition id as an argument now rather than reading template.id, since the question is about the definition and the document may not exist.
The manufacturer code and style name inputs were controlled by the joined list while the change handler re-parsed with toList on every keystroke. A typed comma was therefore deleted the instant it appeared: '2532' + ',' parsed back to ['2532'], which re-rendered as '2532'. A second selector could not be entered at all, and the placeholder the component itself shows -- '2532, 2546' -- named a value the control could not produce. The only workaround, clearing the field first, stored manufacturerCode: [] on the way and tripped the multi-trim selector-less trim error on a template that was fine. The same round trip split an existing styleName containing a comma into two selectors the moment the field was touched, silently changing which VINs that trim claims. The field now holds the raw text while it is being typed and parses on blur, which is what TemplateCell already does for the same reason. An untouched field commits nothing, so a stored value containing a comma survives a focus and a blur unsplit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The editor for the trim templates the extraction pipeline emits.
Trims are columns, attributes are rows. Values shared across trims render
muted; values that differ render at full contrast, so on the real Camry the eye
lands on
powertrain_typeandfuel_tank_capacity_gal— the pair theproduction record blended into one row describing two different cars.
/templatesbrowses device definitions and says which have a template, whichdo not yet, and which carry an id the schema cannot accept
(
subaru_tribeca-(ny/nj)_2008and 136 others are live today)./templates/<id>edits one. Publish is disabled while the draft would failthe worker, and the fault is named in the worker's own words.
/templates/newcreates one.The write path
DEFINITIONS_WRITE_TOKENlives only insrc/services/definitions.ts, whichthrows if it is ever evaluated in a browser — a guarantee rather than a
convention, and tested both ways.
/api/templates/[id]identifies the callerfrom the session Console already has, stamps
authorfrom it, and refuses abody that names its own
authororversionrather than stripping it quietly.Entitlement follows risk: creating a template harms nobody; editing one that
4,212 vehicles resolve to re-describes all of them.
hardwareTemplateIdiscurator-only at every tier and has no UI at all.
The
If-Matchforwarded to the worker is the client's, never the versionthe route just read — the latter would rebase a stale editor onto whatever
landed while it was open and return 200.
Needs review
unreferenced template, but backfill templates carry no
author, which under aliteral reading makes the whole catalog curator-only. This treats an absent
authoras unowned. One function changes if that is wrong(
resolveEntitlement).own needs a proposal store the worker does not have. Those callers get a
read-only editor that says exactly why.
Prerequisites
Requires definitions-worker
#4 (schema route,
compare-and-swap, generated types) deployed first. Deployment is still gated on
dd-api's
Create()/manifest()migration and a production re-run of theextraction.
Verification
203 tests passing, up from 100. This repo's
masteris red before this branch:19 suites / 20 tests fail on stale expectations unrelated to templates (a
toEqualon the whole config object, components that dropped arole). Thefailing set is byte-identical before and after this branch.
npx tsc --noEmitreports zero errors under
src/.npm run buildsucceeds.Note:
CLAUDE.mdsaysnpm run compileis a typecheck; it isgraphql-codegen.