@cosmicdrift/kumiko-guards 0.385.0 → 0.387.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/LICENSE +21 -57
- package/package.json +4 -4
- package/src/_lib/comment-trivia.ts +148 -0
- package/src/_lib/guard-kit.ts +2 -2
- package/src/_lib/scan-scope.ts +1 -1
- package/src/_lib/sql-inventory.ts +2 -2
- package/src/changes.json +7 -0
- package/src/check-as-casts.ts +6 -7
- package/src/check-complexity.ts +0 -2
- package/src/check-real-provider-isolation.ts +1 -1
- package/src/check-runtime-isolation.ts +2 -2
- package/src/check-single-runtime-instance.ts +1 -1
- package/src/guard-admin-api.ts +5 -6
- package/src/guard-app-feature-structure.ts +0 -2
- package/src/guard-comment-issue-ref-only.ts +221 -0
- package/src/guard-comment-lang.ts +4 -135
- package/src/guard-direct-fetch.ts +3 -3
- package/src/guard-feature-integration-tests.ts +1 -1
- package/src/guard-i18n-locale-mount.ts +4 -4
- package/src/guard-i18n-locale-terminology.ts +3 -3
- package/src/guard-i18n-ui-strings.ts +5 -5
- package/src/guard-no-custom-primitives.ts +2 -4
- package/src/guard-no-direct-process-env.ts +2 -2
- package/src/guard-no-inline-styles.ts +1 -1
- package/src/guard-no-raw-hooks.ts +0 -2
- package/src/guard-primitives-discipline.ts +1 -1
- package/src/guard-raw-classname.ts +1 -1
- package/src/guard-raw-interactive-elements.ts +1 -1
- package/src/guard-restricted-symbols.ts +2 -3
- package/src/guard-tailwind-scan-surface.ts +28 -29
- package/src/guard-tenant-escalation.ts +3 -3
- package/src/guard-test-template-drift.ts +1 -1
- package/src/guard-test-timeouts.ts +1 -1
- package/src/guard-write-handler-qns.ts +1 -1
- package/src/run-guards.ts +2 -0
- package/src/run-ui-guards.ts +1 -1
- package/src/runtime-isolation-classify.ts +1 -1
package/LICENSE
CHANGED
|
@@ -1,57 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
|
-
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
-
Licensed Work for any purpose, including for the operation of kumiko.rocks.
|
|
25
|
-
|
|
26
|
-
Change Date: 2030-05-05
|
|
27
|
-
Change License: Apache License, Version 2.0
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Terms
|
|
31
|
-
|
|
32
|
-
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
33
|
-
redistribute, and make non-production use of the Licensed Work. The Licensor may
|
|
34
|
-
make an Additional Use Grant, above, permitting limited production use.
|
|
35
|
-
|
|
36
|
-
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
37
|
-
available distribution of the Licensed Work under this License, whichever comes
|
|
38
|
-
first, this License will convert to the Change License.
|
|
39
|
-
|
|
40
|
-
This Business Source License governs use of the Licensed Work in all cases, except
|
|
41
|
-
as to any use that is explicitly granted in the Additional Use Grant above or
|
|
42
|
-
under the Change License after the Change Date.
|
|
43
|
-
|
|
44
|
-
If your use of the Licensed Work does not comply with the requirements of this
|
|
45
|
-
License, you must cease use of the Licensed Work immediately.
|
|
46
|
-
|
|
47
|
-
All copies of the Licensed Work, and all derivative works thereof, must include
|
|
48
|
-
this License.
|
|
49
|
-
|
|
50
|
-
This License does not grant you any right, title, or interest in any trademark,
|
|
51
|
-
logo, or branding of the Licensor, except as required to comply with this License.
|
|
52
|
-
|
|
53
|
-
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
54
|
-
“AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
55
|
-
WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
56
|
-
TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
|
|
57
|
-
ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Frost
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.387.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"LICENSE"
|
|
52
52
|
],
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
54
|
+
"@cosmicdrift/kumiko-framework": "0.387.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@cosmicdrift/kumiko-framework": {
|
|
@@ -59,6 +59,6 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
62
|
+
"@cosmicdrift/kumiko-framework": "0.387.0"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ts } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
export const COMMENT_KIND_LINE = "line";
|
|
4
|
+
export const COMMENT_KIND_BLOCK = "block";
|
|
5
|
+
|
|
6
|
+
export type CommentTrivia = {
|
|
7
|
+
readonly start: number;
|
|
8
|
+
readonly kind: typeof COMMENT_KIND_LINE | typeof COMMENT_KIND_BLOCK;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Tokens that end an operand without being identifiers or keywords.
|
|
13
|
+
const OPERAND_END_PUNCTUATION_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
14
|
+
ts.SyntaxKind.NumericLiteral,
|
|
15
|
+
ts.SyntaxKind.BigIntLiteral,
|
|
16
|
+
ts.SyntaxKind.StringLiteral,
|
|
17
|
+
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
18
|
+
ts.SyntaxKind.TemplateTail,
|
|
19
|
+
ts.SyntaxKind.RegularExpressionLiteral,
|
|
20
|
+
ts.SyntaxKind.CloseParenToken,
|
|
21
|
+
ts.SyntaxKind.CloseBracketToken,
|
|
22
|
+
ts.SyntaxKind.CloseBraceToken,
|
|
23
|
+
ts.SyntaxKind.PlusPlusToken,
|
|
24
|
+
ts.SyntaxKind.MinusMinusToken,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Reserved words after which an expression (so possibly a regex) starts.
|
|
28
|
+
const EXPRESSION_INTRODUCING_KEYWORDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
29
|
+
ts.SyntaxKind.ReturnKeyword,
|
|
30
|
+
ts.SyntaxKind.TypeOfKeyword,
|
|
31
|
+
ts.SyntaxKind.InstanceOfKeyword,
|
|
32
|
+
ts.SyntaxKind.InKeyword,
|
|
33
|
+
ts.SyntaxKind.CaseKeyword,
|
|
34
|
+
ts.SyntaxKind.VoidKeyword,
|
|
35
|
+
ts.SyntaxKind.DeleteKeyword,
|
|
36
|
+
ts.SyntaxKind.ThrowKeyword,
|
|
37
|
+
ts.SyntaxKind.NewKeyword,
|
|
38
|
+
ts.SyntaxKind.YieldKeyword,
|
|
39
|
+
ts.SyntaxKind.AwaitKeyword,
|
|
40
|
+
ts.SyntaxKind.ElseKeyword,
|
|
41
|
+
ts.SyntaxKind.DoKeyword,
|
|
42
|
+
ts.SyntaxKind.ExtendsKeyword,
|
|
43
|
+
ts.SyntaxKind.DefaultKeyword,
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function isIdentifierOrKeyword(kind: ts.SyntaxKind): boolean {
|
|
47
|
+
return (
|
|
48
|
+
kind === ts.SyntaxKind.Identifier ||
|
|
49
|
+
(kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// After an operand end a `/` is division, not the start of a regex literal.
|
|
54
|
+
// Contextual keywords (`from`, `type`, `of`, ...) scan as keyword kinds but are
|
|
55
|
+
// valid member and variable names, so every non-introducing keyword counts. A `!`
|
|
56
|
+
// is the non-null postfix only when the token before it ended an operand.
|
|
57
|
+
function isOperandEnd(kind: ts.SyntaxKind, previousEndedOperand: boolean): boolean {
|
|
58
|
+
if (kind === ts.SyntaxKind.ExclamationToken) return previousEndedOperand;
|
|
59
|
+
if (OPERAND_END_PUNCTUATION_KINDS.has(kind)) return true;
|
|
60
|
+
return isIdentifierOrKeyword(kind) && !EXPRESSION_INTRODUCING_KEYWORDS.has(kind);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TRIVIA_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
64
|
+
ts.SyntaxKind.SingleLineCommentTrivia,
|
|
65
|
+
ts.SyntaxKind.MultiLineCommentTrivia,
|
|
66
|
+
ts.SyntaxKind.WhitespaceTrivia,
|
|
67
|
+
ts.SyntaxKind.NewLineTrivia,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// The raw scanner always yields SlashToken for `/`; without the rescan a regex
|
|
71
|
+
// body like /\{/ would shift braceDepth and end a template early. After an
|
|
72
|
+
// operand the rescan would instead swallow `/ 2; // {` as a regex and hide the comment.
|
|
73
|
+
function rescanSlashAsRegex(
|
|
74
|
+
scanner: ts.Scanner,
|
|
75
|
+
kind: ts.SyntaxKind,
|
|
76
|
+
previousEndedOperand: boolean,
|
|
77
|
+
): ts.SyntaxKind {
|
|
78
|
+
if (previousEndedOperand) return kind;
|
|
79
|
+
const reScanned = scanner.reScanSlashToken();
|
|
80
|
+
return reScanned === ts.SyntaxKind.RegularExpressionLiteral ? reScanned : kind;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function closesTemplateSubstitution(
|
|
84
|
+
templateBraceDepths: readonly number[],
|
|
85
|
+
braceDepth: number,
|
|
86
|
+
): boolean {
|
|
87
|
+
return (
|
|
88
|
+
templateBraceDepths.length > 0 &&
|
|
89
|
+
braceDepth === templateBraceDepths[templateBraceDepths.length - 1]
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Template substitutions can nest braces (object literals, arrow bodies, other
|
|
94
|
+
// templates). The raw scanner tokenizes a `}` as a plain CloseBraceToken and
|
|
95
|
+
// never resumes template mode, so reScanTemplateToken() must run once brace
|
|
96
|
+
// depth returns to the level the substitution opened at.
|
|
97
|
+
export function scanCommentTrivia(fullText: string): CommentTrivia[] {
|
|
98
|
+
const comments: CommentTrivia[] = [];
|
|
99
|
+
const scanner = ts.createScanner(
|
|
100
|
+
ts.ScriptTarget.Latest,
|
|
101
|
+
/* skipTrivia */ false,
|
|
102
|
+
ts.LanguageVariant.Standard,
|
|
103
|
+
fullText,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const templateBraceDepths: number[] = [];
|
|
107
|
+
let braceDepth = 0;
|
|
108
|
+
let previousEndedOperand = false;
|
|
109
|
+
|
|
110
|
+
let kind = scanner.scan();
|
|
111
|
+
while (kind !== ts.SyntaxKind.EndOfFileToken) {
|
|
112
|
+
switch (kind) {
|
|
113
|
+
case ts.SyntaxKind.SingleLineCommentTrivia:
|
|
114
|
+
case ts.SyntaxKind.MultiLineCommentTrivia:
|
|
115
|
+
comments.push({
|
|
116
|
+
start: scanner.getTokenStart(),
|
|
117
|
+
kind:
|
|
118
|
+
kind === ts.SyntaxKind.SingleLineCommentTrivia ? COMMENT_KIND_LINE : COMMENT_KIND_BLOCK,
|
|
119
|
+
text: scanner.getTokenText(),
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
case ts.SyntaxKind.SlashToken:
|
|
123
|
+
case ts.SyntaxKind.SlashEqualsToken:
|
|
124
|
+
kind = rescanSlashAsRegex(scanner, kind, previousEndedOperand);
|
|
125
|
+
break;
|
|
126
|
+
case ts.SyntaxKind.TemplateHead:
|
|
127
|
+
case ts.SyntaxKind.TemplateMiddle:
|
|
128
|
+
templateBraceDepths.push(braceDepth);
|
|
129
|
+
break;
|
|
130
|
+
case ts.SyntaxKind.OpenBraceToken:
|
|
131
|
+
braceDepth++;
|
|
132
|
+
break;
|
|
133
|
+
case ts.SyntaxKind.CloseBraceToken:
|
|
134
|
+
if (closesTemplateSubstitution(templateBraceDepths, braceDepth)) {
|
|
135
|
+
templateBraceDepths.pop();
|
|
136
|
+
kind = scanner.reScanTemplateToken(false);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (braceDepth > 0) braceDepth--;
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (!TRIVIA_KINDS.has(kind)) previousEndedOperand = isOperandEnd(kind, previousEndedOperand);
|
|
145
|
+
kind = scanner.scan();
|
|
146
|
+
}
|
|
147
|
+
return comments;
|
|
148
|
+
}
|
package/src/_lib/guard-kit.ts
CHANGED
|
@@ -339,7 +339,7 @@ export type RunResult = {
|
|
|
339
339
|
readonly scanKinds?: readonly string[];
|
|
340
340
|
/** Files the scan matched — the runner's own count, basis of the floor. */
|
|
341
341
|
readonly matchedFiles?: number;
|
|
342
|
-
/** Roots whose declared sourceRoots hold no .ts/.tsx at all
|
|
342
|
+
/** Roots whose declared sourceRoots hold no .ts/.tsx at all. */
|
|
343
343
|
readonly violatingRoots?: readonly string[];
|
|
344
344
|
/** Violations excused by the per-repo security baseline — security guards only. */
|
|
345
345
|
readonly frozenFindings?: number;
|
|
@@ -597,7 +597,7 @@ function guardKitVersion(): string {
|
|
|
597
597
|
|
|
598
598
|
/**
|
|
599
599
|
* Pure preflight verdict, injectable for tests: which of the two globally
|
|
600
|
-
* silent-green cases
|
|
600
|
+
* silent-green cases applies, if any. Not to be confused with a
|
|
601
601
|
* single guard finding no target repos — that stays a per-guard `skipped`
|
|
602
602
|
* line in `reportResults`.
|
|
603
603
|
*/
|
package/src/_lib/scan-scope.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type RootScan = {
|
|
|
35
35
|
readonly root: RepoRoot;
|
|
36
36
|
/** Absolute paths handed to the guard, sorted. */
|
|
37
37
|
readonly files: readonly string[];
|
|
38
|
-
/** All .ts/.tsx under the declared sourceRoots minus excludes, before extension/within filters — the
|
|
38
|
+
/** All .ts/.tsx under the declared sourceRoots minus excludes, before extension/within filters — the floor input. */
|
|
39
39
|
readonly sourceSurface: number;
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -58,7 +58,7 @@ export type SqlInventoryReport = {
|
|
|
58
58
|
};
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
/** Paths where `.unsafe()` / `asRawClient()` are permitted
|
|
61
|
+
/** Paths where `.unsafe()` / `asRawClient()` are permitted. */
|
|
62
62
|
export const RAW_SQL_ALLOWLIST: ReadonlyArray<RegExp> = [
|
|
63
63
|
// Layout-agnostic: framework packages/... AND flat app src/... AND enterprise packages/<pkg>/...
|
|
64
64
|
/(^|\/)(packages\/[^/]+\/)?src\/db\/queries\//,
|
|
@@ -107,7 +107,7 @@ const PATTERNS: ReadonlyArray<{
|
|
|
107
107
|
{ kind: "execute", re: /\.execute\s*\(/ },
|
|
108
108
|
];
|
|
109
109
|
|
|
110
|
-
/** Only these kinds fail CI; delete_from/execute stay inventory-only
|
|
110
|
+
/** Only these kinds fail CI; delete_from/execute stay inventory-only. */
|
|
111
111
|
export const BLOCKING_SQL_KINDS: ReadonlyArray<SqlInventoryKind> = ["unsafe", "asRawClient"];
|
|
112
112
|
|
|
113
113
|
const TS_GLOB = new Bun.Glob("**/*.{ts,tsx}");
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.387.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Comment Issue-Ref guard",
|
|
6
|
+
"detail": "The comment-issue-ref-only guard flags comments that consist only of an issue or PR reference or a planning label (`#1234`, `fw#1234`, `Phase 3`, `S2.H1`) with no explanatory word besides it. Consecutive own-line `//` comments count as one paragraph, so a reference at the end of an explaining paragraph passes. Hits are frozen per file in `.kumiko-comment-issue-ref-baseline.json` and may not grow; without the baseline file the guard only warns. Deliberate exceptions take `// kumiko-lint-ignore comment-issue-ref-only <reason>`.",
|
|
7
|
+
"migration": "Optional. Freeze existing hits with `kumiko-guards guards --write-baseline --guard=comment-issue-ref-only`."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.385.0",
|
|
4
11
|
"type": "improvement",
|
package/src/check-as-casts.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function isConstAssertion(cast: AsExpression): boolean {
|
|
|
117
117
|
// optional ParenthesizedExpression in between (depending on parenthesisation):
|
|
118
118
|
// `x as unknown as Y` → AsExpr(target=Y) → AsExpr(target=unknown) → x
|
|
119
119
|
// `(x as unknown) as Y` → AsExpr(target=Y) → ParenExpr → AsExpr(target=unknown) → x
|
|
120
|
-
//
|
|
120
|
+
// Traverse parentheses transparently.
|
|
121
121
|
function unwrapParens(node: Node | undefined): Node | undefined {
|
|
122
122
|
while (node?.getKind() === SyntaxKind.ParenthesizedExpression) {
|
|
123
123
|
node = node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression();
|
|
@@ -277,7 +277,7 @@ function restOfLine(fullText: string, from: number): string {
|
|
|
277
277
|
export function extractBoundaryReason(cast: AsExpression): string | null {
|
|
278
278
|
const fullText = cast.getSourceFile().getFullText();
|
|
279
279
|
|
|
280
|
-
// (1) Leading comments
|
|
280
|
+
// (1) Leading comments of the enclosing statement
|
|
281
281
|
const stmt =
|
|
282
282
|
cast.getFirstAncestorByKind(SyntaxKind.VariableStatement) ??
|
|
283
283
|
cast.getFirstAncestorByKind(SyntaxKind.ExpressionStatement) ??
|
|
@@ -288,7 +288,7 @@ export function extractBoundaryReason(cast: AsExpression): string | null {
|
|
|
288
288
|
if (reason !== null) return reason;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// (2) Cast
|
|
291
|
+
// (2) Cast range itself (multi-line casts, inline comments inside)
|
|
292
292
|
const inlineReason = boundaryReasonIn(fullText.slice(cast.getStart(), cast.getEnd()));
|
|
293
293
|
if (inlineReason !== null) return inlineReason;
|
|
294
294
|
|
|
@@ -327,9 +327,8 @@ const FILE_DEFAULT_REASONS: ReadonlyArray<{
|
|
|
327
327
|
boundaryReason: BoundaryReason;
|
|
328
328
|
}> = [
|
|
329
329
|
{
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// NavDef, ConfigKeys, …). Per Konstruktion ein schema-walk.
|
|
330
|
+
// Feature-AST extractors cast erased ts-morph parse results to typed feature definitions
|
|
331
|
+
// (EntityDefinition, RelationDef, NavDef, ConfigKeys, …), so every cast is a schema walk.
|
|
333
332
|
pattern: /\/engine\/feature-ast\//,
|
|
334
333
|
boundaryReason: "schema-walk",
|
|
335
334
|
},
|
|
@@ -605,7 +604,7 @@ function writeBaseline(all: readonly Site[]): void {
|
|
|
605
604
|
legitRatchet.write(legitCountsByPackage(all));
|
|
606
605
|
}
|
|
607
606
|
|
|
608
|
-
// Per
|
|
607
|
+
// Per file:target: current count against the baseline. More → regression.
|
|
609
608
|
// A cast swap (cast A removed, cast B with a different target added) is detected
|
|
610
609
|
// this way although the total stays stable.
|
|
611
610
|
// Merge-conflict markers, truncated writes and hand edits all land here; the
|
package/src/check-complexity.ts
CHANGED
|
@@ -27,8 +27,6 @@
|
|
|
27
27
|
* bun guards/check-complexity.ts # compare against baseline
|
|
28
28
|
* bun guards/check-complexity.ts --write-baseline # rewrite baseline
|
|
29
29
|
* bun guards/check-complexity.ts --no-baseline # skip comparison
|
|
30
|
-
*
|
|
31
|
-
* Rule: issue kumiko-framework#1282
|
|
32
30
|
*/
|
|
33
31
|
|
|
34
32
|
import * as path from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Real-Provider-Isolation-Guard (stack-
|
|
3
|
+
* Real-Provider-Isolation-Guard (stack-agnostic).
|
|
4
4
|
*
|
|
5
5
|
* Real-provider specs (`*.real.test.ts`, `*.real.spec.ts`) hit live third-party
|
|
6
6
|
* services and must only ever run opt-in (manually or scheduled) via `bun run test:real` /
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
// A file/import edge classified "client" importing "runtime" can be a real
|
|
44
44
|
// finding this guard needs to see, but can also be a known, reviewed
|
|
45
45
|
// exception (e.g. samples/recipes are worked examples, not the framework's
|
|
46
|
-
// own production surface —
|
|
47
|
-
//
|
|
46
|
+
// own production surface — one exception is currently frozen here).
|
|
47
|
+
// Exceptions are tracked via a per-repo baseline file
|
|
48
48
|
// (`.kumiko-runtime-isolation-baseline.json`, same `baselineRatchet`
|
|
49
49
|
// mechanism as check-complexity/etc.), not a
|
|
50
50
|
// hardcoded list in this source file: a consumer repo can freeze or clear
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Single-Runtime-Instance Guard
|
|
3
|
+
* Single-Runtime-Instance Guard.
|
|
4
4
|
*
|
|
5
5
|
* A Kumiko runtime library resolved at two versions in one bun.lock (a nested
|
|
6
6
|
* copy under a feature package next to the top-level install) loads twice in
|
package/src/guard-admin-api.ts
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* Guard: blocks calls to the event-store admin API (`appendRaw`/`appendRawBatch`)
|
|
4
4
|
* outside allowed paths.
|
|
5
5
|
*
|
|
6
|
-
* The admin API is a Marten bypass for legacy data imports
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* next projection rebuild, or never).
|
|
6
|
+
* The admin API is a Marten bypass for legacy data imports. It BYPASSES the
|
|
7
|
+
* pipeline — no projections, no postSave hooks, no SSE/search/audit. Used
|
|
8
|
+
* accidentally from application code, that causes state inconsistencies that
|
|
9
|
+
* surface very late (at the next projection rebuild, or never).
|
|
11
10
|
*
|
|
12
11
|
* Second line of defense next to the deep-import path: even if someone pulls
|
|
13
12
|
* `@kubiko/framework/event-store/admin-api` directly, this guard catches it
|
|
@@ -114,7 +113,7 @@ function findEnclosingName(call: CallExpression): string {
|
|
|
114
113
|
export const guard: AstGuard = {
|
|
115
114
|
name: "Admin-API Guard",
|
|
116
115
|
scan: SCAN,
|
|
117
|
-
// App repos are not exempt — appendRaw/appendRawBatch bypasses the pipeline
|
|
116
|
+
// App repos are not exempt — appendRaw/appendRawBatch bypasses the pipeline.
|
|
118
117
|
security: true,
|
|
119
118
|
hint: "Admin API (appendRaw/appendRawBatch) bypasses the pipeline — only allowed in samples/*/migration/ or scripts/migrations/. For domain events: ctx.appendEvent / write handler.",
|
|
120
119
|
run(files, roots: readonly RepoRoot[] = resolveRepoRoots()) {
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
// ponytail: the handler file convention (*.query.ts/*.write.ts under handlers/)
|
|
14
14
|
// is not enforced yet — add it once the registration API shapes are
|
|
15
15
|
// inventoried and stable.
|
|
16
|
-
//
|
|
17
|
-
// Part of App-Mounting 2.0 (infra#208).
|
|
18
16
|
|
|
19
17
|
import * as path from "node:path";
|
|
20
18
|
import { type SourceFile, SyntaxKind } from "ts-morph";
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Comment Issue-Ref Guard: a comment that holds only an issue or PR reference or a planning
|
|
4
|
+
* label (`#1234`, `fw#1234`, `Phase 3`, `S2.H1`) and no explanatory word is a violation.
|
|
5
|
+
* The number belongs in the commit message; the comment has to carry the reason. Existing
|
|
6
|
+
* hits are frozen per file in `.kumiko-comment-issue-ref-baseline.json` and may not grow;
|
|
7
|
+
* without a baseline file (consumer repos) the guard only warns.
|
|
8
|
+
*
|
|
9
|
+
* Consecutive own-line `//` comments count as one paragraph, so a reference on the last
|
|
10
|
+
* line of an explaining paragraph is not a hit.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import type { SourceFile } from "ts-morph";
|
|
15
|
+
import { COMMENT_KIND_LINE, type CommentTrivia, scanCommentTrivia } from "./_lib/comment-trivia";
|
|
16
|
+
import {
|
|
17
|
+
ALL_REPO_KINDS,
|
|
18
|
+
type AstGuard,
|
|
19
|
+
baselineRatchet,
|
|
20
|
+
buildSharedProject,
|
|
21
|
+
filesForGuard,
|
|
22
|
+
isLocalFinding,
|
|
23
|
+
relFromRepoRoot,
|
|
24
|
+
runStandalone,
|
|
25
|
+
type ScanSpec,
|
|
26
|
+
} from "./_lib/guard-kit";
|
|
27
|
+
import { lineHasIgnoreTag } from "./_lib/ignore-tag";
|
|
28
|
+
import { resolveRepoRoots } from "./_lib/roots";
|
|
29
|
+
|
|
30
|
+
const ROOT = process.cwd();
|
|
31
|
+
const GUARD_NAME = "comment-issue-ref-only";
|
|
32
|
+
const IGNORE_TAG = "kumiko-lint-ignore comment-issue-ref-only";
|
|
33
|
+
|
|
34
|
+
const SCAN: ScanSpec = {
|
|
35
|
+
scope: "source",
|
|
36
|
+
kinds: ALL_REPO_KINDS,
|
|
37
|
+
extensions: ["ts", "tsx"],
|
|
38
|
+
frameworkWithin: ["packages/*/src/**"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const EXCLUDE = /(\.d\.(ts|tsx)$|\/dist\/)/;
|
|
42
|
+
|
|
43
|
+
// `fw#12`, `#1234`, `kumiko-framework#3`, `oven-sh/bun#5`; `'` is an HTML entity.
|
|
44
|
+
const REFERENCE_RE = /(?<!&)[\w./-]*#\d+(?![\w])/g;
|
|
45
|
+
const PLANNING_WORD_RE =
|
|
46
|
+
/\b(?:phase|round|welle|wave|sprint)[- ]?\d+(?:\.\d+)*[a-z]?\b|\bbug-bash(?:[- ]\d+)?\b/gi;
|
|
47
|
+
// Dotted plan labels such as S2.H1, T1.5c, B1.5. Case-sensitive and without V so
|
|
48
|
+
// that version numbers (v1.2, V20.1) never count.
|
|
49
|
+
const DOTTED_LABEL_RE = /\b[A-UW-Z]\d{1,2}(?:\.(?:\d+[a-z]?|[A-Z]\d+))+\b/g;
|
|
50
|
+
const WORD_RE = /[\p{L}_$][\p{L}\p{N}_$'-]*/gu;
|
|
51
|
+
const MIN_EXPLANATORY_WORD_LENGTH = 3;
|
|
52
|
+
|
|
53
|
+
// Words that only frame a reference ("see #12", "fix for fw#3", "part of Phase 2").
|
|
54
|
+
const FRAMING_WORDS: ReadonlySet<string> = new Set([
|
|
55
|
+
"see",
|
|
56
|
+
"siehe",
|
|
57
|
+
"vgl",
|
|
58
|
+
"ref",
|
|
59
|
+
"refs",
|
|
60
|
+
"issue",
|
|
61
|
+
"issues",
|
|
62
|
+
"fix",
|
|
63
|
+
"fixes",
|
|
64
|
+
"fixed",
|
|
65
|
+
"for",
|
|
66
|
+
"from",
|
|
67
|
+
"per",
|
|
68
|
+
"part",
|
|
69
|
+
"the",
|
|
70
|
+
"and",
|
|
71
|
+
"und",
|
|
72
|
+
"via",
|
|
73
|
+
"follow",
|
|
74
|
+
"followup",
|
|
75
|
+
"follow-up",
|
|
76
|
+
"tracking",
|
|
77
|
+
"tracked",
|
|
78
|
+
"todo",
|
|
79
|
+
"fixme",
|
|
80
|
+
"closes",
|
|
81
|
+
"resolves",
|
|
82
|
+
"related",
|
|
83
|
+
"also",
|
|
84
|
+
"nach",
|
|
85
|
+
"aus",
|
|
86
|
+
"laut",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function commentBody(text: string): string {
|
|
90
|
+
return text
|
|
91
|
+
.replace(/^\/\*\*?|\*\/$/g, "")
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((line) => line.replace(/^\s*(?:\/\/+|\*)?/, ""))
|
|
94
|
+
.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function referenceTokensIn(body: string): number {
|
|
98
|
+
return (
|
|
99
|
+
(body.match(REFERENCE_RE)?.length ?? 0) +
|
|
100
|
+
(body.match(PLANNING_WORD_RE)?.length ?? 0) +
|
|
101
|
+
(body.match(DOTTED_LABEL_RE)?.length ?? 0)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isExplanatoryWord(word: string): boolean {
|
|
106
|
+
return word.length >= MIN_EXPLANATORY_WORD_LENGTH && !FRAMING_WORDS.has(word.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** True when the comment carries a reference or planning label and no explanatory word besides it. */
|
|
110
|
+
export function isIssueRefOnlyComment(commentText: string): boolean {
|
|
111
|
+
if (lineHasIgnoreTag(commentText, IGNORE_TAG)) return false;
|
|
112
|
+
const body = commentBody(commentText);
|
|
113
|
+
if (referenceTokensIn(body) === 0) return false;
|
|
114
|
+
const rest = body
|
|
115
|
+
.replace(REFERENCE_RE, " ")
|
|
116
|
+
.replace(PLANNING_WORD_RE, " ")
|
|
117
|
+
.replace(DOTTED_LABEL_RE, " ");
|
|
118
|
+
return !(rest.match(WORD_RE) ?? []).some(isExplanatoryWord);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type CommentParagraph = { readonly start: number; readonly text: string };
|
|
122
|
+
|
|
123
|
+
function startsOwnLine(fullText: string, start: number): boolean {
|
|
124
|
+
const lineStart = fullText.lastIndexOf("\n", start - 1) + 1;
|
|
125
|
+
return fullText.slice(lineStart, start).trim() === "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function continuesLineParagraph(
|
|
129
|
+
fullText: string,
|
|
130
|
+
previous: CommentTrivia,
|
|
131
|
+
next: CommentTrivia,
|
|
132
|
+
): boolean {
|
|
133
|
+
if (previous.kind !== COMMENT_KIND_LINE || next.kind !== COMMENT_KIND_LINE) return false;
|
|
134
|
+
if (!startsOwnLine(fullText, previous.start) || !startsOwnLine(fullText, next.start))
|
|
135
|
+
return false;
|
|
136
|
+
return /^[ \t]*\r?\n[ \t]*$/.test(
|
|
137
|
+
fullText.slice(previous.start + previous.text.length, next.start),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function commentParagraphs(fullText: string): CommentParagraph[] {
|
|
142
|
+
const paragraphs: CommentParagraph[] = [];
|
|
143
|
+
let current: { start: number; parts: string[]; last: CommentTrivia } | undefined;
|
|
144
|
+
for (const comment of scanCommentTrivia(fullText)) {
|
|
145
|
+
if (current && continuesLineParagraph(fullText, current.last, comment)) {
|
|
146
|
+
current.parts.push(comment.text);
|
|
147
|
+
current.last = comment;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (current) paragraphs.push({ start: current.start, text: current.parts.join("\n") });
|
|
151
|
+
current = { start: comment.start, parts: [comment.text], last: comment };
|
|
152
|
+
}
|
|
153
|
+
if (current) paragraphs.push({ start: current.start, text: current.parts.join("\n") });
|
|
154
|
+
return paragraphs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type IssueRefOnlySite = { readonly line: number; readonly text: string };
|
|
158
|
+
|
|
159
|
+
export function findIssueRefOnlyComments(fullText: string): IssueRefOnlySite[] {
|
|
160
|
+
return commentParagraphs(fullText)
|
|
161
|
+
.filter((paragraph) => isIssueRefOnlyComment(paragraph.text))
|
|
162
|
+
.map((paragraph) => ({
|
|
163
|
+
line: fullText.slice(0, paragraph.start).split("\n").length,
|
|
164
|
+
text: paragraph.text,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function issueRefOnlyCounts(
|
|
169
|
+
files: readonly SourceFile[],
|
|
170
|
+
roots: ReadonlyArray<{ readonly absPath: string }> = resolveRepoRoots(),
|
|
171
|
+
): { counts: Record<string, number>; firstLine: Record<string, number> } {
|
|
172
|
+
const counts: Record<string, number> = {};
|
|
173
|
+
const firstLine: Record<string, number> = {};
|
|
174
|
+
for (const sf of files) {
|
|
175
|
+
if (EXCLUDE.test(sf.getFilePath())) continue;
|
|
176
|
+
const sites = findIssueRefOnlyComments(sf.getFullText());
|
|
177
|
+
const first = sites[0];
|
|
178
|
+
if (first === undefined) continue;
|
|
179
|
+
const file = relFromRepoRoot(sf.getFilePath(), roots);
|
|
180
|
+
if (!isLocalFinding({ file })) continue;
|
|
181
|
+
counts[file] = sites.length;
|
|
182
|
+
firstLine[file] = first.line;
|
|
183
|
+
}
|
|
184
|
+
return { counts, firstLine };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const issueRefBaseline = baselineRatchet({
|
|
188
|
+
file: path.join(ROOT, ".kumiko-comment-issue-ref-baseline.json"),
|
|
189
|
+
formatVersion: 1,
|
|
190
|
+
unit: "issue-reference-only comment(s)",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const REMEDIATION =
|
|
194
|
+
"Replace the reference with the reason it stands for, or delete the comment; the number belongs in the commit message.";
|
|
195
|
+
|
|
196
|
+
export const guard: AstGuard = {
|
|
197
|
+
name: GUARD_NAME,
|
|
198
|
+
scan: SCAN,
|
|
199
|
+
hint: `after removing hits: \`kumiko-guards guards --write-baseline --guard=${GUARD_NAME}\`; deliberate exception: // ${IGNORE_TAG} <reason>`,
|
|
200
|
+
run: (files, roots = resolveRepoRoots()) => {
|
|
201
|
+
const { counts, firstLine } = issueRefOnlyCounts(files, roots);
|
|
202
|
+
return {
|
|
203
|
+
violations: issueRefBaseline.check(counts, REMEDIATION, {
|
|
204
|
+
formatDriftRemediation: `Run \`kumiko-guards guards --write-baseline --guard=${GUARD_NAME}\` once.`,
|
|
205
|
+
resolveLine: (file) => firstLine[file] ?? 1,
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
writeBaseline: (files, roots = resolveRepoRoots()) => {
|
|
210
|
+
issueRefBaseline.write(issueRefOnlyCounts(files, roots).counts);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (import.meta.main) {
|
|
215
|
+
if (process.argv.slice(2).includes("--write-baseline")) {
|
|
216
|
+
const project = buildSharedProject([guard]);
|
|
217
|
+
guard.writeBaseline?.(filesForGuard(project, guard));
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
runStandalone(guard);
|
|
221
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { scanCommentTrivia } from "./_lib/comment-trivia";
|
|
4
4
|
import { gitEnv } from "./_lib/git-env";
|
|
5
5
|
import { ALL_REPO_KINDS, cliFlagsError, isLocalFinding } from "./_lib/guard-kit";
|
|
6
6
|
import { lineHasIgnoreTag } from "./_lib/ignore-tag";
|
|
@@ -72,142 +72,11 @@ function germanCommentSite(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// Tokens that end an operand without being identifiers or keywords.
|
|
76
|
-
const OPERAND_END_PUNCTUATION_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
77
|
-
ts.SyntaxKind.NumericLiteral,
|
|
78
|
-
ts.SyntaxKind.BigIntLiteral,
|
|
79
|
-
ts.SyntaxKind.StringLiteral,
|
|
80
|
-
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
81
|
-
ts.SyntaxKind.TemplateTail,
|
|
82
|
-
ts.SyntaxKind.RegularExpressionLiteral,
|
|
83
|
-
ts.SyntaxKind.CloseParenToken,
|
|
84
|
-
ts.SyntaxKind.CloseBracketToken,
|
|
85
|
-
ts.SyntaxKind.CloseBraceToken,
|
|
86
|
-
ts.SyntaxKind.PlusPlusToken,
|
|
87
|
-
ts.SyntaxKind.MinusMinusToken,
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
// Reserved words after which an expression (so possibly a regex) starts.
|
|
91
|
-
const EXPRESSION_INTRODUCING_KEYWORDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
92
|
-
ts.SyntaxKind.ReturnKeyword,
|
|
93
|
-
ts.SyntaxKind.TypeOfKeyword,
|
|
94
|
-
ts.SyntaxKind.InstanceOfKeyword,
|
|
95
|
-
ts.SyntaxKind.InKeyword,
|
|
96
|
-
ts.SyntaxKind.CaseKeyword,
|
|
97
|
-
ts.SyntaxKind.VoidKeyword,
|
|
98
|
-
ts.SyntaxKind.DeleteKeyword,
|
|
99
|
-
ts.SyntaxKind.ThrowKeyword,
|
|
100
|
-
ts.SyntaxKind.NewKeyword,
|
|
101
|
-
ts.SyntaxKind.YieldKeyword,
|
|
102
|
-
ts.SyntaxKind.AwaitKeyword,
|
|
103
|
-
ts.SyntaxKind.ElseKeyword,
|
|
104
|
-
ts.SyntaxKind.DoKeyword,
|
|
105
|
-
ts.SyntaxKind.ExtendsKeyword,
|
|
106
|
-
ts.SyntaxKind.DefaultKeyword,
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
function isIdentifierOrKeyword(kind: ts.SyntaxKind): boolean {
|
|
110
|
-
return (
|
|
111
|
-
kind === ts.SyntaxKind.Identifier ||
|
|
112
|
-
(kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword)
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// After an operand end a `/` is division, not the start of a regex literal.
|
|
117
|
-
// Contextual keywords (`from`, `type`, `of`, ...) scan as keyword kinds but are
|
|
118
|
-
// valid member and variable names, so every non-introducing keyword counts. A `!`
|
|
119
|
-
// is the non-null postfix only when the token before it ended an operand.
|
|
120
|
-
function isOperandEnd(kind: ts.SyntaxKind, previousEndedOperand: boolean): boolean {
|
|
121
|
-
if (kind === ts.SyntaxKind.ExclamationToken) return previousEndedOperand;
|
|
122
|
-
if (OPERAND_END_PUNCTUATION_KINDS.has(kind)) return true;
|
|
123
|
-
return isIdentifierOrKeyword(kind) && !EXPRESSION_INTRODUCING_KEYWORDS.has(kind);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const TRIVIA_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
127
|
-
ts.SyntaxKind.SingleLineCommentTrivia,
|
|
128
|
-
ts.SyntaxKind.MultiLineCommentTrivia,
|
|
129
|
-
ts.SyntaxKind.WhitespaceTrivia,
|
|
130
|
-
ts.SyntaxKind.NewLineTrivia,
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
// The raw scanner always yields SlashToken for `/`; without the rescan a regex
|
|
134
|
-
// body like /\{/ would shift braceDepth and end a template early. After an
|
|
135
|
-
// operand the rescan would instead swallow `/ 2; // {` as a regex and hide the comment.
|
|
136
|
-
function rescanSlashAsRegex(
|
|
137
|
-
scanner: ts.Scanner,
|
|
138
|
-
kind: ts.SyntaxKind,
|
|
139
|
-
previousEndedOperand: boolean,
|
|
140
|
-
): ts.SyntaxKind {
|
|
141
|
-
if (previousEndedOperand) return kind;
|
|
142
|
-
const reScanned = scanner.reScanSlashToken();
|
|
143
|
-
return reScanned === ts.SyntaxKind.RegularExpressionLiteral ? reScanned : kind;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function closesTemplateSubstitution(
|
|
147
|
-
templateBraceDepths: readonly number[],
|
|
148
|
-
braceDepth: number,
|
|
149
|
-
): boolean {
|
|
150
|
-
return (
|
|
151
|
-
templateBraceDepths.length > 0 &&
|
|
152
|
-
braceDepth === templateBraceDepths[templateBraceDepths.length - 1]
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Template substitutions can nest braces (object literals, arrow bodies, other
|
|
157
|
-
// templates). The raw scanner tokenizes a `}` as a plain CloseBraceToken and
|
|
158
|
-
// never resumes template mode, so reScanTemplateToken() must run once brace
|
|
159
|
-
// depth returns to the level the substitution opened at.
|
|
160
75
|
export function scanGermanComments(fullText: string, file: string): Site[] {
|
|
161
76
|
const sites: Site[] = [];
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ts.LanguageVariant.Standard,
|
|
166
|
-
fullText,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const templateBraceDepths: number[] = [];
|
|
170
|
-
let braceDepth = 0;
|
|
171
|
-
let previousEndedOperand = false;
|
|
172
|
-
|
|
173
|
-
let kind = scanner.scan();
|
|
174
|
-
while (kind !== ts.SyntaxKind.EndOfFileToken) {
|
|
175
|
-
switch (kind) {
|
|
176
|
-
case ts.SyntaxKind.SingleLineCommentTrivia:
|
|
177
|
-
case ts.SyntaxKind.MultiLineCommentTrivia: {
|
|
178
|
-
const site = germanCommentSite(
|
|
179
|
-
fullText,
|
|
180
|
-
file,
|
|
181
|
-
scanner.getTokenStart(),
|
|
182
|
-
scanner.getTokenText(),
|
|
183
|
-
);
|
|
184
|
-
if (site) sites.push(site);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
case ts.SyntaxKind.SlashToken:
|
|
188
|
-
case ts.SyntaxKind.SlashEqualsToken:
|
|
189
|
-
kind = rescanSlashAsRegex(scanner, kind, previousEndedOperand);
|
|
190
|
-
break;
|
|
191
|
-
case ts.SyntaxKind.TemplateHead:
|
|
192
|
-
case ts.SyntaxKind.TemplateMiddle:
|
|
193
|
-
templateBraceDepths.push(braceDepth);
|
|
194
|
-
break;
|
|
195
|
-
case ts.SyntaxKind.OpenBraceToken:
|
|
196
|
-
braceDepth++;
|
|
197
|
-
break;
|
|
198
|
-
case ts.SyntaxKind.CloseBraceToken:
|
|
199
|
-
if (closesTemplateSubstitution(templateBraceDepths, braceDepth)) {
|
|
200
|
-
templateBraceDepths.pop();
|
|
201
|
-
kind = scanner.reScanTemplateToken(false);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (braceDepth > 0) braceDepth--;
|
|
205
|
-
break;
|
|
206
|
-
default:
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
if (!TRIVIA_KINDS.has(kind)) previousEndedOperand = isOperandEnd(kind, previousEndedOperand);
|
|
210
|
-
kind = scanner.scan();
|
|
77
|
+
for (const comment of scanCommentTrivia(fullText)) {
|
|
78
|
+
const site = germanCommentSite(fullText, file, comment.start, comment.text);
|
|
79
|
+
if (site) sites.push(site);
|
|
211
80
|
}
|
|
212
81
|
return sites;
|
|
213
82
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Guard: finds raw `fetch(` calls in server code outside the client-scoped
|
|
4
4
|
* exceptions.
|
|
5
5
|
*
|
|
6
|
-
* `egress(policy)` (@cosmicdrift/kumiko-framework/http
|
|
6
|
+
* `egress(policy)` (@cosmicdrift/kumiko-framework/http) is
|
|
7
7
|
* the single exported way for server code to speak outward — it enforces an
|
|
8
8
|
* SSRF policy (external/internal/tenant-supplied) at the call site instead
|
|
9
9
|
* of leaving that to convention. This guard fails on raw `fetch(` in server
|
|
@@ -45,7 +45,7 @@ const EXCLUDE =
|
|
|
45
45
|
/(__tests__|\.test\.ts$|\.integration\.ts$|\.d\.ts$|(?:^|\/)web\/|(?:^|\/)public\/)/;
|
|
46
46
|
|
|
47
47
|
// Prefixed by RepoRoot.name (not kind): kind "app" is shared across
|
|
48
|
-
// unregistered repos
|
|
48
|
+
// unregistered repos.
|
|
49
49
|
|
|
50
50
|
// packages/framework/src/http/egress.ts IS the egress() implementation.
|
|
51
51
|
const ALLOWLIST = [/^kumiko-framework\/packages\/framework\/src\/http\/egress\.ts$/];
|
|
@@ -59,7 +59,7 @@ interface Violation {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// `self` omitted: Worker globals live under web/public (excluded); a local
|
|
62
|
-
// `const self = this` binding was a systematic FP
|
|
62
|
+
// `const self = this` binding was a systematic FP.
|
|
63
63
|
const GLOBAL_FETCH_RECEIVERS = new Set(["globalThis", "window"]);
|
|
64
64
|
|
|
65
65
|
// The marker is a per-line skip for a security guard, so it only counts with a
|
|
@@ -115,7 +115,7 @@ function collectImportedFeatureIds(project: Project, roots: readonly RepoRoot[])
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Baseline from the
|
|
118
|
+
* Baseline from the measurement: features with no integration
|
|
119
119
|
* test reaching their directory at all — only covered (if at all) by unit
|
|
120
120
|
* tests (`feature.test.ts`). Pre-existing at the first sharp run, not
|
|
121
121
|
* introduced by this change. Backfilling is its own scope per feature, not
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* guard-i18n-keys.ts only checks whether a used t() key is defined —
|
|
7
7
|
* and has no concept of a "mount point". A mount that never calls
|
|
8
8
|
* localeDeClient() (client) or localeDe() (server) slips through there
|
|
9
|
-
* completely
|
|
9
|
+
* completely.
|
|
10
10
|
*
|
|
11
11
|
* Scope gate: the package.json closest to the file must point at
|
|
12
12
|
* kumiko-locale-de (currently kumiko-studio, publicstatus, solon,
|
|
@@ -83,9 +83,9 @@ function gateFor(sf: SourceFile, cache: Map<string, Gate>): Gate | undefined {
|
|
|
83
83
|
return gate;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// localeDeClient()
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// localeDeClient() in clientFeatures is matched by call text; `{ de: ... }` also
|
|
87
|
+
// covers a raw <LocaleProvider fallbackBundles={[{ de: bundle }, ...]}> composition
|
|
88
|
+
// that does not use a localeDeClient() call.
|
|
89
89
|
function localeDeClientRef(node: Node): boolean {
|
|
90
90
|
if (node.isKind(SyntaxKind.Identifier) && node.getText() === "localeDeClient") return true;
|
|
91
91
|
if (node.isKind(SyntaxKind.CallExpression)) return localeDeClientRef(node.getExpression());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Guard: locale bundle values must follow Mandant/Organización terminology
|
|
3
|
+
* Guard: locale bundle values must follow Mandant/Organización terminology.
|
|
4
4
|
*
|
|
5
5
|
* German UI copy always says "Mandant" (never "Tenant"/"Organisation"); Spanish, French
|
|
6
6
|
* and Italian always say "Organización"/"organisation"/"organizzazione" (never the
|
|
@@ -50,8 +50,8 @@ const RULES: Record<TerminologyLocale, LocaleRule> = {
|
|
|
50
50
|
de: {
|
|
51
51
|
preferred: "Mandant",
|
|
52
52
|
// Tenants? covers plural; Organisation(en|s)? without trailing \b so
|
|
53
|
-
// compounds like Organisations-ID / Organisationsstruktur still match
|
|
54
|
-
//
|
|
53
|
+
// compounds like Organisations-ID / Organisationsstruktur still match.
|
|
54
|
+
// TenantAdmin stays safe: \b after Tenant fails on 'A'.
|
|
55
55
|
forbidden: [/\bTenants?\b/i, /\bOrganisation(en|s)?/i],
|
|
56
56
|
},
|
|
57
57
|
es: {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// that the keys are defined — this guard closes the gap before that:
|
|
5
5
|
// strings that never became keys, e.g. "Loading tenants…").
|
|
6
6
|
//
|
|
7
|
-
// Part of App-Mounting 2.0
|
|
7
|
+
// Part of App-Mounting 2.0.
|
|
8
8
|
|
|
9
9
|
import { type CallExpression, type Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
10
10
|
import { type AstGuard, type GuardViolation, runStandalone, type ScanSpec } from "./_lib/guard-kit";
|
|
@@ -13,7 +13,7 @@ import { hasIgnoreTag } from "./_lib/ignore-tag";
|
|
|
13
13
|
// Web/public code only — server code (handler error texts) goes through the
|
|
14
14
|
// error i18n pipeline and has its own guards.
|
|
15
15
|
//
|
|
16
|
-
// samples/ deliberately NOT scanned (tried
|
|
16
|
+
// samples/ deliberately NOT scanned (tried and rolled back): the
|
|
17
17
|
// showcase/gallery pages are dev docs about the framework API itself
|
|
18
18
|
// ("Form.title / toolbarTitle", "children — auto from schema.navs"), not
|
|
19
19
|
// app UI text a real user sees. 354 hits in the trial run, almost all such
|
|
@@ -21,7 +21,7 @@ import { hasIgnoreTag } from "./_lib/ignore-tag";
|
|
|
21
21
|
// r.translations definitions); this guard stays scoped to app repos.
|
|
22
22
|
//
|
|
23
23
|
// src/features/**/feature.ts covers feature.ts registrar calls (r.nav/r.screen
|
|
24
|
-
// object-literal arguments) —
|
|
24
|
+
// object-literal arguments) — those sit outside JSX and were a
|
|
25
25
|
// blind spot. Other server code (handlers/, lib/) has its own i18n pipeline
|
|
26
26
|
// and is filtered out below by requiring an r.*-call ancestor.
|
|
27
27
|
const SCAN: ScanSpec = {
|
|
@@ -131,7 +131,7 @@ function collectTernaryLogicText(node: Node): TextPiece[] {
|
|
|
131
131
|
return textPieces(node);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//
|
|
134
|
+
// A hop out of a helper is only taken while that helper call is
|
|
135
135
|
// itself an argument of the next call — a call nested deeper (inside an object
|
|
136
136
|
// or array, e.g. `createTextField({ label })` under `createEntity({ fields })`)
|
|
137
137
|
// builds a value instead of forwarding the registrar's argument. The full chain
|
|
@@ -220,7 +220,7 @@ function checkRegistrarLabelProperties(sf: SourceFile, violations: GuardViolatio
|
|
|
220
220
|
if (!HUMAN_TEXT.test(value) || I18N_KEY_SHAPE.test(value)) continue;
|
|
221
221
|
const callChain = registrarCallChain(prop);
|
|
222
222
|
if (callChain === undefined) continue;
|
|
223
|
-
// The agent-doc slot
|
|
223
|
+
// The agent-doc slot is LLM metadata for the
|
|
224
224
|
// AI tool catalog: English by design, never rendered as UI text. The
|
|
225
225
|
// slot is the options object handed to a call on the registrar
|
|
226
226
|
// argument chain, so forwarding it through a `defineEntity*Handler`
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// exist in the framework (usePrimitives() resp. widgets/) — app rebuilds
|
|
5
5
|
// drift visually and cost every migration twice.
|
|
6
6
|
//
|
|
7
|
-
// Second rule
|
|
7
|
+
// Second rule: raw form HTML (<input>/<select>/<textarea>/
|
|
8
8
|
// <label>) in app/feature web code, regardless of what the file imports —
|
|
9
9
|
// an import gate here previously let the designer's raw <input>s pass just
|
|
10
10
|
// by importing from @cosmicdrift/kumiko-renderer (types only) instead of
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
// closed against an empty baseline while no baseline file exists —
|
|
16
16
|
// otherwise the rule would stay silent in every app repo until someone
|
|
17
17
|
// manually runs --write-baseline there.
|
|
18
|
-
//
|
|
19
|
-
// Part of App-Mounting 2.0 (infra#208).
|
|
20
18
|
|
|
21
19
|
import { existsSync } from "node:fs";
|
|
22
20
|
import * as path from "node:path";
|
|
@@ -93,7 +91,7 @@ const RAW_FORM_HTML_REMEDIATION =
|
|
|
93
91
|
`or usePrimitives(). Real exception: // ${IGNORE_TAG} <reason>`;
|
|
94
92
|
|
|
95
93
|
// The rule is new: measured backlog is 0 in every locally checked repo
|
|
96
|
-
//
|
|
94
|
+
// . Without a baseline file, check fail-closed against an empty
|
|
97
95
|
// baseline instead of (as usual) warn-only until the first
|
|
98
96
|
// `--write-baseline` — otherwise the rule would stay silent in every app
|
|
99
97
|
// repo until it separately commits its own baseline file (every baseline
|
|
@@ -59,7 +59,7 @@ export function isScannableBinFile(rel: string): boolean {
|
|
|
59
59
|
// convention at all — "solon" is deliberately not on it) and intersected
|
|
60
60
|
// with the resolved roots, so a restricted scope actually narrows what this
|
|
61
61
|
// guard scans instead of always walking every repo regardless of scope
|
|
62
|
-
// (
|
|
62
|
+
// (follow-up — a money-horse-scoped push must not fail on a
|
|
63
63
|
// finding in publicstatus).
|
|
64
64
|
const CANDIDATE_ENV_TS_REPOS: ReadonlySet<string> = new Set([
|
|
65
65
|
"kumiko-studio",
|
|
@@ -81,7 +81,7 @@ export function findDirectProcessEnv(roots?: ReadonlyArray<RepoRoot>): ProcessEn
|
|
|
81
81
|
/**
|
|
82
82
|
* Like findDirectProcessEnv, additionally reports the number of files read
|
|
83
83
|
* and repos with bin/env.ts — without that an empty scan can't be told apart
|
|
84
|
-
* from a clean one
|
|
84
|
+
* from a clean one.
|
|
85
85
|
*/
|
|
86
86
|
export function scanDirectProcessEnv(roots: ReadonlyArray<RepoRoot> = resolveRepoRoots()): {
|
|
87
87
|
readonly findings: ProcessEnvFinding[];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// No inline CSS in app code: `style={...}` props and `React.CSSProperties`
|
|
3
3
|
// style objects bypass theme tokens and dark mode. Use widgets/tokens.
|
|
4
4
|
//
|
|
5
|
-
// Part of App-Mounting 2.0
|
|
5
|
+
// Part of App-Mounting 2.0.
|
|
6
6
|
|
|
7
7
|
import { type SourceFile, SyntaxKind } from "ts-morph";
|
|
8
8
|
import {
|
|
@@ -96,7 +96,7 @@ type ScanScope = {
|
|
|
96
96
|
|
|
97
97
|
// Framework checkout enforces bundled-features + samples. App-repo checkout
|
|
98
98
|
// scans the app's web/ screens — primitives-discipline as a ratchet per app
|
|
99
|
-
// repo
|
|
99
|
+
// repo. Every other resolved repo (enterprise, platform, ...)
|
|
100
100
|
// scans alongside the others in one run — flatMap all of them instead of
|
|
101
101
|
// cascading, which would silently skip every root after the first, exactly
|
|
102
102
|
// the silent-skip this guard fixes.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Layout utilities (flex/grid/gap/p-/m-/w-) and token classes
|
|
6
6
|
// (bg-primary, text-status-ok, …) stay allowed.
|
|
7
7
|
//
|
|
8
|
-
// Part of App-Mounting 2.0
|
|
8
|
+
// Part of App-Mounting 2.0. Enabled per app repo in the
|
|
9
9
|
// respective migration PR (ui-guards input in _app-test.yml).
|
|
10
10
|
|
|
11
11
|
import { type JsxAttribute, type SourceFile, SyntaxKind } from "ts-morph";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* <button>/<input>/<select>/<textarea>/<dialog>/<form> are deliberately NOT
|
|
10
10
|
* in this guard: <button>/<input>/<select>/<textarea>/<dialog> are already
|
|
11
11
|
* enforced by guard-primitives-discipline.ts (its own CI step, not this
|
|
12
|
-
* bundle), and <form> is a documented exception (
|
|
12
|
+
* bundle), and <form> is a documented exception (agent-layer.tsx:71
|
|
13
13
|
* uses a native <form onSubmit> on purpose) — adding either back here would
|
|
14
14
|
* duplicate or contradict an existing rule.
|
|
15
15
|
*
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* `getUnscopedAggregateStreamMaxVersion` / `getUnscopedAggregateStreamTenant`
|
|
7
7
|
* (packages/framework/src/event-store/event-store.ts) have no tenant
|
|
8
|
-
* filter — they are an existence oracle for foreign tenants
|
|
9
|
-
*
|
|
10
|
-
* that deliberately works across tenants.
|
|
8
|
+
* filter — they are an existence oracle for foreign tenants. Legitimate only
|
|
9
|
+
* for seed-/system-internal code that deliberately works across tenants.
|
|
11
10
|
*
|
|
12
11
|
* Usage:
|
|
13
12
|
* bun guards/guard-restricted-symbols.ts
|
|
@@ -4,23 +4,22 @@
|
|
|
4
4
|
* `packages/bundled-features/src/**` must already be reachable through the
|
|
5
5
|
* real Tailwind `@source` scan surface (`packages/renderer/src` +
|
|
6
6
|
* `samples/**\/src`, see `packages/renderer/src/web/styles.css`).
|
|
7
|
-
* bundled-features itself is NOT part of that scan surface
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* with no build/lint error.
|
|
7
|
+
* bundled-features itself is NOT part of that scan surface: expanding
|
|
8
|
+
* `@source` there was verified to work in the monorepo build but makes the
|
|
9
|
+
* standalone npm-consumer case (no monorepo-relative scan path) falsely
|
|
10
|
+
* green. A class used only in bundled-features silently drops out of the
|
|
11
|
+
* compiled CSS at runtime, with no build/lint error.
|
|
13
12
|
*
|
|
14
|
-
* V1 scope
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
13
|
+
* V1 scope, deliberately not a Tailwind-CLI CSS regen: only plain
|
|
14
|
+
* string-literal `className="..."` JSX attributes are extracted on the
|
|
15
|
+
* bundled-features side — no `cn(...)`, no template literals, no dynamic
|
|
16
|
+
* class maps. The allow-set (the real scan surface) is read generously
|
|
17
|
+
* instead — every string/template-literal in those files, not just
|
|
18
|
+
* `className` attributes — because Tailwind's own scanner is a text scan,
|
|
19
|
+
* not JSX-attribute-aware; under-approximating the allow side would turn
|
|
20
|
+
* into false positives on a guard that blocks CI. Tokens compare WITH their
|
|
21
|
+
* modifier prefixes intact (`hover:mb-2` is its own Tailwind candidate,
|
|
22
|
+
* distinct from `mb-2`).
|
|
24
23
|
*
|
|
25
24
|
* Baseline-regression guard: pins the
|
|
26
25
|
* currently-known violation count per file. Reductions are allowed but
|
|
@@ -174,11 +173,11 @@ const tailwindScanSurfaceBaseline = (baselineRoot: string) =>
|
|
|
174
173
|
// ships .tsx with `className` under its own scan surface (renderer/src +
|
|
175
174
|
// samples/**/src) needs its own `@source` entry in the app's
|
|
176
175
|
// styles.css — nothing else scans it. Missed cases so far: kumiko-designer
|
|
177
|
-
// in studio
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
176
|
+
// in studio, kumiko-ai-agent in publicstatus — both fixed by hand, both
|
|
177
|
+
// silent (no build/lint error) until noticed visually. This does not need
|
|
178
|
+
// ts-morph: it reads package.json + styles.css + a plain text scan of the
|
|
179
|
+
// dependency's shipped .tsx files directly off disk, independent of the
|
|
180
|
+
// scan-selected SourceFile list above.
|
|
182
181
|
const APP_STYLES_REL = "src/styles.css";
|
|
183
182
|
const PACKAGE_SCOPE = /^@cosmicdrift(?:gamestudio)?\//;
|
|
184
183
|
const CLASSNAME_ATTR = /\bclassName\s*=/;
|
|
@@ -390,14 +389,14 @@ export function scanSourceCoverage(roots: readonly RepoRoot[]): SourceCoverageFi
|
|
|
390
389
|
}
|
|
391
390
|
|
|
392
391
|
// Third, independent rule: an `@source` glob that is already present and
|
|
393
|
-
// resolves on disk
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
392
|
+
// resolves on disk can still match nothing once the referenced package is
|
|
393
|
+
// installed from a registry instead of a workspace symlink — the symlink
|
|
394
|
+
// exposes the package's full source checkout (`src/`) included, a registry
|
|
395
|
+
// tarball only ships what its `package.json` `files` allowlists. This is
|
|
396
|
+
// exactly the bug that got past the guard the first time, so it's a hard
|
|
397
|
+
// violation, not a warning: no baseline/ratchet, every finding fails the run
|
|
398
|
+
// directly (still only under baseline comparison — `--no-baseline` reports it
|
|
399
|
+
// like the other rules but doesn't fail).
|
|
401
400
|
const NODE_MODULES_PKG_SEGMENT = /node_modules\/((?:@[^/]+\/)?[^/]+)\/([^/]+)/;
|
|
402
401
|
|
|
403
402
|
export interface PublishedScanSurfaceFinding {
|
|
@@ -61,8 +61,8 @@ type RoleHandler = { name: string; file: string; line: number };
|
|
|
61
61
|
// A `schema:` property that references an outlined const (`schema:
|
|
62
62
|
// CancelInvitationSchema`) has no `role:`/`roles:` text of its own — the field
|
|
63
63
|
// lives in the const's declaration. Resolving the identifier to its
|
|
64
|
-
// declaration covers that ~43% of write handlers
|
|
65
|
-
//
|
|
64
|
+
// declaration covers that ~43% of write handlers; a text-only check silently
|
|
65
|
+
// skips them. Falls back to the property's own text
|
|
66
66
|
// when the initializer isn't a resolvable identifier.
|
|
67
67
|
function resolvedSchemaText(schemaProp: Node): string {
|
|
68
68
|
const init = schemaProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
@@ -208,7 +208,7 @@ function globalUserWriteMissingCheck(
|
|
|
208
208
|
// Presets that resolve to platform-wide actors only (engine/
|
|
209
209
|
// config-helpers `access`): no tenant boundary exists to cross.
|
|
210
210
|
// Everything else — access.admin, and the openToAll handlers that
|
|
211
|
-
// gate on isAdminActor at runtime (
|
|
211
|
+
// gate on isAdminActor at runtime (this shape) — is reachable by
|
|
212
212
|
// a tenant-scoped TenantAdmin.
|
|
213
213
|
if (PLATFORM_WIDE_ACCESS.test(accessProp.getText())) return undefined;
|
|
214
214
|
if (!TARGETS_OTHER_USER.test(resolvedSchemaText(schema))) return undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* Guard: apps must not locally reimplement the e2e/screenshot template that
|
|
4
|
-
* `@cosmicdrift/kumiko-testing` already owns
|
|
4
|
+
* `@cosmicdrift/kumiko-testing` already owns. Flags, in Playwright
|
|
5
5
|
* configs and e2e/screenshot specs/helpers:
|
|
6
6
|
* (a) a `playwright*.config.ts` that imports `defineConfig` from
|
|
7
7
|
* `@playwright/test` directly instead of `defineAppE2eConfig` from
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* the message.
|
|
24
24
|
*
|
|
25
25
|
* No baseline, no ratchet: any finding
|
|
26
|
-
* fails.
|
|
26
|
+
* fails. The transition period (a `.kumiko-test-timeouts-baseline.json`
|
|
27
27
|
* grandfathering pre-existing sleep loops) is over — every consumer must fix
|
|
28
28
|
* or mark the cause, not accumulate a pinned count.
|
|
29
29
|
*
|
|
@@ -30,7 +30,7 @@ const SCAN: ScanSpec = {
|
|
|
30
30
|
frameworkWithin: ["packages/*/src/**", "samples/**"],
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
// Tests
|
|
33
|
+
// Tests intentionally use generic QNs such as dispatcher.write("x", {}).
|
|
34
34
|
const EXCLUDE = /(__tests__|\.test\.tsx$|\.integration\.tsx$|\/node_modules\/|\/dist\/)/;
|
|
35
35
|
|
|
36
36
|
/**
|
package/src/run-guards.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { guard as complexity } from "./check-complexity";
|
|
|
25
25
|
import { guard as predicateExtraction } from "./check-predicates";
|
|
26
26
|
import { guard as adminApi } from "./guard-admin-api";
|
|
27
27
|
import { guard as appFeatureStructure } from "./guard-app-feature-structure";
|
|
28
|
+
import { guard as commentIssueRefOnly } from "./guard-comment-issue-ref-only";
|
|
28
29
|
import { guard as crossFeatureImports } from "./guard-cross-feature-imports";
|
|
29
30
|
import { guard as directEntityWrites } from "./guard-direct-entity-writes";
|
|
30
31
|
import { guard as directFetch } from "./guard-direct-fetch";
|
|
@@ -82,6 +83,7 @@ export const GUARDS = [
|
|
|
82
83
|
testTimeouts,
|
|
83
84
|
testTemplateDrift,
|
|
84
85
|
fileSize,
|
|
86
|
+
commentIssueRefOnly,
|
|
85
87
|
];
|
|
86
88
|
|
|
87
89
|
export const GUARD_FLAGS = [
|
package/src/run-ui-guards.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function classifyByPath(repoRelativePath: string): Runtime | null {
|
|
|
108
108
|
* for `kumiko-build` entries (`resolveClientEntries` in kumiko-framework's
|
|
109
109
|
* `packages/server-runtime/src/build-prod-bundle.ts`): `src/client.tsx`/
|
|
110
110
|
* `src/client.ts` (single-entry) or `src/client-<suffix>.tsx?` (declared via
|
|
111
|
-
* package.json `kumiko.clientEntries
|
|
111
|
+
* package.json `kumiko.clientEntries`; no longer auto-discovered
|
|
112
112
|
* by this filename alone). Framework/enterprise packages never match —
|
|
113
113
|
* their sources live under `packages/*\/src/`, not a repo-root `src/`. Kept
|
|
114
114
|
* independent of the framework's own regex (cross-package import would be a
|