# rules/eslint/closure-paths-via-ssot.eslint.rule.ts

> 191 lines of code and 34 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-paths-via-ssot-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-paths-via-ssot.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-no-unreachable-file.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-unreachable-file.eslint.rule.ts.md) and before [rules/eslint/closure-register-needs-consumer.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-register-needs-consumer.eslint.rule.ts.md).

## Definitions

- `viaSsot` (lexical_declaration, line 61, exported)
- `importDeclaration` (method_definition, line 102, exported)
- `callExpression` (method_definition, line 78, exported)
- `literal` (method_definition, line 113, exported)
- `variableDeclarator` (method_definition, line 146, exported)
- `create` (method_definition, line 47, exported)
- `templateElement` (method_definition, line 127, exported)
- `SSOT_MODULE` (lexical_declaration, line 36)
- `WRITE_CALLEES` (lexical_declaration, line 38)
- `PendingTarget` (interface_declaration, line 40)
- `{ filename }` (lexical_declaration, line 48, exported)
- `depth` (lexical_declaration, line 52, exported)
- `posixFile` (lexical_declaration, line 53, exported)
- `fromDir` (lexical_declaration, line 54, exported)
- `metaNames` (lexical_declaration, line 55, exported)
- `dirNames` (lexical_declaration, line 56, exported)
- `writtenNames` (lexical_declaration, line 57, exported)
- `pendingTargets` (lexical_declaration, line 58, exported)
- `constSegments` (lexical_declaration, line 59, exported)
- `ssotNames` (lexical_declaration, line 60, exported)
- `parent` (lexical_declaration, line 62, exported)
- `reportPending` (lexical_declaration, line 65, exported)
- `first` (lexical_declaration, line 83, exported)
- `segmented` (lexical_declaration, line 92, exported)
- `target` (lexical_declaration, line 97, exported)
- `local` (lexical_declaration, line 107, exported)
- `value` (lexical_declaration, line 114, exported)
- `cooked` (lexical_declaration, line 128, exported)
- `composed` (lexical_declaration, line 132, exported)
- `token` (lexical_declaration, line 141, exported)
- `id` (lexical_declaration, line 147, exported)
- `name` (lexical_declaration, line 151, exported)
- `init` (lexical_declaration, line 152, exported)
- `folded` (lexical_declaration, line 159, exported)

## Uses

- [shared/analyzers/location.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/location.analyzer.ts.md)
- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/predicates/location.predicate.ts](https://banes-lab.com/source/governance/shared/predicates/location.predicate.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Single Source of Truth](https://banes-lab.com/records/architecture/single-source-of-truth.md)
- [Hardcoded Configuration](https://banes-lab.com/records/architecture/hardcoded-configuration.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener, RuleNode } from "../../types/rule.types.ts";
import {
    anchoredBySsot,
    isModuleSpecifier,
    isPathShaped,
    isProseContext,
    isSsotExemptFile,
} from "../../shared/predicates/location.predicate.ts";
import {
    argumentAt,
    calleeName,
    isType,
    literalString,
    nameOf,
    nodeAt,
    nodesAt,
} from "../../shared/selectors/syntax.selector.ts";
import {
    boundNameOf,
    composedKeyOf,
    cookedOf,
    depthBelowPackage,
    holdsMeta,
    isFileDir,
    isMetaClimb,
    staticTargetOf,
} from "../../shared/analyzers/location.analyzer.ts";
import { collapsePath, normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { composedTokenOf, foldSegments } from "../../shared/analyzers/segment.analyzer.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { defineCheck } from "@govlab/context/check";
import { existsSync } from "node:fs";
import { listener } from "../../shared/factories/listener.factory.ts";
import { tokenIn } from "../../shared/registries/location.registry.ts";

const SSOT_MODULE = "@ssot/paths";

const WRITE_CALLEES = new Set(["cpSync", "mkdir", "mkdirSync", "renameSync", "writeFile", "writeFileSync"]);

interface PendingTarget {
    boundTo: string | null;
    node: RuleNode;
    target: string;
}

export default {
    create(context: RuleContext): RuleListener {
        const { filename } = context;
        if (isSsotExemptFile(filename)) {
            return {};
        }
        const depth = depthBelowPackage(filename);
        const posixFile = normalizePath(filename);
        const fromDir = posixFile.slice(0, posixFile.lastIndexOf("/"));
        const metaNames = new Set<string>();
        const dirNames = new Set<string>();
        const writtenNames = new Set<string>();
        const pendingTargets: PendingTarget[] = [];
        const constSegments = new Map<string, string[]>();
        const ssotNames = new Set<string>();
        const viaSsot = function viaSsot(node: AstNode): boolean {
            const parent = nodeAt(node, "parent");
            return isType(parent, "CallExpression") && ssotNames.has(calleeName(parent));
        };
        const reportPending = function reportPending(_view: AstNode): void {
            for (const pending of pendingTargets) {
                if (pending.boundTo !== null && writtenNames.has(pending.boundTo)) {
                    continue;
                }
                const target = collapsePath(pending.target);
                if (!existsSync(target)) {
                    context.report({ data: { target }, messageId: "metaMissing", node: pending.node });
                }
            }
        };
        return listener(
            {
                callExpression(view, node) {
                    if (ssotNames.has(calleeName(view))) {
                        return;
                    }
                    if (WRITE_CALLEES.has(calleeName(view))) {
                        const first = argumentAt(view, 0);
                        if (isType(first, "Identifier")) {
                            writtenNames.add(nameOf(first));
                        }
                    }
                    if (isMetaClimb(view, metaNames, depth)) {
                        context.report({ messageId: "metaClimb", node });
                        return;
                    }
                    const segmented = composedTokenOf(view, constSegments);
                    if (segmented !== null) {
                        context.report({ data: { token: segmented }, messageId: "segmentedLocation", node });
                        return;
                    }
                    const target = staticTargetOf(view, dirNames, fromDir);
                    if (target !== null) {
                        pendingTargets.push({ boundTo: boundNameOf(view), node, target });
                    }
                },
                importDeclaration(view) {
                    if (literalString(nodeAt(view, "source")) !== SSOT_MODULE) {
                        return;
                    }
                    for (const spec of nodesAt(view, "specifiers")) {
                        const local = nodeAt(spec, "local");
                        if (isType(local, "Identifier")) {
                            ssotNames.add(nameOf(local));
                        }
                    }
                },
                literal(view, node) {
                    const value = literalString(view);
                    if (value === null || isModuleSpecifier(view) || viaSsot(view) || isProseContext(view)) {
                        return;
                    }
                    const token = tokenIn(value);
                    if (token !== null) {
                        context.report({ data: { token }, messageId: "hardcodedLocation", node });
                        return;
                    }
                    if (isPathShaped(value) && !anchoredBySsot(view, ssotNames)) {
                        context.report({ data: { value }, messageId: "unanchoredPath", node });
                    }
                },
                templateElement(view, node) {
                    const cooked = cookedOf(view);
                    if (cooked.length === 0 || isProseContext(view)) {
                        return;
                    }
                    const composed = composedKeyOf(view, ssotNames);
                    if (composed !== null) {
                        context.report({
                            data: { key: composed.key, tail: composed.tail },
                            messageId: "composedKey",
                            node,
                        });
                        return;
                    }
                    const token = tokenIn(cooked);
                    if (token !== null) {
                        context.report({ data: { token }, messageId: "hardcodedLocation", node });
                    }
                },
                variableDeclarator(view) {
                    const id = nodeAt(view, "id");
                    if (!isType(id, "Identifier")) {
                        return;
                    }
                    const name = nameOf(id);
                    const init = nodeAt(view, "init");
                    if (holdsMeta(init)) {
                        metaNames.add(name);
                    }
                    if (isFileDir(init, dirNames)) {
                        dirNames.add(name);
                    }
                    const folded = foldSegments(init, constSegments);
                    if (folded !== null) {
                        constSegments.set(name, folded);
                    }
                },
            },
            reportPending,
        );
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: ["architecture:hardcoded-configuration"],
                enforces: ["architecture:single-source-of-truth"],
            }),
            description:
                "Every workspace location comes from the paths SSOT. A file may not hardcode a member directory in a path-forming position, and may not derive a root by climbing from its own location with import.meta — file-relative arithmetic silently resolves inside the wrong package the moment anything moves, and a hardcoded member name goes stale without failing. Import `@ssot/paths` and call `resolve(key)` / `rel(key)`, adding the key to paths.yaml when it is missing.",
            workspaceWide: true,
        },
        messages: {
            composedKey:
                'This appends `{{ tail }}` to an SSOT lookup to rebuild a location the SSOT already declares as `{{ key }}`. Hand-composing splits one declared fact across a call and a literal, so the container stops being renameable from its declaration. Use `relativePath("{{ key }}")` / `absolutePath("{{ key }}")`.',
            hardcodedLocation:
                '`{{ token }}` is a workspace location owned by the paths SSOT — hardcoding it here goes stale silently when the tree moves. Use `resolve("{{ token }}")` or `rel("{{ token }}")` from `@ssot/paths`, adding the key to `project.paths/paths.yaml` if it has none.',
            metaClimb:
                "This climbs out of its own package from `import.meta`, so it resolves relative to THIS file and silently points into the wrong package once either end moves. Climbing inside the package is fine; escaping it is not — resolve the other package through `@ssot/paths`.",
            metaMissing:
                "This resolves from `import.meta` to `{{ target }}`, which does not exist — file-relative arithmetic followed the file instead of the target when one of them moved. Resolve the location through `@ssot/paths` so it is a declared key rather than a hop count.",
            segmentedLocation:
                "These segments concatenate to `{{ token }}`, a location owned by the paths SSOT — split across arguments no single literal spells it, so the hardcode passes every per-string check and still goes stale when the tree moves. Resolve it in one call through `@ssot/paths`.",
            unanchoredPath:
                "`{{ value }}` is a path spelled from nothing — it names a location without an anchor the SSOT owns, so no rename can follow it and nothing fails when it goes stale. Anchor it: build the path from `resolve(key)` / `rel(key)` and keep only the tail as a literal, adding the key to `project.paths/paths.yaml` when the location has none.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
