Some checks failed
Tests / PR mergeability (push) Successful in 1m37s
Tests / Base branch health (push) Successful in 11s
Tests / Detect test scope (push) Successful in 17s
Tests / lint-tests (push) Failing after 2m16s
Tests / plugin-validate (push) Successful in 1m6s
Tests / test (ubuntu-latest, 24, shard 1/3) (push) Failing after 25s
Tests / test (ubuntu-latest, 24, shard 2/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24, shard 3/3) (push) Failing after 20s
Tests / test (ubuntu-latest, 24) (push) Failing after 19s
Tests / test (inert CI) (push) Has been skipped
Tests / QA loop walk (smell ratchet) (push) Failing after 19s
Tests / Coverage gate (merged shards) (push) Has been skipped
Tests / Publish emitted-baseline artifact (push) Has been skipped
Dismiss Unauthorized PR Approvals / dismiss-unauthorized-approval (push) Successful in 9s
Close Draft PRs (sweep) / Sweep open draft PRs (push) Successful in 8s
Tests / conformance test (macos-latest, 24) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 1/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 2/3) (push) Has been cancelled
Tests / conformance test (windows-latest, 24, shard 3/3) (push) Has been cancelled
Tests / Required tests (push) Has been cancelled
The fork lives on the golem15 Gitea forge, not GitHub. Package identity now parses either host and derives in-place raw URLs for Gitea; the identity-drift lint accepts the new host; README drops GitHub-only badges and the npm quickstart in favour of the checkout installer.
142 lines
5.3 KiB
JavaScript
142 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* Drift-guard lint for the Package Identity seam (issue #498).
|
|
*
|
|
* The seam (`get-shit-done/bin/lib/package-identity.cjs`, derived from // msd-allow-legacy-name
|
|
* package.json) is the single source of MSD's published coordinates. Many
|
|
* runtime surfaces still carry a literal copy of those coordinates because
|
|
* they cannot `require()` the seam at runtime: the bash launcher snippet (and
|
|
* its byte-equal copies across ~85 workflows, kept in lockstep by the
|
|
* runtime-launcher parity test) and the installer's user-facing install/help
|
|
* strings.
|
|
*
|
|
* This lint makes those literals *value-checked*: every MSD package/repo
|
|
* coordinate that appears as a literal must equal the seam's current value.
|
|
* It passes today (the literals are correct) and FAILS the moment a repoint is
|
|
* not propagated — rename package.json, regenerate the seam, and every stale
|
|
* literal is reported until updated. That is what turns a repoint into a
|
|
* one-line change with mechanical enforcement.
|
|
*
|
|
* Scope: the runtime/code surface (bin/, hooks/, scripts/, get-shit-done/). // msd-allow-legacy-name
|
|
* Pure-prose docs and localized READMEs are intentionally out of scope.
|
|
*/
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
// A MSD package coordinate: a scoped npm name whose package part contains
|
|
// "get-shit-done" (so @opengsd/gsd-sdk and unrelated scopes never match). // msd-allow-legacy-name
|
|
const PACKAGE_RE = /@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*/g; // msd-allow-legacy-name
|
|
// A MSD repo slug, only inside a GitHub URL context so it never overlaps the
|
|
// scoped package literal above. The `.git` suffix is trimmed before compare.
|
|
const SLUG_RE = /(?:github\.com[/:]|git\.golem15\.com[/:]|raw\.githubusercontent\.com\/)([A-Za-z0-9._-]+\/[A-Za-z0-9._-]*get-shit-done[A-Za-z0-9._-]*)/g; // msd-allow-legacy-name
|
|
|
|
function lineOf(text, index) {
|
|
let line = 1;
|
|
for (let i = 0; i < index && i < text.length; i++) {
|
|
if (text[i] === '\n') line++;
|
|
}
|
|
return line;
|
|
}
|
|
|
|
/**
|
|
* Pure: find every MSD coordinate literal in `text` that does not match the
|
|
* expected seam values. Returns [{ kind, found, expected, line }].
|
|
*/
|
|
function findCoordinateDrift(text, { packageName, repoSlug }) {
|
|
const out = [];
|
|
for (const m of text.matchAll(PACKAGE_RE)) {
|
|
if (m[0] !== packageName) {
|
|
out.push({ kind: 'package', found: m[0], expected: packageName, line: lineOf(text, m.index) });
|
|
}
|
|
}
|
|
for (const m of text.matchAll(SLUG_RE)) {
|
|
const slug = m[1].replace(/\.git$/, '');
|
|
if (slug !== repoSlug) {
|
|
out.push({ kind: 'slug', found: slug, expected: repoSlug, line: lineOf(text, m.index) });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Directories scanned, relative to repo root.
|
|
const SCAN_DIRS = ['bin', 'hooks', 'scripts', 'msd-core'];
|
|
const SCAN_EXT = new Set(['.js', '.cjs', '.sh', '.md']);
|
|
// Files exempt because they ARE the source of truth / the tooling that defines
|
|
// the coordinate patterns. The generated seam holds the correct value by
|
|
// construction; the generator and this lint carry regex/templates, not stray
|
|
// literals.
|
|
const EXEMPT = new Set([
|
|
path.join('msd-core', 'bin', 'lib', 'package-identity.cjs'),
|
|
path.join('scripts', 'generate-package-identity.cjs'),
|
|
path.join('scripts', 'lint-package-identity-drift.cjs'),
|
|
]);
|
|
|
|
function walk(dir, acc) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
} catch (e) {
|
|
return acc;
|
|
}
|
|
for (const entry of entries) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') continue;
|
|
walk(full, acc);
|
|
} else if (entry.isFile() && SCAN_EXT.has(path.extname(entry.name))) {
|
|
acc.push(full);
|
|
}
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
/**
|
|
* Scan the repo's runtime/code surface and return all coordinate drift, each
|
|
* annotated with the repo-relative file path.
|
|
*/
|
|
function scanRepo(root) {
|
|
const seam = require(path.join(root, 'msd-core', 'bin', 'lib', 'package-identity.cjs'));
|
|
const expected = { packageName: seam.packageName, repoSlug: seam.repoSlug };
|
|
const violations = [];
|
|
for (const dir of SCAN_DIRS) {
|
|
const files = walk(path.join(root, dir), []);
|
|
for (const file of files) {
|
|
const rel = path.relative(root, file);
|
|
if (EXEMPT.has(rel)) continue;
|
|
let text;
|
|
try {
|
|
text = fs.readFileSync(file, 'utf8');
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
for (const d of findCoordinateDrift(text, expected)) {
|
|
violations.push({ file: rel, ...d });
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
function main() {
|
|
const root = path.join(__dirname, '..');
|
|
const violations = scanRepo(root);
|
|
if (violations.length === 0) {
|
|
process.stdout.write('ok identity-drift: all MSD coordinate literals match the seam\n');
|
|
return;
|
|
}
|
|
process.stderr.write('identity-drift: stale MSD coordinate literal(s) found.\n');
|
|
process.stderr.write('Repoint by editing package.json, then `node scripts/generate-package-identity.cjs`,\n');
|
|
process.stderr.write('and update the value-checked materialization sites below:\n');
|
|
for (const d of violations) {
|
|
process.stderr.write(` ${d.file}:${d.line} ${d.kind} '${d.found}' != '${d.expected}'\n`);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
|
|
module.exports = { findCoordinateDrift, scanRepo };
|