Skip to content

Commit cab4f83

Browse files
dmealingclaude
andcommitted
feat(fr-033): Java port S-B1 — embed spec/metamodel JSON + reader + descriptions
Bundles the 15 shared spec/metamodel/*.json into the metadata module (byte-gated) and adds a reader that sources every type/attr/common-attr description (+ rules/ example/whenToUse) from them onto the Java registry — single-sourced, byte-identical to TS, never hand-copied. The manifest's description fields now match the golden; RegistryManifestConformanceTest stays RED pending the strict children graph + cardinality + per-subtype attr scoping reconciliation (sub-step B2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ace7949 commit cab4f83

7 files changed

Lines changed: 615 additions & 10 deletions

File tree

server/java/metadata/pom.xml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@
2828
</execution>
2929
</executions>
3030
</plugin>
31+
<!-- FR-033: bundle the repo-root spec/metamodel/*.json (the 15 shared
32+
provider-definition files; byte-identical cross-port by design) into
33+
the metadata jar at spec/metamodel/, so the SpecMetamodelReader can
34+
read them off the classpath at registration time (AOT-safe, ADR-0001
35+
— embed, never runtime-scan an arbitrary filesystem path). The
36+
byte-identity gate (SpecMetamodelEmbedTest) asserts the embedded copy
37+
equals the repo-root source. -->
38+
<plugin>
39+
<groupId>org.apache.maven.plugins</groupId>
40+
<artifactId>maven-resources-plugin</artifactId>
41+
<executions>
42+
<execution>
43+
<id>bundle-spec-metamodel</id>
44+
<phase>generate-resources</phase>
45+
<goals>
46+
<goal>copy-resources</goal>
47+
</goals>
48+
<configuration>
49+
<outputDirectory>${project.build.outputDirectory}/spec/metamodel</outputDirectory>
50+
<resources>
51+
<resource>
52+
<directory>${maven.multiModuleProjectDirectory}/../../spec/metamodel</directory>
53+
<includes>
54+
<include>*.json</include>
55+
</includes>
56+
<filtering>false</filtering>
57+
</resource>
58+
</resources>
59+
</configuration>
60+
</execution>
61+
</executions>
62+
</plugin>
3163
</plugins>
3264
</build>
3365

server/java/metadata/src/main/java/com/metaobjects/registry/ChildRequirement.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,23 @@ public String getDocDescription() {
320320
return docDescription;
321321
}
322322

323+
/**
324+
* FR-033 — a copy of this requirement carrying the given doc description, with
325+
* all other facets (name/type/subType/required + the constraint matchers /
326+
* constraintId / validationDescription) preserved. Used by
327+
* {@link MetaDataRegistry#applySpecDescriptions} to thread the
328+
* {@code spec/metamodel/*.json} attr description onto an already-registered
329+
* requirement without disturbing its placement/validation behavior.
330+
*
331+
* @param docDescription the FR-033 doc description to carry
332+
* @return a new ChildRequirement identical apart from the doc description
333+
*/
334+
public ChildRequirement withDocDescription(String docDescription) {
335+
return new ChildRequirement(name, expectedType, expectedSubType, required,
336+
parentMatcher, childMatcher, valueValidator, constraintId, validationDescription,
337+
docDescription);
338+
}
339+
323340
@Override
324341
public boolean equals(Object obj) {
325342
if (this == obj) return true;

server/java/metadata/src/main/java/com/metaobjects/registry/CommonAttributeDef.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@
2020
* (every type / every subType). Used by the cross-language commonAttrs
2121
* contract (documentation provider in TS / C# / Python / Java).
2222
*
23-
* @param name bare attribute name (no {@code @} prefix; e.g. {@code "description"})
24-
* @param valueType attribute value subtype (e.g. {@code StringAttribute.SUBTYPE_STRING},
25-
* {@code BooleanAttribute.SUBTYPE_BOOLEAN})
26-
* @param isArray {@code true} if the attribute holds an array of {@code valueType}
27-
* (e.g. {@code aliases}, {@code seeAlso}, {@code replacedBy})
23+
* @param name bare attribute name (no {@code @} prefix; e.g. {@code "description"})
24+
* @param valueType attribute value subtype (e.g. {@code StringAttribute.SUBTYPE_STRING},
25+
* {@code BooleanAttribute.SUBTYPE_BOOLEAN})
26+
* @param isArray {@code true} if the attribute holds an array of {@code valueType}
27+
* (e.g. {@code aliases}, {@code seeAlso}, {@code replacedBy})
28+
* @param description FR-033 — the human/AI-facing documentation prose, sourced from the
29+
* universal {@code *.*} entry of the embedded {@code spec/metamodel/
30+
* documentation.json}. {@code ""} when not sourced (back-compat). The
31+
* registry-conformance manifest emits this on each common attr.
2832
*/
29-
public record CommonAttributeDef(String name, String valueType, boolean isArray) {
33+
public record CommonAttributeDef(String name, String valueType, boolean isArray, String description) {
34+
35+
/** Back-compat constructor — no description (empty string). */
36+
public CommonAttributeDef(String name, String valueType, boolean isArray) {
37+
this(name, valueType, isArray, "");
38+
}
39+
40+
/** A copy of this def carrying the given FR-033 description. */
41+
public CommonAttributeDef withDescription(String description) {
42+
return new CommonAttributeDef(name, valueType, isArray, description != null ? description : "");
43+
}
3044
}

server/java/metadata/src/main/java/com/metaobjects/registry/MetaDataRegistry.java

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,134 @@ public Collection<CommonAttributeDef> getCommonAttributes() {
712712
return List.copyOf(commonAttributes.values());
713713
}
714714

715+
/**
716+
* FR-033 (sub-step B1) — source every type / attr / common-attr <em>description</em>
717+
* (+ optional {@code rules}/{@code example}/{@code whenToUse}) from the shared
718+
* {@code spec/metamodel/*.json} (the cross-port single source of truth, read by
719+
* {@link com.metaobjects.registry.spec.SpecMetamodelReader}) onto this registry.
720+
*
721+
* <p>Applied DURING composition, BEFORE {@link #seal()} (the type definitions are
722+
* immutable, so each {@link TypeDefinition} carrying a JSON description is rebuilt
723+
* and re-put; each attr {@link ChildRequirement} that the JSON describes is copied
724+
* with its {@code docDescription}; each {@link CommonAttributeDef} is replaced by a
725+
* copy carrying the universal {@code *.*} description). Descriptions come from the
726+
* JSON ONLY — never hand-copied — so they are byte-identical to the TS reference.</p>
727+
*
728+
* <p>Where Java registers an attr the JSON does NOT describe for that subtype (or
729+
* vice-versa) this is left untouched — that scoping mismatch is reconciled in
730+
* sub-step B2; B1 applies descriptions only where they match.</p>
731+
*
732+
* @param reader the parsed embedded spec/metamodel reader
733+
*/
734+
public synchronized void applySpecDescriptions(com.metaobjects.registry.spec.SpecMetamodelReader reader) {
735+
Objects.requireNonNull(reader, "reader must not be null");
736+
checkNotSealed("applySpecDescriptions");
737+
738+
// Pass 1 — rebuild every TypeDefinition with the JSON type docs + per-attr
739+
// doc descriptions on its DIRECT (+ wildcard) child requirements.
740+
for (Map.Entry<MetaDataTypeId, TypeDefinition> entry : new ArrayList<>(typeDefinitions.entrySet())) {
741+
MetaDataTypeId id = entry.getKey();
742+
TypeDefinition def = entry.getValue();
743+
744+
com.metaobjects.registry.spec.SpecMetamodelReader.DocFacet typeDoc =
745+
reader.typeDoc(id.type(), id.subType());
746+
String description = (typeDoc != null && typeDoc.description() != null)
747+
? typeDoc.description() : def.getDescription();
748+
String rules = typeDoc != null ? typeDoc.rules() : def.getRules();
749+
String example = typeDoc != null ? typeDoc.example() : def.getExample();
750+
String whenToUse = typeDoc != null ? typeDoc.whenToUse() : def.getWhenToUse();
751+
752+
// Rebuild the DIRECT child requirements, threading attr doc descriptions.
753+
Map<String, ChildRequirement> directReqs = new LinkedHashMap<>();
754+
for (ChildRequirement req : def.getDirectChildRequirements()) {
755+
ChildRequirement rebuilt = req;
756+
if (MetaAttribute.TYPE_ATTR.equals(req.getExpectedType())
757+
&& req.getName() != null && !"*".equals(req.getName())) {
758+
com.metaobjects.registry.spec.SpecMetamodelReader.AttrEntry attrDoc =
759+
reader.attrDoc(id.type(), id.subType(), req.getName());
760+
if (attrDoc != null && attrDoc.description() != null) {
761+
rebuilt = req.withDocDescription(attrDoc.description());
762+
}
763+
}
764+
String key = rebuilt.getName();
765+
if (key == null || "*".equals(key)) {
766+
key = "*:" + rebuilt.getExpectedType() + ":" + rebuilt.getExpectedSubType();
767+
}
768+
directReqs.put(key, rebuilt);
769+
}
770+
771+
TypeDefinition rebuiltDef = new TypeDefinition(
772+
def.getImplementationClass(), id.type(), id.subType(), description,
773+
directReqs, def.getParentType(), def.getParentSubType(),
774+
rules, example, whenToUse, def.getParents());
775+
typeDefinitions.put(id, rebuiltDef);
776+
}
777+
778+
// Pass 2 — re-resolve inheritance so each child re-inherits the REBUILT parent
779+
// requirements (carrying descriptions). Crucially, an inherited attr req is
780+
// re-described against the CHILD's own (type, subType): the JSON sometimes
781+
// scopes an attr to a concrete subtype (e.g. @discriminator on object.entity)
782+
// while Java declares it on the base (object.base) and inherits it. Describing
783+
// the inherited copy by the child subtype lands the JSON description on the
784+
// child where the cross-port golden carries it, and correctly leaves it empty
785+
// on a sibling the JSON does NOT scope it to. Where Java's inheritance direction
786+
// disagrees with the JSON scope, that is the B2 scoping reconciliation — B1 only
787+
// applies the description where the JSON declares it for that subtype.
788+
for (TypeDefinition def : new ArrayList<>(typeDefinitions.values())) {
789+
if (def.hasParent()) {
790+
TypeDefinition parent = typeDefinitions.get(
791+
new MetaDataTypeId(def.getParentType(), def.getParentSubType()));
792+
if (parent != null) {
793+
resolveInheritanceForDefinition(def, parent);
794+
describeInheritedAttrs(def, reader);
795+
}
796+
}
797+
}
798+
799+
// Pass 3 — common-attr descriptions from the universal *.* documentation entry.
800+
for (Map.Entry<String, CommonAttributeDef> e : new ArrayList<>(commonAttributes.entrySet())) {
801+
com.metaobjects.registry.spec.SpecMetamodelReader.AttrEntry doc =
802+
reader.commonAttrDoc(e.getKey());
803+
if (doc != null && doc.description() != null) {
804+
commonAttributes.put(e.getKey(), e.getValue().withDescription(doc.description()));
805+
}
806+
}
807+
}
808+
809+
/**
810+
* FR-033 (sub-step B1) — re-describe a definition's INHERITED attr requirements
811+
* against the definition's OWN {@code (type, subType)}, so a JSON description that
812+
* the spec scopes to a concrete subtype lands on the inherited copy where the
813+
* cross-port golden carries it. Replaces each inherited attr requirement with a
814+
* doc-described copy when the spec describes that attr for this subtype.
815+
*/
816+
private void describeInheritedAttrs(TypeDefinition def,
817+
com.metaobjects.registry.spec.SpecMetamodelReader reader) {
818+
Map<String, ChildRequirement> inherited = def.getInheritedChildRequirements();
819+
if (inherited.isEmpty()) {
820+
return;
821+
}
822+
Map<String, ChildRequirement> redescribed = new LinkedHashMap<>(inherited);
823+
boolean changed = false;
824+
for (Map.Entry<String, ChildRequirement> e : inherited.entrySet()) {
825+
ChildRequirement req = e.getValue();
826+
if (!MetaAttribute.TYPE_ATTR.equals(req.getExpectedType())
827+
|| req.getName() == null || "*".equals(req.getName())) {
828+
continue;
829+
}
830+
com.metaobjects.registry.spec.SpecMetamodelReader.AttrEntry doc =
831+
reader.attrDoc(def.getType(), def.getSubType(), req.getName());
832+
if (doc != null && doc.description() != null
833+
&& !doc.description().equals(req.getDocDescription())) {
834+
redescribed.put(e.getKey(), req.withDocDescription(doc.description()));
835+
changed = true;
836+
}
837+
}
838+
if (changed) {
839+
def.populateInheritedRequirements(redescribed);
840+
}
841+
}
842+
715843
/**
716844
* Lightweight array-shape predicate for common-array-attr validation. Mirrors
717845
* the per-type {@code AttributeConstraintBuilder.isArrayValue} contract:

server/java/metadata/src/main/java/com/metaobjects/registry/RegistryManifest.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,20 @@ private RegistryManifest() {
106106
* @return a new registry composed from the metamodel provider set
107107
*/
108108
public static MetaDataRegistry composeMetamodelRegistry() {
109-
return MetaDataRegistry.compose(metamodelProviders());
109+
MetaDataRegistry registry = MetaDataRegistry.compose(metamodelProviders());
110+
// Force the lazy core-constraint init NOW: it expands the named inherited
111+
// attr child-requirements (e.g. field.base's `required`/`default`/`unique`
112+
// onto each concrete field subtype). Those named requirements must exist
113+
// BEFORE applySpecDescriptions rebuilds the type definitions, otherwise the
114+
// rebuild would not see them and their descriptions would never land.
115+
registry.getAllValidationConstraints();
116+
// FR-033 (sub-step B1): source every type / attr / common-attr description
117+
// (+ rules/example/whenToUse) from the embedded spec/metamodel/*.json — the
118+
// cross-port single source of truth — onto the freshly-composed registry,
119+
// BEFORE any seal. Single-sourced, byte-identical to TS; never hand-copied.
120+
registry.applySpecDescriptions(
121+
com.metaobjects.registry.spec.SpecMetamodelReader.load());
122+
return registry;
110123
}
111124

112125
/**
@@ -572,12 +585,13 @@ public static String emit(MetaDataRegistry registry) {
572585
// commonAttrs: sorted by name. An array-shaped common attr is the scalar
573586
// value-type plus the orthogonal isArray flag (the retired stringarray
574587
// subtype) — matching the cross-port {valueType, isArray} contract. The
575-
// FR-033 per-commonAttr description has no Java source yet (CommonAttributeDef
576-
// carries none) → empty string; sub-step B sources it from the embedded JSON.
588+
// FR-033 (sub-step B): the per-commonAttr description is now sourced from the
589+
// universal *.* entry of the embedded spec/metamodel/documentation.json and
590+
// applied onto each CommonAttributeDef at composition time (pre-seal).
577591
List<ManifestAttr> commonAttrs = new ArrayList<>();
578592
for (CommonAttributeDef def : registry.getCommonAttributes()) {
579593
commonAttrs.add(new ManifestAttr(def.name(), def.valueType(), def.isArray(), false,
580-
"", null, null, null));
594+
def.description() != null ? def.description() : "", null, null, null));
581595
}
582596
commonAttrs.sort(Comparator.comparing(ManifestAttr::name));
583597

0 commit comments

Comments
 (0)