Skip to content

Metamodel + knowledge bundles#50

Open
jimador wants to merge 49 commits into
embabel:mainfrom
jimador:feat/metamodel-and-bundles
Open

Metamodel + knowledge bundles#50
jimador wants to merge 49 commits into
embabel:mainfrom
jimador:feat/metamodel-and-bundles

Conversation

@jimador

@jimador jimador commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Metamodel + knowledge bundles

Last of the four-PR stack. Stacked on #49 — review after #47#49 merge; until then the diff
includes their commits.

Carves out two modules: dice-metamodel, dice-bundle.

What's in it

  • Version the metamodel and catch drift before it corrupts the graph (Version the metamodel and catch drift before it corrupts the graph #45) — versions the
    metamodel so each version can be diffed against the last, and quarantines mention-types that drift
    from the model for review instead of letting them silently land in the graph.
  • Snapshot and move what DICE knows (Snapshot and move what DICE knows #46) — a local export/import format for knowledge bundles,
    so a graph (propositions, provenance, and decay status) can be written out and reloaded faithfully
    in another instance.

Importer hardening

The Jackson bundle importer's parse path was tightened so untrusted input can't slip past the guards:

  • A present-but-non-textual formatVersion is rejected through the version gate rather than coerced
    to the current version.
  • Empty, whitespace, and null input returns a clean ParseFailure instead of throwing.
  • The maxBundleBytes cap now applies to all three import paths (importFromString,
    importFromStream, importFromReader); the stream and reader paths are bounded mid-read, and the
    reader caps by UTF-8 byte length rather than character count.

No pre-existing design issues map to this PR.

Closes #45
Closes #46

@jimador jimador requested review from jasperblues and johnsonr June 19, 2026 12:28
@jimador jimador force-pushed the feat/metamodel-and-bundles branch 10 times, most recently from 12bd5e6 to a930a0e Compare June 22, 2026 19:41
@jimador jimador force-pushed the feat/metamodel-and-bundles branch 5 times, most recently from 691debd to 11fdd8a Compare June 24, 2026 15:02
jimador added 12 commits June 30, 2026 09:38
Versions the schema and guards it against silent drift.

- MetamodelVersion, MetamodelDiff, and MetamodelDiffer (Javers-backed) version the
  schema and diff one version against another
- DriftQuarantinePolicy with MentionTypeDriftQuarantinePolicy quarantines a mention
  type the current metamodel doesn't know about, instead of letting it drift the
  schema silently
- MetamodelConfiguration ties it together

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Exports a slice of knowledge to a portable bundle and imports it back, locally.

- KnowledgeBundle with KnowledgeBundleExporter / KnowledgeBundleImporter, both
  Jackson-backed
- the import result reports what was added versus skipped

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Test-only coverage that pairs with the lifecycle, provenance, and agent surfaces.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Add docs/design/metamodel-governance.md (versioned schema stamps, additive-vs-lossy
diffing, and non-destructive drift quarantine) and docs/design/knowledge-bundles.md
(carrying the full proposition so a reloaded fact keeps its decay anchor, and
treating an incoming bundle as untrusted). Add module AGENTS.md for dice-metamodel
and dice-bundle, and list them in the root guide.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…dle paths

Wire SLF4J loggers through the bundle export and metamodel diff/wiring seams:

- bundle: log export proposition counts and context
- metamodel: log diff result counts (added/removed/modified types and
  relationships) and which default policy implementations get wired

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
JacksonKnowledgeBundleImporter guarded the import size with serialised.length,
which counts UTF-16 code units. The constant, the log message, and the failure
reason all say "bytes", so a bundle of multi-byte characters could be up to ~2x
the intended limit yet still pass. Measure the real UTF-8 byte length instead.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
This branch adds a dedicated DecayStatusPolicyTest in the common test package.
Now that DecayStatusPolicy lives in spi, mirror its test there too so it accesses
the type same-package, matching the other spi tests. No assertion changes.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
The metamodel-governance and knowledge-bundles design notes existed but weren't
linked anywhere. Add them to the README design-notes index and the root navigation
guide so the governance and transfer subsystems are discoverable.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Compare entity-type label and property sets directly in JaversMetamodelDiffer
instead of a space-joined string projection. The string form could collapse two
genuinely different sets (a label or property whose name contains the delimiter)
into a false 'unchanged', silently dropping a lossy modification and skipping
quarantine. Removes the now-dead TypeShapeSnapshot machinery.

Merge same-named domain types by union when stamping a MetamodelVersion. A
DataDictionary can hold two types that share a name but differ in shape, and
associate() silently kept only the last — dropping the other's labels/properties
from the content hash so their later removal would go undetected. groupBy + union
keeps the fingerprint complete.

Count an OVERWRITE that replaces a pre-existing proposition separately from a
net-new import and leave a note for it, so an idempotency audit can see the
destructive write. Dedup by id within a single bundle so a repeated id can't
double-write or double-count; the repeat is skipped with a note. Document that
SKIP_EXISTING's skip is best-effort under concurrent imports to the same store.

Give the drift sweep's already-quarantined pass-through an observable trace:
count it apart from genuinely-conforming propositions and surface the number in
the summary log.

Tests: differ delimiter-collision regression; same-named-type union merge;
bundle overwrite counting and within-bundle id dedup; a round-trip test asserting
the abstraction, pinning, and reinforcement fields survive; and a status
assertion that a re-swept proposition stays STALE.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…cy-safe

The bundle importer's SKIP_EXISTING path read the store then wrote, so two concurrent
imports of the same id could both see 'absent' and both write, losing the skip. Add a
saveIfAbsent primitive to the PropositionStore SPI: it writes only when the id is absent
and returns null otherwise. The SPI default stays read-then-write (documented best-effort);
the in-memory store overrides it with ConcurrentHashMap.putIfAbsent and the Drivine store
runs the check and insert inside its existing stripe-locked transaction, both genuinely
atomic. The event-emitting decorator forwards to the delegate's saveIfAbsent so the atomic
guarantee survives the decorator and a single PropositionPersisted fires only on a real
insert. The importer's SKIP_EXISTING path now writes through saveIfAbsent.

Tests: a cross-backend contract test (run against both the in-memory and the Neo4j store)
proving insert-once-by-id; an in-memory concurrency test proving exactly one writer wins a
contended id; and decorator tests proving saveIfAbsent emits on insert and stays silent on
a skip.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
The test was disabled because the runtime wiring that fires a trigger-bound @action was
considered unverifiable here. It is verifiable and deterministic: build the agent straight
from the annotated bean with AgentMetadataReader, drop a PropositionPersisted on the
blackboard as the last result, and let the in-process GOAP planner run the single triggered
action — no LLM, no threads. Adding @AchievesGoal gives the planner a goal to plan toward
(the trigger precondition is excluded from the goal, so it stays achievable once the action
runs). Now asserts the action actually fired.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
jimador added 30 commits July 4, 2026 15:30
…criptors

diffAgainstObserved regex-parsed the bare relationship type name out of
MetamodelVersion's rendered "From-[name]->To" descriptors. Since these names
are free-text/LLM-derived, a name can itself contain a "-[...]->"-shaped
substring, and the greedy regex would backtrack to the wrong segment,
silently misclassifying drift.

Change DeclaredObservedDiffer.diffAgainstObserved to take the declared bare
relationship type names as an explicit parameter instead of recovering them
by parsing. Callers hold the un-rendered relationship definitions before
they're stamped into a MetamodelVersion and can supply the set directly, no
lossy round-trip required.

Also drop ObservedSchema.entityTypeCounts: nothing reads it and the
quarantine runner keys off type names only, so it was untested dead data
rather than a real requirement.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Add exportScoped(contextId) and exportAllContexts() to export propositions from
the repository filtered by context. The exporter reads only the specified
context via findByContextIdValue and assembles one bundle per context.
All propositions in memory per context (no streaming).

- exportScoped() returns a single KnowledgeBundle for a given context ID
- exportAllContexts() returns one bundle per distinct context in the store
- empty contexts return valid empty bundles with correct contextId
- existing serialization methods unchanged; all round-trip tests pass

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
… drift diff (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…RITE honest

Import gains an optional targetContextId so a multi-user caller can restore a
bundle into a different workspace than the one it was exported from, instead
of only ever re-importing into its original context. Conflict detection
(SKIP_EXISTING/OVERWRITE) now runs against the target context: since
PropositionStore keys by id alone, an id already used by a *different*
context is a real storage-level collision and is rejected rather than
silently relocated or overwritten.

OVERWRITE was already non-atomic (findById + save, no store primitive ties
them together); there's no transactional overwrite primitive to add without
touching the PropositionStore SPI, which is out of scope here. Instead the
policy's contract is spelled out as best-effort explicitly, and ImportResult
now carries overwriteFailures so a caller can reconcile ids that failed
mid-overwrite instead of assuming the store ended up in either the old or
new state.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ics (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Per inspection findings, move context-scoped bundle assembly out of the
KnowledgeBundleExporter SPI (which is pure serialization) into a new
KnowledgeBundleAssembler collaborator class. Exporters stay focused on
format serialization; callers compose by assembling bundles, then exporting.

Fixes:
- Blocking (F1): Use findByContextId(ContextId) instead of the unoptimized
  findByContextIdValue fallback, ensuring backend-pushed filtering works
- High (F2): Remove repository-access methods from pure-serialization SPI
- Medium (F3): No more nullable-sentinel defaults; assembly is explicit

New KnowledgeBundleAssembler takes PropositionRepository as a constructor
parameter and provides:
- forContext(contextId): assemble single context bundle (uses optimized
  findByContextId for backend filtering)
- allContexts(): assemble one bundle per distinct context

Tests adapted to target the assembler. Added read-narrowness test verifying
forContext never triggers full-store scan fallback.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Add a durable store for metamodel versions and drift reports:
- MetamodelStore SPI in dice-metamodel (no graph deps) with methods to save/query
  versions and drift observations by schema name
- DriftReport data class captures observed undeclared types at a point in time
- DrivineMetamodelStore implementation in dice-storage using Neo4j with:
  - MetamodelVersion nodes MERGED on (schemaName, contentHash)
  - MetamodelDriftReport nodes MERGED on (schemaName, versionHash, capturedAt)
  - Type sets serialized as tab/newline-delimited strings for robust round-trip
  - Results ordered by timestamp DESC in Cypher (not in memory)
- RowMappers handle serialization/deserialization with fallback to EPOCH
- Tests cover round-trip, idempotency, ordering newest-first, schema isolation

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…oped reads (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…edAt on re-save

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…rt store (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ort consumer SPIs

Bundles can now optionally carry entity snapshots (data a consuming app owns for
the opaque resolvedId strings on mentions) and embedding vectors (base64 float32,
avoiding a re-embed at LLM cost on import). Both sections are additive and
default-empty: a bundle without them stays valid under formatVersion "2.0", and
dice keeps working standalone via null-object port defaults.

- EntitySnapshotPort / EmbeddingPort (dice-bundle): export + import fun interfaces,
  each with a NoOp default.
- KnowledgeBundle gains `entities` and `embeddings` (default empty) sections.
- KnowledgeBundleAssembler takes optional exporter collaborators and populates the
  sections when wired.
- JacksonKnowledgeBundleImporter takes optional importer collaborators (nullable,
  not defaulted) so a bundle carrying a section with no port wired is recorded as
  a note rather than silently dropped. Entities import before propositions;
  embeddings import after.
- DrivineEmbeddingAccess (dice-storage) implements EmbeddingPort against the
  Proposition node's own embedding property in Neo4j, reusing
  DrivinePropositionRepository's VECTOR_INDEX_LABEL/PROPERTY constants.
- dice-storage now depends on dice-bundle for the EmbeddingPort SPI.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…h dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ation

Wires drift detection end to end: DriftCheckRunner snapshots a live graph via
ObservedSchemaSource, diffs it against a DeclaredSchemaSource, always persists
a DriftReport (including zero-drift runs), and delegates quarantine to the
existing DriftQuarantinePolicy on a non-dry run. MetamodelAutoConfiguration
wires this behind embabel.dice.metamodel.enabled (default off, dryRun default
on), with DrivineObservedSchemaSource excluding dice's own bookkeeping labels
from the observed schema. No scheduler is included; consumers call run()
themselves.

Adds the missing dice-metamodel and embabel-agent-rag-core dependencies to
dice-storage-autoconfigure's pom -- the latter mirrors the same
provided/non-transitive dependency dice-metamodel already needed for
PropositionRepository's CoreSearchOperations supertype.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…toconfiguration (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…e-saves write fully

Drivine's GraphObjectManager caches a JSON snapshot of every view it saves or
loads, keyed by (view class, id), and diffs the next save against it to
decide which fields actually need writing ("optimized save"). delete() has
no way to evict that cache -- GraphObjectManager exposes no eviction API and
its SessionManager field is private -- so a proposition re-saved under an id
it had before being deleted got diffed against its own stale pre-delete
state: any field that happened to still match was treated as "unchanged" and
never written to what is actually a brand-new, empty node. Confirmed by
direct reproduction: delete(id) then saveIfAbsent(sameId, sameValues)
persisted a node with only `id` set.

This is exactly what bundle restore does (wipe a context, reimport a bundle
that preserves original proposition IDs), which is why BundleRoundTripIT
surfaced it.

Fix: doPersist now builds its merge statements against a throwaway,
always-empty SessionManager instead of GraphObjectManager's shared one, via
Drivine's own public GraphObjectMergeBuilder.forClass(...). An empty session
makes every id look "never seen before", which is exactly the full-write
path a proposition already gets on its first save -- this reuses Drivine's
real serialization and mention-cascade logic verbatim rather than
reimplementing it. Trade-off: every save now writes every field instead of
just the diff, an acceptable cost at this store's documented ~10-50K row
scale. setProvenance() still goes through the optimized path and carries the
same latent risk if it turns out to need the same treatment later.

Also fixes a real precision bug found while chasing this down:
JacksonKnowledgeBundleExporter serialized Instant as a numeric
epoch-seconds-plus-fraction, which Jackson's jsr310 module parses back
through a double -- losing nanosecond precision at real-world epoch
magnitudes (a roundtrip of ...123456789Z came back ...123456700Z). Disabling
WRITE_DATES_AS_TIMESTAMPS switches it to ISO-8601 strings, which parse back
exactly -- the same fix Drivine's own Neo4jObjectMapper already applies for
the identical reason.

Adds a focused regression test pinning delete-then-resave-same-id behavior
directly against the repository, independent of the bundle import path.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…bundle backup/restore

Relocated from dice-integration-tests, where the Drivine testcontainer
harness never registered the 'neo' database (attempt 1's blocker); reuses
dice-storage's own TestApplication, already proven by
DrivineEmbeddingAccessIntegrationTest and DrivineMetamodelStoreIntegrationTest.
Named *IntegrationTest, not *IT, to match this module's surefire naming
convention -- an *IT-suffixed file is silently skipped by a plain `mvn test`
here since there's no failsafe wiring.

Covers the full backup/restore roundtrip: scoped export excludes other
contexts, export/reimport fidelity (ids, text, contextId, confidence, decay,
status), byte-identical decay-anchor timestamps, embeddings restored via
DrivineEmbeddingAccess without re-embedding, mentions with resolvedIds,
ctx-B isolation, targetContextId re-scoping into a fresh context, and
delimiter-adversarial text/mention spans (pipes, tabs, newlines, quotes).

Two test-file bugs fixed along the way: the importer wasn't wired with an
embeddingImporter, so embeddings were silently never re-imported; and the
re-scoping test asserted an impossible scenario (the same proposition ids
live under both the original and the target context at once, which
PropositionStore's global id-uniqueness correctly refuses) -- fixed to
delete the source rows before re-scoping, matching a real migrate-into-a-new-
context restore.

All 9 tests pass against a live Neo4j testcontainer, now that the session-
snapshot bug (previous commit) no longer corrupts re-saved rows.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ve right after a delete

Inspection caught a real regression in the previous fix: routing EVERY save
through an always-empty Drivine SessionManager (saveFully) repaired the
delete-then-resave-same-id staleness bug, but broke DELETE_ORPHAN for every
other save. GraphViewMergeBuilder derives its relationship diff from the
session snapshot -- no snapshot means every current relationship reads as
"added", never "removed" -- so an ordinary re-save that shrinks a
proposition's mentions silently orphaned the stale Mention node instead of
deleting it. Reproduced live by the inspector; reproduced again here with a
permanent regression test.

Checked Drivine 0.0.58's actual sources first for a cleaner fix: no eviction
API exists at any level. GraphObjectManager.delete() never touches the
session; SessionManager is a private field with no getter; SessionManager
itself only offers a whole-session clear(), no per-id form. There is
nothing to call.

Fix: track ids this repository instance has delete()d in a JVM-local
ConcurrentHashMap-backed set (staleAfterDelete). doPersist consults it and
consumes it one-shot: only the very next save for a just-deleted id goes
through saveFully's always-full-write path; every other save goes straight
through graphObjectManager.save, which is session-aware and correctly
computes DELETE_ORPHAN's removed-relationship set. JVM-local is the correct
scope, not a shortcut: the staleness being tracked IS JVM-local in-memory
state, since Drivine's SessionManager lives inside this process's
GraphObjectManager bean.

Rewrote doPersist's and saveFully's KDoc to describe both paths truthfully
instead of the now-false "DELETE_ORPHAN is always authoritative" claim.

Adds `save with fewer mentions removes stale mention relationship`
(DrivinePropositionStoreIntegrationTest embabel#11): save with 2 mentions, re-save
the same id with 1 mention (no delete involved), assert exactly 1 mention
and no orphaned Mention node in the graph.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…tence fix (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Extend both design docs with component/sequence diagrams, a full format
spec (with a verified sample bundle) for knowledge-bundles.md, and the
new declared-vs-observed drift-check layer (ObservedSchemaSource,
DriftCheckRunner, DriftReport persistence, bookkeeping-label exclusion,
autoconfigure properties) for metamodel-governance.md. Fix dice-bundle/AGENTS.md
references to the old 1.0 format and pre-entities/embeddings ImportResult shape.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
knowledge-bundles.md referenced a "Full-proposition fidelity" heading dropped
during the rewrite; replace with inline rationale. Also fold in the
dice-metamodel/AGENTS.md extension covering the new declared-vs-observed
drift-check layer (ObservedSchema/Source, DeclaredSchema/Source, DriftReport,
MetamodelStore, DriftCheckRunner), consistent with metamodel-governance.md.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ec, consumer samples (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
Null context keeps whole-graph behavior; a non-null context confines the
check end to end — candidate propositions, schema observation, the persisted
drift report, and quarantine blast radius — to one context. Scoped observation
derives entity types from that context's own mention types; the scoped
relationship set is intentionally empty (no honest per-context attribution
yet). Adds the context-scoped section + updated diagrams to the design doc.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…GE key

The natural key (schemaName, versionHash, capturedAt) excluded contextId, so a scoped
report and a global/other-context report for the same schema+version landing on the same
capturedAt instant silently overwrote each other -- exactly the canary + multi-tenant-batch
path this feature exists for.

Add a contextKey property (the real contextId when scoped, a sentinel when global) to the
MERGE match pattern. contextKey has to be a property distinct from the nullable contextId
itself, since Cypher property-map equality on a literal null never matches -- merging
directly on a nullable contextId would make every global report take the CREATE branch and
duplicate on every retry instead of updating in place.

Adds an integration test asserting a null-context and a tenant-context report at the exact
same instant coexist as two nodes, and updates the test schema's uniqueness constraint and
the design doc's natural-key description to match.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…(dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
A semicolon inside a Note text is a mermaid statement separator and broke
the diagram render; use a dash instead.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ift API

DriftReport.contextId and MetamodelStore.driftReports(schemaName, contextId)
now carry ContextId instead of a raw String, closing the asymmetry m5 left
between DriftCheckRunner (already ContextId) and the persistence layer.
The Neo4j property is still a plain string (bound via ContextId.value on
write, wrapped back on read) and the contextKey MERGE-sentinel logic is
unchanged.

Also adds the idempotency test flagged Low in the m5 re-review: re-saving
an identical drift report (null and tenant-scoped variants) MERGEs in
place instead of duplicating.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…API (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…ip set via sourcePropositions join

DrivineObservedSchemaSource's context-scoped observation always reported an empty
relationship-type set, because there was no honest way to attribute a relationship
edge to a context -- dice didn't write anything a scoped query could join against.
That's no longer true: every edge carries a `sourcePropositions` property (the ids
of the propositions that produced it), so a relationship type belongs to a
context's scoped set if at least one edge of that type has a sourceProposition
whose owning Proposition has that contextId.

This deliberately does not collapse multi-context edges: an edge sourced from two
different contexts' propositions appears in both contexts' scoped sets, since an
undeclared relationship type present in a context's data is drift in that context
regardless of who else also produced it. No write-side changes -- this is a
read-only join added to the existing scoped observation query.

Adds real Neo4j testcontainer infrastructure to dice-storage-autoconfigure (it had
none) so the join, the no-collapse behavior, and delimiter/case-identity edge cases
can be proven against a live graph rather than asserted against a fake
PersistenceManager that only echoes back canned rows.

Claude-Session: https://claude.ai/code/session_01LmaqyBmfyCKu6P8icneoaT
Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
…rcePropositions join (dish dice-pr50-replan)

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
drivine4j 0.0.58 pins its testcontainer to neo4j:5.26.1-community, which has a
dynamic-relationship-type query-plan-cache bug (neo4j/neo4j#13597) that silently
drops a relationship type on the second write. Point the ITs at a self-managed
Neo4jContainer on 2026.05-community (where the bug is fixed) via test.neo4j.use-local
+ @DynamicPropertySource, so we control the version rather than inherit the pinned one.

Signed-off-by: James Dunnam <7660553+jimador@users.noreply.github.com>
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.

Snapshot and move what DICE knows Version the metamodel and catch drift before it corrupts the graph

1 participant