fix(core-eslint): fail loudly on unparseable manifest in wiring gate

usecase-must-be-wired returned {} when the manifest parsed to null,
silently disabling the error-level gate when the manifest existed but
could not be read. It now reports unparseableManifest on Program in
that case; a genuinely missing manifest stays a no-op (that is
feature-must-have-manifest's job).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:11:59 +02:00
parent 3bf0c652e7
commit 0c1df7f5d4
2 changed files with 49 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
import fs from "node:fs";
import path from "node:path";
import { parseManifestUseCases } from "./_manifest-ast.js";
import {
@@ -39,6 +40,8 @@ export default {
messages: {
missing:
'Use case "{{useCase}}" is declared in {{feature}}.manifest.ts but not wired through wireUseCase({ name: "{{useCase}}", ... }) in this binder. Add the wireUseCase call or remove the manifest entry.',
unparseableManifest:
"feature.manifest.ts for {{feature}} exists but could not be parsed by the conformance rules; the wiring gate cannot run. Fix the manifest shape (defineFeature({...} as const)).",
},
},
create(context) {
@@ -49,8 +52,23 @@ export default {
const featureRoot = featureRootForFile(filename, repoRoot);
if (!featureRoot) return {};
const manifest = parseManifestUseCases(manifestPathForFeature(featureRoot));
if (!manifest) return {};
const manifestPath = manifestPathForFeature(featureRoot);
const manifest = parseManifestUseCases(manifestPath);
if (!manifest) {
// An unparseable manifest must FAIL the gate, not silently disable it.
if (fs.existsSync(manifestPath)) {
return {
Program(node) {
context.report({
node,
messageId: "unparseableManifest",
data: { feature: path.basename(featureRoot) },
});
},
};
}
return {};
}
const declaredNames = Object.keys(manifest);
if (declaredNames.length === 0) return {};
const featureName = path.basename(featureRoot);

View File

@@ -153,6 +153,35 @@ describe("usecase-must-be-wired", () => {
});
});
it("reports unparseableManifest when the manifest exists but cannot be parsed", () => {
const { repoRoot, binderFile } = makeFixture({
manifestUseCases: {},
binderBody: `export function bindProductionDemo(ctx) {}`,
});
// Overwrite the manifest with a shape the AST walker cannot read
// (no defineFeature call) — the gate must fail loudly, not no-op.
fs.writeFileSync(
path.join(repoRoot, "packages", "demo", "src", "feature.manifest.ts"),
`export const demoManifest = { name: "demo" };`,
);
tester.run("usecase-must-be-wired", rule, {
valid: [],
invalid: [
{
filename: binderFile,
code: fs.readFileSync(binderFile, "utf8"),
options: [{ repoRoot }],
errors: [
{
messageId: "unparseableManifest",
data: { feature: "demo" },
},
],
},
],
});
});
it("is a no-op when the feature manifest declares no use cases", () => {
const { repoRoot, binderFile } = makeFixture({
manifestUseCases: {},