From 0c1df7f5d4335de5d9e419c5f6ddc2516be21ba1 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 10 Jul 2026 16:11:59 +0200 Subject: [PATCH] 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 --- .../rules/usecase-must-be-wired.js | 22 ++++++++++++-- .../rules/usecase-must-be-wired.test.js | 29 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/core-eslint/rules/usecase-must-be-wired.js b/packages/core-eslint/rules/usecase-must-be-wired.js index b914399..92b8325 100644 --- a/packages/core-eslint/rules/usecase-must-be-wired.js +++ b/packages/core-eslint/rules/usecase-must-be-wired.js @@ -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); diff --git a/packages/core-eslint/rules/usecase-must-be-wired.test.js b/packages/core-eslint/rules/usecase-must-be-wired.test.js index 7e2fb0f..5abcce2 100644 --- a/packages/core-eslint/rules/usecase-must-be-wired.test.js +++ b/packages/core-eslint/rules/usecase-must-be-wired.test.js @@ -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: {},