333 lines
14 KiB
Markdown
333 lines
14 KiB
Markdown
|
|
# Conflict Model (how AtoCore handles contradictory facts)
|
||
|
|
|
||
|
|
## Why this document exists
|
||
|
|
|
||
|
|
Any system that accumulates facts from multiple sources — interactions,
|
||
|
|
ingested documents, repo history, PKM notes — will eventually see
|
||
|
|
contradictory facts about the same thing. AtoCore's operating model
|
||
|
|
already has the hard rule:
|
||
|
|
|
||
|
|
> **Bad memory is worse than no memory.**
|
||
|
|
|
||
|
|
The practical consequence of that rule is: AtoCore must never
|
||
|
|
silently merge contradictory facts, never silently pick a winner,
|
||
|
|
and never silently discard evidence. Every conflict must be
|
||
|
|
surfaced to a human reviewer with full audit context.
|
||
|
|
|
||
|
|
This document defines what "conflict" means in AtoCore, how
|
||
|
|
conflicts are detected, how they are represented, how they are
|
||
|
|
surfaced, and how they are resolved.
|
||
|
|
|
||
|
|
## What counts as a conflict
|
||
|
|
|
||
|
|
A conflict exists when two or more facts in the system claim
|
||
|
|
incompatible values for the same conceptual slot. More precisely:
|
||
|
|
|
||
|
|
A conflict is a set of two or more **active** rows (across memories,
|
||
|
|
entities, project_state) such that:
|
||
|
|
|
||
|
|
1. They share the same **target identity** — same entity type and
|
||
|
|
same semantic key
|
||
|
|
2. Their **claimed values** are incompatible
|
||
|
|
3. They are all in an **active** status (not superseded, not
|
||
|
|
invalid, not candidate)
|
||
|
|
|
||
|
|
Examples that are conflicts:
|
||
|
|
|
||
|
|
- Two active `Decision` entities affecting the same `Subsystem`
|
||
|
|
with contradictory values for the same decided field (e.g.
|
||
|
|
lateral support material = GF-PTFE vs lateral support material = PEEK)
|
||
|
|
- An active `preference` memory "prefers rebase workflow" and an
|
||
|
|
active `preference` memory "prefers merge-commit workflow"
|
||
|
|
- A `project_state` entry `p05 / decision / lateral_support_material = GF-PTFE`
|
||
|
|
and an active `Decision` entity also claiming the lateral support
|
||
|
|
material is PEEK (cross-layer conflict)
|
||
|
|
|
||
|
|
Examples that are NOT conflicts:
|
||
|
|
|
||
|
|
- Two active memories both saying "prefers small diffs" — same
|
||
|
|
meaning, not contradictory
|
||
|
|
- An active memory saying X and a candidate memory saying Y —
|
||
|
|
candidates are not active, so this is part of the review queue
|
||
|
|
flow, not the conflict flow
|
||
|
|
- A superseded `Decision` saying X and an active `Decision` saying Y
|
||
|
|
— supersession is a resolved history, not a conflict
|
||
|
|
- Two active `Requirement` entities each constraining the same
|
||
|
|
component in different but compatible ways (e.g. one caps mass,
|
||
|
|
one caps heat flux) — different fields, no contradiction
|
||
|
|
|
||
|
|
## Detection triggers
|
||
|
|
|
||
|
|
Conflict detection must fire at every write that could create a new
|
||
|
|
active fact. That means the following hook points:
|
||
|
|
|
||
|
|
1. **`POST /memory` creating an active memory** (legacy path)
|
||
|
|
2. **`POST /memory/{id}/promote`** (candidate → active)
|
||
|
|
3. **`POST /entities` creating an active entity** (future)
|
||
|
|
4. **`POST /entities/{id}/promote`** (candidate → active, future)
|
||
|
|
5. **`POST /project/state`** (curating trusted state directly)
|
||
|
|
6. **`POST /memory/{id}/graduate`** (memory → entity graduation,
|
||
|
|
future — the resulting entity could conflict with something)
|
||
|
|
|
||
|
|
Extraction passes do NOT trigger conflict detection at candidate
|
||
|
|
write time. Candidates are allowed to sit in the queue in an
|
||
|
|
apparently-conflicting state; the reviewer will see them during
|
||
|
|
promotion and decision-detection fires at that moment.
|
||
|
|
|
||
|
|
## Detection strategy per layer
|
||
|
|
|
||
|
|
### Memory layer
|
||
|
|
|
||
|
|
For identity / preference / episodic memories (the ones that stay
|
||
|
|
in the memory layer):
|
||
|
|
|
||
|
|
- Matching key: `(memory_type, project, normalized_content_family)`
|
||
|
|
- `normalized_content_family` is not a hash of the content — that
|
||
|
|
would require exact equality — but a slot identifier extracted
|
||
|
|
by a small per-type rule set:
|
||
|
|
- identity: slot is "role" / "background" / "credentials"
|
||
|
|
- preference: slot is the first content word after "prefers" / "uses" / "likes"
|
||
|
|
normalized to a lowercase noun stem, OR the rule id that extracted it
|
||
|
|
- episodic: no slot — episodic entries are intrinsically tied to
|
||
|
|
a moment in time and rarely conflict
|
||
|
|
|
||
|
|
A conflict is flagged when two active memories share a
|
||
|
|
`(memory_type, project, slot)` but have different content bodies.
|
||
|
|
|
||
|
|
### Entity layer (V1)
|
||
|
|
|
||
|
|
For each V1 entity type, the conflict key is a short tuple that
|
||
|
|
uniquely identifies the "slot" that entity is claiming:
|
||
|
|
|
||
|
|
| Entity type | Conflict slot |
|
||
|
|
|-------------------|-------------------------------------------------------|
|
||
|
|
| Project | `(project_id)` |
|
||
|
|
| Subsystem | `(project_id, subsystem_name)` |
|
||
|
|
| Component | `(project_id, subsystem_name, component_name)` |
|
||
|
|
| Requirement | `(project_id, requirement_key)` |
|
||
|
|
| Constraint | `(project_id, constraint_target, constraint_kind)` |
|
||
|
|
| Decision | `(project_id, decision_target, decision_field)` |
|
||
|
|
| Material | `(project_id, component_id)` |
|
||
|
|
| Parameter | `(project_id, parameter_scope, parameter_name)` |
|
||
|
|
| AnalysisModel | `(project_id, subsystem_id, model_name)` |
|
||
|
|
| Result | `(project_id, analysis_model_id, result_key)` |
|
||
|
|
| ValidationClaim | `(project_id, claim_key)` |
|
||
|
|
| Artifact | no conflict detection — artifacts are additive |
|
||
|
|
|
||
|
|
A conflict is two active entities with the same slot but
|
||
|
|
different structural values. The exact "which fields count as
|
||
|
|
structural" list is per-type and lives in the entity schema doc
|
||
|
|
(not yet written — tracked as future `engineering-ontology-v1.md`
|
||
|
|
updates).
|
||
|
|
|
||
|
|
### Cross-layer (memory vs entity vs trusted project state)
|
||
|
|
|
||
|
|
Trusted project state trumps active entities trumps active
|
||
|
|
memories. This is the trust hierarchy from the operating model.
|
||
|
|
|
||
|
|
Cross-layer conflict detection works by a nightly job that walks
|
||
|
|
the three layers and flags any slot that has entries in more than
|
||
|
|
one layer with incompatible values:
|
||
|
|
|
||
|
|
- If trusted project state and an entity disagree: the entity is
|
||
|
|
flagged; trusted state is assumed correct
|
||
|
|
- If an entity and a memory disagree: the memory is flagged; the
|
||
|
|
entity is assumed correct
|
||
|
|
- If trusted state and a memory disagree: the memory is flagged;
|
||
|
|
trusted state is assumed correct
|
||
|
|
|
||
|
|
In all three cases the lower-trust row gets a `conflicts_with`
|
||
|
|
reference pointing at the higher-trust row but does NOT auto-move
|
||
|
|
to superseded. The flag is an alert, not an action.
|
||
|
|
|
||
|
|
## Representation
|
||
|
|
|
||
|
|
Conflicts are represented as rows in a new `conflicts` table
|
||
|
|
(V1 schema, not yet shipped):
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE conflicts (
|
||
|
|
id TEXT PRIMARY KEY,
|
||
|
|
detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
slot_kind TEXT NOT NULL, -- "memory_slot" or "entity_slot" or "cross_layer"
|
||
|
|
slot_key TEXT NOT NULL, -- JSON-encoded tuple identifying the slot
|
||
|
|
project TEXT DEFAULT '',
|
||
|
|
status TEXT NOT NULL DEFAULT 'open', -- open | resolved | dismissed
|
||
|
|
resolved_at DATETIME,
|
||
|
|
resolution TEXT DEFAULT '', -- free text from the reviewer
|
||
|
|
-- links to conflicting rows live in conflict_members
|
||
|
|
UNIQUE(slot_kind, slot_key, status) -- ensures only one open conflict per slot
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE conflict_members (
|
||
|
|
conflict_id TEXT NOT NULL REFERENCES conflicts(id) ON DELETE CASCADE,
|
||
|
|
member_kind TEXT NOT NULL, -- "memory" | "entity" | "project_state"
|
||
|
|
member_id TEXT NOT NULL,
|
||
|
|
member_layer_trust INTEGER NOT NULL,-- 1=memory, 2=entity, 3=project_state
|
||
|
|
PRIMARY KEY (conflict_id, member_kind, member_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
Constraint rationale:
|
||
|
|
|
||
|
|
- `UNIQUE(slot_kind, slot_key, status)` where status='open' prevents
|
||
|
|
duplicate "conflict already open for this slot" rows. At most one
|
||
|
|
open conflict exists per slot at a time; new conflicting rows are
|
||
|
|
added as members to the existing conflict, not as a new conflict.
|
||
|
|
- `conflict_members.member_layer_trust` is denormalized so the
|
||
|
|
conflict resolution UI can sort conflicting rows by trust tier
|
||
|
|
without re-querying.
|
||
|
|
- `status='dismissed'` exists separately from `resolved` because
|
||
|
|
"the reviewer looked at this and declared it not a real conflict"
|
||
|
|
is a valid distinct outcome (the two rows really do describe
|
||
|
|
different things and the detector was overfitting).
|
||
|
|
|
||
|
|
## API shape
|
||
|
|
|
||
|
|
```
|
||
|
|
GET /conflicts list open conflicts
|
||
|
|
GET /conflicts?status=resolved list resolved conflicts
|
||
|
|
GET /conflicts?project=p05-interferometer scope by project
|
||
|
|
GET /conflicts/{id} full detail including all members
|
||
|
|
|
||
|
|
POST /conflicts/{id}/resolve mark resolved with notes
|
||
|
|
body: {
|
||
|
|
"resolution_notes": "...",
|
||
|
|
"winner_member_id": "...", # optional: if specified,
|
||
|
|
# other members are auto-superseded
|
||
|
|
"action": "supersede_others" # or "no_action" if reviewer
|
||
|
|
# wants to resolve without touching rows
|
||
|
|
}
|
||
|
|
|
||
|
|
POST /conflicts/{id}/dismiss mark dismissed ("not a real conflict")
|
||
|
|
body: {
|
||
|
|
"reason": "..."
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Conflict detection must also surface in existing endpoints:
|
||
|
|
|
||
|
|
- `GET /memory/{id}` — response includes a `conflicts` array if
|
||
|
|
the memory is a member of any open conflict
|
||
|
|
- `GET /entities/{type}/{id}` (future) — same
|
||
|
|
- `GET /health` — includes `open_conflicts_count` so the operator
|
||
|
|
sees at a glance that review is pending
|
||
|
|
|
||
|
|
## Supersession as a conflict resolution tool
|
||
|
|
|
||
|
|
When the reviewer resolves a conflict with `action: "supersede_others"`,
|
||
|
|
the winner stays active and every other member is flipped to
|
||
|
|
status="superseded" with a `superseded_by` pointer to the winner.
|
||
|
|
This is the normal path: "we used to think X, now we know Y, flag
|
||
|
|
X as superseded so the audit trail keeps X visible but X no longer
|
||
|
|
influences context".
|
||
|
|
|
||
|
|
The conflict resolution audit record links back to all superseded
|
||
|
|
members, so the conflict history itself is queryable:
|
||
|
|
|
||
|
|
- "Show me every conflict that touched Subsystem X"
|
||
|
|
- "Show me every Decision that superseded another Decision because
|
||
|
|
of a conflict"
|
||
|
|
|
||
|
|
These are entries in the V1 query catalog (see Q-014 decision history).
|
||
|
|
|
||
|
|
## Detection latency
|
||
|
|
|
||
|
|
Conflict detection runs at two latencies:
|
||
|
|
|
||
|
|
1. **Synchronous (at write time)** — every create/promote/update of
|
||
|
|
an active row in a conflict-enabled type runs a synchronous
|
||
|
|
same-layer detector. If a conflict is detected the write still
|
||
|
|
succeeds but a row is inserted into `conflicts` and the API
|
||
|
|
response includes a `conflict_id` field so the caller knows
|
||
|
|
immediately.
|
||
|
|
|
||
|
|
2. **Asynchronous (nightly sweep)** — a scheduled job walks all
|
||
|
|
three layers looking for cross-layer conflicts that slipped
|
||
|
|
past write-time detection (e.g. a memory that was already
|
||
|
|
active before an entity with the same slot was promoted). The
|
||
|
|
sweep also looks for slot overlaps that the synchronous
|
||
|
|
detector can't see because the slot key extraction rules have
|
||
|
|
improved since the row was written.
|
||
|
|
|
||
|
|
Both paths write to the same `conflicts` table and both are
|
||
|
|
surfaced in the same review queue.
|
||
|
|
|
||
|
|
## The "flag, never block" rule
|
||
|
|
|
||
|
|
Detection **never** blocks writes. The operating rule is:
|
||
|
|
|
||
|
|
- If the write is otherwise valid (schema, permissions, trust
|
||
|
|
hierarchy), accept it
|
||
|
|
- Log the conflict
|
||
|
|
- Surface it to the reviewer
|
||
|
|
- Let the system keep functioning with the conflict in place
|
||
|
|
|
||
|
|
The alternative — blocking writes on conflict — would mean that
|
||
|
|
one stale fact could prevent all future writes until manually
|
||
|
|
resolved, which in practice makes the system unusable for normal
|
||
|
|
work. The "flag, never block" rule keeps AtoCore responsive while
|
||
|
|
still making conflicts impossible to ignore (the `/health`
|
||
|
|
endpoint's `open_conflicts_count` makes them loud).
|
||
|
|
|
||
|
|
The one exception: writing to `project_state` (layer 3) when an
|
||
|
|
open conflict already exists on that slot will return a warning
|
||
|
|
in the response body. The write still happens, but the reviewer
|
||
|
|
is explicitly told "you just wrote to a slot that has an open
|
||
|
|
conflict". This is the highest-trust layer so we want extra
|
||
|
|
friction there without actually blocking.
|
||
|
|
|
||
|
|
## Showing conflicts in the Human Mirror
|
||
|
|
|
||
|
|
When the Human Mirror template renders a project overview, any
|
||
|
|
open conflict in that project shows as a **"⚠ disputed"** marker
|
||
|
|
next to the affected field, with a link to the conflict detail.
|
||
|
|
This makes conflicts visible to anyone reading the derived
|
||
|
|
human-facing pages, not just to reviewers who think to check the
|
||
|
|
`/conflicts` endpoint.
|
||
|
|
|
||
|
|
The Human Mirror render rules (not yet written — tracked as future
|
||
|
|
`human-mirror-rules.md`) will specify exactly where and how the
|
||
|
|
disputed marker appears.
|
||
|
|
|
||
|
|
## What this document does NOT solve
|
||
|
|
|
||
|
|
1. **Automatic conflict resolution.** No policy will ever
|
||
|
|
automatically promote one conflict member over another. The
|
||
|
|
trust hierarchy is an *alert ordering* for reviewers, not an
|
||
|
|
auto-resolve rule. The human signs off on every resolution.
|
||
|
|
|
||
|
|
2. **Cross-project conflicts.** If p04 and p06 both have
|
||
|
|
entities claiming conflicting things about a shared component,
|
||
|
|
that is currently out of scope because the V1 slot keys all
|
||
|
|
include `project_id`. Cross-project conflict detection is a
|
||
|
|
future concern that needs its own slot key strategy.
|
||
|
|
|
||
|
|
3. **Temporal conflicts with partial overlap.** If a fact was
|
||
|
|
true during a time window and another fact is true in a
|
||
|
|
different time window, that is not a conflict — it's history.
|
||
|
|
Representing time-bounded facts is deferred to a future
|
||
|
|
temporal-entities doc.
|
||
|
|
|
||
|
|
4. **Probabilistic "soft" conflicts.** If two entities claim the
|
||
|
|
same slot with slightly different values (e.g. "4.8 kg" vs
|
||
|
|
"4.82 kg"), is that a conflict? For V1, yes — the string
|
||
|
|
values are unequal so they're flagged. Tolerance-aware
|
||
|
|
numeric comparisons are a V2 concern.
|
||
|
|
|
||
|
|
## TL;DR
|
||
|
|
|
||
|
|
- Conflicts = two or more active rows claiming the same slot with
|
||
|
|
incompatible values
|
||
|
|
- Detection fires on every active write AND in a nightly sweep
|
||
|
|
- Conflicts are stored in a dedicated `conflicts` table with a
|
||
|
|
`conflict_members` join
|
||
|
|
- Resolution is always human (promote-winner / supersede-others
|
||
|
|
/ dismiss-as-not-a-conflict)
|
||
|
|
- "Flag, never block" — writes always succeed, conflicts are
|
||
|
|
surfaced via `/conflicts`, `/health`, per-entity responses, and
|
||
|
|
the Human Mirror
|
||
|
|
- Trusted project state is the top of the trust hierarchy and is
|
||
|
|
assumed correct in any cross-layer conflict until the reviewer
|
||
|
|
says otherwise
|