jhste-skills 0.1.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +254 -0
  3. package/README.ko.md +254 -0
  4. package/README.md +254 -0
  5. package/README.zh.md +254 -0
  6. package/adapters/claude/README.md +7 -0
  7. package/adapters/codex/README.md +25 -0
  8. package/adapters/generic/README.md +7 -0
  9. package/cli/baseline.mjs +32 -0
  10. package/cli/connect.mjs +84 -0
  11. package/cli/deep-scan/analyze.mjs +167 -0
  12. package/cli/deep-scan/collect.mjs +133 -0
  13. package/cli/deep-scan/report.mjs +197 -0
  14. package/cli/deep-scan.mjs +56 -0
  15. package/cli/guard/baseline.mjs +64 -0
  16. package/cli/guard/config.mjs +48 -0
  17. package/cli/guard/profile-commands.mjs +87 -0
  18. package/cli/guard/registry.mjs +47 -0
  19. package/cli/guard/reporting.mjs +165 -0
  20. package/cli/guard/scanners/code-health.mjs +213 -0
  21. package/cli/guard/scanners/data-boundary-locality.mjs +125 -0
  22. package/cli/guard/scanners/data-boundary.mjs +237 -0
  23. package/cli/guard/scanners/external-input.mjs +74 -0
  24. package/cli/guard/scanners/index.mjs +136 -0
  25. package/cli/guard/scanners/single-responsibility.mjs +205 -0
  26. package/cli/guard/scanners/ui-runtime.mjs +140 -0
  27. package/cli/guard/scanners/utils.mjs +167 -0
  28. package/cli/guard/scope.mjs +181 -0
  29. package/cli/guard.mjs +125 -0
  30. package/cli/hook-utils.mjs +127 -0
  31. package/cli/hooks.mjs +127 -0
  32. package/cli/index.mjs +35 -0
  33. package/cli/install-actions/apply-plan.mjs +39 -0
  34. package/cli/install-actions/bridge-writer.mjs +52 -0
  35. package/cli/install-actions/output.mjs +45 -0
  36. package/cli/install-actions/preflight.mjs +58 -0
  37. package/cli/install-actions/profile-writer.mjs +21 -0
  38. package/cli/install-actions/skills.mjs +148 -0
  39. package/cli/install-actions.mjs +4 -0
  40. package/cli/install-flow/options.mjs +234 -0
  41. package/cli/install-flow/output.mjs +106 -0
  42. package/cli/install-flow/plan-helpers.mjs +29 -0
  43. package/cli/install-flow/plan.mjs +200 -0
  44. package/cli/install-flow/prompts.mjs +210 -0
  45. package/cli/install-flow.mjs +16 -0
  46. package/cli/install.mjs +77 -0
  47. package/cli/json-file.mjs +39 -0
  48. package/cli/profile/loader.mjs +13 -0
  49. package/cli/profile/parser.mjs +226 -0
  50. package/cli/profile/schema.mjs +81 -0
  51. package/cli/profile/settings.mjs +45 -0
  52. package/cli/profile/validator.mjs +86 -0
  53. package/cli/profile.mjs +5 -0
  54. package/cli/shared/args.mjs +32 -0
  55. package/cli/shared/files.mjs +70 -0
  56. package/cli/shared/git.mjs +28 -0
  57. package/cli/shared/paths.mjs +27 -0
  58. package/cli/shared/prompt.mjs +32 -0
  59. package/cli/shared/templates.mjs +71 -0
  60. package/cli/shared/time.mjs +3 -0
  61. package/cli/shared.mjs +7 -0
  62. package/cli/sync-core.mjs +213 -0
  63. package/cli/sync.mjs +7 -0
  64. package/cli/tune.mjs +101 -0
  65. package/cli/uninstall.mjs +288 -0
  66. package/cli/update.mjs +7 -0
  67. package/docs/ACCEPTANCE_CHECK.md +54 -0
  68. package/docs/CLI.md +212 -0
  69. package/docs/CONFLICT_RESOLUTION.md +58 -0
  70. package/docs/PUBLIC_SAFETY.md +26 -0
  71. package/docs/RULES.md +94 -0
  72. package/docs/VENDORING.md +23 -0
  73. package/examples/profile.yaml +45 -0
  74. package/package.json +51 -0
  75. package/packs/api.yaml +13 -0
  76. package/packs/core.yaml +19 -0
  77. package/packs/crawler.yaml +8 -0
  78. package/packs/database.yaml +8 -0
  79. package/packs/web.yaml +10 -0
  80. package/rules/core/api_contract_compatibility.yaml +25 -0
  81. package/rules/core/authz_data_isolation.yaml +27 -0
  82. package/rules/core/build_runtime_env_safety.yaml +26 -0
  83. package/rules/core/external_input_validation.yaml +27 -0
  84. package/rules/core/file_size_advisory.yaml +28 -0
  85. package/rules/core/no_secret_logging.yaml +24 -0
  86. package/rules/core/no_silent_failure.yaml +30 -0
  87. package/rules/core/null_state_safety.yaml +25 -0
  88. package/rules/core/performance_duplicate_fetch.yaml +25 -0
  89. package/rules/core/public_safe_error.yaml +24 -0
  90. package/rules/core/responsibility_budget.yaml +44 -0
  91. package/rules/core/side_effect_boundary.yaml +24 -0
  92. package/rules/core/single_responsibility_advisory.yaml +35 -0
  93. package/rules/core/workflow_security.yaml +25 -0
  94. package/rules/core/write_safety_idempotency.yaml +25 -0
  95. package/rules/crawler/crawler_producer_boundary.yaml +24 -0
  96. package/rules/database/db_row_validation.yaml +24 -0
  97. package/rules/database/sql_parameter_binding.yaml +24 -0
  98. package/rules/nextjs/thin_api_route.yaml +24 -0
  99. package/rules/python/broad_exception_advisory.yaml +24 -0
  100. package/rules/react/component_responsibility.yaml +24 -0
  101. package/rules/typescript/type_escape_advisory.yaml +24 -0
  102. package/scripts/docs-check-data.mjs +71 -0
  103. package/scripts/docs-check.mjs +261 -0
  104. package/scripts/guard-fixtures/helpers.mjs +58 -0
  105. package/scripts/guard-fixtures-test.mjs +273 -0
  106. package/scripts/profile-fixtures-test.mjs +83 -0
  107. package/scripts/public-safety-check.mjs +88 -0
  108. package/scripts/public-safety-fixtures-test.mjs +60 -0
  109. package/scripts/release-gates-test.mjs +52 -0
  110. package/scripts/single-responsibility-fixtures-test.mjs +86 -0
  111. package/scripts/smoke/connect-scenarios.mjs +47 -0
  112. package/scripts/smoke/fixture.mjs +49 -0
  113. package/scripts/smoke/guard-and-hook-scenarios.mjs +211 -0
  114. package/scripts/smoke/helpers.mjs +51 -0
  115. package/scripts/smoke/install-scenarios.mjs +244 -0
  116. package/scripts/smoke/mode-scenarios.mjs +76 -0
  117. package/scripts/smoke-test.mjs +17 -0
  118. package/scripts/syntax-check.mjs +37 -0
  119. package/scripts/vendor-check.mjs +87 -0
  120. package/skills/codebase-design/DEEPENING.md +37 -0
  121. package/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  122. package/skills/codebase-design/SKILL.md +122 -0
  123. package/skills/diagnose/SKILL.md +125 -0
  124. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  125. package/skills/diagnosing-bugs/SKILL.md +142 -0
  126. package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  127. package/skills/domain-modeling/ADR-FORMAT.md +47 -0
  128. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  129. package/skills/domain-modeling/SKILL.md +82 -0
  130. package/skills/grill-me/SKILL.md +18 -0
  131. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  132. package/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  133. package/skills/grill-with-docs/SKILL.md +96 -0
  134. package/skills/grilling/SKILL.md +18 -0
  135. package/skills/handoff/SKILL.md +23 -0
  136. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  139. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/skills/improve-codebase-architecture/SKILL.md +93 -0
  141. package/skills/jhste-architecture-review/SKILL.md +28 -0
  142. package/skills/jhste-architecture-review/references/architecture-review.md +41 -0
  143. package/skills/jhste-code-quality/SKILL.md +33 -0
  144. package/skills/jhste-code-quality/references/code-quality.md +45 -0
  145. package/skills/jhste-crawler-automation/SKILL.md +23 -0
  146. package/skills/jhste-crawler-automation/references/crawler-automation.md +11 -0
  147. package/skills/jhste-db-api-boundary/SKILL.md +28 -0
  148. package/skills/jhste-db-api-boundary/references/db-api-boundary.md +21 -0
  149. package/skills/jhste-engineering-judgment/SKILL.md +107 -0
  150. package/skills/jhste-engineering-judgment/references/structure-templates.md +41 -0
  151. package/skills/jhste-red-team-review/SKILL.md +101 -0
  152. package/skills/jhste-red-team-review/references/red-team-review.md +83 -0
  153. package/skills/prototype/LOGIC.md +79 -0
  154. package/skills/prototype/SKILL.md +38 -0
  155. package/skills/prototype/UI.md +112 -0
  156. package/skills/setup/SKILL.md +21 -0
  157. package/skills/setup/references/conflict-policy.md +11 -0
  158. package/skills/setup/references/setup-flow.md +18 -0
  159. package/skills/to-issues/SKILL.md +91 -0
  160. package/skills/to-prd/SKILL.md +82 -0
  161. package/skills/triage/AGENT-BRIEF.md +168 -0
  162. package/skills/triage/OUT-OF-SCOPE.md +101 -0
  163. package/skills/triage/SKILL.md +111 -0
  164. package/skills/write-a-skill/SKILL.md +125 -0
  165. package/vendor/matt-pocock/LICENSE +21 -0
  166. package/vendor/matt-pocock/NOTICE.md +10 -0
  167. package/vendor/matt-pocock/allowlist.json +16 -0
  168. package/vendor/matt-pocock/source-lock.json +119 -0
@@ -0,0 +1,125 @@
1
+ import {
2
+ hasPersistenceRead,
3
+ hasPersistenceWrite,
4
+ localWindow,
5
+ maskCommentsAndStrings,
6
+ } from './utils.mjs';
7
+
8
+ const scopeToken = /\b(userId|user\.id|accountId|orgId|tenantId|ownerId|workspaceId|teamId|projectId)\b/i;
9
+ const loopPattern = /(forEach\s*\(|for\s*\([^)]*;|for\s*\(\s*const\s+.+\s+of\s+|\.map\s*\(|while\s*\()/i;
10
+
11
+ function callWindow(maskedText, index, after = 900) {
12
+ const source = String(maskedText || '');
13
+ const open = source.indexOf('(', Number(index || 0));
14
+ if (open < 0 || open - Number(index || 0) > 120) return localWindow(source, index, 0, after);
15
+ let depth = 0;
16
+ const endLimit = Math.min(source.length, Number(index || 0) + after);
17
+ for (let cursor = open; cursor < endLimit; cursor += 1) {
18
+ if (source[cursor] === '(') depth += 1;
19
+ if (source[cursor] === ')') {
20
+ depth -= 1;
21
+ if (depth === 0) return source.slice(Number(index || 0), cursor + 1);
22
+ }
23
+ }
24
+ return localWindow(source, index, 0, after);
25
+ }
26
+
27
+ function statementWindow(text, index, before = 900, after = 900) {
28
+ const source = String(text || '');
29
+ const masked = maskCommentsAndStrings(source);
30
+ const anchor = Number(index || 0);
31
+ const min = Math.max(0, anchor - before);
32
+ let start = min;
33
+ for (let cursor = anchor - 1; cursor >= min; cursor -= 1) {
34
+ if (/[;{}]/u.test(masked[cursor])) {
35
+ start = cursor + 1;
36
+ break;
37
+ }
38
+ }
39
+ const max = Math.min(source.length, anchor + after);
40
+ let end = max;
41
+ for (let cursor = anchor; cursor < max; cursor += 1) {
42
+ if (masked[cursor] === ';') {
43
+ end = cursor + 1;
44
+ break;
45
+ }
46
+ }
47
+ return source.slice(start, end);
48
+ }
49
+
50
+ function collectPersistenceAccesses(text, { reads = true, writes = true } = {}) {
51
+ const source = String(text || '');
52
+ const masked = maskCommentsAndStrings(source);
53
+ const accesses = [];
54
+ const addMaskedCalls = (pattern) => {
55
+ for (const match of masked.matchAll(pattern)) {
56
+ accesses.push({
57
+ index: match.index || 0,
58
+ window: callWindow(masked, match.index || 0),
59
+ });
60
+ }
61
+ };
62
+ const addTextWindows = (pattern, before = 40, after = 700) => {
63
+ for (const match of source.matchAll(pattern)) {
64
+ accesses.push({
65
+ index: match.index || 0,
66
+ window: localWindow(source, match.index || 0, before, after),
67
+ });
68
+ }
69
+ };
70
+
71
+ if (reads) addMaskedCalls(/\bprisma\.\w+\.(find(?:Unique|First|Many)?|aggregate|count)\s*\(/gi);
72
+ if (writes) addMaskedCalls(/\bprisma\.\w+\.(create|update|delete|upsert)\s*\(/gi);
73
+ if (reads) addTextWindows(/\bSELECT\b[\s\S]{0,180}\bFROM\b/gi);
74
+ if (writes) addTextWindows(/\b(UPDATE\s+\w+\s+SET|DELETE\s+FROM|INSERT\s+INTO)\b/gi);
75
+ if (reads || writes) {
76
+ addMaskedCalls(/\b(pool|client|db|database)\.(query|execute|select|from|update|delete|insert)\b/gi);
77
+ }
78
+ return accesses;
79
+ }
80
+
81
+ function accessHasScopedPredicate(window) {
82
+ const codeWindow = maskCommentsAndStrings(window);
83
+ return ((/\bwhere\s*:/.test(codeWindow) && scopeToken.test(codeWindow))
84
+ || (/\bWHERE\b/i.test(window) && scopeToken.test(window)));
85
+ }
86
+
87
+ function hasScopedPersistencePredicate(text) {
88
+ return collectPersistenceAccesses(text).some((access) => accessHasScopedPredicate(access.window));
89
+ }
90
+
91
+ function hasPersistenceForOptions(text, { reads = true, writes = true } = {}) {
92
+ return (reads && hasPersistenceRead(text)) || (writes && hasPersistenceWrite(text));
93
+ }
94
+
95
+ export function hasUnscopedPersistenceAccess(text, options) {
96
+ const accesses = collectPersistenceAccesses(text, options);
97
+ if (accesses.length === 0) return hasPersistenceForOptions(text, options) && !hasScopedPersistencePredicate(text);
98
+ return accesses.some((access) => !accessHasScopedPredicate(access.window));
99
+ }
100
+
101
+ function hasWriteSafetyMarker(text) {
102
+ return /\b(transaction|batch|Promise\.allSettled|idempotenc|dedup|dedupe|upsert|ON CONFLICT|on conflict)\b/i.test(maskCommentsAndStrings(text));
103
+ }
104
+
105
+ function collectPersistenceWrites(text) {
106
+ return collectPersistenceAccesses(text, { reads: false, writes: true });
107
+ }
108
+
109
+ function writeStatementHasSafety(text, index) {
110
+ return hasWriteSafetyMarker(statementWindow(text, index));
111
+ }
112
+
113
+ export function hasLoopedWriteWithoutSafety(text) {
114
+ const masked = maskCommentsAndStrings(text);
115
+ return collectPersistenceWrites(text).some((access) => {
116
+ const nearby = localWindow(masked, access.index, 360, 160);
117
+ return loopPattern.test(nearby) && !writeStatementHasSafety(text, access.index);
118
+ });
119
+ }
120
+
121
+ export function hasWriteWithoutLocalSafety(text) {
122
+ const writes = collectPersistenceWrites(text);
123
+ if (writes.length === 0) return hasPersistenceWrite(text) && !hasWriteSafetyMarker(text);
124
+ return writes.some((access) => !writeStatementHasSafety(text, access.index));
125
+ }
@@ -0,0 +1,237 @@
1
+ import {
2
+ hasAuthContext,
3
+ hasMutationHandler,
4
+ hasPersistenceAccess,
5
+ hasPersistenceWrite,
6
+ hasReadHandler,
7
+ isCrawlerProducerPath,
8
+ isRouteLikePath,
9
+ isScriptPipelinePath,
10
+ isSourceCodePath,
11
+ lineAt,
12
+ violation,
13
+ } from './utils.mjs';
14
+ import {
15
+ hasLoopedWriteWithoutSafety,
16
+ hasUnscopedPersistenceAccess,
17
+ hasWriteWithoutLocalSafety,
18
+ } from './data-boundary-locality.mjs';
19
+
20
+ export function scanAuthzDataIsolation(relPath, text) {
21
+ if (!isRouteLikePath(relPath)) return [];
22
+ const out = [];
23
+ const hasDbAccess = hasPersistenceAccess(text);
24
+ const authContextVisible = hasAuthContext(text);
25
+ const unscopedAccessVisible = hasUnscopedPersistenceAccess(text);
26
+ const unscopedReadVisible = hasUnscopedPersistenceAccess(text, { reads: true, writes: false });
27
+ if (hasDbAccess && authContextVisible && unscopedAccessVisible && !hasReadHandler(text)) {
28
+ out.push(violation({
29
+ ruleId: 'authz.scope_not_visible',
30
+ severity: 'warning',
31
+ relPath,
32
+ symbol: 'authz-scope',
33
+ message: 'Route uses auth context and persistence but no obvious owner or tenant filter is visible; review data isolation before ship.',
34
+ confidence: 'low',
35
+ }));
36
+ }
37
+ if (hasDbAccess && hasReadHandler(text) && !authContextVisible) {
38
+ out.push(violation({
39
+ ruleId: 'authz.read_without_auth_context',
40
+ severity: 'warning',
41
+ relPath,
42
+ symbol: 'authz-read',
43
+ message: 'Read path touches persistence without obvious auth or permission context; confirm whether the route is intentionally public.',
44
+ confidence: 'low',
45
+ }));
46
+ }
47
+ if (hasDbAccess && hasReadHandler(text) && authContextVisible && unscopedReadVisible) {
48
+ out.push(violation({
49
+ ruleId: 'authz.read_scope_not_visible',
50
+ severity: 'warning',
51
+ relPath,
52
+ symbol: 'authz-read-scope',
53
+ message: 'Read path uses auth context and persistence but no obvious owner or tenant filter is visible; review data isolation before ship.',
54
+ confidence: 'low',
55
+ }));
56
+ }
57
+ if (hasDbAccess && hasMutationHandler(text) && !authContextVisible) {
58
+ out.push(violation({
59
+ ruleId: 'authz.mutation_without_auth_context',
60
+ severity: 'warning',
61
+ relPath,
62
+ symbol: 'authz-mutation',
63
+ message: 'Mutation path touches persistence without obvious auth or permission context; confirm whether the route is intentionally public.',
64
+ confidence: 'low',
65
+ }));
66
+ }
67
+ return out;
68
+ }
69
+
70
+ export function scanWriteSafety(relPath, text) {
71
+ const out = [];
72
+ const hasWrite = hasPersistenceWrite(text);
73
+ const unsafeLoopedWrite = hasLoopedWriteWithoutSafety(text);
74
+ const unsafeWrite = hasWriteWithoutLocalSafety(text);
75
+ const writeSafetyPath = isRouteLikePath(relPath)
76
+ || isScriptPipelinePath(relPath)
77
+ || /(^|\/)(repositories?|queries|db|database|migrations?)\//i.test(relPath);
78
+ if (writeSafetyPath
79
+ && hasWrite
80
+ && unsafeLoopedWrite) {
81
+ out.push(violation({
82
+ ruleId: 'write.loop_without_transaction',
83
+ severity: 'warning',
84
+ relPath,
85
+ symbol: 'write-loop',
86
+ message: 'Repeated writes appear inside a loop without an obvious transaction, batch, or dedupe strategy; review write safety before ship.',
87
+ confidence: 'low',
88
+ }));
89
+ }
90
+ if (isRouteLikePath(relPath)
91
+ && hasMutationHandler(text)
92
+ && hasWrite
93
+ && unsafeWrite) {
94
+ out.push(violation({
95
+ ruleId: 'write.mutation_retry_safety',
96
+ severity: 'warning',
97
+ relPath,
98
+ symbol: 'mutation-retry-safety',
99
+ message: 'Mutation route has no obvious idempotency, dedupe, or transaction marker; review duplicate execution and partial-write risk.',
100
+ confidence: 'low',
101
+ }));
102
+ }
103
+ return out;
104
+ }
105
+
106
+ export function scanApiContractCompatibility(relPath, text) {
107
+ if (!isRouteLikePath(relPath)) return [];
108
+ const out = [];
109
+ if (/\b(request\.json\(|req\.body\b|params\.[A-Za-z_$]|\bsearchParams\.get\(|new URLSearchParams\b)/.test(text)
110
+ && !/\b(safeParse|parseAsync|schema|z\.object|validate|validator|assert)\b/.test(text)) {
111
+ out.push(violation({
112
+ ruleId: 'contract.boundary_without_schema',
113
+ severity: 'warning',
114
+ relPath,
115
+ symbol: 'boundary-without-schema',
116
+ message: 'Route reads request body, params, or search params without an obvious schema or validator; review contract compatibility before ship.',
117
+ confidence: 'medium',
118
+ }));
119
+ }
120
+ if (/\b(Response\.json|NextResponse\.json|res\.json)\(\s*await\s+(?:prisma|db|client|pool)|\breturn\s+(?:await\s+)?(?:prisma|db|client|pool)\./.test(text)) {
121
+ out.push(violation({
122
+ ruleId: 'contract.raw_storage_response',
123
+ severity: 'warning',
124
+ relPath,
125
+ symbol: 'raw-storage-response',
126
+ message: 'Route appears to expose storage-shaped data directly; review DTO mapping and caller compatibility before ship.',
127
+ confidence: 'low',
128
+ relatedKey: 'raw-storage-response',
129
+ }));
130
+ }
131
+ return out;
132
+ }
133
+
134
+ export function scanSqlParameterBinding(relPath, text) {
135
+ if (!isSourceCodePath(relPath)) return [];
136
+ const out = [];
137
+ const rawSqlTemplate = /(?:(\b(?:sql|Prisma\.sql|db\.sql|pgSql))\s*)?`[^`]*(?:SELECT\s+[\s\S]{0,120}\s+FROM|INSERT\s+INTO|UPDATE\s+[A-Za-z_][\w.]*\s+SET|DELETE\s+FROM)[^`]*\$\{[^`]+`/gis;
138
+ const rawSqlConcat = /(?:query|execute)\s*\(\s*['"][^'"]*(?:SELECT\s+[\s\S]{0,120}\s+FROM|INSERT\s+INTO|UPDATE\s+[A-Za-z_][\w.]*\s+SET|DELETE\s+FROM)[^'"]*['"]\s*\+/isu;
139
+ const pythonFStringSql = /f["'][^"']*(?:SELECT\s+[\s\S]{0,120}\s+FROM|INSERT\s+INTO|UPDATE\s+[A-Za-z_][\w.]*\s+SET|DELETE\s+FROM)[^"']*\{[^"']+["']/isu;
140
+ const unsafeTemplate = [...text.matchAll(rawSqlTemplate)].some((match) => !match[1]);
141
+ const assembledQueryNames = new Set();
142
+ for (const match of text.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:`[^`]*(?:SELECT\s+[\s\S]{0,120}\s+FROM|INSERT\s+INTO|UPDATE\s+[A-Za-z_][\w.]*\s+SET|DELETE\s+FROM)[^`]*\$\{[^`]+`|['"][^'"]*(?:SELECT\s+[\s\S]{0,120}\s+FROM|INSERT\s+INTO|UPDATE\s+[A-Za-z_][\w.]*\s+SET|DELETE\s+FROM)[^'"]*['"]\s*\+)/gis)) {
143
+ assembledQueryNames.add(match[1]);
144
+ }
145
+ const assembledQueryExecuted = [...assembledQueryNames].some((name) => new RegExp(`\\b(?:query|execute)\\s*\\(\\s*${name}\\b`).test(text));
146
+ if (unsafeTemplate || rawSqlConcat.test(text) || pythonFStringSql.test(text) || assembledQueryExecuted) {
147
+ out.push(violation({
148
+ ruleId: 'sql.raw_interpolation',
149
+ severity: assembledQueryExecuted && !unsafeTemplate ? 'warning' : 'error',
150
+ relPath,
151
+ symbol: assembledQueryExecuted ? 'assembled-query-interpolation' : 'raw-sql-interpolation',
152
+ message: assembledQueryExecuted
153
+ ? 'SQL-like query string appears assembled before execution; verify placeholders are used instead of raw interpolation.'
154
+ : 'SQL-like string interpolation detected; use placeholders and pass values separately.',
155
+ confidence: assembledQueryExecuted && !unsafeTemplate ? 'medium' : 'high',
156
+ }));
157
+ }
158
+ return out;
159
+ }
160
+
161
+ export function scanPublicSafeError(relPath, text) {
162
+ if (!isRouteLikePath(relPath)) return [];
163
+ const out = [];
164
+ const lines = text.split(/\r?\n/);
165
+ lines.forEach((line, index) => {
166
+ if (/\b(Response\.json|NextResponse\.json|res\.json)\b/.test(line)
167
+ && /\b(stack|error\.message|err\.message|cause|details)\b/i.test(line)) {
168
+ out.push(violation({
169
+ ruleId: 'error.public_raw_details',
170
+ severity: 'warning',
171
+ relPath,
172
+ line: index + 1,
173
+ symbol: 'public-error-details',
174
+ message: 'Public response appears to include raw error details; map to a stable public code and keep diagnostics internal.',
175
+ confidence: 'medium',
176
+ }));
177
+ }
178
+ });
179
+ for (const match of text.matchAll(/\b(Response\.json|NextResponse\.json|res\.json)\s*\(([\s\S]{0,360})\)/gu)) {
180
+ const expression = match[0];
181
+ if (!/\n/.test(expression)) continue;
182
+ if (/\b(stack|error\.message|err\.message|cause|details)\b/i.test(expression)) {
183
+ out.push(violation({
184
+ ruleId: 'error.public_raw_details',
185
+ severity: 'warning',
186
+ relPath,
187
+ line: lineAt(text, match.index || 0),
188
+ symbol: 'public-error-details',
189
+ message: 'Public response appears to include raw error details; map to a stable public code and keep diagnostics internal.',
190
+ confidence: 'medium',
191
+ }));
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
197
+ export function scanDbRowValidation(relPath, text) {
198
+ if (!isRouteLikePath(relPath)) return [];
199
+ const directStorageResponse = /\b(Response\.json|NextResponse\.json|res\.json)\(\s*await\s+(?:prisma|db|client|pool)\b/su;
200
+ const rawVariables = [...text.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+(?:prisma|db|client|pool)\b/gsu)].map((match) => match[1]);
201
+ const responseExpressions = [...text.matchAll(/\b(?:Response\.json|NextResponse\.json|res\.json)\(([\s\S]{0,300})\)/gu)].map((match) => match[1] || '');
202
+ const rawVariableReturned = rawVariables.some((name) => responseExpressions.some((expr) => new RegExp(`\\b${name}\\b`).test(expr)));
203
+ if (!directStorageResponse.test(text) && !rawVariableReturned) return [];
204
+ return [violation({
205
+ ruleId: 'database.raw_row_public_response',
206
+ severity: 'warning',
207
+ relPath,
208
+ symbol: 'raw-row-response',
209
+ message: 'Route appears to return storage-shaped data directly; validate or map rows before public DTO output.',
210
+ confidence: 'low',
211
+ relatedKey: 'raw-storage-response',
212
+ })];
213
+ }
214
+
215
+ export function scanThinApiRoute(relPath, text) {
216
+ if (!isRouteLikePath(relPath) || !hasPersistenceAccess(text)) return [];
217
+ return [violation({
218
+ ruleId: 'route.direct_db_access',
219
+ severity: 'warning',
220
+ relPath,
221
+ symbol: 'route-db-access',
222
+ message: 'Route/controller appears to contain direct persistence access; review whether auth, validation, usecase, repository, and response seams are thin enough.',
223
+ confidence: 'low',
224
+ })];
225
+ }
226
+
227
+ export function scanCrawlerProducerBoundary(relPath, text) {
228
+ if (!isCrawlerProducerPath(relPath) || !hasPersistenceWrite(text)) return [];
229
+ return [violation({
230
+ ruleId: 'crawler.producer_direct_persistence',
231
+ severity: 'warning',
232
+ relPath,
233
+ symbol: 'crawler-direct-write',
234
+ message: 'Crawler/automation producer appears to write directly to persistence; review artifact handoff and consumer-side validation before ship.',
235
+ confidence: 'low',
236
+ })];
237
+ }
@@ -0,0 +1,74 @@
1
+ import { localWindow, maskCommentsAndStrings } from './utils.mjs';
2
+
3
+ export function hasValidationMarker(text) {
4
+ return /\b(safeParse|parseAsync|schema\.parse|schema\.safeParse|z\.object|validate|validator|assert|parseEnv|requiredEnv|validated|sanitize)\b/i.test(text);
5
+ }
6
+
7
+ function hasLocalValidation(text, index, before = 360, after = 720) {
8
+ return hasValidationMarker(localWindow(maskCommentsAndStrings(text), index, before, after));
9
+ }
10
+
11
+ export function externalInputValidationFindings(relPath, text) {
12
+ const out = [];
13
+ const markerText = maskCommentsAndStrings(text);
14
+ for (const match of markerText.matchAll(/\bJSON\.parse\s*\(/g)) {
15
+ const before = localWindow(markerText, match.index || 0, 260, 20);
16
+ if (/\b(readFileSync|readFile)\s*\(/.test(before) && !hasLocalValidation(text, match.index || 0)) {
17
+ out.push({
18
+ ruleId: 'input.file_parse_unvalidated',
19
+ severity: 'warning',
20
+ relPath,
21
+ line: undefined,
22
+ symbol: 'file-json-parse',
23
+ message: 'File input appears parsed without an obvious schema/validator; validate before trusting file contents.',
24
+ confidence: 'low',
25
+ });
26
+ break;
27
+ }
28
+ }
29
+ for (const match of markerText.matchAll(/\.json\s*\(\s*\)/g)) {
30
+ const window = localWindow(text, match.index || 0, 420, 260);
31
+ if (/\bfetch\s*\(\s*['"]https?:\/\//.test(window) && !hasLocalValidation(text, match.index || 0)) {
32
+ out.push({
33
+ ruleId: 'input.third_party_json_unvalidated',
34
+ severity: 'warning',
35
+ relPath,
36
+ symbol: 'third-party-json',
37
+ message: 'Third-party fetch JSON appears used without an obvious schema/validator; validate response shape before trusting it.',
38
+ confidence: 'low',
39
+ });
40
+ break;
41
+ }
42
+ }
43
+ for (const match of markerText.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+(?:request|req)\.json\s*\(/g)) {
44
+ const bodyVar = match[1];
45
+ if (hasLocalValidation(text, match.index || 0, 40, 720)) continue;
46
+ const after = localWindow(markerText, match.index || 0, 0, 1100);
47
+ const directUse = new RegExp(`\\b(?:create|update|upsert|delete|save|mutate|service\\.[A-Za-z_$][\\w$]*|usecase\\.[A-Za-z_$][\\w$]*)\\s*\\(\\s*${bodyVar}\\b`).test(after);
48
+ if (directUse) {
49
+ out.push({
50
+ ruleId: 'input.request_body_direct_use',
51
+ severity: 'warning',
52
+ relPath,
53
+ symbol: 'request-body-direct-use',
54
+ message: 'Request body appears passed directly into a mutation/service call without an obvious schema/validator.',
55
+ confidence: 'low',
56
+ });
57
+ break;
58
+ }
59
+ }
60
+ for (const match of markerText.matchAll(/\bexport\s+(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:\{[\s\S]{0,240})?process\.env\.[A-Z0-9_]+\b/g)) {
61
+ if (!hasLocalValidation(text, match.index || 0)) {
62
+ out.push({
63
+ ruleId: 'input.env_export_unvalidated',
64
+ severity: 'warning',
65
+ relPath,
66
+ symbol: 'env-export-unvalidated',
67
+ message: 'Environment-derived config appears exported without an obvious validation/fallback boundary.',
68
+ confidence: 'low',
69
+ });
70
+ break;
71
+ }
72
+ }
73
+ return out;
74
+ }
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { effectiveRuleMode } from '../../profile.mjs';
4
+ import { FINDING_METADATA } from '../registry.mjs';
5
+ import { externalInputValidationFindings } from './external-input.mjs';
6
+ import {
7
+ scanBroadExceptionAdvisory,
8
+ scanFileSizeAdvisory,
9
+ scanResponsibilityBudget,
10
+ scanSecretLogging,
11
+ scanSilentFailures,
12
+ scanTypeEscapeAdvisory,
13
+ scanWorkflowSecurity,
14
+ } from './code-health.mjs';
15
+ import {
16
+ scanApiContractCompatibility,
17
+ scanAuthzDataIsolation,
18
+ scanCrawlerProducerBoundary,
19
+ scanDbRowValidation,
20
+ scanPublicSafeError,
21
+ scanSqlParameterBinding,
22
+ scanThinApiRoute,
23
+ scanWriteSafety,
24
+ } from './data-boundary.mjs';
25
+ import {
26
+ scanClientServerBoundary,
27
+ scanPerformanceDuplicateFetch,
28
+ scanRuntimeEnvSafety,
29
+ scanSideEffectBoundary,
30
+ scanStateSafety,
31
+ } from './ui-runtime.mjs';
32
+ import { scanSingleResponsibility } from './single-responsibility.mjs';
33
+ import { isSourceCodePath, violation } from './utils.mjs';
34
+
35
+ const ACTIVE_PROFILE_MODES = new Set(['advisory', 'changed-files', 'baseline-new-only', 'strict']);
36
+
37
+ export { violation } from './utils.mjs';
38
+
39
+ function scanExternalInputValidation(relPath, text) {
40
+ if (!isSourceCodePath(relPath)) return [];
41
+ return externalInputValidationFindings(relPath, text).map((item) => violation(item));
42
+ }
43
+
44
+ export const SCANNER_REGISTRY = [
45
+ { id: 'scanSilentFailures', families: ['no_silent_failure'], scan: ({ relPath, text }) => scanSilentFailures(relPath, text) },
46
+ { id: 'scanSecretLogging', families: ['no_secret_logging'], scan: ({ relPath, text }) => scanSecretLogging(relPath, text) },
47
+ { id: 'scanClientServerBoundary', families: ['component_responsibility'], scan: ({ relPath, text }) => scanClientServerBoundary(relPath, text) },
48
+ { id: 'scanWorkflowSecurity', families: ['workflow_security'], scan: ({ relPath, text }) => scanWorkflowSecurity(relPath, text) },
49
+ { id: 'scanExternalInputValidation', families: ['external_input_validation'], scan: ({ relPath, text }) => scanExternalInputValidation(relPath, text) },
50
+ { id: 'scanFileSizeAdvisory', families: ['file_size_advisory'], scan: ({ relPath, text, settings }) => scanFileSizeAdvisory(relPath, text, settings.fileSize) },
51
+ { id: 'scanResponsibilityBudget', families: ['responsibility_budget'], scan: ({ relPath, text, settings }) => scanResponsibilityBudget(relPath, text, settings.responsibilityBudget) },
52
+ { id: 'scanSingleResponsibility', families: ['single_responsibility_advisory'], scan: ({ relPath, text, settings }) => scanSingleResponsibility(relPath, text, settings.singleResponsibility) },
53
+ { id: 'scanStateSafety', families: ['null_state_safety'], scan: ({ relPath, text }) => scanStateSafety(relPath, text) },
54
+ { id: 'scanAuthzDataIsolation', families: ['authz_data_isolation'], scan: ({ relPath, text }) => scanAuthzDataIsolation(relPath, text) },
55
+ { id: 'scanRuntimeEnvSafety', families: ['build_runtime_env_safety'], scan: ({ relPath, text }) => scanRuntimeEnvSafety(relPath, text) },
56
+ { id: 'scanWriteSafety', families: ['write_safety_idempotency'], scan: ({ relPath, text }) => scanWriteSafety(relPath, text) },
57
+ { id: 'scanApiContractCompatibility', families: ['api_contract_compatibility'], scan: ({ relPath, text }) => scanApiContractCompatibility(relPath, text) },
58
+ { id: 'scanPerformanceDuplicateFetch', families: ['performance_duplicate_fetch'], scan: ({ relPath, text }) => scanPerformanceDuplicateFetch(relPath, text) },
59
+ { id: 'scanSqlParameterBinding', families: ['sql_parameter_binding'], scan: ({ relPath, text }) => scanSqlParameterBinding(relPath, text) },
60
+ { id: 'scanPublicSafeError', families: ['public_safe_error'], scan: ({ relPath, text }) => scanPublicSafeError(relPath, text) },
61
+ { id: 'scanDbRowValidation', families: ['db_row_validation'], scan: ({ relPath, text }) => scanDbRowValidation(relPath, text) },
62
+ { id: 'scanThinApiRoute', families: ['thin_api_route'], scan: ({ relPath, text }) => scanThinApiRoute(relPath, text) },
63
+ { id: 'scanTypeEscapeAdvisory', families: ['type_escape_advisory'], scan: ({ relPath, text }) => scanTypeEscapeAdvisory(relPath, text) },
64
+ { id: 'scanSideEffectBoundary', families: ['side_effect_boundary'], scan: ({ relPath, text }) => scanSideEffectBoundary(relPath, text) },
65
+ { id: 'scanCrawlerProducerBoundary', families: ['crawler_producer_boundary'], scan: ({ relPath, text }) => scanCrawlerProducerBoundary(relPath, text) },
66
+ { id: 'scanBroadExceptionAdvisory', families: ['broad_exception_advisory'], scan: ({ relPath, text }) => scanBroadExceptionAdvisory(relPath, text) },
67
+ ];
68
+
69
+ export function validateScannerRegistry() {
70
+ const errors = [];
71
+ const seen = new Set();
72
+ for (const scanner of SCANNER_REGISTRY) {
73
+ if (!scanner?.id || typeof scanner.scan !== 'function' || !Array.isArray(scanner.families)) {
74
+ errors.push(`Invalid scanner registry entry: ${JSON.stringify(scanner?.id || scanner)}`);
75
+ continue;
76
+ }
77
+ if (seen.has(scanner.id)) errors.push(`Duplicate scanner registry id: ${scanner.id}`);
78
+ seen.add(scanner.id);
79
+ }
80
+ for (const [findingId, metadata] of Object.entries(FINDING_METADATA)) {
81
+ if (!seen.has(metadata.scanner)) errors.push(`Finding ${findingId} references missing scanner registry id ${metadata.scanner}`);
82
+ }
83
+ return errors;
84
+ }
85
+
86
+ const registryErrors = validateScannerRegistry();
87
+ if (registryErrors.length) {
88
+ throw new Error(`Invalid guard scanner registry:\n${registryErrors.map((error) => `- ${error}`).join('\n')}`);
89
+ }
90
+
91
+ function decorateViolation(item, profile) {
92
+ const metadata = FINDING_METADATA[item.rule_id] || { family: item.rule_id, pack: 'core', scanner: item.source || 'unknown' };
93
+ const effectiveMode = effectiveRuleMode(profile, metadata);
94
+ return {
95
+ ...item,
96
+ rule_family: metadata.family,
97
+ scanner_id: metadata.scanner,
98
+ effective_mode: effectiveMode,
99
+ };
100
+ }
101
+
102
+ function isModeActiveForScope(mode, scope) {
103
+ if (mode === 'changed-files' && scope === 'all') return false;
104
+ return ACTIVE_PROFILE_MODES.has(mode);
105
+ }
106
+
107
+ function applyProfileModes(violations, profile, { scope } = {}) {
108
+ return violations
109
+ .map((item) => decorateViolation(item, profile))
110
+ .filter((item) => isModeActiveForScope(item.effective_mode, scope));
111
+ }
112
+
113
+ export function scanText(relPath, text, settings = {}) {
114
+ const raw = SCANNER_REGISTRY.flatMap((scanner) => scanner.scan({ relPath, text, settings }));
115
+ if (settings.applyProfile === false) return raw.map((item) => decorateViolation(item, settings.profile || {}));
116
+ return applyProfileModes(raw, settings.profile || {}, { scope: settings.scope });
117
+ }
118
+
119
+ export function scanFile(repoRoot, relPath, settings = {}) {
120
+ const full = path.join(repoRoot, relPath);
121
+ let text;
122
+ try {
123
+ text = fs.readFileSync(full, 'utf8');
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ return {
127
+ violations: [],
128
+ failure: {
129
+ code: 'guard.scan.file_read',
130
+ message: `Failed to read ${relPath}`,
131
+ details: [message],
132
+ },
133
+ };
134
+ }
135
+ return { violations: scanText(relPath, text, settings), failure: null };
136
+ }