Skip to content

Commit 2673c9f

Browse files
committed
Add example-value extraction; fix Long fields; probe for ephemeral store
KonceptExtractor now picks each pattern's earliest-authored real semantic as its deterministic example, resolving the referenced component and every field value to a koncept identifier when one exists and plain display text otherwise. Feeds the guide's per-pattern shape tables (term -> schema -> example). KonceptExtractorPatternShapeIT covers both paths; note the integration module never binds failsafe (ike-parent only manages its version), so run it via surefire: mvn -pl integration test -Dtest=KonceptExtractorPatternShapeIT. EntityRecordFactory.externalToInternalObject gains the missing Long case -- any Long-valued field (STAMP Time, RosterOrder, DisplaySequence) previously crashed on external-to-internal conversion. BindingsMain now uses a store only when one is available: it probes for the Load Ephemeral Store controller with the same PluggableService discovery selectControllerByName uses, running composition inside a disposable ephemeral-store lifecycle when a provider is on the classpath (sources like ConstraintPatternSet resolve nids eagerly during PatternBuilder.at()) and store-free under the original documented contract when not, so bindings modules without a store provider (rich-surface-bindings) keep working. A store-free composition that needs nids now fails with explicit add-a-provider guidance. Bridge until the ike-knowledge-spi service contract lands (IKE-Network/ike-issues#850). Refs: IKE-Network/ike-issues#880
1 parent 836f3e1 commit 2673c9f

4 files changed

Lines changed: 229 additions & 7 deletions

File tree

entity/src/main/java/dev/ikm/tinkar/entity/EntityRecordFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ public static Object externalToInternalObject(Object externalObject) {
650650
case Float floatField -> floatField;
651651
case byte[] byteField -> byteField;
652652
case Integer integerField -> integerField;
653+
case Long longField -> longField;
653654
case String stringField -> stringField.strip();
654655
case Instant instantField -> instantField;
655656
case PlanarPoint planarPointField -> planarPointField;

entity/src/main/java/dev/ikm/tinkar/entity/builder/BindingsMain.java

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515
*/
1616
package dev.ikm.tinkar.entity.builder;
1717

18+
import dev.ikm.tinkar.common.service.CachingService;
19+
import dev.ikm.tinkar.common.service.DataServiceController;
20+
import dev.ikm.tinkar.common.service.PluggableService;
21+
import dev.ikm.tinkar.common.service.PrimitiveData;
22+
import dev.ikm.tinkar.common.service.ServiceKeys;
23+
import dev.ikm.tinkar.common.service.ServiceProperties;
24+
25+
import java.nio.file.Files;
1826
import java.nio.file.Path;
1927
import java.util.ArrayList;
2028
import java.util.List;
@@ -27,9 +35,17 @@
2735
* <p>
2836
* This is the stable reflective seam for the {@code ike:knowledge-bindings} Maven goal:
2937
* the goal loads this class in a classloader over the project's dependency classpath and
30-
* invokes {@link #main(String[])} in-process. Composition is store-free (builders defer
31-
* nid-minting work to {@link KnowledgeSet#write()}), so no datastore, providers, or
32-
* service registrations are needed on the generation classpath.
38+
* invokes {@link #main(String[])} in-process. Composition is store-free when the
39+
* source's builders defer all nid-minting to {@link KnowledgeSet#write()}; sources that
40+
* resolve nids eagerly during composition ({@code PatternBuilder.at()}/
41+
* {@code flushPendingVersion()} via {@code EntityProxy.nid()}) additionally need a
42+
* running {@link PrimitiveData} service. To serve both without forcing a store
43+
* dependency onto every consumer's generation classpath, this entry point probes for
44+
* the {@value #EPHEMERAL_STORE_CONTROLLER} controller: when present, composition runs
45+
* inside a disposable ephemeral-store lifecycle (the same pattern used by other
46+
* in-process {@code KnowledgeSetSource} consumers, e.g. {@code FoundationFidelityIT});
47+
* when absent, composition runs store-free under the original contract. Nothing is
48+
* persisted either way.
3349
* <p>
3450
* Arguments: {@code outputDir packageName className [sourceClassName]} — the source
3551
* class argument selects an implementation when the classpath provides more than one
@@ -46,6 +62,13 @@
4662
@Deprecated
4763
public final class BindingsMain {
4864

65+
/**
66+
* Controller name of the disposable in-memory store used around composition when a
67+
* provider is on the classpath. Must match the name the probe and
68+
* {@link PrimitiveData#selectControllerByName(String)} both see.
69+
*/
70+
private static final String EPHEMERAL_STORE_CONTROLLER = "Load Ephemeral Store";
71+
4972
private BindingsMain() {
5073
}
5174

@@ -81,6 +104,63 @@ public static void main(String[] args) throws Exception {
81104
source = found.getFirst();
82105
}
83106

107+
if (ephemeralStoreAvailable()) {
108+
CachingService.clearAll();
109+
ServiceProperties.set(ServiceKeys.DATA_STORE_ROOT,
110+
Files.createTempDirectory("ike-bindings").toFile());
111+
PrimitiveData.selectControllerByName(EPHEMERAL_STORE_CONTROLLER);
112+
PrimitiveData.start();
113+
try {
114+
composeAndWrite(source, outputDir, packageName, className);
115+
} finally {
116+
PrimitiveData.stop();
117+
}
118+
} else {
119+
try {
120+
composeAndWrite(source, outputDir, packageName, className);
121+
} catch (IllegalStateException e) {
122+
if (e.getMessage() != null
123+
&& e.getMessage().startsWith("No PrimitiveDataService provider available")) {
124+
throw new IllegalStateException(source.getClass().getName()
125+
+ " resolved nids during composition, which needs a running data store,"
126+
+ " but no \"" + EPHEMERAL_STORE_CONTROLLER + "\" controller is on the"
127+
+ " generation classpath. Add an ephemeral-store provider (e.g."
128+
+ " network.ike.knowledge:ike-knowledge-provider) as a runtime dependency"
129+
+ " of the bindings module.", e);
130+
}
131+
throw e;
132+
}
133+
}
134+
}
135+
136+
/**
137+
* Probes the classpath for the {@value #EPHEMERAL_STORE_CONTROLLER} controller using
138+
* the same discovery mechanism as
139+
* {@link PrimitiveData#selectControllerByName(String)}, so the probe and the
140+
* subsequent selection cannot disagree.
141+
*
142+
* @return whether an ephemeral-store controller is available to select
143+
*/
144+
private static boolean ephemeralStoreAvailable() {
145+
for (DataServiceController<?> controller : PluggableService.load(DataServiceController.class)) {
146+
if (EPHEMERAL_STORE_CONTROLLER.equals(controller.controllerName())) {
147+
return true;
148+
}
149+
}
150+
return false;
151+
}
152+
153+
/**
154+
* Composes the source and writes the generated bindings class.
155+
*
156+
* @param source the knowledge-set source to compose
157+
* @param outputDir directory the bindings source file is written under
158+
* @param packageName package of the generated class
159+
* @param className simple name of the generated class
160+
* @throws Exception if composition or writing fails
161+
*/
162+
private static void composeAndWrite(KnowledgeSetSource source, Path outputDir,
163+
String packageName, String className) throws Exception {
84164
KnowledgeSet knowledgeSet = source.compose();
85165
Path file = BindingsWriter.write(knowledgeSet, packageName, className, outputDir);
86166
System.out.println("Bindings written: " + file + " (" + knowledgeSet.declarations().size()

entity/src/main/java/dev/ikm/tinkar/entity/builder/KonceptExtractor.java

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package dev.ikm.tinkar.entity.builder;
1717

18+
import dev.ikm.tinkar.common.id.IntIdList;
19+
import dev.ikm.tinkar.common.id.IntIdSet;
1820
import dev.ikm.tinkar.common.service.PrimitiveData;
1921
import dev.ikm.tinkar.common.util.time.DateTimeUtil;
2022
import dev.ikm.tinkar.coordinate.Calculators;
@@ -35,10 +37,12 @@
3537
import dev.ikm.tinkar.entity.graph.DiTreeEntity;
3638
import dev.ikm.tinkar.entity.graph.adaptor.axiom.LogicalAxiom;
3739
import dev.ikm.tinkar.entity.graph.adaptor.axiom.LogicalExpression;
40+
import dev.ikm.tinkar.terms.EntityFacade;
3841
import dev.ikm.tinkar.terms.State;
3942
import dev.ikm.tinkar.terms.TinkarTerm;
4043
import org.eclipse.collections.api.list.ImmutableList;
4144

45+
import java.time.Instant;
4246
import java.util.ArrayList;
4347
import java.util.Comparator;
4448
import java.util.LinkedHashMap;
@@ -175,6 +179,11 @@ public static String extractYaml(Integer narrativePatternNid) {
175179
sb.append(" referencedComponentPurpose: ")
176180
.append(shape.referencedComponentPurpose()).append('\n');
177181
}
182+
if (shape.referencedComponentExample() != null) {
183+
sb.append(" referencedComponentExample: ")
184+
.append(exampleYaml(shape.referencedComponentExample(), identifierByNid.values()))
185+
.append('\n');
186+
}
178187
List<PatternFieldShape> resolvedFields = shape.fields().stream()
179188
.filter(f -> f.meaning() != null && f.purpose() != null && f.dataType() != null)
180189
.toList();
@@ -184,6 +193,11 @@ public static String extractYaml(Integer narrativePatternNid) {
184193
sb.append(" - meaning: ").append(field.meaning()).append('\n');
185194
sb.append(" purpose: ").append(field.purpose()).append('\n');
186195
sb.append(" dataType: ").append(field.dataType()).append('\n');
196+
if (field.example() != null) {
197+
sb.append(" example: ")
198+
.append(exampleYaml(field.example(), identifierByNid.values()))
199+
.append('\n');
200+
}
187201
}
188202
}
189203
}
@@ -434,18 +448,28 @@ private static List<Integer> statedParents(int componentNid) {
434448
* meaning concept, or {@code null} if unresolvable
435449
* @param referencedComponentPurpose the koncept identifier of the referenced-component
436450
* purpose concept, or {@code null} if unresolvable
451+
* @param referencedComponentExample the referenced component of a real, deterministically
452+
* chosen semantic of this pattern already in the store —
453+
* a koncept identifier if it resolves to one, otherwise its
454+
* display text — or {@code null} if this pattern has no
455+
* semantics yet
437456
* @param fields the pattern's own field definitions, in declared order
438457
*/
439458
private record PatternShape(String referencedComponentMeaning, String referencedComponentPurpose,
440-
List<PatternFieldShape> fields) {
459+
String referencedComponentExample, List<PatternFieldShape> fields) {
441460
}
442461

443462
/**
444463
* One field of a pattern: its own meaning, purpose, and data type, each a koncept
445464
* identifier, or {@code null} if that field's concept isn't itself resolvable (no FQN
446465
* description in this store).
466+
*
467+
* @param example this field's actual value on the same example semantic used for
468+
* {@link PatternShape#referencedComponentExample()} — a koncept identifier
469+
* if the value resolves to one, otherwise its display text — or {@code null}
470+
* if no example semantic was found
447471
*/
448-
private record PatternFieldShape(String meaning, String purpose, String dataType) {
472+
private record PatternFieldShape(String meaning, String purpose, String dataType, String example) {
449473
}
450474

451475
/**
@@ -461,21 +485,107 @@ private static PatternShape patternShape(int patternNid, Map<Integer, String> id
461485
PatternEntity<PatternEntityVersion> pattern = EntityService.get().getEntityFast(patternNid);
462486
PatternEntityVersion version = pattern.lastVersion();
463487
if (version == null) {
464-
return new PatternShape(null, null, List.of());
488+
return new PatternShape(null, null, null, List.of());
465489
}
466490
List<PatternFieldShape> fields = new ArrayList<>();
467491
for (FieldDefinitionForEntity field : version.fieldDefinitions()) {
468492
fields.add(new PatternFieldShape(
469493
identifierByNid.get(field.meaningNid()),
470494
identifierByNid.get(field.purposeNid()),
471-
identifierByNid.get(field.dataTypeNid())));
495+
identifierByNid.get(field.dataTypeNid()),
496+
null));
497+
}
498+
499+
String referencedComponentExample = null;
500+
ExampleSemantic example = exampleSemanticOf(patternNid);
501+
if (example != null) {
502+
referencedComponentExample = displayText(example.referencedComponentNid(), identifierByNid);
503+
for (int i = 0; i < fields.size() && i < example.fieldValues().size(); i++) {
504+
PatternFieldShape f = fields.get(i);
505+
fields.set(i, new PatternFieldShape(f.meaning(), f.purpose(), f.dataType(),
506+
displayText(example.fieldValues().get(i), identifierByNid)));
507+
}
472508
}
509+
473510
return new PatternShape(
474511
identifierByNid.get(version.semanticMeaningNid()),
475512
identifierByNid.get(version.semanticPurposeNid()),
513+
referencedComponentExample,
476514
fields);
477515
}
478516

517+
/**
518+
* The referenced component and field values of one real semantic of {@code patternNid},
519+
* chosen deterministically as the earliest-authored (by {@link #earliestStampTime}) —
520+
* reproducible across regenerations as long as this store's own authoring order doesn't
521+
* change, unlike picking by nid (assignment order is an implementation detail).
522+
*
523+
* @param patternNid the pattern whose semantics to search
524+
* @return the chosen semantic's referenced component and field values, or {@code null} if
525+
* this pattern has no semantics with a resolvable current version in this store
526+
*/
527+
private record ExampleSemantic(int referencedComponentNid, ImmutableList<Object> fieldValues) {
528+
}
529+
530+
private static ExampleSemantic exampleSemanticOf(int patternNid) {
531+
int bestNid = -1;
532+
long bestTime = Long.MAX_VALUE;
533+
for (int semanticNid : EntityService.get().semanticNidsOfPattern(patternNid)) {
534+
if (latestVersion(EntityHandle.get(semanticNid).expectSemantic()) == null) {
535+
continue;
536+
}
537+
long time = earliestStampTime(semanticNid);
538+
if (time < bestTime) {
539+
bestTime = time;
540+
bestNid = semanticNid;
541+
}
542+
}
543+
if (bestNid == -1) {
544+
return null;
545+
}
546+
SemanticEntity<?> semantic = EntityHandle.get(bestNid).expectSemantic();
547+
return new ExampleSemantic(semantic.referencedComponentNid(), latestVersion(semantic).fieldValues());
548+
}
549+
550+
/**
551+
* Display text for one example value — an entity-valued field (or the referenced
552+
* component itself) resolves to its koncept identifier when this store has one, otherwise
553+
* to {@link PrimitiveData#text}; every other {@link dev.ikm.tinkar.component.FieldDataType}
554+
* a semantic field can hold falls back to a plain rendering, mirroring
555+
* {@code SemanticVersionRecord.toString()}'s existing per-type handling without depending
556+
* on that debug format.
557+
*/
558+
private static String displayText(int nid, Map<Integer, String> identifierByNid) {
559+
String identifier = identifierByNid.get(nid);
560+
return identifier != null ? identifier : PrimitiveData.text(nid);
561+
}
562+
563+
private static String displayText(Object value, Map<Integer, String> identifierByNid) {
564+
return switch (value) {
565+
case null -> null;
566+
case EntityFacade entity -> displayText(entity.nid(), identifierByNid);
567+
case Instant instant -> DateTimeUtil.format(instant);
568+
case IntIdList intIdList -> intIdList.isEmpty() ? "(none)"
569+
: String.join(", ", intIdList.intStream()
570+
.mapToObj(memberNid -> displayText(memberNid, identifierByNid)).toList());
571+
case IntIdSet intIdSet -> intIdSet.isEmpty() ? "(none)"
572+
: String.join(", ", intIdSet.intStream()
573+
.mapToObj(memberNid -> displayText(memberNid, identifierByNid)).toList());
574+
default -> value.toString();
575+
};
576+
}
577+
578+
/**
579+
* YAML for one example value: a bare, unquoted koncept identifier when {@code value} is one
580+
* (matching {@code meaning}/{@code purpose}/{@code dataType}'s existing unquoted style, so
581+
* the renderer can badge-link it), otherwise a quoted literal via {@link #yaml} — free text
582+
* is vanishingly unlikely to collide with the single-word PascalCase identifiers this store
583+
* mints, so no separate reference/literal flag is needed.
584+
*/
585+
private static String exampleYaml(String value, java.util.Collection<String> identifiers) {
586+
return identifiers.contains(value) ? value : yaml(value);
587+
}
588+
479589
/** The latest version of a semantic by stamp time, or null. */
480590
private static SemanticEntityVersion latestVersion(SemanticEntity<?> semantic) {
481591
if (semantic == null) {

integration/src/test/java/dev/ikm/tinkar/integration/builder/KonceptExtractorPatternShapeIT.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package dev.ikm.tinkar.integration.builder;
1717

18+
import dev.ikm.tinkar.common.id.PublicIds;
1819
import dev.ikm.tinkar.entity.builder.ActiveStamp;
1920
import dev.ikm.tinkar.entity.builder.KnowledgeSet;
2021
import dev.ikm.tinkar.entity.builder.KonceptExtractor;
@@ -28,6 +29,8 @@
2829
import org.junit.jupiter.api.Test;
2930
import org.junit.jupiter.api.TestInstance;
3031

32+
import java.util.UUID;
33+
3134
import static org.junit.jupiter.api.Assertions.assertTrue;
3235

3336
/**
@@ -70,6 +73,12 @@ static void composeAndWrite() {
7073
TEST_SET.concept("Pattern shape probe field data type (Test)").at(birth)
7174
.definition("Field data-type concept for the pattern-shape probe test.")
7275
.isA(TinkarTerm.MODEL_CONCEPT);
76+
// The referenced component of the example semantic below -- a real concept, so
77+
// referencedComponentExample resolves to its own koncept identifier (the bare,
78+
// unquoted style), the same way a field value that resolves to a koncept does.
79+
TEST_SET.concept("Pattern shape probe subject (Test)").at(birth)
80+
.definition("The subject an example semantic of the probe pattern attaches to.")
81+
.isA(TinkarTerm.MODEL_CONCEPT);
7382

7483
TEST_SET.pattern("Pattern shape probe pattern (Test)").at(birth)
7584
.meaning(TEST_SET.conceptRef("Pattern shape probe meaning (Test)"))
@@ -78,6 +87,14 @@ static void composeAndWrite() {
7887
TEST_SET.conceptRef("Pattern shape probe field purpose (Test)"),
7988
TEST_SET.conceptRef("Pattern shape probe field data type (Test)"));
8089

90+
// One real semantic of the probe pattern -- exercises exampleSemanticOf()'s search and
91+
// both display-text branches: the referenced component resolves to a koncept identifier
92+
// (bare/unquoted), while this plain-String field value does not (quoted literal).
93+
TEST_SET.concept("Pattern shape probe subject (Test)").at(birth)
94+
.semantic(TEST_SET.patternRef("Pattern shape probe pattern (Test)"),
95+
PublicIds.of(UUID.fromString("7a8b5c1d-2e3f-4a5b-8c9d-0e1f2a3b4c5d")),
96+
"an example field value");
97+
8198
TEST_SET.write();
8299
}
83100

@@ -104,6 +121,20 @@ void patternShapeExtracts() {
104121
"the field's meaning/purpose/dataType must resolve correctly:\n" + block);
105122
}
106123

124+
@Test
125+
@DisplayName("a real semantic's referenced component and field values extract as live examples")
126+
void exampleValuesExtractFromARealSemantic() {
127+
String yaml = KonceptExtractor.extractYaml();
128+
String block = entryBlock(yaml, "PatternShapeProbePattern");
129+
130+
assertTrue(block.contains(" referencedComponentExample: PatternShapeProbeSubject\n"),
131+
"an entity-valued referenced component must resolve to its own koncept "
132+
+ "identifier, bare and unquoted:\n" + block);
133+
assertTrue(block.contains(" example: \"an example field value\"\n"),
134+
"a plain-String field value has no koncept identifier to resolve to, so it "
135+
+ "must render as a quoted literal, not a bare identifier:\n" + block);
136+
}
137+
107138
/** The text from an entry's identifier header to the next blank line. */
108139
private static String entryBlock(String yaml, String identifier) {
109140
int start = yaml.indexOf(identifier + ":\n");

0 commit comments

Comments
 (0)