Skip to content

Commit c99ddf1

Browse files
dmealingclaude
andcommitted
docs: defer @rows registration (no clean cross-port home)
Neither candidate home for @rows on view.textarea works: ui.json's extends throws in the non-TS ports where view.textarea is deregistered, and core view.json violates the deliberate FR-033 invariant that core registers zero own attrs on any view subtype (enforced by view-definition-completeness + metamodel-docs golden tests). Attaching an attribute to a TS-only view subtype needs its own cross-port design, out of scope for form controls. This cycle ships the textarea dispatch with a fixed rows={4} default; configurable @rows becomes a documented Deferred-work item. Spec Unit A + the plan's Task 1 are marked deferred; the dispatch task and its render test drop the @rows read and assert rows={4}. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGQ7oSuNcjhsMHWwZzhBwr
1 parent 6c63b3a commit c99ddf1

2 files changed

Lines changed: 71 additions & 61 deletions

File tree

docs/superpowers/plans/2026-07-18-form-controls-view-dispatch.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
44
5-
**Goal:** Make the generated `<Entity>Form` render the right control for each field's declared view kind (enum→`<select>`, `view.textarea``<textarea>`, `view.checkbox`, `view.radio`) instead of a bare `<input>` per scalar, and register the two attributes the codegen already reads`@rows` and `@formExclude`.
5+
**Goal:** Make the generated `<Entity>Form` render the right control for each field's declared view kind (enum→`<select>`, `view.textarea``<textarea>`, `view.checkbox`, `view.radio`) instead of a bare `<input>` per scalar, and register `@formExclude` (which the codegen already reads).
66

7-
**Architecture:** Three units. Unit A registers `@rows` on `view.textarea` via `spec/metamodel/view.json` (TS-registration-only). Unit B registers `@formExclude` on the `field.*` wildcard via `spec/metamodel/ui.json` (cross-port, data-driven — JSON sync + regen, no non-TS code). Unit C upgrades the `renderFormFile` template with a view-kind dispatch. Cross-port ports are data-driven: they load the spec JSON, so Units A/B are JSON edits + regeneration, not per-port code.
7+
**Architecture:** Two implemented units (Unit A / `@rows` is deferred — see Task 1). Unit B registers `@formExclude` on the `field.*` wildcard via `spec/metamodel/ui.json` (cross-port, data-driven — JSON sync + regen, no non-TS code). Unit C upgrades the `renderFormFile` template with a view-kind dispatch (textarea uses a fixed `rows={4}`). Cross-port ports are data-driven: they load the spec JSON, so Unit B is JSON edits + regeneration, not per-port code.
88

99
**Tech Stack:** TypeScript (Bun test runner, ts-poet templates), the `@metaobjectsdev/metadata` loader + registry, the `metaobjects-ui` / `metaobjects-core-types` metamodel providers, `fixtures/registry-conformance` cross-port gate. Design spec: `docs/superpowers/specs/2026-07-18-form-controls-view-dispatch-design.md`.
1010

@@ -24,8 +24,8 @@
2424

2525
## File Structure
2626

27-
**Unit A — `@rows` (view.json):**
28-
- Modify: `spec/metamodel/view.json` — add `rows` int attr to the `view.textarea` entry.
27+
**Unit A — `@rows` (view.json): DEFERRED — not implemented this cycle (see Task 1).**
28+
- ~~Modify: `spec/metamodel/view.json` — add `rows` int attr to the `view.textarea` entry.~~
2929
- Modify (sync copy): `server/csharp/MetaObjects/SpecMetamodel/view.json`
3030
- Modify (sync copy): `server/python/src/metaobjects/spec_metamodel/view.json`
3131
- Regenerate: `server/typescript/packages/metadata/src/presentation/view/view-definition.embedded.ts`
@@ -48,7 +48,16 @@
4848

4949
---
5050

51-
## Task 1: Register `@rows` on `view.textarea` (view.json, TS-registration-only)
51+
## Task 1: Register `@rows` on `view.textarea`**DEFERRED (do not implement)**
52+
53+
> **Deferred during execution.** Neither candidate home works: `ui.json`'s `extends`
54+
> throws in C#/Python/Java (where `view.textarea` is deregistered), and `view.json`
55+
> (core) violates the deliberate FR-033 invariant that core registers zero own attrs
56+
> on any view subtype (enforced by `view-definition-completeness.test.ts` +
57+
> `metamodel-docs*.test.ts`). Attaching an attr to a TS-only view subtype needs its own
58+
> cross-port design (out of scope for form controls). This cycle ships the textarea
59+
> dispatch with a fixed `rows={4}` default (Task 3); configurable `@rows` is a documented
60+
> future item. **Skip this task entirely.** The section below is retained for the record.
5261
5362
**Files:**
5463
- Modify: `spec/metamodel/view.json:14-18` (the `view.textarea` entry)
@@ -242,7 +251,7 @@ git commit -m "feat(metadata): register @formExclude on field.* wildcard (cross-
242251
- Test: `server/typescript/packages/codegen-ts-react/test/form-view-dispatch.test.ts` (new)
243252

244253
**Interfaces:**
245-
- Consumes: `VIEW_TEXTAREA_ATTR_ROWS` (Task 1), `FIELD_ATTR_FORM_EXCLUDE` (Task 2), and the existing `VIEW_SUBTYPE_{TEXTAREA,DROPDOWN,CHECKBOX,RADIO}`, `FIELD_SUBTYPE_ENUM`, `FIELD_ATTR_{VALUES,REQUIRED}` constants — all from `@metaobjectsdev/metadata`.
254+
- Consumes: `FIELD_ATTR_FORM_EXCLUDE` (Task 2) and the existing `VIEW_SUBTYPE_{TEXTAREA,DROPDOWN,CHECKBOX,RADIO}`, `FIELD_SUBTYPE_ENUM`, `FIELD_ATTR_{VALUES,REQUIRED}` constants — all from `@metaobjectsdev/metadata`. (`@rows`/`VIEW_TEXTAREA_ATTR_ROWS` is deferred — Task 1; textarea uses a fixed `rows={4}`.)
246255
- Produces: the upgraded generated form output (final task; nothing downstream depends on it).
247256

248257
- [ ] **Step 1: Write the failing test**
@@ -279,8 +288,8 @@ async function loadModel(): Promise<{ root: MetaRoot; report: MetaObject }> {
279288
{ "field.string": { name: "name", "@required": true } },
280289
// enum with no explicit view -> dropdown default
281290
{ "field.enum": { name: "status", "@required": true, "@values": ["draft", "active", "closed"] } },
282-
// view.textarea @rows -> <textarea rows={8}>
283-
{ "field.string": { name: "notes", children: [{ "view.textarea": { "@rows": 8 } }] } },
291+
// view.textarea -> <textarea rows={4}> (configurable @rows deferred)
292+
{ "field.string": { name: "notes", children: [{ "view.textarea": {} }] } },
284293
// view.checkbox -> checkbox
285294
{ "field.boolean": { name: "archived", children: [{ "view.checkbox": {} }] } },
286295
// view.radio over enum values -> radio fieldset
@@ -325,11 +334,11 @@ describe("form controls — view-kind dispatch", () => {
325334
expect(out).not.toMatch(/form\.input\.status/);
326335
});
327336

328-
test("view.textarea renders a <textarea> with the @rows count", async () => {
337+
test("view.textarea renders a <textarea> with the default row count", async () => {
329338
const { root, report } = await loadModel();
330339
const out = renderFormFile(report, ctxFor(root));
331340
expect(out).toContain("<textarea");
332-
expect(out).toContain("rows={8}");
341+
expect(out).toContain("rows={4}");
333342
expect(out).toContain('form.register("notes")');
334343
});
335344

@@ -390,7 +399,6 @@ import {
390399
VIEW_SUBTYPE_DROPDOWN,
391400
VIEW_SUBTYPE_CHECKBOX,
392401
VIEW_SUBTYPE_RADIO,
393-
VIEW_TEXTAREA_ATTR_ROWS,
394402
} from "@metaobjectsdev/metadata";
395403
```
396404

@@ -457,10 +465,10 @@ Inside `renderFormFile`, next to the existing `scalarBlock` (so it can fall back
457465
);
458466
}
459467
if (kind === VIEW_SUBTYPE_TEXTAREA) {
460-
const rows = (field.views()[0]?.attr(VIEW_TEXTAREA_ATTR_ROWS) as number | undefined) ?? 4;
468+
// Configurable @rows is deferred (see the spec's Deferred work); default to 4.
461469
return labelAndError(
462470
name,
463-
` <textarea className="metaobjects-field-input" rows={${rows}} {...form.register("${name}")} />`,
471+
` <textarea className="metaobjects-field-input" rows={4} {...form.register("${name}")} />`,
464472
);
465473
}
466474
if (kind === VIEW_SUBTYPE_CHECKBOX) {
@@ -584,10 +592,10 @@ Note the issue URL/number in the commit that closes out the cycle (or reply to t
584592
## Self-Review
585593

586594
**1. Spec coverage:**
587-
- Unit A (`@rows` via `view.json`, TS-registration-only, drift-gate sync) → Task 1. ✓
595+
- Unit A (`@rows`) → **DEFERRED** during execution (no clean cross-port home; FR-033 invariant). Task 1 skipped; textarea uses `rows={4}`.
588596
- Unit B (`@formExclude` via `ui.json`, cross-port, data-driven, atomic) → Task 2. ✓
589597
- Unit C (`viewKindFor` / `labelAndError` / `fieldControlFor` dispatch, enum→dropdown default, styled submit + semantic class names, constant usage) → Task 3. ✓
590-
- Named constants (`VIEW_TEXTAREA_ATTR_ROWS`, `FIELD_ATTR_FORM_EXCLUDE`) defined (Tasks 1/2) + used (Task 3). ✓
598+
- Named constant (`FIELD_ATTR_FORM_EXCLUDE`) defined (Task 2) + used (Task 3). ✓
591599
- Testing (loader/registration gated by embed + conformance; render tests) → Tasks 1/2 gates + Task 3 render tests. ✓
592600
- Deferred blank-optional fix + tracking issue → Task 4. ✓
593601
- Out-of-scope (image branch, `form.css` file) → not implemented, correctly absent. ✓
@@ -596,4 +604,4 @@ Note the issue URL/number in the commit that closes out the cycle (or reply to t
596604

597605
**3. Type consistency:** `viewKindFor(field: MetaField): string | null`; `fieldControlFor(field: MetaField): string`; `labelAndError(f: string, control: string)`; `scalarBlock(f: string)` (existing, name-string arg — matched). `field.views()[0]?.attr(...)`, `field.attr(...)` return `unknown`, narrowed with `as` to the expected shapes. Constant names match their definitions in Tasks 1/2. ✓
598606

599-
**Dependency note:** Task 3's render test loads a fixture using `view.textarea @rows` and `field.string @formExclude`; under the strict loader those must be registered first, so Tasks 1 and 2 must complete before Task 3. Task 4 is independent.
607+
**Dependency note:** Task 1 (`@rows`) is DEFERRED. Task 3's render test loads a fixture using `field.string @formExclude`; under the strict loader that must be registered first, so Task 2 must complete before Task 3. Task 4 is independent.

0 commit comments

Comments
 (0)