@cosmicdrift/kumiko-guards 0.1.1 → 0.3.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 +4 -1
- 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 +185 -0
- package/src/guard-escape-hatch-declared.ts +26 -19
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"default": "./src/index.ts"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"kumiko-guards": "./src/cli.ts"
|
|
28
|
+
},
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"@cosmicdrift/kumiko-repo-manifest": "0.1.0",
|
|
28
31
|
"ts-morph": "^28.0.0"
|
package/src/_lib/guard-kit.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
-
import { relative as pathRelative } from "node:path";
|
|
16
|
+
import { relative as pathRelative, resolve } from "node:path";
|
|
17
17
|
import { Project, type SourceFile } from "ts-morph";
|
|
18
18
|
import { compareToBaseline, findRepoRootFor } from "./baseline-compare";
|
|
19
19
|
import {
|
|
@@ -126,6 +126,13 @@ export function isAllowlisted(relPath: string, allowlist: readonly RegExp[]): bo
|
|
|
126
126
|
return allowlist.some((re) => re.test(relPath));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// allRepos guards render sibling hits as "../<repo>/..." relative to ROOT —
|
|
130
|
+
// freezing those into a repo-local baseline would fail a sibling's own
|
|
131
|
+
// refactor locally while CI (no sibling checked out there) never sees it.
|
|
132
|
+
export function isLocalFinding<T extends { readonly file: string }>(item: T): boolean {
|
|
133
|
+
return !item.file.startsWith("../");
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
type BaselinePayload = {
|
|
130
137
|
readonly format: number;
|
|
131
138
|
readonly generated: string;
|
|
@@ -161,7 +168,7 @@ export function baselineRatchet(args: {
|
|
|
161
168
|
perFile,
|
|
162
169
|
};
|
|
163
170
|
writeFileSync(args.file, `${JSON.stringify(payload, null, 2)}\n`);
|
|
164
|
-
console.log(` Baseline
|
|
171
|
+
console.log(` Baseline written: ${args.file} (total ${total})`);
|
|
165
172
|
};
|
|
166
173
|
const check = (
|
|
167
174
|
current: Readonly<Record<string, number>>,
|
|
@@ -173,7 +180,7 @@ export function baselineRatchet(args: {
|
|
|
173
180
|
): GuardViolation[] => {
|
|
174
181
|
if (!existsSync(args.file)) {
|
|
175
182
|
console.log(
|
|
176
|
-
`
|
|
183
|
+
` No baseline found (${args.file}). Freeze it first with \`--write-baseline\` — warning until then, no fail.`,
|
|
177
184
|
);
|
|
178
185
|
return [];
|
|
179
186
|
}
|
|
@@ -185,7 +192,7 @@ export function baselineRatchet(args: {
|
|
|
185
192
|
{
|
|
186
193
|
file: args.file,
|
|
187
194
|
line: 1,
|
|
188
|
-
message: `Baseline
|
|
195
|
+
message: `Baseline file unreadable (broken JSON, merge marker, aborted write): ${e instanceof Error ? e.message : String(e)}. ${opts?.formatDriftRemediation ?? remediation}`,
|
|
189
196
|
},
|
|
190
197
|
];
|
|
191
198
|
}
|
|
@@ -200,8 +207,8 @@ export function baselineRatchet(args: {
|
|
|
200
207
|
line: 1,
|
|
201
208
|
message:
|
|
202
209
|
raw.format !== args.formatVersion
|
|
203
|
-
? `Baseline
|
|
204
|
-
: `Baseline
|
|
210
|
+
? `Baseline format drift: expected format=${args.formatVersion}, read format=${raw.format ?? "<missing>"}. ${opts?.formatDriftRemediation ?? remediation}`
|
|
211
|
+
: `Baseline file has no valid "perFile" object. ${opts?.formatDriftRemediation ?? remediation}`,
|
|
205
212
|
},
|
|
206
213
|
];
|
|
207
214
|
}
|
|
@@ -209,14 +216,14 @@ export function baselineRatchet(args: {
|
|
|
209
216
|
const { regressions, reduced } = compareToBaseline(current, baseline.perFile);
|
|
210
217
|
if (regressions.length === 0) {
|
|
211
218
|
const total = Object.values(current).reduce((sum, count) => sum + count, 0);
|
|
212
|
-
console.log(` ✓ Baseline (${baseline.total}) —
|
|
213
|
-
if (reduced > 0) console.log(` ✓ ${reduced} ${args.unit}
|
|
219
|
+
console.log(` ✓ Baseline (${baseline.total}) — current ${total}`);
|
|
220
|
+
if (reduced > 0) console.log(` ✓ ${reduced} ${args.unit} reduced since baseline.`);
|
|
214
221
|
return [];
|
|
215
222
|
}
|
|
216
223
|
return regressions.map((regression) => ({
|
|
217
224
|
file: regression.file,
|
|
218
225
|
line: opts?.resolveLine?.(regression.file) ?? 1,
|
|
219
|
-
message: `${args.unit}
|
|
226
|
+
message: `${args.unit} over baseline: baseline=${regression.baseline} current=${regression.current} (+${regression.current - regression.baseline}). ${remediation}`,
|
|
220
227
|
}));
|
|
221
228
|
};
|
|
222
229
|
const handleCli = (current: Readonly<Record<string, number>>): boolean => {
|
|
@@ -226,7 +233,7 @@ export function baselineRatchet(args: {
|
|
|
226
233
|
return true;
|
|
227
234
|
}
|
|
228
235
|
if (cliArgs.includes("--no-baseline")) {
|
|
229
|
-
console.log(" Baseline
|
|
236
|
+
console.log(" Baseline comparison skipped (--no-baseline).");
|
|
230
237
|
return true;
|
|
231
238
|
}
|
|
232
239
|
return false;
|
|
@@ -362,7 +369,7 @@ export type RepoCheck = {
|
|
|
362
369
|
};
|
|
363
370
|
|
|
364
371
|
const VACUOUS_MESSAGE =
|
|
365
|
-
"0
|
|
372
|
+
"0 files scanned even though the target repos are in the checkout — the globs are not matching.";
|
|
366
373
|
|
|
367
374
|
/** Standalone-`main()` guards (their own scan/walk, no shared ts-morph project) run in-process through this. */
|
|
368
375
|
export async function runRepoChecks(
|
|
@@ -435,7 +442,7 @@ export function explainGuards(
|
|
|
435
442
|
for (const { root, source } of roots) {
|
|
436
443
|
const rootScan = scanByAbsPath.get(root.absPath);
|
|
437
444
|
if (!rootScan) {
|
|
438
|
-
lines.push(` ${root.name} [${source}] —
|
|
445
|
+
lines.push(` ${root.name} [${source}] — outside kinds`);
|
|
439
446
|
continue;
|
|
440
447
|
}
|
|
441
448
|
// Exact-path lookup, never a re-glob: `project.getSourceFiles(globs)`
|
|
@@ -443,13 +450,61 @@ export function explainGuards(
|
|
|
443
450
|
// `[id].tsx` is then a broken (or pathologically slow) character class.
|
|
444
451
|
const fileCount = rootScan.files.filter((f) => project.getSourceFile(f) !== undefined).length;
|
|
445
452
|
lines.push(
|
|
446
|
-
` ${root.name} [${source}] ${fileCount}
|
|
453
|
+
` ${root.name} [${source}] ${fileCount} files (source surface ${rootScan.sourceSurface})`,
|
|
447
454
|
);
|
|
448
455
|
}
|
|
449
456
|
}
|
|
450
457
|
return lines;
|
|
451
458
|
}
|
|
452
459
|
|
|
460
|
+
function guardKitVersion(): string {
|
|
461
|
+
const pkgPath = resolve(import.meta.dir, "../../package.json");
|
|
462
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { readonly version?: string };
|
|
463
|
+
return pkg.version ?? "0.0.0";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Pure preflight verdict, injectable for tests: which of the two globally
|
|
468
|
+
* silent-green cases (infra#2863) applies, if any. Not to be confused with a
|
|
469
|
+
* single guard finding no target repos — that stays a per-guard `skipped`
|
|
470
|
+
* line in `reportResults`.
|
|
471
|
+
*/
|
|
472
|
+
export function guardKitPreflightError(guardCount: number, rootCount: number): string | undefined {
|
|
473
|
+
if (rootCount === 0) {
|
|
474
|
+
return "No repo root resolved — checkout has no package.json with a kumiko.json/src layout above cwd.";
|
|
475
|
+
}
|
|
476
|
+
if (guardCount === 0) {
|
|
477
|
+
return "No guards registered — the runner's guard array is empty.";
|
|
478
|
+
}
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export type GuardKitBannerDeps = {
|
|
483
|
+
readonly resolution?: RootResolution;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Shared entrypoint banner for the three bin/run-*.ts runners: fails closed
|
|
488
|
+
* on the two globally-empty cases above, otherwise prints the header every
|
|
489
|
+
* run starts with, before the first guard result. Not used by `--explain`,
|
|
490
|
+
* which already prints its own "Repo: ..." header.
|
|
491
|
+
*/
|
|
492
|
+
export function printGuardKitBanner(
|
|
493
|
+
guardCount: number,
|
|
494
|
+
project?: Project,
|
|
495
|
+
deps: GuardKitBannerDeps = {},
|
|
496
|
+
): void {
|
|
497
|
+
const { roots } = deps.resolution ?? explainRepoRoots();
|
|
498
|
+
const error = guardKitPreflightError(guardCount, roots.length);
|
|
499
|
+
if (error !== undefined) {
|
|
500
|
+
console.error(error);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
console.log(`kumiko-guards ${guardKitVersion()} - ${guardCount} guards registered`);
|
|
504
|
+
console.log(`Roots: ${roots.map((r) => r.root.name).join(", ")} (${roots.length})`);
|
|
505
|
+
if (project) console.log(`Project: ${project.getSourceFiles().length} files`);
|
|
506
|
+
}
|
|
507
|
+
|
|
453
508
|
/**
|
|
454
509
|
* Vacuity floor for guards with their own `main()`.
|
|
455
510
|
*
|
|
@@ -483,11 +538,11 @@ export function reportResults(results: readonly RunResult[]): number {
|
|
|
483
538
|
// Not applicable does not mean checked — that must stay visible,
|
|
484
539
|
// otherwise a standalone run reads like a complete one.
|
|
485
540
|
const scope = r.notApplicable
|
|
486
|
-
? " —
|
|
487
|
-
: ` (${r.matchedFiles ?? 0}
|
|
541
|
+
? " — skipped, target repos not in checkout"
|
|
542
|
+
: ` (${r.matchedFiles ?? 0} files)`;
|
|
488
543
|
const frozen =
|
|
489
544
|
r.frozenFindings && r.frozenFindings > 0
|
|
490
|
-
? ` — ${r.frozenFindings}
|
|
545
|
+
? ` — ${r.frozenFindings} frozen security findings (baseline)`
|
|
491
546
|
: "";
|
|
492
547
|
console.log(` ✓ ${r.name} (${r.ms}ms)${scope}${frozen}`);
|
|
493
548
|
for (const w of r.warnings ?? []) {
|
|
@@ -507,14 +562,14 @@ export function reportResults(results: readonly RunResult[]): number {
|
|
|
507
562
|
}
|
|
508
563
|
if (r.violatingRoots && r.violatingRoots.length > 0) {
|
|
509
564
|
console.error(
|
|
510
|
-
` 0
|
|
565
|
+
` 0 source files in: ${r.violatingRoots.join(", ")} — kumiko.json declares sourceRoots, scope "source" yields nothing there.`,
|
|
511
566
|
);
|
|
512
567
|
console.error(
|
|
513
|
-
"
|
|
568
|
+
" Check the manifest (kumiko.json) or the checkout; without a manifest the derived fallback packages/*/src or src/ applies.",
|
|
514
569
|
);
|
|
515
570
|
}
|
|
516
571
|
if (r.security) {
|
|
517
|
-
console.error(` [security]
|
|
572
|
+
console.error(` [security] Findings without baseline coverage always FAIL.`);
|
|
518
573
|
}
|
|
519
574
|
for (const v of r.outcome?.violations ?? []) {
|
|
520
575
|
console.error(` ${v.file}:${v.line} ${v.message}`);
|
package/src/_lib/qn.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers für Qualified-Names (QN, z.B. "tickets:write:close").
|
|
3
|
+
* Extrahiert aus guard-action-wiring.ts + guard-write-handler-qns.ts, die
|
|
4
|
+
* beide eine byte-identische toKebab-Implementierung inline hatten.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function toKebab(s: string): string {
|
|
8
|
+
return s
|
|
9
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
10
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Regex für ein gültiges Write-Handler-QN.
|
|
15
|
+
// Erlaubt 3 Segmente (feature:write:handler) oder 4+ (feature:write:entity:verb, feature:write:domain:entity:verb).
|
|
16
|
+
// Akzeptiert camelCase + kebab-case — Normalisierung auf kebab erfolgt vor Manifest-Match.
|
|
17
|
+
// Jedes Segment einzeln validiert (kein ":" im Segment-Zeichensatz) — sonst
|
|
18
|
+
// akzeptiert die letzte Segment-Gruppe trailing/doppelte Colons wie
|
|
19
|
+
// "tickets:write:close:" als valide.
|
|
20
|
+
export const VALID_QN_RE =
|
|
21
|
+
/^[a-zA-Z][a-zA-Z0-9-]*:write:[a-zA-Z][a-zA-Z0-9-]*(:[a-zA-Z][a-zA-Z0-9-]*)*$/;
|
|
@@ -28,7 +28,7 @@ export function writeSecurityBaselines(guards: readonly AstGuard[], project: Pro
|
|
|
28
28
|
for (const root of roots) {
|
|
29
29
|
const load = loadSecurityBaseline(root.name, root.absPath);
|
|
30
30
|
if (load.kind === "invalid") {
|
|
31
|
-
console.error(` ✗ Security
|
|
31
|
+
console.error(` ✗ Security baseline ${load.file}: ${load.reason}`);
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
34
34
|
hardFailByRepo.set(root.name, load.hardFail);
|
|
@@ -38,7 +38,7 @@ export function writeSecurityBaselines(guards: readonly AstGuard[], project: Pro
|
|
|
38
38
|
const hardFail = hardFailByRepo.get(root.name) ?? [];
|
|
39
39
|
const baseline = buildSecurityBaseline(root.name, guardViolations, roots, cwd, hardFail);
|
|
40
40
|
const path = writeSecurityBaseline(baseline, root.absPath);
|
|
41
|
-
console.log(` Security
|
|
41
|
+
console.log(` Security baseline written: ${path} (total ${baseline.total})`);
|
|
42
42
|
for (const guardName of hardFail) {
|
|
43
43
|
const count = guardViolations
|
|
44
44
|
.filter((gv) => gv.guardName === guardName)
|
|
@@ -46,7 +46,7 @@ export function writeSecurityBaselines(guards: readonly AstGuard[], project: Pro
|
|
|
46
46
|
.filter((v) => locateFinding(v.file, roots, cwd)?.repo === root.name).length;
|
|
47
47
|
if (count > 0) {
|
|
48
48
|
console.warn(
|
|
49
|
-
` ! ${root.name}: ${count}
|
|
49
|
+
` ! ${root.name}: ${count} findings from ${guardName} not frozen (hardFail) — the guard run blocks them`,
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -98,18 +98,18 @@ export function loadSecurityBaseline(repo: string, repoDir: string): SecurityBas
|
|
|
98
98
|
return {
|
|
99
99
|
kind: "invalid",
|
|
100
100
|
file,
|
|
101
|
-
reason: `
|
|
101
|
+
reason: `broken JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
const parsed = parseSecurityBaseline(raw);
|
|
105
105
|
if (!parsed) {
|
|
106
|
-
return { kind: "invalid", file, reason: "
|
|
106
|
+
return { kind: "invalid", file, reason: "unexpected baseline format (format/repo/findings)" };
|
|
107
107
|
}
|
|
108
108
|
if (parsed.repo !== repo) {
|
|
109
109
|
return {
|
|
110
110
|
kind: "invalid",
|
|
111
111
|
file,
|
|
112
|
-
reason: `repo
|
|
112
|
+
reason: `repo field "${parsed.repo}" does not match the file name "${repo}"`,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
return { kind: "ok", findings: parsed.findings, hardFail: parsed.hardFail ?? [] };
|
|
@@ -192,7 +192,7 @@ export function applySecurityBaseline(args: {
|
|
|
192
192
|
{
|
|
193
193
|
file: baseline.file,
|
|
194
194
|
line: 1,
|
|
195
|
-
message: `Security
|
|
195
|
+
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
196
196
|
},
|
|
197
197
|
]);
|
|
198
198
|
for (const e of allEntries) blockingEntries.push([e.index, e.violation]);
|
|
@@ -204,7 +204,7 @@ export function applySecurityBaseline(args: {
|
|
|
204
204
|
e.index,
|
|
205
205
|
{
|
|
206
206
|
...e.violation,
|
|
207
|
-
message: `${e.violation.message} (
|
|
207
|
+
message: `${e.violation.message} (security baseline ${repo}: ${guardName} finished migrating — no baseline tolerance)`,
|
|
208
208
|
},
|
|
209
209
|
]);
|
|
210
210
|
}
|
|
@@ -224,7 +224,7 @@ export function applySecurityBaseline(args: {
|
|
|
224
224
|
e.index,
|
|
225
225
|
{
|
|
226
226
|
...e.violation,
|
|
227
|
-
message: `${e.violation.message} (
|
|
227
|
+
message: `${e.violation.message} (security baseline ${repo}: allowed=${regression.baseline}, current=${regression.current})`,
|
|
228
228
|
},
|
|
229
229
|
]);
|
|
230
230
|
}
|
|
@@ -247,7 +247,7 @@ export function applySecurityBaseline(args: {
|
|
|
247
247
|
{
|
|
248
248
|
file: baseline.file,
|
|
249
249
|
line: 1,
|
|
250
|
-
message: `Security
|
|
250
|
+
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
251
251
|
},
|
|
252
252
|
]);
|
|
253
253
|
continue;
|
|
@@ -267,7 +267,7 @@ export function applySecurityBaseline(args: {
|
|
|
267
267
|
{
|
|
268
268
|
file: join(root.absPath, r.file),
|
|
269
269
|
line: 1,
|
|
270
|
-
message: `Security
|
|
270
|
+
message: `Security baseline stale: ${root.name}/${r.file} allows ${r.baseline}, found ${r.current} — run \`--write-security-baseline\` and commit, otherwise the headroom covers new findings.`,
|
|
271
271
|
},
|
|
272
272
|
]);
|
|
273
273
|
}
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Add a consumer CLI entry so bunx @cosmicdrift/kumiko-guards runs all three suites"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"version": "0.3.0",
|
|
9
|
+
"type": "fix",
|
|
10
|
+
"title": "Fix two false-positive findings in the Secret-Literal and Thin-Wrappers guards",
|
|
11
|
+
"detail": "The Secret-Literal Guard flagged secret-fallback patterns documented inside comments: only line-start `//` comments were skipped, so a block-comment line (JSDoc-style `*`, `/*`, `*/`) explaining the rule tripped the guard on its own documentation. Block-comment lines are now recognized as comments too. The Thin-Wrappers Guard misread `/regex/.test(x)` as a call to a function literally named `test`, reporting the enclosing function as a thin wrapper around it. That misclassification no longer fires."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.3.0",
|
|
15
|
+
"type": "improvement",
|
|
16
|
+
"title": "Port the last five infra/guards guards",
|
|
17
|
+
"detail": "app-feature-structure, lib-test-coverage, i18n-locale-terminology, feature-integration-tests, and test-stack-drift ported from the private infra/guards package into the public @cosmicdrift/kumiko-guards package. The latter two were rebuilt from ad-hoc infra scripts onto the public RepoCheck pattern; the other three are literal AstGuard ports."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.3.0",
|
|
21
|
+
"type": "improvement",
|
|
22
|
+
"title": "Add startup banner, fail-closed on zero roots/guards, and finish English output"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "0.2.0",
|
|
26
|
+
"type": "improvement",
|
|
27
|
+
"title": "Migrate remaining framework-semantic guards out of infra/guards"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"version": "0.2.0",
|
|
31
|
+
"type": "improvement",
|
|
32
|
+
"title": "Add runner parity with the private infra/guards package"
|
|
33
|
+
},
|
|
2
34
|
{
|
|
3
35
|
"version": "0.1.1",
|
|
4
36
|
"type": "fix",
|