Skip to content

Commit 80bf081

Browse files
dmealingclaude
andcommitted
feat(java): FR5a foundation — ErrorSource hierarchy, JsonPath, SemanticDiff
Per ADR-0009 / the FR5a cross-port spec, lay down the Java realization of the loader error-envelope + source-on-node foundation. New classes only — no modifications yet to the loader/parser/MetaData; commit 2 wires them in. The sealed-interface hierarchy mirrors the C# closed `abstract record` hierarchy (`server/csharp/MetaObjects/Source/ErrorSource.cs`): ErrorSource (sealed) ├── JsonSource(files [size==1], jsonPath) ← FR5a populates ├── YamlSource(files, jsonPath, yamlPosition) ← FR5b slot ├── MergedSource(files, jsonPath, contributors) ← FR5c slot ├── ResolvedSource(files, jsonPath, referrer, target) ← FR5d slot ├── DatabaseSource(dbLocation, jsonPath) ← FR5e slot └── CodeSource(caller) ← programmatic default JsonSource enforces the FR5a length-1 invariant at construction (throws IllegalArgumentException; points at MergedSource for multi-file) to match the C# constraint exactly. CodeSource.DEFAULT is the canonical singleton returned for any node not built by a loader phase. JsonPath ships the canonical builder + static one-shot helpers: - dot notation for identifier-safe keys - bracket-quoted form for everything else (escaping ' as \\') - bracket form for array indices - root is `$` SemanticDiff ships the skeleton + tests now even though FR5a doesn't exercise it — FR5c will consume the boolean for the WARN_DUPLICATE_DECLARATION path. LoaderError / LoaderWarning are envelope records ready for adoption when the loader gains a non-exception error channel (future). 24 new unit tests (14 JsonPath, 10 SemanticDiff) — all green. Refs: ADR-0009, docs/superpowers/specs/2026-05-25-fr5a-json-shape-loader-errors.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 36be2e7 commit 80bf081

17 files changed

Lines changed: 1065 additions & 0 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
/**
10+
* Programmatic / test-construction provenance — the default for any node not
11+
* built by a loader phase.
12+
*
13+
* <p>Mirrors {@code CodeSource} in
14+
* {@code server/csharp/MetaObjects/Source/ErrorSource.cs}. The
15+
* {@link #DEFAULT} singleton is the canonical no-caller instance returned by
16+
* {@code MetaData.getSource()} when nothing has been set.</p>
17+
*
18+
* @param caller optional human label (e.g. {@code "QueriesTest.makePost"});
19+
* may be {@code null}
20+
*/
21+
public record CodeSource(String caller) implements ErrorSource {
22+
23+
/** Canonical singleton for the no-caller case. */
24+
public static final CodeSource DEFAULT = new CodeSource(null);
25+
26+
/** Convenience constructor: no caller label. */
27+
public CodeSource() {
28+
this(null);
29+
}
30+
31+
@Override
32+
public String format() {
33+
return "code";
34+
}
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
/**
10+
* One contributor in a {@link MergedSource} (FR5c reserved slot).
11+
*
12+
* <p>Mirrors {@code Contributor} in
13+
* {@code server/csharp/MetaObjects/Source/ErrorSource.cs}.</p>
14+
*
15+
* @param file project-root-relative path with forward slashes
16+
* @param role one of {@code "overlay-base"} / {@code "overlay-extension"} /
17+
* {@code "extends-base"} / {@code "extends-extension"}
18+
*/
19+
public record Contributor(String file, String role) {
20+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
/**
10+
* Future provenance: database-sourced metadata (FR5e — reserved slot, gated on
11+
* FR-003; FR5a does not populate this variant).
12+
*
13+
* <p>Mirrors {@code DatabaseSource} in
14+
* {@code server/csharp/MetaObjects/Source/ErrorSource.cs}.</p>
15+
*
16+
* @param dbLocation database table + id of the originating row
17+
* @param jsonPath optional canonical JSONPath within the row's payload; may be {@code null}
18+
*/
19+
public record DatabaseSource(DbLocation dbLocation, String jsonPath) implements ErrorSource {
20+
21+
/**
22+
* Canonical constructor: rejects a null {@code dbLocation}.
23+
*
24+
* @throws NullPointerException if {@code dbLocation} is {@code null}
25+
*/
26+
public DatabaseSource {
27+
if (dbLocation == null) {
28+
throw new NullPointerException("DatabaseSource dbLocation must not be null");
29+
}
30+
}
31+
32+
/** Convenience constructor: no JSONPath. */
33+
public DatabaseSource(DbLocation dbLocation) {
34+
this(dbLocation, null);
35+
}
36+
37+
@Override
38+
public String format() {
39+
return "database";
40+
}
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
/**
10+
* Database location for a node sourced from a row (FR5e reserved slot).
11+
*
12+
* <p>Mirrors {@code DbLocation} in
13+
* {@code server/csharp/MetaObjects/Source/ErrorSource.cs}.</p>
14+
*
15+
* @param table table name (no schema prefix unless the row is in a non-default schema)
16+
* @param id identifier of the originating row (string-form regardless of underlying type)
17+
*/
18+
public record DbLocation(String table, String id) {
19+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
/**
10+
* Provenance envelope for a metadata node or loader error (FR5a / ADR-0009).
11+
*
12+
* <p>Discriminated union over the provenance variants a metadata node or error
13+
* can carry. Cross-port-aligned: every metaobjects port emits the same envelope
14+
* shape so a tool consuming errors from multiple language ports can compare
15+
* them byte-identically.</p>
16+
*
17+
* <p>This Java realization uses a {@code sealed interface} with {@code record}
18+
* permits, mirroring the C# {@code abstract record} hierarchy
19+
* ({@code server/csharp/MetaObjects/Source/ErrorSource.cs}). Pattern-match on
20+
* the concrete subtype (e.g. {@code switch (src) { case JsonSource js -> ...; }})
21+
* to access variant-specific fields.</p>
22+
*
23+
* <p>The {@link #format()} method returns the discriminant tag (one of
24+
* {@code "json"}, {@code "yaml"}, {@code "merged"}, {@code "resolved"},
25+
* {@code "database"}, {@code "code"}).</p>
26+
*
27+
* @see JsonSource for FR5a authoring-time single-file JSON provenance
28+
* @see CodeSource for the programmatic / test-construction default
29+
*/
30+
public sealed interface ErrorSource
31+
permits JsonSource, YamlSource, MergedSource, ResolvedSource, DatabaseSource, CodeSource {
32+
33+
/**
34+
* The discriminant tag — one of {@code "json"} / {@code "yaml"} /
35+
* {@code "merged"} / {@code "resolved"} / {@code "database"} / {@code "code"}.
36+
*
37+
* @return the canonical format tag for this provenance variant
38+
*/
39+
String format();
40+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.regex.Pattern;
12+
13+
/**
14+
* Canonical JSONPath builder (FR5a / ADR-0009).
15+
*
16+
* <p>Construction rules — cross-port-aligned; every port emits this canonical
17+
* form byte-identically:</p>
18+
* <ul>
19+
* <li>Root is {@code $}.</li>
20+
* <li>Object keys matching {@code ^[A-Za-z_][A-Za-z0-9_]*$} use dot notation:
21+
* {@code .foo}.</li>
22+
* <li>All other keys use single-quoted bracket form: {@code ['my-key']},
23+
* {@code ['@attr']}.</li>
24+
* <li>Array indices use bracket form: {@code [N]}.</li>
25+
* <li>Embedded single quotes are escaped as {@code \'}.</li>
26+
* <li>No trailing dots, no whitespace.</li>
27+
* </ul>
28+
*
29+
* <p>Mirrors {@code JsonPathBuilder} in
30+
* {@code server/csharp/MetaObjects/Source/JsonPath.cs} and
31+
* {@code typescript/packages/metadata/src/json-path.ts}.</p>
32+
*
33+
* <p>Static one-shot helpers ({@link #segmentForKey(String)},
34+
* {@link #segmentForIndex(int)}) cover the cases where a builder is overkill.</p>
35+
*/
36+
public final class JsonPath {
37+
38+
/** Identifier regex for dot-vs-bracket dispatch. Package-private so the builder can share it. */
39+
static final Pattern IDENT = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
40+
41+
private JsonPath() {
42+
// Static-only utility.
43+
}
44+
45+
/**
46+
* Render a single object-key segment as it would appear in canonical form,
47+
* without the leading {@code $} — i.e. {@code .foo} or {@code ['my-key']}.
48+
*
49+
* @param key the object key
50+
* @return the segment rendering
51+
*/
52+
public static String segmentForKey(String key) {
53+
if (IDENT.matcher(key).matches()) {
54+
return "." + key;
55+
}
56+
return "['" + escapeSingleQuotes(key) + "']";
57+
}
58+
59+
/**
60+
* Render a single array-index segment: {@code [N]}.
61+
*
62+
* @param idx the zero-based array index
63+
* @return the segment rendering
64+
*/
65+
public static String segmentForIndex(int idx) {
66+
return "[" + idx + "]";
67+
}
68+
69+
/**
70+
* Escape embedded single quotes in a key for bracket-quoted output.
71+
*
72+
* <p>The canonical form escapes {@code '} as {@code \'} (mirrors the C# /
73+
* TS implementations).</p>
74+
*/
75+
static String escapeSingleQuotes(String s) {
76+
if (s.indexOf('\'') < 0) {
77+
return s;
78+
}
79+
return s.replace("'", "\\'");
80+
}
81+
82+
/**
83+
* Mutable builder that mirrors the parser's tree-walk: push a key or index
84+
* when descending into a child, pop when returning.
85+
*
86+
* <p>This class is intentionally not {@link Cloneable} — each parse uses
87+
* its own builder. Not thread-safe.</p>
88+
*/
89+
public static final class Builder {
90+
91+
private final List<Segment> segments = new ArrayList<>();
92+
93+
/** Push an object-key segment (e.g. {@code .foo} or {@code ['my-key']}). */
94+
public void pushKey(String key) {
95+
segments.add(new Segment(SegmentKind.KEY, key, 0));
96+
}
97+
98+
/** Push an array-index segment (e.g. {@code [2]}). */
99+
public void pushIndex(int idx) {
100+
segments.add(new Segment(SegmentKind.INDEX, null, idx));
101+
}
102+
103+
/**
104+
* Pop the most recently pushed segment.
105+
*
106+
* <p>No-op when the stack is empty — pop balance is the caller's
107+
* responsibility, but a defensive bottom matches the C# behavior so
108+
* the builder never throws on imbalanced parse paths.</p>
109+
*/
110+
public void pop() {
111+
if (!segments.isEmpty()) {
112+
segments.remove(segments.size() - 1);
113+
}
114+
}
115+
116+
/** Number of segments currently on the stack (root is segment 0; not counted). */
117+
public int depth() {
118+
return segments.size();
119+
}
120+
121+
/**
122+
* Render the current stack as a canonical JSONPath string.
123+
*
124+
* @return the canonical form (always starts with {@code $})
125+
*/
126+
@Override
127+
public String toString() {
128+
// ~8 chars per segment is a reasonable starting cap.
129+
StringBuilder sb = new StringBuilder(segments.size() * 8 + 1);
130+
sb.append('$');
131+
for (Segment seg : segments) {
132+
if (seg.kind == SegmentKind.INDEX) {
133+
sb.append('[').append(seg.index).append(']');
134+
} else {
135+
String key = seg.key;
136+
if (IDENT.matcher(key).matches()) {
137+
sb.append('.').append(key);
138+
} else {
139+
sb.append("['").append(escapeSingleQuotes(key)).append("']");
140+
}
141+
}
142+
}
143+
return sb.toString();
144+
}
145+
146+
private enum SegmentKind { KEY, INDEX }
147+
148+
private static final class Segment {
149+
final SegmentKind kind;
150+
final String key;
151+
final int index;
152+
153+
Segment(SegmentKind kind, String key, int index) {
154+
this.kind = kind;
155+
this.key = key;
156+
this.index = index;
157+
}
158+
}
159+
}
160+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2003 Doug Mealing LLC dba Meta Objects. All Rights Reserved.
3+
*
4+
* This software is the proprietary information of Doug Mealing LLC dba Meta Objects.
5+
* Use is subject to license terms.
6+
*/
7+
package com.metaobjects.source;
8+
9+
import java.util.List;
10+
11+
/**
12+
* Authoring-time provenance: a single JSON file (FR5a).
13+
*
14+
* <p>Mirrors {@code JsonSource} in {@code server/csharp/MetaObjects/Source/ErrorSource.cs}.
15+
* FR5a invariant: <strong>exactly one file path</strong> — multi-file provenance
16+
* lives on {@link MergedSource} (FR5c). The invariant is enforced at construction
17+
* so the type can be trusted by cross-port comparison.</p>
18+
*
19+
* @param files length-1 file list; project-root-relative path with forward slashes
20+
* @param jsonPath canonical JSONPath string for the node within {@code files[0]}
21+
*/
22+
public record JsonSource(List<String> files, String jsonPath) implements ErrorSource {
23+
24+
/**
25+
* Canonical constructor: validates the FR5a length-1 invariant and returns
26+
* an immutable copy of the input list.
27+
*
28+
* @throws IllegalArgumentException if {@code files} does not contain exactly one entry
29+
* @throws NullPointerException if {@code files} or {@code jsonPath} is {@code null}
30+
*/
31+
public JsonSource {
32+
if (files == null) {
33+
throw new NullPointerException("JsonSource files must not be null");
34+
}
35+
if (jsonPath == null) {
36+
throw new NullPointerException("JsonSource jsonPath must not be null");
37+
}
38+
if (files.size() != 1) {
39+
throw new IllegalArgumentException(
40+
"JsonSource requires exactly one file path; got " + files.size()
41+
+ ". Use MergedSource for multi-file provenance.");
42+
}
43+
// Defensive copy to enforce immutability of the record component.
44+
files = List.copyOf(files);
45+
}
46+
47+
@Override
48+
public String format() {
49+
return "json";
50+
}
51+
}

0 commit comments

Comments
 (0)