feat(metamodel): drift checking and quarantine - #86
Conversation
feeca32 to
044c4eb
Compare
044c4eb to
b3efb05
Compare
07a8277 to
cacc7f5
Compare
95a4d5e to
f799a8a
Compare
afc4c2e to
099e902
Compare
099e902 to
a8ce6bc
Compare
a8ce6bc to
ec7b957
Compare
ec7b957 to
9d001d5
Compare
1b9703f to
ee2df40
Compare
|
From CODEX:
This can miss propositions that should be quarantined. Example:
That proposition should be quarantined because it refers to the type that changed. But this policy only recognizes the type as:
It does not recognize Person as the same type. So when a proposition is stored under Person, this policy treats it as unrelated to Conclusion: |
|
From CODEX - inquiry on concurrency:
markSwept() is last-write-wins with no guard against an older sweep finishing after a newer one, so the baseline can move backward. Example:
After that, sweptVersion() returns A again even though B was already reconciled. Why that matters: markSwept() needs some way to reject stale completions, or the caller has to guarantee sweeps for one schema never overlap. |
846a15a to
d57052a
Compare
d57052a to
fbed94e
Compare
|
On Codex finding 1: The On finding 2 (concurrency): Correct: |
fbed94e to
1fe86db
Compare
1fe86db to
5df4d34
Compare
DriftReport with a bounded, explicitly scoped DriftReportStore contract (scope-before-limit pinned by test), DriftCheckRunner with dryRun-first semantics, and the quarantine layer: lossiness decided on property signatures (narrowing quarantines, widening never), already-quarantined classification independent of the current diff, non-destructive STALE writes. DefaultDriftCheckRunner stamps the declared version every run so report hashes always resolve, and takes the base PropositionStore port. dice-metamodel now depends on dice core. Refs #45; stacks on feat/metamodel-diff.
Comment and doc text only; no code change.
A paired rename quarantines nothing by itself: the diff folds the rename's own propagation, so the policy only ever sees deltas that survived, and a renamed property's delta is judged by the same narrowing rules as any signature change. Candidate matching reads the declaration's accumulated former names, and a removed type's former names from the older stamp, so data labeled under any former name is caught however far the rename and the loss are separated — while a former name redeclared as a live type is excluded, and a deliberately retired one surfaces as undeclared drift instead. Four type widenings stop reading as lossy: int to long, float to double, and their boxed pair, pinned against the dictionary's real rendered names.
The drift runner compared each declaration against the newest stored version, which is the version it had just written itself, so a lossy declared change was invisible to the very run that recorded it. MetamodelVersionStore gains a sweptVersion/markSwept pair. The runner reads the baseline before its history write and advances it last, only when a live unscoped sweep completed. Both methods default-forward to latestVersion/saveVersion, so existing stores compile unchanged and keep the old behaviour until they override; InMemoryMetamodelVersionStore tracks the pointer independently. Alongside that: - QuarantineDecision.Protected and QuarantineResult.protected report propositions a pin held back, which used to look like a plain skip. - PropositionStatusChanged is emitted per quarantined proposition, and only when the status actually moved. - The merged diff keeps MetamodelDiff's global ordering: removals lead as one sorted block whichever source produced them. - @jvmoverloads on the runner constructor so Java callers can omit the listener.
The runner now evaluates and reports only: run() stamps the declared version, writes the report, and touches no proposition and no swept baseline. Sweeping is a host call through PropositionStoreDriftSweep, whose candidate selection is bounded and ContextId-scoped, closing the findAll path that materialized every tenant. Quarantine records the prior status in metadata and release restores it while clearing the reason, so a quarantine is reversible. sweptVersion and markSwept move to SweptBaselineStore with no default bodies: saving a version never implies a completed sweep, and a store that cannot track the baseline lacks the surface to claim one. Reports carry declaredDiff, so a dry report shows the facts a deliberate sweep would act on. The quarantine policy matches declared types by own label, consistent with the differ.
Quarantine was a STALE plus a reason key, and the default decay policy revives any STALE proposition whose utility clears the recovery threshold with no look at the reason — so a quarantined proposition with healthy confidence came back on the next decay sweep and the two sweeps alternated. PropositionStatus.QUARANTINED ends that: reads that filter on ACTIVE exclude it structurally, the lifecycle policies leave it alone entirely, and release is an explicit transition that restores the recorded prior status, clears the reason, and announces the change. The quarantine machinery moves into dice beside StatusTransitionPolicy, where the conflict it resolves lives, and dice-metamodel returns to a leaf over DataDictionary with no dice dependency. Declared renames and aliases change how the diff and the drift check read old data; nothing rewrites a stored mention type.
The architecture doc inverted the module direction and still described quarantine as STALE plus a reason, the lifecycle doc had no QUARANTINED entry, and the QUARANTINE_REASON KDoc carried the old model. All three now state what ships: dice-metamodel is a leaf, QUARANTINED is its own status entered only by a deliberate sweep and left only by release, and DriftMode is OFF and OBSERVE. The quarantine and swept-baseline entries carry the EXPERIMENTAL marker and their opt-in triggers.
The lifecycle diagram showed one road in: ACTIVE to QUARANTINED. The policy quarantines from any non-quarantined status, STALE included, and release restores the recorded prior status. The diagram now draws both, and the trigger list carries the new transitions the way every other transition has one. The drift and sweep types carry ApiStatus.Experimental.
…ontract dice-metamodel declared org.jetbrains:annotations twice, the second time with a hard-coded version. The first declaration resolves from the root dependencyManagement and is the one that stays. markSwept is last-write-wins, so its KDoc now says that sweeps of one schema must not overlap and that the call site runs them one at a time.
Commit 959712c added a duplicate org.jetbrains:annotations block here as it did to dice-metamodel. The first declaration, with no version, is the correct one; the version comes from the root pom's dependencyManagement through jetbrains.annotations.version.
5df4d34 to
56d1bd6
Compare
PR 4 of the metamodel train, stacked on #85. Drift checking and quarantine.
DriftCheckRunneris a reporting-only contract:run()declares, stamps, observes, compares against the graph and against the swept baseline, and persists aDriftReport— its implementation holds no policy, no proposition store and no listener, so a check structurally cannot move a proposition. Acting on a check is a separate SPI,DriftSweepCapable: bounded,ContextId-scoped candidate paging,applyQuarantine,releaseFromQuarantine, and a defaultedsweepthat persists whatDriftQuarantinePolicyflags. Lossiness rules live inMentionTypeDriftQuarantinePolicy: type change, value/reference flip, or cardinality narrowing quarantines; widening never does; declared renames quarantine nothing by themselves, and former names are reachable however far a rename and a loss are separated.DriftReport/DriftReportStorecarry the persistence contract with three explicitly scoped, bounded reads.Changed in this review round:
PropositionStatus.QUARANTINEDreplaces the STALE-plus-metadata encoding, which the decay lifecycle used to undo:DecayStatusPolicyrevived a quarantined proposition on the next sweep and the drift sweep re-held it, alternating forever. NowDecayStatusPolicyrefusesQUARANTINEDoutright, reads filtering onACTIVEexclude it structurally,isAlreadyQuarantinedkeys on status alone, and release is the only way out — restoring the recorded prior status, clearing the reason, emittingPropositionStatusChanged. A regression test proves three decay-then-drift rounds leave the hold in place.dice(com.embabel.dice.spi, besideStatusTransitionPolicy), sodice-metamodeldrops itsdicedependency and is again a leaf over the agentDataDictionary.QuarantineDecision.Protectedand never flips them.Breaking changes: additive on the released surface, with three stated source-level exceptions —
QuarantineDecisionis sealed and gainsProtected;PropositionStatusgainsQUARANTINED, so an exhaustivewhenneeds a branch (one internal site changed; a host that matches on status exhaustively recompiles); the quarantine types moved package before ever shipping. Persistence round-trips by enum name, so no stored value changes meaning.Opt-in and status: EXPERIMENTAL. Nothing quarantines until a host calls
DriftSweepCapable.sweep()on a diff it decided to act on; there is no scheduler and no default that moves a proposition.flowchart TD A[DriftCheckRunner.run] --> B[diff declared vs previous swept baseline] A --> C[diff declared vs observed graph] B --> D[DriftReport, persisted] C --> D E[host calls DriftSweepCapable.sweep] --> F[DriftQuarantinePolicy] B --> F F --> G{pinned?} G -- yes --> H[Protected, reported and untouched] G -- no --> I{lossy change?} I -- yes --> J[QUARANTINED plus PropositionStatusChanged] I -- no --> K[untouched] J --> L[releaseFromQuarantine: prior status restored, reason cleared] M[decay sweep] -. cannot lift the hold .-> JNext in stack: #87 Drivine drift persistence and observation, #88 wiring.