---
name: Analogia Protocol
slug: analogia-protocol
category: AI Engineering
description: Analogia Protocol validates structural mappings between abstract and concrete domains. It surfaces concrete instantiations when a mapping is uncertain and supports user grounding, repair, or withdrawal.
github: "https://github.com/jongwony/epistemic-protocols/tree/main/analogia/skills/ground"
language: JavaScript
stars: 161
forks: 15
install: "npx degit https://github.com/jongwony/epistemic-protocols/tree/main/analogia/skills/ground ~/.claude/skills/ground"
installs_to: ~/.claude/skills/ground
source_path: analogia/skills/ground/SKILL.md
collection_size: 25
category_size: 3670
collection_url: "https://dirskills.com/collections/jongwony/epistemic-protocols"
added: 2026-09-08T05:36:03.376Z
last_synced: 2026-09-08T05:36:03.376Z
canonical_url: "https://dirskills.com/skills/analogia-protocol"
---

# Analogia Protocol

Analogia Protocol validates structural mappings between abstract and concrete domains. It surfaces concrete instantiations when a mapping is uncertain and supports user grounding, repair, or withdrawal.

**Install:**

```bash
npx degit https://github.com/jongwony/epistemic-protocols/tree/main/analogia/skills/ground ~/.claude/skills/ground
```

## README

# Analogia Protocol

Validate structural mapping between abstract and concrete domains through AI-guided detection, whole-draft grounding, and user-grounded standing or repair. Type: `(MappingUncertain, AI, GROUND, R) → ValidatedMapping`.

## Definition

**Analogia** (ἀναλογία): A dialogical act of validating structural correspondences between domains, where AI detects mapping uncertainty between abstract frameworks and concrete application contexts, constructs an explicit whole mapping with concrete instantiations, and lets the user's natural next move establish standing, repair named exceptions, withdraw named parts, or explore without disposition.

```
── FLOW ──
Analogia(R) → attempts := 0 ; superseded_by := None ; refuted_focuses := ∅ ; zero_gap_confirmed := false ; fit_map := None ; draft := None ; continuation := None ; focus := None ; mappings := ∅ ; confirmed := ∅ ; dismissed := ∅ ; remaining := ∅ ; validations := [] ; partition_reading := None ; reopen_seed := None → Detect(R) → [¬uncertain: Qs(zero_gap_finding) → Stop → (Uptake(k): zero_gap_confirmed := true ; continuation := k ; deactivate ; [k = Some(_): handoff(k)] | Reopen(q): reopen_seed := q, proceed | Explore(u): answer(u) from the scan's basis ; identity over all state → re-present zero_gap_finding)] → (Sₐ, Sₜ) → derive_focus_candidates(Sₐ, Sₜ) → candidates → settle_focus(candidates, R, context) → φₚ → [focus_settled(φₚ) ∧ φₚ ∉ Λ.refuted_focuses: FocusReadback(φₚ) → φ' := φₚ | ¬focus_settled(φₚ) ∨ φₚ ∈ Λ.refuted_focuses: Qc(candidates) → Stop → FocusAnswer → (Select(φₛ): φ' := φₛ | Reframe(d): [¬pair_committed(Λ): (Sₐ, Sₜ) := decompose(R, d, context) ; self_grounding := self_grounding(Sₐ, Sₜ) → re-enter derive_focus_candidates(Sₐ, Sₜ, d) | pair_committed(Λ) ∧ d replaces Sₐ or Sₜ: Λ.superseded_by := Some(d) ; R' := annotate(R, superseded_mapping_status, superseded_residual(Λ)) → DomainSuperseded | pair_committed(Λ) ∧ ¬replaces: re-enter derive_focus_candidates(Sₐ, Sₜ, d)])] → [Λ.focus = Some(φ) ∧ φ' ≠ φ: invalidate_derived(Λ)] → [focus_change_requires_disposition(Λ, φ'): Qc(focus_change) → Stop → FocusChangeAnswer → (ParkPriorJudgments: retain(affected(Λ)) | RevalidatePriorJudgments: return_pending(affected(Λ)))] → Λ.focus := φ' → [attempts_exhausted(Λ): D_f := declare_exhaustion_disposition(Λ) ; R' := annotate(R, unresolved_mapping_status, exhaustion_residual(Λ)) → AttemptExhausted] → attempts := attempts + 1 → Map(Sₐ, Sₜ, φ') → M → [M ≠ ∅: carry_over(M) | M = ∅: Λ.mappings := ∅] → AssessFit(M, Sₐ, Sₜ) → F → Λ.fit_map := Some(F) → [self_grounding(Sₐ, Sₜ): PartitionRead(F, Sₜ) → Λ.partition_reading := Some(partition_reading(F, Sₜ))] → [M = ∅: surface(no correspondence constructed along φ', F.missing) → Λ.refuted_focuses := Λ.refuted_focuses ∪ {φ'} ; Λ.partition_reading := None → [attempts_exhausted(Λ): D_f := declare_exhaustion_disposition(Λ) ; R' := annotate(R, unresolved_mapping_status, exhaustion_residual(Λ)) → AttemptExhausted | ¬attempts_exhausted(Λ): derive_focus_candidates(Sₐ, Sₜ, F.missing) → re-enter focus settlement] | M ≠ ∅ ∧ Λ.remaining = ∅: declare_fit_disposition(F) → D_f ; R' := annotate(R, mapping_status, D_f) ; [self_grounding: R' carries PartitionReading + routing] → terminalize | M ≠ ∅ ∧ Λ.remaining ≠ ∅: Draft(M, F, Sₜ, φ', standing(Λ)) → D → Qs(D, standing_affordances) → Stop → UserUtterance → interpret_turn(user_utterance, D, context) → T → integrate_turn(T, D, R, F) → (D_f, R')] → (loop until terminalized ∨ attempts_exhausted(Λ) ∨ domain_superseded(Λ))

── MORPHISM ──
R
  → detect(R, context)                 -- infer mapping uncertainty
  → decompose(abstract, concrete)      -- identify source and target domains
  → derive_focus_candidates(Sₐ, Sₜ, R, context) -- surface plausible MappingFocus candidates (source_scope, target_scope, relation, purpose), before any correspondence is constructed
  → settle_focus(candidates, R, context) → φₚ -- bind the provisional comparison focus the settlement guard then reads; focus_settled(φₚ) is checked PER FIELD, not object-wide (see focus_settled), and decides relay (FocusReadback) vs gate (FocusSelector) — this step yields the value, it does not itself make that choice; once per activation for the initial selection, before construction, plus once more per empty-construction retry
  → constitute_focus_change_disposition(Λ, φ') -- conditional: when terminal judgments stand under an outgoing focus, the user constitutes their fate (park or revalidate) before construction runs on the successor focus
  → construct(mapping, Sₐ→Sₜ, φ)        -- build structural correspondences along the settled focus φ
  → assess_fit(mapping, Sₐ, Sₜ, context) -- sort correspondence adequacy before the whole draft acquires user-grounded standing
  → read_partition(fit_map, Sₜ) -- DERIVED split-vs-trim reading over the misfit MEMBERS (self-grounding case only — guarded; relay, no gate): misfits clustering into a coherent rival essence → Split → decompose recovery (route to the /conduct recipe) vs scattered misfits → Trim → narrow in place (/induce Narrow) vs no misfit → Hold
  → draft(mapping, fit_map, target, focus, standing) -- assemble the whole repairable mapping with one concrete example per correspondence and each one's current standing
  → surface(draft, repair_affordance) -- present the whole mapping once; turn-reading constructors stay internal rather than becoming a response menu
  → interpret_turn(user_utterance, draft, context) → T -- read positive uptake, direct repair, withdrawal, or exploration from the user's natural next move
  → integrate_turn(T, draft, R, fit_map) -- establish standing by uptake or named exception, reconstruct a repair, or preserve the pending draft during exploration
  → terminalize(mapping, user, fit_disposition) -- make mapping status and the utterance that established it explicit in output
  → ValidatedMapping
requires: uncertain(mapping(Sₐ, Sₜ))    -- runtime checkpoint (Phase 0)
deficit:  MappingUncertain               -- activation precondition (Layer 1/2)
preserves: content_identity(R)           -- output content invariant; mapping status recorded in R'
invariant: Structural Correspondence over Abstract Assertion

── TYPES ──
R        = Text containing abstract structures (source-agnostic: AI output, user analysis, or external reference)
             -- Input type: morphism processes R uniformly; enumeration scopes the definition, not behavioral dispatch
Detect   = Mapping uncertainty detection: R → Bool
Sₐ       = Source domain (abstract structure in R)
Sₜ       = Target domain (user's concrete application context)
self_grounding(Sₐ, Sₜ) ≡ instances(Sₐ) = Sₜ   -- self-grounding case: Sₜ is the source abstraction's OWN member instances (Sₐ a LOCATED candidate fused abstraction validated against the instances it claims to subsume), not a separate application domain. located(Sₐ) holds — this is what distinguishes self-grounding from the colimit route-away case (locator absent → /induce); here the abstraction already has a name and is checked for wrong-fusion against its own members
MappingFocus = { source_scope: Set(Component), target_scope: Set(Component), relation: String, purpose: String }  -- protocol-local: the comparison focus that conditions Map before any correspondence is constructed; not a Correspondence itself — MappingFocus scopes and orients WHICH comparison Map will build, a Correspondence is one constructed pairing. once pair_committed(Λ), source_scope and target_scope range over components WITHIN (Sₐ, Sₜ); they select what to compare inside the bound domains and never replace a domain. Before commitment the pair itself is still open, so a Reframe there re-decomposes instead
φ        = MappingFocus  -- the settled comparison focus Map consumes; bound to Λ.focus once determined (φ = Λ.focus when Some), the same symbol threaded through FLOW/MORPHISM/PHASE TRANSITIONS below
pair_committed(Λ) ≡ Λ.attempts > 0  -- the pair is committed once a Map has run against it, and attempts increments immediately before each Map, so this is the one reading of "has construction begun". Before it the decomposition is still provisional and a Reframe may replace (Sₐ, Sₜ) outright: nothing is yet standing on the pair — no fit evidence, no draft, no judgment, no spent attempt — so replacing it strands nothing and needs no disposition. After it, the pair is what the activation's question IS, and replacing it is DomainSuperseded rather than a revision
forced(f) ≡ the decomposition admits exactly one value for f, with that uniqueness citable  -- an axis with a single possible value is a readback, not a selection
determined(f) ≡ f is fixed by explicit user language or a citable standing rule
focus_settled(φ) ≡ ∀ f ∈ {source_scope, target_scope, relation, purpose} : determined(f) ∨ forced(f)  -- PER-FIELD settlement predicate, never a whole-object test: EVERY axis must be independently determined or forced. An axis where the protocol would pick among viable alternatives is a selection, not a readback, and one such axis is enough to fire the gate.
FocusAnswer = Select(MappingFocus) ∪ Reframe(description)  -- user's answer to the mapping-focus checkpoint; Reframe's reach turns on pair_committed(Λ), and on nothing else. BEFORE commitment it may re-decompose — the pair is provisional, the description feeds decompose(R, d, context) afresh, and the replacement strands nothing because no Map has run. AFTER commitment it revises the four axes WITHIN the fixed pair — scopes included, since they range inside it — and re-enters candidate derivation over that same pair; there, replacing Sₐ or Sₜ asks a different mapping question and crosses the activation seam (DomainSuperseded) rather than re-entering this settlement
FocusChangeAnswer = ParkPriorJudgments ∪ RevalidatePriorJudgments  -- user's answer to the focus-change checkpoint; occasion-local, neither constructor establishes a standing policy. ParkPriorJudgments carries the affected judgments' standing forward, so the correspondences they addressed stay addressed under the successor focus; RevalidatePriorJudgments withdraws that standing, returning those correspondences to remaining
affected(Λ) = { the LAST r ∈ Λ.validations per correspondence | r.correspondence ∈ (Λ.confirmed ∪ Λ.dismissed) }  -- the terminal judgments STANDING at the checkpoint. Records are append-only, so a correspondence judged more than once holds several and the last is the one standing; the earlier ones are its history and are not up for revalidation, whichever focus each was originally made under: confirmed/dismissed carry current standing, so a judgment an earlier Park carried forward is included and stays revalidatable
focus_change_requires_disposition(Λ, φ') ≡ ∃ φ : Λ.focus = Some(φ) ∧ φ' ≠ φ ∧ affected(Λ) ≠ ∅  -- the checkpoint's guard: a stored focus exists, the comparison focus is about to change, and terminal judgments stand under the outgoing one. Comparing focus values is the WHOLE test, not a proxy for a wider one: the domain pair is committed at the first Map and cannot move inside an activation, so the focus is the only component of the basis that can change and an equal focus means an unchanged basis. False on the initial settle path (Λ.focus = None), so the checkpoint does not fire there
return_pending(A) = reopen_targets({ r.correspondence | r ∈ A })  -- RevalidatePriorJudgments state update; records stay, the trace shows the return
retain(A) = identity over confirmed, dismissed, remaining  -- ParkPriorJudgments state update: the affected judgments keep standing, so a correspondence they addressed stays addressed under the successor focus and is not re-gated
invalidate_presentation(Λ) = draft := None ; partition_reading := None  -- everything derived from the CURRENT fit map's presentation: the whole-draft surface and the partition reading taken over it. EVERY route that leaves the basis runs this, and it exists so those routes cannot list the values one by one and drift apart — a route that cleared the draft and kept the reading would let an exit render a partition of the mapping it is leaving
invalidate_derived(Λ) = mappings := ∅ ; fit_map := None ; invalidate_presentation(Λ)  -- the ASSESSED evidence Map/AssessFit and repairable draft derived from (Sₐ, Sₜ, φ), discarded whenever the focus changes, so no exit can present a departed focus's fit reading as the incoming one's. Λ.remaining is deliberately NOT cleared — a cap that fires before Map is required to declare the unresolved correspondences, and a RevalidatePriorJudgments disposition writes into exactly this set. Map consumes the domain pair and the focus, and the pair is fixed for the activation, so the focus commit is the one transition that invalidates this evidence. The standing judgments are deliberately absent here — confirmed and dismissed survive a focus change, because their fate is the focus-change checkpoint's to decide, and validations is append-only so no transition removes from it at all. attempts is absent too: the cap is per activation, so a focus change must not refund it
carry_over(M') = mappings := M' ; confirmed := confirmed ∩ M' ; dismissed := dismissed ∩ M' ; remaining := M' \ (confirmed ∪ dismissed)  -- reconstruction carry-over, applied when Map rebuilds the correspondence set into a NON-EMPTY M'. Records are not filtered to M': a correspondence this reconstruction dropped WAS judged, and the trace reports its record as superseded rather than losing it
Map      = Structure-preserving mapping construction: (Sₐ, Sₜ, φ) → Set(Correspondence)   -- consumes the settled MappingFocus φ; construction is scoped and oriented by φ, not by (Sₐ, Sₜ) alone
M        = Set(Correspondence)                                   -- mapping result
Correspondence = { abstract: Component, concrete: Component, relation: String }
Component = { name: String, structure: String }
Context  = Observable mapping context from R, session context, and cited domain evidence
AssessFit = Correspondence adequacy assessment: M × Sₐ × Sₜ × Context → F
F        = CorrespondenceFitMap { preserved, partial, missing, overextended, open }
preserved = Set(Correspondence) where target structure preserves source relation
partial  = Set(Correspondence) where correspondence exists but some structural dimensions lack evidence
missing  = Set(Component) from Sₐ with no evidenced Sₜ correspondent
overextended = Set(Correspondence) where source relation adds unsupported target constraints
open     = Set(StructuralQuestion) where answer could change validation of M
StructuralQuestion = { structure: Component, reason: String, evidence_needed: String }
FitLabel ∈ {Preserved, Partial, Overextended}
fit_classification(F, c) =
  Preserved if c ∈ F.preserved
  Partial if c ∈ F.partial
  Overextended if c ∈ F.overextended
fit_partition(F, M) = F.preserved ∪ F.partial ∪ F.overextended = M (pairwise disjoint)
ResidualFitIssue ∈ Missing(Component) ∪ OpenQuestion(StructuralQuestion)
residual_issues(F) = { Missing(x) | x ∈ F.missing } ∪ { OpenQuestion(q) | q ∈ F.open }
FitDisposition = { issues: Set(ResidualFitIssue), status: None ∪ Bounded, declaration: String }
D_f      = FitDisposition
fit_disposition_declared(F, D_f) =
  (residual_issues(F) = ∅ ∧ D_f.status = None)
  ∨ (D_f.status = Bounded ∧ D_f.issues = residual_issues(F))
MemberInstance = a member of Sₜ in the self-grounding case (an instance the abstraction Sₐ claims to subsume)  -- Sₜ's elements; self-grounding-only. Distinct from Component (a structural element of a domain): the partition reading ranges over members, not over correspondences or facets
PartitionReading = { verdict ∈ {Split, Trim, Hold}, rival_essences: Set(InstanceCluster), trim_outliers: Set(MemberInstance), core_remainder: Set(MemberInstance), basis: F }  -- DERIVED split-vs-trim reading; verdict = Hold means no significant misfit (the fusion holds, no partition action). Computed ONLY in the self-grounding case; when ¬self_grounding the protocol holds Λ.partition_reading = None (Option — not computed at all), DISTINCT from verdict = Hold. NOT a sixth fit cell — it is SECOND-ORDER over the misfit MEMBER set (via the misfit_instances projection), not a partition of correspondences M, so it leaves fit_partition(F, M) intact. Relay (derived from F), never a gate.
   -- THREE pairwise-disjoint member groups that PARTITION Sₜ exhaustively: core_remainder = members that genuinely FIT the original abstraction (= Sₜ minus ALL misfits, so it never contains a misfit); rival_essences = the disjoint coherent rival cells within the misfits (each a candidate new abstraction → decompose); trim_outliers = the remaining scattered/ambiguous misfits in no rival cell (narrow-out candidates). Sₜ = core_remainder ⊎ (⋃ rival_essences.members) ⊎ trim_outliers. The cell-candidate partition /conduct constitutes is { core_remainder } ∪ { rival cells }; trim_outliers is surfaced for the checkpoint to narrow-out or place (never silently folded into the core)
InstanceCluster = { members: Set(MemberInstance), candidate_essence: String }  -- a coherent sub-group of Sₜ members + the rival essence they support; these are the rival cell candidates the /conduct decompose-recovery boundary-checkpoint consumes (cell assignment is the user's constitutive judgment there, not here)
member_facet(F) = { facet ∈ F.missing | asserted_of_all(facet) }  -- the subset of F.missing the abstraction asserts of ALL members (universally-quantified facets), so each is per-member testable via exhibits(·); other missing components are not member-testable and stay in basis, never projecting onto members
misfit_instances : F × Sₜ → Set(MemberInstance)  -- DERIVED projection from the fit map onto the member set: m ∈ Sₜ is misfit iff (∃ c ∈ F.overextended : m violates c's added target constraint) ∨ (∃ facet ∈ member_facet(F) : ¬exhibits(m, facet)). Only member_facet(F) ⊆ F.missing — the universally-asserted, per-member-testable facets — projects onto members; the rest of F.missing and all source-side cells of F.overextended stay in basis, never in the member set — so the reading is genuinely over instances and leaves fit_partition(F, M) intact
rival_clusters(mis) : Set(MemberInstance) → Set(InstanceCluster)  -- the PAIRWISE-DISJOINT coherent rival-essence sub-groups within the misfit member set mis (each clustered misfit assigned to exactly one rival; a maximal InstanceCluster supporting one rival essence); ∅ when mis's members are all scattered/ambiguous with no coherent rival. Misfits in no cluster fall to trim_outliers, never to core_remainder
partition_reading(F, Sₜ) =   -- invoked ONLY under self_grounding (guarded in FLOW / PHASE TRANSITIONS); returns a full PartitionReading RECORD, never a bare verdict token and never the Option None
  let mis = misfit_instances(F, Sₜ), clusters = rival_clusters(mis),
      core = Sₜ \ mis, outliers = mis \ ⋃ { c.members | c ∈ clusters },
      cells = (if core ≠ ∅ then {core} else ∅) ∪ clusters       -- the NON-EMPTY recovery cells: the core cell (when non-empty) + the rival cells
  in { verdict        = Hold if mis = ∅ ; Split if |cells| ≥ 2 ; Trim otherwise,
       rival_essences = clusters,                                 -- pairwise disjoint; may be a single cluster even when verdict = Trim (one rival, empty core)
       trim_outliers  = outliers,                                 -- misfits in no rival cell: = ∅ when Hold, = mis when there is no cluster, the scattered remainder when split
       core_remainder = core,                                     -- the members that genuinely FIT (= Sₜ when Hold); NEVER contains a misfit
       basis          = F }
  -- verdict = Split (|cells| ≥ 2) → wrong fusion: route to /conduct decompose (n ≥ 2 cells satisfies the conduct warrant; trim_outliers narrowed-out or placed at the checkpoint); Trim (|cells| ≤ 1: scattered misfits around a core, OR a single coherent cell with empty core) → SINGLE-MOVE /induce recovery (Narrow or Reorient), NOT a decompose; Hold → fusion holds, no partition acti
