mirror of
https://github.com/srid/nixos-config.git
synced 2026-05-01 19:35:40 +08:00
scripts
This commit is contained in:
parent
36352405c1
commit
e8a31a494b
3 changed files with 617 additions and 1 deletions
|
|
@ -91,7 +91,15 @@ Always frame simplifications as *structural refactors*, not style preferences. "
|
|||
|
||||
## Automated Fitness Functions
|
||||
|
||||
When the user asks for fitness functions, structural checks, or CI-enforceable rules, generate concrete implementations. Here are the patterns to draw from, adapted to the codebase's language and tooling:
|
||||
When the user asks for fitness functions, structural checks, or CI-enforceable rules, generate concrete implementations. Here are the patterns to draw from, adapted to the codebase's language and tooling.
|
||||
|
||||
### Ready-made scripts
|
||||
|
||||
The `scripts/` directory (relative to this skill) contains ready-to-use tooling:
|
||||
|
||||
- **`scripts/complect-detect.ts`** — a ts-morph script that runs five structural checks against a TypeScript project: mutable state density, closure-over-mutable-state, circular event flow, module concern mixing, and lifecycle nesting. Run with `npx tsx complect-detect.ts --project ./tsconfig.json`. See `scripts/README.md` for full usage and CI integration.
|
||||
|
||||
When working with a TypeScript project, prefer pointing the user to this script (or running it directly) rather than generating checks from scratch.
|
||||
|
||||
### For TypeScript/JavaScript codebases
|
||||
|
||||
|
|
|
|||
75
AI/skills/hickey/scripts/README.md
Normal file
75
AI/skills/hickey/scripts/README.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# complect-detect
|
||||
|
||||
Structural simplicity checks for TypeScript projects, based on Rich Hickey's "Simple Made Easy" framework. Detects accidental complexity that tests can't catch.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install -D ts-morph tsx
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npx tsx complect-detect.ts --project ./tsconfig.json --threshold 3
|
||||
```
|
||||
|
||||
- `--project` — path to `tsconfig.json` (default: `./tsconfig.json`)
|
||||
- `--threshold` — minimum mutable ops per function to report (default: 3)
|
||||
|
||||
JSON report goes to stdout, human-readable summary to stderr. Exit 0 = clean, exit 1 = findings.
|
||||
|
||||
## Checks
|
||||
|
||||
### 1. Mutable state density
|
||||
|
||||
Counts `let` declarations + reassignments (`=`, `+=`, `++`, etc.) per exported function and async generator. Functions at or above the threshold are flagged. Each mutable binding is a temporal entanglement — correctness depends on *when*, not just *what*.
|
||||
|
||||
### 2. Closure over mutable state
|
||||
|
||||
Finds inner functions (arrows, function expressions) that reference a `let`-declared variable from an enclosing scope. Reports the variable name and both locations. These closures braid the inner function's behavior with the outer function's timeline.
|
||||
|
||||
### 3. Circular event flow
|
||||
|
||||
Finds functions that both subscribe to an EventEmitter event (`.on`, `.addEventListener`, `for await...of subscribeAndYield(...)`) AND emit on the same event. Catches feedback loops that no test suite surfaces — the producer/consumer distinction collapses.
|
||||
|
||||
### 4. Module concern mixing
|
||||
|
||||
Classifies each export as `pure-function`, `async-generator`, `stateful`, or `side-effect`. Reports files mixing categories. A module that exports both pure queries and stateful watchers is braiding two concerns.
|
||||
|
||||
### 5. Lifecycle nesting
|
||||
|
||||
Finds `new AbortController()`, `fs.watch(`, `setTimeout(`, `setInterval(` inside async generator bodies. Reports generators with 2+ lifecycle constructs — each nested lifecycle is a "when" concern braided into existing control flow.
|
||||
|
||||
## CI integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Structural Simplicity
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
complect-detect:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- name: Run complect-detect
|
||||
run: |
|
||||
npx tsx path/to/complect-detect.ts \
|
||||
--project ./tsconfig.json \
|
||||
--threshold 3 \
|
||||
> complect-report.json
|
||||
- name: Upload report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: complect-report
|
||||
path: complect-report.json
|
||||
```
|
||||
|
||||
The script exits 1 when findings exist, failing the CI step. Adjust `--threshold` to control sensitivity. Start permissive and tighten over time.
|
||||
533
AI/skills/hickey/scripts/complect-detect.ts
Normal file
533
AI/skills/hickey/scripts/complect-detect.ts
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* complect-detect — structural simplicity checks for TypeScript projects.
|
||||
*
|
||||
* Implements five checks from the Hickey "Simple Made Easy" framework:
|
||||
* 1. Mutable state density per function
|
||||
* 2. Closure-over-mutable-state
|
||||
* 3. Circular event flow
|
||||
* 4. Module concern mixing
|
||||
* 5. Lifecycle nesting in async generators
|
||||
*
|
||||
* Usage: npx tsx complect-detect.ts [--project ./tsconfig.json] [--threshold 3]
|
||||
*/
|
||||
|
||||
import { Project, SyntaxKind, Node, SourceFile, ts } from "ts-morph";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { values: args } = parseArgs({
|
||||
options: {
|
||||
project: { type: "string", default: "./tsconfig.json" },
|
||||
threshold: { type: "string", default: "3" },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const tsconfigPath = args.project!;
|
||||
const mutableThreshold = parseInt(args.threshold!, 10);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MutableStateFinding {
|
||||
file: string;
|
||||
function: string;
|
||||
line: number;
|
||||
letCount: number;
|
||||
reassignmentCount: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ClosureMutableFinding {
|
||||
file: string;
|
||||
outerFunction: string;
|
||||
innerFunction: string;
|
||||
variable: string;
|
||||
outerLine: number;
|
||||
innerLine: number;
|
||||
}
|
||||
|
||||
interface CircularEventFinding {
|
||||
file: string;
|
||||
function: string;
|
||||
line: number;
|
||||
event: string;
|
||||
}
|
||||
|
||||
type ExportKind = "pure-function" | "async-generator" | "stateful" | "side-effect";
|
||||
|
||||
interface ModuleConcernFinding {
|
||||
file: string;
|
||||
kinds: ExportKind[];
|
||||
exports: Array<{ name: string; kind: ExportKind }>;
|
||||
}
|
||||
|
||||
interface LifecycleNestingFinding {
|
||||
file: string;
|
||||
generator: string;
|
||||
line: number;
|
||||
constructs: string[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface Report {
|
||||
mutableStateDensity: MutableStateFinding[];
|
||||
closureOverMutable: ClosureMutableFinding[];
|
||||
circularEventFlow: CircularEventFinding[];
|
||||
moduleConcernMixing: ModuleConcernFinding[];
|
||||
lifecycleNesting: LifecycleNestingFinding[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const filePath = (sf: SourceFile): string => sf.getFilePath();
|
||||
|
||||
const isFunctionLike = (node: Node): boolean =>
|
||||
Node.isFunctionDeclaration(node) ||
|
||||
Node.isFunctionExpression(node) ||
|
||||
Node.isArrowFunction(node) ||
|
||||
Node.isMethodDeclaration(node);
|
||||
|
||||
const functionName = (node: Node): string => {
|
||||
if (Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node)) {
|
||||
return (node as any).getName?.() ?? "<anonymous>";
|
||||
}
|
||||
// arrow / expression assigned to a variable
|
||||
const parent = node.getParent();
|
||||
if (parent && Node.isVariableDeclaration(parent)) {
|
||||
return parent.getName();
|
||||
}
|
||||
return "<anonymous>";
|
||||
};
|
||||
|
||||
const isAsyncGenerator = (node: Node): boolean => {
|
||||
if (
|
||||
Node.isFunctionDeclaration(node) ||
|
||||
Node.isFunctionExpression(node) ||
|
||||
Node.isMethodDeclaration(node)
|
||||
) {
|
||||
const decl = node as any;
|
||||
return !!decl.isAsync?.() && !!decl.isGenerator?.();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check 1: Mutable state density
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const countMutableState = (fn: Node): { lets: number; reassigns: number } => {
|
||||
let lets = 0;
|
||||
let reassigns = 0;
|
||||
|
||||
fn.forEachDescendant((child) => {
|
||||
// Skip nested function-like nodes (they get their own count)
|
||||
if (isFunctionLike(child) && child !== fn) return;
|
||||
|
||||
if (Node.isVariableDeclaration(child)) {
|
||||
const stmt = child.getParent();
|
||||
if (stmt && Node.isVariableDeclarationList(stmt)) {
|
||||
if (stmt.getDeclarationKind() === ts.NodeFlags.None) {
|
||||
// "let" shows up as None in some ts-morph versions; check text
|
||||
}
|
||||
const kindText = stmt.getFlags();
|
||||
// More reliable: check the keyword directly
|
||||
const text = stmt.getText();
|
||||
if (text.startsWith("let ")) lets++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count binary assignments that aren't initializers
|
||||
if (Node.isBinaryExpression(child)) {
|
||||
const op = child.getOperatorToken().getKind();
|
||||
if (
|
||||
op === SyntaxKind.EqualsToken ||
|
||||
op === SyntaxKind.PlusEqualsToken ||
|
||||
op === SyntaxKind.MinusEqualsToken
|
||||
) {
|
||||
reassigns++;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix/postfix increment/decrement
|
||||
if (
|
||||
Node.isPrefixUnaryExpression(child) ||
|
||||
Node.isPostfixUnaryExpression(child)
|
||||
) {
|
||||
const op = (child as any).getOperatorToken?.() ?? child.getChildAtIndex(0);
|
||||
const opKind = child.compilerNode.operator;
|
||||
if (
|
||||
opKind === ts.SyntaxKind.PlusPlusToken ||
|
||||
opKind === ts.SyntaxKind.MinusMinusToken
|
||||
) {
|
||||
reassigns++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { lets, reassigns };
|
||||
};
|
||||
|
||||
const checkMutableStateDensity = (
|
||||
sourceFiles: SourceFile[],
|
||||
threshold: number,
|
||||
): MutableStateFinding[] =>
|
||||
sourceFiles.flatMap((sf) => {
|
||||
const findings: MutableStateFinding[] = [];
|
||||
sf.forEachDescendant((node) => {
|
||||
if (!isFunctionLike(node)) return;
|
||||
// Only check exported functions and async generators at top level
|
||||
const isExported =
|
||||
Node.isFunctionDeclaration(node) && node.isExported();
|
||||
const isTopLevelVar =
|
||||
node.getParent() &&
|
||||
Node.isVariableDeclaration(node.getParent()!) &&
|
||||
(node.getParent()!.getParent()?.getParent() as any)?.isExported?.();
|
||||
|
||||
if (!isExported && !isTopLevelVar && !isAsyncGenerator(node)) return;
|
||||
|
||||
const { lets, reassigns } = countMutableState(node);
|
||||
const total = lets + reassigns;
|
||||
if (total >= threshold) {
|
||||
findings.push({
|
||||
file: filePath(sf),
|
||||
function: functionName(node),
|
||||
line: node.getStartLineNumber(),
|
||||
letCount: lets,
|
||||
reassignmentCount: reassigns,
|
||||
total,
|
||||
});
|
||||
}
|
||||
});
|
||||
return findings;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check 2: Closure over mutable state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const checkClosureOverMutable = (
|
||||
sourceFiles: SourceFile[],
|
||||
): ClosureMutableFinding[] =>
|
||||
sourceFiles.flatMap((sf) => {
|
||||
const findings: ClosureMutableFinding[] = [];
|
||||
|
||||
sf.forEachDescendant((outer) => {
|
||||
if (!isFunctionLike(outer)) return;
|
||||
|
||||
// Collect let-declared variables in this scope
|
||||
const letVars = new Map<string, number>();
|
||||
outer.forEachChild((child) => {
|
||||
if (Node.isVariableStatement(child)) {
|
||||
const decl = child.getDeclarationList();
|
||||
if (decl.getText().startsWith("let ")) {
|
||||
for (const v of decl.getDeclarations()) {
|
||||
letVars.set(v.getName(), v.getStartLineNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (letVars.size === 0) return;
|
||||
|
||||
// Find inner functions that reference these variables
|
||||
outer.forEachDescendant((inner) => {
|
||||
if (!isFunctionLike(inner) || inner === outer) return;
|
||||
inner.forEachDescendant((ref) => {
|
||||
if (Node.isIdentifier(ref)) {
|
||||
const name = ref.getText();
|
||||
if (letVars.has(name)) {
|
||||
findings.push({
|
||||
file: filePath(sf),
|
||||
outerFunction: functionName(outer),
|
||||
innerFunction: functionName(inner),
|
||||
variable: name,
|
||||
outerLine: letVars.get(name)!,
|
||||
innerLine: ref.getStartLineNumber(),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return findings;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check 3: Circular event flow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const extractEventName = (callExpr: Node): string | undefined => {
|
||||
if (!Node.isCallExpression(callExpr)) return undefined;
|
||||
const args = callExpr.getArguments();
|
||||
if (args.length === 0) return undefined;
|
||||
const first = args[0];
|
||||
if (Node.isStringLiteral(first)) return first.getLiteralText();
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const checkCircularEventFlow = (
|
||||
sourceFiles: SourceFile[],
|
||||
): CircularEventFinding[] =>
|
||||
sourceFiles.flatMap((sf) => {
|
||||
const findings: CircularEventFinding[] = [];
|
||||
|
||||
sf.forEachDescendant((fn) => {
|
||||
if (!isFunctionLike(fn)) return;
|
||||
|
||||
const subscribedEvents = new Set<string>();
|
||||
const emittedEvents = new Set<string>();
|
||||
|
||||
fn.forEachDescendant((node) => {
|
||||
if (!Node.isCallExpression(node)) return;
|
||||
const expr = node.getExpression();
|
||||
if (!Node.isPropertyAccessExpression(expr)) return;
|
||||
|
||||
const methodName = expr.getName();
|
||||
const eventName = extractEventName(node);
|
||||
if (!eventName) return;
|
||||
|
||||
if (methodName === "on" || methodName === "addEventListener" || methodName === "addListener") {
|
||||
subscribedEvents.add(eventName);
|
||||
}
|
||||
if (methodName === "emit" || methodName === "dispatchEvent") {
|
||||
emittedEvents.add(eventName);
|
||||
}
|
||||
});
|
||||
|
||||
// Also check for subscribeAndYield pattern in for-await
|
||||
fn.forEachDescendant((node) => {
|
||||
if (Node.isForOfStatement(node) && node.isAwaited()) {
|
||||
const initializer = node.getInitializer();
|
||||
const iterExpr = node.getExpression();
|
||||
if (Node.isCallExpression(iterExpr)) {
|
||||
const callee = iterExpr.getExpression();
|
||||
if (Node.isIdentifier(callee) && callee.getText() === "subscribeAndYield") {
|
||||
const eventName = iterExpr.getArguments()[1];
|
||||
if (eventName && Node.isStringLiteral(eventName)) {
|
||||
subscribedEvents.add(eventName.getLiteralText());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find overlap
|
||||
for (const event of subscribedEvents) {
|
||||
if (emittedEvents.has(event)) {
|
||||
findings.push({
|
||||
file: filePath(sf),
|
||||
function: functionName(fn),
|
||||
line: fn.getStartLineNumber(),
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return findings;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check 4: Module concern mixing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SIDE_EFFECT_MODULES = new Set([
|
||||
"fs", "node:fs", "fs/promises", "node:fs/promises",
|
||||
"net", "node:net", "http", "node:http", "https", "node:https",
|
||||
"child_process", "node:child_process",
|
||||
"process",
|
||||
]);
|
||||
|
||||
const classifyExport = (node: Node, sf: SourceFile): ExportKind => {
|
||||
if (isAsyncGenerator(node)) return "async-generator";
|
||||
|
||||
// Check for class / mutable module-level state
|
||||
if (Node.isClassDeclaration(node)) return "stateful";
|
||||
if (Node.isVariableDeclaration(node)) {
|
||||
const init = node.getInitializer();
|
||||
if (init && Node.isNewExpression(init)) return "stateful";
|
||||
}
|
||||
|
||||
// Check if function body references side-effect modules
|
||||
if (isFunctionLike(node)) {
|
||||
const text = node.getText();
|
||||
const imports = sf.getImportDeclarations();
|
||||
for (const imp of imports) {
|
||||
const mod = imp.getModuleSpecifierValue();
|
||||
if (SIDE_EFFECT_MODULES.has(mod)) {
|
||||
// Check if any imported name is used in this function
|
||||
for (const named of imp.getNamedImports()) {
|
||||
if (text.includes(named.getName())) return "side-effect";
|
||||
}
|
||||
const ns = imp.getNamespaceImport();
|
||||
if (ns && text.includes(ns.getText())) return "side-effect";
|
||||
const def = imp.getDefaultImport();
|
||||
if (def && text.includes(def.getText())) return "side-effect";
|
||||
}
|
||||
}
|
||||
return "pure-function";
|
||||
}
|
||||
|
||||
return "pure-function";
|
||||
};
|
||||
|
||||
const checkModuleConcernMixing = (
|
||||
sourceFiles: SourceFile[],
|
||||
): ModuleConcernFinding[] =>
|
||||
sourceFiles.flatMap((sf) => {
|
||||
const exports: Array<{ name: string; kind: ExportKind }> = [];
|
||||
|
||||
for (const decl of sf.getExportedDeclarations()) {
|
||||
const [name, nodes] = decl;
|
||||
for (const node of nodes) {
|
||||
exports.push({ name, kind: classifyExport(node, sf) });
|
||||
}
|
||||
}
|
||||
|
||||
const kinds = [...new Set(exports.map((e) => e.kind))];
|
||||
if (kinds.length > 1) {
|
||||
return [{ file: filePath(sf), kinds, exports }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check 5: Lifecycle nesting in async generators
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LIFECYCLE_PATTERNS = [
|
||||
"new AbortController",
|
||||
"fs.watch(",
|
||||
"setTimeout(",
|
||||
"setInterval(",
|
||||
"fs.watchFile(",
|
||||
];
|
||||
|
||||
const checkLifecycleNesting = (
|
||||
sourceFiles: SourceFile[],
|
||||
): LifecycleNestingFinding[] =>
|
||||
sourceFiles.flatMap((sf) => {
|
||||
const findings: LifecycleNestingFinding[] = [];
|
||||
|
||||
sf.forEachDescendant((node) => {
|
||||
if (!isAsyncGenerator(node)) return;
|
||||
|
||||
const body = node.getText();
|
||||
const constructs: string[] = [];
|
||||
|
||||
for (const pattern of LIFECYCLE_PATTERNS) {
|
||||
if (body.includes(pattern)) {
|
||||
constructs.push(pattern.replace("(", "").trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for new AbortController via AST
|
||||
node.forEachDescendant((child) => {
|
||||
if (
|
||||
Node.isNewExpression(child) &&
|
||||
child.getExpression().getText() === "AbortController" &&
|
||||
!constructs.includes("new AbortController")
|
||||
) {
|
||||
constructs.push("new AbortController");
|
||||
}
|
||||
});
|
||||
|
||||
if (constructs.length >= 2) {
|
||||
findings.push({
|
||||
file: filePath(sf),
|
||||
generator: functionName(node),
|
||||
line: node.getStartLineNumber(),
|
||||
constructs,
|
||||
count: constructs.length,
|
||||
});
|
||||
}
|
||||
});
|
||||
return findings;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const run = (tsconfigPath: string, threshold: number): Report => {
|
||||
const project = new Project({ tsConfigFilePath: tsconfigPath });
|
||||
const sourceFiles = project
|
||||
.getSourceFiles()
|
||||
.filter((sf) => !sf.isDeclarationFile() && !sf.getFilePath().includes("node_modules"));
|
||||
|
||||
return {
|
||||
mutableStateDensity: checkMutableStateDensity(sourceFiles, threshold),
|
||||
closureOverMutable: checkClosureOverMutable(sourceFiles),
|
||||
circularEventFlow: checkCircularEventFlow(sourceFiles),
|
||||
moduleConcernMixing: checkModuleConcernMixing(sourceFiles),
|
||||
lifecycleNesting: checkLifecycleNesting(sourceFiles),
|
||||
};
|
||||
};
|
||||
|
||||
const summarize = (report: Report): void => {
|
||||
const log = (msg: string) => process.stderr.write(msg + "\n");
|
||||
log("\n=== Complect Detect — Structural Simplicity Report ===\n");
|
||||
|
||||
const section = (name: string, items: unknown[]) => {
|
||||
log(`${name}: ${items.length} finding(s)`);
|
||||
};
|
||||
|
||||
section("Mutable state density", report.mutableStateDensity);
|
||||
for (const f of report.mutableStateDensity) {
|
||||
log(` ${f.file}:${f.line} — ${f.function} (${f.total} mutable ops: ${f.letCount} lets, ${f.reassignmentCount} reassignments)`);
|
||||
}
|
||||
|
||||
section("Closure over mutable state", report.closureOverMutable);
|
||||
for (const f of report.closureOverMutable) {
|
||||
log(` ${f.file}:${f.innerLine} — inner fn "${f.innerFunction}" captures \`${f.variable}\` from "${f.outerFunction}" (declared line ${f.outerLine})`);
|
||||
}
|
||||
|
||||
section("Circular event flow", report.circularEventFlow);
|
||||
for (const f of report.circularEventFlow) {
|
||||
log(` ${f.file}:${f.line} — ${f.function} subscribes and emits "${f.event}"`);
|
||||
}
|
||||
|
||||
section("Module concern mixing", report.moduleConcernMixing);
|
||||
for (const f of report.moduleConcernMixing) {
|
||||
log(` ${f.file} — mixes: ${f.kinds.join(", ")}`);
|
||||
for (const e of f.exports) {
|
||||
log(` ${e.name}: ${e.kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
section("Lifecycle nesting", report.lifecycleNesting);
|
||||
for (const f of report.lifecycleNesting) {
|
||||
log(` ${f.file}:${f.line} — ${f.generator} (${f.count} lifecycle constructs: ${f.constructs.join(", ")})`);
|
||||
}
|
||||
|
||||
const total =
|
||||
report.mutableStateDensity.length +
|
||||
report.closureOverMutable.length +
|
||||
report.circularEventFlow.length +
|
||||
report.moduleConcernMixing.length +
|
||||
report.lifecycleNesting.length;
|
||||
|
||||
log(`\nTotal findings: ${total}`);
|
||||
log(total === 0 ? "✓ No structural simplicity issues detected." : "✗ Findings above — review for accidental complexity.");
|
||||
};
|
||||
|
||||
// Run
|
||||
const report = run(tsconfigPath, mutableThreshold);
|
||||
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
||||
summarize(report);
|
||||
process.exit(
|
||||
report.mutableStateDensity.length +
|
||||
report.closureOverMutable.length +
|
||||
report.circularEventFlow.length +
|
||||
report.moduleConcernMixing.length +
|
||||
report.lifecycleNesting.length > 0
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue