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,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJsonFile, validateJsonObject } from '../json-file.mjs';
4
+ import { nowIso, relativeDisplay } from '../shared.mjs';
5
+
6
+ export function loadBaseline(repoRoot, baselinePath, callbacks = {}) {
7
+ const failConfig = callbacks.failConfig || ((message) => { throw new Error(message); });
8
+ if (!fs.existsSync(baselinePath)) return new Map();
9
+ try {
10
+ const data = readJsonFile(baselinePath, {
11
+ description: `baseline ${relativeDisplay(repoRoot, baselinePath)}`,
12
+ validate: validateJsonObject,
13
+ });
14
+ const items = Array.isArray(data.violations) ? data.violations : [];
15
+ return new Map(items.filter((item) => typeof item.fingerprint === 'string').map((item) => [item.fingerprint, item]));
16
+ } catch (error) {
17
+ failConfig(`Failed to parse baseline ${relativeDisplay(repoRoot, baselinePath)}.`, [error instanceof Error ? error.message : String(error)]);
18
+ }
19
+ return new Map();
20
+ }
21
+
22
+ export function writeBaseline(baselinePath, violations, existingBaseline = new Map()) {
23
+ fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
24
+ const now = nowIso();
25
+ const rowsByFingerprint = new Map();
26
+ for (const item of violations) {
27
+ const existing = existingBaseline.get(item.fingerprint) || {};
28
+ rowsByFingerprint.set(item.fingerprint, {
29
+ fingerprint: item.fingerprint,
30
+ occurrence_key: item.occurrence_key,
31
+ rule_id: item.rule_id,
32
+ path: item.path,
33
+ severity: item.severity,
34
+ first_seen: existing.first_seen || now,
35
+ last_seen: now,
36
+ reason: existing.reason || 'remediation queue item; fix or explicitly keep tracking before enforcing',
37
+ owner: existing.owner || null,
38
+ expires_at: existing.expires_at || null,
39
+ fix_tracking: existing.fix_tracking || null,
40
+ });
41
+ }
42
+ const rows = [...rowsByFingerprint.values()];
43
+ fs.writeFileSync(baselinePath, `${JSON.stringify({ version: 1, created_at: now, updated_at: now, violations: rows }, null, 2)}\n`);
44
+ }
45
+
46
+ export function applyBaseline(violations, baselineMap, mode, callbacks = {}) {
47
+ const failConfig = callbacks.failConfig || ((message) => { throw new Error(message); });
48
+ if (!['off', 'use', 'update', 'ratchet'].includes(mode)) failConfig(`Unsupported --baseline ${mode}. Use off, use, update, or ratchet.`);
49
+ if (mode === 'off' || mode === 'update') return violations.map((item) => ({ ...item, baseline_status: 'unmanaged' }));
50
+ return violations.map((item) => {
51
+ const baseline = baselineMap.get(item.fingerprint);
52
+ if (!baseline) return { ...item, baseline_status: 'new' };
53
+ return {
54
+ ...item,
55
+ baseline_status: 'matched',
56
+ baseline_reason: baseline.reason || '',
57
+ baseline_first_seen: baseline.first_seen || null,
58
+ baseline_last_seen: baseline.last_seen || null,
59
+ baseline_owner: baseline.owner || null,
60
+ baseline_expires_at: baseline.expires_at || null,
61
+ baseline_fix_tracking: baseline.fix_tracking || null,
62
+ };
63
+ });
64
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_BASELINE_PATH } from '../profile.mjs';
4
+ import { relativeDisplay, resolveRepoContainedPath } from '../shared.mjs';
5
+
6
+ export function profileUsesMode(profile, mode) {
7
+ if (profile?.mode === mode) return true;
8
+ if (Object.values(profile?.packs || {}).some((config) => config?.mode === mode)) return true;
9
+ if (Object.values(profile?.rules || {}).some((config) => config?.mode === mode)) return true;
10
+ return false;
11
+ }
12
+
13
+ export function resolveGuardConfig(args, profileState, repoRoot, callbacks = {}) {
14
+ const failConfig = callbacks.failConfig || ((message) => { throw new Error(message); });
15
+ const inManagedHook = callbacks.inManagedHook || (() => false);
16
+ const format = String(args.format || profileState.profile.guard.default_format || 'text');
17
+ if (!['text', 'json'].includes(format)) failConfig('--format must be text or json.');
18
+
19
+ const strictProfile = profileUsesMode(profileState.profile, 'strict');
20
+ const baselineNewOnlyProfile = profileUsesMode(profileState.profile, 'baseline-new-only');
21
+ const failOn = String(args['fail-on'] || profileState.profile.guard.fail_on || (strictProfile ? 'error' : 'none'));
22
+ if (!['none', 'warning', 'error'].includes(failOn)) failConfig('--fail-on must be none, warning, or error.');
23
+ if (strictProfile && failOn === 'none') {
24
+ failConfig('Profile mode strict requires enforcement; set guard.fail_on to error/warning or choose a non-strict mode.');
25
+ }
26
+
27
+ const baselineMode = String(args.baseline || (baselineNewOnlyProfile ? 'ratchet' : (profileState.profile.baseline.enabled ? 'use' : 'off')));
28
+ if (!['off', 'use', 'update', 'ratchet'].includes(baselineMode)) {
29
+ failConfig(`Unsupported --baseline ${baselineMode}. Use off, use, update, or ratchet.`);
30
+ }
31
+ let baselinePath;
32
+ try {
33
+ baselinePath = resolveRepoContainedPath(repoRoot, String(args['baseline-path'] || profileState.profile.baseline.path || DEFAULT_BASELINE_PATH), { label: '--baseline-path' });
34
+ } catch (error) {
35
+ failConfig(error instanceof Error ? error.message : String(error));
36
+ }
37
+ if (inManagedHook() && baselineMode === 'update') {
38
+ failConfig('Managed hook execution is read-only; --baseline update is not allowed while JHSTE_HOOK_ACTIVE=1.');
39
+ }
40
+ if (baselineMode === 'ratchet' && !fs.existsSync(baselinePath)) {
41
+ failConfig(`--baseline ratchet requires an existing baseline at ${relativeDisplay(repoRoot, baselinePath)}.`);
42
+ }
43
+ const scopedArgs = {
44
+ ...args,
45
+ scope: args.scope || (strictProfile ? 'all' : (profileState.profile.guard.default_scope || 'changed')),
46
+ };
47
+ return { format, failOn, baselineMode, baselinePath, scopedArgs };
48
+ }
@@ -0,0 +1,87 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { violation } from './scanners/index.mjs';
3
+
4
+ const DEFAULT_COMMAND_TIMEOUT_MS = 120000;
5
+ const PROFILE_OUTPUT_LIMIT = 4000;
6
+
7
+ function redactSecretLike(text) {
8
+ return String(text || '')
9
+ .replace(/sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g, '[REDACTED_OPENAI_KEY]')
10
+ .replace(/gh[pousr]_[A-Za-z0-9_]{20,}/g, '[REDACTED_GITHUB_TOKEN]')
11
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi, 'Bearer [REDACTED_TOKEN]')
12
+ .replace(/\bAuthorization\s*:\s*(?:Bearer|Basic)\s+[^\r\n'"]+/gi, 'Authorization: [REDACTED_AUTHORIZATION]')
13
+ .replace(/\bCookie\s*:\s*[^\r\n]+/gi, 'Cookie: [REDACTED_COOKIE]')
14
+ .replace(/\b(password|secret|token|api[_-]?key|authorization|cookie|session)\s*[:=]\s*(['"]?)[^\s'"]{8,}\2/gi, '$1=[REDACTED_SECRET]');
15
+ }
16
+
17
+ function compactOutput(text) {
18
+ const value = redactSecretLike(text).trim();
19
+ if (value.length <= PROFILE_OUTPUT_LIMIT) return value;
20
+ return `${value.slice(0, PROFILE_OUTPUT_LIMIT)}\n... truncated ${value.length - PROFILE_OUTPUT_LIMIT} chars`;
21
+ }
22
+
23
+ function safeCommandRuleId(name) {
24
+ const slug = String(name || 'unnamed').toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
25
+ return `profile.command.${slug || 'unnamed'}`;
26
+ }
27
+
28
+ export function profileCommandExecutionErrors(commands, { trusted = false, allowShell = false } = {}) {
29
+ const errors = [];
30
+ if (!trusted && (commands || []).length > 0) {
31
+ errors.push('--run-profile-commands executes repo-local commands; pass --trust-repo-profile after reviewing .jhste/profile.yaml.');
32
+ }
33
+ for (const command of commands || []) {
34
+ if (command.run && !allowShell) {
35
+ errors.push(`Profile command ${command.name || 'unnamed'} uses legacy shell run; pass --allow-profile-shell to execute it.`);
36
+ }
37
+ }
38
+ return errors;
39
+ }
40
+
41
+ export function runProfileCommands(repoRoot, commands, { allowShell = false } = {}) {
42
+ const violations = [];
43
+ const failures = [];
44
+ for (const command of commands || []) {
45
+ const usingShell = Boolean(command.run);
46
+ if (usingShell && !allowShell) {
47
+ failures.push({
48
+ code: 'profile.command.config',
49
+ message: `Profile command shell execution is not allowed: ${command.name}`,
50
+ details: ['Pass --allow-profile-shell only after reviewing .jhste/profile.yaml.'],
51
+ });
52
+ continue;
53
+ }
54
+ const executable = usingShell ? command.run : command.cmd;
55
+ const commandArgs = usingShell ? [] : (command.args || []).map(String);
56
+ const result = spawnSync(executable, commandArgs, {
57
+ cwd: repoRoot,
58
+ shell: usingShell,
59
+ encoding: 'utf8',
60
+ timeout: command.timeoutSeconds ? command.timeoutSeconds * 1000 : DEFAULT_COMMAND_TIMEOUT_MS,
61
+ maxBuffer: 1024 * 1024,
62
+ });
63
+ if (result.error) {
64
+ failures.push({
65
+ code: 'profile.command.runtime',
66
+ message: `Profile command could not run: ${command.name}`,
67
+ details: [redactSecretLike(result.error.message), redactSecretLike(usingShell ? command.run : [command.cmd, ...commandArgs].join(' '))],
68
+ });
69
+ continue;
70
+ }
71
+ if (result.status !== 0) {
72
+ const output = compactOutput([result.stdout, result.stderr].filter(Boolean).join('\n'));
73
+ violations.push(violation({
74
+ ruleId: safeCommandRuleId(command.name),
75
+ severity: command.severity || 'error',
76
+ relPath: '.jhste/profile.yaml',
77
+ line: 1,
78
+ symbol: command.name,
79
+ message: `Profile command failed: ${command.name}`,
80
+ source: 'profile',
81
+ confidence: 'high',
82
+ }));
83
+ if (output) violations[violations.length - 1].details = [`exit=${result.status}`, output];
84
+ }
85
+ }
86
+ return { violations, failures };
87
+ }
@@ -0,0 +1,47 @@
1
+ export const FINDING_METADATA = {
2
+ 'silent.catch.empty': { family: 'no_silent_failure', pack: 'core', scanner: 'scanSilentFailures' },
3
+ 'silent.promise_catch.empty': { family: 'no_silent_failure', pack: 'core', scanner: 'scanSilentFailures' },
4
+ 'silent.python_except.pass': { family: 'no_silent_failure', pack: 'core', scanner: 'scanSilentFailures' },
5
+ 'silent.catch.fallback_no_reason': { family: 'no_silent_failure', pack: 'core', scanner: 'scanSilentFailures' },
6
+ 'secret.logging': { family: 'no_secret_logging', pack: 'core', scanner: 'scanSecretLogging' },
7
+ 'file_size.warning': { family: 'file_size_advisory', pack: 'core', scanner: 'scanFileSizeAdvisory' },
8
+ 'file_size.review': { family: 'file_size_advisory', pack: 'core', scanner: 'scanFileSizeAdvisory' },
9
+ 'boundary.import.server_in_client': { family: 'component_responsibility', pack: 'web', scanner: 'scanClientServerBoundary' },
10
+ 'workflow.input_interpolation.run': { family: 'workflow_security', pack: 'core', scanner: 'scanWorkflowSecurity' },
11
+ 'workflow.action.unpinned': { family: 'workflow_security', pack: 'core', scanner: 'scanWorkflowSecurity' },
12
+ 'input.file_parse_unvalidated': { family: 'external_input_validation', pack: 'core', scanner: 'scanExternalInputValidation' },
13
+ 'input.third_party_json_unvalidated': { family: 'external_input_validation', pack: 'core', scanner: 'scanExternalInputValidation' },
14
+ 'input.request_body_direct_use': { family: 'external_input_validation', pack: 'core', scanner: 'scanExternalInputValidation' },
15
+ 'input.env_export_unvalidated': { family: 'external_input_validation', pack: 'core', scanner: 'scanExternalInputValidation' },
16
+ 'responsibility.page.budget': { family: 'responsibility_budget', pack: 'core', scanner: 'scanResponsibilityBudget' },
17
+ 'responsibility.client.budget': { family: 'responsibility_budget', pack: 'core', scanner: 'scanResponsibilityBudget' },
18
+ 'responsibility.route.budget': { family: 'responsibility_budget', pack: 'core', scanner: 'scanResponsibilityBudget' },
19
+ 'responsibility.script.budget': { family: 'responsibility_budget', pack: 'core', scanner: 'scanResponsibilityBudget' },
20
+ 'responsibility.python_orchestrator.budget': { family: 'responsibility_budget', pack: 'core', scanner: 'scanResponsibilityBudget' },
21
+ 'srp.function.length': { family: 'single_responsibility_advisory', pack: 'core', scanner: 'scanSingleResponsibility' },
22
+ 'srp.function.mixed_responsibility': { family: 'single_responsibility_advisory', pack: 'core', scanner: 'scanSingleResponsibility' },
23
+ 'srp.module.mixed_exports': { family: 'single_responsibility_advisory', pack: 'core', scanner: 'scanSingleResponsibility' },
24
+ 'state.non_null_assertion': { family: 'null_state_safety', pack: 'core', scanner: 'scanStateSafety' },
25
+ 'state.async_ui_missing_fallback': { family: 'null_state_safety', pack: 'core', scanner: 'scanStateSafety' },
26
+ 'authz.scope_not_visible': { family: 'authz_data_isolation', pack: 'core', scanner: 'scanAuthzDataIsolation' },
27
+ 'authz.read_scope_not_visible': { family: 'authz_data_isolation', pack: 'core', scanner: 'scanAuthzDataIsolation' },
28
+ 'authz.mutation_without_auth_context': { family: 'authz_data_isolation', pack: 'core', scanner: 'scanAuthzDataIsolation' },
29
+ 'authz.read_without_auth_context': { family: 'authz_data_isolation', pack: 'core', scanner: 'scanAuthzDataIsolation' },
30
+ 'runtime.env_direct_access': { family: 'build_runtime_env_safety', pack: 'core', scanner: 'scanRuntimeEnvSafety' },
31
+ 'runtime.import_meta_env_direct_access': { family: 'build_runtime_env_safety', pack: 'core', scanner: 'scanRuntimeEnvSafety' },
32
+ 'runtime.getenv_direct_access': { family: 'build_runtime_env_safety', pack: 'core', scanner: 'scanRuntimeEnvSafety' },
33
+ 'write.loop_without_transaction': { family: 'write_safety_idempotency', pack: 'core', scanner: 'scanWriteSafety' },
34
+ 'write.mutation_retry_safety': { family: 'write_safety_idempotency', pack: 'core', scanner: 'scanWriteSafety' },
35
+ 'contract.boundary_without_schema': { family: 'api_contract_compatibility', pack: 'core', scanner: 'scanApiContractCompatibility' },
36
+ 'contract.raw_storage_response': { family: 'api_contract_compatibility', pack: 'core', scanner: 'scanApiContractCompatibility' },
37
+ 'performance.multiple_fetch_sources': { family: 'performance_duplicate_fetch', pack: 'core', scanner: 'scanPerformanceDuplicateFetch' },
38
+ 'performance.fetch_in_effect': { family: 'performance_duplicate_fetch', pack: 'core', scanner: 'scanPerformanceDuplicateFetch' },
39
+ 'sql.raw_interpolation': { family: 'sql_parameter_binding', pack: 'database', scanner: 'scanSqlParameterBinding' },
40
+ 'error.public_raw_details': { family: 'public_safe_error', pack: 'api', scanner: 'scanPublicSafeError' },
41
+ 'database.raw_row_public_response': { family: 'db_row_validation', pack: 'database', scanner: 'scanDbRowValidation' },
42
+ 'route.direct_db_access': { family: 'thin_api_route', pack: 'api', scanner: 'scanThinApiRoute' },
43
+ 'type.escape': { family: 'type_escape_advisory', pack: 'web', scanner: 'scanTypeEscapeAdvisory' },
44
+ 'side_effect.hidden_in_helper': { family: 'side_effect_boundary', pack: 'core', scanner: 'scanSideEffectBoundary' },
45
+ 'crawler.producer_direct_persistence': { family: 'crawler_producer_boundary', pack: 'crawler', scanner: 'scanCrawlerProducerBoundary' },
46
+ 'python.broad_exception': { family: 'broad_exception_advisory', pack: 'core', scanner: 'scanBroadExceptionAdvisory' },
47
+ };
@@ -0,0 +1,165 @@
1
+ import { nowIso } from '../shared.mjs';
2
+ import { FINDING_METADATA } from './registry.mjs';
3
+
4
+ export const EXIT_VIOLATION = 1;
5
+ export const EXIT_GUARD_FAILURE = 2;
6
+ export const SEVERITIES = ['info', 'warning', 'error'];
7
+
8
+ export function summarize(violations, failures = []) {
9
+ const active = violations.filter((item) => item.baseline_status !== 'matched');
10
+ const baselineMatched = violations.length - active.length;
11
+ const summary = {
12
+ error: 0,
13
+ warning: 0,
14
+ info: 0,
15
+ baseline_matched: baselineMatched,
16
+ // Backward-compatible alias for schema_version: 1 consumers. Prefer baseline_matched in new integrations.
17
+ suppressed: baselineMatched,
18
+ failures: failures.length,
19
+ };
20
+ for (const item of active) summary[item.severity] = (summary[item.severity] || 0) + 1;
21
+ return summary;
22
+ }
23
+
24
+ export function guardResult(violations, failures = [], meta = {}) {
25
+ return {
26
+ schema_version: 1,
27
+ generated_at: nowIso(),
28
+ summary: summarize(violations, failures),
29
+ meta,
30
+ violations,
31
+ failures,
32
+ };
33
+ }
34
+
35
+ export function severityMeets(severity, threshold) {
36
+ if (threshold === 'none') return false;
37
+ const severityIndex = SEVERITIES.indexOf(severity);
38
+ const thresholdIndex = SEVERITIES.indexOf(threshold);
39
+ return severityIndex >= thresholdIndex;
40
+ }
41
+
42
+ export function exitCodeFor(result, failOn, baselineMode) {
43
+ if (result.failures.length > 0) return EXIT_GUARD_FAILURE;
44
+ if (baselineMode === 'update') return 0;
45
+ const active = result.violations.filter((item) => item.baseline_status !== 'matched');
46
+ if (baselineMode === 'ratchet' && active.length > 0) return EXIT_VIOLATION;
47
+ if (active.some((item) => severityMeets(item.severity, failOn))) return EXIT_VIOLATION;
48
+ return 0;
49
+ }
50
+
51
+ function guidanceForFinding(item) {
52
+ const family = item.rule_family || FINDING_METADATA[item.rule_id]?.family || item.rule_id;
53
+ if (family === 'file_size_advisory') {
54
+ return {
55
+ means: 'This file exceeds the configured line limit. It is not proof of a bug, but it increases the chance that review, conflict resolution, and test boundaries become harder to manage.',
56
+ next: 'Split the file along natural boundaries so each helper, adapter, scanner family, or test fixture carries one main responsibility at a time.',
57
+ };
58
+ }
59
+ if (family === 'responsibility_budget') {
60
+ return {
61
+ means: 'This file may be combining multiple responsibilities such as UI, state, IO, validation, persistence, and response shaping.',
62
+ next: 'Check whether the code can move into smaller modules with explicit names such as loader, service, repository, or view.',
63
+ };
64
+ }
65
+
66
+ if (family === 'single_responsibility_advisory') {
67
+ return {
68
+ means: 'This class, module, or function may have more than one reason to change. The finding is heuristic and should be reviewed, not treated as proof.',
69
+ next: 'Name the one main responsibility, then move only independently changing work behind a real seam; avoid extracting shallow pass-through wrappers.',
70
+ };
71
+ }
72
+ if (family === 'external_input_validation') {
73
+ return {
74
+ means: 'This is a candidate path where external input such as files, request bodies, third-party API data, or env values may be trusted without shape validation.',
75
+ next: 'Add an explicit validation boundary such as schema.safeParse, a validator, or assert/parseEnv, or make an existing validation step clearly visible in code.',
76
+ };
77
+ }
78
+ if (family === 'no_secret_logging') {
79
+ return {
80
+ means: 'This is a candidate path where logs may include sensitive fields such as tokens, passwords, or session values.',
81
+ next: 'Confirm that real secrets are not emitted, and redact values before logging when needed.',
82
+ };
83
+ }
84
+ if (family === 'build_runtime_env_safety') {
85
+ return {
86
+ means: 'Direct env reads can fail differently across build and runtime environments, producing undefined values or invalid configuration.',
87
+ next: 'Use startup-time env schema validation, defaults, or a requiredEnv helper so failure points are explicit.',
88
+ };
89
+ }
90
+ if (family === 'write_safety_idempotency') {
91
+ return {
92
+ means: 'This is a candidate path where repeated or retried writes could create duplicate records or partial success states.',
93
+ next: 'Check whether transactions, upserts, idempotency keys, dedupe logic, or batch semantics are the right safety mechanism here.',
94
+ };
95
+ }
96
+ if (family === 'api_contract_compatibility') {
97
+ return {
98
+ means: 'The request or response shape may be unclear at the API boundary, or storage-layer shapes may be leaking directly to callers.',
99
+ next: 'Add an input schema and public DTO mapping so the caller-facing contract stays explicit and stable.',
100
+ };
101
+ }
102
+ if (family === 'authz_data_isolation') {
103
+ return {
104
+ means: 'Authn/authz checks or tenant-owner scoping may be unclear in code, creating a candidate risk of cross-user data access.',
105
+ next: 'Make requireUser/permission checks and owner filters such as userId, orgId, or tenantId visible in the same flow.',
106
+ };
107
+ }
108
+ if (family === 'performance_duplicate_fetch') {
109
+ return {
110
+ means: 'Duplicate fetches or missing cache use in the same path may cause slow rendering or unnecessary network calls.',
111
+ next: 'Check whether these calls can be consolidated behind a shared loader, query hook, or cache key.',
112
+ };
113
+ }
114
+ if (item.confidence === 'low') {
115
+ return {
116
+ means: 'This is a low-confidence static heuristic candidate. It does not mean the code is definitely buggy.',
117
+ next: 'If the code is safe, make the validation or safeguard more visible. If the risk is real, add a small boundary fix.',
118
+ };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ export function printResult(result, format) {
124
+ if (format === 'json') {
125
+ console.log(JSON.stringify(result, null, 2));
126
+ return;
127
+ }
128
+ const { summary } = result;
129
+ console.log(`jhste guard: errors=${summary.error} warnings=${summary.warning} info=${summary.info} baseline-matched=${summary.baseline_matched} failures=${summary.failures}`);
130
+ if (result.meta?.git?.file_source) {
131
+ const fallback = result.meta.git.file_source === 'filesystem-fallback' ? ` (fallback: ${result.meta.git.fallback_reason || 'unknown reason'})` : '';
132
+ console.log(`File collection: ${result.meta.git.file_source}${fallback}`);
133
+ }
134
+ if (result.failures.length) {
135
+ console.log('\nGuard failures:');
136
+ for (const failure of result.failures) console.log(`- [${failure.code}] ${failure.message}${failure.details?.length ? ` (${failure.details.join('; ')})` : ''}`);
137
+ }
138
+ const active = result.violations.filter((item) => item.baseline_status !== 'matched');
139
+ const visible = active.slice(0, 80);
140
+ if (visible.length) {
141
+ console.log('\nViolations:');
142
+ for (const item of visible) {
143
+ const confidence = item.confidence ? ` [${item.confidence}-confidence]` : '';
144
+ const family = item.rule_family && item.rule_family !== item.rule_id ? ` (${item.rule_family})` : '';
145
+ const related = item.related_key ? ` [related: ${item.related_key}]` : '';
146
+ console.log(`- [${item.severity}]${confidence} ${item.rule_id}${family} ${item.path}:${item.line}${related} — ${item.message}`);
147
+ const guidance = guidanceForFinding(item);
148
+ if (guidance) {
149
+ console.log(` Meaning: ${guidance.means}`);
150
+ console.log(` Next: ${guidance.next}`);
151
+ }
152
+ }
153
+ if (active.length > visible.length) console.log(`- ... ${active.length - visible.length} more omitted from text output`);
154
+ }
155
+ const matched = result.violations.filter((item) => item.baseline_status === 'matched');
156
+ if (matched.length) {
157
+ console.log('\nExisting known issues encountered from baseline (remediation queue; not a pass):');
158
+ for (const item of matched.slice(0, 40)) {
159
+ const family = item.rule_family && item.rule_family !== item.rule_id ? ` (${item.rule_family})` : '';
160
+ const reason = item.baseline_reason ? ` — ${item.baseline_reason}` : '';
161
+ console.log(`- [${item.severity}] ${item.rule_id}${family} ${item.path}:${item.line}${reason}`);
162
+ }
163
+ if (matched.length > 40) console.log(`- ... ${matched.length - 40} more baseline issues encountered in this scan scope`);
164
+ }
165
+ }
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import {
3
+ hasUseClientDirective,
4
+ isSourceCodePath,
5
+ lineAt,
6
+ violation,
7
+ } from './utils.mjs';
8
+
9
+ export function scanSilentFailures(relPath, text) {
10
+ if (!isSourceCodePath(relPath)) return [];
11
+ const out = [];
12
+ for (const match of text.matchAll(/\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/gsu)) {
13
+ const before = text.slice(Math.max(0, (match.index || 0) - 40), match.index || 0);
14
+ if (/['"`]\s*$/.test(before)) continue;
15
+ if (/matchAll\s*\(\s*\/|RegExp\s*\(/.test(before)) continue;
16
+ out.push(violation({
17
+ ruleId: 'silent.catch.empty',
18
+ severity: 'error',
19
+ relPath,
20
+ line: lineAt(text, match.index || 0),
21
+ symbol: 'catch{}',
22
+ message: 'Empty catch block hides failures; return a failure, log a redacted reason, or document an intentional fallback.',
23
+ confidence: 'high',
24
+ }));
25
+ }
26
+ for (const match of text.matchAll(/\.catch\s*\(\s*(?:async\s*)?\(?\s*[^)]*\)?\s*=>\s*\{\s*\}\s*\)/gsu)) {
27
+ out.push(violation({
28
+ ruleId: 'silent.promise_catch.empty',
29
+ severity: 'error',
30
+ relPath,
31
+ line: lineAt(text, match.index || 0),
32
+ symbol: '.catch-empty',
33
+ message: 'Empty promise rejection handler hides failures; return a failure, log a redacted reason, or document an intentional fallback.',
34
+ confidence: 'high',
35
+ }));
36
+ }
37
+ for (const match of text.matchAll(/(?:^|\n)\s*except\s+(?:Exception|BaseException)?\s*:\s*pass\b/gu)) {
38
+ out.push(violation({
39
+ ruleId: 'silent.python_except.pass',
40
+ severity: 'error',
41
+ relPath,
42
+ line: lineAt(text, match.index || 0),
43
+ symbol: 'except-pass',
44
+ message: 'Broad Python exception handler with pass hides failures; record a redacted reason or return an explicit fallback result.',
45
+ confidence: 'high',
46
+ }));
47
+ }
48
+ for (const match of text.matchAll(/\bcatch\s*(?:\([^)]*\))?\s*\{(?:(?!\}).){0,180}\breturn\s+(?:\[\]|null|undefined|false)\s*;?\s*\}/gsu)) {
49
+ const block = match[0];
50
+ if (/console\.|logger\.|reason|fallback|best[- ]?effort|optional|allowlist/i.test(block)) continue;
51
+ out.push(violation({
52
+ ruleId: 'silent.catch.fallback_no_reason',
53
+ severity: 'warning',
54
+ relPath,
55
+ line: lineAt(text, match.index || 0),
56
+ symbol: 'catch-fallback',
57
+ message: 'Exception is converted to an empty/null fallback without log, metric, reason, or best-effort marker.',
58
+ confidence: 'medium',
59
+ }));
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export function scanWorkflowSecurity(relPath, text) {
65
+ if (!/^\.github\/workflows\/.+\.ya?ml$/u.test(relPath)) return [];
66
+ const out = [];
67
+ const lines = text.split(/\r?\n/);
68
+ lines.forEach((line, index) => {
69
+ if (/\brun\s*:/.test(line) && /\$\{\{\s*(github\.event\.inputs|inputs)\./.test(line)) {
70
+ out.push(violation({
71
+ ruleId: 'workflow.input_interpolation.run',
72
+ severity: 'error',
73
+ relPath,
74
+ line: index + 1,
75
+ symbol: 'workflow-input-run',
76
+ message: 'Workflow run command directly interpolates dispatch input; pass input through env and validate it before shell use.',
77
+ confidence: 'high',
78
+ }));
79
+ }
80
+ if (/\buses\s*:\s*[^\s#]+\/[^\s#]+@(?:v\d+|main|master)\b/.test(line)) {
81
+ out.push(violation({
82
+ ruleId: 'workflow.action.unpinned',
83
+ severity: 'warning',
84
+ relPath,
85
+ line: index + 1,
86
+ symbol: line.trim(),
87
+ message: 'External workflow action is not pinned to a full commit SHA; consider pinning for supply-chain safety.',
88
+ confidence: 'medium',
89
+ }));
90
+ }
91
+ });
92
+ return out;
93
+ }
94
+
95
+ export function scanFileSizeAdvisory(relPath, text, settings) {
96
+ const ext = path.extname(relPath).toLowerCase();
97
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py'].includes(ext)) return [];
98
+ const lineCount = text.split(/\r?\n/).length;
99
+ if (lineCount >= settings.source_file_review_lines) {
100
+ return [violation({
101
+ ruleId: 'file_size.review',
102
+ severity: 'warning',
103
+ relPath,
104
+ symbol: 'source-file-review',
105
+ message: `${lineCount} lines in source file; limit is ${settings.source_file_review_lines}. Large files are harder to review, test, and safely change.`,
106
+ confidence: 'medium',
107
+ })];
108
+ }
109
+ if (lineCount >= settings.source_file_warning_lines) {
110
+ return [violation({
111
+ ruleId: 'file_size.warning',
112
+ severity: 'info',
113
+ relPath,
114
+ symbol: 'source-file-warning',
115
+ message: `${lineCount} lines in source file; warning threshold is ${settings.source_file_warning_lines}. Watch for responsibility creep.`,
116
+ confidence: 'medium',
117
+ })];
118
+ }
119
+ return [];
120
+ }
121
+
122
+ export function scanResponsibilityBudget(relPath, text, settings) {
123
+ const ext = path.extname(relPath).toLowerCase();
124
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py'].includes(ext)) return [];
125
+ const lineCount = text.split(/\r?\n/).length;
126
+ const out = [];
127
+ const nextPage = relPath.endsWith('/page.tsx') && /(^|\/)(app|src\/app|apps\/[^/]+\/src\/app)\//.test(relPath);
128
+ if (nextPage && lineCount > settings.next_page_review_lines) {
129
+ out.push(violation({ ruleId: 'responsibility.page.budget', severity: 'warning', relPath, symbol: 'next-page', message: `${lineCount} lines in Next page; review loader/model/view split.`, confidence: 'medium' }));
130
+ }
131
+ if (hasUseClientDirective(text) && lineCount > settings.client_module_review_lines) {
132
+ out.push(violation({ ruleId: 'responsibility.client.budget', severity: 'warning', relPath, symbol: 'use-client', message: `${lineCount} lines in client module; review hook/adapter/presentation split.`, confidence: 'medium' }));
133
+ }
134
+ const routeLike = /(^|\/)(api|routes?|controllers?|pages\/api)\//i.test(relPath) || /route\.(ts|js)$/.test(relPath);
135
+ if (routeLike && lineCount >= settings.route_review_lines) {
136
+ out.push(violation({ ruleId: 'responsibility.route.budget', severity: 'warning', relPath, symbol: 'route', message: `${lineCount} lines in route/controller-like file; review auth/validation/service/response seams.`, confidence: 'medium' }));
137
+ }
138
+ const scriptPipeline = /(^|\/)scripts\/(data|ops|import|imports|backfill|repair|migrate|migration)\//.test(relPath);
139
+ if (scriptPipeline && lineCount >= settings.import_ops_script_review_lines) {
140
+ out.push(violation({ ruleId: 'responsibility.script.budget', severity: 'warning', relPath, symbol: 'script-pipeline', message: `${lineCount} lines in import/ops-style script; review CLI/loader/transform/persist/report seams.`, confidence: 'medium' }));
141
+ }
142
+ if (ext === '.py' && /(^|\/)(main|.*orchestrator|.*runner|stage_runner)\.py$/.test(relPath) && lineCount >= settings.python_orchestrator_review_lines) {
143
+ out.push(violation({ ruleId: 'responsibility.python_orchestrator.budget', severity: 'warning', relPath, symbol: 'python-orchestrator', message: `${lineCount} lines in Python orchestrator/runner; review policy/IO/runtime/notification/result seams.`, confidence: 'medium' }));
144
+ }
145
+ return out;
146
+ }
147
+
148
+ export function scanSecretLogging(relPath, text) {
149
+ if (!isSourceCodePath(relPath)) return [];
150
+ const out = [];
151
+ const lines = text.split(/\r?\n/);
152
+ lines.forEach((line, index) => {
153
+ if (/\b(console\.(log|info|warn|error|debug)|logger\.(info|warn|error|debug)|print)\s*\(/.test(line)
154
+ && /\b(secret|token|password|authorization|cookie|session|api[_-]?key)\b/i.test(line)) {
155
+ const withoutStrings = line.replace(/(['"`])(?:\\.|(?!\1).)*\1/gu, '');
156
+ const hasSecretIdentifier = /\b[A-Za-z_$][\w$]*(secret|token|password|authorization|cookie|session|apiKey|api_key)[\w$]*\b/i.test(withoutStrings)
157
+ || /\b(secret|token|password|authorization|cookie|session|api[_-]?key)[A-Za-z_$][\w$]*\b/i.test(withoutStrings);
158
+ const stringOnly = !hasSecretIdentifier;
159
+ out.push(violation({
160
+ ruleId: 'secret.logging',
161
+ severity: stringOnly ? 'warning' : 'error',
162
+ relPath,
163
+ line: index + 1,
164
+ symbol: 'secret-like-log',
165
+ message: stringOnly
166
+ ? 'Log message contains secret-like wording; confirm it does not include secret values.'
167
+ : 'Log statement references secret-like data; log a stable request id or redacted reason code instead.',
168
+ confidence: stringOnly ? 'low' : 'high',
169
+ }));
170
+ }
171
+ });
172
+ return out;
173
+ }
174
+
175
+ export function scanTypeEscapeAdvisory(relPath, text) {
176
+ if (!/\.(tsx?|jsx?)$/u.test(relPath)) return [];
177
+ const out = [];
178
+ const lines = text.split(/\r?\n/);
179
+ lines.forEach((line, index) => {
180
+ if (/\bas\s+any\b|:\s*any\b|@ts-ignore/.test(line)) {
181
+ out.push(violation({
182
+ ruleId: 'type.escape',
183
+ severity: 'warning',
184
+ relPath,
185
+ line: index + 1,
186
+ symbol: 'type-escape',
187
+ message: 'Broad TypeScript escape detected; localize it or add a boundary parser/type guard where data enters.',
188
+ confidence: 'medium',
189
+ }));
190
+ }
191
+ });
192
+ return out;
193
+ }
194
+
195
+ export function scanBroadExceptionAdvisory(relPath, text) {
196
+ if (!relPath.endsWith('.py')) return [];
197
+ const out = [];
198
+ const lines = text.split(/\r?\n/);
199
+ lines.forEach((line, index) => {
200
+ if (/^\s*except\s*(?:Exception|BaseException)?\s*(?:as\s+\w+)?\s*:/.test(line) && !/\bpass\b/.test(line)) {
201
+ out.push(violation({
202
+ ruleId: 'python.broad_exception',
203
+ severity: 'warning',
204
+ relPath,
205
+ line: index + 1,
206
+ symbol: 'broad-exception',
207
+ message: 'Broad Python exception handler detected; prefer specific exceptions or record a clear fallback reason.',
208
+ confidence: 'low',
209
+ }));
210
+ }
211
+ });
212
+ return out;
213
+ }