Skip to content

SDE testing scorecard ​

What protects src/sde, measured. The Static Data Export module ships as its own sub-paths (./sde, ./sde/memory), shares no code with the HTTP pipeline, and is scored, ratcheted and restored on its own in every tier the core has. This page is the SDE's copy of the core's Where the suite stands and Where the scores stand: one table per tier, the command that reproduces it, and the floor that holds it.

The first three suite rows, the coverage and the specification counts were re-measured on 2026-10-08 after Run M merged; the mutation section says where each score comes from, and the remaining rows were measured on e4daf989 on 2026-09-28. The SDE's own docs, the module's role and its isolation rule are in SDE.md.

Where the suite stands ​

CommandConfigSuitesTestsResult
npx jest --config config/jest/unit.config.cjs tests/tdd/sdeconfig/jest/unit.config.cjs17609All pass but one, about 7 s; the ./sde/memory bundle test skips until npm run build has made dist/
npm run bdd:sdesame23227All pass, about 9 s; every scenario executes (bdd:report)
npx jest --config config/jest/unit.config.cjs sdesame431,001The two above plus sde-bench, sde-drift and sde-spec-coverage under tests/tdd/, about 22 s
npx jest --config config/jest/fuzz.config.cjs sdeconfig/jest/fuzz.config.cjs3249All pass, about 18 s; 100 runs per property on a pull request, 10,000 in nightly-properties.yml
npx jest --config config/jest/integration.config.cjs sdeconfig/jest/integration.config.cjs1114Skip without sde-data/; all 114 pass against CCP's current export in nightly-sde.yml
npm run test:typestsd2—sde.test-d.ts and sde-memory.test-d.ts, 801 and 603 lines of expectType / expectError assertions over the two entry points
npm run test:consumertests/consumer/2—Steps 7 and 8: ./sde loads without js-yaml and adm-zip and names the missing one; with them installed it reads real YAML and ZIP, CJS and ESM

The unit suites are one per module: the IStaticDataProvider contract (94 tests over the 96 methods, null and empty returns and the typing of each), MemorySdeProvider, SdeDataProvider, SdeTestDataFactory, schemas, errors, clock, optionalPeers, order, index, the ./sde/memory bundle test, and five under ingestion/ (SdeDownloader, SdeExtractor, transforms, registry, index).

Coverage over src/sde/** (excluding src/sde/testing/)Unit suites aloneUnit and BDD togetherFloor (config/jest/unit.config.cjs)
Statements98.45%99.88%90%
Branches98.26%98.26%80%
Functions97.28%97.28%75%
Lines98.65%100.00%90%

Reproduce with npx jest --config config/jest/unit.config.cjs --coverage --collectCoverageFrom='src/sde/**/*.ts' --collectCoverageFrom='!src/sde/testing/**' tests/tdd/sde tests/bdd/specs/sde. The floors are the repository's; the SDE has no separate ones, and its numbers sit above the core's on every line. Coverage says a line ran; the mutation table below says whether a test would notice it changing.

Specification (npm run spec:audit:verbose, features/sde/)Count
Feature files23, all on the step library (tests/bdd/specs/sde/, tests/bdd/steps/); none on legacy defineFeature files
Rule: requirements156, one EARS shall each
Scenarios223, a Scenario Outline counted once
Audit result23 of 23 pass; scripts/spec/spec-audit-exceptions.json lists no SDE file
Method coverage (npm run spec:coverage:sde:ci)96 of 96 IStaticDataProvider methods reached by a Rule or a bound step; scripts/sde/sde-spec-coverage-baseline.json is empty, so a method that loses its Rule fails CI

The feature files run from 0001-static-data-lookup to 0023-sde-ingestion: the entity families in the interface's order (types, universe, market, dogma, characters, NPC organisations, presentation tables, industry, mission content, generic access), then loading, the optional peers, the ./sde/memory entry and ingestion. The largest are 0023-sde-ingestion (33 Rules, 43 scenarios), 0014-sde-npc-organisations (16, 25) and 0020-sde-loading (14, 19).

Where the scores stand ​

Three mutation runs score the SDE, each with its own shard and its own floors, ratcheted the way the core's are (guides/TESTING.md, "Where the scores stand"): a complete nightly raises a floor to that night's score and never lowers one, and a pull request cannot lower or remove one.

Track S Run M (#458, 2026-10-07 and 08) took every SDE directory to 90 or above on both the unit and the BDD-only run, one directory per pull request. Each pull request gave every surviving mutant a verdict: a new test or scenario kills it, or it is equivalent and carries // Stryker disable next-line <Mutator>: <reason> in the source, or it is listed below with the reason no test should name it. The scores are local runs with the nightly's own configs (config/mutation/stryker.config.mjs and stryker.bdd.config.mjs, --mutate narrowed to the directory), except the unit src/sde row, which Run M did not need to touch and which comes from the nightly of 2026-09-28.

DirectoryUnit scoreKilled / validUnit floorPull requestBDD-only scoreKilled / validBDD floorPull request
src/sde93.8%15/1685.6(nightly)100.0%15/1593#611
src/sde/ingestion100.0%184/18498#60897.6%162/16697#612
src/sde/providers100.0%92/9298#60790.5%86/9590#610

The src/sde/ingestion figures include SdeDatabaseBuilder.ts and the SQLite transforms, removed after 12.2.0; the next nightly scores the directory without them.

Before Run M the same directories scored 93.8 / 73.9 / 89.6 on the unit run and 50.0 / 59.0 / 58.6 BDD-only. The unit src/sde floor still holds its provisional seed, under its score, until a complete nightly raises it.

src/sde is the root files: clock.ts, errors.ts and optionalPeers.ts. src/sde/domain holds only type declarations and Zod schemas and yields no valid mutants; src/sde/testing is excluded from both runs like src/testing. A mutant that crashes the runner (a missing-file check removed, a transform recursing on itself) counts as a runtime error and in no score. A unit run takes 10 to 15 minutes a directory on four cores; a BDD-only run over all of src/sde takes about 50, because the BDD dry run cannot narrow to related tests and runs every spec (guides/TESTING.md, "How it works" under Mutation testing).

Run M changed one behaviour on the way: the database builder took its columns from the first 50 records of a file, so a field that first appeared later was dropped from the table. It then scanned every record (#609); the builder and that Rule were removed with the SQLite path after 12.2.0. Nothing else in the source changed except equivalent-mutant annotations; the scores rose because the tests and scenarios grew.

The BDD-only survivors, each left on purpose because unit tests pin it and no scenario has a reason to name it:

FileSurvivorsWhy no scenario kills it
providers/order.ts8compareIds on mixed number and string IDs (no SDE table mixes them), and the <= / >= / 0 forms that only differ for equal IDs, which keys never are
providers/yaml/SdeDataProvider.ts1The null skip when indexing a foreign key: no lookup asks for null
ingestion/transforms.ts1The array guard in extractLocale (an array has no en key); Run M's second survivor, in toSqliteValue, went with the SQLite path

The unit runs have no survivors in providers or ingestion; src/sde's one is in optionalPeers.ts.

Type mutation (job)ScoreKilled / validInvalidCandidatesFloor (config/mutation/type-thresholds.json)
./sde29.5%36/122201,90632 (below)
./sde/memory28.0%30/107111,60825

Type mutation rewrites the built dist/sde/*.d.ts (a field made optional, a union widened, a parameter's type loosened) and runs tsd per mutant, on a sample of the candidates each night. ./sde scored 29.5 against a floor of 32 that Run 8 seeded from a local sample; the nightly's sample is the one that counts, so the floor stays and the gap is the next tsd work, not a lowered ratchet. The core's entry points score between 4.1% (./schemas) and 30% (./errors) on the same night, so both SDE entry points sit at the top of the table.

The other tiers ​

TierWhat holds the SDEProven by
Properties and fuzzsde-provider-model.property.test.ts drives MemorySdeProvider and SdeDataProvider against a model of the port over generated universes; sde-transforms.property.test.ts holds the ingestion transforms to their invariants (field-name normalisation is idempotent and reversible, locale extraction always answers a string, the record and metadata transforms match a naive recursive model); sde-schema-fuzz.test.ts throws arbitrary and near-valid records at every domain schemaEach model-based property must fail against its registered known-bad implementations, checked on every run (tests/fuzz/support/property.ts)
Real data, nightlynightly-sde.yml at 05:15 UTC downloads CCP's current export (cached per build), runs the 114 real-data tests with SDE_REQUIRE_DATA=1, sde:drift (the file list against the registry, every record's keys against its Zod schema), the four examples/sde-*.ts, then the six sde/ benchmarks and the heap soak on the real exportKeeps one issue ("sde: nightly real-data run failed or the export drifted") open while any job is red; last green run 36404108879
Isolation and layersnpm run lint:layers: src/sde imports nothing of the package but src/core/ports, nothing outside it imports the SDE, and inside the module each layer imports only the layers listed for it in SDE_LAYERS (config/eslint/layers.rules.cjs): ingestion knows nothing of the providers, domain nothing of either; memory-entry-bundle.test.ts proves the shipped ./sde/memory carries no file-system, YAML, ZIP or SQLite codetests/tdd/layers/layers-lint.test.ts lints a negative fixture at each real path; the bundle test checks itself against a known string
Determinismnpm run lint:determinism: src/sde/clock.ts is the module's only reader of time, and the baseline lists no SDE exceptiontests/tdd/determinism-lint/
Benchmarks and heap soaktests/benchmark/sde.bench.ts: load, getType, getTypesByGroup cold and indexed, name search by fragment and by whole name, on a seeded 50,000-type export; sde-soak.ts answers 100,000 lookups per cycle for three cycles and requires the heap back within 8 MiB or 5% after each close()An injected slowdown and an injected leak must be flagged (tests/tdd/benchmark/sde-bench.test.ts); the tasks ride ci.yml's benchmarks job and nightly-benchmarks.yml
Package surfacenpm run size: ./sde 45.4 kB ESM / 47.9 kB CJS and ./sde/memory 20.7 kB / 22.5 kB budgets (.size-limit.cjs); npm run test:docs-examples type-checks every TypeScript block in guides/sde/, guides/SDE.md and src/sde/README.md against the packed package; npm run api-report tracks both entry points in etc/esi.ts.api.mdsize-limit fails over budget; the doc-examples tier has negative fixtures in tests/tdd/doc-examples/; api-report:semver fails on a lost line without a breaking commit
Mutation on every PRmutation-pr in ci.yml mutates the changed src/ files, SDE files included, plus any neighbour the nightly's incremental report does not cover, and refuses a pull request that lowers or removes a floor; the nightly's mutation:ratchet and mutation:bdd:ratchet hold the three SDE directories to the floors abovetests/mutation-fixture/ must leave survivors and kill at least one mutant

What is open ​

  • ./sde type mutation sits 2.5 points under its floor on the nightly sample. The floor does not move; the fix is more expectError assertions where the survivors cluster (reports/type-mutation/type-mutation.md from the run names them).
  • The Run M floors come from local runs. The next complete nightly re-measures all six and raises any floor that sits under its score; a nightly score under a floor fails the ratchet and is the next piece of work.
  • Two behaviours recorded by the programme and left for the maintainer, not the tests: an empty search fragment matches everything up to the limit, and a search limit below 1 answers one record (ROADMAP.md, Track S).

Every floor here is a one-way ratchet: the next complete nightly raises a floor to its score, a pull request cannot lower one, and a directory without a floor fails the ratchet.

Released under the GPL-3.0-or-later license.