All files / web/src/components/toys/euclid types.ts

100% Statements 573/573
100% Branches 4/4
100% Functions 1/1
100% Lines 573/573

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 5741x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 12x 11x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import type { FactStore } from './engine/factStore'
import type { ProofFact } from './engine/facts'
import type { KidLanguageStyle } from '@/db/schema/player-session-preferences'
 
// ── Byrne-inspired palette ─────────────────────────────────────────
export const BYRNE = {
  given: '#1A1A2E',
  red: '#E15759',
  blue: '#4E79A7',
  yellow: '#F0C75E',
} as const
 
/** Cycle through Byrne accent colors for new constructions */
export const BYRNE_CYCLE = [BYRNE.red, BYRNE.blue, BYRNE.yellow] as const
 
// ── Vec2 (shared 2D vector) ──────────────────────────────────────
 
export interface Vec2 {
  x: number
  y: number
}
 
// ── Superposition interaction ────────────────────────────────────
 
export type SuperpositionPhase =
  | { tag: 'idle' }
  | {
      tag: 'lifting'
      startTime: number
      srcTriIds: [string, string, string]
      tgtTriIds: [string, string, string]
      mapping: [string, string][]
    }
  | {
      tag: 'dragging'
      srcTriIds: [string, string, string]
      tgtTriIds: [string, string, string]
      mapping: [string, string][]
      cutoutVertices: [Vec2, Vec2, Vec2]
      dragAnchor: Vec2
      initialCentroid: Vec2
    }
  | {
      tag: 'mismatched'
      cutoutVertices: [Vec2, Vec2, Vec2]
      srcTriIds: [string, string, string]
      tgtTriIds: [string, string, string]
      mapping: [string, string][]
      settleTime: number
    }
  | {
      tag: 'flipping'
      startTime: number
      axisPoint: Vec2
      axisDir: Vec2
      preFlipVertices: [Vec2, Vec2, Vec2]
      postFlipVertices: [Vec2, Vec2, Vec2]
      srcTriIds: [string, string, string]
      tgtTriIds: [string, string, string]
      mapping: [string, string][]
    }
  | {
      tag: 'snapping'
      startTime: number
      fromVertices: [Vec2, Vec2, Vec2]
      toVertices: [Vec2, Vec2, Vec2]
      srcTriIds: [string, string, string]
      tgtTriIds: [string, string, string]
      mapping: [string, string][]
    }
  | { tag: 'settled' }
 
export interface SuperpositionEstablishes {
  congruence: { statement: string }
  cascade: Array<{
    kind: 'segment-equality' | 'angle-equality'
    params: Record<string, string>
    statement: string
    justification: string
  }>
}
 
// ── Viewport ───────────────────────────────────────────────────────
 
export interface EuclidViewportState {
  center: { x: number; y: number }
  /** Uniform zoom — same scale on both axes */
  pixelsPerUnit: number
}
 
// ── Geometric elements ─────────────────────────────────────────────
 
export type ElementOrigin =
  | 'given'
  | 'compass'
  | 'straightedge'
  | 'intersection'
  | 'free'
  | 'extend'
 
export interface ConstructionPoint {
  kind: 'point'
  id: string
  x: number
  y: number
  label: string
  color: string
  origin: ElementOrigin
}
 
export interface ConstructionCircle {
  kind: 'circle'
  id: string
  centerId: string
  radiusPointId: string
  color: string
  origin: 'compass'
}
 
export interface ConstructionSegment {
  kind: 'segment'
  id: string
  fromId: string
  toId: string
  color: string
  origin: 'straightedge' | 'given'
}
 
export type ConstructionElement = ConstructionPoint | ConstructionCircle | ConstructionSegment
 
// ── Construction state ─────────────────────────────────────────────
 
export interface ConstructionState {
  elements: ConstructionElement[]
  nextLabelIndex: number
  nextColorIndex: number
}
 
// ── Intersection candidates ────────────────────────────────────────
 
export interface IntersectionCandidate {
  x: number
  y: number
  /** First element involved */
  ofA: string
  /** Second element involved */
  ofB: string
  /** Distinguisher when two elements have multiple intersections */
  which: number
}
 
// ── Tool state machines ────────────────────────────────────────────
 
export type CompassPhase =
  | { tag: 'idle' }
  | { tag: 'center-set'; centerId: string }
  | {
      tag: 'radius-set'
      centerId: string
      radiusPointId: string
      radius: number
      enterTime: number
    }
  | {
      tag: 'sweeping'
      centerId: string
      radiusPointId: string
      radius: number
      startAngle: number
      prevAngle: number
      cumulativeSweep: number
    }
 
export type StraightedgePhase = { tag: 'idle' } | { tag: 'from-set'; fromId: string }
 
export type ExtendPhase =
  | { tag: 'idle' }
  | { tag: 'base-set'; baseId: string }
  | { tag: 'extending'; baseId: string; throughId: string }
 
export type ActiveTool = 'compass' | 'straightedge' | 'macro' | 'move' | 'point' | 'extend'
 
export type MacroPhase =
  | { tag: 'idle' }
  /** Macro tool active, proposition not yet chosen — picker is open */
  | { tag: 'choosing' }
  | {
      tag: 'selecting'
      propId: number
      inputs: import('./engine/macros').MacroInput[]
      selectedPointIds: string[]
    }
 
// ── Element selectors ─────────────────────────────────────────────
// Reference circles/segments by their defining points, not creation-order IDs.
// Point IDs (e.g. "pt-A") pass through as plain strings.
 
export type ElementSelector =
  | string
  | { kind: 'circle'; centerId: string; radiusPointId: string }
  | { kind: 'segment'; fromId: string; toId: string }
 
// ── Tutorial hints ─────────────────────────────────────────────────
 
export type TutorialHint =
  | { type: 'point'; pointId: string }
  | { type: 'arrow'; fromId: string; toId: string }
  | { type: 'sweep'; centerId: string; radiusPointId: string }
  | { type: 'candidates'; ofA?: ElementSelector; ofB?: ElementSelector; beyondId?: string }
  | { type: 'triangle-highlight'; ids: [string, string, string] }
  | { type: 'none' }
 
/** Trigger that advances the tutorial to the next sub-step. */
export type AdvanceOn =
  | { kind: 'compass-phase'; phase: 'center-set' | 'radius-set' }
  | { kind: 'macro-select'; index: number }
  | { kind: 'extend-phase'; phase: 'base-set' | 'extending' }
  | {
      kind: 'superposition-phase'
      phase: 'dragging' | 'mismatched' | 'flipping' | 'snapping' | 'settled'
    }
 
export interface TutorialSubStep {
  /** Short display text */
  instruction: string
  /** Longer conversational text for TTS */
  speech: string
  /** Visual hint rendered on canvas */
  hint: TutorialHint
  /**
   * Tool phase event that triggers advancement to next sub-step.
   * null = terminal sub-step (advanced when the proposition step completes).
   */
  advanceOn: AdvanceOn | null
}
 
// ── Angle specification ──────────────────────────────────────────
 
/** An angle defined by a vertex and two ray endpoints */
export interface AngleSpec {
  vertex: string
  ray1End: string
  ray2End: string
}
 
// ── Proposition stepper ────────────────────────────────────────────
 
export type ExpectedAction =
  | { type: 'compass'; centerId: string; radiusPointId: string }
  | {
      type: 'intersection'
      ofA?: ElementSelector
      ofB?: ElementSelector
      beyondId?: string
      label?: string
    }
  | { type: 'straightedge'; fromId: string; toId: string }
  | { type: 'extend'; baseId: string; throughId: string; distance?: number; label: string }
  | {
      type: 'macro'
      propId: number
      inputPointIds: string[]
      outputLabels?: Record<string, string>
    }
  | { type: 'observation'; id: string }
  | {
      type: 'superposition'
      src: [string, string, string]
      tgt: [string, string, string]
      mapping: [string, string][]
      establishes: SuperpositionEstablishes
    }
 
export interface PropositionStep {
  instruction: string
  expected: ExpectedAction
  /** Element IDs to highlight as hints */
  highlightIds: string[]
  /** Tool to auto-select for this step (null = no tool needed, e.g. tap intersection) */
  tool: ActiveTool | null
  /** Citation reference for this step, e.g. "Post.1", "I.1", "Def.15" */
  citation?: string
  /** For observation steps: speech narration text */
  observationSpeech?: string
}
 
export interface PropositionDef {
  id: number
  title: string
  /** Character ID for the teacher who narrates this proof. Absent = default (euclid). */
  characterId?: string
  /** Unique proof variant ID. Absent = canonical/default proof. */
  proofVariant?: string
  /** Human-readable label for proof selector (e.g., "Pappus's proof") */
  proofLabel?: string
  /** Short description shown below proof selector */
  proofDescription?: string
  givenElements: ConstructionElement[]
  steps: PropositionStep[]
  completionMessage?: string
  /** Segments to highlight as the construction result on completion */
  resultSegments?: Array<{ fromId: string; toId: string }>
  /** 'construction' (default) shows Q.E.F.; 'theorem' shows Q.E.D. */
  kind?: 'construction' | 'theorem'
  /** Equality facts pre-loaded into the fact store before any construction */
  givenFacts?: Array<{
    left: { a: string; b: string }
    right: { a: string; b: string }
    statement: string
  }>
  /** Angle arcs to render (visual indicators at vertices) */
  givenAngles?: Array<{
    spec: AngleSpec
    color: string
    /** Per-angle arc radius in pixels (defaults to 18) */
    radiusPx?: number
  }>
  /** Pairs of equal angles — matching tick marks on arcs (visual only) */
  equalAngles?: Array<[AngleSpec, AngleSpec]>
  /** Angle equality facts pre-loaded into the fact store before any construction.
   *  These appear as real [Given] facts in the proof panel. */
  givenAngleFacts?: Array<{
    left: { vertex: string; ray1: string; ray2: string }
    right: { vertex: string; ray1: string; ray2: string }
    statement: string
  }>
  /** Text conclusion for theorems (bypasses fact-store derivation display) */
  theoremConclusion?: string
  /** Dynamic conclusion computed from current construction state (for position-dependent proofs) */
  computeTheoremConclusion?: (state: ConstructionState) => string
  /** Superposition flash configuration for C.N.4 visual */
  superpositionFlash?: {
    pairs: Array<{ src: string; tgt: string }>
    triA: [string, string, string]
    triB: [string, string, string]
  }
  /** IDs of given points the user can drag post-completion */
  draggablePointIds?: string[]
  /** Factory that recomputes all given elements from current point positions.
   *  Called on each drag frame. Receives a map of draggable point ID → current {x,y}.
   *  Returns a fresh givenElements array with updated coordinates. */
  computeGivenElements?: (positions: Map<string, { x: number; y: number }>) => ConstructionElement[]
  /** Tutorial sub-step generator for guided interaction. Each inner array
   *  corresponds to one proposition step; sub-steps break the gesture into
   *  teachable micro-interactions. */
  getTutorial?: (isTouch: boolean, narration?: EuclidNarrationOptions) => TutorialSubStep[][]
  /** Post-completion exploration narration (intro speech + per-point tips) */
  explorationNarration?: ExplorationNarration
  /** Optional exploration narration variants keyed by language style */
  explorationNarrationByStyle?: Partial<Record<KidLanguageStyle, ExplorationNarration>>
  /** Optional step instruction overrides per language style */
  stepInstructionsByStyle?: Partial<Record<KidLanguageStyle, string[]>>
  /** Derive conclusion facts when the proposition completes. Mutates the
   *  fact store in place and returns newly derived facts. */
  deriveConclusion?: (store: FactStore, state: ConstructionState, atStep: number) => ProofFact[]
  /** Dynamic step resolution for adaptive constructions (e.g. Prop 5 free extends).
   *  Called when a step becomes current. Returns overrides for the step's expected action,
   *  instruction, and highlightIds. Null = no override. */
  resolveStep?: (
    stepIndex: number,
    state: ConstructionState,
    stepData: Map<number, Record<string, unknown>>
  ) => { expected?: ExpectedAction; instruction?: string; highlightIds?: string[] } | null
  /** Compute result segment IDs dynamically from the construction state.
   *  When present, overrides the static `resultSegments` array. */
  computeResultSegments?: (state: ConstructionState) => Array<{ fromId: string; toId: string }>
  /** Dynamic tutorial sub-step resolution for adaptive constructions.
   *  Called when a step becomes current. Returns replacement tutorial sub-steps
   *  for that step index, or null for no override. */
  resolveTutorialStep?: (
    stepIndex: number,
    state: ConstructionState,
    isTouch: boolean
  ) => TutorialSubStep[] | null
}
 
export interface EuclidNarrationOptions {
  languageStyle?: KidLanguageStyle
}
 
// ── Exploration narration (post-completion drag phase) ────────────
 
export interface PointExplorationTip {
  pointId: string
  speech: string
}
 
export interface ExplorationNarration {
  /** Speech played once when the proposition is first completed */
  introSpeech: string
  /** Per-point tips played on first drag of each given point */
  pointTips: PointExplorationTip[]
  /** Speech played once when the construction breaks down during drag
   *  (e.g. a precondition is violated and intersections disappear) */
  breakdownTip?: string
}
 
// ── Ghost geometry (dependency visualization) ────────────────────
 
/** Lightweight geometry for the ghost layer — not part of construction state */
export interface GhostCircle {
  kind: 'circle'
  cx: number
  cy: number
  r: number
  color: string
}
export interface GhostSegment {
  kind: 'segment'
  x1: number
  y1: number
  x2: number
  y2: number
  color: string
  isProduction?: boolean
}
export interface GhostPoint {
  kind: 'point'
  x: number
  y: number
  label: string
  color: string
}
export type GhostElement = GhostCircle | GhostSegment | GhostPoint
 
/** A collection of ghost elements from one macro invocation at a specific depth */
export interface GhostLayer {
  propId: number // which proposition's internals these represent
  depth: number // 1 = direct dependency, 2 = dependency's dependency, etc.
  elements: GhostElement[]
  atStep: number // construction step index that produced this layer
  /**
   * Ordered groups of element indices for the macro reveal ceremony.
   * Each inner array is revealed together; groups are shown sequentially.
   * If absent, all elements reveal at once.
   */
  revealGroups?: number[][]
  /**
   * TTS narration spoken when this layer finishes its reveal ceremony.
   * Only meaningful on depth-1 layers (the top-level macro result).
   */
  keyNarration?: string
}
 
/** State driving the macro reveal ceremony in the RAF loop */
export interface MacroCeremonyState {
  /** Ordered list of (layerKey, groupIndex) to reveal, deepest-depth first */
  sequence: Array<{ layerKey: string; groupIndex: number; msDelay: number }>
  /** How many sequence entries have been revealed so far */
  revealed: number
  /** Timestamp (performance.now()) when the last reveal fired */
  lastRevealMs: number
  /** Narration text to speak when all groups are shown */
  narrationText: string
  narrationFired: boolean
  /** Timestamp when all groups were shown (null = not yet complete) */
  allShownMs: number | null
  /** ms after allShownMs before advancing the step */
  postNarrationDelayMs: number
  /** The deferred step-advance closure */
  advanceStep: () => void
  /**
   * Layers pre-revealed from frame 1 (depth-1 layers already visible as the
   * live macro preview). Maps layerKey → full group count. These stay visible
   * throughout the ceremony while deeper layers animate in underneath.
   */
  preRevealedLayers: Map<string, number>
  /**
   * Per-element draw animation state.
   * Key = `${layerKey}:${elementIdx}`. Populated when each group is revealed.
   */
  elementAnims: Map<string, { startMs: number; durationMs: number }>
  /**
   * IDs of construction elements added by the macro that must stay hidden
   * until the ceremony completes. Cleared when `advanceStep` fires.
   */
  hiddenElementIds: Set<string>
}
 
/**
 * Whether a proposition needs Post.2 segment extension for intersection computation.
 * Derived from step definitions: any step with `beyondId` implies the construction
 * "produces" (extends) a finite line, requiring circle-line intersection on the extension.
 */
export function needsExtendedSegments(prop: PropositionDef): boolean {
  return prop.steps.some(
    (s) =>
      s.expected.type === 'extend' ||
      (s.expected.type === 'intersection' && s.expected.beyondId != null)
  )
}
 
// ── Proof Editor JSON types ────────────────────────────────────────
 
export interface ProofJSON {
  id: number
  title: string
  kind: 'construction' | 'theorem'
  givenElements: SerializedElement[]
  steps: SerializedStep[]
  givenFacts?: SerializedEqualityFact[]
  givenAngleFacts?: SerializedAngleEqualityFact[]
  resultSegments?: { fromId: string; toId: string }[]
  authorNotes?: string
}
 
export interface SerializedElement {
  kind: 'point' | 'circle' | 'segment'
  id: string
  label?: string
  x?: number
  y?: number
  centerId?: string
  radiusPointId?: string
  fromId?: string
  toId?: string
  color: string
  origin: ElementOrigin | 'compass' | 'given'
}
 
export interface SerializedStep {
  citation: string
  instruction: string
  action: SerializedAction
  intersections?: SerializedIntersection[]
  notes?: string
}
 
export type SerializedAction =
  | { type: 'compass'; centerId: string; radiusPointId: string }
  | { type: 'straightedge'; fromId: string; toId: string }
  | { type: 'intersection'; ofA: string; ofB: string; label: string; beyondId?: string }
  | {
      type: 'macro'
      propId: number
      inputPointIds: string[]
      outputLabels?: Record<string, string>
    }
  | { type: 'fact-only' }
  | { type: 'extend'; baseId: string; throughId: string; distance: number; label: string }
 
export interface SerializedIntersection {
  x: number
  y: number
  ofA: string
  ofB: string
  label: string
}
 
export interface SerializedEqualityFact {
  left: { a: string; b: string }
  right: { a: string; b: string }
  statement: string
}
 
export interface SerializedAngleEqualityFact {
  left: { vertex: string; ray1: string; ray2: string }
  right: { vertex: string; ray1: string; ray2: string }
  statement: string
}
 
// ── Proposition reference data ────────────────────────────────────
 
export interface PropositionRef {
  id: number
  type: 'C' | 'T'
  title: string
  statement: string
  method: string
  deps: string[]
  note: string
  block: string
}