Files
agentic-dev/packages/core-eslint/rules/usecase-must-be-wired.js
Danijel Martinek 0c1df7f5d4 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>
2026-07-10 16:11:59 +02:00

115 lines
3.9 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { parseManifestUseCases } from "./_manifest-ast.js";
import {
manifestPathForFeature,
featureRootForFile,
} from "./_manifest-source.js";
/**
* Every manifest-declared use case must be bound through `wireUseCase(...)` in
* the feature's `bind-production.ts` and `bind-dev-seed.ts`. `wireUseCase` is
* the canonical helper that wraps factories with `withSpan` + `withCapture`
* (+ `withAudit` for mutating use cases) and attaches the corresponding
* brands. Skipping it produces an unbranded binding that `assertFeatureConformance`
* will reject at boot — this rule shifts that check from ~3s (boot) to <1s
* (lint), so drift is caught while typing.
*
* Lint-time complement to:
* - the TypeScript brand check (`ProductionUseCase<I,O>` slot type) — compile time
* - the boot-time assertion (`assertFeatureConformance`) — runtime
* - the smoke test (`bind-production.smoke.test.ts`) — CI
*/
const BINDER_BASENAMES = new Set(["bind-production.ts", "bind-dev-seed.ts"]);
/** @type {import("eslint").Rule.RuleModule} */
export default {
meta: {
type: "problem",
docs: {
description:
"Every manifest use case must be bound through wireUseCase(...) in the feature's bind-production.ts / bind-dev-seed.ts.",
},
schema: [
{
type: "object",
properties: { repoRoot: { type: "string" } },
additionalProperties: false,
},
],
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) {
const opts = context.options[0] ?? {};
const repoRoot = opts.repoRoot ?? context.cwd ?? process.cwd();
const filename = context.filename;
if (!BINDER_BASENAMES.has(path.basename(filename))) return {};
const featureRoot = featureRootForFile(filename, repoRoot);
if (!featureRoot) 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);
const wired = new Set();
return {
CallExpression(node) {
if (
node.callee.type !== "Identifier" ||
node.callee.name !== "wireUseCase"
) {
return;
}
const arg = node.arguments[0];
if (!arg || arg.type !== "ObjectExpression") return;
const nameProp = arg.properties.find(
(p) =>
p.type === "Property" &&
p.key.type === "Identifier" &&
p.key.name === "name",
);
if (
nameProp &&
nameProp.value.type === "Literal" &&
typeof nameProp.value.value === "string"
) {
wired.add(nameProp.value.value);
}
},
"Program:exit"(programNode) {
for (const useCase of declaredNames) {
if (!wired.has(useCase)) {
context.report({
node: programNode,
messageId: "missing",
data: { useCase, feature: featureName },
});
}
}
},
};
},
};