Skip to content

Checker Pipeline

The checker reads .shape files and check options, and nothing else. checkShapeFiles parses every file with the Langium grammar (Langium Grammar), lowers the parsed modules into one Model (Fact Lowering), runs the semantic checks and binding enforcement over that model (Rule Evaluation), and returns a CheckResult with sorted diagnostics. Each phase reads only the previous phase’s output: rules read the Model and check options, and formatDiagnostics reads only the CheckResult.

Shape files are parsed, lowered into one Model, checked by the ordered rules and then bindings, and sorted into a CheckResult; a parse failure in any file stops the pipeline with exit code 2 and no Model.

Paths are relative to packages/shp-checker/src/.

PhaseCodeOutputFailure outcome
Read and parsecheckShapeFiles reads each path; parseShapeModule in parser.ts parses it with the Langium services from language/shape-module.ts.One ShapeModule AST per file.A read error, or any lexer or parser error in any file, ends the check. The result holds only parse diagnostics, exitCode is 2, and no Model is built.
LowerlowerShapeModules in checker/lowerer.ts, driving the domain lowerers in checker/lowering/*.Model: typed indexes, facts, and lowering diagnostics.Lowering always completes. Problems it finds (duplicate declarations, ambiguous or unknown names, invalid relations, invalid candidate effects, invalid require_context) become model.diagnostics.
CheckrunSemanticChecks runs SEMANTIC_CHECKS in checker/rules.ts; then checkBindings runs unless enforceBindings is false.Semantic diagnostics.No check stops a later one; each appends its diagnostics.
AssemblecheckLoweredShapeModel in checker/api.ts.CheckResult: ok, exitCode, sorted diagnostics, and facts when includeFacts is set.exitCode is 1 when any blocking diagnostic remains; stale attestations and downgraded unknown_effects are warnings.
RenderformatDiagnostics in checker/diagnostics.ts.Text.None. It reads only the CheckResult.

checkLoweredShapeModel assembles the diagnostic list in a fixed order: lowering diagnostics, then the registry’s output, then binding diagnostics. It then applies the allowUnknownEffects downgrade, computes ok and exitCode, sorts the diagnostics, and attaches sorted facts when requested. The sort makes the assembly order invisible in the returned list.

  • checkShapeFiles(paths, options) is the path shp check takes. It reads and parses every path, in the given order, before lowering any of them. When all of them parse, it classifies each module’s origin with moduleOriginForShapeFile against the normalized repoRoot and passes the modules to checkShapeModules. A module is generated AST only when its name is shape.generated.ast or starts with shape.generated.ast., and its path relative to repoRoot is under shape/generated/ast/.
  • checkShapeModules(modules, options) takes parsed ShapeModule[] or CheckModuleInput[] (module, optional filePath, optional origin). It trusts only explicit origins: an input without origin: "generated_ast" is authored. It normalizes the options, calls lowerShapeModules, and passes the model to checkLoweredShapeModel. It never returns exit code 2.
  • checkLoweredShapeModel(model, normalizedOptions) is internal. checker/api.ts exports it, but the package does not. Both entry points and the incremental checker assemble their results through it. When checkLoosening adds diagnostics afterwards, checkShapeModules recomputes ok and the order with the same helpers, isWarning and compareShapeDiagnostics.
  • IncrementalShapeChecker caches parsed documents and the last lowered model for callers that check an in-memory workspace repeatedly. See Incremental checking.

@shape/shp-checker (src/index.ts) exports checkShapeFiles, checkShapeModules, and IncrementalShapeChecker. It does not export lowerShapeModules or checkLoweredShapeModel. The read-only query helpers in checker/query.ts (explainShapeModules, graphShapeModules, listMemoryGuardsShapeModules, and others) lower the modules themselves and never decide pass or fail; listShapeObligations calls checkShapeModules and filters its diagnostics.

normalizeCheckOptions resolves the repository root, summarizes any base model, and validates the freshness date before semantic checking:

OptionDefaultRead byEffect
changedFilesnonecheckCoverage, checkBindingsPaths are normalized against repoRoot. With no paths, coverage and bindings report nothing.
baseModulesnonecheckCoverage, checkBindings, checkGuardedSources, checkStaleAttestationsSummarized once into attestation, reevaluation, and context keys. An attestation identical to the base does not count and is reported as stale_attestation.
checkLooseningoffcheckShapeModulesReverts each rule-layer edit to the base in turn and reports rule_loosening. Requires baseModules.
repositoryFilesnonecheckCitedPaths, the loosening checkEvery file in the repository. Cited paths must be in it, and the loosening check ignores coverage errors about files the change deleted.
repoRootthe working directorycheckShapeFiles, checkCoverage, checkBindingsResolved to an absolute path. It classifies module origins and normalizes absolute changed-file and provenance paths.
freshnessDateunsetcheckFreshnessAn ISO YYYY-MM-DD date; unset turns freshness off. An invalid value throws a TypeError instead of producing a diagnostic.
enforceBindingsoncheckLoweredShapeModelfalse skips checkBindings.
allowUnknownEffectsoffcheckLoweredShapeModelDowngrades every unknown_effects diagnostic to severity warning before ok is computed.
includeFactsoffcheckLoweredShapeModelReturns model.facts in the result.

lowerShapeModules takes no options. Only repoRoot can affect its input, through origin classification in checkShapeFiles and the incremental checker, so one lowered Model can be checked under any values of the other options.

CheckResult.ok is true when every diagnostic is non-blocking, including when there are none. Only two kinds are non-blocking: stale_attestation, always, and unknown_effects once allowUnknownEffects has set its severity to warning.

exitCodeMeaning
0ok is true. formatDiagnostics prints Shape check passed., or the warnings followed by Shape check passed with warnings.
1The model was lowered, and at least one blocking diagnostic remains.
2checkShapeFiles could not read or parse at least one file. Only parse diagnostics are reported, and no semantic check ran.

The exit codes of each shp command, including usage errors, are listed in the CLI Reference.

The same .shape files and check options produce the same lowered model, facts, and diagnostics. The order of diagnostics is deterministic over the input set, not its source order:

  • checkLoweredShapeModel sorts diagnostics with compareShapeDiagnostics from checker/diagnostics.ts: by kind, then by rendered text, comparing by Unicode codepoint (compareCodepointStrings), so the order does not depend on locale. Diagnostic order therefore depends on neither the SEMANTIC_CHECKS order nor declaration order.
  • With includeFacts, facts are sorted by the codepoint order of their JSON text.
  • A parse-failure result is not sorted. Parse diagnostics follow the order of the input paths, and within one file lexer errors come before parser errors. shp sorts the paths its default discovery finds; explicit file arguments keep their order.
  • The checker never reads the system clock. checkFreshness compares against the injected freshnessDate; the CLI computes today’s date when --strict-freshness asks for it.
  • forbid path and forbid hypercycle witnesses are chosen canonically. The algorithm and its tie-breaks are in Rule Evaluation.

packages/shp-checker/src/behavioural/determinism.test.ts enforces these properties: byte-identical output across repeated runs of every public surface, identical diagnostics under a permutation of top-level declarations, and no clock reads in the checker.

IncrementalShapeChecker serves library callers that check an in-memory workspace repeatedly. Each check(documents, options) call supplies the complete current snapshot, so a document missing from the list counts as removed. It returns the CheckResult and an invalidation report.

import { IncrementalShapeChecker } from "@shape/shp-checker";
const checker = new IncrementalShapeChecker();
const { result, invalidation } = checker.check(
[{ filePath: "shape/history.shape", source: "module history\nresource Revision\n" }],
{ includeFacts: true }
);

Any document change rebuilds the complete Model and fact list, because imports, duplicate declarations, change blocks, concrete targets, and derived facts cross file boundaries. No file owns an independent fact shard.

Change since the previous callReparsedModel and factsRulesReport (causes; derivedFacts; diagnostics)
First callevery documentbuiltruninitial_check; rebuilt; recomputed
A document added, removed, or changed (its source or explicit origin)changed documents onlyrebuiltrerunshape_documents_changed; rebuilt; recomputed
Options only, and the new repoRoot reclassifies a document with an absolute path and no explicit originnonerebuilt without reparsingreruncheck_options_changed; rebuilt; recomputed
Any other options-only change, including changedFilesnonereusedreruncheck_options_changed; reused; recomputed
Nothingnonereusednot run; the previous result is returnedempty; reused; reused
Any document fails to parsechanged documentsdiscardednot run; the result holds only parse diagnostics, exit code 2the cause from the matching row above; unavailable; recomputed if documents changed, otherwise reused

causes lists both shape_documents_changed and check_options_changed when a call changes documents and options together.

The boundary contracts:

  • Document paths must be unique within a snapshot. A duplicate path throws a TypeError rather than letting array order decide which source wins.
  • reparsedDocuments, reusedDocuments, and removedDocuments are sorted by codepoint.
  • Every returned CheckResult is a structuredClone of the cached one, so a caller’s mutation cannot corrupt the cache.
  • Like checkShapeFiles, the incremental checker classifies implicit origins against the normalized repoRoot, which defaults to the working directory.

The cache changes how much work is reused, not what the checker decides. Rules always run through checkLoweredShapeModel, and packages/shp-checker/src/checker/incremental.test.ts compares incremental results with the uncached full check, which remains authoritative.

ChangeWhere
Syntaxlanguage/shape.langium, then regenerate. See Langium Grammar.
Parse diagnosticsparser.ts
Model, fact, diagnostic, and option typeschecker/model.ts, which holds data shapes only
Pass order during loweringchecker/lowerer.ts
Lowering one kind of declarationchecker/lowering/*
Name and module resolutionchecker/symbols.ts and module-resolution.ts
change staging and change eventschecker/lowering/changes.ts and checker/change-planning.ts
Prelude traits, context obligations, and relation kindsprelude.ts; checker/prelude-seed.ts seeds its traits and obligations into the model
Queries shared by lowering, rules, and helperschecker/derivations.ts
A semantic checka module under checker/rules/, registered in SEMANTIC_CHECKS. See Rule Evaluation.
Guard matching over change eventsmemory-guards.ts
Pass condition, option normalization, and result assemblychecker/api.ts
Diagnostic text and orderingchecker/diagnostics.ts
explain, graph, memory, and obligations outputchecker/query.ts
shp inspect outputchecker/inspection.ts
Incremental reusechecker/incremental.ts

Analyzer hints, AST drafts, and the authoring helpers sit outside this pipeline and never decide pass or fail; see Helper APIs. The experiments in src/experiments/ and experiments/semantic-kernel/ also stay outside it; see Rule Evaluation.