Skip to content

feat: port the scope-pattern grammar to Java and C# - #496

Merged
dmealing merged 3 commits into
mainfrom
fm/mo-deps-p2-scope
Oct 28, 2026
Merged

dmealing merged 3 commits into
mainfrom
fm/mo-deps-p2-scope

Conversation

@dmealing

Copy link
Copy Markdown
Member

What Changed

  • Added com.metaobjects.config.Scope (Java, inherited by Kotlin) and MetaObjects.Config.Scope (C#). Both port compilePattern / compileScope / matchesScope from the TypeScript reference. A pattern with an empty segment, a trailing :: or an odd run of : is refused with ERR_SCOPE_PATTERN_INVALID. Java splits with a -1 limit and matches with Matcher.matches(). C# anchors with \A…\z. Nothing reads scope from .metaobjects/config.json in either port yet, so no command's behavior changes. This is S0a/S0b of FR-023 Phase 2 (FR-023 Phase 2: share a model across separate repos in Java, Kotlin and C# (Maven/NuGet packaged) #482).
  • Added ScopeConformanceTest (Java) and ScopeConformanceTests (C#). Both run the shared fixtures/scope-conformance/ corpus. Each also pins its port-specific guard (Java trailing-empty-segment, C# trailing-newline anchor), because the corpus has no invalid-pattern case.
  • Updated CHANGELOG.md, docs/CONFORMANCE.md, docs/features/metadata-sources.md and the two fixture READMEs. The scope-conformance row now shows Java, Kotlin and C# as running the corpus, and the stale "no runner yet" claims are removed.
Validation

Risk Assessment

✅ Low: The change is a small, pure library port with no I/O and no callers yet. I traced it against the TypeScript and Python references: splits keep empty segments, the same metacharacters are escaped, the patterns match the whole name, and include/exclude work the same way. The shared corpus gates it, and separate tests cover invalid patterns.

Testing

The reported failure was the 30-minute stryker mutation gate being killed by SIGTERM (exit 143) in earlier runs. It was a harness timeout, not a code defect: the diff touches no TypeScript file, and the same command passed on re-run (mutation score 56.37, above the break threshold of 35). The new Java and C# Scope classes run the shared scope corpus. They also pin the adversarial cases the corpus lacks: empty pattern, empty segment, trailing ::, odd colon run, trailing newline in the name, and a star next to a metacharacter. The full C# conformance suite and the full Java metadata module are green. Only text and command output are available as evidence because the change has no UI. The Kotlin port inherits the Java loader.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Java Scope matches every shared scope-conformance corpus case like the TS reference ✅ pass live mvn ScopeConformanceTest.everyCorpusCaseMatchesTheReference passed (surefire: tests=7 failures=0)
C# Scope matches every shared scope-conformance corpus case like the TS reference ✅ pass live dotnet test ScopeConformanceTests: 19 passed
Invalid patterns (empty, empty segment, trailing ::, odd colon run) are refused with ERR_SCOPE_PATTERN_INVALID in Java and C# ✅ pass live Java anEmptyPatternIsRefused, anEmptySegmentIsRefused, aTrailingSeparatorIsRefusedNotDropped and anOddColonRunIsRefusedNotSilentlyUnmatchable passed; C# AnInvalidPatternIsRefusedWithItsCode theory pas…
Adversarial: a name with a trailing newline never matches, and a star next to a metacharacter keeps the literal ✅ pass live Java aTrailingNewlineInTheNameNeverMatches and aStarAdjacentToAMetacharacterKeepsTheLiteral passed; C# ATrailingNewlineInTheNameNeverMatches and AStarAdjacentToAMetacharacterKeepsTheLiteral passed
Configured ts-fast/ts-unit CI command passes with no TS regression ✅ pass live ci-local-ts2.log: LOCAL CI PASSED, EXIT=0. The earlier exit 143 was a SIGTERM during the mutation gate.
Evidence: ci-local ts-fast/ts-unit passing run log
metaobjects local CI  (mode: only:ts-fast,ts-unit)

── ▶ ts build + typecheck ──────────────────────────────────────────────
bun install v1.3.14 (0d9b296a)

Checked 590 installs across 619 packages (no changes) [254.00ms]
@metaobjectsdev/forge build: Exited with code 0
@metaobjectsdev/render build: Exited with code 0
@metaobjectsdev/metadata build: Exited with code 0
@metaobjectsdev/sdk build: bundled agent-context → ~/.no-mistakes/worktrees/5b8c6c97e760/01M60YM7PY59JEGSMQWE8N73X0/server/typescript/packages/sdk/agent-context
@metaobjectsdev/conformance build: Exited with code 0
@metaobjectsdev/runtime-web build: Exited with code 0
@metaobjectsdev/docs-site build: Exited with code 0
@metaobjectsdev/sdk build: Exited with code 0
@metaobjectsdev/angular build: Exited with code 0
@metaobjectsdev/tanstack build: Exited with code 0
@metaobjectsdev/migrate-ts build: Exited with code 0
@metaobjectsdev/react build: Exited with code 0
@metaobjectsdev/runtime-ts build: Exited with code 0
@metaobjectsdev/ai-runtime build: Exited with code 0
@metaobjectsdev/codegen-ts build: Exited with code 0
@metaobjectsdev/codegen-ts-angular build: Exited with code 0
@metaobjectsdev/codegen-ts-react build: Exited with code 0
@metaobjectsdev/codegen-ts-tanstack build: Exited with code 0
@metaobjectsdev/test-generators build: Exited with code 0
@metaobjectsdev/cli build: Exited with code 0
@metaobjectsdev/forge typecheck: Exited with code 0
@metaobjectsdev/render typecheck: Exited with code 0
@metaobjectsdev/metadata typecheck: Exited with code 0
@metaobjectsdev/conformance typecheck: Exited with code 0
@metaobjectsdev/runtime-web typecheck: Exited with code 0
@metaobjectsdev/docs-site typecheck: Exited with code 0
@metaobjectsdev/sdk typecheck: Exited with code 0
@metaobjectsdev/angular typecheck: Exited with code 0
@metaobjectsdev/tanstack typecheck: Exited with code 0
@metaobjectsdev/react typecheck: Exited with code 0
@metaobjectsdev/migrate-ts typecheck: Exited with code 0
@metaobjectsdev/runtime-ts typecheck: Exited with code 0
@metaobjectsdev/ai-runtime typecheck: Exited with code 0
@metaobjectsdev/codegen-ts typecheck: Exited with code 0
@metaobjectsdev/codegen-ts-angular typecheck: Exited with code 0
@metaobjectsdev/test-generators typecheck: Exited with code 0
@metaobjectsdev/codegen-ts-react typecheck: Exited with code 0
@metaobjectsdev/codegen-ts-tanstack typecheck: Exited with code 0
@metaobjectsdev/cli typecheck: Exited with code 0
── ✓ ts build + typecheck (30s)

── ▶ conformance: typescript ──────────────────────────────────────────────
bun install v1.3.14 (0d9b296a)

Checked 590 installs across 619 packages (no changes) [10.00ms]
bun test v1.3.14 (0d9b296a)

 844 pass
 0 fail
 912 expect() calls
Ran 844 tests across 3 files. [2.32s]
bun test v1.3.14 (0d9b296a)

test/registry-coverage.test.ts:

[registry-coverage] subtypes: 53/65 exercised (81.5%), 12 UNTESTED
[registry-coverage] untested subtypes:
  attr.boolean
  attr.class
  attr.double
  attr.expression
  attr.filter
  attr.int
  attr.intMap
  attr.long
  validator.atLeastOne
  validator.comparison
  validator.presentIff
  validator.requiredWhen
[registry-coverage] exercised subtypes with untested attrs: 27

 10 pass
 0 fail
 2062 expect() calls
Ran 10 tests across 2 files. [126.00ms]
bun test v1.3.14 (0d9b296a)

 118 pass
 0 fail
 941 expect() calls
Ran 118 tests across 4 files. [81.00ms]
bun test v1.3.14 (0d9b296a)

 85 pass
 0 fail
 107 expect() calls
Ran 85 tests across 2 files. [463.00ms]
bun test v1.3.14 (0d9b296a)

 1045 pass
 34 skip
 0 fail
 2254 expect() calls
Ran 1079 tests across 161 files. [38.49s]
bun test v1.3.14 (0d9b296a)

test/format.test.ts:
[codegen-ts] Biome formatter: 2 diagnostics; first: expected `,` but instead found `'x'`

 2807 pass
 0 fail
 2 snapshots, 11875 expect() calls
Ran 2807 tests across 286 files. [104.58s]
bun test v1.3.14 (0d9b296a)

test/migrate-drop-unmanaged.test.ts:
meta: migrate: refusing to drop public.theirs — absent from the committed schema snapshot, so this toolchain never managed it and the migration could not replay against a database where it never existed. Re-run with '--allow drop-unmanaged' if the drop is intended.
meta migrate — sqlite, file:/tmp/drop-unmanaged-s85MPt/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-s85MPt/.metaobjects/migrations/20261028111700-x/up.sql
    /tmp/drop-unmanaged-s85MPt/.metaobjects/migrations/20261028111700-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-KG6eRm/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-KG6eRm/.metaobjects/migrations/20261028111700-x/up.sql
    /tmp/drop-unmanaged-KG6eRm/.metaobjects/migrations/20261028111700-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-NCfm6x/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-NCfm6x/.metaobjects/migrations/20261028111701-x/up.sql
    /tmp/drop-unmanaged-NCfm6x/.metaobjects/migrations/20261028111701-x/down.sql

meta: migrate: refusing to drop public.orders.orders_job_id_fk — absent from the committed schema snapshot, so this toolchain never managed it and the migration could not replay against a database where it never existed. Re-run with '--allow drop-unmanaged' if the drop is intended.
meta migrate — sqlite, file:/tmp/drop-unmanaged-fk-m7JIJX/t.db

  Changes: 1 drop-fk

  Written:
    /tmp/drop-unmanaged-fk-m7JIJX/.metaobjects/migrations/20261028111701-x/up.sql
    /tmp/drop-unmanaged-fk-m7JIJX/.metaobjects/migrations/20261028111701-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-fk-ApOIrT/t.db

  Changes: 1 drop-fk

  Written:
    /tmp/drop-unmanaged-fk-ApOIrT/.metaobjects/migrations/20261028111701-x/up.sql
    /tmp/drop-unmanaged-fk-ApOIrT/.metaobjects/migrations/20261028111701-x/down.sql


test/help-and-exit.test.ts:
meta gen — codegen TS targets from your declared metadata

USAGE:
  meta gen [<entity>...] [flags]

FLAGS:
  --dry-run             Compute and print, don't write
  --list                Print the generator CATALOG (name, layer, tier, what it emits,
                        what to install) and exit. Codegen is opt-in: nothing runs until
                        you wire it, and this is where you choose. No config or metadata
                        required — add --format json for the same catalog, machine-readable.
  --probe               With --list: construct every catalog generator and dry-run it
                        against YOUR model, reporting how many files each would emit.
                        A count per generator beats any category label — and it cannot go
                        stale, because it runs the generators. Needs a project.
  --baseline <default|adopt|fresh>
                        First-time-on-existing-file behavior. Default: refuse a file that
                        cannot be proved to be generated output. "adopt" records the files
                        you have as the merge base and writes NOTHING — the first run a
                        project with no committed .hashes.json can perform. "fresh"
                        overwrites them with fresh output and re-baselines (discards edits).
  --no-antipatterns     Suppress the advisory "hand-rolled what MetaObjects can model" pass
  --limit <n|all>       Advisory lines TEXT output prints before truncating (default 20)
  --generators <names>  Narrow the run to these DECLARED generators, by stable name, comma-
                        separated (e.g. docs,api-docs to write only the documentation). A
                        name metaobjects.config.ts does not declare is refused: it never adds one.
  --format <toon|json|text>   Output format (global flag; default toon off-TTY)
  <entity> [<entity>]   Positional filter on entity names
  --help, -h            Print this help

A real write run (not --dry-run) also runs an ADVISORY anti-pattern pass: it scans
your authored source for hand-rolled aggregates, money-as-float, and CHECK-IN enums
and 

... [419751 bytes truncated] ...

_UNIQUE_KEY_SUBTYPES.includes(node.subType) || IDENTITY_UNIQUE_KEY_SUBTYPES.includes(target.subType)

[Survived] StringLiteral
src/super-resolve.ts:109:3
-     "a dotted extends must target a node of the same type and subtype — the one exception " +
+     "" +

[Survived] StringLiteral
src/super-resolve.ts:110:3
-     "is an identity, which may extend any UNIQUE key (identity.primary or identity.secondary)";
+     "";

[Survived] ConditionalExpression
src/super-resolve.ts:122:20
-     const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
+     const segStart = true ? 0 : lastSep + PACKAGE_SEPARATOR.length;

[Survived] ArithmeticOperator
src/super-resolve.ts:122:41
-     const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
+     const segStart = lastSep === -1 ? 0 : lastSep - PACKAGE_SEPARATOR.length;

[Survived] ConditionalExpression
src/super-resolve.ts:125:7
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (false) return undefined;

[Survived] LogicalOperator
src/super-resolve.ts:125:7
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (parts.length < 2 && parts.some(p => p === "")) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:125:7
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (false || parts.some((p) => p === "")) return undefined;

[Survived] ArrowFunction
src/super-resolve.ts:125:38
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (parts.length < 2 || parts.some(() => undefined)) return undefined;

[Survived] MethodExpression
src/super-resolve.ts:125:27
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (parts.length < 2 || parts.every(p => p === "")) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:125:45
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (parts.length < 2 || parts.some((p) => false)) return undefined;

[Survived] StringLiteral
src/super-resolve.ts:125:51
-     if (parts.length < 2 || parts.some((p) => p === "")) return undefined;
+     if (parts.length < 2 || parts.some((p) => p === "Stryker was here!")) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:173:9
-       if (referrerScope === undefined) return undefined;
+       if (false) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:175:9
-       if (parsed === undefined) return undefined; // degenerate (empty segment)
+       if (false) return undefined; // degenerate (empty segment)

[Survived] ConditionalExpression
src/super-resolve.ts:177:9
-       if (current === undefined) return undefined;
+       if (false) return undefined;

[Survived] ConditionalExpression
src/super-resolve.ts:195:9
-       if (found !== undefined) return found;
+       if (true) return found;

[Survived] ArrayDeclaration
src/super-resolve.ts:264:31
-     const pending: MetaData[] = [];
+     const pending: MetaData[] = ["Stryker was here"];

[Survived] ConditionalExpression
src/super-resolve.ts:266:9
-       if (n.superRef !== undefined) pending.push(n);
+       if (true) pending.push(n);

[Survived] ConditionalExpression
src/super-resolve.ts:278:11
-         if (e instanceof RefOverDropError) return "over-drop";
+         if (true) return "over-drop";

[Survived] ConditionalExpression
src/super-resolve.ts:284:40
-       if (node.superRef === undefined || node.superResolved !== undefined) return;
+       if (node.superRef === undefined || false) return;

[Survived] ConditionalExpression
src/super-resolve.ts:285:9
-       if (attempted.has(node)) return; // already resolved-or-failed this pass — never re-report
+       if (false) return; // already resolved-or-failed this pass — never re-report

[Survived] StringLiteral
src/super-resolve.ts:287:69
-       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "";
+       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "Stryker was here!";

[Survived] ConditionalExpression
src/super-resolve.ts:293:9
-       if (isChildTargetingRef(node.superRef)) {
+       if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:295:11
-         if (parsed !== undefined) {
+         if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (true) resolveNode(ownerNode);

[Survived] ConditionalExpression
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (true && ownerNode !== "over-drop") resolveNode(ownerNode);

[Survived] LogicalOperator
src/super-resolve.ts:297:13
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined || ownerNode !== "over-drop") resolveNode(ownerNode);

[Survived] ConditionalExpression
src/super-resolve.ts:297:40
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined && true) resolveNode(ownerNode);

[Survived] StringLiteral
src/super-resolve.ts:297:54
-           if (ownerNode !== undefined && ownerNode !== "over-drop") resolveNode(ownerNode);
+           if (ownerNode !== undefined && ownerNode !== "") resolveNode(ownerNode);

[Survived] ObjectLiteral
src/super-resolve.ts:332:19
-             target: { type: target.type, subType: target.subType },
+             target: {},

[Survived] ObjectLiteral
src/super-resolve.ts:333:21
-             referrer: { type: node.type, subType: node.subType },
+             referrer: {},

[Survived] StringLiteral
src/super-resolve.ts:351:15
-           kind: "unresolved",
+           kind: "",

Ran 0.96 tests per mutant on average.
--------------------|------------------|----------|-----------|------------|----------|----------|
                    | % Mutation score |          |           |            |          |          |
File                |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
All files           |  56.37 |   56.37 |      921 |        74 |        770 |        0 |        0 |
 parser-core.ts     |  48.37 |   48.37 |      452 |        53 |        539 |        0 |        0 |
 provider.ts        |  61.54 |   61.54 |       39 |         1 |         25 |        0 |        0 |
 serializer-json.ts |  64.80 |   64.80 |      192 |        16 |        113 |        0 |        0 |
 subtype-rules.ts   |  69.23 |   69.23 |      117 |         0 |         52 |        0 |        0 |
 super-resolve.ts   |  75.30 |   75.30 |      121 |         4 |         41 |        0 |        0 |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
�[32m07:31:43 (2499873) INFO MutationTestReportHelper�[39m Final mutation score of 56.37 is greater than or equal to break threshold 35
�[32m07:31:44 (2499873) INFO HtmlReporter�[39m Your report can be found at: file://~/.no-mistakes/worktrees/5b8c6c97e760/01M60YM7PY59JEGSMQWE8N73X0/server/typescript/packages/metadata/reports/mutation/mutation.html
�[32m07:31:44 (2499873) INFO MutationTestExecutor�[39m Done in 12 minutes and 33 seconds.
── ✓ completeness-gate (mutation) (12m34s)

══════════════════════ SUMMARY ══════════════════════
  ✓ ts build + typecheck  (30s)
  ✓ conformance: typescript  (4m20s)
  ✓ ts unit suites  (17s)
  ✓ completeness-gate (mutation)  (12m34s)
── slowest steps ──
    12m34s  completeness-gate (mutation)
     4m20s  conformance: typescript
       30s  ts build + typecheck
       17s  ts unit suites
  total wall time: 17m41s
══════════════════════════════════════════════════════
LOCAL CI PASSED
EXIT=0
  • Outcome: ⚠️ 1 error across 1 run (1h44m29s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ server/java/metadata/src/main/java/com/metaobjects/config/Scope.java:62 - The Javadoc on Scope.Compiled said 'Both arrays are already anchored to the whole name'. That is false in Java: compilePattern builds the Pattern with no anchors, and only Matcher.matches() in matchesScope makes the match cover the whole name. A later consumer that trusted the comment and called find() on the public include()/exclude() lists would get substring matches. I rewrote the comment to say the patterns carry no anchors and must be matched with Matcher.matches(), never find(). The C# CompiledScope comment is accurate because its patterns use \A…\z anchors.
⚠️ **Test** - 1 error
  • 🚨 configured test command failed with exit code 1
  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Java Scope matches every shared scope-conformance corpus case like the TS reference ✅ pass live mvn ScopeConformanceTest.everyCorpusCaseMatchesTheReference passed (surefire: tests=7 failures=0)
C# Scope matches every shared scope-conformance corpus case like the TS reference ✅ pass live dotnet test ScopeConformanceTests: 19 passed
Invalid patterns (empty, empty segment, trailing ::, odd colon run) are refused with ERR_SCOPE_PATTERN_INVALID in Java and C# ✅ pass live Java anEmptyPatternIsRefused, anEmptySegmentIsRefused, aTrailingSeparatorIsRefusedNotDropped and anOddColonRunIsRefusedNotSilentlyUnmatchable passed; C# AnInvalidPatternIsRefusedWithItsCode theory pas…
Adversarial: a name with a trailing newline never matches, and a star next to a metacharacter keeps the literal ✅ pass live Java aTrailingNewlineInTheNameNeverMatches and aStarAdjacentToAMetacharacterKeepsTheLiteral passed; C# ATrailingNewlineInTheNameNeverMatches and AStarAdjacentToAMetacharacterKeepsTheLiteral passed
Configured ts-fast/ts-unit CI command passes with no TS regression ✅ pass live ci-local-ts2.log: LOCAL CI PASSED, EXIT=0. The earlier exit 143 was a SIGTERM during the mutation gate.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • mvn -o -pl metadata test -Dtest=ScopeConformanceTest (7 tests, 0 failures)
  • dotnet test MetaObjects.Conformance.Tests --filter ScopeConformanceTests (19 passed)
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (build+typecheck, TS conformance, TS unit suites, mutation gate; exit 0 in 17m41s)
  • dotnet test MetaObjects.Conformance.Tests (full, 2113 passed)
  • mvn -o -pl metadata test (full Java metadata module, 2891 tests, 0 errors/failures)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…orpus (FR-023 Phase 2 S0a/S0b)

Java (Kotlin inherits) and C# gain compilePattern/compileScope/matchesScope
matching the TypeScript reference, gated by fixtures/scope-conformance/.
@dmealing
dmealing merged commit d54c9fd into main Oct 28, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-deps-p2-scope branch October 28, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant