RGreview-game-corethe engine behind concept-first review games

Feature detail

Graph contracts / query / projector

The graph layer projects authored learning structure into Neo4j as a rebuildable read model instead of turning the graph into an operational source of truth.

graph/contractsgraph/querygraph/projector

Worked example

Projection relationship shape

Graph contracts define canonical ladders, objectives, and artifacts without forcing the graph to own app state.

Input

{
  "objective": "solve-quadratic-by-factoring",
  "concept": "factoring-quadratics",
  "ladderSteps": [
    "recognition",
    "setup",
    "independent_proof"
  ]
}

Output

{
  "projectionRows": [
    {
      "objectiveId": "solve-quadratic-by-factoring",
      "gameConceptId": "factoring-quadratics",
      "ladderId": "factoring-quadratics-core"
    }
  ]
}

Real source excerpt

Projection contracts

src/graph/contracts/index.ts
export const GRAPH_SCHEMA_VERSION = 1

export const graphStepLayerSchema = z.enum([
  'recognition',
  'setup',
  'light_application',
  'independent_proof',
  'harder_transfer',
])

export type GraphStepLayer = z.infer<typeof graphStepLayerSchema>

export const graphProjectionStatusSchema = z.enum([
  'started',
  'applied',
  'reconciled',
  'failed',
])

export type GraphProjectionStatus = z.infer<typeof graphProjectionStatusSchema>

const nonEmptyString = z.string().trim().min(1)
const isoDateTimeString = z.string().datetime({ offset: true })

export const projectionRunMetadataSchema = z.object({
  projectionId: nonEmptyString,
  gameId: nonEmptyString,
  sourceRepo: nonEmptyString,
  sourceCommitSha: nonEmptyString,
  contentHash: nonEmptyString,
  generatedAt: isoDateTimeString,
  schemaVersion: z.number().int().positive(),
  status: graphProjectionStatusSchema,
  startedAt: isoDateTimeString.optional(),
  completedAt: isoDateTimeString.nullable().optional(),
})

export type ProjectionRunMetadata = z.infer<typeof projectionRunMetadataSchema>

export const graphGameProjectionSchema = z.object({
  gameId: nonEmptyString,
  label: nonEmptyString,
  description: z.string().trim().optional(),
})

export type GraphGameProjection = z.infer<typeof graphGameProjectionSchema>

export const graphUnitProjectionSchema = z.object({
  gameId: nonEmptyString,
  unitId: nonEmptyString,
  label: nonEmptyString,
  ordinal: z.number().int().nonnegative().optional(),
  description: z.string().trim().optional(),
})

export type GraphUnitProjection = z.infer<typeof graphUnitProjectionSchema>

export const graphSectionProjectionSchema = z.object({
  gameId: nonEmptyString,
  unitId: nonEmptyString,
  sectionId: nonEmptyString,
  label: nonEmptyString,
  ordinal: z.number().int().nonnegative().optional(),
  description: z.string().trim().optional(),
})

export type GraphSectionProjection = z.infer<typeof graphSectionProjectionSchema>

export const graphObjectiveProjectionSchema = z.object({
  gameId: nonEmptyString,
  unitId: nonEmptyString,
  sectionId: nonEmptyString,
  objectiveId: nonEmptyString,
  label: nonEmptyString,
  description: z.string().trim().optional(),
  requiredForCompletion: z.boolean().default(true),
  sourceMode: z.string().trim().optional(),
  sourceRefs: z.array(nonEmptyString).default([]),
})

export type GraphObjectiveProjection = z.infer<typeof graphObjectiveProjectionSchema>

export const graphCanonicalConceptProjectionSchema = z.object({
  canonicalConceptId: nonEmptyString,
  label: nonEmptyString,
  description: z.string().trim().optional(),
  domain: z.string().trim().optional(),
})

export type GraphCanonicalConceptProjection = z.infer<typeof graphCanonicalConceptProjectionSchema>

export const graphGameConceptProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  label: nonEmptyString,
  description: z.string().trim().optional(),
  canonicalConceptId: nonEmptyString.optional(),
})

export type GraphGameConceptProjection = z.infer<typeof graphGameConceptProjectionSchema>

export const graphSubskillProjectionSchema = z.object({
  subskillId: nonEmptyString,
  label: nonEmptyString,
  description: z.string().trim().optional(),
})

export type GraphSubskillProjection = z.infer<typeof graphSubskillProjectionSchema>

export const graphLadderTemplateProjectionSchema = z.object({
  gameId: nonEmptyString,
  ladderId: nonEmptyString,
  gameConceptId: nonEmptyString,
  completionStepId: nonEmptyString,
  masteryStepId: nonEmptyString,
  retentionStepId: nonEmptyString,
  repairStepId: nonEmptyString,
})

export type GraphLadderTemplateProjection = z.infer<typeof graphLadderTemplateProjectionSchema>

export const graphLadderStepProjectionSchema = z.object({
  gameId: nonEmptyString,
  ladderId: nonEmptyString,
  stepId: nonEmptyString,
  layer: graphStepLayerSchema,
  ordinal: z.number().int().nonnegative(),
  supportPolicy: z.enum(['clean_only', 'hybrid', 'supported_ok']),
  completionCredit: z.boolean().default(false),
  masteryCredit: z.boolean().default(false),
})

export type GraphLadderStepProjection = z.infer<typeof graphLadderStepProjectionSchema>

export const graphQuestionArtifactProjectionSchema = z.object({
  gameId: nonEmptyString,
  questionId: nonEmptyString,
  unitId: nonEmptyString,
  sectionId: nonEmptyString,
  objectiveId: nonEmptyString,
  conceptIds: z.array(nonEmptyString).min(1),
  targetStepIds: z.array(nonEmptyString).default([]),
  subskills: z.array(nonEmptyString).default([]),
  variantFamily: z.string().trim().optional(),
  difficultyTag: z.string().trim().optional(),
  canonicalSignature: nonEmptyString.optional(),
  promptHash: nonEmptyString,
  sourceRef: nonEmptyString.optional(),
  promptMirror: z.string().optional(),
  explanationMirror: z.string().optional(),
})

export type GraphQuestionArtifactProjection = z.infer<typeof graphQuestionArtifactProjectionSchema>

export const graphObjectiveTeachesConceptProjectionSchema = z.object({
  gameId: nonEmptyString,
  objectiveId: nonEmptyString,
  gameConceptId: nonEmptyString,
})

export type GraphObjectiveTeachesConceptProjection = z.infer<typeof graphObjectiveTeachesConceptProjectionSchema>

export const graphGameConceptRequiresProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  requiredGameConceptId: nonEmptyString,
})

export type GraphGameConceptRequiresProjection = z.infer<typeof graphGameConceptRequiresProjectionSchema>

export const graphGameConceptRelatedProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  relatedGameConceptId: nonEmptyString,
})

export type GraphGameConceptRelatedProjection = z.infer<typeof graphGameConceptRelatedProjectionSchema>

export const graphGameConceptTransferProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  transferGameConceptId: nonEmptyString,
})

export type GraphGameConceptTransferProjection = z.infer<typeof graphGameConceptTransferProjectionSchema>

export const graphGameConceptUsesSubskillProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  subskillId: nonEmptyString,
})

export type GraphGameConceptUsesSubskillProjection = z.infer<typeof graphGameConceptUsesSubskillProjectionSchema>

export const graphConceptHasLadderProjectionSchema = z.object({
  gameId: nonEmptyString,
  gameConceptId: nonEmptyString,
  ladderId: nonEmptyString,
})

export type GraphConceptHasLadderProjection = z.infer<typeof graphConceptHasLadderProjectionSchema>

export const graphLadderHasStepProjectionSchema = z.object({
  gameId: nonEmptyString,
  ladderId: nonEmptyString,
  stepId: nonEmptyString,
})

export type GraphLadderHasStepProjection = z.infer<typeof graphLadderHasStepProjectionSchema>

export const graphLadderStepNextProjectionSchema = z.object({
  gameId: nonEmptyString,
  ladderId: nonEmptyString,
  stepId: nonEmptyString,
  nextStepId: nonEmptyString,
})

export type GraphLadderStepNextProjection = z.infer<typeof graphLadderStepNextProjectionSchema>

export const graphLadderStepRepairsToProjectionSchema = z.object({
  gameId: nonEmptyString,
  ladderId: nonEmptyString,
  stepId: nonEmptyString,
  repairStepId: nonEmptyString,
})

export type GraphLadderStepRepairsToProjection = z.infer<typeof graphLadderStepRepairsToProjectionSchema>

export const graphQuestionAssessesConceptProjectionSchema = z.object({
  gameId: nonEmptyString,
  questionId: nonEmptyString,
  gameConceptId: nonEmptyString,
})

export type GraphQuestionAssessesConceptProjection = z.infer<typeof graphQuestionAssessesConceptProjectionSchema>