* feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1) Make the capability manifest versioned — the data substrate the Capability Ecosystem (ADR-1244) keys off: - capability.json gains a REQUIRED semver `version` plus the optional ecosystem envelope (`engines.gsd`, `compatVersions`, `integrity`, `provenance`); the build-time conformance validator enforces them via a new `validateVersionEnvelope()` (exported for the Phase 2 runtime overlay). - All 32 native capabilities stamped with `version` (= package version, lockstep) + `engines.gsd`; `sync-manifest-versions.cjs` gains a glob sweep that keeps them in sync, and the issue-844 regression guard is extended. - Strict SemVer 2.0.0 grammar blocks metacharacter/space/unicode smuggling in version strings; range/integrity fields are shape-validated (satisfaction and the load-time gate are deferred to Phase 2/4). - Capability rel-paths emitted forward-slash for cross-platform git correctness. Closes #1430 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1430): add changeset for versioned capability manifest Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
295
tests/capability-manifest-version.test.cjs
Normal file
295
tests/capability-manifest-version.test.cjs
Normal file
@@ -0,0 +1,295 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Phase 1 (ADR-1244 / issue #1430): versioned capability manifest.
|
||||
*
|
||||
* The build-time validator in scripts/gen-capability-registry.cjs must:
|
||||
* - REQUIRE a semver `version` on every capability (the registry rejects a
|
||||
* manifest without one — ADR-1244 D1).
|
||||
* - Shape-validate the optional ecosystem envelope fields `engines`,
|
||||
* `compatVersions`, `integrity`, `provenance` when present.
|
||||
*
|
||||
* Every native capabilities/<id>/capability.json must carry a valid `version`
|
||||
* and `engines.gsd` (the conformance / parity requirement: the build fails when
|
||||
* a native manifest lacks a version).
|
||||
*
|
||||
* These are behavioral tests against the exported validator + generator
|
||||
* pipeline — no source-grep. They mirror the harness in
|
||||
* tests/capability-registry.test.cjs (makeTempCapDir + loadAndValidate +
|
||||
* buildRegistry).
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const helpers = require(path.join(__dirname, 'helpers.cjs'));
|
||||
const {
|
||||
validateCapability,
|
||||
loadAndValidate,
|
||||
buildRegistry,
|
||||
SEMVER_RE,
|
||||
} = require(path.join(ROOT, 'scripts', 'gen-capability-registry.cjs'));
|
||||
|
||||
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
|
||||
const CAPABILITIES_DIR = path.join(ROOT, 'capabilities');
|
||||
|
||||
// Single source of truth: the validator's own strict-semver regex.
|
||||
const SEMVER = SEMVER_RE;
|
||||
|
||||
// ─── Minimal, otherwise-valid fixtures ───────────────────────────────────────
|
||||
|
||||
function featureCap(overrides) {
|
||||
return {
|
||||
id: 'demo',
|
||||
role: 'feature',
|
||||
version: '1.2.3',
|
||||
title: 'Demo',
|
||||
description: 'A demo capability.',
|
||||
tier: 'standard',
|
||||
requires: [],
|
||||
engines: { gsd: '>=1.6.0' },
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [],
|
||||
agents: [],
|
||||
hooks: [],
|
||||
config: {},
|
||||
steps: [],
|
||||
contributions: [],
|
||||
gates: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeCap(overrides) {
|
||||
return {
|
||||
id: 'demo-rt',
|
||||
role: 'runtime',
|
||||
version: '1.2.3',
|
||||
title: 'Demo RT',
|
||||
description: 'A demo runtime.',
|
||||
tier: 'standard',
|
||||
requires: [],
|
||||
engines: { gsd: '>=1.6.0' },
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.demo', env: [] },
|
||||
configFormat: 'settings-json',
|
||||
artifactLayout: { global: [], local: [] },
|
||||
commandStyle: 'slash-hyphen',
|
||||
hooksSurface: 'settings-json',
|
||||
sandboxTier: 'none',
|
||||
supportTier: 2,
|
||||
installSurface: 'settings-json',
|
||||
writesSharedSettings: false,
|
||||
permissionWriter: null,
|
||||
extendedHookEvents: [],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeTempCapDir(capabilities) {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-ver-test-'));
|
||||
for (const [id, cap] of Object.entries(capabilities)) {
|
||||
const subDir = path.join(tmpDir, id);
|
||||
fs.mkdirSync(subDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(subDir, 'capability.json'), JSON.stringify(cap), 'utf8');
|
||||
}
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
// ─── Sanity: the base fixtures are valid as-is ───────────────────────────────
|
||||
|
||||
describe('version envelope — base fixtures are valid', () => {
|
||||
test('a feature cap with a valid version passes validation', () => {
|
||||
assert.deepEqual(validateCapability(featureCap(), 'demo'), []);
|
||||
});
|
||||
test('a runtime cap with a valid version passes validation', () => {
|
||||
assert.deepEqual(validateCapability(runtimeCap(), 'demo-rt'), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── version is required + semver ────────────────────────────────────────────
|
||||
|
||||
describe('version is required and must be semver', () => {
|
||||
test('missing version is rejected (feature)', () => {
|
||||
const { version: _v, ...cap } = featureCap();
|
||||
const errors = validateCapability(cap, 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
|
||||
test('missing version is rejected (runtime)', () => {
|
||||
const { version: _v, ...cap } = runtimeCap();
|
||||
const errors = validateCapability(cap, 'demo-rt');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
|
||||
test('empty-string version is rejected', () => {
|
||||
const errors = validateCapability(featureCap({ version: '' }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')));
|
||||
});
|
||||
|
||||
test('whitespace-only version is rejected', () => {
|
||||
const errors = validateCapability(featureCap({ version: ' ' }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')));
|
||||
});
|
||||
|
||||
test('non-semver versions are rejected', () => {
|
||||
for (const bad of ['1.0', 'v1.0.0', '1.0.0.0', '1', 'latest', '1.x', '1.0.0 ', '01.2.3']) {
|
||||
const errors = validateCapability(featureCap({ version: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected "${bad}" to be rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('malformed/hostile prerelease & build identifiers are rejected (strict semver)', () => {
|
||||
// The prerelease/build suffix must be dot-separated [0-9A-Za-z-] identifiers
|
||||
// with no leading-zero numerics and no empty segments — so a version can
|
||||
// never smuggle shell metacharacters, spaces or unicode downstream.
|
||||
for (const bad of ['1.2.3-01', '1.2.3-..', '1.2.3-', '1.2.3+', '1.2.3-foo bar', '1.2.3-$(whoami)', '1.2.3-`id`', '1.2.3-😈', '1.2.3+build meta']) {
|
||||
const errors = validateCapability(featureCap({ version: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected hostile suffix "${bad}" to be rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('non-string version is rejected', () => {
|
||||
for (const bad of [123, null, {}, ['1.0.0']]) {
|
||||
const errors = validateCapability(featureCap({ version: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected ${JSON.stringify(bad)} to be rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('valid semver versions (incl. prerelease/build) pass', () => {
|
||||
for (const ok of ['1.0.0', '0.0.1', '10.20.30', '1.2.3-dev.0', '1.2.3-rc.1', '1.2.3+build.5', PKG_VERSION]) {
|
||||
const errors = validateCapability(featureCap({ version: ok }), 'demo');
|
||||
assert.deepEqual(errors, [], `expected "${ok}" to pass, got: ${JSON.stringify(errors)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('hostile version strings are rejected (shell metachars, newline, unicode)', () => {
|
||||
for (const bad of ['1.0.0; rm -rf /', '1.0.0\n2.0.0', '1.0.0$(whoami)', '१.२.३', '1.0.0`id`']) {
|
||||
const errors = validateCapability(featureCap({ version: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected hostile "${bad}" to be rejected`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── engines (optional; shape-validated) ─────────────────────────────────────
|
||||
|
||||
describe('engines is optional but shape-validated when present', () => {
|
||||
test('omitting engines is valid', () => {
|
||||
const { engines: _e, ...cap } = featureCap();
|
||||
assert.deepEqual(validateCapability(cap, 'demo'), []);
|
||||
});
|
||||
|
||||
test('engines must be an object', () => {
|
||||
for (const bad of ['>=1.6.0', 123, ['gsd'], null]) {
|
||||
const errors = validateCapability(featureCap({ engines: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('engines')), `expected engines=${JSON.stringify(bad)} rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('engines.gsd must be a non-empty range string', () => {
|
||||
for (const bad of ['', ' ', 123, {}, 'not a range!!', '>=1.0.0; rm -rf', 'abcx', '()x']) {
|
||||
const errors = validateCapability(featureCap({ engines: { gsd: bad } }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('engines')), `expected engines.gsd=${JSON.stringify(bad)} rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('valid engines.gsd ranges pass', () => {
|
||||
for (const ok of ['>=1.6.0', '>=1.6.0 <3.0.0', '^1.0.0', '~1.2.0', '1.x', '*', '>=1.6.0 || >=2.0.0']) {
|
||||
const errors = validateCapability(featureCap({ engines: { gsd: ok } }), 'demo');
|
||||
assert.deepEqual(errors, [], `expected range "${ok}" to pass, got: ${JSON.stringify(errors)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── compatVersions / integrity / provenance (optional; shape-validated) ──────
|
||||
|
||||
describe('optional ecosystem envelope fields are shape-validated', () => {
|
||||
test('compatVersions must be an object of semver→range strings', () => {
|
||||
assert.deepEqual(validateCapability(featureCap({ compatVersions: { '1.0.0': '>=1.6.0' } }), 'demo'), []);
|
||||
for (const bad of ['x', 123, { '1.0.0': 5 }, { 'not-semver': '>=1.6.0' }]) {
|
||||
const errors = validateCapability(featureCap({ compatVersions: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('compatVersions')), `expected compatVersions=${JSON.stringify(bad)} rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('integrity must be sha512-<base64>', () => {
|
||||
const good = 'sha512-' + 'a'.repeat(86) + '==';
|
||||
assert.deepEqual(validateCapability(featureCap({ integrity: good }), 'demo'), []);
|
||||
for (const bad of ['abc', 'sha256-deadbeef', 'sha512-', 'sha512-abc', 'sha512-' + 'a'.repeat(40) + '==', 123, 'sha512-not base64!!']) {
|
||||
const errors = validateCapability(featureCap({ integrity: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('integrity')), `expected integrity=${JSON.stringify(bad)} rejected`);
|
||||
}
|
||||
});
|
||||
|
||||
test('provenance must be { sourceRepo, commit } strings', () => {
|
||||
assert.deepEqual(validateCapability(featureCap({ provenance: { sourceRepo: 'https://x/y', commit: 'abc123' } }), 'demo'), []);
|
||||
for (const bad of ['x', 123, { sourceRepo: 5, commit: 'c' }, { sourceRepo: 'r' }, { commit: 'c' }]) {
|
||||
const errors = validateCapability(featureCap({ provenance: bad }), 'demo');
|
||||
assert.ok(errors.some((e) => e.includes('provenance')), `expected provenance=${JSON.stringify(bad)} rejected`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Registry pass-through ────────────────────────────────────────────────────
|
||||
|
||||
describe('generated registry preserves version + engines', () => {
|
||||
test('buildRegistry carries version and engines onto the capability object', (t) => {
|
||||
const capDir = makeTempCapDir({ demo: featureCap({ id: 'demo', version: '2.5.0', engines: { gsd: '>=1.6.0 <2.0.0' } }) });
|
||||
t.after(() => helpers.cleanup(capDir));
|
||||
|
||||
const { capMap, errors } = loadAndValidate(new Set(), capDir);
|
||||
assert.deepEqual(errors, [], `loadAndValidate errors: ${JSON.stringify(errors)}`);
|
||||
const registry = buildRegistry(capMap);
|
||||
assert.equal(registry.capabilities.demo.version, '2.5.0');
|
||||
assert.equal(registry.capabilities.demo.engines.gsd, '>=1.6.0 <2.0.0');
|
||||
});
|
||||
|
||||
test('loadAndValidate rejects a capability dir whose manifest lacks a version', (t) => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-ver-noversion-'));
|
||||
t.after(() => helpers.cleanup(tmpDir));
|
||||
const sub = path.join(tmpDir, 'demo');
|
||||
fs.mkdirSync(sub, { recursive: true });
|
||||
const { version: _v, ...noVersion } = featureCap({ id: 'demo' });
|
||||
fs.writeFileSync(path.join(sub, 'capability.json'), JSON.stringify(noVersion), 'utf8');
|
||||
|
||||
const { errors } = loadAndValidate(new Set(), tmpDir);
|
||||
assert.ok(errors.some((e) => e.includes('version')), `expected a version error, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Native manifest conformance (the ADR-1244 parity requirement) ────────────
|
||||
|
||||
describe('every native capability.json carries a valid version + engines.gsd', () => {
|
||||
const ids = fs
|
||||
.readdirSync(CAPABILITIES_DIR, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
test('there are native capabilities to check', () => {
|
||||
assert.ok(ids.length >= 30, `expected the full native capability set, found ${ids.length}`);
|
||||
});
|
||||
|
||||
for (const id of ids) {
|
||||
test(`capabilities/${id}/capability.json has a semver version`, () => {
|
||||
const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8'));
|
||||
assert.equal(typeof cap.version, 'string', `${id}: version must be a string`);
|
||||
assert.ok(SEMVER.test(cap.version), `${id}: version "${cap.version}" must be semver`);
|
||||
});
|
||||
|
||||
test(`capabilities/${id}/capability.json declares engines.gsd`, () => {
|
||||
const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8'));
|
||||
assert.ok(cap.engines && typeof cap.engines.gsd === 'string' && cap.engines.gsd.length > 0,
|
||||
`${id}: engines.gsd must be a non-empty string`);
|
||||
});
|
||||
|
||||
test(`capabilities/${id}/capability.json passes validateCapability`, () => {
|
||||
const cap = JSON.parse(fs.readFileSync(path.join(CAPABILITIES_DIR, id, 'capability.json'), 'utf8'));
|
||||
assert.deepEqual(validateCapability(cap, id), [], `${id}: native manifest must validate`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1470,6 +1470,7 @@ describe('S1: fragment.path traversal guard', () => {
|
||||
JSON.stringify({
|
||||
id: 'planning-advice',
|
||||
role: 'feature',
|
||||
version: '1.0.0',
|
||||
title: 'Planning advice',
|
||||
description: 'Synthetic fixture for fragment path materialization.',
|
||||
tier: 'full',
|
||||
@@ -1515,6 +1516,7 @@ describe('S1: fragment.path traversal guard', () => {
|
||||
JSON.stringify({
|
||||
id: 'research',
|
||||
role: 'feature',
|
||||
version: '1.0.0',
|
||||
title: 'Research',
|
||||
description: 'Synthetic fixture for step fragment materialization.',
|
||||
tier: 'standard',
|
||||
@@ -1702,7 +1704,7 @@ describe('C3: role:runtime body validation', () => {
|
||||
// configHome is now an object (Decision 1), artifactLayout is { global, local } (Decision 3),
|
||||
// commandStyle is closed enum (Decision 4), hooksSurface is closed enum (Decision 5).
|
||||
const VALID_RUNTIME_CAP = {
|
||||
id: 'cursor', role: 'runtime', title: 'Cursor', description: 'Cursor IDE runtime',
|
||||
id: 'cursor', role: 'runtime', version: '1.0.0', title: 'Cursor', description: 'Cursor IDE runtime',
|
||||
tier: 'standard', requires: [],
|
||||
runtime: {
|
||||
configHome: { kind: 'dot-home', name: '.cursor', env: ['CURSOR_CONFIG_DIR'] },
|
||||
@@ -2886,6 +2888,7 @@ function makeCommandCap(id, commands) {
|
||||
return {
|
||||
id,
|
||||
role: 'feature',
|
||||
version: '1.0.0',
|
||||
title: 'Test cap ' + id,
|
||||
description: 'Synthetic capability for ADR-959 command tests.',
|
||||
tier: 'full',
|
||||
@@ -3085,6 +3088,7 @@ function makeRuntimeCap(overrides) {
|
||||
return {
|
||||
id: 'test-rt',
|
||||
role: 'runtime',
|
||||
version: '1.0.0',
|
||||
title: 'Test Runtime',
|
||||
description: 'A synthetic runtime capability for testing.',
|
||||
tier: 'core',
|
||||
|
||||
@@ -27,6 +27,8 @@ const {
|
||||
syncManifestVersions,
|
||||
getPackageVersion,
|
||||
stageManifests,
|
||||
listCapabilityManifests,
|
||||
syncCapabilityVersions,
|
||||
} = require(path.join(ROOT, 'scripts', 'sync-manifest-versions.cjs'));
|
||||
|
||||
// ─── A: RED→GREEN repro via temp fixture ─────────────────────────────────────
|
||||
@@ -172,12 +174,91 @@ describe('B: real manifests match package.json version', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ─── B2: native capability manifests track package.json version (ADR-1244 D6) ─
|
||||
describe('B2: native capability manifests match package.json version', () => {
|
||||
|
||||
const pkgVersion = getPackageVersion(ROOT);
|
||||
const capManifests = listCapabilityManifests({ root: ROOT });
|
||||
|
||||
test('there is at least one native capability manifest', () => {
|
||||
assert.ok(capManifests.length >= 30, `expected the native capability set, found ${capManifests.length}`);
|
||||
});
|
||||
|
||||
for (const rel of capManifests) {
|
||||
test(`${rel} version === ${pkgVersion}`, () => {
|
||||
const m = JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8'));
|
||||
assert.equal(
|
||||
m.version,
|
||||
pkgVersion,
|
||||
`${rel} version (${m.version}) must match package.json version (${pkgVersion}). ` +
|
||||
'Run `node scripts/sync-manifest-versions.cjs` to fix.'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── B3: syncCapabilityVersions stamps + is idempotent (temp fixture) ─────────
|
||||
describe('B3: syncCapabilityVersions — temp fixture', () => {
|
||||
|
||||
test('stamps stale capability manifests to package version, then is idempotent', () => {
|
||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-844-cap-'));
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'x', version: '9.9.9-test.0' }, null, 2) + '\n'
|
||||
);
|
||||
// Two stale capability manifests.
|
||||
for (const id of ['alpha', 'beta']) {
|
||||
const dir = path.join(tmpRoot, 'capabilities', id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'capability.json'),
|
||||
JSON.stringify({ id, role: 'feature', version: '0.0.0', title: id }, null, 2) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
const found = listCapabilityManifests({ root: tmpRoot });
|
||||
assert.equal(found.length, 2, 'should discover both capability manifests');
|
||||
|
||||
const changed = syncCapabilityVersions({ root: tmpRoot });
|
||||
assert.equal(changed.length, 2, 'both manifests should be stamped on first run');
|
||||
for (const rel of found) {
|
||||
const m = JSON.parse(fs.readFileSync(path.join(tmpRoot, rel), 'utf8'));
|
||||
assert.equal(m.version, '9.9.9-test.0', `${rel} should be stamped`);
|
||||
assert.equal(m.title, m.id, `${rel} non-version fields preserved`);
|
||||
}
|
||||
|
||||
// Idempotent second run.
|
||||
assert.deepEqual(syncCapabilityVersions({ root: tmpRoot }), [], 'second run is a no-op');
|
||||
} finally {
|
||||
helpers.cleanup(tmpRoot);
|
||||
}
|
||||
});
|
||||
|
||||
test('listCapabilityManifests returns [] when there is no capabilities/ dir', () => {
|
||||
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-844-nocaps-'));
|
||||
try {
|
||||
assert.deepEqual(listCapabilityManifests({ root: tmpRoot }), []);
|
||||
} finally {
|
||||
helpers.cleanup(tmpRoot);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── C: Regression guard — all version-bearing JSON files are registered ──────
|
||||
describe('C: regression guard — version-bearing JSON files must be registered', () => {
|
||||
|
||||
// package.json is the version source; package-lock.json is npm-managed.
|
||||
// Both inherently track the version without the sync script.
|
||||
const ALLOWED = new Set([...VERSIONED_MANIFESTS, 'package.json', 'package-lock.json']);
|
||||
// Native capability manifests (capabilities/<id>/capability.json) are
|
||||
// version-swept by syncCapabilityVersions (ADR-1244 D6) — discovered by glob,
|
||||
// so every one is "registered" without an explicit entry here.
|
||||
const ALLOWED = new Set([
|
||||
...VERSIONED_MANIFESTS,
|
||||
...listCapabilityManifests({ root: ROOT }),
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
]);
|
||||
|
||||
// Semver-ish: matches X.Y.Z with optional pre-release/build metadata.
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:[-+].+)?$/;
|
||||
|
||||
Reference in New Issue
Block a user