Skip to content

Commit d2eab21

Browse files
dmealingclaude
andcommitted
fix(audit-skill): a retired descriptor member, and the corpus the last two skill edits left stale
Two reds on main, one mine and one inherited. MINE — I added a retired vocabulary name to three shipped teaching surfaces. Widening signature 11 yesterday, I listed the entity descriptor's members and included one that left the descriptor in 0.25.0. I took it from a test file whose name and whole purpose are to assert it is NOT emitted; reading a negative assertion as evidence of emission is the exact failure `check-no-api-prefix.ts` exists to catch, and it caught it. The gate scans docs/, agent-context/ and examples/ for the bare token with no exemption for prose that means to explain the retirement — correctly, since a skill that names it teaches it — so the sentence is rewritten without the token: `$path` is the ENTITY-RELATIVE resource path, and the base URL is a deployment fact supplied at runtime by the fetcher provider, which is why a hand-written `fetch("/api/customers")` hardcodes two facts at once. INHERITED — `fixtures/agent-context-conformance/` mirrors `agent-context/` into five assembled stacks, and an edit to a skill is only half a change until the corpus is regenerated. The previous commit (99d173b) edited the audit skill alone and left five cases stale, so ts-unit went red on a docs-only commit and stayed red through mine. Both edits are now reflected in all five. Nothing about the assembly changed — this is `assemble()` output for the current content, byte for byte. The lesson is the second one: an `agent-context/` edit is not done when the file reads right. The corpus is the gate that proves what an adopter actually receives, and it does not regenerate itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent c7f0697 commit d2eab21

15 files changed

Lines changed: 158 additions & 13 deletions

File tree

‎agent-context/skills/metaobjects-audit/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
345345

346346
**(i) Physical names — every port.** A table / view / stored-proc / column / schema name spelled as a literal outside its metadata declaration: in a hand-written repository, raw SQL, a migration script, a log line, or a hand-maintained body-to-column map. The per-object names artifact (`<Entity>.names.ts` / `<entity_snake>_names.py` / `<Entity>Names`) carries these on all five ports. Every port emits a per-object names artifact (`<Entity>Names` / `<entity_snake>_names.py`) from the declaration, so a literal is a second spelling of the same fact; and because the physical column is free-form, deriving it from the field name is a guess that fails silently.
347347

348-
**(ii) The rest of the constants surface — TypeScript today.** The generated entity module and its browser-safe `<Entity>.meta.ts` twin carry `$entity` (the logical name), `$table`, **`$path` (the resource route path)**, `$apiPrefix`, and a per-field object keyed by field name carrying `name`, `label`, `view`, `htmlType`, the RHF-shaped `rules` derived from validator children, and a `field.enum`'s member symbols. The generated file's own docblock says to use them *instead of magic strings*. **The test is whether anything TYPE-CHECKS the literal, not whether it looks like a name.** Score a site only where nothing does: a hand-written **API resource path** (`fetch("/api/subscribers")`, `app.get("/subscribers")`), a **form label** re-typed in hand JSX, a **re-typed validation limit or message**, or a **field name passed as loose text** to something that takes a bare `string`. Where the generated artifact is a union type or a typed handle, the compiler is already the gate and the literal is CORRECT — measured: an estate's 332 bare enum-member strings (`status === "edited_approved"`) are each checked against the generated `export type <Entity><Field> = "a" | "b"` union, so renaming the member in metadata breaks the build at every site rather than drifting silently. That is the opposite of the physical-name case, and flagging it would have produced 332 findings that are all false. Enum members, typed filter objects and ORM handles are checked; paths, labels and messages are not. On the other four ports the equivalent of (ii) is the generated typed handle (a Pydantic model attribute, an EF property, an Exposed `Column`) — already the gold standard, see the do-not-flag list.
348+
**(ii) The rest of the constants surface — TypeScript today.** The generated entity module and its browser-safe `<Entity>.meta.ts` twin carry `$entity` (the logical name), `$table`, and **`$path` — the ENTITY-RELATIVE resource path** (`/customers`; the base URL is a DEPLOYMENT fact supplied at runtime by the fetcher provider and is deliberately not in the descriptor, so a hand-written `fetch("/api/customers")` hardcodes both facts at once) — plus a per-field object keyed by field name carrying `name`, `label`, `view`, `htmlType`, the RHF-shaped `rules` derived from validator children, and a `field.enum`'s member symbols. The generated file's own docblock says to use them *instead of magic strings*. **The test is whether anything TYPE-CHECKS the literal, not whether it looks like a name.** Score a site only where nothing does: a hand-written **API resource path** (`fetch("/api/subscribers")`, `app.get("/subscribers")`), a **form label** re-typed in hand JSX, a **re-typed validation limit or message**, or a **field name passed as loose text** to something that takes a bare `string`. Where the generated artifact is a union type or a typed handle, the compiler is already the gate and the literal is CORRECT — measured: an estate's 332 bare enum-member strings (`status === "edited_approved"`) are each checked against the generated `export type <Entity><Field> = "a" | "b"` union, so renaming the member in metadata breaks the build at every site rather than drifting silently. That is the opposite of the physical-name case, and flagging it would have produced 332 findings that are all false. Enum members, typed filter objects and ORM handles are checked; paths, labels and messages are not. On the other four ports the equivalent of (ii) is the generated typed handle (a Pydantic model attribute, an EF property, an Exposed `Column`) — already the gold standard, see the do-not-flag list.
349349

350350
**Score only where a constant actually exists.** A fact declared in the project's CODEGEN CONFIG rather than in metadata — a bespoke route mount path, a hand-chosen JSON envelope key — is genuinely spelled twice when a client re-types it, but no generated constant holds it. That is a GENERATOR GAP, reported as one, never a literal-site finding: never invent a constant that the emitters do not emit.
351351

‎agent-context/skills/metaobjects-audit/references/capability-checklist.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ subtypes with opposite polarity: `requirement.functional` fails when NOTHING imp
273273
## Cross-cutting
274274

275275
- **Generated constants vs magic strings beyond SQL (drift signature 11, half ii).** On TypeScript
276-
the entity module and its `<Entity>.meta.ts` twin emit `$entity` / `$table` / `$path` / `$apiPrefix`
276+
the entity module and its `<Entity>.meta.ts` twin emit `$entity` / `$table` / `$path` (entity-relative)
277277
plus a per-field object (`name`, `label`, `view`, `htmlType`, `rules`, enum member symbols) whose own
278278
docblock says to use them INSTEAD of magic strings. Hunt hand-written route paths and fetch URLs,
279279
sort/filter field names passed as text, enum members compared as bare strings, re-typed labels, and

‎agent-context/skills/metaobjects-audit/references/typescript.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ value-sets against the constructs that replace them.
5555
| camelCase↔snake_case mapping tables | generated views handle this; diff field by field |
5656
| `@generated` file with hand edits | three-way merge preserves them; review at codegen; never delete |
5757
| a table/column string in a `sql` fragment, a Kysely identifier, or a migration/seed script outside the generated dir | second spelling of a declared physical name — reference `<Entity>.names.ts` (`ProgramNames.fields.<f>.column`): `generators: [...]` is the COMPLETE list and there is no default suite, so a project scaffolded before 1.0 emits NO names artifact and the missing `namesFile()` is the finding first; a Drizzle column object in its place is correct, not a finding |
58-
| an API resource path typed as a string — `fetch("/api/subscribers")`, `app.get("/subscribers")`, a TanStack `queryKey` path | second spelling of the generated `$path` / `$apiPrefix` on the entity module and its `<Entity>.meta.ts` twin — signature 11 half (ii). Most-missed row in this table, because the names hunt gets scoped to SQL |
58+
| an API resource path typed as a string — `fetch("/api/subscribers")`, `app.get("/subscribers")`, a TanStack `queryKey` path | second spelling of the generated `$path` (entity-relative) on the entity module and its `<Entity>.meta.ts` twin — signature 11 half (ii). Most-missed row in this table, because the names hunt gets scoped to SQL |
5959
| **NOT a finding:** a client-side page route or link (`<Route path="/programs">`, `<a href="/programs">`) | a navigation URL the app chose, declared nowhere in metadata and free to diverge from the API path it resembles. Measured on one estate: 11 apparent hits, 0 real. Match the fetch or the mount, not the string's shape |
6060
| a field name passed as TEXT — `sort: "created_at"`, `filter["email"]`, a column-id string in a grid config | the per-field constants object carries `name`; a typed property access is NOT this row and must be left alone |
6161
| **NOT a finding:** an enum member as a bare string (`status === "archived"`) | the generated `export type <Entity><Field> = "a" \| "b"` union already checks it — renaming the member in metadata breaks the build at every site. Measured on one estate: 332 such literals, 0 real findings. Emitted `options: [...]` on `.meta.ts` is for populating a dropdown, not for replacing checked literals |

0 commit comments

Comments
 (0)