@cosmicdrift/kumiko-guards 0.1.1 → 0.281.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/_lib/guard-kit.ts +74 -19
- package/src/_lib/qn.ts +21 -0
- package/src/_lib/security-baseline-cli.ts +3 -3
- package/src/_lib/security-baseline.ts +8 -8
- package/src/changes.json +32 -0
- package/src/check-as-casts.ts +648 -0
- package/src/check-complexity.ts +292 -0
- package/src/check-predicates.ts +218 -0
- package/src/check-secret-literals.ts +126 -0
- package/src/cli.ts +59 -0
- package/src/guard-admin-api.ts +1 -1
- package/src/guard-app-feature-structure.ts +114 -0
- package/src/guard-broker-subscribe.ts +99 -0
- package/src/guard-error-reasons.ts +193 -0
- package/src/guard-escape-hatch-declared.ts +155 -35
- package/src/guard-fake-tests.ts +1 -1
- package/src/guard-feature-integration-tests.ts +184 -0
- package/src/guard-html-escape.ts +1 -1
- package/src/guard-i18n-keys.ts +440 -0
- package/src/guard-i18n-locale-mount.ts +317 -0
- package/src/guard-i18n-locale-terminology.ts +117 -0
- package/src/guard-i18n-ui-strings.ts +248 -0
- package/src/guard-lib-test-coverage.ts +156 -0
- package/src/guard-loadall-events.ts +133 -0
- package/src/guard-no-custom-primitives.ts +9 -10
- package/src/guard-no-date-api.ts +1 -1
- package/src/guard-no-direct-fs.ts +1 -1
- package/src/guard-no-inline-styles.ts +4 -4
- package/src/guard-no-logic-in-views.ts +3 -3
- package/src/guard-no-raw-hooks.ts +4 -5
- package/src/guard-open-to-all-reason.ts +1 -1
- package/src/guard-pii-annotations.ts +267 -0
- package/src/guard-pre-es-patterns.ts +1 -1
- package/src/guard-primitives-discipline.ts +3 -3
- package/src/guard-raw-classname.ts +3 -3
- package/src/guard-raw-interactive-elements.ts +3 -3
- package/src/guard-raw-sql.ts +2 -2
- package/src/guard-renderer-boundaries.ts +1 -1
- package/src/guard-restricted-symbols.ts +1 -1
- package/src/guard-screen-conventions.ts +161 -0
- package/src/guard-silent-skip.ts +1 -1
- package/src/guard-table-ddl.ts +159 -0
- package/src/guard-tailwind-scan-surface.ts +12 -12
- package/src/guard-test-stack-drift.ts +147 -0
- package/src/guard-text-field-stance.ts +222 -0
- package/src/guard-thin-wrappers.ts +6 -1
- package/src/guard-unsafe-json-parse.ts +1 -1
- package/src/guard-write-handler-qns.ts +242 -0
- package/src/run-guards.ts +36 -3
- package/src/run-repo-checks.ts +10 -1
- package/src/run-ui-guards.ts +11 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// lib/ ist die Heimat der extrahierten Logik (Berechnung, Parsing, Mapping) —
|
|
3
|
+
// der no-logic-in-views-Guard schiebt sie dorthin, dieser Guard sorgt dafür,
|
|
4
|
+
// dass sie an der neuen Stelle auch GETESTET ist. Für jede lib-Datei mit
|
|
5
|
+
// mindestens einer exportierten Funktion muss ein Test existieren, der aus dem
|
|
6
|
+
// Modul importiert, und jede exportierte Funktion muss dort namentlich
|
|
7
|
+
// vorkommen.
|
|
8
|
+
//
|
|
9
|
+
// Warum statisch (ts-morph) statt Coverage-Threshold: Coverage lügt zweifach —
|
|
10
|
+
// sie sieht nie-importierte Module gar nicht, und "ausgeführt" heißt nicht
|
|
11
|
+
// "sinnvoll geprüft" (ein Integrationstest, der die Funktion transitiv über
|
|
12
|
+
// HTTP streift, färbt sie grün ohne eine einzige Assertion auf ihr Verhalten).
|
|
13
|
+
// Der Guard ist der strukturelle Gegenpart: er koppelt Test↔Modul über den
|
|
14
|
+
// echten Import und zählt namentliche Referenzen, nicht Zeilen. Der
|
|
15
|
+
// Coverage-Threshold in bunfig.toml bleibt daneben bestehen — beide zusammen,
|
|
16
|
+
// nie der Threshold allein.
|
|
17
|
+
//
|
|
18
|
+
// Ausgenommen: Dateien ohne exportierte Funktion (reine Typen/Konstanten wie
|
|
19
|
+
// eine Farb-Map — ein Test dafür wäre ein Fake-Test). IO-Loader, die bereits
|
|
20
|
+
// durch einen Integrationstest über HTTP gedeckt sind, tragen den ignore-Tag
|
|
21
|
+
// mit wahrheitsgemäßer Begründung.
|
|
22
|
+
//
|
|
23
|
+
// Bewusst out-of-scope: Re-Exports (`export { foo } from "./bar"`) zählen
|
|
24
|
+
// nicht als eigene Callable — die Quelldatei "./bar" trägt ihre eigene
|
|
25
|
+
// Coverage-Pflicht. Kommt lib/ je re-exportierte Funktionen ohne eigene
|
|
26
|
+
// Quelldatei im Scope vor, ist das ein stiller Blindspot; bisher (Stand
|
|
27
|
+
// dieser Fix-Runde) kommt das Pattern in keinem App-Repo vor.
|
|
28
|
+
|
|
29
|
+
import { dirname, resolve } from "node:path";
|
|
30
|
+
import { Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
31
|
+
import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
32
|
+
import { hasIgnoreTag } from "./_lib/ignore-tag";
|
|
33
|
+
|
|
34
|
+
// isTestFile() classifies by regex on the file's own path, not by which within-glob matched it, so lib/ vs. test classification stays correct regardless of scan-scope resolution.
|
|
35
|
+
const SCAN: ScanSpec = {
|
|
36
|
+
scope: "source",
|
|
37
|
+
extensions: ["ts", "tsx"],
|
|
38
|
+
kinds: ["library", "app"],
|
|
39
|
+
within: ["lib/**/*.ts", "features/*/lib/**/*.ts", "**/*.test.ts", "**/*.test.tsx"],
|
|
40
|
+
};
|
|
41
|
+
const IGNORE_TAG = "kumiko-lint-ignore lib-test-coverage";
|
|
42
|
+
|
|
43
|
+
type Export = { readonly name: string; readonly node: Node };
|
|
44
|
+
|
|
45
|
+
function isTestFile(path: string): boolean {
|
|
46
|
+
return /\.test\.tsx?$/.test(path) || /\/__tests__\//.test(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Exportierte Callables: `export function f` und `export const f = () => …` /
|
|
50
|
+
// `= function () {}`. Reine Werte (`export const X = 5`), Typen und Interfaces
|
|
51
|
+
// zählen nicht — sie tragen keine Logik, die ein Verhaltenstest prüfen könnte.
|
|
52
|
+
function exportedCallables(sf: SourceFile): Export[] {
|
|
53
|
+
const out: Export[] = [];
|
|
54
|
+
for (const fd of sf.getFunctions()) {
|
|
55
|
+
const name = fd.getName();
|
|
56
|
+
if (name !== undefined && (fd.isExported() || fd.isDefaultExport()))
|
|
57
|
+
out.push({ name, node: fd });
|
|
58
|
+
}
|
|
59
|
+
for (const vs of sf.getVariableStatements()) {
|
|
60
|
+
if (!vs.isExported()) continue;
|
|
61
|
+
for (const decl of vs.getDeclarations()) {
|
|
62
|
+
const init = decl.getInitializer();
|
|
63
|
+
if (init !== undefined && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
|
|
64
|
+
out.push({ name: decl.getName(), node: decl });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripExt(path: string): string {
|
|
72
|
+
return path.replace(/\.tsx?$/, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ein Test ist mit einer lib-Datei verknüpft, wenn er relativ aus genau dieser
|
|
76
|
+
// Datei importiert — nicht per Pfad-Konvention. Das erlaubt Tests eine Ebene
|
|
77
|
+
// über lib/ (features/<x>/__tests__/foo.test.ts → "../lib/foo") und verhindert,
|
|
78
|
+
// dass generische Namen (fieldText, targetForRow) in fremden Tests fälschlich
|
|
79
|
+
// als Referenz zählen.
|
|
80
|
+
function testImportsLib(testSf: SourceFile, libPathNoExt: string): boolean {
|
|
81
|
+
const testDir = dirname(testSf.getFilePath());
|
|
82
|
+
for (const imp of testSf.getImportDeclarations()) {
|
|
83
|
+
const spec = imp.getModuleSpecifierValue();
|
|
84
|
+
if (!spec.startsWith(".")) continue;
|
|
85
|
+
if (stripExt(resolve(testDir, spec)) === libPathNoExt) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Import-Deklarationen ausklammern: ein benannter Import ohne jede Nutzung
|
|
91
|
+
// (`import { addFees, subFees } from "../calc"`, nur addFees aufgerufen)
|
|
92
|
+
// erfuellte den Guard fuer subFees schon durch den Import-Text allein —
|
|
93
|
+
// der per-Funktions-Check war fuer named imports quasi tautologisch.
|
|
94
|
+
function bodyTextWithoutImports(testSf: SourceFile): string {
|
|
95
|
+
const importRanges = testSf
|
|
96
|
+
.getDescendantsOfKind(SyntaxKind.ImportDeclaration)
|
|
97
|
+
.map((d) => [d.getStart(), d.getEnd()] as const);
|
|
98
|
+
const full = testSf.getFullText();
|
|
99
|
+
let out = "";
|
|
100
|
+
let cursor = 0;
|
|
101
|
+
for (const [start, end] of importRanges) {
|
|
102
|
+
out += full.slice(cursor, start);
|
|
103
|
+
cursor = end;
|
|
104
|
+
}
|
|
105
|
+
out += full.slice(cursor);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function referencesName(testSf: SourceFile, name: string): boolean {
|
|
110
|
+
return new RegExp(`\\b${name}\\b`).test(bodyTextWithoutImports(testSf));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const guard: AstGuard = {
|
|
114
|
+
name: "Lib-Test-Coverage Guard (App-Repos)",
|
|
115
|
+
scan: SCAN,
|
|
116
|
+
hint:
|
|
117
|
+
"Jede lib-Datei mit exportierter Funktion braucht einen Test, der aus dem " +
|
|
118
|
+
"Modul importiert und jede Funktion namentlich referenziert. IO-Loader, die " +
|
|
119
|
+
`ein Integrationstest deckt: // ${IGNORE_TAG} <Grund, z.B. integration-covered via …>`,
|
|
120
|
+
run(files: readonly SourceFile[]) {
|
|
121
|
+
const violations: GuardViolation[] = [];
|
|
122
|
+
const testFiles: SourceFile[] = [];
|
|
123
|
+
const libFiles: SourceFile[] = [];
|
|
124
|
+
for (const sf of files) {
|
|
125
|
+
if (isTestFile(sf.getFilePath())) testFiles.push(sf);
|
|
126
|
+
else if (!sf.getFilePath().endsWith(".d.ts")) libFiles.push(sf);
|
|
127
|
+
}
|
|
128
|
+
for (const sf of libFiles) {
|
|
129
|
+
const relevant = exportedCallables(sf).filter((e) => !hasIgnoreTag(e.node, IGNORE_TAG));
|
|
130
|
+
if (relevant.length === 0) continue;
|
|
131
|
+
|
|
132
|
+
const libPathNoExt = stripExt(sf.getFilePath());
|
|
133
|
+
const linked = testFiles.filter((t) => testImportsLib(t, libPathNoExt));
|
|
134
|
+
if (linked.length === 0) {
|
|
135
|
+
violations.push({
|
|
136
|
+
file: sf.getFilePath(),
|
|
137
|
+
line: 1,
|
|
138
|
+
message: `Kein Test importiert dieses lib-Modul (${relevant.length} exportierte Funktion(en) ungetestet)`,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const e of relevant) {
|
|
144
|
+
if (linked.some((t) => referencesName(t, e.name))) continue;
|
|
145
|
+
violations.push({
|
|
146
|
+
file: sf.getFilePath(),
|
|
147
|
+
line: e.node.getStartLineNumber(),
|
|
148
|
+
message: `Exportierte Funktion "${e.name}" wird in keinem verknüpften Test namentlich referenziert`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { violations };
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (import.meta.main) runStandalone(guard);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Guard: blockt Aufrufe von `loadAllEventsByType()` ausserhalb von Tests,
|
|
4
|
+
* Ops-Scripts und der Definition selbst.
|
|
5
|
+
*
|
|
6
|
+
* `loadAllEventsByType` buffert ALLE Events eines aggregate_type in den
|
|
7
|
+
* Speicher (ein `SELECT … ORDER BY` ohne Limit). Jenseits ~100k Events pro
|
|
8
|
+
* Typ ist das ein OOM-Cliff, der erst unter Prod-Last auffällt — genau die
|
|
9
|
+
* Art still-und-spät-Fehler, die ein Guard fängt bevor sie ausgeliefert wird.
|
|
10
|
+
*
|
|
11
|
+
* Der memory-bounded Ersatz ist `streamAllEventsByType` (yield't batchweise,
|
|
12
|
+
* nie mehr als batchSize Rows resident). Production-Projection-Rebuild geht
|
|
13
|
+
* bereits über diesen Streaming-Pfad. `loadAllEventsByType` bleibt legitim
|
|
14
|
+
* für Tests (kleine, kontrollierte Stores) und Ops-Scripts auf bekannt
|
|
15
|
+
* kleinen aggregate_types — daher die Allowlist statt einer Entfernung.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun packages/guards/src/guard-loadall-events.ts
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { type CallExpression, type Identifier, type SourceFile, SyntaxKind } from "ts-morph";
|
|
23
|
+
import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
24
|
+
|
|
25
|
+
const ROOT = process.cwd();
|
|
26
|
+
|
|
27
|
+
const SCAN: ScanSpec = {
|
|
28
|
+
scope: "source",
|
|
29
|
+
extensions: ["ts"],
|
|
30
|
+
kinds: ["framework", "library"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Test-Dateien dürfen die API frei benutzen — sie sind die primären
|
|
34
|
+
// Verifizierer und laufen gegen kleine, kontrollierte Stores.
|
|
35
|
+
const EXCLUDE = /(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$)/;
|
|
36
|
+
|
|
37
|
+
// Erlaubte Aufrufer: die Definition selbst + Ops-/Migration-Scripts (laufen
|
|
38
|
+
// auf bekannt kleinen aggregate_types, nicht im Prod-Hot-Path).
|
|
39
|
+
// ROOT ist gegen process.cwd() (Repo-Root) verankert, `^`-Anker matcht nur
|
|
40
|
+
// echte Top-Level-scripts/, nicht packages/.../scripts/*.
|
|
41
|
+
const ALLOWLIST: readonly RegExp[] = [
|
|
42
|
+
/^packages\/framework\/src\/event-store\/event-store\.ts$/,
|
|
43
|
+
/^scripts\//,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const GUARDED_CALLS = new Set(["loadAllEventsByType"]);
|
|
47
|
+
|
|
48
|
+
export interface Violation {
|
|
49
|
+
file: string;
|
|
50
|
+
line: number;
|
|
51
|
+
functionName: string;
|
|
52
|
+
enclosingFunction: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isAllowed(relativePath: string): boolean {
|
|
56
|
+
return ALLOWLIST.some((re) => re.test(relativePath));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function collectViolations(sourceFile: SourceFile): Violation[] {
|
|
60
|
+
const violations: Violation[] = [];
|
|
61
|
+
const relativePath = path.relative(ROOT, sourceFile.getFilePath());
|
|
62
|
+
if (isAllowed(relativePath)) return violations;
|
|
63
|
+
|
|
64
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
65
|
+
for (const call of calls) {
|
|
66
|
+
const fnName = getCalleeName(call);
|
|
67
|
+
if (!fnName || !GUARDED_CALLS.has(fnName)) continue;
|
|
68
|
+
violations.push({
|
|
69
|
+
file: relativePath,
|
|
70
|
+
line: call.getStartLineNumber(),
|
|
71
|
+
functionName: fnName,
|
|
72
|
+
enclosingFunction: findEnclosingName(call),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return violations;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Extract the called name. Handles:
|
|
79
|
+
// loadAllEventsByType(...) → "loadAllEventsByType" (Identifier)
|
|
80
|
+
// someNamespace.loadAllEventsByType(...) → "loadAllEventsByType" (PropertyAccess)
|
|
81
|
+
// Any other callee shape returns null.
|
|
82
|
+
function getCalleeName(call: CallExpression): string | null {
|
|
83
|
+
const expr = call.getExpression();
|
|
84
|
+
if (expr.getKind() === SyntaxKind.Identifier) {
|
|
85
|
+
return (expr as Identifier).getText();
|
|
86
|
+
}
|
|
87
|
+
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
88
|
+
return expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression).getName();
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findEnclosingName(call: CallExpression): string {
|
|
94
|
+
let cur = call.getParent();
|
|
95
|
+
while (cur) {
|
|
96
|
+
if (cur.isKind(SyntaxKind.FunctionDeclaration) || cur.isKind(SyntaxKind.MethodDeclaration)) {
|
|
97
|
+
return cur.getName() ?? "<anonymous>";
|
|
98
|
+
}
|
|
99
|
+
if (cur.isKind(SyntaxKind.FunctionExpression) || cur.isKind(SyntaxKind.ArrowFunction)) {
|
|
100
|
+
const parent = cur.getParent();
|
|
101
|
+
if (parent?.isKind(SyntaxKind.VariableDeclaration)) return parent.getName();
|
|
102
|
+
if (parent?.isKind(SyntaxKind.PropertyAssignment)) return parent.getName();
|
|
103
|
+
return "<anonymous>";
|
|
104
|
+
}
|
|
105
|
+
cur = cur.getParent();
|
|
106
|
+
}
|
|
107
|
+
return "<top-level>";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const guard: AstGuard = {
|
|
111
|
+
name: "loadAllEventsByType Guard",
|
|
112
|
+
scan: SCAN,
|
|
113
|
+
hint:
|
|
114
|
+
"loadAllEventsByType buffers ALL events of an aggregate_type in memory (OOM cliff > ~100k events). " +
|
|
115
|
+
"Use streamAllEventsByType (batched, memory-bounded) in production code. " +
|
|
116
|
+
"Allowed only in tests, scripts/, and the event-store definition.",
|
|
117
|
+
run(files) {
|
|
118
|
+
const violations: GuardViolation[] = [];
|
|
119
|
+
for (const sf of files) {
|
|
120
|
+
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
121
|
+
for (const v of collectViolations(sf)) {
|
|
122
|
+
violations.push({
|
|
123
|
+
file: v.file,
|
|
124
|
+
line: v.line,
|
|
125
|
+
message: `${v.functionName}(...) in ${v.enclosingFunction}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { violations };
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (import.meta.main) runStandalone(guard);
|
|
@@ -85,11 +85,11 @@ const BASELINE_PATH = path.join(ROOT, BASELINE_FILE);
|
|
|
85
85
|
const rawFormHtmlBaseline = baselineRatchet({
|
|
86
86
|
file: BASELINE_PATH,
|
|
87
87
|
formatVersion: 1,
|
|
88
|
-
unit: "
|
|
88
|
+
unit: "raw form HTML finding(s)",
|
|
89
89
|
});
|
|
90
90
|
const RAW_FORM_HTML_REMEDIATION =
|
|
91
|
-
"
|
|
92
|
-
`
|
|
91
|
+
"Use a framework widget (@cosmicdrift/kumiko-renderer-web: Field/Input, ComboboxInput, …) " +
|
|
92
|
+
`or usePrimitives(). Real exception: // ${IGNORE_TAG} <reason>`;
|
|
93
93
|
|
|
94
94
|
// The rule is new: measured backlog is 0 in every locally checked repo
|
|
95
95
|
// (infra#748). Without a baseline file, check fail-closed against an empty
|
|
@@ -105,12 +105,11 @@ function checkRawFormHtmlBaseline(findings: readonly RawFormFinding[]): GuardVio
|
|
|
105
105
|
return regressions.map((r) => ({
|
|
106
106
|
file: r.file,
|
|
107
107
|
line: resolveLine(r.file),
|
|
108
|
-
message: `${r.current}
|
|
108
|
+
message: `${r.current} raw form HTML finding(s) (no baseline — existing backlog is 0). ${RAW_FORM_HTML_REMEDIATION}`,
|
|
109
109
|
}));
|
|
110
110
|
}
|
|
111
111
|
return rawFormHtmlBaseline.check(current, RAW_FORM_HTML_REMEDIATION, {
|
|
112
|
-
formatDriftRemediation:
|
|
113
|
-
"Einmalig `bun guards/guard-no-custom-primitives.ts --write-baseline` aufrufen.",
|
|
112
|
+
formatDriftRemediation: "Run `bun guards/guard-no-custom-primitives.ts --write-baseline` once.",
|
|
114
113
|
resolveLine,
|
|
115
114
|
});
|
|
116
115
|
}
|
|
@@ -149,13 +148,13 @@ function analyse(
|
|
|
149
148
|
violations.push({
|
|
150
149
|
file: sf.getFilePath(),
|
|
151
150
|
line: c.line,
|
|
152
|
-
message: `App-
|
|
151
|
+
message: `App-local UI primitive "${c.name}" — use a framework widget/primitive`,
|
|
153
152
|
});
|
|
154
153
|
}
|
|
155
154
|
rawFormFindings.push(...collectRawFormHtml(sf));
|
|
156
155
|
}
|
|
157
156
|
if (!compareBaseline) {
|
|
158
|
-
console.log(" Baseline
|
|
157
|
+
console.log(" Baseline comparison skipped (--no-baseline).");
|
|
159
158
|
} else {
|
|
160
159
|
violations.push(...checkRawFormHtmlBaseline(rawFormFindings));
|
|
161
160
|
}
|
|
@@ -166,8 +165,8 @@ export const guard: AstGuard = {
|
|
|
166
165
|
name: "No-Custom-Primitives Guard (App-Repos)",
|
|
167
166
|
scan: SCAN,
|
|
168
167
|
hint:
|
|
169
|
-
"
|
|
170
|
-
`
|
|
168
|
+
"Use a framework widget (@cosmicdrift/kumiko-renderer-web: StatCard, SectionCard, StatusBadge, QueryTable, Charts, …) " +
|
|
169
|
+
`or usePrimitives(). Real domain component without a framework equivalent: // ${IGNORE_TAG} <reason>`,
|
|
171
170
|
run: (files: readonly SourceFile[]) => analyse(files, true),
|
|
172
171
|
};
|
|
173
172
|
|
package/src/guard-no-date-api.ts
CHANGED
|
@@ -162,7 +162,7 @@ function findDateApiUsages(sf: SourceFile): Omit<Violation, "file">[] {
|
|
|
162
162
|
export const guard: AstGuard = {
|
|
163
163
|
name: "No-Date-API Guard",
|
|
164
164
|
scan: SCAN,
|
|
165
|
-
hint: "
|
|
165
|
+
hint: "Replace with Temporal.Now.instant() / Temporal.Instant.from / .toString() / .epochMilliseconds — see docs/plans/architecture/timezones.md.",
|
|
166
166
|
run(files) {
|
|
167
167
|
const violations: Array<{ file: string; line: number; message: string }> = [];
|
|
168
168
|
|
|
@@ -203,7 +203,7 @@ export const guard: AstGuard = {
|
|
|
203
203
|
name: "No-Direct-Fs Guard",
|
|
204
204
|
scan: SCAN,
|
|
205
205
|
security: true,
|
|
206
|
-
hint: "
|
|
206
|
+
hint: "Direct node:fs import outside the allowlist — use FileStorageProvider (packages/framework/src/files/) instead of wiring fs yourself. The path-traversal guard (resolveContainedPath) only exists there. Legitimate new tooling caller? Extend the allowlist in guard-no-direct-fs.ts, with a reason + repo scope.",
|
|
207
207
|
run(files) {
|
|
208
208
|
const violations: Array<{ file: string; line: number; message: string }> = [];
|
|
209
209
|
const roots = resolveRepoRoots();
|
|
@@ -20,8 +20,8 @@ export const guard: AstGuard = {
|
|
|
20
20
|
name: "No-Inline-Styles Guard (App-Repos)",
|
|
21
21
|
scan: SCAN,
|
|
22
22
|
hint:
|
|
23
|
-
"style=/CSSProperties in
|
|
24
|
-
`
|
|
23
|
+
"Replace style=/CSSProperties in app code with widgets + theme tokens. " +
|
|
24
|
+
`Justified exception (e.g. dynamic width from data): // ${IGNORE_TAG} <reason>`,
|
|
25
25
|
run(files: readonly SourceFile[]) {
|
|
26
26
|
const violations: GuardViolation[] = [];
|
|
27
27
|
for (const sf of files) {
|
|
@@ -32,7 +32,7 @@ export const guard: AstGuard = {
|
|
|
32
32
|
violations.push({
|
|
33
33
|
file: sf.getFilePath(),
|
|
34
34
|
line: attr.getStartLineNumber(),
|
|
35
|
-
message: "style
|
|
35
|
+
message: "style= prop in app code (use theme tokens/widgets)",
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
// ponytail: name-text comparison doesn't tolerate aliased imports
|
|
@@ -47,7 +47,7 @@ export const guard: AstGuard = {
|
|
|
47
47
|
violations.push({
|
|
48
48
|
file: sf.getFilePath(),
|
|
49
49
|
line: ref.getStartLineNumber(),
|
|
50
|
-
message: "CSSProperties
|
|
50
|
+
message: "CSSProperties style object in app code (use theme tokens/widgets)",
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -124,8 +124,8 @@ export const guard: AstGuard = {
|
|
|
124
124
|
name: "No-Logic-in-Views Guard (App-Repos)",
|
|
125
125
|
scan: SCAN,
|
|
126
126
|
hint:
|
|
127
|
-
"
|
|
128
|
-
`
|
|
127
|
+
"Computation/parsing/aggregation belongs in lib/ (with a test) — web/ holds only " +
|
|
128
|
+
`components and hooks. Justified exception: // ${IGNORE_TAG} <reason>`,
|
|
129
129
|
run(files: readonly SourceFile[]) {
|
|
130
130
|
const violations: GuardViolation[] = [];
|
|
131
131
|
for (const sf of files) {
|
|
@@ -136,7 +136,7 @@ export const guard: AstGuard = {
|
|
|
136
136
|
violations.push({
|
|
137
137
|
file: sf.getFilePath(),
|
|
138
138
|
line: fn.getStartLineNumber(),
|
|
139
|
-
message: `View
|
|
139
|
+
message: `View logic "${name}" belongs in lib/ (with a test) — web/ only components/hooks`,
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -40,8 +40,8 @@ export const guard: AstGuard = {
|
|
|
40
40
|
name: "No-Raw-Hooks Guard (App-Repos)",
|
|
41
41
|
scan: SCAN,
|
|
42
42
|
hint:
|
|
43
|
-
"
|
|
44
|
-
`
|
|
43
|
+
"Use the framework hook set: useQuery (live: true for SSE), useMutation, useDisclosure. " +
|
|
44
|
+
`Real special case (DOM integration or similar): // ${IGNORE_TAG} <reason>`,
|
|
45
45
|
run(files: readonly SourceFile[]) {
|
|
46
46
|
const violations: GuardViolation[] = [];
|
|
47
47
|
for (const sf of files) {
|
|
@@ -54,7 +54,7 @@ export const guard: AstGuard = {
|
|
|
54
54
|
violations.push({
|
|
55
55
|
file: sf.getFilePath(),
|
|
56
56
|
line: call.getStartLineNumber(),
|
|
57
|
-
message: `${name} in App-Screen —
|
|
57
|
+
message: `${name} in App-Screen — use framework hooks (useQuery/useMutation/useDisclosure)`,
|
|
58
58
|
});
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
@@ -63,8 +63,7 @@ export const guard: AstGuard = {
|
|
|
63
63
|
violations.push({
|
|
64
64
|
file: sf.getFilePath(),
|
|
65
65
|
line: call.getStartLineNumber(),
|
|
66
|
-
message:
|
|
67
|
-
"fetch() in App-Screen — useQuery/useMutation bzw. einen Api-Client (*.ts) nutzen",
|
|
66
|
+
message: "fetch() in App-Screen — use useQuery/useMutation or an API client (*.ts)",
|
|
68
67
|
});
|
|
69
68
|
}
|
|
70
69
|
}
|
|
@@ -85,7 +85,7 @@ export function createOpenToAllReasonGuard(opts: { root: string }): AstGuard {
|
|
|
85
85
|
name: "Open-To-All-Reason Guard",
|
|
86
86
|
scan: SCAN,
|
|
87
87
|
security: true,
|
|
88
|
-
hint: 'openToAll: { reason: "<why any authenticated user may call this>" }
|
|
88
|
+
hint: 'Provide openToAll: { reason: "<why any authenticated user may call this>" } (+ personalData: "tenant-members" for write handlers with unbound personal data). Baseline after a deliberate reduction: `bun guards/run-guards.ts --write-security-baseline`',
|
|
89
89
|
run(files) {
|
|
90
90
|
const violations: GuardViolation[] = [
|
|
91
91
|
...findGenericOpenToAllReasons(files, opts.root).map((f) => ({
|