@lyse-labs/lyse 0.1.0-alpha.2 → 0.2.0-alpha.1
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 +0 -5
- package/dist/cli.js +67 -11
- package/dist/cli.js.map +1 -1
- package/dist/commands/__tests__/add-ci-gate.test.d.ts +1 -0
- package/dist/commands/__tests__/add-ci-gate.test.js +149 -0
- package/dist/commands/__tests__/add-ci-gate.test.js.map +1 -0
- package/dist/commands/add-ci-gate.d.ts +26 -0
- package/dist/commands/add-ci-gate.js +429 -0
- package/dist/commands/add-ci-gate.js.map +1 -0
- package/dist/commands/audit-pipeline.js +1 -1
- package/dist/commands/audit-pipeline.js.map +1 -1
- package/dist/commands/fix.d.ts +8 -8
- package/dist/commands/init.d.ts +3 -3
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp-setup.js +1 -1
- package/dist/commands/mcp-setup.js.map +1 -1
- package/dist/config/schema.d.ts +14 -14
- package/dist/llm/__tests__/layer4-stage.test.d.ts +1 -0
- package/dist/llm/__tests__/layer4-stage.test.js +157 -0
- package/dist/llm/__tests__/layer4-stage.test.js.map +1 -0
- package/dist/llm/__tests__/validator.test.d.ts +1 -0
- package/dist/llm/__tests__/validator.test.js +156 -0
- package/dist/llm/__tests__/validator.test.js.map +1 -0
- package/dist/llm/connectors/__tests__/anthropic-adapter.test.d.ts +1 -0
- package/dist/llm/connectors/__tests__/anthropic-adapter.test.js +99 -0
- package/dist/llm/connectors/__tests__/anthropic-adapter.test.js.map +1 -0
- package/dist/llm/connectors/__tests__/cache.test.d.ts +1 -0
- package/dist/llm/connectors/__tests__/cache.test.js +50 -0
- package/dist/llm/connectors/__tests__/cache.test.js.map +1 -0
- package/dist/llm/connectors/__tests__/noop-adapter.test.d.ts +1 -0
- package/dist/llm/connectors/__tests__/noop-adapter.test.js +21 -0
- package/dist/llm/connectors/__tests__/noop-adapter.test.js.map +1 -0
- package/dist/llm/connectors/__tests__/openai-compatible-adapter.test.d.ts +1 -0
- package/dist/llm/connectors/__tests__/openai-compatible-adapter.test.js +82 -0
- package/dist/llm/connectors/__tests__/openai-compatible-adapter.test.js.map +1 -0
- package/dist/llm/connectors/__tests__/resolver.test.d.ts +1 -0
- package/dist/llm/connectors/__tests__/resolver.test.js +140 -0
- package/dist/llm/connectors/__tests__/resolver.test.js.map +1 -0
- package/dist/llm/connectors/anthropic-adapter.d.ts +12 -0
- package/dist/llm/connectors/anthropic-adapter.js +57 -0
- package/dist/llm/connectors/anthropic-adapter.js.map +1 -0
- package/dist/llm/connectors/cache.d.ts +15 -0
- package/dist/llm/connectors/cache.js +57 -0
- package/dist/llm/connectors/cache.js.map +1 -0
- package/dist/llm/connectors/noop-adapter.d.ts +4 -0
- package/dist/llm/connectors/noop-adapter.js +12 -0
- package/dist/llm/connectors/noop-adapter.js.map +1 -0
- package/dist/llm/connectors/openai-compatible-adapter.d.ts +13 -0
- package/dist/llm/connectors/openai-compatible-adapter.js +47 -0
- package/dist/llm/connectors/openai-compatible-adapter.js.map +1 -0
- package/dist/llm/connectors/resolver.d.ts +9 -0
- package/dist/llm/connectors/resolver.js +162 -0
- package/dist/llm/connectors/resolver.js.map +1 -0
- package/dist/llm/connectors/types.d.ts +25 -0
- package/dist/llm/connectors/types.js +13 -0
- package/dist/llm/connectors/types.js.map +1 -0
- package/dist/llm/layer4-stage.d.ts +8 -6
- package/dist/llm/layer4-stage.js +145 -9
- package/dist/llm/layer4-stage.js.map +1 -1
- package/dist/llm/rubric-stub.d.ts +8 -0
- package/dist/llm/rubric-stub.js +4 -0
- package/dist/llm/rubric-stub.js.map +1 -0
- package/dist/llm/validator.d.ts +18 -0
- package/dist/llm/validator.js +81 -0
- package/dist/llm/validator.js.map +1 -0
- package/dist/parsers/ai-tokens.d.ts +10 -0
- package/dist/parsers/ai-tokens.js +156 -0
- package/dist/parsers/ai-tokens.js.map +1 -0
- package/dist/reliability/catalogue/__tests__/sub-axes.test.js +13 -3
- package/dist/reliability/catalogue/__tests__/sub-axes.test.js.map +1 -1
- package/dist/reliability/catalogue/sub-axes.js +16 -0
- package/dist/reliability/catalogue/sub-axes.js.map +1 -1
- package/dist/reliability/types.d.ts +1 -1
- package/dist/rule-runner.js +1 -1
- package/dist/rule-runner.js.map +1 -1
- package/dist/rules/ai-governance-ai-content-live-region.d.ts +11 -0
- package/dist/rules/ai-governance-ai-content-live-region.js +304 -0
- package/dist/rules/ai-governance-ai-content-live-region.js.map +1 -0
- package/dist/rules/ai-governance-ai-loading-error-states.d.ts +9 -0
- package/dist/rules/ai-governance-ai-loading-error-states.js +214 -0
- package/dist/rules/ai-governance-ai-loading-error-states.js.map +1 -0
- package/dist/rules/ai-governance-ai-marker-anti-patterns.d.ts +15 -0
- package/dist/rules/ai-governance-ai-marker-anti-patterns.js +178 -0
- package/dist/rules/ai-governance-ai-marker-anti-patterns.js.map +1 -0
- package/dist/rules/ai-governance-ai-marker-component-present.d.ts +28 -0
- package/dist/rules/ai-governance-ai-marker-component-present.js +320 -0
- package/dist/rules/ai-governance-ai-marker-component-present.js.map +1 -0
- package/dist/rules/ai-governance-ai-token-requires-marker.d.ts +17 -0
- package/dist/rules/ai-governance-ai-token-requires-marker.js +206 -0
- package/dist/rules/ai-governance-ai-token-requires-marker.js.map +1 -0
- package/dist/rules/ai-governance-ai-tokens-reserved.d.ts +8 -0
- package/dist/rules/ai-governance-ai-tokens-reserved.js +85 -0
- package/dist/rules/ai-governance-ai-tokens-reserved.js.map +1 -0
- package/dist/rules/ai-governance-disclaimer-present.d.ts +18 -0
- package/dist/rules/ai-governance-disclaimer-present.js +210 -0
- package/dist/rules/ai-governance-disclaimer-present.js.map +1 -0
- package/dist/rules/ai-governance-explainability-affordance.d.ts +14 -0
- package/dist/rules/ai-governance-explainability-affordance.js +196 -0
- package/dist/rules/ai-governance-explainability-affordance.js.map +1 -0
- package/dist/rules/ai-governance-feedback-control-present.d.ts +19 -0
- package/dist/rules/ai-governance-feedback-control-present.js +223 -0
- package/dist/rules/ai-governance-feedback-control-present.js.map +1 -0
- package/dist/rules/ai-governance-human-control-affordances.d.ts +16 -0
- package/dist/rules/ai-governance-human-control-affordances.js +228 -0
- package/dist/rules/ai-governance-human-control-affordances.js.map +1 -0
- package/dist/rules/ai-governance-value-gate-doc-present.d.ts +18 -0
- package/dist/rules/ai-governance-value-gate-doc-present.js +206 -0
- package/dist/rules/ai-governance-value-gate-doc-present.js.map +1 -0
- package/dist/rules/ai-surface-agent-instruction-files.d.ts +33 -0
- package/dist/rules/ai-surface-agent-instruction-files.js +310 -0
- package/dist/rules/ai-surface-agent-instruction-files.js.map +1 -0
- package/dist/rules/ai-surface-llms-txt-structure.d.ts +18 -0
- package/dist/rules/ai-surface-llms-txt-structure.js +200 -0
- package/dist/rules/ai-surface-llms-txt-structure.js.map +1 -0
- package/dist/rules/ai-surface-mcp-config-present.d.ts +23 -0
- package/dist/rules/ai-surface-mcp-config-present.js +193 -0
- package/dist/rules/ai-surface-mcp-config-present.js.map +1 -0
- package/dist/rules/ai-surface-shadcn-registry-valid.d.ts +22 -0
- package/dist/rules/ai-surface-shadcn-registry-valid.js +247 -0
- package/dist/rules/ai-surface-shadcn-registry-valid.js.map +1 -0
- package/dist/rules/components-contracts-strictness.d.ts +48 -0
- package/dist/rules/components-contracts-strictness.js +372 -0
- package/dist/rules/components-contracts-strictness.js.map +1 -0
- package/dist/rules/registry.js +32 -0
- package/dist/rules/registry.js.map +1 -1
- package/dist/rules/tokens-dtcg-conformance.d.ts +55 -19
- package/dist/rules/tokens-dtcg-conformance.js +320 -82
- package/dist/rules/tokens-dtcg-conformance.js.map +1 -1
- package/dist/scorer.js +4 -3
- package/dist/scorer.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/package.json +41 -16
- package/rules-manifest.json +431 -6
- package/schemas/v1/lyse-rules.json +1 -1
- package/dist/commands/ci-setup.d.ts +0 -9
- package/dist/commands/ci-setup.js +0 -42
- package/dist/commands/ci-setup.js.map +0 -1
- package/dist/commands/templates/lyse-workflow.yml.template +0 -30
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Rule } from "../types.js";
|
|
2
|
+
export declare function detectNamedLoadingWithText(source: string, componentName: string): boolean;
|
|
3
|
+
export declare function detectAiErrorState(source: string, componentName: string): boolean;
|
|
4
|
+
export declare const _internal: {
|
|
5
|
+
detectNamedLoadingWithText: typeof detectNamedLoadingWithText;
|
|
6
|
+
detectAiErrorState: typeof detectAiErrorState;
|
|
7
|
+
isAllowlisted: (repoRoot: string) => boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const rule: Rule;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { createLyseRule } from "./_rule-module.js";
|
|
4
|
+
import { isAiMarkerName, safeReadText, extractNamesFromSource, extractVueNames, COMPONENT_GLOB, SCAN_IGNORE, deriveComponentNameFromPath, makeAllowlistCheck, } from "./ai-governance-ai-marker-component-present.js";
|
|
5
|
+
const RULE_ID = "ai-governance/ai-loading-error-states";
|
|
6
|
+
const DISABLE_DIRECTIVE = `lyse-disable ${RULE_ID}`;
|
|
7
|
+
const isAllowlisted = makeAllowlistCheck(DISABLE_DIRECTIVE);
|
|
8
|
+
// Component names whose presence alone implies a named AI loading state.
|
|
9
|
+
const LOADING_NAME_PATTERNS = [
|
|
10
|
+
"generating",
|
|
11
|
+
"thinking",
|
|
12
|
+
"ailoading",
|
|
13
|
+
"streamingindicator",
|
|
14
|
+
"loadingstate",
|
|
15
|
+
"aistatus",
|
|
16
|
+
];
|
|
17
|
+
// Text signals that pair with the component to confirm it is not a bare spinner.
|
|
18
|
+
// Matches: loadingText prop, aria-label attribute, or a visible AI-progress
|
|
19
|
+
// status string (quoted or unquoted in JSX text content).
|
|
20
|
+
const PAIRED_TEXT_RE = /loadingText|aria-label\s*=|["` >]Generating|["` >]Thinking|Please wait|["` >]Loading AI/i;
|
|
21
|
+
// Names that are unambiguously descriptive and carry implicit text semantics.
|
|
22
|
+
const SELF_DESCRIBING_LOADING = ["generating", "thinking", "streamingindicator"];
|
|
23
|
+
export function detectNamedLoadingWithText(source, componentName) {
|
|
24
|
+
const lower = componentName.toLowerCase();
|
|
25
|
+
const hasNamedMatch = LOADING_NAME_PATTERNS.some((p) => lower.includes(p));
|
|
26
|
+
if (!hasNamedMatch)
|
|
27
|
+
return false;
|
|
28
|
+
if (SELF_DESCRIBING_LOADING.some((p) => lower.includes(p)))
|
|
29
|
+
return true;
|
|
30
|
+
return PAIRED_TEXT_RE.test(source);
|
|
31
|
+
}
|
|
32
|
+
// Match "ai" only on a word/segment boundary to avoid false positives from
|
|
33
|
+
// substrings like "email", "retail", "cocktail".
|
|
34
|
+
const AI_WORD_RE = /(^|[^a-z])ai([^a-z]|$)/;
|
|
35
|
+
const AI_COMPOUND_KEYWORDS = ["generation", "genai", "llm", "generative"];
|
|
36
|
+
const ERROR_KEYWORDS = ["error", "failure", "failed", "timeout"];
|
|
37
|
+
const ERROR_NAME_PATTERNS = [
|
|
38
|
+
"aierror",
|
|
39
|
+
"generationerror",
|
|
40
|
+
"aifailure",
|
|
41
|
+
"generationfailed",
|
|
42
|
+
"aitimeout",
|
|
43
|
+
];
|
|
44
|
+
export function detectAiErrorState(source, componentName) {
|
|
45
|
+
const lower = componentName.toLowerCase();
|
|
46
|
+
if (ERROR_NAME_PATTERNS.some((p) => lower.includes(p)))
|
|
47
|
+
return true;
|
|
48
|
+
const hasAi = AI_WORD_RE.test(lower) || AI_COMPOUND_KEYWORDS.some((k) => lower.includes(k));
|
|
49
|
+
const hasError = ERROR_KEYWORDS.some((k) => lower.includes(k));
|
|
50
|
+
return hasAi && hasError;
|
|
51
|
+
}
|
|
52
|
+
export const _internal = { detectNamedLoadingWithText, detectAiErrorState, isAllowlisted };
|
|
53
|
+
function namesFromFile(source, relPath) {
|
|
54
|
+
if (relPath.endsWith(".vue"))
|
|
55
|
+
return extractVueNames(source);
|
|
56
|
+
const fromSource = extractNamesFromSource(source);
|
|
57
|
+
if (fromSource.length > 0)
|
|
58
|
+
return fromSource;
|
|
59
|
+
return [deriveComponentNameFromPath(relPath)];
|
|
60
|
+
}
|
|
61
|
+
function hasAiSurface(files, repoRoot) {
|
|
62
|
+
for (const rel of files) {
|
|
63
|
+
const source = safeReadText(join(repoRoot, rel));
|
|
64
|
+
if (!source)
|
|
65
|
+
continue;
|
|
66
|
+
const names = namesFromFile(source, rel);
|
|
67
|
+
const pathName = deriveComponentNameFromPath(rel);
|
|
68
|
+
const allNames = [...names, pathName];
|
|
69
|
+
if (allNames.some((n) => isAiMarkerName(n)))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const evaluate = async (ctx, _files) => {
|
|
75
|
+
const findings = [];
|
|
76
|
+
if (!ctx.repoRoot)
|
|
77
|
+
return { findings, opportunities: 0 };
|
|
78
|
+
if (isAllowlisted(ctx.repoRoot))
|
|
79
|
+
return { findings, opportunities: 0 };
|
|
80
|
+
let componentFiles = [];
|
|
81
|
+
try {
|
|
82
|
+
componentFiles = fg.sync(COMPONENT_GLOB, {
|
|
83
|
+
cwd: ctx.repoRoot,
|
|
84
|
+
absolute: false,
|
|
85
|
+
dot: false,
|
|
86
|
+
ignore: SCAN_IGNORE,
|
|
87
|
+
onlyFiles: true,
|
|
88
|
+
unique: true,
|
|
89
|
+
}).sort();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { findings, opportunities: 0 };
|
|
93
|
+
}
|
|
94
|
+
if (!hasAiSurface(componentFiles, ctx.repoRoot)) {
|
|
95
|
+
return { findings, opportunities: 0 };
|
|
96
|
+
}
|
|
97
|
+
let foundNamedLoading = false;
|
|
98
|
+
let foundAiError = false;
|
|
99
|
+
for (const rel of componentFiles) {
|
|
100
|
+
const source = safeReadText(join(ctx.repoRoot, rel));
|
|
101
|
+
if (!source)
|
|
102
|
+
continue;
|
|
103
|
+
const names = namesFromFile(source, rel);
|
|
104
|
+
const pathName = deriveComponentNameFromPath(rel);
|
|
105
|
+
const allNames = [...names, pathName];
|
|
106
|
+
for (const name of allNames) {
|
|
107
|
+
if (!foundNamedLoading && detectNamedLoadingWithText(source, name)) {
|
|
108
|
+
foundNamedLoading = true;
|
|
109
|
+
}
|
|
110
|
+
if (!foundAiError && detectAiErrorState(source, name)) {
|
|
111
|
+
foundAiError = true;
|
|
112
|
+
}
|
|
113
|
+
if (foundNamedLoading && foundAiError)
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (foundNamedLoading && foundAiError)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const location = { file: "src/index.ts", line: 1, column: 1 };
|
|
120
|
+
if (foundNamedLoading && foundAiError) {
|
|
121
|
+
findings.push({
|
|
122
|
+
ruleId: RULE_ID,
|
|
123
|
+
axis: "ai-governance",
|
|
124
|
+
severity: "info",
|
|
125
|
+
location,
|
|
126
|
+
message: "AI loading state (named, with paired text) and AI-specific error state detected — both present.",
|
|
127
|
+
suggestion: "Ensure the error state communicates the AI context clearly and the loading state always carries visible or accessible text (AWS Cloudscape gen-AI pattern).",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!foundNamedLoading) {
|
|
132
|
+
findings.push({
|
|
133
|
+
ruleId: RULE_ID,
|
|
134
|
+
axis: "ai-governance",
|
|
135
|
+
severity: "warning",
|
|
136
|
+
location,
|
|
137
|
+
message: "AI surface detected but no named AI loading state with paired text found — a bare spinner is not sufficient (AWS Cloudscape gen-AI: loading states must carry visible text, e.g. \"Generating response…\").",
|
|
138
|
+
suggestion: "Add a named component such as Generating, Thinking, AILoading, or StreamingIndicator that always renders a visible/accessible status string alongside the spinner.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!foundAiError) {
|
|
142
|
+
findings.push({
|
|
143
|
+
ruleId: RULE_ID,
|
|
144
|
+
axis: "ai-governance",
|
|
145
|
+
severity: "warning",
|
|
146
|
+
location,
|
|
147
|
+
message: "AI surface detected but no AI-specific error state found — generic error boundaries do not communicate AI-specific failure context.",
|
|
148
|
+
suggestion: "Add a component such as AIError or GenerationError that clearly communicates the AI operation failed and what the user can do next.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { findings, opportunities: componentFiles.length };
|
|
153
|
+
};
|
|
154
|
+
export const rule = createLyseRule({
|
|
155
|
+
meta: {
|
|
156
|
+
axis: "ai-governance",
|
|
157
|
+
lyseRuleId: RULE_ID,
|
|
158
|
+
defaultSeverity: "warning",
|
|
159
|
+
shortDescription: "Named AI loading state with paired text + AI-specific error state present",
|
|
160
|
+
fullDescription: "Scans component files (`**/*.{tsx,jsx,vue}`) for (a) a named AI loading state that carries paired visible or accessible text — not a bare spinner — and (b) an AI-specific error state component. Recognised loading vocabulary: `*Generating*`, `*Thinking*`, `*AILoading*`, `*StreamingIndicator*`, `*AIStatus*`, `*LoadingState*`. A bare generic spinner (`Spinner`, `LoadingSpinner`) without an AI-named wrapper and without a `loadingText` prop or visible status string does NOT satisfy the requirement. Recognised error vocabulary: `*AIError*`, `*GenerationError*`, `*AIFailure*`, `*GenerationFailed*`, `*AITimeout*`, or any name combining an AI keyword (`ai`, `generation`, `genai`, `llm`, `generative`) with an error keyword (`error`, `failure`, `failed`, `timeout`). Emits `warning` for each absent state type when an AI marker surface is detected; emits `info` when both are present; emits nothing when no AI surface is detected. Recovery-flow detection (retry orchestration, post-error behavior) is out of scope — tracked in Track 4 (#16).",
|
|
161
|
+
helpUri: "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-loading-error-states.md",
|
|
162
|
+
rationale: `Why it matters
|
|
163
|
+
|
|
164
|
+
AI-generating surfaces have two failure modes invisible to generic DS rules: (1) a loading state that gives no context — users see a spinner but don't know if the model is generating, stuck, or done — and (2) a generic error boundary that offers no AI-specific message, leaving users without guidance when a generation fails.
|
|
165
|
+
|
|
166
|
+
AWS Cloudscape's generative-AI patterns mandate that every AI loading state carry named, visible text (e.g. "Generating response…") so users understand what the system is doing and can judge when to wait vs. cancel. A bare, unlabelled spinner violates this requirement.
|
|
167
|
+
|
|
168
|
+
An AI-specific error component is equally critical: it must communicate that the AI operation failed (not a generic network error) and ideally suggest next steps — though the recovery-flow logic itself is deferred to Track 4.
|
|
169
|
+
|
|
170
|
+
This rule detects the static presence of both states. It cross-conditions: if an AI marker is found but either state is absent, a warning is emitted; if both are present, an info finding confirms the DS is provisioned for AI-state handling.`,
|
|
171
|
+
examples: [
|
|
172
|
+
{
|
|
173
|
+
good: `// Generating.tsx
|
|
174
|
+
export const Generating = () => (
|
|
175
|
+
<div role="status" aria-live="polite">
|
|
176
|
+
<Spinner /> Generating response…
|
|
177
|
+
</div>
|
|
178
|
+
);`,
|
|
179
|
+
bad: `// LoadingSpinner.tsx — bare spinner, no AI name, no paired text
|
|
180
|
+
export const LoadingSpinner = () => <svg className="spin" />;`,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
good: `// AILoading.tsx — loadingText prop satisfies paired-text requirement
|
|
184
|
+
export function AILoading({ loadingText }: { loadingText: string }) {
|
|
185
|
+
return <div><Spinner /><span>{loadingText}</span></div>;
|
|
186
|
+
}`,
|
|
187
|
+
bad: `// No AI-named loading state at all — only generic Spinner exported`,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
good: `// AIError.tsx
|
|
191
|
+
export const AIError = ({ message }: { message: string }) => (
|
|
192
|
+
<div role="alert">
|
|
193
|
+
<strong>Generation failed</strong>
|
|
194
|
+
<p>{message}</p>
|
|
195
|
+
</div>
|
|
196
|
+
);`,
|
|
197
|
+
bad: `// ErrorBoundary.tsx — generic, gives no AI context
|
|
198
|
+
export class ErrorBoundary extends React.Component {
|
|
199
|
+
render() { return this.props.children; }
|
|
200
|
+
}`,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
allowlist: [
|
|
204
|
+
"repos containing `lyse-disable ai-governance/ai-loading-error-states` in an adjacent README, README.md, README.mdx, .lyse.yaml, or .lyse.yml — rule is N/A",
|
|
205
|
+
"repos with no AI marker surface detected (no AILabel, AIBadge, magic-* etc.) — no AI surface, rule emits nothing",
|
|
206
|
+
"files larger than 1 MB — skipped to avoid pathological cases",
|
|
207
|
+
"files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`",
|
|
208
|
+
"recovery-flow detection (retry orchestration, post-error navigation) — explicitly deferred to Track 4 (#16)",
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
defaultOptions: [],
|
|
212
|
+
create: () => ({ evaluate }),
|
|
213
|
+
});
|
|
214
|
+
//# sourceMappingURL=ai-governance-ai-loading-error-states.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-governance-ai-loading-error-states.js","sourceRoot":"","sources":["../../src/rules/ai-governance-ai-loading-error-states.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Rule } from "../types.js";
|
|
2
|
+
export declare function detectSparkleOnlyMarker(source: string): boolean;
|
|
3
|
+
export declare function fileHasAiContext(source: string): boolean;
|
|
4
|
+
export declare function detectAiInCtaLabel(source: string): {
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare const rule: Rule;
|
|
8
|
+
export declare const _internal: {
|
|
9
|
+
detectSparkleOnlyMarker: typeof detectSparkleOnlyMarker;
|
|
10
|
+
detectAiInCtaLabel: typeof detectAiInCtaLabel;
|
|
11
|
+
fileHasAiContext: typeof fileHasAiContext;
|
|
12
|
+
isAllowlisted: (repoRoot: string) => boolean;
|
|
13
|
+
DISABLE_DIRECTIVE: string;
|
|
14
|
+
ALLOWLIST_CANDIDATES: string[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { createLyseRule } from "./_rule-module.js";
|
|
4
|
+
import { isAiMarkerName, extractNamesFromSource, safeReadText, COMPONENT_GLOB, SCAN_IGNORE, makeAllowlistCheck, } from "./ai-governance-ai-marker-component-present.js";
|
|
5
|
+
const RULE_ID = "ai-governance/ai-marker-anti-patterns";
|
|
6
|
+
const DISABLE_DIRECTIVE = `lyse-disable ${RULE_ID}`;
|
|
7
|
+
const ALLOWLIST_CANDIDATES = [
|
|
8
|
+
"README.md",
|
|
9
|
+
"README",
|
|
10
|
+
"README.mdx",
|
|
11
|
+
"readme.md",
|
|
12
|
+
".lyse.yaml",
|
|
13
|
+
".lyse.yml",
|
|
14
|
+
];
|
|
15
|
+
const isAllowlisted = makeAllowlistCheck(DISABLE_DIRECTIVE);
|
|
16
|
+
const SPARKLE_LITERAL = /[✨✩\u{1F31F}❇]/u;
|
|
17
|
+
const SPARKLE_IMPORT = /\b(Sparkle|Sparkles|SparkleIcon)\b/;
|
|
18
|
+
const SPARKLE_ICON_PROP = /icon\s*=\s*["'`]\s*sparkle/i;
|
|
19
|
+
const TEXT_MARKER_COMPOUND = /\b(generated by ai|ai[- ]generated|powered by ai)\b/i;
|
|
20
|
+
const TEXT_MARKER_STANDALONE = /(^|[^A-Za-z])AI([^A-Za-z]|$)/;
|
|
21
|
+
const CTA_STRIP = /<(?:button|Button|a)\b[^>]*>[\s\S]*?<\/(?:button|Button|a)\s*>|<[A-Za-z][\w.-]*\b[^>]*\brole\s*=\s*["']button["'][^>]*>[\s\S]*?<\/[A-Za-z][\w.-]*\s*>/g;
|
|
22
|
+
export function detectSparkleOnlyMarker(source) {
|
|
23
|
+
const hasSparkle = SPARKLE_LITERAL.test(source) ||
|
|
24
|
+
SPARKLE_IMPORT.test(source) ||
|
|
25
|
+
SPARKLE_ICON_PROP.test(source);
|
|
26
|
+
if (!hasSparkle)
|
|
27
|
+
return false;
|
|
28
|
+
if (TEXT_MARKER_COMPOUND.test(source))
|
|
29
|
+
return false;
|
|
30
|
+
const stripped = source.replace(CTA_STRIP, " ");
|
|
31
|
+
if (TEXT_MARKER_STANDALONE.test(stripped))
|
|
32
|
+
return false;
|
|
33
|
+
for (const tag of source.matchAll(/<\s*([A-Za-z][\w.-]*)/g)) {
|
|
34
|
+
if (tag[1] !== undefined && isAiMarkerName(tag[1]))
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export function fileHasAiContext(source) {
|
|
40
|
+
for (const name of extractNamesFromSource(source)) {
|
|
41
|
+
if (isAiMarkerName(name))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
for (const tag of source.matchAll(/<\s*([A-Za-z][\w.-]*)/g)) {
|
|
45
|
+
if (tag[1] !== undefined && isAiMarkerName(tag[1]))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const CTA_OPEN = /<(button|Button|a)\b[^>]*>|<[A-Za-z][\w.-]*\b[^>]*\brole\s*=\s*["']button["'][^>]*>/g;
|
|
51
|
+
const STANDALONE_AI = /(^|[^A-Za-z])AI([^A-Za-z]|$)/;
|
|
52
|
+
export function detectAiInCtaLabel(source) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let m;
|
|
55
|
+
CTA_OPEN.lastIndex = 0;
|
|
56
|
+
while ((m = CTA_OPEN.exec(source)) !== null) {
|
|
57
|
+
const openTag = m[0];
|
|
58
|
+
const tagNameMatch = /^<([A-Za-z][\w.-]*)/.exec(openTag);
|
|
59
|
+
const tagName = tagNameMatch?.[1] ?? "div";
|
|
60
|
+
const rest = source.slice(m.index + openTag.length);
|
|
61
|
+
const closeRe = new RegExp(`<\\/\\s*${tagName}\\s*>`);
|
|
62
|
+
const close = rest.search(closeRe);
|
|
63
|
+
const inner = (close === -1 ? rest : rest.slice(0, close))
|
|
64
|
+
.replace(/<[^>]*>/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
if (inner.length > 0 && STANDALONE_AI.test(inner)) {
|
|
67
|
+
out.push({ label: inner.replace(/\s+/g, " ") });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
const evaluate = async (ctx, _files) => {
|
|
73
|
+
const findings = [];
|
|
74
|
+
if (!ctx.repoRoot) {
|
|
75
|
+
return { findings, opportunities: 0 };
|
|
76
|
+
}
|
|
77
|
+
if (isAllowlisted(ctx.repoRoot)) {
|
|
78
|
+
return { findings, opportunities: 0 };
|
|
79
|
+
}
|
|
80
|
+
let componentFiles = [];
|
|
81
|
+
try {
|
|
82
|
+
componentFiles = fg
|
|
83
|
+
.sync(COMPONENT_GLOB, {
|
|
84
|
+
cwd: ctx.repoRoot,
|
|
85
|
+
ignore: SCAN_IGNORE,
|
|
86
|
+
absolute: false,
|
|
87
|
+
dot: false,
|
|
88
|
+
onlyFiles: true,
|
|
89
|
+
unique: true,
|
|
90
|
+
})
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// non-fatal
|
|
95
|
+
}
|
|
96
|
+
for (const rel of componentFiles) {
|
|
97
|
+
const source = safeReadText(join(ctx.repoRoot, rel));
|
|
98
|
+
if (!source)
|
|
99
|
+
continue;
|
|
100
|
+
if (fileHasAiContext(source) && detectSparkleOnlyMarker(source)) {
|
|
101
|
+
findings.push({
|
|
102
|
+
ruleId: RULE_ID,
|
|
103
|
+
axis: "ai-governance",
|
|
104
|
+
severity: "warning",
|
|
105
|
+
location: { file: rel, line: 1, column: 1 },
|
|
106
|
+
message: "Sparkle used as the sole AI marker — pair it with a text label or AI-marker component (SAP Fiori XAI forbids icon-only AI marking).",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (const hit of detectAiInCtaLabel(source)) {
|
|
110
|
+
findings.push({
|
|
111
|
+
ruleId: RULE_ID,
|
|
112
|
+
axis: "ai-governance",
|
|
113
|
+
severity: "warning",
|
|
114
|
+
location: { file: rel, line: 1, column: 1 },
|
|
115
|
+
message: `"AI" used as a CTA action label ("${hit.label}") — describe the action, not the technology (GitLab Pajamas).`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
findings.sort((a, b) => {
|
|
120
|
+
const fa = a.location?.file ?? "";
|
|
121
|
+
const fb = b.location?.file ?? "";
|
|
122
|
+
if (fa < fb)
|
|
123
|
+
return -1;
|
|
124
|
+
if (fa > fb)
|
|
125
|
+
return 1;
|
|
126
|
+
return (a.location?.line ?? 0) - (b.location?.line ?? 0);
|
|
127
|
+
});
|
|
128
|
+
return { findings, opportunities: componentFiles.length };
|
|
129
|
+
};
|
|
130
|
+
export const rule = createLyseRule({
|
|
131
|
+
meta: {
|
|
132
|
+
axis: "ai-governance",
|
|
133
|
+
lyseRuleId: RULE_ID,
|
|
134
|
+
defaultSeverity: "warning",
|
|
135
|
+
shortDescription: "Lint AI-marker anti-patterns in component source files",
|
|
136
|
+
fullDescription: "Scans component files (`**/*.{tsx,jsx,vue}`) for two forbidden AI-marking anti-patterns. Anti-pattern A: a sparkle signal (`✨` literal, `Sparkle`/`Sparkles`/`SparkleIcon` import, or `icon=\"sparkle*\"` prop) used as the sole AI marker with no accompanying text label or AI-marker component — SAP Fiori XAI forbids icon-only marking because it fails accessibility and is ambiguous. Anti-pattern B: the standalone, case-sensitive token `AI` used as the primary action label of a `<button>`, `<Button>`, or `<a>` element — GitLab Pajamas forbids this because `AI` describes the technology, not the user action. Both detectors are pure source heuristics with no runtime or behavioral analysis. Emits `warning` for each occurrence. A sparkle accompanied by a text AI-marker or AI-marker component (`AILabel`, `AIBadge`, magic-* etc.) is not flagged. `AI` appearing only in body or descriptor text (headings, paragraphs) is not flagged.",
|
|
137
|
+
helpUri: "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-marker-anti-patterns.md",
|
|
138
|
+
rationale: `Why it matters
|
|
139
|
+
|
|
140
|
+
Icon-only AI marking (a lone sparkle ✨) silently fails users who rely on assistive technology: a screen reader announces an image description or nothing at all — not "AI-generated." SAP Fiori XAI made this explicit: sparkle MUST be paired with a text label or dedicated AI-marker component. Lyse enforces the same constraint statically.
|
|
141
|
+
|
|
142
|
+
Labelling a CTA button "AI" is a UX anti-pattern: "AI" is a noun describing technology, not an action verb. GitLab Pajamas explicitly forbids it, recommending "Summarize," "Explain," or "Generate" instead — words that tell the user what will happen, not what engine does it. Lyse catches this in source before it ships.
|
|
143
|
+
|
|
144
|
+
Both findings are warnings (not errors) because the fix is mechanical and low-risk.`,
|
|
145
|
+
examples: [
|
|
146
|
+
{
|
|
147
|
+
good: '<span><AILabel>AI</AILabel> <Sparkles aria-hidden/></span>',
|
|
148
|
+
bad: '<span>✨ {generatedText}</span>',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
good: '<button>Summarize</button>',
|
|
152
|
+
bad: '<button>AI</button>',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
good: '<button>Draft reply</button>',
|
|
156
|
+
bad: '<button>Ask AI</button>',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
allowlist: [
|
|
160
|
+
"repos containing `lyse-disable ai-governance/ai-marker-anti-patterns` in an adjacent README or `.lyse.yaml` — rule is N/A for the entire repo",
|
|
161
|
+
"files larger than 1 MB — skipped to avoid pathological cases",
|
|
162
|
+
"files under `node_modules/`, `dist/`, `build/`",
|
|
163
|
+
"sparkle accompanied by a text AI-marker (`AILabel`, `AIBadge`, magic-*, GenAI*, etc.) — not flagged",
|
|
164
|
+
"`AI` in non-CTA elements (headings, paragraphs, spans) — not flagged",
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
defaultOptions: [],
|
|
168
|
+
create: () => ({ evaluate }),
|
|
169
|
+
});
|
|
170
|
+
export const _internal = {
|
|
171
|
+
detectSparkleOnlyMarker,
|
|
172
|
+
detectAiInCtaLabel,
|
|
173
|
+
fileHasAiContext,
|
|
174
|
+
isAllowlisted,
|
|
175
|
+
DISABLE_DIRECTIVE,
|
|
176
|
+
ALLOWLIST_CANDIDATES,
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=ai-governance-ai-marker-anti-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-governance-ai-marker-anti-patterns.js","sourceRoot":"","sources":["../../src/rules/ai-governance-ai-marker-anti-patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Rule } from "../types.js";
|
|
2
|
+
export declare const AI_MARKER_NAMES: ReadonlySet<string>;
|
|
3
|
+
export declare const COMPONENT_GLOB = "**/*.{tsx,jsx,vue}";
|
|
4
|
+
export declare const SCAN_IGNORE: string[];
|
|
5
|
+
declare function isAllowlisted(repoRoot: string): boolean;
|
|
6
|
+
export declare function isAiMarkerName(name: string): boolean;
|
|
7
|
+
export declare function safeReadText(absPath: string): string | null;
|
|
8
|
+
export declare function extractNamesFromSource(source: string): string[];
|
|
9
|
+
export declare function extractVueNames(source: string): string[];
|
|
10
|
+
export declare function deriveComponentNameFromPath(relPath: string): string;
|
|
11
|
+
export declare function fileHasAiMarker(source: string, relPath: string): boolean;
|
|
12
|
+
export declare function makeAllowlistCheck(disableDirective: string): (repoRoot: string) => boolean;
|
|
13
|
+
export declare function scanForMarkerComponents(repoRoot: string): string[];
|
|
14
|
+
export declare const rule: Rule;
|
|
15
|
+
export declare const _internal: {
|
|
16
|
+
isAiMarkerName: typeof isAiMarkerName;
|
|
17
|
+
isAllowlisted: typeof isAllowlisted;
|
|
18
|
+
scanForMarkerComponents: typeof scanForMarkerComponents;
|
|
19
|
+
extractNamesFromSource: typeof extractNamesFromSource;
|
|
20
|
+
DISABLE_DIRECTIVE: string;
|
|
21
|
+
ALLOWLIST_CANDIDATES: string[];
|
|
22
|
+
COMPONENT_GLOB: string;
|
|
23
|
+
SCAN_IGNORE: string[];
|
|
24
|
+
deriveComponentNameFromPath: typeof deriveComponentNameFromPath;
|
|
25
|
+
fileHasAiMarker: typeof fileHasAiMarker;
|
|
26
|
+
makeAllowlistCheck: typeof makeAllowlistCheck;
|
|
27
|
+
};
|
|
28
|
+
export {};
|