# rules/eslint/closure-no-type-layer-drift.eslint.rule.ts

> 71 lines of code and 10 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-no-type-layer-drift-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-no-type-layer-drift.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-no-reachable-dead-exports.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-reachable-dead-exports.eslint.rule.ts.md) and before [rules/eslint/closure-no-undeclared-type-layer.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-undeclared-type-layer.eslint.rule.ts.md).

## Definitions

- `deriveLayer` (lexical_declaration, line 20)
- `create` (method_definition, line 46, exported)
- `collectDrift` (lexical_declaration, line 32)
- `MANIFEST_BASENAME` (lexical_declaration, line 16)
- `GRAPH` (lexical_declaration, line 18)
- `importerLayers` (lexical_declaration, line 21)
- `declared` (lexical_declaration, line 33)
- `drift` (lexical_declaration, line 34)
- `DRIFT` (lexical_declaration, line 43)
- `program` (method_definition, line 51, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/manifests/layer.manifest.ts](https://banes-lab.com/source/governance/shared/manifests/layer.manifest.ts.md)

## Records this file checks

- [Layered Architecture](https://banes-lab.com/records/architecture/layered-architecture.md)

## Source

```typescript
import { LAYER_SUBJECT, basenameOf, projectFiles } from "../../shared/resolvers/anchor.resolver.ts";
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import {
    STRING_LAYERS,
    TYPE_LAYERS,
    classifyFile,
    relativeFromMember,
    resolveImportPath,
} from "../../shared/manifests/layer.manifest.ts";
import type { ClosureGraph } from "../../types/closure.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { loadClosureGraph } from "../../shared/loaders/graph.loader.ts";
import { resolveFile } from "../../shared/matchers/filename.matcher.ts";

const MANIFEST_BASENAME = basenameOf(resolveFile(LAYER_SUBJECT, "manifest", projectFiles()));

const GRAPH = loadClosureGraph();

const deriveLayer = function deriveLayer(closure: ClosureGraph, targetFile: string): string {
    const importerLayers = new Set(
        closure.imports
            .filter((imp) => resolveImportPath(imp.file, imp.from) === targetFile)
            .map((imp) => classifyFile(imp.file)),
    );
    if (importerLayers.has("platform")) {
        return "platform";
    }
    return importerLayers.has("product") ? "product" : "unused";
};

const collectDrift = function collectDrift(closure: ClosureGraph): string[] {
    const declared = new Map<string, string>([...TYPE_LAYERS.entries(), ...STRING_LAYERS.entries()]);
    const drift: string[] = [];
    for (const [path, layer] of declared) {
        if (layer === "platform" && deriveLayer(closure, path) === "product") {
            drift.push(path);
        }
    }
    return drift;
};

const DRIFT = GRAPH === null ? [] : collectDrift(GRAPH);

export default {
    create(context: RuleContext): RuleListener {
        if (!relativeFromMember(context.filename).endsWith(MANIFEST_BASENAME)) {
            return {};
        }
        return listener({
            program(_view, node) {
                if (GRAPH === null) {
                    context.report({ messageId: "graphMissing", node });
                    return;
                }
                for (const path of DRIFT) {
                    context.report({ data: { path }, messageId: "platformOverclaim", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:layered-architecture"] }),
            description:
                "A TYPE_LAYERS or STRING_LAYERS entry must match what the import graph actually shows. A file declared `platform` that only product code imports is overclaiming: the platform declaration is the stricter constraint, since it asserts the file is usable by the reusable tier, while `product` merely says the application tier uses it. This rule surfaces a declaration broader than the usage justifying it, which is how a tier boundary quietly stops meaning anything.",
        },
        messages: {
            graphMissing:
                "closure-graph missing. Run `npm run verify` to regenerate. Fail-close until the graph is available.",
            platformOverclaim:
                "File `{{ path }}` is declared `platform` in the layer manifest, but the import graph shows only product code consuming it. Either reclassify it as `product` to match reality, or wire a platform consumer that justifies the broader claim.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
