forgedev 1.2.0 → 1.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/README.md +57 -10
- package/bin/chainproof.js +126 -0
- package/package.json +25 -7
- package/src/chainproof-bridge.js +330 -0
- package/src/ci-mode.js +85 -0
- package/src/claude-configurator.js +86 -49
- package/src/cli.js +30 -7
- package/src/composer.js +159 -34
- package/src/doctor-checks-chainproof.js +106 -0
- package/src/doctor-checks.js +39 -20
- package/src/doctor-prompts.js +9 -9
- package/src/doctor.js +37 -4
- package/src/guided.js +3 -3
- package/src/index.js +31 -10
- package/src/init-mode.js +64 -11
- package/src/menu.js +178 -0
- package/src/prompts.js +5 -12
- package/src/recommender.js +134 -10
- package/src/scanner.js +57 -2
- package/src/uat-generator.js +204 -189
- package/src/update-check.js +9 -4
- package/src/update.js +1 -1
- package/src/utils.js +64 -5
- package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
- package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
- package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
- package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
- package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
- package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
- package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
- package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
- package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
- package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
- package/templates/backend/express/Dockerfile.template +18 -0
- package/templates/backend/express/package.json.template +33 -0
- package/templates/backend/express/src/index.ts.template +34 -0
- package/templates/backend/express/src/routes/health.ts.template +27 -0
- package/templates/backend/express/tsconfig.json +17 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
- package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
- package/templates/backend/fastapi/backend/app/main.py.template +3 -1
- package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
- package/templates/backend/hono/Dockerfile.template +18 -0
- package/templates/backend/hono/package.json.template +31 -0
- package/templates/backend/hono/src/index.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +27 -0
- package/templates/backend/hono/tsconfig.json +18 -0
- package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
- package/templates/chainproof/base/.chainproof/config.json.template +11 -0
- package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
- package/templates/chainproof/base/.mcp.json +9 -0
- package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
- package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
- package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
- package/templates/claude-code/agents/architect.md +25 -11
- package/templates/claude-code/agents/build-error-resolver.md +19 -5
- package/templates/claude-code/agents/chief-of-staff.md +42 -8
- package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
- package/templates/claude-code/agents/database-reviewer.md +15 -1
- package/templates/claude-code/agents/deep-reviewer.md +191 -0
- package/templates/claude-code/agents/doc-updater.md +19 -5
- package/templates/claude-code/agents/docs-lookup.md +19 -5
- package/templates/claude-code/agents/e2e-runner.md +26 -12
- package/templates/claude-code/agents/enforcement-gate.md +102 -0
- package/templates/claude-code/agents/frontend-builder.md +188 -0
- package/templates/claude-code/agents/harness-optimizer.md +36 -1
- package/templates/claude-code/agents/loop-operator.md +27 -13
- package/templates/claude-code/agents/planner.md +21 -7
- package/templates/claude-code/agents/product-strategist.md +24 -10
- package/templates/claude-code/agents/production-readiness.md +14 -0
- package/templates/claude-code/agents/prompt-auditor.md +115 -0
- package/templates/claude-code/agents/refactor-cleaner.md +22 -8
- package/templates/claude-code/agents/security-reviewer.md +14 -0
- package/templates/claude-code/agents/spec-validator.md +15 -1
- package/templates/claude-code/agents/tdd-guide.md +21 -7
- package/templates/claude-code/agents/uat-validator.md +14 -0
- package/templates/claude-code/claude-md/base.md +14 -7
- package/templates/claude-code/claude-md/fastapi.md +8 -8
- package/templates/claude-code/claude-md/fullstack.md +6 -6
- package/templates/claude-code/claude-md/hono.md +18 -0
- package/templates/claude-code/claude-md/nextjs.md +5 -5
- package/templates/claude-code/claude-md/remix.md +18 -0
- package/templates/claude-code/commands/audit-security.md +14 -0
- package/templates/claude-code/commands/audit-spec.md +14 -0
- package/templates/claude-code/commands/audit-wiring.md +14 -0
- package/templates/claude-code/commands/build-fix.md +28 -0
- package/templates/claude-code/commands/build-ui.md +59 -0
- package/templates/claude-code/commands/code-review.md +53 -31
- package/templates/claude-code/commands/fix-loop.md +211 -0
- package/templates/claude-code/commands/full-audit.md +36 -8
- package/templates/claude-code/commands/generate-prd.md +1 -1
- package/templates/claude-code/commands/generate-sdd.md +74 -0
- package/templates/claude-code/commands/generate-uat.md +107 -35
- package/templates/claude-code/commands/help.md +68 -0
- package/templates/claude-code/commands/live-uat.md +268 -0
- package/templates/claude-code/commands/optimize-claude-md.md +15 -1
- package/templates/claude-code/commands/plan.md +3 -3
- package/templates/claude-code/commands/pre-pr.md +57 -19
- package/templates/claude-code/commands/product-strategist.md +21 -0
- package/templates/claude-code/commands/resume-session.md +10 -10
- package/templates/claude-code/commands/run-uat.md +59 -2
- package/templates/claude-code/commands/save-session.md +10 -10
- package/templates/claude-code/commands/simplify.md +36 -0
- package/templates/claude-code/commands/tdd.md +17 -18
- package/templates/claude-code/commands/verify-all.md +24 -0
- package/templates/claude-code/commands/verify-intent.md +55 -0
- package/templates/claude-code/commands/workflows.md +52 -40
- package/templates/claude-code/hooks/polyglot.json +10 -1
- package/templates/claude-code/hooks/python.json +10 -1
- package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
- package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
- package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
- package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
- package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
- package/templates/claude-code/hooks/typescript.json +10 -1
- package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
- package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
- package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
- package/templates/claude-code/skills/playwright/SKILL.md +5 -5
- package/templates/claude-code/skills/security-api/SKILL.md +1 -1
- package/templates/claude-code/skills/security-web/SKILL.md +1 -1
- package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
- package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
- package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
- package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
- package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
- package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
- package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
- package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
- package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
- package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
- package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
- package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
- package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
- package/templates/frontend/nextjs/package.json.template +3 -1
- package/templates/frontend/react/index.html.template +12 -0
- package/templates/frontend/react/package.json.template +34 -0
- package/templates/frontend/react/src/App.tsx.template +10 -0
- package/templates/frontend/react/src/index.css +1 -0
- package/templates/frontend/react/src/main.tsx +10 -0
- package/templates/frontend/react/tsconfig.json +17 -0
- package/templates/frontend/react/vite.config.ts.template +15 -0
- package/templates/frontend/react/vitest.config.ts +9 -0
- package/templates/frontend/remix/app/root.tsx.template +31 -0
- package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
- package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
- package/templates/frontend/remix/app/tailwind.css +1 -0
- package/templates/frontend/remix/package.json.template +39 -0
- package/templates/frontend/remix/tsconfig.json +18 -0
- package/templates/frontend/remix/vite.config.ts.template +7 -0
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
- package/docs/00-README.md +0 -310
- package/docs/01-universal-prompt-library.md +0 -1049
- package/docs/02-claude-code-mastery-playbook.md +0 -283
- package/docs/03-multi-agent-verification.md +0 -565
- package/docs/04-errata-and-verification-checklist.md +0 -284
- package/docs/05-universal-scaffolder-vision.md +0 -452
- package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
- package/docs/errata.md +0 -58
- package/docs/multi-agent-verification.md +0 -66
- package/docs/playbook.md +0 -95
- package/docs/prompt-library.md +0 -160
- package/docs/uat/UAT_CHECKLIST.csv +0 -9
- package/docs/uat/UAT_TEMPLATE.md +0 -163
- package/templates/claude-code/commands/done.md +0 -19
- /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Code Hygiene Gate — runs on Stop to enforce structural quality
|
|
3
|
+
// Checks: file length, duplicate code blocks, repeated functions, stale test files
|
|
4
|
+
// Exit 0 = pass (with warnings), Exit 2 = blocked (critical issues found)
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join, relative, extname, basename } from 'node:path';
|
|
8
|
+
|
|
9
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
10
|
+
const MAX_FILE_LINES = 300;
|
|
11
|
+
const MAX_FUNCTION_LINES = 50;
|
|
12
|
+
const MIN_DUPLICATE_LINES = 6; // minimum consecutive matching lines to flag
|
|
13
|
+
const MAX_FILES_PER_DIR = 20; // warn if a single directory has too many files
|
|
14
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']);
|
|
15
|
+
const IGNORE_DIRS = new Set(['node_modules', '.next', '__pycache__', '.git', 'dist', 'build', '.claude', 'coverage', '.venv', 'venv']);
|
|
16
|
+
const IGNORE_FILES = new Set(['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']);
|
|
17
|
+
|
|
18
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
function walk(dir, files = []) {
|
|
20
|
+
let entries;
|
|
21
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
24
|
+
const full = join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
walk(full, files);
|
|
27
|
+
} else if (SOURCE_EXTENSIONS.has(extname(entry.name)) && !IGNORE_FILES.has(entry.name)) {
|
|
28
|
+
files.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLines(filePath) {
|
|
35
|
+
try { return readFileSync(filePath, 'utf-8').split('\n'); } catch { return []; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Checks ──────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function checkFileLengths(files, cwd) {
|
|
41
|
+
const warnings = [];
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const lines = readLines(file);
|
|
44
|
+
if (lines.length > MAX_FILE_LINES) {
|
|
45
|
+
warnings.push({
|
|
46
|
+
level: lines.length > MAX_FILE_LINES * 2 ? 'critical' : 'warning',
|
|
47
|
+
file: relative(cwd, file),
|
|
48
|
+
message: `${lines.length} lines (limit: ${MAX_FILE_LINES}). Split into smaller, focused modules.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return warnings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkFunctionLengths(files, cwd) {
|
|
56
|
+
const warnings = [];
|
|
57
|
+
// Match common function declarations across JS/TS/Python
|
|
58
|
+
const fnPatterns = [
|
|
59
|
+
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // function foo()
|
|
60
|
+
/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/, // const foo = (
|
|
61
|
+
/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[^=])\s*=>/, // const foo = () =>
|
|
62
|
+
/^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // method() {
|
|
63
|
+
/^(?:async\s+)?def\s+(\w+)/, // def foo (Python)
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const fileExt = extname(file);
|
|
68
|
+
if (fileExt === '.py') continue;
|
|
69
|
+
|
|
70
|
+
const lines = readLines(file);
|
|
71
|
+
let currentFn = null;
|
|
72
|
+
let fnStart = 0;
|
|
73
|
+
let braceDepth = 0;
|
|
74
|
+
let inFunction = false;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
|
|
79
|
+
// Check if this line starts a new function
|
|
80
|
+
let newFnStarted = false;
|
|
81
|
+
for (const pattern of fnPatterns) {
|
|
82
|
+
const match = line.match(pattern);
|
|
83
|
+
if (match) {
|
|
84
|
+
// Close previous function if open
|
|
85
|
+
if (inFunction && currentFn) {
|
|
86
|
+
const len = i - fnStart;
|
|
87
|
+
if (len > MAX_FUNCTION_LINES) {
|
|
88
|
+
warnings.push({
|
|
89
|
+
level: 'warning',
|
|
90
|
+
file: relative(cwd, file),
|
|
91
|
+
message: `Function "${currentFn}" is ${len} lines (limit: ${MAX_FUNCTION_LINES}) at line ${fnStart + 1}. Extract helper functions.`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
currentFn = match[1];
|
|
96
|
+
fnStart = i;
|
|
97
|
+
inFunction = true;
|
|
98
|
+
braceDepth = 0;
|
|
99
|
+
newFnStarted = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip brace tracking on the line that started a new function
|
|
105
|
+
if (newFnStarted) continue;
|
|
106
|
+
|
|
107
|
+
// Track brace depth for JS/TS
|
|
108
|
+
if (inFunction) {
|
|
109
|
+
for (const ch of line) {
|
|
110
|
+
if (ch === '{') braceDepth++;
|
|
111
|
+
if (ch === '}') braceDepth--;
|
|
112
|
+
}
|
|
113
|
+
if (braceDepth <= 0 && i > fnStart && line.trim()) {
|
|
114
|
+
const len = i - fnStart + 1;
|
|
115
|
+
if (len > MAX_FUNCTION_LINES) {
|
|
116
|
+
warnings.push({
|
|
117
|
+
level: 'warning',
|
|
118
|
+
file: relative(cwd, file),
|
|
119
|
+
message: `Function "${currentFn}" is ${len} lines (limit: ${MAX_FUNCTION_LINES}) at line ${fnStart + 1}. Extract helper functions.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
inFunction = false;
|
|
123
|
+
currentFn = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return warnings;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function checkDuplicateBlocks(files, cwd) {
|
|
132
|
+
const warnings = [];
|
|
133
|
+
// Build a map of normalized line sequences -> locations
|
|
134
|
+
const blockMap = new Map();
|
|
135
|
+
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const lines = readLines(file).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('#') && !l.startsWith('*') && !l.startsWith('import') && !l.startsWith('from'));
|
|
138
|
+
|
|
139
|
+
// Sliding window of MIN_DUPLICATE_LINES
|
|
140
|
+
for (let i = 0; i <= lines.length - MIN_DUPLICATE_LINES; i++) {
|
|
141
|
+
const block = lines.slice(i, i + MIN_DUPLICATE_LINES).join('\n');
|
|
142
|
+
// Skip trivial blocks (mostly braces, returns, empty patterns)
|
|
143
|
+
if (block.replace(/[{}\s();\n]/g, '').length < 30) continue;
|
|
144
|
+
|
|
145
|
+
if (!blockMap.has(block)) {
|
|
146
|
+
blockMap.set(block, []);
|
|
147
|
+
}
|
|
148
|
+
blockMap.get(block).push({ file: relative(cwd, file), line: i + 1 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Report blocks found in multiple files
|
|
153
|
+
const reported = new Set();
|
|
154
|
+
for (const [block, locations] of blockMap) {
|
|
155
|
+
const uniqueFiles = [...new Set(locations.map(l => l.file))];
|
|
156
|
+
if (uniqueFiles.length < 2) continue;
|
|
157
|
+
|
|
158
|
+
const key = uniqueFiles.sort().join('|');
|
|
159
|
+
if (reported.has(key)) continue;
|
|
160
|
+
reported.add(key);
|
|
161
|
+
|
|
162
|
+
const preview = block.split('\n')[0].substring(0, 60);
|
|
163
|
+
warnings.push({
|
|
164
|
+
level: 'warning',
|
|
165
|
+
file: uniqueFiles[0],
|
|
166
|
+
message: `Duplicate code block found in ${uniqueFiles.length} files: "${preview}..." Also in: ${uniqueFiles.slice(1).join(', ')}. Extract to a shared utility.`,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Limit duplicate reports to avoid noise
|
|
170
|
+
if (warnings.length > 5) break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return warnings;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function checkDirectoryBloat(cwd) {
|
|
177
|
+
const warnings = [];
|
|
178
|
+
|
|
179
|
+
function checkDir(dir) {
|
|
180
|
+
let entries;
|
|
181
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
182
|
+
|
|
183
|
+
const sourceFiles = entries.filter(e =>
|
|
184
|
+
!e.isDirectory() && SOURCE_EXTENSIONS.has(extname(e.name)) && !IGNORE_FILES.has(e.name)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (sourceFiles.length > MAX_FILES_PER_DIR) {
|
|
188
|
+
warnings.push({
|
|
189
|
+
level: 'warning',
|
|
190
|
+
file: relative(cwd, dir) || '.',
|
|
191
|
+
message: `Directory has ${sourceFiles.length} source files (limit: ${MAX_FILES_PER_DIR}). Consider grouping into subdirectories by feature or domain.`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (entry.isDirectory() && !IGNORE_DIRS.has(entry.name)) {
|
|
197
|
+
checkDir(join(dir, entry.name));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
checkDir(cwd);
|
|
203
|
+
return warnings;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkStaleTests(files, cwd) {
|
|
207
|
+
const warnings = [];
|
|
208
|
+
const sourceFiles = new Set(
|
|
209
|
+
files
|
|
210
|
+
.filter(f => !basename(f).includes('.test.') && !basename(f).includes('.spec.') && !f.includes('__tests__'))
|
|
211
|
+
.map(f => basename(f).replace(extname(f), ''))
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const testFiles = files.filter(f =>
|
|
215
|
+
basename(f).includes('.test.') || basename(f).includes('.spec.') || f.includes('__tests__')
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
for (const testFile of testFiles) {
|
|
219
|
+
const testBase = basename(testFile)
|
|
220
|
+
.replace('.test', '')
|
|
221
|
+
.replace('.spec', '')
|
|
222
|
+
.replace(extname(testFile), '');
|
|
223
|
+
|
|
224
|
+
// If the source file this test corresponds to doesn't exist, flag it
|
|
225
|
+
if (testBase && !sourceFiles.has(testBase)) {
|
|
226
|
+
const lines = readLines(testFile);
|
|
227
|
+
// Check if the test file imports something that doesn't exist
|
|
228
|
+
const imports = lines.filter(l => l.includes('import') && l.includes('from'));
|
|
229
|
+
let hasDeadImport = false;
|
|
230
|
+
for (const imp of imports) {
|
|
231
|
+
const match = imp.match(/from\s+['"]([^'"]+)['"]/);
|
|
232
|
+
if (match && match[1].startsWith('.')) {
|
|
233
|
+
// Relative import - check if the file exists
|
|
234
|
+
const importPath = join(testFile, '..', match[1]);
|
|
235
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.py', ''];
|
|
236
|
+
const exists = extensions.some(ext => existsSync(importPath + ext) || existsSync(importPath));
|
|
237
|
+
if (!exists) hasDeadImport = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (hasDeadImport) {
|
|
242
|
+
warnings.push({
|
|
243
|
+
level: 'warning',
|
|
244
|
+
file: relative(cwd, testFile),
|
|
245
|
+
message: `Test file may be stale — imports reference files that no longer exist. Review and delete if no longer needed.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return warnings;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
const cwd = process.cwd();
|
|
257
|
+
const files = walk(cwd);
|
|
258
|
+
|
|
259
|
+
const allWarnings = [
|
|
260
|
+
...checkFileLengths(files, cwd),
|
|
261
|
+
...checkFunctionLengths(files, cwd),
|
|
262
|
+
...checkDuplicateBlocks(files, cwd),
|
|
263
|
+
...checkDirectoryBloat(cwd),
|
|
264
|
+
...checkStaleTests(files, cwd),
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const criticals = allWarnings.filter(w => w.level === 'critical');
|
|
268
|
+
const warnings = allWarnings.filter(w => w.level === 'warning');
|
|
269
|
+
|
|
270
|
+
if (allWarnings.length === 0) {
|
|
271
|
+
process.stderr.write('\n[code-hygiene] All clean. No structural issues found.\n');
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
process.stderr.write('\n[code-hygiene] Structural quality report:\n\n');
|
|
276
|
+
|
|
277
|
+
for (const w of criticals) {
|
|
278
|
+
process.stderr.write(` CRITICAL ${w.file}\n ${w.message}\n\n`);
|
|
279
|
+
}
|
|
280
|
+
for (const w of warnings) {
|
|
281
|
+
process.stderr.write(` WARNING ${w.file}\n ${w.message}\n\n`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.stderr.write(` Summary: ${criticals.length} critical, ${warnings.length} warnings\n\n`);
|
|
285
|
+
|
|
286
|
+
if (criticals.length > 0) {
|
|
287
|
+
process.stderr.write('[code-hygiene] BLOCKED: Fix critical issues before completing.\n');
|
|
288
|
+
process.stderr.write(' Tip: Use /simplify to auto-refactor long files and extract shared utilities.\n');
|
|
289
|
+
process.exit(2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
process.stderr.write('[code-hygiene] Passed with warnings. Consider running /simplify to clean up.\n');
|
|
293
|
+
process.exit(0);
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-commit gate — the single quality checkpoint before any commit.
|
|
5
|
+
* Replaces /done. Triggered by PreToolUse hook on Bash when command contains "git commit".
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 (automated, fast):
|
|
8
|
+
* - No .env files or secrets staged
|
|
9
|
+
* - No merge conflict markers
|
|
10
|
+
* - Lint passes
|
|
11
|
+
* - Tests pass
|
|
12
|
+
*
|
|
13
|
+
* Phase 2 (agent review):
|
|
14
|
+
* - Tells Claude to run code-quality-reviewer and security-reviewer on changed files
|
|
15
|
+
* - Only triggers if Phase 1 passes and changed files haven't been reviewed yet
|
|
16
|
+
*
|
|
17
|
+
* Exit 0 = allow commit
|
|
18
|
+
* Exit 2 = block commit with message
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from 'node:child_process';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const input = process.env.CLAUDE_TOOL_INPUT || '{}';
|
|
26
|
+
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(input);
|
|
30
|
+
} catch {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const command = parsed.command || '';
|
|
35
|
+
|
|
36
|
+
// Only gate actual git commit commands
|
|
37
|
+
if (!command.match(/git\s+commit/)) {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Skip amend-only or empty commits
|
|
42
|
+
if (command.includes('--allow-empty')) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
// Get staged files once, reuse across checks
|
|
49
|
+
let stagedFiles = '';
|
|
50
|
+
try {
|
|
51
|
+
stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim();
|
|
52
|
+
} catch {
|
|
53
|
+
// git not available
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// === PHASE 1: Automated checks ===
|
|
57
|
+
|
|
58
|
+
// Check 1: No secrets staged
|
|
59
|
+
if (stagedFiles) {
|
|
60
|
+
const secretFilePatterns = ['.env', 'credentials', '.pem', '.key'];
|
|
61
|
+
const secretNamePatterns = ['secret'];
|
|
62
|
+
const flagged = stagedFiles.split('\n').filter(f => {
|
|
63
|
+
const base = path.basename(f).toLowerCase();
|
|
64
|
+
if (f.includes('.chainproof/keys/public.pem')) return false;
|
|
65
|
+
// Always flag secret file extensions regardless of language
|
|
66
|
+
if (secretFilePatterns.some(p => base.includes(p))) return true;
|
|
67
|
+
return secretNamePatterns.some(p => base.includes(p));
|
|
68
|
+
});
|
|
69
|
+
if (flagged.length > 0) {
|
|
70
|
+
errors.push(`Potential secrets staged: ${flagged.join(', ')}\nUnstage these files or confirm they are safe.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check 2: No merge conflict markers
|
|
75
|
+
try {
|
|
76
|
+
const staged = execSync('git diff --cached', { encoding: 'utf-8' });
|
|
77
|
+
if (staged.includes('<<<<<<<') || staged.includes('>>>>>>>')) {
|
|
78
|
+
errors.push('Merge conflict markers found in staged changes. Resolve conflicts first.');
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// skip
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Helper: run a shell command, push to errors on failure
|
|
85
|
+
function tryExec(cmd, label, timeout = 30000) {
|
|
86
|
+
try {
|
|
87
|
+
execSync(`${cmd} 2>&1`, { encoding: 'utf-8', timeout });
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.status || e.killed) {
|
|
91
|
+
const msg = e.killed ? 'Command timed out' : (e.stdout || e.message);
|
|
92
|
+
errors.push(`${label}:\n${String(msg).slice(0, 500)}`);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readPkg(file = 'package.json') {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check 3: Lint
|
|
107
|
+
function runLint() {
|
|
108
|
+
if (fs.existsSync('package.json')) {
|
|
109
|
+
const pkg = readPkg();
|
|
110
|
+
if (!pkg) { errors.push('Lint skipped: package.json is malformed JSON'); return; }
|
|
111
|
+
if (pkg.scripts?.lint) { tryExec('npm run lint', 'Lint failed'); return; }
|
|
112
|
+
tryExec('npx eslint . --ext .js,.ts,.tsx', 'Lint failed');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (fs.existsSync('requirements.txt') || fs.existsSync('pyproject.toml')) {
|
|
116
|
+
tryExec('ruff check .', 'Lint failed');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (fs.existsSync('frontend/package.json')) {
|
|
120
|
+
tryExec('cd frontend && npx eslint .', 'Frontend lint failed');
|
|
121
|
+
}
|
|
122
|
+
if (fs.existsSync('backend/requirements.txt')) {
|
|
123
|
+
tryExec('cd backend && ruff check .', 'Backend lint failed');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 4: Tests
|
|
128
|
+
function runTests() {
|
|
129
|
+
const testTimeout = 120000;
|
|
130
|
+
if (fs.existsSync('package.json')) {
|
|
131
|
+
const pkg = readPkg();
|
|
132
|
+
if (!pkg) { errors.push('Tests skipped: package.json is malformed JSON'); return; }
|
|
133
|
+
if (pkg.scripts?.test) { tryExec('npm test', 'Tests failed', testTimeout); return; }
|
|
134
|
+
tryExec('npx vitest run', 'Tests failed', testTimeout);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (fs.existsSync('requirements.txt') || fs.existsSync('pyproject.toml')) {
|
|
138
|
+
tryExec('pytest', 'Tests failed', testTimeout);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (fs.existsSync('frontend/package.json')) {
|
|
142
|
+
tryExec('cd frontend && npx vitest run', 'Frontend tests failed', testTimeout);
|
|
143
|
+
}
|
|
144
|
+
if (fs.existsSync('backend/requirements.txt')) {
|
|
145
|
+
tryExec('cd backend && pytest', 'Backend tests failed', testTimeout);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
runLint();
|
|
150
|
+
runTests();
|
|
151
|
+
|
|
152
|
+
if (errors.length > 0) {
|
|
153
|
+
console.error('[pre-commit] BLOCKED - fix these issues first:\n');
|
|
154
|
+
errors.forEach(e => console.error(` ${e}\n`));
|
|
155
|
+
console.error('Run /build-fix to auto-resolve lint and build errors.');
|
|
156
|
+
process.exit(2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// === PHASE 2: Agent review gate ===
|
|
160
|
+
// Check if changed files have been reviewed in this session.
|
|
161
|
+
// We use a marker file that gets created when agents complete review.
|
|
162
|
+
|
|
163
|
+
const reviewMarker = '.claude/.last-review';
|
|
164
|
+
let needsReview = false;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (!stagedFiles) {
|
|
168
|
+
// Nothing staged, allow
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stagedList = stagedFiles.split('\n').filter(f =>
|
|
173
|
+
f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.tsx') ||
|
|
174
|
+
f.endsWith('.py') || f.endsWith('.jsx') || f.endsWith('.mjs')
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (stagedList.length === 0) {
|
|
178
|
+
// No code files staged (just docs/config), skip review
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fs.existsSync(reviewMarker)) {
|
|
183
|
+
try {
|
|
184
|
+
const review = JSON.parse(fs.readFileSync(reviewMarker, 'utf-8'));
|
|
185
|
+
const reviewedFiles = new Set(review.files || []);
|
|
186
|
+
const unreviewed = stagedList.filter(f => !reviewedFiles.has(f));
|
|
187
|
+
if (unreviewed.length > 0) {
|
|
188
|
+
needsReview = true;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
needsReview = true;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
needsReview = true;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Can't determine, allow
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (needsReview) {
|
|
202
|
+
console.error('[pre-commit] Code review required before commit.\n');
|
|
203
|
+
console.error('Run code-quality-reviewer and security-reviewer agents on the changed files,');
|
|
204
|
+
console.error('then retry the commit. Or run /code-review to do this automatically.\n');
|
|
205
|
+
console.error('To mark files as reviewed, the agents will update .claude/.last-review.');
|
|
206
|
+
process.exit(2);
|
|
207
|
+
}
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
"command": "node .claude/hooks/guard-protected-files.mjs"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Bash",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node .claude/hooks/pre-commit-gate.mjs"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
12
21
|
}
|
|
13
22
|
],
|
|
14
23
|
"PostToolUse": [
|
|
@@ -27,7 +36,7 @@
|
|
|
27
36
|
"hooks": [
|
|
28
37
|
{
|
|
29
38
|
"type": "command",
|
|
30
|
-
"command": "
|
|
39
|
+
"command": "node .claude/hooks/code-hygiene.mjs"
|
|
31
40
|
}
|
|
32
41
|
]
|
|
33
42
|
}
|
|
@@ -1,44 +1,122 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: AI/LLM integration patterns and
|
|
2
|
+
name: ai-prompts
|
|
3
|
+
description: AI/LLM integration patterns, guardrails infrastructure, and compliance (EU AI Act, NIST AI RMF)
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# AI/LLM Integration
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
6
|
+
# AI/LLM Integration & Guardrails
|
|
7
|
+
|
|
8
|
+
This project includes AI guardrails infrastructure in `src/lib/ai/` (TypeScript) or `app/ai/` (Python).
|
|
9
|
+
|
|
10
|
+
## Using the AI Client
|
|
11
|
+
|
|
12
|
+
All AI calls MUST go through the guardrails client. Never call the Anthropic SDK directly.
|
|
13
|
+
|
|
14
|
+
**TypeScript:**
|
|
15
|
+
```typescript
|
|
16
|
+
import { getAIClient } from '@/lib/ai';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
const ai = getAIClient();
|
|
20
|
+
const result = await ai.generate({
|
|
21
|
+
prompt: 'Analyze this text',
|
|
22
|
+
schema: z.object({ sentiment: z.enum(['positive', 'negative', 'neutral']), confidence: z.number() }),
|
|
23
|
+
purpose: 'sentiment-analysis', // Required for audit trail
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (result.needsHumanReview) {
|
|
27
|
+
// Route to approval queue — confidence below threshold
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Python:**
|
|
32
|
+
```python
|
|
33
|
+
from app.ai import get_ai_client
|
|
34
|
+
from pydantic import BaseModel
|
|
35
|
+
|
|
36
|
+
class Sentiment(BaseModel):
|
|
37
|
+
sentiment: str
|
|
38
|
+
confidence: float
|
|
39
|
+
|
|
40
|
+
ai = get_ai_client()
|
|
41
|
+
result = await ai.generate(prompt="Analyze this text", schema=Sentiment, purpose="sentiment-analysis")
|
|
42
|
+
|
|
43
|
+
if result.needs_human_review:
|
|
44
|
+
# Route to approval queue
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Guardrails Architecture
|
|
48
|
+
|
|
49
|
+
| Layer | What It Does | Compliance |
|
|
50
|
+
|-------|-------------|------------|
|
|
51
|
+
| **Input Guard** | Prompt injection detection, input sanitization, length limits | EU AI Act Art. 15, NIST Manage 2.2 |
|
|
52
|
+
| **Output Validation** | Zod/Pydantic schema validation with retry on parse failure | NIST Manage 2.4 |
|
|
53
|
+
| **Confidence Scoring** | Scores each response (0-1), routes low confidence to human review | NIST Measure 2.5, EU AI Act Art. 14 |
|
|
54
|
+
| **Audit Logger** | Structured logging of every AI interaction (input preview, output, confidence, model, latency) | EU AI Act Art. 12, NIST Manage 1.3 |
|
|
55
|
+
| **Health Metrics** | AI-specific health endpoint: availability, confidence distribution, error rates, per-model stats | NIST Manage 3.2, EU AI Act Art. 9 |
|
|
56
|
+
| **AI Disclosure** | All AI responses carry `aiGenerated: true` flag | EU AI Act Art. 50 |
|
|
57
|
+
|
|
58
|
+
## Rules
|
|
59
|
+
|
|
60
|
+
- **Never call the Anthropic/OpenAI SDK directly.** Always use `getAIClient()` / `get_ai_client()`
|
|
61
|
+
- **Never use raw string responses in application logic.** Always validate with Zod/Pydantic schemas
|
|
62
|
+
- **Never skip the `purpose` parameter.** Every AI call must be tagged for audit traceability
|
|
63
|
+
- **Never trust AI output for security decisions.** Always validate independently
|
|
64
|
+
- **Never log full prompts.** The audit logger captures only a 200-char preview to avoid PII leakage
|
|
65
|
+
- **Always handle `needsHumanReview`.** If confidence is below threshold, the response must be reviewed before acting on it
|
|
66
|
+
|
|
67
|
+
## Confidence Thresholds
|
|
68
|
+
|
|
69
|
+
| Confidence | Action | Use Case |
|
|
70
|
+
|-----------|--------|----------|
|
|
71
|
+
| > 0.9 | Auto-accept | Low-risk: summaries, formatting, classification |
|
|
72
|
+
| 0.7 - 0.9 | Accept with logging | Medium-risk: recommendations, content generation |
|
|
73
|
+
| 0.5 - 0.7 | Flag for review | High-risk: decisions, user-facing content |
|
|
74
|
+
| < 0.5 | Require human approval | Critical: financial, medical, legal, safety |
|
|
75
|
+
|
|
76
|
+
Configure the threshold per-call via `confidenceThreshold` parameter.
|
|
77
|
+
|
|
78
|
+
## Prompt Injection Protection
|
|
79
|
+
|
|
80
|
+
The input guard detects:
|
|
81
|
+
- **Instruction override**: "ignore previous instructions", "disregard your rules"
|
|
82
|
+
- **Role manipulation**: "you are now a...", "pretend to be..."
|
|
83
|
+
- **System prompt extraction**: "show me your system prompt", "repeat your instructions"
|
|
84
|
+
- **Delimiter injection**: `<system>`, `[INST]`, `<|im_start|>`
|
|
85
|
+
- **Data exfiltration**: "send this data to..."
|
|
86
|
+
|
|
87
|
+
Detected injections are blocked and logged. Suspicious patterns (encoded payloads, code execution) are logged but not blocked.
|
|
88
|
+
|
|
89
|
+
## AI Health Endpoint
|
|
90
|
+
|
|
91
|
+
Mount at `/api/ai/health`. Returns:
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"status": "ok | degraded | unhealthy",
|
|
95
|
+
"aiAvailable": true,
|
|
96
|
+
"metrics": {
|
|
97
|
+
"totalCalls": 142,
|
|
98
|
+
"successRate": 0.97,
|
|
99
|
+
"avgConfidence": 0.84,
|
|
100
|
+
"avgLatencyMs": 1230,
|
|
101
|
+
"lowConfidenceRate": 0.08,
|
|
102
|
+
"errorRate": 0.03
|
|
103
|
+
},
|
|
104
|
+
"models": {
|
|
105
|
+
"claude-sonnet-4-20250514": { "calls": 142, "successRate": 0.97, "avgLatencyMs": 1230 }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Recommended Libraries
|
|
111
|
+
|
|
112
|
+
**TypeScript:** Zod (validation), @anthropic-ai/sdk (model calls), @instructor-ai/instructor (structured extraction)
|
|
113
|
+
**Python:** Pydantic (validation), anthropic (model calls), instructor (structured extraction), guardrails-ai (advanced validation), presidio (PII detection)
|
|
114
|
+
**Observability:** Langfuse (open-source LLM tracing), OpenTelemetry (spans), Helicone (proxy logging)
|
|
115
|
+
|
|
116
|
+
## Testing AI Integrations
|
|
117
|
+
|
|
118
|
+
- Mock AI responses in unit tests — never make real API calls in CI
|
|
119
|
+
- Use golden datasets for regression testing prompt quality
|
|
120
|
+
- Test timeout, retry, and error handling paths explicitly
|
|
121
|
+
- Test with adversarial inputs (prompt injection patterns)
|
|
122
|
+
- Monitor confidence score distribution for drift over time
|