linksTo never in attributes - #112
Conversation
Rule 14 — a linksTo field never appears in attributes, not even as null. Models writing card instances put a bare "theme": null inside attributes.cardInfo; that passes lint and writes successfully, and then every read of the instance throws "linkTo field 'theme' cannot deserialize non-relationship value null" until the raw JSON is repaired by hand. Rules 12 and 13 cover what goes inside links.self and the linksToMany shape; nothing said a linksTo may not sit in attributes at all. Rule 15 — base modules import by URL, never by package name. Models write the npm-style import "@cardstack/base/card-api" where a realm requires "https://cardstack.com/base/card-api"; the module never resolves and the file bounces back for a repair turn. The skill page never showed a correct base import, so a model that skips the common-imports reference had nothing to overrule its prior. Both rules are mirrored in the index conventions and the glossary like their siblings, and common-imports.md now opens with the trap before the preflight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Staging Workspace Sync SuccessfulSuccessfully synced changes to staging workspace Sync Details |
habdelra
left a comment
There was a problem hiding this comment.
Docs-only change adding Cardinal Rules 14 (linksTo never in attributes) and 15 (base modules import by URL). The good news first: rule numbering is clean (1–15, no collisions), the cardInfo.theme dotted-key shape matches the rest of the repo, the base module names are all correct, and glossary.md was updated per the maintenance rule.
Two inline comments above. The important one is on common-imports.md — Rule 15's absolute "@cardstack/base/... never resolves / @cardstack/* only correct for three packages" contradicts existing docs that use @cardstack/base/workspace as a valid adoptsFrom. It's the only finding here that could induce a wrong edit (a model "fixing" a correct workspace reference); everything else is a propagation/consistency gap.
Two more findings that don't map to a changed line, so they're here rather than inline:
-
Rule 14 isn't added to the
boxel-workspace-cardinal-ruleschecklist.index.mdpoints authors to that skill as the pre-finish silent-failure gate ("check every card/field against it before finishing"), and its list stops at item 9. ThelinksTo-in-attributestrap (writes fine, throws on every read) is exactly the class it catalogs — an author who runs the checklist gets a false all-clear on the very bug this PR targets. Worth adding it there. -
Rules 14/15 landed only in the
skills/tree, not the parallelSkill/tree. The maintainer note inindex.md/CLAUDE.mdsays a convention change must be authored into both trees or they drift, so the in-app assistant that consumesSkill/cards won't learn these rules. (Rules 12/13 are already absent fromSkill/, so this continues an existing drift rather than starting it — same gap, worth closing.)
Net: solid, useful additions. The @cardstack/base over-broad phrasing is the one thing I'd fix before merge; the rest are follow-ups.
Generated by Claude Code
|
|
||
| ## The `@cardstack/base` trap | ||
|
|
||
| Base card modules resolve by URL, not by package name. `import StringField from '@cardstack/base/string'` looks like every other npm import and is always wrong in a realm — the module never resolves, the card fails to load, and the correctness check bounces the file back for a repair turn. Everything under the base realm imports as `https://cardstack.com/base/<module>`: `card-api`, `string`, `number`, `boolean`, `date`, `datetime`, `text-area`, `url`, `markdown`, and the rest. The `@cardstack/*` scope is only correct for the packages listed in this file: `@cardstack/boxel-ui/*`, `@cardstack/boxel-icons/*`, `@cardstack/runtime-common`. |
There was a problem hiding this comment.
This is correct for base module imports in .gts (card-api, string, etc.) — @cardstack/base/string genuinely won't resolve there. But the absolute framing ("The @cardstack/* scope is only correct for @cardstack/boxel-ui/*, @cardstack/boxel-icons/*, @cardstack/runtime-common") is too broad and collides with the repo's own docs: @cardstack/base/... does resolve as a registered VirtualNetwork prefix, and @cardstack/base/workspace is documented as a valid adoptsFrom:
skills/boxel/references/card-references.md:90— "@cardstack/base/...can resolve through prefix mappings"skills/boxel/references/default-index-card.md:3,63— setadoptsFromtoWorkspace/@cardstack/base/workspaceskills/glossary.md:26andSkill/dev-core-concept.md:22— same
Suggest scoping the rule to base field / card-api module imports rather than the whole @cardstack/base namespace, so a model doesn't "fix" a correct @cardstack/base/workspace adoptsFrom and break the default index card. The same absolute wording was added to the sibling copies here too — skills/boxel/SKILL.md Rule 15, index.md Rule 15, and skills/glossary.md — worth adjusting all four together.
Generated by Claude Code
There was a problem hiding this comment.
(Written by Claude on Matic's behalf.)
Removed the rule entirely rather than scoping it — #113 establishes the @cardstack/base/... prefix as canonical (it resolves for module imports too, e.g. tracked experiments-realm cards import @cardstack/base/card-api), so any form of this rule points the wrong way.
| | 11 | **`DateField` vs `DateTimeField` — schema MUST match value format.** `contains(DateField)` requires JSON value `YYYY-MM-DD` (NO `T`). `contains(DateTimeField)` requires ISO datetime with `T` (`YYYY-MM-DDTHH:MM:SS[.sss]Z`). A mismatch passes `npx boxel file lint`, writes successfully, AND indexes — then blows up at render time as `RangeError: Invalid time value` from date-fns inside `Contains.serialize`. Pick the type by whether time-of-day is meaningful (`*At` suffix → DateTimeField; `*Date`/`*On`/`hireDate`/`dob` → DateField), then keep instance values in lockstep. See `references/base-field-catalog.md`. | | ||
| | 12 | **🚨 External URLs in JSON:API `relationships.<field>.links.self` brick the entire realm.** A relationship's `links.self` is a card identifier — relative paths (`"../Theme/foo"`) or absolute realm URLs only. **NEVER put an external image/asset URL there.** The indexer fetches the URL expecting a card document, gets binary bytes (JPEG, PNG, etc.), `JSON.parse` throws on the binary, the error message contains the binary's NULL byte, postgres rejects the JSONB write with `22P05: unsupported Unicode escape sequence`, and the transaction rolls back — taking every other card in the batch with it. The whole realm stays unindexed until the bad instance is fixed. For image URLs, use the `cardInfo` pair pattern: `@field heroImage = linksTo(ImageDef)` + `@field heroImageURL = contains(UrlField)`; the URL goes in `attributes.heroImageURL`, not the relationship. See `references/base-field-catalog.md` "Image fields — the URL/ImageDef pair pattern". | | ||
| | 13 | **🚨 `linksToMany` JSON shape uses INDEXED KEYS, never an array under `links.self`.** Each linked item in a `linksToMany` field gets its own top-level relationship key with an indexed suffix. Correct: `"activityFeed.0": { "links": { "self": "..." } }`, `"activityFeed.1": { "links": { "self": "..." } }`. WRONG (and the host rejects with "instance ... is not a card resource document"): `"activityFeed": { "links": { "self": ["...", "..."] } }`. The array-inside-`self` shape is intuitive but not valid Boxel JSON:API — `links.self` is a single string per JSON:API spec, and Boxel's encoding of "many" is indexed top-level keys. See `references/core-patterns.md` "JSON:API instance shapes". | | ||
| | 14 | **🚨 `linksTo` fields never appear in `attributes` — not even as `null`.** A `linksTo` field is serialized under `relationships`, keyed by its field path — for a linksTo nested inside a contained field, a dotted key: `"cardInfo.theme": { "links": { "self": "../Theme/foo" } }`. An empty link is `{ "links": { "self": null } }`, or omit the key entirely. Writing `"theme": null` (or any value) into `attributes` passes lint and writes successfully — then every read of the instance throws `linkTo field 'theme' cannot deserialize non-relationship value null` until the raw JSON is repaired by hand. | |
There was a problem hiding this comment.
The example mixes nested and top-level shapes. The right-shape example uses the nested dotted key "cardInfo.theme", but the wrong-shape example is a top-level "theme": null and the error quotes linkTo field 'theme'. For a link nested inside cardInfo, the incorrect attributes shape a model actually emits is "cardInfo": { "theme": null }, not a top-level "theme". As written it's ambiguous whether the rule targets nested links or top-level ones — suggest making the wrong example "cardInfo": { "theme": null } to match, or use a single top-level linksTo field name throughout.
Minor: every other rule in this table (11–13, 15) ends with a See references/... pointer; Rule 14 doesn't. Not blocking (there's no reference doc for this trap today), just noting the inconsistency.
Generated by Claude Code
There was a problem hiding this comment.
(Written by Claude on Matic's behalf.)
Fixed — the wrong-shape example is now "cardInfo": { "theme": null }, matching the nested dotted-key shape in the right-shape example.
…ecklist The @cardstack/base/... prefix is a registered VirtualNetwork mapping and is the canonical reference form, so the rule that banned it taught the assistant to "repair" valid imports. The linksTo-in-attributes rule stays: its wrong-shape example now uses the nested cardInfo shape, and the trap is added to the boxel-workspace-cardinal-rules checklist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
(Written by Claude on Matic's behalf.) Review addressed: the base-import rule is removed from all four files (#113 is the source of truth — the |
Teaches the assistant the linksTo-in-attributes trap: a
linksTofield written intoattributes(even asnull) passes lint and writes successfully, then every read of the instance fails withcannot deserialize non-relationship valueuntil the raw JSON is repaired by hand.The rule lands everywhere the other silent-failure traps already live: Cardinal Rule 14 in
skills/boxel/SKILL.md, a mirror bullet inindex.md(in every prompt), a glossary bullet, and item 10 in theboxel-workspace-cardinal-rulespre-finish checklist.