This commit is contained in:
+236
-16
@@ -288,6 +288,100 @@ function rejectDuplicateIds(values, label, failures) {
|
||||
}
|
||||
}
|
||||
|
||||
function requireString(value, label, failures, { allowEmpty = false } = {}) {
|
||||
if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
|
||||
failures.push(`${label}: expected ${allowEmpty ? 'a string' : 'a non-empty string'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateStringArray(value, label, failures, { allowEmpty = false } = {}) {
|
||||
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
||||
failures.push(`${label}: expected ${allowEmpty ? 'an array' : 'a non-empty array'}`);
|
||||
return;
|
||||
}
|
||||
if (value.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
|
||||
failures.push(`${label}: entries must be non-empty strings`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateOutcome(value, label, failures) {
|
||||
rejectUnknownKeys(
|
||||
value,
|
||||
new Set(['exitCode', 'outputPattern', 'notOutputPattern']),
|
||||
label,
|
||||
failures,
|
||||
);
|
||||
if (typeof value?.exitCode !== 'number' || !Number.isInteger(value.exitCode)) {
|
||||
failures.push(`${label}.exitCode: expected an integer`);
|
||||
}
|
||||
for (const key of ['outputPattern', 'notOutputPattern']) {
|
||||
if (value?.[key] !== undefined && typeof value[key] !== 'string') {
|
||||
failures.push(`${label}.${key}: expected a string`);
|
||||
} else if (typeof value?.[key] === 'string' && value[key].trim().length === 0) {
|
||||
failures.push(`${label}.${key}: expected a non-empty pattern`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateFixture(fixture, label, failures) {
|
||||
if (fixture === undefined) return;
|
||||
rejectUnknownKeys(
|
||||
fixture,
|
||||
new Set(['writeFiles', 'replaceFiles', 'removePaths', 'copyPaths']),
|
||||
label,
|
||||
failures,
|
||||
);
|
||||
const writeFiles = Array.isArray(fixture?.writeFiles) ? fixture.writeFiles : [];
|
||||
const replaceFiles = Array.isArray(fixture?.replaceFiles) ? fixture.replaceFiles : [];
|
||||
for (const [index, entry] of writeFiles.entries()) {
|
||||
rejectUnknownKeys(
|
||||
entry,
|
||||
new Set(['path', 'content', 'mode']),
|
||||
`${label}.writeFiles[${index}]`,
|
||||
failures,
|
||||
);
|
||||
requireString(entry?.path, `${label}.writeFiles[${index}].path`, failures);
|
||||
if (typeof entry?.content !== 'string')
|
||||
failures.push(`${label}.writeFiles[${index}].content: expected a string`);
|
||||
if (entry?.mode !== undefined && (!Number.isInteger(entry.mode) || entry.mode < 0)) {
|
||||
failures.push(`${label}.writeFiles[${index}].mode: expected a non-negative integer`);
|
||||
}
|
||||
}
|
||||
for (const [index, entry] of replaceFiles.entries()) {
|
||||
rejectUnknownKeys(
|
||||
entry,
|
||||
new Set(['path', 'find', 'replace']),
|
||||
`${label}.replaceFiles[${index}]`,
|
||||
failures,
|
||||
);
|
||||
requireString(entry?.path, `${label}.replaceFiles[${index}].path`, failures);
|
||||
if (typeof entry?.find !== 'string')
|
||||
failures.push(`${label}.replaceFiles[${index}].find: expected a string`);
|
||||
if (typeof entry?.replace !== 'string')
|
||||
failures.push(`${label}.replaceFiles[${index}].replace: expected a string`);
|
||||
}
|
||||
for (const key of ['removePaths', 'copyPaths']) {
|
||||
if (fixture?.[key] !== undefined)
|
||||
validateStringArray(fixture[key], `${label}.${key}`, failures, { allowEmpty: true });
|
||||
}
|
||||
for (const key of ['writeFiles', 'replaceFiles']) {
|
||||
if (fixture?.[key] !== undefined && !Array.isArray(fixture[key])) {
|
||||
failures.push(`${label}.${key}: expected an array`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnvironment(environment, label, failures) {
|
||||
if (environment === undefined) return;
|
||||
if (!environment || typeof environment !== 'object' || Array.isArray(environment)) {
|
||||
failures.push(`${label}: expected an object`);
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(environment)) {
|
||||
if (!key || typeof value !== 'string') failures.push(`${label}.${key}: expected a string`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateClosedSchema(manifest, failures) {
|
||||
if (manifest.schemaVersion !== 1)
|
||||
failures.push(`unsupported schemaVersion ${String(manifest.schemaVersion)}`);
|
||||
@@ -295,7 +389,6 @@ function validateClosedSchema(manifest, failures) {
|
||||
manifest,
|
||||
new Set([
|
||||
'schemaVersion',
|
||||
'activationCommit',
|
||||
'gateRoots',
|
||||
'governingClaimFiles',
|
||||
'coverageBoundary',
|
||||
@@ -308,6 +401,43 @@ function validateClosedSchema(manifest, failures) {
|
||||
'manifest',
|
||||
failures,
|
||||
);
|
||||
validateStringArray(manifest.gateRoots, 'manifest.gateRoots', failures);
|
||||
validateStringArray(manifest.governingClaimFiles, 'manifest.governingClaimFiles', failures, {
|
||||
allowEmpty: true,
|
||||
});
|
||||
rejectUnknownKeys(
|
||||
manifest.coverageBoundary,
|
||||
new Set(['included', 'excluded', 'trackedBy']),
|
||||
'coverageBoundary',
|
||||
failures,
|
||||
);
|
||||
validateStringArray(manifest.coverageBoundary?.included, 'coverageBoundary.included', failures);
|
||||
validateStringArray(manifest.coverageBoundary?.excluded, 'coverageBoundary.excluded', failures, {
|
||||
allowEmpty: true,
|
||||
});
|
||||
requireString(manifest.coverageBoundary?.trackedBy, 'coverageBoundary.trackedBy', failures);
|
||||
if (manifest.mergeAssertions !== undefined) {
|
||||
rejectUnknownKeys(
|
||||
manifest.mergeAssertions,
|
||||
new Set([
|
||||
'mode',
|
||||
'providerEvidence',
|
||||
'deferredReplayOwner',
|
||||
'trustDependencies',
|
||||
'postMergeResponse',
|
||||
]),
|
||||
'mergeAssertions',
|
||||
failures,
|
||||
);
|
||||
for (const key of ['mode', 'providerEvidence', 'deferredReplayOwner', 'postMergeResponse']) {
|
||||
requireString(manifest.mergeAssertions?.[key], `mergeAssertions.${key}`, failures);
|
||||
}
|
||||
validateStringArray(
|
||||
manifest.mergeAssertions?.trustDependencies,
|
||||
'mergeAssertions.trustDependencies',
|
||||
failures,
|
||||
);
|
||||
}
|
||||
rejectDuplicateIds(manifest.criteria, 'criterion', failures);
|
||||
for (const criterion of manifest.criteria ?? []) {
|
||||
rejectUnknownKeys(
|
||||
@@ -324,6 +454,27 @@ function validateClosedSchema(manifest, failures) {
|
||||
`criterion ${criterion.id}`,
|
||||
failures,
|
||||
);
|
||||
for (const key of ['id', 'originalText', 'currentText', 'claimType', 'source']) {
|
||||
requireString(criterion[key], `criterion ${criterion.id}.${key}`, failures);
|
||||
}
|
||||
if (!Array.isArray(criterion.meaningChanges)) {
|
||||
failures.push(`criterion ${criterion.id}.meaningChanges: expected an array`);
|
||||
}
|
||||
for (const [index, change] of (criterion.meaningChanges ?? []).entries()) {
|
||||
rejectUnknownKeys(
|
||||
change,
|
||||
new Set(['originalText', 'restatement', 'reason', 'finding', 'task', 'date']),
|
||||
`criterion ${criterion.id}.meaningChanges[${index}]`,
|
||||
failures,
|
||||
);
|
||||
for (const key of ['originalText', 'restatement', 'reason', 'finding', 'task', 'date']) {
|
||||
requireString(
|
||||
change?.[key],
|
||||
`criterion ${criterion.id}.meaningChanges[${index}].${key}`,
|
||||
failures,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(criterion.caseRefs) || criterion.caseRefs.length === 0) {
|
||||
failures.push(`${criterion.id}: no declared exercising cases`);
|
||||
} else {
|
||||
@@ -343,6 +494,9 @@ function validateClosedSchema(manifest, failures) {
|
||||
`prose claim ${claim.id}`,
|
||||
failures,
|
||||
);
|
||||
for (const key of ['id', 'criterionId', 'caseRef']) {
|
||||
requireString(claim[key], `GATE-CLAIM:${claim.id}.${key}`, failures);
|
||||
}
|
||||
if (typeof claim.caseRef !== 'string' || claim.caseRef.length === 0) {
|
||||
failures.push(`GATE-CLAIM:${claim.id} has no declared exercising case`);
|
||||
}
|
||||
@@ -363,15 +517,22 @@ function validateClosedSchema(manifest, failures) {
|
||||
`compatibility scenario ${scenario.id}`,
|
||||
failures,
|
||||
);
|
||||
for (const key of ['id', 'construction']) {
|
||||
requireString(scenario[key], `compatibility scenario ${scenario.id}.${key}`, failures);
|
||||
}
|
||||
if (!Array.isArray(scenario.caseRefs) || scenario.caseRefs.length === 0) {
|
||||
failures.push(`${scenario.id}: compatibility construction has no referenced conditions`);
|
||||
} else {
|
||||
validateStringArray(scenario.caseRefs, `${scenario.id}.caseRefs`, failures);
|
||||
}
|
||||
if (!Array.isArray(scenario.invocation) || scenario.invocation.length === 0) {
|
||||
failures.push(`${scenario.id}: compatibility construction invocation is missing`);
|
||||
} else {
|
||||
validateStringArray(scenario.invocation, `${scenario.id}.invocation`, failures);
|
||||
}
|
||||
if (typeof scenario.expected?.exitCode !== 'number') {
|
||||
failures.push(`${scenario.id}: compatibility construction exact expected exit is missing`);
|
||||
}
|
||||
validateOutcome(scenario.expected, `${scenario.id}.expected`, failures);
|
||||
validateEnvironment(scenario.environment, `${scenario.id}.environment`, failures);
|
||||
validateFixture(scenario.fixture, `${scenario.id}.fixture`, failures);
|
||||
}
|
||||
rejectDuplicateIds(manifest.gates, 'gate', failures);
|
||||
for (const gate of manifest.gates ?? []) {
|
||||
@@ -389,13 +550,57 @@ function validateClosedSchema(manifest, failures) {
|
||||
`gate ${gate.id}`,
|
||||
failures,
|
||||
);
|
||||
requireString(gate.id, `gate ${gate.id}.id`, failures);
|
||||
requireString(gate.source, `gate ${gate.id}.source`, failures);
|
||||
rejectDuplicateIds(gate.cases, `case in gate ${gate.id}`, failures);
|
||||
if (!Array.isArray(gate.invocation) || gate.invocation.length === 0) {
|
||||
failures.push(`${gate.id}: exact invocation is missing`);
|
||||
} else {
|
||||
validateStringArray(gate.invocation, `${gate.id}.invocation`, failures);
|
||||
}
|
||||
if (gate.discoveryAliases !== undefined) {
|
||||
validateStringArray(gate.discoveryAliases, `${gate.id}.discoveryAliases`, failures, {
|
||||
allowEmpty: true,
|
||||
});
|
||||
}
|
||||
rejectUnknownKeys(
|
||||
gate.deployment,
|
||||
new Set(['kind', 'reason', 'source', 'path', 'unavailableOwner', 'observedSha256']),
|
||||
`${gate.id}.deployment`,
|
||||
failures,
|
||||
);
|
||||
if (!['none', 'file'].includes(gate.deployment?.kind)) {
|
||||
failures.push(`${gate.id}: unsupported deployment kind ${String(gate.deployment?.kind)}`);
|
||||
} else if (gate.deployment.kind === 'none') {
|
||||
requireString(gate.deployment.reason, `${gate.id}.deployment.reason`, failures);
|
||||
} else {
|
||||
for (const key of ['source', 'path', 'unavailableOwner', 'observedSha256']) {
|
||||
requireString(gate.deployment[key], `${gate.id}.deployment.${key}`, failures);
|
||||
}
|
||||
}
|
||||
rejectUnknownKeys(
|
||||
gate.inertMutation,
|
||||
new Set(['file', 'find', 'replace', 'caseId', 'expected', 'sandboxFiles']),
|
||||
`${gate.id}.inertMutation`,
|
||||
failures,
|
||||
);
|
||||
for (const key of ['file', 'find', 'replace']) {
|
||||
if (typeof gate.inertMutation?.[key] !== 'string') {
|
||||
failures.push(`${gate.id}.inertMutation.${key}: expected a string`);
|
||||
}
|
||||
}
|
||||
if (gate.inertMutation?.caseId !== undefined) {
|
||||
requireString(gate.inertMutation.caseId, `${gate.id}.inertMutation.caseId`, failures);
|
||||
}
|
||||
if (gate.inertMutation?.sandboxFiles !== undefined) {
|
||||
validateStringArray(
|
||||
gate.inertMutation.sandboxFiles,
|
||||
`${gate.id}.inertMutation.sandboxFiles`,
|
||||
failures,
|
||||
{ allowEmpty: true },
|
||||
);
|
||||
}
|
||||
validateOutcome(gate.inertMutation?.expected, `${gate.id}.inertMutation.expected`, failures);
|
||||
for (const gateCase of gate.cases ?? []) {
|
||||
rejectUnknownKeys(
|
||||
gateCase,
|
||||
@@ -414,25 +619,40 @@ function validateClosedSchema(manifest, failures) {
|
||||
`${gate.id}/${gateCase.id}`,
|
||||
failures,
|
||||
);
|
||||
if (
|
||||
typeof gateCase.required?.exitCode !== 'number' ||
|
||||
typeof gateCase.actual?.exitCode !== 'number'
|
||||
) {
|
||||
failures.push(
|
||||
`${gate.id}/${gateCase.id}: required and actual exact exit codes are mandatory`,
|
||||
);
|
||||
requireString(gateCase.id, `${gate.id}/${gateCase.id}.id`, failures);
|
||||
validateStringArray(
|
||||
gateCase.criterionIds,
|
||||
`${gate.id}/${gateCase.id}.criterionIds`,
|
||||
failures,
|
||||
);
|
||||
if (typeof gateCase.mustFail !== 'boolean') {
|
||||
failures.push(`${gate.id}/${gateCase.id}.mustFail: expected a boolean`);
|
||||
}
|
||||
if (
|
||||
gateCase.invocation &&
|
||||
(!Array.isArray(gateCase.invocation) || gateCase.invocation.length === 0)
|
||||
) {
|
||||
failures.push(`${gate.id}/${gateCase.id}: case invocation must be non-empty`);
|
||||
validateOutcome(gateCase.required, `${gate.id}/${gateCase.id}.required`, failures);
|
||||
validateOutcome(gateCase.actual, `${gate.id}/${gateCase.id}.actual`, failures);
|
||||
if (gateCase.invocation !== undefined) {
|
||||
validateStringArray(gateCase.invocation, `${gate.id}/${gateCase.id}.invocation`, failures);
|
||||
}
|
||||
if (typeof gateCase.reasonPattern !== 'string') {
|
||||
failures.push(`${gate.id}/${gateCase.id}.reasonPattern: expected a string`);
|
||||
}
|
||||
if (gateCase.mustFail === true && !gateCase.reasonPattern?.trim()) {
|
||||
failures.push(
|
||||
`${gate.id}/${gateCase.id}: must-fail case requires a non-empty reasonPattern`,
|
||||
);
|
||||
}
|
||||
validateEnvironment(gateCase.environment, `${gate.id}/${gateCase.id}.environment`, failures);
|
||||
validateFixture(gateCase.fixture, `${gate.id}/${gateCase.id}.fixture`, failures);
|
||||
if (gateCase.defect !== undefined) {
|
||||
rejectUnknownKeys(
|
||||
gateCase.defect,
|
||||
new Set(['owner', 'reason']),
|
||||
`${gate.id}/${gateCase.id}.defect`,
|
||||
failures,
|
||||
);
|
||||
requireString(gateCase.defect.owner, `${gate.id}/${gateCase.id}.defect.owner`, failures);
|
||||
requireString(gateCase.defect.reason, `${gate.id}/${gateCase.id}.defect.reason`, failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user