@hongmaple0820/scale-engine 0.15.0 → 0.16.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.
Files changed (89) hide show
  1. package/README.en.md +13 -5
  2. package/README.md +13 -5
  3. package/dist/agents/LeadershipPresets.d.ts +16 -0
  4. package/dist/agents/LeadershipPresets.js +152 -0
  5. package/dist/agents/LeadershipPresets.js.map +1 -0
  6. package/dist/api/cli.js +729 -5
  7. package/dist/api/cli.js.map +1 -1
  8. package/dist/api/doctor.d.ts +2 -0
  9. package/dist/api/doctor.js +83 -0
  10. package/dist/api/doctor.js.map +1 -1
  11. package/dist/api/mcp.js +2 -1
  12. package/dist/api/mcp.js.map +1 -1
  13. package/dist/artifact/types.d.ts +4 -0
  14. package/dist/artifact/types.js.map +1 -1
  15. package/dist/capabilities/InstalledSkillsIntegration.d.ts +3 -0
  16. package/dist/capabilities/InstalledSkillsIntegration.js +41 -17
  17. package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -1
  18. package/dist/cli/phaseCommands.d.ts +14 -0
  19. package/dist/cli/phaseCommands.js +214 -5
  20. package/dist/cli/phaseCommands.js.map +1 -1
  21. package/dist/cli/vibeCommands.d.ts +20 -0
  22. package/dist/cli/vibeCommands.js +150 -173
  23. package/dist/cli/vibeCommands.js.map +1 -1
  24. package/dist/core/logger.d.ts +2 -0
  25. package/dist/core/logger.js +33 -1
  26. package/dist/core/logger.js.map +1 -1
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/output/HTMLDocumentRenderer.js +3 -2
  31. package/dist/output/HTMLDocumentRenderer.js.map +1 -1
  32. package/dist/prompts/VibeTemplateGallery.d.ts +25 -0
  33. package/dist/prompts/VibeTemplateGallery.js +295 -0
  34. package/dist/prompts/VibeTemplateGallery.js.map +1 -0
  35. package/dist/skills/ExternalSkills.js +9 -4
  36. package/dist/skills/ExternalSkills.js.map +1 -1
  37. package/dist/skills/SkillDiscovery.js +5 -3
  38. package/dist/skills/SkillDiscovery.js.map +1 -1
  39. package/dist/skills/SkillDoctor.js +178 -1
  40. package/dist/skills/SkillDoctor.js.map +1 -1
  41. package/dist/skills/SkillInstaller.js +5 -0
  42. package/dist/skills/SkillInstaller.js.map +1 -1
  43. package/dist/skills/SkillRepository.d.ts +63 -0
  44. package/dist/skills/SkillRepository.js +365 -0
  45. package/dist/skills/SkillRepository.js.map +1 -0
  46. package/dist/skills/routing/SkillPolicy.js +168 -5
  47. package/dist/skills/routing/SkillPolicy.js.map +1 -1
  48. package/dist/tools/ToolCapabilityRegistry.d.ts +46 -0
  49. package/dist/tools/ToolCapabilityRegistry.js +223 -0
  50. package/dist/tools/ToolCapabilityRegistry.js.map +1 -0
  51. package/dist/tools/ToolEvidenceGate.d.ts +39 -0
  52. package/dist/tools/ToolEvidenceGate.js +117 -0
  53. package/dist/tools/ToolEvidenceGate.js.map +1 -0
  54. package/dist/tools/ToolEvidenceStore.d.ts +58 -0
  55. package/dist/tools/ToolEvidenceStore.js +129 -0
  56. package/dist/tools/ToolEvidenceStore.js.map +1 -0
  57. package/dist/tools/ToolOrchestrator.d.ts +67 -0
  58. package/dist/tools/ToolOrchestrator.js +193 -0
  59. package/dist/tools/ToolOrchestrator.js.map +1 -0
  60. package/dist/tools/ToolPolicy.d.ts +33 -0
  61. package/dist/tools/ToolPolicy.js +157 -0
  62. package/dist/tools/ToolPolicy.js.map +1 -0
  63. package/dist/tools/index.d.ts +5 -0
  64. package/dist/tools/index.js +6 -0
  65. package/dist/tools/index.js.map +1 -0
  66. package/dist/version.d.ts +3 -0
  67. package/dist/version.js +15 -0
  68. package/dist/version.js.map +1 -0
  69. package/dist/workflow/EngineeringStandards.d.ts +212 -0
  70. package/dist/workflow/EngineeringStandards.js +1021 -0
  71. package/dist/workflow/EngineeringStandards.js.map +1 -0
  72. package/dist/workflow/GovernanceTemplatePacks.d.ts +1 -1
  73. package/dist/workflow/GovernanceTemplatePacks.js +101 -18
  74. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  75. package/dist/workflow/GovernanceTemplates.d.ts +1 -1
  76. package/dist/workflow/GovernanceTemplates.js +225 -37
  77. package/dist/workflow/GovernanceTemplates.js.map +1 -1
  78. package/dist/workflow/ResourceGovernance.d.ts +120 -0
  79. package/dist/workflow/ResourceGovernance.js +512 -0
  80. package/dist/workflow/ResourceGovernance.js.map +1 -0
  81. package/dist/workflow/TaskArtifactScaffolder.js +3 -0
  82. package/dist/workflow/TaskArtifactScaffolder.js.map +1 -1
  83. package/dist/workflow/VerificationProfile.d.ts +2 -0
  84. package/dist/workflow/VerificationProfile.js +7 -0
  85. package/dist/workflow/VerificationProfile.js.map +1 -1
  86. package/dist/workflow/index.d.ts +2 -0
  87. package/dist/workflow/index.js +2 -0
  88. package/dist/workflow/index.js.map +1 -1
  89. package/package.json +2 -2
@@ -0,0 +1,1021 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ const DEFAULT_SOURCE_DIRECTORIES = ['src', 'app', 'packages', 'services', 'cmd', 'internal', 'pkg'];
4
+ const DEFAULT_IGNORED_DIRECTORIES = [
5
+ '.git',
6
+ '.scale',
7
+ 'node_modules',
8
+ 'dist',
9
+ 'build',
10
+ 'coverage',
11
+ 'test-results',
12
+ 'playwright-report',
13
+ 'tmp',
14
+ 'temp',
15
+ 'docs',
16
+ 'tests',
17
+ '__tests__',
18
+ 'e2e',
19
+ ];
20
+ const DEFAULT_ALLOWED_CONSOLE_DIRECTORIES = ['src/api', 'src/cli', 'scripts'];
21
+ const DEFAULT_ALLOWED_CONSOLE_FILES = ['src/dashboard/DashboardServer.ts'];
22
+ const DEFAULT_SENSITIVE_FIELDS = [
23
+ 'password',
24
+ 'passwd',
25
+ 'token',
26
+ 'accessToken',
27
+ 'refreshToken',
28
+ 'secret',
29
+ 'authorization',
30
+ 'cookie',
31
+ 'apiKey',
32
+ 'credential',
33
+ 'privateKey',
34
+ ];
35
+ const SOURCE_EXTENSIONS = new Set([
36
+ '.ts',
37
+ '.tsx',
38
+ '.js',
39
+ '.jsx',
40
+ '.mjs',
41
+ '.cjs',
42
+ '.go',
43
+ '.py',
44
+ '.java',
45
+ '.cs',
46
+ '.kt',
47
+ '.php',
48
+ '.rb',
49
+ '.rs',
50
+ '.vue',
51
+ '.svelte',
52
+ ]);
53
+ export function engineeringStandardsPolicyPath(projectDir = process.cwd(), scaleDir = '.scale') {
54
+ return join(projectDir, scaleDir, 'engineering-standards.json');
55
+ }
56
+ export function frameworksCatalogPath(projectDir = process.cwd(), scaleDir = '.scale') {
57
+ return join(projectDir, scaleDir, 'frameworks.json');
58
+ }
59
+ export function engineeringStandardsBaselinePath(projectDir = process.cwd(), scaleDir = '.scale') {
60
+ return join(projectDir, scaleDir, 'engineering-standards-baseline.json');
61
+ }
62
+ export function engineeringStandardsPolicyTemplate() {
63
+ return JSON.stringify({
64
+ version: 1,
65
+ mode: 'warn',
66
+ sourceDirectories: DEFAULT_SOURCE_DIRECTORIES,
67
+ ignoredDirectories: DEFAULT_IGNORED_DIRECTORIES,
68
+ allowedConsoleDirectories: DEFAULT_ALLOWED_CONSOLE_DIRECTORIES,
69
+ allowedConsoleFiles: DEFAULT_ALLOWED_CONSOLE_FILES,
70
+ maxFileLines: 500,
71
+ logging: {
72
+ approvedLoggers: ['pino', 'winston', 'zap', 'zerolog', 'logrus', 'slog'],
73
+ sensitiveFields: DEFAULT_SENSITIVE_FIELDS,
74
+ },
75
+ architecture: {
76
+ enforceLayering: true,
77
+ },
78
+ blockingRules: [],
79
+ allowedFindingPatterns: [],
80
+ baselineFindings: [],
81
+ }, null, 2) + '\n';
82
+ }
83
+ export function engineeringStandardsBaselineTemplate() {
84
+ return JSON.stringify({
85
+ version: 1,
86
+ generatedAt: '',
87
+ scope: 'Legacy findings tracked separately. New findings and changed-file findings must still be fixed before completion.',
88
+ findings: [],
89
+ }, null, 2) + '\n';
90
+ }
91
+ export function frameworksCatalogTemplate() {
92
+ return JSON.stringify({
93
+ version: 1,
94
+ lastReviewedAt: '',
95
+ reviewIntervalDays: 90,
96
+ frameworks: [],
97
+ orm: [],
98
+ ui: {
99
+ designSystem: '',
100
+ componentLibrary: '',
101
+ visualReviewRequired: true,
102
+ },
103
+ architecture: {
104
+ layers: ['api', 'service', 'domain', 'repository', 'infrastructure'],
105
+ dependencyRule: 'outer layers depend inward through explicit interfaces',
106
+ },
107
+ bannedImports: [],
108
+ }, null, 2) + '\n';
109
+ }
110
+ export function loadEngineeringStandardsPolicy(projectDir = process.cwd(), scaleDir = '.scale') {
111
+ const path = engineeringStandardsPolicyPath(projectDir, scaleDir);
112
+ const warnings = [];
113
+ let parsed = {};
114
+ if (existsSync(path)) {
115
+ try {
116
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
117
+ }
118
+ catch (error) {
119
+ warnings.push(`Failed to read ${path}: ${error.message}; using built-in defaults.`);
120
+ }
121
+ }
122
+ else {
123
+ warnings.push(`No engineering standards policy found at ${path}; using built-in defaults.`);
124
+ }
125
+ return {
126
+ version: typeof parsed.version === 'number' ? parsed.version : 1,
127
+ mode: parsed.mode === 'block' ? 'block' : 'warn',
128
+ sourceDirectories: parsed.sourceDirectories ?? DEFAULT_SOURCE_DIRECTORIES,
129
+ ignoredDirectories: parsed.ignoredDirectories ?? DEFAULT_IGNORED_DIRECTORIES,
130
+ allowedConsoleDirectories: parsed.allowedConsoleDirectories ?? DEFAULT_ALLOWED_CONSOLE_DIRECTORIES,
131
+ allowedConsoleFiles: parsed.allowedConsoleFiles ?? DEFAULT_ALLOWED_CONSOLE_FILES,
132
+ maxFileLines: parsed.maxFileLines ?? 500,
133
+ logging: {
134
+ approvedLoggers: parsed.logging?.approvedLoggers ?? ['pino', 'winston', 'zap', 'zerolog', 'logrus', 'slog'],
135
+ sensitiveFields: parsed.logging?.sensitiveFields ?? DEFAULT_SENSITIVE_FIELDS,
136
+ },
137
+ architecture: {
138
+ enforceLayering: parsed.architecture?.enforceLayering ?? true,
139
+ },
140
+ blockingRules: Array.isArray(parsed.blockingRules)
141
+ ? parsed.blockingRules.filter(ruleId => typeof ruleId === 'string' && ruleId.length > 0)
142
+ : [],
143
+ allowedFindingPatterns: resolveAllowedFindingPatterns(parsed, warnings),
144
+ baselineFindings: Array.isArray(parsed.baselineFindings)
145
+ ? parsed.baselineFindings
146
+ .filter(item => typeof item.ruleId === 'string' && typeof item.path === 'string')
147
+ .map(item => ({
148
+ ...item,
149
+ line: typeof item.line === 'number' ? item.line : undefined,
150
+ }))
151
+ : [],
152
+ warnings,
153
+ };
154
+ }
155
+ export function loadEngineeringStandardsBaseline(projectDir = process.cwd(), scaleDir = '.scale') {
156
+ const path = engineeringStandardsBaselinePath(projectDir, scaleDir);
157
+ const warnings = [];
158
+ let parsed = {};
159
+ if (!existsSync(path))
160
+ return { findings: [], warnings };
161
+ try {
162
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
163
+ }
164
+ catch (error) {
165
+ warnings.push(`Failed to read ${path}: ${error.message}; external standards baseline ignored.`);
166
+ return { findings: [], warnings };
167
+ }
168
+ return {
169
+ findings: Array.isArray(parsed.findings)
170
+ ? parsed.findings
171
+ .filter(item => typeof item.ruleId === 'string' && typeof item.path === 'string')
172
+ .map(item => ({
173
+ ruleId: item.ruleId,
174
+ path: normalizePath(item.path),
175
+ line: typeof item.line === 'number' ? item.line : undefined,
176
+ reason: typeof item.reason === 'string' ? item.reason : undefined,
177
+ }))
178
+ : [],
179
+ warnings,
180
+ };
181
+ }
182
+ function resolveAllowedFindingPatterns(parsed, warnings) {
183
+ if (!Array.isArray(parsed.allowedFindingPatterns))
184
+ return [];
185
+ const patterns = [];
186
+ for (const item of parsed.allowedFindingPatterns) {
187
+ if (!item || typeof item !== 'object')
188
+ continue;
189
+ if (typeof item.ruleId !== 'string' || item.ruleId.length === 0)
190
+ continue;
191
+ if (typeof item.evidencePattern !== 'string' && typeof item.messagePattern !== 'string')
192
+ continue;
193
+ const validEvidencePattern = typeof item.evidencePattern !== 'string' || isValidRegex(item.evidencePattern);
194
+ const validMessagePattern = typeof item.messagePattern !== 'string' || isValidRegex(item.messagePattern);
195
+ if (!validEvidencePattern || !validMessagePattern) {
196
+ warnings.push(`Invalid allowedFindingPatterns entry for ${item.ruleId}; regex could not be compiled.`);
197
+ continue;
198
+ }
199
+ patterns.push({
200
+ ruleId: item.ruleId,
201
+ path: typeof item.path === 'string' ? item.path : undefined,
202
+ evidencePattern: typeof item.evidencePattern === 'string' ? item.evidencePattern : undefined,
203
+ messagePattern: typeof item.messagePattern === 'string' ? item.messagePattern : undefined,
204
+ reason: typeof item.reason === 'string' ? item.reason : undefined,
205
+ });
206
+ }
207
+ return patterns;
208
+ }
209
+ function isValidRegex(pattern) {
210
+ try {
211
+ new RegExp(pattern);
212
+ return true;
213
+ }
214
+ catch {
215
+ return false;
216
+ }
217
+ }
218
+ export function loadFrameworksCatalog(projectDir = process.cwd(), scaleDir = '.scale', now = new Date()) {
219
+ const path = frameworksCatalogPath(projectDir, scaleDir);
220
+ const warnings = [];
221
+ let parsed = {};
222
+ if (existsSync(path)) {
223
+ try {
224
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
225
+ }
226
+ catch (error) {
227
+ warnings.push({
228
+ ruleId: 'frameworks-catalog-warning',
229
+ message: `Failed to read ${path}: ${error.message}; using empty framework catalog.`,
230
+ });
231
+ }
232
+ }
233
+ const lastReviewedAt = typeof parsed.lastReviewedAt === 'string' ? parsed.lastReviewedAt : undefined;
234
+ const reviewIntervalDays = typeof parsed.reviewIntervalDays === 'number' ? parsed.reviewIntervalDays : undefined;
235
+ if (lastReviewedAt && reviewIntervalDays && isFrameworkCatalogStale(lastReviewedAt, reviewIntervalDays, now)) {
236
+ warnings.push({
237
+ ruleId: 'frameworks-catalog-stale',
238
+ message: `Framework catalog was last reviewed at ${lastReviewedAt}; review interval is ${reviewIntervalDays} days.`,
239
+ });
240
+ }
241
+ return {
242
+ version: typeof parsed.version === 'number' ? parsed.version : 1,
243
+ lastReviewedAt,
244
+ reviewIntervalDays,
245
+ bannedImports: Array.isArray(parsed.bannedImports)
246
+ ? parsed.bannedImports
247
+ .filter(rule => typeof rule.source === 'string' && rule.source.length > 0)
248
+ .map(rule => ({
249
+ source: rule.source,
250
+ severity: rule.severity === 'info' || rule.severity === 'warn' || rule.severity === 'fail'
251
+ ? rule.severity
252
+ : 'fail',
253
+ reason: typeof rule.reason === 'string' ? rule.reason : undefined,
254
+ replacement: typeof rule.replacement === 'string' ? rule.replacement : undefined,
255
+ }))
256
+ : [],
257
+ warnings,
258
+ };
259
+ }
260
+ export function scanEngineeringStandards(options = {}) {
261
+ const projectDir = options.projectDir ?? process.cwd();
262
+ const scaleDir = options.scaleDir ?? '.scale';
263
+ const policy = loadEngineeringStandardsPolicy(projectDir, scaleDir);
264
+ const externalBaseline = loadEngineeringStandardsBaseline(projectDir, scaleDir);
265
+ const baselineFindings = [...policy.baselineFindings, ...externalBaseline.findings];
266
+ const frameworks = loadFrameworksCatalog(projectDir, scaleDir, options.now);
267
+ const files = findSourceFiles(projectDir, policy, options.changedFiles);
268
+ const findings = files
269
+ .flatMap(file => scanFile(projectDir, file, policy, frameworks))
270
+ .map(finding => applyRuleSeverityPolicy(finding, policy))
271
+ .filter(finding => !isAllowedFindingPattern(finding, policy) &&
272
+ (options.includeBaselineFindings || !isBaselineFinding(finding, baselineFindings)));
273
+ return {
274
+ projectDir,
275
+ policyPath: engineeringStandardsPolicyPath(projectDir, scaleDir),
276
+ baselinePath: engineeringStandardsBaselinePath(projectDir, scaleDir),
277
+ frameworksPath: frameworksCatalogPath(projectDir, scaleDir),
278
+ policy,
279
+ frameworks,
280
+ findings,
281
+ summary: summarizeStandards(files.length, findings),
282
+ warnings: [...policy.warnings, ...externalBaseline.warnings, ...frameworks.warnings.map(warning => warning.message)],
283
+ };
284
+ }
285
+ export function doctorEngineeringStandards(options = {}) {
286
+ const scan = scanEngineeringStandards(options);
287
+ const policyWarningFindings = scan.policy.warnings.map(message => ({
288
+ severity: 'warn',
289
+ category: 'framework',
290
+ ruleId: 'standards-policy-warning',
291
+ path: scan.policyPath,
292
+ message,
293
+ fix: 'Run scale init or add .scale/engineering-standards.json.',
294
+ }));
295
+ const frameworkWarningFindings = scan.frameworks.warnings.map(warning => ({
296
+ severity: 'warn',
297
+ category: 'framework',
298
+ ruleId: warning.ruleId,
299
+ path: scan.frameworksPath,
300
+ message: warning.message,
301
+ fix: 'Fix .scale/frameworks.json or regenerate it with scale init.',
302
+ }));
303
+ const findings = [...policyWarningFindings, ...frameworkWarningFindings, ...scan.findings];
304
+ return {
305
+ ok: !findings.some(finding => finding.severity === 'fail'),
306
+ projectDir: scan.projectDir,
307
+ findings,
308
+ scan: { ...scan, findings },
309
+ };
310
+ }
311
+ export function settleEngineeringStandards(options = {}) {
312
+ const doctor = doctorEngineeringStandards(options);
313
+ const standardsImpactPath = options.artifactsDir
314
+ ? appendStandardsImpact({
315
+ projectDir: options.projectDir ?? process.cwd(),
316
+ artifactsDir: options.artifactsDir,
317
+ taskId: options.taskId,
318
+ doctor,
319
+ })
320
+ : undefined;
321
+ return {
322
+ ok: doctor.ok,
323
+ taskId: options.taskId,
324
+ standardsImpactPath,
325
+ doctor,
326
+ };
327
+ }
328
+ export function baselineEngineeringStandards(options = {}) {
329
+ const projectDir = options.projectDir ?? process.cwd();
330
+ const scaleDir = options.scaleDir ?? '.scale';
331
+ const reason = options.reason ?? 'legacy standards debt accepted for staged remediation';
332
+ const scan = scanEngineeringStandards({
333
+ ...options,
334
+ projectDir,
335
+ scaleDir,
336
+ changedFiles: undefined,
337
+ includeBaselineFindings: true,
338
+ });
339
+ const baselineEntries = baselineEntriesFromFindings(scan.findings, reason);
340
+ const debt = classifyStandardsDebt(scan.summary.filesScanned, scan.findings);
341
+ const baselinePath = engineeringStandardsBaselinePath(projectDir, scaleDir);
342
+ if (options.writeBaseline) {
343
+ writeStandardsBaselineFile({
344
+ projectDir,
345
+ scaleDir,
346
+ baselinePath,
347
+ taskId: options.taskId,
348
+ reason,
349
+ entries: baselineEntries,
350
+ });
351
+ }
352
+ const legacyDebtPath = options.artifactsDir
353
+ ? writeLegacyDebtReport({
354
+ projectDir,
355
+ artifactsDir: options.artifactsDir,
356
+ taskId: options.taskId,
357
+ wroteBaseline: Boolean(options.writeBaseline),
358
+ baselinePath,
359
+ baselineEntries,
360
+ debt,
361
+ findings: scan.findings,
362
+ maxFindingsInReport: options.maxFindingsInReport ?? 200,
363
+ })
364
+ : undefined;
365
+ return {
366
+ ok: true,
367
+ projectDir,
368
+ baselinePath,
369
+ legacyDebtPath,
370
+ wroteBaseline: Boolean(options.writeBaseline),
371
+ baselineEntries,
372
+ debt,
373
+ scan,
374
+ warnings: scan.warnings,
375
+ };
376
+ }
377
+ function scanFile(projectDir, absolutePath, policy, frameworks) {
378
+ const path = normalizePath(relative(projectDir, absolutePath));
379
+ const content = readFileSync(absolutePath, 'utf-8');
380
+ const lines = content.split(/\r?\n/);
381
+ const findings = [];
382
+ if (lines.length > policy.maxFileLines) {
383
+ findings.push({
384
+ severity: 'warn',
385
+ category: 'architecture',
386
+ ruleId: 'large-source-file',
387
+ path,
388
+ message: `Source file has ${lines.length} lines, above ${policy.maxFileLines}.`,
389
+ fix: 'Split responsibilities or document why this file is intentionally large.',
390
+ });
391
+ }
392
+ for (let index = 0; index < lines.length; index += 1) {
393
+ const line = lines[index];
394
+ if (isNonExecutablePatternLine(line))
395
+ continue;
396
+ const lineNumber = index + 1;
397
+ findings.push(...scanLine(path, line, lineNumber, policy, frameworks));
398
+ }
399
+ findings.push(...findEmptyCatchBlocks(path, lines));
400
+ return dedupeFindings(findings);
401
+ }
402
+ function scanLine(path, line, lineNumber, policy, frameworks) {
403
+ const findings = [];
404
+ const sensitiveMatcher = sensitiveFieldPattern(policy);
405
+ const evidence = line.trim().slice(0, 160);
406
+ findings.push(...scanBannedImports(path, line, lineNumber, evidence, frameworks));
407
+ if (isHardcodedSecret(line, policy)) {
408
+ findings.push({
409
+ severity: 'fail',
410
+ category: 'security',
411
+ ruleId: 'hardcoded-secret',
412
+ path,
413
+ line: lineNumber,
414
+ message: 'Secret-like value appears to be hardcoded in source.',
415
+ evidence,
416
+ fix: 'Move secrets to approved secret storage or environment configuration and keep placeholders non-sensitive.',
417
+ });
418
+ }
419
+ if ((isLogCall(line) || isAdHocOutputCall(line)) && sensitiveMatcher.test(line)) {
420
+ findings.push({
421
+ severity: 'fail',
422
+ category: 'logging',
423
+ ruleId: 'sensitive-log',
424
+ path,
425
+ line: lineNumber,
426
+ message: 'Sensitive field appears in a log statement.',
427
+ evidence,
428
+ fix: 'Remove the field, mask it, or use an approved redaction helper before logging.',
429
+ });
430
+ }
431
+ else if (isAdHocOutputCall(line) && !isConsoleAllowed(path, policy)) {
432
+ findings.push({
433
+ severity: 'warn',
434
+ category: 'logging',
435
+ ruleId: 'ad-hoc-console-log',
436
+ path,
437
+ line: lineNumber,
438
+ message: 'Ad-hoc console logging was found outside approved CLI or script paths.',
439
+ evidence,
440
+ fix: 'Use the project logger, remove the debug print, or add an explicit policy exception.',
441
+ });
442
+ }
443
+ if (isRawSqlConstruction(line)) {
444
+ findings.push({
445
+ severity: 'fail',
446
+ category: 'database',
447
+ ruleId: 'raw-sql-construction',
448
+ path,
449
+ line: lineNumber,
450
+ message: 'SQL appears to be constructed with dynamic input.',
451
+ evidence,
452
+ fix: 'Use parameterized queries, ORM bind parameters, or a query builder with placeholders.',
453
+ });
454
+ }
455
+ if (/dangerouslySetInnerHTML|\.innerHTML\s*=|document\.write\s*\(/.test(line)) {
456
+ findings.push({
457
+ severity: 'fail',
458
+ category: 'security',
459
+ ruleId: 'unsafe-html-sink',
460
+ path,
461
+ line: lineNumber,
462
+ message: 'Unsafe HTML sink can create XSS risk.',
463
+ evidence,
464
+ fix: 'Use text rendering or sanitize trusted HTML with an approved sanitizer.',
465
+ });
466
+ }
467
+ if (/\beval\s*\(|new\s+Function\s*\(/.test(line)) {
468
+ findings.push({
469
+ severity: 'fail',
470
+ category: 'security',
471
+ ruleId: 'unsafe-code-execution',
472
+ path,
473
+ line: lineNumber,
474
+ message: 'Dynamic code execution was found.',
475
+ evidence,
476
+ fix: 'Replace eval or Function with a typed parser, dispatch table, or safe interpreter.',
477
+ });
478
+ }
479
+ if (/^\s*(?:\/\/|\/\*)\s*@ts-ignore\b/.test(line)) {
480
+ findings.push({
481
+ severity: 'fail',
482
+ category: 'code-quality',
483
+ ruleId: 'ts-ignore',
484
+ path,
485
+ line: lineNumber,
486
+ message: 'TypeScript errors are suppressed with @ts-ignore.',
487
+ evidence,
488
+ fix: 'Fix the type boundary or use a narrow typed adapter with a documented reason.',
489
+ });
490
+ }
491
+ if (/\bas\s+any\b|:\s*any\b|<any\b|Array<any>|Promise<any>|Record<[^>]+,\s*any>/.test(line)) {
492
+ findings.push({
493
+ severity: 'warn',
494
+ category: 'code-quality',
495
+ ruleId: 'type-escape',
496
+ path,
497
+ line: lineNumber,
498
+ message: 'New any-based type escape weakens interface contracts.',
499
+ evidence,
500
+ fix: 'Model the real type or isolate unknown input at the boundary.',
501
+ });
502
+ }
503
+ if (/Math\.random\s*\(\)/.test(line) && /\b(token|secret|session|password|credential|nonce)\b/i.test(line)) {
504
+ findings.push({
505
+ severity: 'fail',
506
+ category: 'security',
507
+ ruleId: 'weak-random-security-token',
508
+ path,
509
+ line: lineNumber,
510
+ message: 'Math.random is used for security-sensitive data.',
511
+ evidence,
512
+ fix: 'Use a cryptographically secure random source.',
513
+ });
514
+ }
515
+ if (policy.architecture.enforceLayering && isOuterLayerPath(path) && importsInnerPersistence(line)) {
516
+ findings.push({
517
+ severity: 'warn',
518
+ category: 'architecture',
519
+ ruleId: 'layer-boundary-bypass',
520
+ path,
521
+ line: lineNumber,
522
+ message: 'Outer layer appears to import persistence internals directly.',
523
+ evidence,
524
+ fix: 'Route through service/usecase interfaces and keep persistence behind a repository boundary.',
525
+ });
526
+ }
527
+ return findings;
528
+ }
529
+ function findEmptyCatchBlocks(path, lines) {
530
+ const findings = [];
531
+ for (let index = 0; index < lines.length; index += 1) {
532
+ const line = lines[index];
533
+ if (/catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\*.*?\*\/|\/\/.*)?\s*\}/.test(line)) {
534
+ findings.push(emptyCatchFinding(path, index + 1, line));
535
+ continue;
536
+ }
537
+ if (!/catch\s*(?:\([^)]*\))?\s*\{\s*$/.test(line))
538
+ continue;
539
+ for (const next of lines.slice(index + 1, index + 8)) {
540
+ const trimmed = next.trim();
541
+ if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
542
+ continue;
543
+ if (/^}\s*[),;]?$/.test(trimmed))
544
+ findings.push(emptyCatchFinding(path, index + 1, line));
545
+ break;
546
+ }
547
+ }
548
+ return findings;
549
+ }
550
+ function emptyCatchFinding(path, line, text) {
551
+ return {
552
+ severity: 'fail',
553
+ category: 'code-quality',
554
+ ruleId: 'empty-catch',
555
+ path,
556
+ line,
557
+ message: 'Empty or comment-only catch block hides failures.',
558
+ evidence: text.trim().slice(0, 160),
559
+ fix: 'Handle the error, return a typed failure, or log through the approved redacted logger.',
560
+ };
561
+ }
562
+ function findSourceFiles(projectDir, policy, changedFiles) {
563
+ if (changedFiles)
564
+ return findChangedSourceFiles(projectDir, policy, changedFiles);
565
+ const files = [];
566
+ for (const sourceDir of policy.sourceDirectories) {
567
+ const absolute = join(projectDir, sourceDir);
568
+ if (!existsSync(absolute))
569
+ continue;
570
+ walk(absolute, projectDir, policy, files);
571
+ }
572
+ return files;
573
+ }
574
+ function findChangedSourceFiles(projectDir, policy, changedFiles) {
575
+ const files = [];
576
+ const seen = new Set();
577
+ for (const changedFile of changedFiles) {
578
+ const normalized = normalizeChangedPath(projectDir, changedFile);
579
+ if (!normalized || seen.has(normalized))
580
+ continue;
581
+ seen.add(normalized);
582
+ if (!SOURCE_EXTENSIONS.has(extname(normalized).toLowerCase()))
583
+ continue;
584
+ if (!isUnderSourceDirectory(normalized, policy))
585
+ continue;
586
+ if (isIgnoredPath(normalized, policy))
587
+ continue;
588
+ const absolute = resolve(projectDir, ...normalized.split('/'));
589
+ if (!existsSync(absolute) || !statSync(absolute).isFile())
590
+ continue;
591
+ if (statSync(absolute).size <= 1024 * 1024)
592
+ files.push(absolute);
593
+ }
594
+ return files;
595
+ }
596
+ function normalizeChangedPath(projectDir, path) {
597
+ const relativePath = isAbsolute(path) ? relative(projectDir, path) : path;
598
+ const normalized = normalizePath(relativePath);
599
+ if (!normalized || normalized.startsWith('..'))
600
+ return '';
601
+ return normalized;
602
+ }
603
+ function isUnderSourceDirectory(path, policy) {
604
+ return policy.sourceDirectories
605
+ .map(normalizePath)
606
+ .some(sourceDir => path === sourceDir || path.startsWith(`${sourceDir}/`));
607
+ }
608
+ function isIgnoredPath(path, policy) {
609
+ return policy.ignoredDirectories
610
+ .map(normalizePath)
611
+ .some(ignored => path === ignored || path.startsWith(`${ignored}/`) || path.split('/').includes(ignored));
612
+ }
613
+ function walk(dir, projectDir, policy, files) {
614
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
615
+ const fullPath = join(dir, entry.name);
616
+ const rel = normalizePath(relative(projectDir, fullPath));
617
+ if (entry.isDirectory()) {
618
+ if (policy.ignoredDirectories.some(ignored => rel === normalizePath(ignored) || rel.startsWith(`${normalizePath(ignored)}/`) || entry.name === ignored))
619
+ continue;
620
+ walk(fullPath, projectDir, policy, files);
621
+ }
622
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
623
+ if (statSync(fullPath).size <= 1024 * 1024)
624
+ files.push(fullPath);
625
+ }
626
+ }
627
+ }
628
+ function appendStandardsImpact(options) {
629
+ const dir = isAbsolute(options.artifactsDir)
630
+ ? options.artifactsDir
631
+ : resolve(options.projectDir, options.artifactsDir);
632
+ if (!existsSync(dir))
633
+ mkdirSync(dir, { recursive: true });
634
+ const path = join(dir, 'standards-impact.md');
635
+ if (!existsSync(path))
636
+ writeFileSync(path, '# Standards Impact\n\n', 'utf-8');
637
+ appendFileSync(path, standardsSettlementMarkdown(options.taskId, options.doctor), 'utf-8');
638
+ return path;
639
+ }
640
+ function standardsSettlementMarkdown(taskId, doctor) {
641
+ const findings = doctor.findings.length
642
+ ? doctor.findings.map(finding => `| ${finding.severity.toUpperCase()} | ${finding.ruleId} | ${escapeCell(finding.path)} | ${finding.line ?? ''} | ${escapeCell(finding.message)} |`).join('\n')
643
+ : '| OK | no-findings | | | No engineering standards findings. |';
644
+ return `
645
+ ## SCALE Engineering Standards Settlement - ${new Date().toISOString()}
646
+
647
+ Task: ${taskId ?? 'unspecified'}
648
+ Status: ${doctor.ok ? 'passed' : 'blocked'}
649
+
650
+ | Metric | Value |
651
+ | --- | ---: |
652
+ | Files scanned | ${doctor.scan.summary.filesScanned} |
653
+ | Total findings | ${doctor.scan.summary.totalFindings} |
654
+ | Blocking findings | ${doctor.scan.summary.blockingFindings} |
655
+
656
+ | Severity | Rule | Path | Line | Message |
657
+ | --- | --- | --- | ---: | --- |
658
+ ${findings}
659
+ `;
660
+ }
661
+ function baselineEntriesFromFindings(findings, reason) {
662
+ const entries = new Map();
663
+ for (const finding of [...findings].sort(compareFindings)) {
664
+ const path = normalizePath(finding.path);
665
+ const key = `${finding.ruleId}\0${path}\0${finding.line ?? ''}`;
666
+ if (entries.has(key))
667
+ continue;
668
+ entries.set(key, {
669
+ ruleId: finding.ruleId,
670
+ path,
671
+ line: finding.line,
672
+ reason,
673
+ severity: finding.severity,
674
+ category: finding.category,
675
+ message: finding.message,
676
+ });
677
+ }
678
+ return [...entries.values()];
679
+ }
680
+ function writeStandardsBaselineFile(options) {
681
+ mkdirSync(join(options.projectDir, options.scaleDir), { recursive: true });
682
+ writeFileSync(options.baselinePath, JSON.stringify({
683
+ version: 1,
684
+ generatedAt: new Date().toISOString(),
685
+ taskId: options.taskId,
686
+ scope: 'Legacy findings tracked separately. New findings and changed-file findings must still be fixed before completion.',
687
+ reason: options.reason,
688
+ findings: options.entries.map(entry => ({
689
+ ruleId: entry.ruleId,
690
+ path: entry.path,
691
+ ...(entry.line === undefined ? {} : { line: entry.line }),
692
+ reason: entry.reason,
693
+ })),
694
+ }, null, 2) + '\n', 'utf-8');
695
+ }
696
+ function classifyStandardsDebt(filesScanned, findings) {
697
+ const bySeverity = { info: 0, warn: 0, fail: 0 };
698
+ const byCategory = emptyDebtCategorySummary();
699
+ const byScope = emptyDebtScopeSummary();
700
+ const byRule = {};
701
+ const byFile = new Map();
702
+ for (const finding of findings) {
703
+ bySeverity[finding.severity] += 1;
704
+ incrementDebtGroup(byCategory[finding.category], finding.severity);
705
+ incrementDebtGroup(byScope[classifyDebtScope(finding.path)], finding.severity);
706
+ byRule[finding.ruleId] ??= { ...emptyDebtGroup(), category: finding.category };
707
+ incrementDebtGroup(byRule[finding.ruleId], finding.severity);
708
+ const fileGroup = byFile.get(finding.path) ?? { ...emptyDebtGroup(), path: finding.path };
709
+ incrementDebtGroup(fileGroup, finding.severity);
710
+ byFile.set(finding.path, fileGroup);
711
+ }
712
+ return {
713
+ filesScanned,
714
+ totalFindings: findings.length,
715
+ blockingFindings: findings.filter(finding => finding.severity === 'fail').length,
716
+ bySeverity,
717
+ byScope,
718
+ byCategory,
719
+ byRule,
720
+ topFiles: [...byFile.values()]
721
+ .sort((a, b) => b.total - a.total || a.path.localeCompare(b.path))
722
+ .slice(0, 20),
723
+ };
724
+ }
725
+ function writeLegacyDebtReport(options) {
726
+ const dir = isAbsolute(options.artifactsDir)
727
+ ? options.artifactsDir
728
+ : resolve(options.projectDir, options.artifactsDir);
729
+ if (!existsSync(dir))
730
+ mkdirSync(dir, { recursive: true });
731
+ const path = join(dir, 'standards-legacy-debt.md');
732
+ writeFileSync(path, legacyDebtMarkdown(options), 'utf-8');
733
+ return path;
734
+ }
735
+ function legacyDebtMarkdown(options) {
736
+ const categories = Object.entries(options.debt.byCategory)
737
+ .filter(([, group]) => group.total > 0)
738
+ .map(([category, group]) => `| ${category} | ${group.total} | ${group.blocking} | ${group.warn} | ${group.info} |`)
739
+ .join('\n') || '| none | 0 | 0 | 0 | 0 |';
740
+ const scopes = Object.entries(options.debt.byScope)
741
+ .filter(([, group]) => group.total > 0)
742
+ .map(([scope, group]) => `| ${scope} | ${group.total} | ${group.blocking} | ${group.warn} | ${group.info} |`)
743
+ .join('\n') || '| none | 0 | 0 | 0 | 0 |';
744
+ const rules = Object.entries(options.debt.byRule)
745
+ .sort(([, a], [, b]) => b.total - a.total)
746
+ .map(([ruleId, group]) => `| ${ruleId} | ${group.category} | ${group.total} | ${group.blocking} | ${group.warn} | ${group.info} |`)
747
+ .join('\n') || '| none | none | 0 | 0 | 0 | 0 |';
748
+ const files = options.debt.topFiles
749
+ .map(group => `| ${escapeCell(group.path)} | ${group.total} | ${group.blocking} | ${group.warn} | ${group.info} |`)
750
+ .join('\n') || '| none | 0 | 0 | 0 | 0 |';
751
+ const detailLimit = Math.max(0, options.maxFindingsInReport);
752
+ const details = options.findings
753
+ .slice(0, detailLimit)
754
+ .map(finding => `| ${finding.severity.toUpperCase()} | ${finding.category} | ${finding.ruleId} | ${escapeCell(finding.path)} | ${finding.line ?? ''} | ${escapeCell(finding.message)} |`)
755
+ .join('\n') || '| OK | none | no-findings | | | No engineering standards findings. |';
756
+ const truncated = options.findings.length > detailLimit
757
+ ? `\n\nDetail rows truncated to ${detailLimit} of ${options.findings.length}. The baseline file contains the complete machine-readable list.\n`
758
+ : '';
759
+ return `# SCALE Engineering Standards Legacy Debt Classification
760
+
761
+ Generated: ${new Date().toISOString()}
762
+ Task: ${options.taskId ?? 'unspecified'}
763
+ Baseline written: ${options.wroteBaseline ? 'yes' : 'no'}
764
+ Baseline path: ${options.baselinePath}
765
+ Baseline entries: ${options.baselineEntries.length}
766
+
767
+ ## Summary
768
+
769
+ | Metric | Value |
770
+ | --- | ---: |
771
+ | Files scanned | ${options.debt.filesScanned} |
772
+ | Total findings | ${options.debt.totalFindings} |
773
+ | Blocking findings | ${options.debt.blockingFindings} |
774
+ | Warnings | ${options.debt.bySeverity.warn} |
775
+ | Info | ${options.debt.bySeverity.info} |
776
+
777
+ ## By Category
778
+
779
+ | Category | Total | Blocking | Warn | Info |
780
+ | --- | ---: | ---: | ---: | ---: |
781
+ ${categories}
782
+
783
+ ## By Scope
784
+
785
+ | Scope | Total | Blocking | Warn | Info |
786
+ | --- | ---: | ---: | ---: | ---: |
787
+ ${scopes}
788
+
789
+ ## By Rule
790
+
791
+ | Rule | Category | Total | Blocking | Warn | Info |
792
+ | --- | --- | ---: | ---: | ---: | ---: |
793
+ ${rules}
794
+
795
+ ## Top Files
796
+
797
+ | Path | Total | Blocking | Warn | Info |
798
+ | --- | ---: | ---: | ---: | ---: |
799
+ ${files}
800
+
801
+ ## Finding Details
802
+
803
+ | Severity | Category | Rule | Path | Line | Message |
804
+ | --- | --- | --- | --- | ---: | --- |
805
+ ${details}${truncated}
806
+ `;
807
+ }
808
+ function summarizeStandards(filesScanned, findings) {
809
+ const bySeverity = { info: 0, warn: 0, fail: 0 };
810
+ const byCategory = emptyCategorySummary();
811
+ for (const finding of findings) {
812
+ bySeverity[finding.severity] += 1;
813
+ byCategory[finding.category] += 1;
814
+ }
815
+ return {
816
+ filesScanned,
817
+ totalFindings: findings.length,
818
+ blockingFindings: findings.filter(finding => finding.severity === 'fail').length,
819
+ bySeverity,
820
+ byCategory,
821
+ };
822
+ }
823
+ function emptyCategorySummary() {
824
+ return {
825
+ logging: 0,
826
+ security: 0,
827
+ database: 0,
828
+ architecture: 0,
829
+ 'code-quality': 0,
830
+ framework: 0,
831
+ testing: 0,
832
+ uiux: 0,
833
+ };
834
+ }
835
+ function emptyDebtCategorySummary() {
836
+ return {
837
+ logging: emptyDebtGroup(),
838
+ security: emptyDebtGroup(),
839
+ database: emptyDebtGroup(),
840
+ architecture: emptyDebtGroup(),
841
+ 'code-quality': emptyDebtGroup(),
842
+ framework: emptyDebtGroup(),
843
+ testing: emptyDebtGroup(),
844
+ uiux: emptyDebtGroup(),
845
+ };
846
+ }
847
+ function emptyDebtScopeSummary() {
848
+ return {
849
+ production: emptyDebtGroup(),
850
+ test: emptyDebtGroup(),
851
+ generated: emptyDebtGroup(),
852
+ };
853
+ }
854
+ function emptyDebtGroup() {
855
+ return {
856
+ total: 0,
857
+ blocking: 0,
858
+ warn: 0,
859
+ info: 0,
860
+ };
861
+ }
862
+ function classifyDebtScope(path) {
863
+ const normalized = normalizePath(path).toLowerCase();
864
+ if (normalized.includes('/dist/') ||
865
+ normalized.includes('/build/') ||
866
+ normalized.includes('/coverage/') ||
867
+ normalized.includes('/vendor') ||
868
+ normalized.includes('/generated/') ||
869
+ normalized.includes('/assets/web-auth/js/vendor-') ||
870
+ normalized.includes('/auth/js/vendor-') ||
871
+ normalized.endsWith('.min.js') ||
872
+ normalized.endsWith('.bundle.js')) {
873
+ return 'generated';
874
+ }
875
+ if (/(^|\/)(__tests__|tests?|e2e|spec)(\/|$)/.test(normalized) ||
876
+ /\.(test|spec)\.[cm]?[jt]sx?$/.test(normalized) ||
877
+ normalized.endsWith('test.java') ||
878
+ normalized.endsWith('tests.java')) {
879
+ return 'test';
880
+ }
881
+ return 'production';
882
+ }
883
+ function incrementDebtGroup(group, severity) {
884
+ group.total += 1;
885
+ if (severity === 'fail')
886
+ group.blocking += 1;
887
+ if (severity === 'warn')
888
+ group.warn += 1;
889
+ if (severity === 'info')
890
+ group.info += 1;
891
+ }
892
+ function compareFindings(a, b) {
893
+ return a.path.localeCompare(b.path) ||
894
+ a.ruleId.localeCompare(b.ruleId) ||
895
+ (a.line ?? 0) - (b.line ?? 0) ||
896
+ a.message.localeCompare(b.message);
897
+ }
898
+ function applyRuleSeverityPolicy(finding, policy) {
899
+ if (finding.severity === 'fail' || !policy.blockingRules.includes(finding.ruleId))
900
+ return finding;
901
+ return {
902
+ ...finding,
903
+ severity: 'fail',
904
+ message: `${finding.message} This rule is configured as blocking.`,
905
+ };
906
+ }
907
+ function scanBannedImports(path, line, lineNumber, evidence, frameworks) {
908
+ if (!/\bimport\b|\brequire\s*\(/.test(line))
909
+ return [];
910
+ return frameworks.bannedImports
911
+ .filter(rule => importsSource(line, rule.source))
912
+ .map(rule => ({
913
+ severity: rule.severity ?? 'fail',
914
+ category: 'framework',
915
+ ruleId: 'banned-import',
916
+ path,
917
+ line: lineNumber,
918
+ message: `Import from "${rule.source}" violates the framework catalog.${rule.reason ? ` ${rule.reason}` : ''}`,
919
+ evidence,
920
+ fix: rule.replacement
921
+ ? `Use ${rule.replacement} instead.`
922
+ : 'Use the project-approved framework, ORM, component, or boundary from .scale/frameworks.json.',
923
+ }));
924
+ }
925
+ function importsSource(line, source) {
926
+ const escaped = escapeRegex(source);
927
+ const sourceBoundary = `(?:['"]|/)`;
928
+ return new RegExp(`\\bfrom\\s+['"]${escaped}${sourceBoundary}|\\brequire\\s*\\(\\s*['"]${escaped}${sourceBoundary}`).test(line);
929
+ }
930
+ function isFrameworkCatalogStale(lastReviewedAt, reviewIntervalDays, now) {
931
+ const reviewedAt = new Date(`${lastReviewedAt}T00:00:00Z`);
932
+ if (Number.isNaN(reviewedAt.getTime()) || reviewIntervalDays <= 0)
933
+ return false;
934
+ return now.getTime() - reviewedAt.getTime() > reviewIntervalDays * 24 * 60 * 60 * 1000;
935
+ }
936
+ function sensitiveFieldPattern(policy) {
937
+ const fields = policy.logging.sensitiveFields.map(escapeRegex).join('|');
938
+ return new RegExp(`\\b(?:${fields})\\b`, 'i');
939
+ }
940
+ function isLogCall(line) {
941
+ return /\b(?:console\.(?:log|debug|info|warn|error)|logger\.(?:trace|debug|info|warn|error|fatal)|log(?:ger)?\.(?:trace|debug|info|warn|error|fatal)|log[A-Za-z0-9_]*\s*\()\b/.test(line);
942
+ }
943
+ function isAdHocOutputCall(line) {
944
+ return /\bconsole\.(?:log|debug|info|warn|error)\s*\(|\bfmt\.Print(?:f|ln)?\s*\(|\bprint(?:ln)?\s*\(|\bSystem\.out\.print(?:ln)?\s*\(|\bConsole\.Write(?:Line)?\s*\(|\bprintln!\s*\(/.test(line);
945
+ }
946
+ function isHardcodedSecret(line, policy) {
947
+ const fields = policy.logging.sensitiveFields.map(escapeRegex).join('|');
948
+ const match = new RegExp(`\\b\\w*(?:${fields})\\w*\\b\\s*[:=]\\s*(['"\`])([^'"\`]{12,})\\1`, 'i').exec(line);
949
+ if (!match)
950
+ return false;
951
+ return !/\b(example|sample|placeholder|changeme|replace-me|dummy|test-value)\b/i.test(match[2]);
952
+ }
953
+ function isRawSqlConstruction(line) {
954
+ return /\b(?:query|execute|exec|raw|rawQuery)\s*\(/i.test(line) &&
955
+ /\b(?:SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE)\b/i.test(line) &&
956
+ (line.includes('+') || line.includes('${') || /\breq\./.test(line));
957
+ }
958
+ function isConsoleAllowed(path, policy) {
959
+ const normalized = normalizePath(path);
960
+ if (policy.allowedConsoleFiles.map(normalizePath).includes(normalized))
961
+ return true;
962
+ return policy.allowedConsoleDirectories
963
+ .map(normalizePath)
964
+ .some(prefix => normalized === prefix || normalized.startsWith(`${prefix}/`));
965
+ }
966
+ function isOuterLayerPath(path) {
967
+ return /(^|\/)(api|controller|controllers|handler|handlers|routes|pages)(\/|$)/i.test(path);
968
+ }
969
+ function importsInnerPersistence(line) {
970
+ return /\bimport\b.*(?:repository|repositories|dao|model|models|entity|entities|infra|infrastructure)|\bfrom\s+['"].*(?:repository|repositories|dao|model|models|entity|entities|infra|infrastructure)/i.test(line);
971
+ }
972
+ function isNonExecutablePatternLine(line) {
973
+ const trimmed = line.trim();
974
+ if (trimmed.includes('String.raw`') || trimmed.startsWith('templateBody:'))
975
+ return true;
976
+ return /^\/.*\/[dgimsuy]*,?$/.test(trimmed) ||
977
+ /^\/.*\/[dgimsuy]*,?\s*\/\/.*$/.test(trimmed) ||
978
+ /^\/.*\/[dgimsuy]*\.(?:test|exec)\(/.test(trimmed) ||
979
+ /^return\s+\/.*\/[dgimsuy]*\.(?:test|exec)\(/.test(trimmed) ||
980
+ /=\s*\/.*\/[dgimsuy]*\s*(?:[),;]|$)/.test(trimmed) ||
981
+ /^pattern:\s*\/.*\/[dgimsuy]*,?$/.test(trimmed) ||
982
+ /\(\s*\/.*\/[dgimsuy]*\.(?:test|exec)\(/.test(trimmed);
983
+ }
984
+ function isAllowedFindingPattern(finding, policy) {
985
+ return policy.allowedFindingPatterns.some(item => {
986
+ if (item.ruleId !== finding.ruleId)
987
+ return false;
988
+ if (item.path && normalizePath(item.path) !== normalizePath(finding.path))
989
+ return false;
990
+ if (item.evidencePattern && !new RegExp(item.evidencePattern).test(finding.evidence ?? ''))
991
+ return false;
992
+ if (item.messagePattern && !new RegExp(item.messagePattern).test(finding.message))
993
+ return false;
994
+ return true;
995
+ });
996
+ }
997
+ function isBaselineFinding(finding, baselineFindings) {
998
+ return baselineFindings.some(item => item.ruleId === finding.ruleId &&
999
+ normalizePath(item.path) === normalizePath(finding.path) &&
1000
+ (item.line === undefined || item.line === finding.line));
1001
+ }
1002
+ function dedupeFindings(findings) {
1003
+ const seen = new Set();
1004
+ return findings.filter(finding => {
1005
+ const key = `${finding.ruleId}:${finding.path}:${finding.line ?? 0}`;
1006
+ if (seen.has(key))
1007
+ return false;
1008
+ seen.add(key);
1009
+ return true;
1010
+ });
1011
+ }
1012
+ function normalizePath(path) {
1013
+ return path.split(sep).join('/').replace(/^\.\//, '');
1014
+ }
1015
+ function escapeRegex(value) {
1016
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1017
+ }
1018
+ function escapeCell(value) {
1019
+ return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
1020
+ }
1021
+ //# sourceMappingURL=EngineeringStandards.js.map