Back to portfolio

Personal project / Software validation & build pipelines

Preflight

Preflight checks whether a machine and a change meet project requirements before a commit or build. It serves game studios and software companies in general: it detects missing SDKs, oversized files and policy violations through explicit rules, with diagnostics that explain what to fix.

Why run it before a commit?

An oversized source texture or generated binary can enter Git and only be discovered in CI. The pre-submit stage checks changed files against policy before submission: the author gets the path, limit, actual size and corrective action while the change is still easy to adjust. A commit hook and CI can invoke the same logic; the team must configure that hook.

Deterministic means that identical files, inspected environment, rules, policy and target produce the same verdict and finding order. Decisions follow verifiable conditions, such as comparing bytes with a limit, without AI interpretation. This does not guarantee a successful build or working software: it catches known prerequisites early. Durations and run identifiers vary between runs.

  • C#
  • .NET
  • JSON
  • SARIF
  • Windows
Illustrative diagnostic, using a file-size rule from the public documentation. This is a reading example, not a captured run.
preflight run --stage pre-submit --changed-from origin/main --platform win64

core.presubmit.large-file   Failed
  at        Art/Characters/hero_diffuse.tga
  expected  <= 2,621,440 bytes
  actual    11,400,000 bytes
  fix       Remove the file from version control, or ask the pipeline owner to review the limit.

preflight explain core.presubmit.large-file --platform win64

The file contains 11,400,000 bytes, exceeding the Win64 limit of 2,621,440 bytes. The rule compares size with maxBytes; it does not judge a texture’s artistic value or code correctness. preflight explain lets the author check why that limit applies.

01 / Context

Late feedback, cascading errors and diverging scripts

In a studio, the issue might be an asset over budget; in a software company, a missing SDK, a forbidden path or a build artifact added to the repository. These conditions can be checked before expensive pipeline steps. Putting validation only in CI delays feedback until a queued job runs; maintaining a separate developer script introduces another source of divergence.

I separated the check, compiled in C#, from its policy, written in JSON, so the logic exists once while requirements vary by project. Infrastructure owners define rules, limits and blocking behaviour and publish a versioned package. Developers install that package and run the tool without implementing checks. CI remains the team’s gate and executes the same logic.

What happens during a pre-submit run

  1. 01

    Policy resolution

    preflight run --stage pre-submit --changed-from origin/main --platform win64 selects the pipeline declared by the checkout and an accepted installed version. The platform argument selects its policy layer.

  2. 02

    Rule and dependency selection

    --changed-from origin/main defines the Git diff reference; it is not a staged-files-only filter. The pre-submit stage selects root rules and includes their prerequisites, even when they belong to other stages.

  3. 03

    Execution and reporting

    Rules execute by graph level with bounded parallelism. The report gives location, expected value, actual value and fix. preflight explain core.presubmit.large-file --platform win64 shows where the applied limit came from.

workspace
Checks the machine and development environment.
pre-submit
Checks changed files against project policy.
build-readiness
Checks the prerequisites for a build.

The command evaluates tracked files in the Git diff against the chosen reference. It does not commit or automatically install a hook.

02 / Design & architecture

Architecture decisions and their consequences

Rule & policy

One implementation, different project limits

A rule knows how to check something. Its policy decides whether it runs, its settings, severity and blocking behaviour. Two projects can use the same assembly with different file-size budgets, without forking the code. This makes the validation logic independently testable and lets pipeline owners change requirements without rebuilding the tool.

Example JSON policy: one rule with a general limit and a Win64 override.
{
    "schemaVersion": 1,
    "pipeline": "projecta",
    "rules": {
        "core.presubmit.large-file": {
            "settings": {
                "maxBytes": 5242880
            }
        }
    },
    "targets": {
        "win64": {
            "rules": {
                "core.presubmit.large-file": {
                    "settings": {
                        "maxBytes": 2621440
                    }
                }
            }
        }
    }
}

Without an explicit target, maxBytes is 5,242,880 (5 MiB). With --platform win64, it becomes 2,621,440 (2.5 MiB). The C# implementation stays the same; the requirement changes. This excerpt illustrates parameter selection without reproducing the distribution manifest.

Execution graph

Dependencies executed in graph levels

Rules declare dependencies and execute in topological levels. Independent checks share a level; later levels wait for their prerequisites. If the toolchain check fails, a dependent compile probe can be skipped rather than producing another predictable failure.

Root-cause attribution follows the dependency chain through intermediate skips. The final message points back to the missing toolchain, giving the developer one problem to solve instead of a list of symptoms. The stage selects the roots of this graph, rather than discarding dependencies from other stages.

I chose a barrier between levels to simplify failure propagation and coordination. A slow rule holds up the next level even when part of it could already start. This is a deliberate cost: execution is easier to audit, and reports are ordered by level and ID regardless of which task finishes first.

Two independent controls

blocking and gating answer different questions

A naming convention can block a submit without making compilation meaningless. An optional probe can be a technical prerequisite even when its failure should not reject the submission. A single boolean cannot express both cases. Preflight keeps blocking, which affects the verdict and exit code, separate from gating, which stops dependent rules. Severity remains the communication level, not a substitute for either decision.

Policy provenance

Origin and precedence of policy values

Policies can inherit settings, apply explicit platform/configuration targets and seal keys against downstream overrides. Seals accumulate along the inheritance chain, so a project cannot quietly remove an organisation-wide restriction. Local machine overlays are kept out of CI.

The explain command records where effective values came from, including package, file, line and the values they replaced. Target axes must be explicitly supplied to match a target block. That prevents a configuration default from silently choosing a different production policy.

Precedence must be visible because a JSON value alone does not explain the effective configuration. Without provenance, investigating a difference between a local machine and CI would require reconstructing inheritance by hand. preflight explain makes that investigation part of the resolver’s own data.

Plugin boundary

One contract for built-in and external rules

Rules implement IValidationRule against Preflight.Abstractions. Filesystem, process, changes and policy access arrive through RuleContext services, allowing checks to be tested without the real workspace. The built-in rule assembly has no privileged dependency on Core.

Plugins load into separate collectible assembly contexts while sharing the contract assembly with the host. Dependency versions can coexist; duplicate rule IDs and incompatible contracts produce named refusals instead of an arbitrary winner. This is dependency isolation, not a security sandbox: plugins remain code trusted by the pipeline owner.

Distribution

Rules and policy distributed as a versioned package

A pipeline package contains policy, rule assemblies and a manifest with per-file SHA-256 digests and a supported contract range. Stable entry order and fixed archive timestamps make package bytes reproducible. Installation verifies the archive before committing it to the installed store.

The checkout declares an accepted version range; a machine can pin a version for rollback. Installing a package does not move that pin. Preflight does not fetch updates itself, leaving distribution to the studio’s existing artifact channel and avoiding surprise rule changes across developer machines.

Trustworthy output

Result states with distinct meanings

Passed, Warning, Failed, Errored, Skipped and NotApplicable have distinct meanings. A check with nothing applicable to inspect does not claim success; a crashed rule is distinguished from a defect in the workspace. Console, JSON and SARIF render the same report data. Exit codes separate a blocked change from invalid configuration and an internal error.

History is local append-only NDJSON; duration statistics appear only with enough observations. The tool can time a build command, but timing it does not establish that it was validated or that the software works.

Determinism applies to the verdict and finding order when inputs match, including the inspected environment and pipeline version. Duration and runId vary by design; comparing report bytes requires controlling those fields. The same policy on machines with different SDKs can correctly produce different results.

Incremental cache

Caching conditional on input identity

Caching is opt-in and only available for rules that supply an explicit input fingerprint. The key also includes the rule’s effective policy, stage, target, contract generation and assembly identity. Changing a threshold or rebuilding a plugin therefore invalidates the old result. Cached outcomes are visibly marked, so saved work is never presented as a fresh check.

The cost is requiring rule authors to describe relevant inputs. For a probe that cannot safely represent its environment, rerunning is preferable to reusing incomplete evidence. Caching is off by default.

03 / Under the hood

Boundaries between contracts, execution, rules and interface

Preflight.Abstractions

The plugin-facing vocabulary: descriptors, outcomes, context and service interfaces. It depends on the base class library, keeping the contract small enough for external rules to reference.

Preflight.Core

Policy resolution, dependency graphs, execution, plugin loading, cache and history. Report data is computed here without a dependency back into the CLI.

Preflight.Rules

Built-in checks use the public contract, just as a team plugin does. They demonstrate workspace, change and build-readiness validation without embedding each project’s requirements in the tool.

Preflight.Cli

Commands, parsing, pipeline packaging and output renderers. The command line hosts the core; another integration can consume report data without scraping terminal text.

How the behaviour is checked

The repository combines unit tests, plugin-contract checks, exact console-output tests and Gherkin scenarios that run the published executable. Boundary checks prevent built-in rules from depending on Core and prevent Core from depending on the CLI. Its verification script checks formatting, builds with warnings as errors, runs the suites and collects coverage. These are development checks on the tool, separate from the checks a game pipeline supplies.

04 / Work in progress

Current implementation and contract limits

Work in progress

Preflight is public under the MIT licence and remains below 1.0. The current implementation targets .NET 10 and is developed on Windows. Rules, policies, pipeline packages and reports already work together; the public API is still allowed to change.

The direction is to strengthen checks and evidence for game pipelines and software development in general. Specific rules belong to the team that knows its project’s requirements. Compilation and tests remain with their existing tools; an IDE or build farm could host the same core. This page describes the existing CLI.

Explore the project

The README covers installation, daily use, rule authoring, policies and pipeline distribution. The source shows how these contracts connect to execution and reporting.

Read the documentation on GitHub

Contact

Québec City, QC, Canada / Available for opportunities

I'm open to opportunities in software development, tools programming and gameplay programming, as well as junior 3D artist roles. If my experience could be a good fit for your team, I'd be happy to connect through my social profiles.