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.
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
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>