Files
msd-core/tests/helpers/emitted-baseline.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

235 lines
10 KiB
JavaScript

'use strict';
/**
* Baseline acquisition for the differential attribution check (ADR-2719 §5, #2723).
*
* The baseline is the emitted-manifest set built at `next` HEAD. It is CACHED, not
* committed — committing it would recreate the derived-state-in-git problem this epic
* exists to delete, and would double the rate `next` advances.
*
* ── The load-bearing part ────────────────────────────────────────────────────
* ADR-2719 §5: "keyed on the `next` sha the PR was merged with. That key discipline is
* the one thing that has to be exactly right — a stale baseline silently mis-attributes."
*
* Note the asymmetry that makes staleness worse than absence: a MISSING baseline fails
* loudly and gets fixed. A STALE one produces a confident wrong answer — it attributes
* deltas to the wrong commit's state, so real ripples read as explained. Every path
* through this module therefore fails closed on a key mismatch.
*
* ── No bare `return` anywhere ────────────────────────────────────────────────
* ADR-2719 §6 calls this out explicitly: in node:test a bare `return` is a PASS, not a
* skip, so a baseline-unavailable path that returns would make the whole gate fail open
* with nothing in CI to say so. This module returns an explicit {ok:false} result and
* the caller asserts on it.
*
* IO is INJECTED (readJson / exists / buildFallback) so every failure mode above is
* unit-testable without touching the filesystem or spawning 19 installers.
*/
/**
* Operator pin: an explicit "use THIS baseline artifact" override.
*
* #2854: this was previously described as "the env var a CI cache-restore step points
* at", and CI did exactly that. That is what broke — a value arriving here is treated
* as intentional, so ANY rejection is a hard stop rather than a fall-through (see
* `resolveBaseline` below). Correct for a human who pinned a path on purpose; wrong
* for a cache restore, which is recoverable by definition.
*
* CI no longer publishes anything here. Its restore lands on `DEFAULT_CACHE_PATH`,
* which `resolveBaseline` reads anyway, so a stale, malformed, or wrong-version
* artifact degrades to the in-job build as ADR-2719 §5 specifies. Announcing the same
* file through this door could only ever convert recoverable into fatal.
*
* Keep it that way: anything that sets this variable is asserting "use THIS, or stop".
*/
const BASELINE_ENV = 'MSD_EMITTED_BASELINE';
/** Default on-disk cache location, relative to the repo root. */
const DEFAULT_CACHE_PATH = '.msd-cache/emitted-baseline.json';
/** Baseline artifact schema version — pinned so a format change fails loudly.
* v2 (#3547): the harness now measures every global family in the REAL
* config-home shape (<root>/<globalSuffix> instead of the collapsed
* <root>), so manifests from a v1 baseline — built with the collapsed
* harness — are not comparable (HOME-level paths present, prefix bytes
* absent). Bumping refuses v1 caches and forces the same-schema fallback
* build; the push-to-next job republishes at v2 when this merges. */
const BASELINE_VERSION = 2;
/**
* Validate a baseline artifact's shape and freshness.
*
* @param {*} doc parsed artifact
* @param {string} expectedSha the `next` sha this PR is being evaluated against
* @param {string} source where it came from (named in every error)
* @returns {{ok: true, baseline: object, sizeBaseline: object|null, sha: string}
* |{ok: false, errors: string[]}}
*/
function validateBaseline(doc, expectedSha, source) {
const errors = [];
if (doc === null || doc === undefined) {
return { ok: false, errors: [`${source}: baseline is absent`] };
}
if (typeof doc !== 'object' || Array.isArray(doc)) {
// Same class as Phase 2's fixture loader: a document that parses but is not an
// object must never be read as "no entries", which would pass vacuously.
return {
ok: false,
errors: [`${source}: baseline must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`],
};
}
if (doc.version !== undefined && doc.version !== BASELINE_VERSION) {
errors.push(`${source}: unsupported baseline version ${JSON.stringify(doc.version)} (expected ${BASELINE_VERSION})`);
}
if (typeof doc.sha !== 'string' || !/^[0-9a-f]{40}$/.test(doc.sha)) {
errors.push(`${source}: baseline "sha" must be a 40-hex commit sha, got ${JSON.stringify(doc.sha)}`);
} else if (typeof expectedSha === 'string' && expectedSha !== '' && doc.sha !== expectedSha) {
// THE staleness gate. Never silently used.
errors.push(
`${source}: STALE baseline — built at ${doc.sha} but this PR is being evaluated ` +
`against next@${expectedSha}. A stale baseline mis-attributes silently, so it is ` +
'refused rather than used. Rebuild it, or let the in-job fallback run.',
);
}
if (!doc.manifests || typeof doc.manifests !== 'object' || Array.isArray(doc.manifests)) {
errors.push(`${source}: baseline "manifests" must be an object keyed by runtime`);
}
if (errors.length) return { ok: false, errors };
return {
ok: true,
baseline: doc.manifests,
sizeBaseline: (doc.sizes && typeof doc.sizes === 'object' && !Array.isArray(doc.sizes))
? doc.sizes
: null,
sha: doc.sha,
};
}
/**
* Resolve the baseline through the documented precedence, reporting WHICH step supplied
* it so a failure message can say where the answer came from.
*
* 1. `MSD_EMITTED_BASELINE` — an operator pin; a mismatch here is a HARD STOP
* 2. the on-disk cache, validated against the expected sha — a mismatch RECOVERS
* 3. an in-job build at `origin/next` (slow fallback)
* 4. none → explicit failure (NEVER a silent pass)
*
* #2854: steps 1 and 2 differ only in what a mismatch means, so which door a given
* artifact arrives through decides whether the run recovers or dies. CI publishes to
* step 1 only after validating, so an unvalidated restore lands on step 2 and degrades.
*
* @param {object} opts
* @param {string} opts.expectedSha `next` sha under test
* @param {object} [opts.env] environment (injected)
* @param {string} [opts.cachePath]
* @param {function} opts.readJson (path) => parsed | null (null when absent)
* @param {function} [opts.buildFallback] () => artifact | null (the slow path)
* @returns {{ok: true, via: string, attempted: string[], baseline: object,
* sizeBaseline: object|null, sha: string}
* |{ok: false, via: string, attempted: string[], errors: string[]}}
* `attempted` lists the sources actually REACHED, in order — callers render it
* instead of assuming all three ran.
*/
function resolveBaseline({
expectedSha,
env = process.env,
cachePath = DEFAULT_CACHE_PATH,
readJson,
buildFallback = null,
} = {}) {
if (typeof readJson !== 'function') {
return {
ok: false, via: 'none', attempted: [],
errors: ['resolveBaseline: readJson must be supplied'],
};
}
const attempts = [];
// #2854: the sources actually REACHED, in order. The caller renders this in its
// failure message; hardcoding "tried env, cache, and an in-job build" there claimed
// three attempts on every early return, including ones that reached only the first.
const attempted = [];
const envPath = env && env[BASELINE_ENV];
if (envPath) {
attempted.push(`env:${BASELINE_ENV}`);
let doc = null;
let readError = null;
try {
doc = readJson(envPath);
} catch (err) {
readError = `${BASELINE_ENV}=${envPath}: ${err.message}`;
}
if (readError) {
attempts.push(readError);
} else {
const v = validateBaseline(doc, expectedSha, `${BASELINE_ENV}=${envPath}`);
if (v.ok) return { ok: true, via: `env:${BASELINE_ENV}`, attempted, ...v };
attempts.push(...v.errors);
// An EXPLICITLY pointed-at baseline that is stale or malformed is a hard stop, not
// a reason to quietly fall through to a different one — the operator said "use this".
//
// #2854: this is a guarantee about a HAND-SET path, and it is why CI must not
// publish its cache restore here. It used to, so a restore that was merely stale
// or malformed died here instead of degrading one branch below.
return { ok: false, via: `env:${BASELINE_ENV}`, attempted, errors: attempts };
}
}
attempted.push(`cache:${cachePath}`);
let cacheDoc = null;
let cacheErr = null;
try {
cacheDoc = readJson(cachePath);
} catch (err) {
cacheErr = `${cachePath}: ${err.message}`;
}
if (cacheErr) {
attempts.push(cacheErr);
} else if (cacheDoc !== null && cacheDoc !== undefined) {
const v = validateBaseline(cacheDoc, expectedSha, cachePath);
if (v.ok) return { ok: true, via: `cache:${cachePath}`, attempted, ...v };
// A stale CACHE is recoverable: fall through to the build fallback, but keep the
// reason so the final message explains why the slow path ran.
attempts.push(...v.errors);
} else {
attempts.push(`${cachePath}: absent`);
}
if (typeof buildFallback === 'function') {
attempted.push('build');
let built = null;
try {
built = buildFallback();
} catch (err) {
attempts.push(`in-job build at origin/next failed: ${err.message}`);
return { ok: false, via: 'build', attempted, errors: attempts };
}
const v = validateBaseline(built, expectedSha, 'in-job build at origin/next');
if (v.ok) return { ok: true, via: 'build', attempted, ...v };
attempts.push(...v.errors);
return { ok: false, via: 'build', attempted, errors: attempts };
}
attempts.push(
'no baseline available and no in-job build fallback was supplied. This is a hard ' +
'failure on purpose: a skipped propagation gate is worth less than a slow one ' +
'(ADR-2719 §6), and in node:test a bare `return` is a PASS, not a skip.',
);
return { ok: false, via: 'none', attempted, errors: attempts };
}
module.exports = {
BASELINE_ENV,
BASELINE_VERSION,
DEFAULT_CACHE_PATH,
validateBaseline,
resolveBaseline,
};