|
| 1 | +# WA1 — Case-preserving (case-sensitive) registry — Implementation Plan |
| 2 | + |
| 3 | +> **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. |
| 4 | +
|
| 5 | +**Goal:** Make the Java metamodel registry **case-sensitive on the exact (canonical) type/subtype vocabulary** — remove the type/subtype lowercasing in `MetaDataTypeId` + `ChildRequirement` — so the standard's camelCase subtypes (`dbTable`, `dbView`, `dataGrid`) are matched and round-tripped faithfully, and non-canonical-case input is rejected rather than silently accepted. |
| 6 | + |
| 7 | +**Architecture:** The lowercasing lives only in the **registry lookup key** (`MetaDataTypeId`, which keys the type-definition map + pattern matching) and **placement rules** (`ChildRequirement`). Node instances already store their subtype verbatim (`createInstance` passes the verbatim args; node classes hardcode their canonical subtype constant), so the canonical *output* casing comes from the registered constants — not the key. Removing the lowercasing makes lookups exact-match (case-sensitive), which (a) rejects non-canonical input instead of silently accepting it, and (b) eliminates the latent path where a parsed wrong-case subtype could leak to canonical output. Foundational for WA3 (camelCase `source.dbView`/`dbTable`). |
| 8 | + |
| 9 | +**Tech Stack:** Java 21, Maven `metadata` module under `server/java/`, JUnit4. |
| 10 | + |
| 11 | +**Spec:** `docs/superpowers/specs/2026-05-23-java-standard-alignment-and-loader-consolidation-design.md` (WA1). This is the first of several plans from that spec; WA2/WA3/WA4 follow. |
| 12 | + |
| 13 | +**Scope note:** Today Java's entire registered vocabulary is single-word **lowercase** (`string`, `int`, `pojo`, `base`, …), which is already canonical — so removing the lowercasing is largely a **no-op for current types** and a **strictness + future-camelCase enabler**. The audit (Task 4) should therefore find few or no breakages; any it finds are genuine non-canonical-case usages to correct. |
| 14 | + |
| 15 | +**Leave-list (do NOT change — these lowercase *names*/provider-ids, not type-keys):** `CanonicalJsonSerializer:439-440` (auto-name prefix for unnamed nodes), `BaseMetaDataParser:276-277` (auto-name prefix), `MetaField.java:383` (`switch(subType.toLowerCase())` — harmless: field subtypes are canonically lowercase), `MetaDataTypeProvider:93,97` (provider-id derivation). |
| 16 | + |
| 17 | +**Worktree:** execute in this worktree (`java-casing-fix`); integrate by merging forward into `main` (never rewrite main). |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## Task 1: `MetaDataTypeId` — case-sensitive key + pattern matching |
| 22 | + |
| 23 | +**Files:** |
| 24 | +- Modify: `server/java/metadata/src/main/java/com/metaobjects/MetaDataTypeId.java` (lines 43-44, 68-69, 104) |
| 25 | +- Test: `server/java/metadata/src/test/java/com/metaobjects/MetaDataTypeIdCaseTest.java` |
| 26 | + |
| 27 | +- [ ] **Step 1: Write the failing test** |
| 28 | + |
| 29 | +```java |
| 30 | +package com.metaobjects; |
| 31 | + |
| 32 | +import org.junit.Test; |
| 33 | +import static org.junit.Assert.*; |
| 34 | + |
| 35 | +public class MetaDataTypeIdCaseTest { |
| 36 | + |
| 37 | + @Test public void preserves_camelCase_subtype() { |
| 38 | + MetaDataTypeId id = new MetaDataTypeId("source", "dbView"); |
| 39 | + assertEquals("source", id.type()); |
| 40 | + assertEquals("dbView", id.subType()); // not "dbview" |
| 41 | + assertEquals("source.dbView", id.toQualifiedName()); |
| 42 | + } |
| 43 | + |
| 44 | + @Test public void differs_by_case_is_not_equal() { |
| 45 | + assertNotEquals(new MetaDataTypeId("source", "dbView"), |
| 46 | + new MetaDataTypeId("source", "dbview")); |
| 47 | + } |
| 48 | + |
| 49 | + @Test public void wildcard_pattern_matches_case_sensitively() { |
| 50 | + MetaDataTypeId id = new MetaDataTypeId("source", "dbView"); |
| 51 | + assertTrue(id.matches("source.*")); |
| 52 | + assertTrue(id.matches("source.dbView")); |
| 53 | + assertFalse(id.matches("source.dbview")); // exact case required |
| 54 | + assertFalse(id.matches("Source.dbView")); // type case too |
| 55 | + } |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +- [ ] **Step 2: Run, verify it fails** |
| 60 | + |
| 61 | +Run: `cd server/java && mvn -o -pl metadata test -Dtest=MetaDataTypeIdCaseTest` |
| 62 | +Expected: `preserves_camelCase_subtype` fails (`subType()` returns `"dbview"`); `differs_by_case_is_not_equal` fails (both lowercase to equal); `matches("source.dbview")` returns true (should be false). |
| 63 | + |
| 64 | +- [ ] **Step 3: Remove the lowercasing (keep `trim()`)** |
| 65 | + |
| 66 | +In `MetaDataTypeId.java`, the compact constructor (lines ~43-44): |
| 67 | + |
| 68 | +```java |
| 69 | + type = type.trim(); |
| 70 | + subType = subType.trim(); |
| 71 | +``` |
| 72 | + |
| 73 | +In `matches(String pattern)` (lines ~68-69): |
| 74 | + |
| 75 | +```java |
| 76 | + String patternType = parts[0].trim(); |
| 77 | + String patternSubType = parts[1].trim(); |
| 78 | +``` |
| 79 | + |
| 80 | +In `pattern(String type, String subType)` (line ~104): |
| 81 | + |
| 82 | +```java |
| 83 | + return new MetaDataTypeId(type.trim(), subType.trim()); |
| 84 | +``` |
| 85 | + |
| 86 | +(Do not touch `fromQualifiedName` — it delegates to the constructor. Do not touch `matches(MetaDataTypeId)` — it already compares `type`/`subType` directly.) |
| 87 | + |
| 88 | +- [ ] **Step 4: Run, verify pass** |
| 89 | + |
| 90 | +Run: `cd server/java && mvn -o -pl metadata test -Dtest=MetaDataTypeIdCaseTest` |
| 91 | +Expected: `Tests run: 3, Failures: 0, Errors: 0`. |
| 92 | + |
| 93 | +- [ ] **Step 5: Commit** |
| 94 | + |
| 95 | +```bash |
| 96 | +git add server/java/metadata/src/main/java/com/metaobjects/MetaDataTypeId.java \ |
| 97 | + server/java/metadata/src/test/java/com/metaobjects/MetaDataTypeIdCaseTest.java |
| 98 | +git commit -m "fix(metadata): MetaDataTypeId case-sensitive on canonical type/subtype (no lowercasing)" |
| 99 | +``` |
| 100 | +(End the commit message with the trailer line `Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>`.) |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +## Task 2: `ChildRequirement` — case-sensitive placement keys |
| 105 | + |
| 106 | +**Files:** |
| 107 | +- Modify: `server/java/metadata/src/main/java/com/metaobjects/registry/ChildRequirement.java` (lines 67-68) |
| 108 | +- Test: `server/java/metadata/src/test/java/com/metaobjects/registry/ChildRequirementCaseTest.java` |
| 109 | + |
| 110 | +- [ ] **Step 1: Write the failing test** |
| 111 | + |
| 112 | +```java |
| 113 | +package com.metaobjects.registry; |
| 114 | + |
| 115 | +import org.junit.Test; |
| 116 | +import static org.junit.Assert.*; |
| 117 | + |
| 118 | +public class ChildRequirementCaseTest { |
| 119 | + |
| 120 | + @Test public void preserves_camelCase_expected_subtype() { |
| 121 | + ChildRequirement r = new ChildRequirement( |
| 122 | + "*", "source", "dbView", false, null, null, null, null, null); |
| 123 | + assertEquals("source", r.getExpectedType()); |
| 124 | + assertEquals("dbView", r.getExpectedSubType()); // not "dbview" |
| 125 | + } |
| 126 | + |
| 127 | + @Test public void null_becomes_wildcard() { |
| 128 | + ChildRequirement r = new ChildRequirement( |
| 129 | + "*", null, null, false, null, null, null, null, null); |
| 130 | + assertEquals("*", r.getExpectedType()); |
| 131 | + assertEquals("*", r.getExpectedSubType()); |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +(Confirm the `ChildRequirement` constructor signature + the `getExpectedType()`/`getExpectedSubType()` accessor names by reading the class first; adjust the test to the real accessors. The contract: a camelCase `expectedSubType` is stored verbatim, and null still maps to `"*"`.) |
| 137 | + |
| 138 | +- [ ] **Step 2: Run, verify it fails** |
| 139 | + |
| 140 | +Run: `cd server/java && mvn -o -pl metadata test -Dtest=ChildRequirementCaseTest` |
| 141 | +Expected: `preserves_camelCase_expected_subtype` fails (`getExpectedSubType()` returns `"dbview"`). |
| 142 | + |
| 143 | +- [ ] **Step 3: Remove the lowercasing** |
| 144 | + |
| 145 | +In `ChildRequirement.java` (lines ~67-68), drop the `.toLowerCase()` (keep the null→`"*"` default): |
| 146 | + |
| 147 | +```java |
| 148 | + this.expectedType = expectedType != null ? expectedType : "*"; |
| 149 | + this.expectedSubType = expectedSubType != null ? expectedSubType : "*"; |
| 150 | +``` |
| 151 | + |
| 152 | +(Update the adjacent `// Normalize types to lowercase…` comment to reflect that types are now stored verbatim / matched case-sensitively.) |
| 153 | + |
| 154 | +- [ ] **Step 4: Run, verify pass** |
| 155 | + |
| 156 | +Run: `cd server/java && mvn -o -pl metadata test -Dtest=ChildRequirementCaseTest` |
| 157 | +Expected: `Tests run: 2, Failures: 0, Errors: 0`. |
| 158 | + |
| 159 | +- [ ] **Step 5: Commit** |
| 160 | + |
| 161 | +```bash |
| 162 | +git add server/java/metadata/src/main/java/com/metaobjects/registry/ChildRequirement.java \ |
| 163 | + server/java/metadata/src/test/java/com/metaobjects/registry/ChildRequirementCaseTest.java |
| 164 | +git commit -m "fix(metadata): ChildRequirement placement keys case-sensitive (no lowercasing)" |
| 165 | +``` |
| 166 | +(Append the `Co-Authored-By` trailer.) |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## Task 3: End-to-end gate — a camelCase subtype round-trips, wrong-case is rejected |
| 171 | + |
| 172 | +Proves the full pipeline (register → parse → registry lookup → canonical serialize) preserves camelCase casing and now rejects non-canonical case. Since no camelCase subtype exists yet, register a **test-only** one, mirroring the existing Open-Closed proof test. |
| 173 | + |
| 174 | +**Files:** |
| 175 | +- Test: `server/java/metadata/src/test/java/com/metaobjects/registry/CamelCaseSubtypeRoundTripTest.java` |
| 176 | + |
| 177 | +- [ ] **Step 1: Read the Open-Closed proof harness** |
| 178 | + |
| 179 | +Find the existing test that registers a one-off type via a class + a single `registry.registerType(...)` line and loads/serializes it (per ADR-0002 — search `server/java/metadata/src/test` for "Open-Closed", "fizz", or a registry-completeness/extensibility test). Reuse its registration + load + canonical-serialize harness. Confirm the canonical serializer entry point + how a test registers a transient type without polluting the shared registry. |
| 180 | + |
| 181 | +- [ ] **Step 2: Write the failing test** |
| 182 | + |
| 183 | +Behavioral contract (adapt to the real harness): |
| 184 | + |
| 185 | +```java |
| 186 | +// CamelCaseSubtypeRoundTripTest |
| 187 | +// 1. Register a test field subtype with a CAMELCASE name, e.g. field.fizzBuzz, via the same |
| 188 | +// one-class + registerType pattern the Open-Closed proof test uses (a tiny MetaField subclass |
| 189 | +// with SUBTYPE = "fizzBuzz" and a registerTypes() that inheritsFrom field.base). |
| 190 | +// 2. Load metadata declaring it: { "metadata.root": { "package":"t::x", "children":[ |
| 191 | +// { "object.pojo": { "name":"X", "children":[ { "field.fizzBuzz": { "name":"f" } } ] } } ] } } |
| 192 | +// (use object.pojo — entity/value arrive in WA2). |
| 193 | +// 3. Canonical-serialize the loaded root; assert the output contains the fused key "field.fizzBuzz" |
| 194 | +// (camelCase preserved end-to-end), NOT "field.fizzbuzz". |
| 195 | +// 4. Assert a wrong-case fixture { "field.fizzbuzz": {...} } now FAILS to load (unregistered type — |
| 196 | +// case-sensitive lookup rejects it): expect a load exception. |
| 197 | +``` |
| 198 | + |
| 199 | +Write the full JUnit4 test against the real harness; the assertions in (3) + (4) are the contract. |
| 200 | + |
| 201 | +- [ ] **Step 3: Run, verify it passes** (Tasks 1+2 already make the pipeline case-sensitive) |
| 202 | + |
| 203 | +Run: `cd server/java && mvn -o -pl metadata test -Dtest=CamelCaseSubtypeRoundTripTest` |
| 204 | +Expected: both assertions pass — `field.fizzBuzz` round-trips with case preserved; the wrong-case `field.fizzbuzz` fixture is rejected. |
| 205 | +(If (3) fails because a node-creation path still normalizes the subtype, trace it and fix the normalization at its source — do **not** weaken the assertion. If (4) does NOT reject — i.e. wrong-case still loads — the lookup isn't fully case-sensitive yet; find the remaining case-insensitive lookup and fix it.) |
| 206 | + |
| 207 | +- [ ] **Step 4: Commit** |
| 208 | + |
| 209 | +```bash |
| 210 | +git add server/java/metadata/src/test/java/com/metaobjects/registry/CamelCaseSubtypeRoundTripTest.java |
| 211 | +git commit -m "test(metadata): camelCase subtype round-trips end-to-end; wrong-case rejected (WA1 gate)" |
| 212 | +``` |
| 213 | +(Append the `Co-Authored-By` trailer.) |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## Task 4: Audit + reactor green |
| 218 | + |
| 219 | +- [ ] **Step 1: Run the full metadata suite; triage breakages** |
| 220 | + |
| 221 | +Run: `cd server/java && mvn -o -pl metadata test 2>&1 | grep -E "Tests run: [0-9]+, Fail|<<< (FAIL|ERROR)|BUILD" | grep -vE "Time elapsed" | tail -30` |
| 222 | +Expected baseline: the only pre-existing errors are the 2 known `CanonicalJsonParserTest` CWD-path NPEs (`corpusSpotCheck_loaderBasicEmptyPackage`, `corpusSpotCheck_smokeEmptyMetadata`). **Any other failure** is a case-sensitivity breakage from Tasks 1-2: a registration, fixture, or test that used a non-canonical-case type/subtype and relied on the old lowercasing. |
| 223 | + |
| 224 | +- [ ] **Step 2: Fix each breakage to canonical casing** |
| 225 | + |
| 226 | +For each new failure, the fix is to correct the offending type/subtype string to its **canonical** registered casing (e.g. a fixture `"field.String"` → `"field.string"`; a lookup `getType("Object","Base")` → `"object","base"`). Do **not** re-introduce lowercasing to paper over it — the canonical casing is the contract. Re-run the failing test after each fix. |
| 227 | + |
| 228 | +- [ ] **Step 3: Downstream modules green (the registry change is module-wide)** |
| 229 | + |
| 230 | +Run: `cd server/java && mvn -o install -pl core,metadata -DskipTests >/dev/null 2>&1 && mvn -o -pl omdb test 2>&1 | grep -E "Tests run:|BUILD" | tail -2 && mvn -o -pl core test 2>&1 | grep -E "Tests run:|BUILD" | tail -2 && mvn -o -pl maven-plugin test 2>&1 | grep -E "Tests run:|BUILD" | tail -2` |
| 231 | +Expected: `omdb`, `core`, `maven-plugin` BUILD SUCCESS (case-sensitivity must not regress them; fix any case-breakage the same way as Step 2). |
| 232 | + |
| 233 | +- [ ] **Step 4: Final commit (if any audit fixes) + ready for review** |
| 234 | + |
| 235 | +```bash |
| 236 | +git add -A && git commit -m "fix(metadata): audit — correct non-canonical type/subtype casing after case-sensitive registry" |
| 237 | +``` |
| 238 | +(Append the `Co-Authored-By` trailer. Skip if Steps 1-3 found nothing to fix.) |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## Self-Review |
| 243 | + |
| 244 | +- **Spec coverage (WA1):** removes the lowercasing in `MetaDataTypeId` (Task 1) + `ChildRequirement` (Task 2) → case-sensitive on canonical vocabulary; the end-to-end gate (Task 3) proves camelCase round-trips + wrong-case is rejected; the audit (Task 4) corrects any non-canonical usage. The leave-list (auto-name prefixes, provider-id, field-subtype switch) is documented and untouched. ✓ |
| 245 | +- **Accurate root-cause:** the lowercasing is in the lookup *key* + placement rules, not the node's stored subtype (which comes from the verbatim ctor args / canonical class constants) — so this is a case-*sensitivity* fix, not an output-mangling fix; framed as such. ✓ |
| 246 | +- **Placeholder scan:** Tasks 1-2 carry full code + exact line targets; Task 3 is a behavioral contract + "read the Open-Closed proof harness first" (a real test that must be matched, per the accepted convention); Task 4 is an empirical audit with concrete commands + a concrete fix rule (correct to canonical casing). No TBDs. ✓ |
| 247 | +- **Type consistency:** `MetaDataTypeId.type()/subType()/toQualifiedName()/matches()`; `ChildRequirement.getExpectedType()/getExpectedSubType()` (verify exact accessor names in Task 2). ✓ |
| 248 | +- **Hygiene:** repo-relative paths; generic `t::x`/`source.dbView` examples; no home paths/private names. ✓ |
0 commit comments