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,84 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applyPlan,
4
+ confirmPlan,
5
+ EXIT_CONFIG_FAILURE,
6
+ maybeInstallMissingForConnect,
7
+ normalizeOptions,
8
+ printApplyResult,
9
+ printConfigErrors,
10
+ printPlanSummary,
11
+ resolvePlan,
12
+ usage,
13
+ } from './install-flow.mjs';
14
+
15
+ async function main() {
16
+ const nonInteractive = !process.stdin.isTTY;
17
+ const options = normalizeOptions(process.argv.slice(2), {
18
+ command: 'connect',
19
+ cwd: process.cwd(),
20
+ nonInteractive,
21
+ });
22
+ if (options.help) {
23
+ usage('connect');
24
+ return;
25
+ }
26
+ if (options.errors.length > 0) {
27
+ printConfigErrors('connect', options.errors);
28
+ return;
29
+ }
30
+
31
+ if (!options.mode && !options.yes) {
32
+ options.mode = 'normal';
33
+ } else if (!options.mode && options.yes) {
34
+ options.mode = 'normal';
35
+ }
36
+
37
+ let resolved = await resolvePlan(options);
38
+ if (resolved.cancelled) {
39
+ console.log('Connection cancelled. No changes made.');
40
+ return;
41
+ }
42
+ if (resolved.errors?.length) {
43
+ printConfigErrors('connect', resolved.errors);
44
+ return;
45
+ }
46
+
47
+ let { plan } = resolved;
48
+ printPlanSummary(plan);
49
+
50
+ let confirmation = await confirmPlan(plan);
51
+ if (confirmation === 'cancel') {
52
+ console.log('Connection cancelled. No changes made.');
53
+ return;
54
+ }
55
+ if (confirmation === 'custom') {
56
+ resolved = await resolvePlan({ ...options, mode: 'custom', explicitMode: true });
57
+ if (resolved.errors?.length) {
58
+ printConfigErrors('connect', resolved.errors);
59
+ return;
60
+ }
61
+ plan = resolved.plan;
62
+ printPlanSummary(plan);
63
+ confirmation = await confirmPlan(plan);
64
+ if (confirmation !== 'yes') {
65
+ console.log('Connection cancelled. No changes made.');
66
+ return;
67
+ }
68
+ }
69
+
70
+ const missing = await maybeInstallMissingForConnect(plan);
71
+ if (!missing.ok) {
72
+ printConfigErrors('connect', missing.errors);
73
+ return;
74
+ }
75
+
76
+ const result = applyPlan(plan);
77
+ printApplyResult(plan, result);
78
+ if (result.exitCode) process.exitCode = result.exitCode;
79
+ }
80
+
81
+ main().catch((error) => {
82
+ console.error(error instanceof Error ? error.message : String(error));
83
+ process.exit(EXIT_CONFIG_FAILURE);
84
+ });
@@ -0,0 +1,167 @@
1
+ import fs from 'node:fs';
2
+ import { scanText as scanSharedGuardText } from '../guard/scanners/index.mjs';
3
+
4
+ function candidate(list, kind, file, line, detail, severity = 'advisory') {
5
+ list.push({ kind, file: file.rel, line, detail, severity });
6
+ }
7
+
8
+ const SHARED_SCANNER_BUCKETS = new Map([
9
+ ['no_silent_failure', 'silentFailures'],
10
+ ['broad_exception_advisory', 'silentFailures'],
11
+ ['no_secret_logging', 'secretLogging'],
12
+ ['file_size_advisory', 'largeFiles'],
13
+ ['responsibility_budget', 'responsibilityBudget'],
14
+ ['single_responsibility_advisory', 'singleResponsibility'],
15
+ ['component_responsibility', 'clientServerSeam'],
16
+ ['external_input_validation', 'externalInput'],
17
+ ['null_state_safety', 'stateSafety'],
18
+ ['authz_data_isolation', 'authzIsolation'],
19
+ ['build_runtime_env_safety', 'runtimeEnv'],
20
+ ['write_safety_idempotency', 'writeSafety'],
21
+ ['api_contract_compatibility', 'apiContract'],
22
+ ['public_safe_error', 'apiContract'],
23
+ ['db_row_validation', 'apiContract'],
24
+ ['performance_duplicate_fetch', 'performanceDuplication'],
25
+ ['sql_parameter_binding', 'rawSql'],
26
+ ['thin_api_route', 'dbInRoutes'],
27
+ ['type_escape_advisory', 'typeEscapes'],
28
+ ['side_effect_boundary', 'hiddenSideEffects'],
29
+ ['crawler_producer_boundary', 'hiddenSideEffects'],
30
+ ]);
31
+
32
+ function deepScanSeverity(finding) {
33
+ if (finding.rule_id === 'file_size.review' || finding.severity === 'error') return 'review';
34
+ if (finding.severity === 'warning') return 'warning';
35
+ return 'advisory';
36
+ }
37
+
38
+ function addSharedScannerCandidates(file, text, findings, thresholds) {
39
+ const sharedFindings = scanSharedGuardText(file.rel, text, {
40
+ applyProfile: false,
41
+ fileSize: thresholds.fileSize,
42
+ responsibilityBudget: thresholds.responsibility,
43
+ singleResponsibility: thresholds.singleResponsibility,
44
+ });
45
+ const seen = new Set();
46
+ for (const finding of sharedFindings) {
47
+ const bucket = SHARED_SCANNER_BUCKETS.get(finding.rule_family);
48
+ if (!bucket || !findings[bucket]) continue;
49
+ const key = `${bucket}:${finding.rule_id}:${finding.line}:${finding.message}`;
50
+ if (seen.has(key)) continue;
51
+ seen.add(key);
52
+ candidate(
53
+ findings[bucket],
54
+ `${finding.rule_family} candidate`,
55
+ file,
56
+ finding.line || 1,
57
+ finding.message,
58
+ deepScanSeverity(finding),
59
+ );
60
+ if (finding.rule_id === 'responsibility.route.budget') {
61
+ candidate(
62
+ findings.routeResponsibility,
63
+ `${finding.rule_family} candidate`,
64
+ file,
65
+ finding.line || 1,
66
+ finding.message,
67
+ deepScanSeverity(finding),
68
+ );
69
+ }
70
+ }
71
+ }
72
+
73
+ function hasUseClientDirective(text) {
74
+ return /^\s*(?:"use client"|'use client')\s*;?/u.test(text);
75
+ }
76
+
77
+ function isScriptPipeline(file) {
78
+ return /(^|\/)scripts\/(data|ops|import|imports|backfill|repair|migrate|migration)\//.test(file.rel)
79
+ && /\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(file.rel);
80
+ }
81
+
82
+ function matchedResponsibilityHints(text, hintGroups) {
83
+ return hintGroups
84
+ .filter((group) => group.patterns.some((pattern) => pattern.test(text)))
85
+ .map((group) => group.label);
86
+ }
87
+
88
+ function scanMixedResponsibilities(file, text, findings) {
89
+ if (hasUseClientDirective(text)) {
90
+ const hints = matchedResponsibilityHints(text, [
91
+ { label: 'browser storage', patterns: [/\b(localStorage|sessionStorage)\b/] },
92
+ { label: 'network/API', patterns: [/\bfetch\s*\(/, /\baxios\./, /\buse(Query|Mutation)\s*\(/] },
93
+ { label: 'toast/notification', patterns: [/\btoast\b/, /\bnotify\b/] },
94
+ { label: 'modal/dialog state', patterns: [/\b(Dialog|Modal|Sheet)\b/, /\bopen[A-Z]\w*\b/, /\bis[A-Z]\w*Open\b/] },
95
+ { label: 'route navigation', patterns: [/\buseRouter\s*\(/, /\brouter\.(push|replace|refresh)\b/] },
96
+ { label: 'heavy mapping', patterns: [/\.(map|filter|reduce)\s*\(/] },
97
+ ]);
98
+ if (hints.length >= 3) {
99
+ candidate(findings.responsibilityBudget, 'mixed client responsibility candidate', file, 1, `client module mixes ${hints.slice(0, 4).join(', ')}; review hook/adapter/presentation split`, 'warning');
100
+ }
101
+ }
102
+
103
+ const routeLike = /(^|\/)(api|routes?|controllers?|pages\/api)\//i.test(file.rel) || /route\.(ts|js)$/.test(file.rel);
104
+ if (routeLike) {
105
+ const hints = matchedResponsibilityHints(text, [
106
+ { label: 'auth/session', patterns: [/\b(auth|session|permission|currentUser|getUser)\b/i] },
107
+ { label: 'validation', patterns: [/\b(z\.object|safeParse|parseAsync|validate|schema)\b/] },
108
+ { label: 'database', patterns: [/\b(prisma|pool\.query|client\.query|SELECT|INSERT|UPDATE|DELETE|db\.)\b/i] },
109
+ { label: 'response formatting', patterns: [/\b(Response\.json|NextResponse\.json|res\.json)\b/] },
110
+ ]);
111
+ if (hints.length >= 3) {
112
+ candidate(findings.responsibilityBudget, 'mixed route responsibility candidate', file, 1, `route/controller mixes ${hints.join(', ')}; review route/service/repository/response split`, 'warning');
113
+ }
114
+ }
115
+
116
+ if (isScriptPipeline(file)) {
117
+ const hints = matchedResponsibilityHints(text, [
118
+ { label: 'CLI parsing', patterns: [/\b(process\.argv|argparse|ArgumentParser|commander)\b/] },
119
+ { label: 'file IO', patterns: [/\b(readFile|writeFile|open\(|Path\(|fs\.)\b/] },
120
+ { label: 'data transform', patterns: [/\.(map|filter|reduce)\s*\(/, /\bjson\.loads\b/i, /\bJSON\.parse\b/] },
121
+ { label: 'persistence/network', patterns: [/\b(fetch|pool\.query|client\.query|INSERT|UPDATE|DELETE|requests\.)\b/i] },
122
+ { label: 'reporting', patterns: [/\b(console\.|print\(|logger\.)\b/] },
123
+ ]);
124
+ if (hints.length >= 4) {
125
+ candidate(findings.responsibilityBudget, 'mixed script responsibility candidate', file, 1, `script mixes ${hints.join(', ')}; review CLI/loader/transform/persist/report seams`, 'warning');
126
+ }
127
+ }
128
+ }
129
+
130
+ function newFindingsBag() {
131
+ return {
132
+ largeFiles: [],
133
+ silentFailures: [],
134
+ typeEscapes: [],
135
+ rawSql: [],
136
+ dbInRoutes: [],
137
+ routeResponsibility: [],
138
+ responsibilityBudget: [],
139
+ singleResponsibility: [],
140
+ clientServerSeam: [],
141
+ hiddenSideEffects: [],
142
+ secretLogging: [],
143
+ stateSafety: [],
144
+ authzIsolation: [],
145
+ runtimeEnv: [],
146
+ writeSafety: [],
147
+ apiContract: [],
148
+ performanceDuplication: [],
149
+ externalInput: [],
150
+ scanWarnings: [],
151
+ };
152
+ }
153
+
154
+ export function scanFiles(files, thresholds) {
155
+ const findings = newFindingsBag();
156
+ for (const file of files) {
157
+ try {
158
+ const text = fs.readFileSync(file.full, 'utf8');
159
+ addSharedScannerCandidates(file, text, findings, thresholds);
160
+ scanMixedResponsibilities(file, text, findings);
161
+ } catch (error) {
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ candidate(findings.scanWarnings, 'scan warning', file, 1, `file could not be scanned and was omitted from rule candidates: ${message}`, 'warning');
164
+ }
165
+ }
166
+ return findings;
167
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { isPathInside, relativeDisplay } from '../shared.mjs';
5
+
6
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.sql', '.md', '.mdx', '.json']);
7
+ const TEXT_EXTENSIONS = new Set([...SOURCE_EXTENSIONS, '.yaml', '.yml', '.toml']);
8
+ const EXCLUDED_DIRS = new Set([
9
+ '.git',
10
+ 'node_modules',
11
+ 'vendor',
12
+ 'dist',
13
+ 'build',
14
+ '.next',
15
+ 'out',
16
+ 'coverage',
17
+ '.turbo',
18
+ '.cache',
19
+ '__pycache__',
20
+ ]);
21
+ const EXCLUDED_FILE_NAMES = new Set([
22
+ 'package-lock.json',
23
+ 'pnpm-lock.yaml',
24
+ 'yarn.lock',
25
+ 'bun.lockb',
26
+ 'bun.lock',
27
+ 'poetry.lock',
28
+ 'Pipfile.lock',
29
+ ]);
30
+ const SECRET_FILE_RE = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx|crt)|id_rsa|id_ed25519)$/i;
31
+ const MAX_FILE_BYTES = 1024 * 1024;
32
+
33
+ export function collectFiles(repoRoot) {
34
+ const files = [];
35
+ const skipped = [];
36
+ const source = {
37
+ type: 'git-ls-files',
38
+ command: 'git ls-files --cached --others --exclude-standard -z',
39
+ fallback: false,
40
+ fallback_reason: null,
41
+ listed_count: 0,
42
+ };
43
+ function skip(reason, fullPath) {
44
+ skipped.push({ reason, path: relativeDisplay(repoRoot, fullPath) });
45
+ }
46
+ function consider(full, entryName = path.basename(full)) {
47
+ const rel = relativeDisplay(repoRoot, full);
48
+ if (EXCLUDED_FILE_NAMES.has(entryName) || SECRET_FILE_RE.test(rel)) {
49
+ skip('excluded sensitive or lock file', full);
50
+ return;
51
+ }
52
+ const ext = path.extname(entryName).toLowerCase();
53
+ if (!TEXT_EXTENSIONS.has(ext)) {
54
+ skip('non-source extension', full);
55
+ return;
56
+ }
57
+ const stat = fs.statSync(full);
58
+ if (stat.size > MAX_FILE_BYTES) {
59
+ skip('large file', full);
60
+ return;
61
+ }
62
+ files.push({ full, rel, ext, size: stat.size });
63
+ }
64
+ function walk(dir) {
65
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
66
+ const full = path.join(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ if (EXCLUDED_DIRS.has(entry.name)) {
69
+ skip('excluded directory', full);
70
+ continue;
71
+ }
72
+ walk(full);
73
+ continue;
74
+ }
75
+ if (!entry.isFile()) {
76
+ skip('non-file', full);
77
+ continue;
78
+ }
79
+ consider(full, entry.name);
80
+ }
81
+ }
82
+ try {
83
+ execFileSync('git', ['-C', repoRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
84
+ const raw = execFileSync('git', ['-C', repoRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], { encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'] });
85
+ const rels = raw.toString('utf8').split('\0').map((item) => item.trim()).filter(Boolean);
86
+ source.listed_count = rels.length;
87
+ for (const rel of [...new Set(rels)].sort()) {
88
+ const full = path.resolve(repoRoot, rel);
89
+ if (!isPathInside(repoRoot, full)) {
90
+ skip('outside repository', full);
91
+ continue;
92
+ }
93
+ if (!fs.existsSync(full) || !fs.statSync(full).isFile()) {
94
+ skip('missing or non-file', full);
95
+ continue;
96
+ }
97
+ if (rel.split('/').some((part) => EXCLUDED_DIRS.has(part))) {
98
+ skip('excluded directory', full);
99
+ continue;
100
+ }
101
+ consider(full);
102
+ }
103
+ } catch (error) {
104
+ source.type = 'filesystem-fallback';
105
+ source.fallback = true;
106
+ source.fallback_reason = error instanceof Error ? error.message : String(error);
107
+ walk(repoRoot);
108
+ source.listed_count = files.length + skipped.length;
109
+ }
110
+ return { files, skipped, source };
111
+ }
112
+
113
+ export function detectStack(repoRoot, files) {
114
+ const rels = files.map((file) => file.rel);
115
+ const packageJsonPath = path.join(repoRoot, 'package.json');
116
+ const packageText = fs.existsSync(packageJsonPath) ? fs.readFileSync(packageJsonPath, 'utf8') : '';
117
+ return {
118
+ typescript: rels.some((rel) => /\.(ts|tsx)$/.test(rel)) || /typescript/.test(packageText),
119
+ react: rels.some((rel) => /\.(jsx|tsx)$/.test(rel)) || /"react"\s*:/.test(packageText),
120
+ nextjs: rels.some((rel) => /(^|\/)(next\.config\.|app\/.*route\.(js|ts)|pages\/api\/)/.test(rel)) || /"next"\s*:/.test(packageText),
121
+ python: rels.some((rel) => rel.endsWith('.py')),
122
+ postgresql: /postgres|pg\b|postgresql/i.test(packageText) || rels.some((rel) => /migrations?|sql|database|db/i.test(rel)),
123
+ crawler: rels.some((rel) => /crawler|scraper|automation|playwright|puppeteer|worker|scheduler/i.test(rel)) || /playwright|puppeteer|scrap/i.test(packageText),
124
+ };
125
+ }
126
+
127
+ export function detectInstructions(repoRoot) {
128
+ return {
129
+ agents: fs.existsSync(path.join(repoRoot, 'AGENTS.md')),
130
+ claude: fs.existsSync(path.join(repoRoot, 'CLAUDE.md')),
131
+ docs: fs.existsSync(path.join(repoRoot, 'docs')),
132
+ };
133
+ }
@@ -0,0 +1,197 @@
1
+ import path from 'node:path';
2
+ import { nowIso } from '../shared.mjs';
3
+
4
+ function tableRows(rows) {
5
+ if (rows.length === 0) return '- None found in scanned files.\n';
6
+ return rows.slice(0, 50).map((row) => `- \`${row.file}:${row.line}\` — ${row.detail}`).join('\n') + (rows.length > 50 ? `\n- ... ${rows.length - 50} more candidates omitted from summary` : '') + '\n';
7
+ }
8
+
9
+ function yesNo(value) {
10
+ return value ? 'yes' : 'no';
11
+ }
12
+
13
+ export function recommendedPacks(stack, findings) {
14
+ return [
15
+ ['core', 'advisory', 'Safe default for all repositories'],
16
+ ['web', stack.react || stack.nextjs ? 'advisory' : 'off', stack.react || stack.nextjs ? 'React or web stack detected' : 'No web stack detected'],
17
+ ['api', stack.nextjs || findings.dbInRoutes.length || findings.routeResponsibility.length ? 'changed-files' : 'advisory', 'API boundary candidates can start with changed files only'],
18
+ ['database', stack.postgresql || findings.rawSql.length ? 'advisory' : 'off', stack.postgresql || findings.rawSql.length ? 'Database or SQL hints detected' : 'No database hints detected'],
19
+ ['crawler', stack.crawler ? 'advisory' : 'off', stack.crawler ? 'Crawler or automation hints detected' : 'No crawler hints detected'],
20
+ ];
21
+ }
22
+
23
+ export function renderReport({ repoRoot, files, skipped, source, stack, instructions, findings }) {
24
+ const packRows = recommendedPacks(stack, findings);
25
+ return `# Deep Scan Report
26
+
27
+ ## Summary
28
+ - Scan time: ${nowIso()}
29
+ - Repository: ${path.basename(repoRoot)}
30
+ - Files inspected: ${files.length}
31
+ - Files skipped: ${skipped.length}
32
+ - File collection source: ${source?.type || 'unknown'}
33
+ - File collection fallback: ${source?.fallback ? `yes (${source.fallback_reason || 'unknown reason'})` : 'no'}
34
+ - Recommended packs: ${packRows.filter((row) => row[1] !== 'off').map((row) => row[0]).join(', ') || 'core'}
35
+ - Suggested default mode: advisory
36
+ - Code modified: no
37
+
38
+ ## Detected stack
39
+ - TypeScript: ${yesNo(stack.typescript)}
40
+ - React: ${yesNo(stack.react)}
41
+ - Next.js: ${yesNo(stack.nextjs)}
42
+ - Python: ${yesNo(stack.python)}
43
+ - PostgreSQL: ${yesNo(stack.postgresql)}
44
+ - Crawler/automation: ${yesNo(stack.crawler)}
45
+
46
+ ## Existing local instructions
47
+ - AGENTS.md: ${instructions.agents ? 'found' : 'not found'}
48
+ - CLAUDE.md: ${instructions.claude ? 'found' : 'not found'}
49
+ - docs guidance: ${instructions.docs ? 'found' : 'not found'}
50
+ - Conflict policy: repo-local instructions remain authoritative
51
+
52
+ ## Recommended rule modes
53
+ | Pack | Recommended mode | Reason |
54
+ |---|---|---|
55
+ ${packRows.map((row) => `| ${row[0]} | ${row[1]} | ${row[2]} |`).join('\n')}
56
+
57
+ ## Existing debt candidates
58
+
59
+ ### Existing large files
60
+ ${tableRows(findings.largeFiles)}
61
+ ### Existing silent failure candidates
62
+ ${tableRows(findings.silentFailures)}
63
+ ### Existing type escape candidates
64
+ ${tableRows(findings.typeEscapes)}
65
+ ### Existing raw SQL candidates
66
+ ${tableRows(findings.rawSql)}
67
+ ### Existing DB/API seam candidates
68
+ ${tableRows([...findings.dbInRoutes, ...findings.routeResponsibility])}
69
+ ### Existing responsibility budget candidates
70
+ ${tableRows(findings.responsibilityBudget)}
71
+ ### Existing single-responsibility candidates
72
+ ${tableRows(findings.singleResponsibility)}
73
+ ### Existing client/server seam candidates
74
+ ${tableRows(findings.clientServerSeam)}
75
+ ### Existing hidden side-effect candidates
76
+ ${tableRows(findings.hiddenSideEffects)}
77
+ ### Existing null/state safety candidates
78
+ ${tableRows(findings.stateSafety)}
79
+ ### Existing auth/data isolation candidates
80
+ ${tableRows(findings.authzIsolation)}
81
+ ### Existing runtime/env safety candidates
82
+ ${tableRows(findings.runtimeEnv)}
83
+ ### Existing write safety candidates
84
+ ${tableRows(findings.writeSafety)}
85
+ ### Existing API contract candidates
86
+ ${tableRows(findings.apiContract)}
87
+ ### Existing performance duplication candidates
88
+ ${tableRows(findings.performanceDuplication)}
89
+ ### Existing external input validation candidates
90
+ ${tableRows(findings.externalInput)}
91
+ ### Secret-like logging candidates
92
+ ${tableRows(findings.secretLogging)}
93
+ ### Scan warnings
94
+ ${tableRows(findings.scanWarnings)}
95
+
96
+ ## New-code guard candidates
97
+ - Start with changed-files mode for no_silent_failure and no_secret_logging after human review.
98
+ - Consider baseline-new-only only after accepting a baseline generated from this report.
99
+ - Keep strict disabled unless a user explicitly opts in.
100
+
101
+ ## Skipped file summary
102
+ - Excluded generated/vendor/build/dependency folders, lockfiles, large files, binary-like extensions, and secret/env-like files.
103
+ - Skipped entries recorded: ${skipped.length}
104
+ - External input validation candidates: ${findings.externalInput.length}
105
+ - Per-file scan warnings: ${findings.scanWarnings.length}
106
+
107
+ ## Risks
108
+ - Static analysis can produce false positives and cannot prove runtime behavior.
109
+ - This report redacts secret-like content and does not include raw sensitive values.
110
+ - Human review is needed before enabling strict mode or CI enforcement.
111
+ `;
112
+ }
113
+
114
+ export function renderRecommendedProfile({ stack, findings, thresholds }) {
115
+ const apiMode = stack.nextjs || findings.dbInRoutes.length || findings.routeResponsibility.length ? 'changed-files' : 'advisory';
116
+ const databaseMode = stack.postgresql || findings.rawSql.length ? 'advisory' : 'off';
117
+ const crawlerMode = stack.crawler ? 'advisory' : 'off';
118
+ return `version: 1
119
+ mode: advisory
120
+ recommendations:
121
+ generated_by: deep-scan
122
+ generated_at: "${nowIso()}"
123
+ apply_requires_user_approval: true
124
+ packs:
125
+ core:
126
+ mode: advisory
127
+ web:
128
+ mode: ${stack.react || stack.nextjs ? 'advisory' : 'off'}
129
+ api:
130
+ mode: ${apiMode}
131
+ database:
132
+ mode: ${databaseMode}
133
+ crawler:
134
+ mode: ${crawlerMode}
135
+ rules:
136
+ no_silent_failure:
137
+ mode: changed-files
138
+ no_secret_logging:
139
+ mode: changed-files
140
+ workflow_security:
141
+ mode: advisory
142
+ file_size_advisory:
143
+ mode: advisory
144
+ source_file_warning_lines: ${thresholds.fileSize.source_file_warning_lines}
145
+ source_file_review_lines: ${thresholds.fileSize.source_file_review_lines}
146
+ responsibility_budget:
147
+ mode: advisory
148
+ next_page_review_lines: ${thresholds.responsibility.next_page_review_lines}
149
+ client_module_review_lines: ${thresholds.responsibility.client_module_review_lines}
150
+ route_review_lines: ${thresholds.responsibility.route_review_lines}
151
+ import_ops_script_review_lines: ${thresholds.responsibility.import_ops_script_review_lines}
152
+ python_orchestrator_review_lines: ${thresholds.responsibility.python_orchestrator_review_lines}
153
+ single_responsibility_advisory:
154
+ mode: advisory
155
+ function_review_lines: ${thresholds.singleResponsibility.function_review_lines}
156
+ mixed_responsibility_hints: ${thresholds.singleResponsibility.mixed_responsibility_hints}
157
+ module_export_family_hints: ${thresholds.singleResponsibility.module_export_family_hints}
158
+ external_input_validation:
159
+ mode: advisory
160
+ null_state_safety:
161
+ mode: advisory
162
+ authz_data_isolation:
163
+ mode: advisory
164
+ build_runtime_env_safety:
165
+ mode: advisory
166
+ write_safety_idempotency:
167
+ mode: advisory
168
+ api_contract_compatibility:
169
+ mode: advisory
170
+ performance_duplicate_fetch:
171
+ mode: advisory
172
+ public_safe_error:
173
+ mode: advisory
174
+ sql_parameter_binding:
175
+ mode: ${databaseMode}
176
+ db_row_validation:
177
+ mode: ${databaseMode}
178
+ type_escape_advisory:
179
+ mode: advisory
180
+ thin_api_route:
181
+ mode: ${apiMode}
182
+ component_responsibility:
183
+ mode: ${stack.react || stack.nextjs ? 'advisory' : 'off'}
184
+ side_effect_boundary:
185
+ mode: advisory
186
+ broad_exception_advisory:
187
+ mode: ${stack.python ? 'advisory' : 'off'}
188
+ crawler_producer_boundary:
189
+ mode: ${crawlerMode}
190
+ baseline:
191
+ enabled: false
192
+ candidate_report: .jhste/deep-scan-report.md
193
+ strict:
194
+ enabled: false
195
+ requires_explicit_opt_in: true
196
+ `;
197
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import {
4
+ atomicWrite,
5
+ ensureDir,
6
+ findGitRoot,
7
+ parseArgs,
8
+ relativeDisplay,
9
+ } from './shared.mjs';
10
+ import {
11
+ fileSizeSettings,
12
+ loadProfileConfig,
13
+ responsibilityBudgetSettings,
14
+ singleResponsibilitySettings,
15
+ } from './profile.mjs';
16
+ import { collectFiles, detectInstructions, detectStack } from './deep-scan/collect.mjs';
17
+ import { scanFiles } from './deep-scan/analyze.mjs';
18
+ import { renderRecommendedProfile, renderReport } from './deep-scan/report.mjs';
19
+
20
+ async function main() {
21
+ const args = parseArgs(process.argv.slice(2));
22
+ const repoRoot = findGitRoot(args.repo || process.cwd());
23
+ const profileState = loadProfileConfig(repoRoot);
24
+ const thresholds = {
25
+ fileSize: fileSizeSettings(profileState.profile),
26
+ responsibility: responsibilityBudgetSettings(profileState.profile),
27
+ singleResponsibility: singleResponsibilitySettings(profileState.profile),
28
+ };
29
+ const { files, skipped, source } = collectFiles(repoRoot);
30
+ const stack = detectStack(repoRoot, files);
31
+ const instructions = detectInstructions(repoRoot);
32
+ const findings = scanFiles(files, thresholds);
33
+ const jhsteDir = path.join(repoRoot, '.jhste');
34
+ ensureDir(jhsteDir);
35
+ const reportPath = path.join(jhsteDir, 'deep-scan-report.md');
36
+ const recommendedPath = path.join(jhsteDir, 'profile.recommended.yaml');
37
+ atomicWrite(reportPath, renderReport({ repoRoot, files, skipped, source, stack, instructions, findings }));
38
+ atomicWrite(recommendedPath, renderRecommendedProfile({ stack, findings, thresholds }));
39
+
40
+ console.log('Deep scan completed. No code was modified.');
41
+ console.log(`- Detected stack: ${Object.entries(stack).filter(([, value]) => value).map(([key]) => key).join(', ') || 'none'}`);
42
+ console.log(`- Files inspected: ${files.length}`);
43
+ console.log(`- Files skipped: ${skipped.length}`);
44
+ console.log(`- File source: ${source.type}${source.fallback ? ` (fallback: ${source.fallback_reason || 'unknown reason'})` : ''}`);
45
+ console.log('- Recommendation: keep advisory as the default; apply changed-files candidates only after user approval');
46
+ console.log('\nOutput files:');
47
+ console.log(`- ${relativeDisplay(repoRoot, reportPath)}`);
48
+ console.log(`- ${relativeDisplay(repoRoot, recommendedPath)}`);
49
+ console.log('\nTo apply the recommended settings:');
50
+ console.log(' npx jhste-skills tune');
51
+ }
52
+
53
+ main().catch((error) => {
54
+ console.error(error instanceof Error ? error.message : String(error));
55
+ process.exit(1);
56
+ });