@stupify/cli 0.0.3 → 0.0.5

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 (59) hide show
  1. package/README.md +26 -31
  2. package/dist/analysis.d.ts +11 -9
  3. package/dist/analysis.js +30 -173
  4. package/dist/checks.d.ts +1 -0
  5. package/dist/checks.js +89 -2
  6. package/dist/command.js +55 -91
  7. package/dist/constants.d.ts +1 -1
  8. package/dist/constants.js +1 -1
  9. package/dist/counter-scout.js +70 -8
  10. package/dist/doctor.d.ts +4 -0
  11. package/dist/doctor.js +131 -0
  12. package/dist/git.d.ts +4 -1
  13. package/dist/git.js +34 -0
  14. package/dist/hooks.d.ts +3 -0
  15. package/dist/hooks.js +117 -0
  16. package/dist/model.d.ts +1 -15
  17. package/dist/model.js +37 -21
  18. package/dist/prompts.d.ts +8 -5
  19. package/dist/prompts.js +58 -168
  20. package/dist/render.d.ts +2 -2
  21. package/dist/render.js +70 -78
  22. package/dist/repomix-provider.d.ts +10 -2
  23. package/dist/repomix-provider.js +62 -11
  24. package/dist/search-bench.d.ts +1 -0
  25. package/dist/search-bench.js +675 -0
  26. package/dist/search-profile.d.ts +6 -0
  27. package/dist/search-profile.js +73 -0
  28. package/dist/sem-provider.d.ts +2 -2
  29. package/dist/sem-provider.js +33 -7
  30. package/dist/stupify.d.ts +2 -0
  31. package/dist/stupify.js +185 -334
  32. package/dist/types.d.ts +193 -109
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -268
  35. package/src/checks.ts +91 -2
  36. package/src/command.ts +62 -107
  37. package/src/constants.ts +1 -1
  38. package/src/counter-scout.ts +63 -7
  39. package/src/doctor.ts +140 -0
  40. package/src/git.ts +35 -1
  41. package/src/hooks.ts +134 -0
  42. package/src/model.ts +39 -26
  43. package/src/prompts.ts +66 -202
  44. package/src/render.ts +68 -79
  45. package/src/repomix-provider.ts +66 -10
  46. package/src/search-bench.ts +783 -0
  47. package/src/search-profile.ts +89 -0
  48. package/src/sem-provider.ts +36 -9
  49. package/src/stupify.ts +215 -527
  50. package/src/types.ts +195 -119
  51. package/dist/batcher.d.ts +0 -3
  52. package/dist/batcher.js +0 -142
  53. package/dist/candidate-context.d.ts +0 -2
  54. package/dist/candidate-context.js +0 -40
  55. package/dist/experiment.d.ts +0 -1
  56. package/dist/experiment.js +0 -225
  57. package/src/batcher.ts +0 -198
  58. package/src/candidate-context.ts +0 -43
  59. package/src/experiment.ts +0 -317
package/README.md CHANGED
@@ -2,59 +2,54 @@
2
2
 
3
3
  Local-only diagnostic CLI for checking whether AI is making you dumber.
4
4
 
5
- This iteration analyzes a recent net diff locally. The default engine uses
6
- line-sized diff batches. The sem engine uses entity-level changes, scouts
7
- candidate entity IDs, optionally packs selected candidate files with Repomix,
8
- and prints findings.
5
+ Stupify has one analysis path:
9
6
 
10
- ```sh
11
- npx @stupify/cli --commit HEAD
7
+ ```text
8
+ sem diff -> counter scout -> Repomix context -> local search model
12
9
  ```
13
10
 
11
+ It emits search `matches`, not audit findings.
12
+
14
13
  ```sh
15
- npx @stupify/cli --engine sem --commit HEAD
14
+ npx @stupify/cli --staged
15
+ npx @stupify/cli --since "2 weeks ago"
16
+ npx @stupify/cli --commit HEAD
17
+ npx @stupify/cli --commits 20
18
+ git diff HEAD~1..HEAD | npx @stupify/cli --stdin
16
19
  ```
17
20
 
18
- The sem engine splits findings-audit batches before local inference when the
19
- prompt exceeds `--max-audit-input-tokens`. Use `--audit-concurrency` to tune
20
- parallel local audit calls. The default sem scout uses fast deterministic
21
- signal counters; use `--scout llm` to compare against the older local model
22
- scout. Use `--audit-context none|repomix` and `--audit-prompt strict|high_bar`
23
- to run audit ablations without changing code.
21
+ Install the warn-only pre-commit hook:
24
22
 
25
23
  ```sh
26
- stupify experiment experiments/bevyl-last-week.json
27
- stupify experiment experiments/bevyl-per-check.json
24
+ stupify hook install
28
25
  ```
29
26
 
30
- The experiment runner shells out to the CLI and writes local JSON plus
31
- manual-label markdown files under `experiments/results/`.
27
+ The hook runs `stupify --staged` and exits 0.
32
28
 
33
- By default, `stupify` is equivalent to `stupify --since "2 weeks ago"`.
34
- Commit mode analyzes `<commit>^..commit` as a net diff.
35
- The default registry currently runs nine concise checks for duplicated schemas,
36
- unnecessary complexity, fake precision, noisy metadata, mega-files,
37
- over-commenting, lint bypasses, inconsistent patterns, and reinvented
38
- utilities. `operator_style_mismatch` remains available with `--checks`, but is
39
- not enabled by default in the sem audit path.
29
+ Check local setup:
40
30
 
41
31
  ```sh
42
- npx @stupify/cli --commits 20
32
+ stupify doctor
43
33
  ```
44
34
 
45
- Recent-commits mode analyzes the selected range as one change. Findings are
46
- range-level for now, not per-commit blame.
35
+ Default search enables the checks that currently pass the local hook-safety
36
+ bench: `duplicated_schema`, `unnecessary_complexity`, `over_commenting`,
37
+ `lint_bypass`, and `reinvented_utils`. Other registry patterns can be opted in
38
+ with `--checks`.
47
39
 
48
40
  ```sh
49
- git diff HEAD~1..HEAD | npx @stupify/cli --stdin
41
+ stupify --staged --checks over_commenting
50
42
  ```
51
43
 
44
+ Large search inputs are skipped rather than truncated:
45
+
52
46
  ```sh
53
- stupify --help
47
+ stupify --staged --max-search-input-tokens 24000
54
48
  ```
55
49
 
56
50
  The package is prepared for the public `@stupify` npm scope. Publishing should
57
51
  run the TypeScript build first so the executable points at `dist/stupify.js`.
58
52
 
59
- This iteration intentionally does not compare baselines, share data, call hosted
60
- LLM APIs, integrate with Ollama, or scan the whole repo.
53
+ This iteration intentionally does not run findings audit, validators, judges,
54
+ baselines, hosted LLM APIs, GitHub integration, dashboards, or repo-wide
55
+ crawling.
@@ -1,14 +1,16 @@
1
1
  import type { LocalModel } from "./model.ts";
2
- import type { CandidateContext, DiffBatch, AuditPromptName, AuditReviewResult, FindingsResult, NetDiff, SemCandidate, SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
3
- export declare function scoutBatch(model: LocalModel, batch: DiffBatch, checks: readonly StupifyCheck[], sourceLabel: string): Promise<readonly string[]>;
4
- export declare function auditCandidates(model: LocalModel, diff: NetDiff, contexts: readonly CandidateContext[], checks: readonly StupifyCheck[]): Promise<FindingsResult>;
5
- export declare function scoutSemChanges(model: LocalModel, changeSet: SemChangeSet, checks: readonly StupifyCheck[], maxCandidates: number): Promise<readonly SemCandidate[]>;
6
- export declare function runFindingsAudit(model: LocalModel, changeSet: SemChangeSet, contexts: readonly SemContext[], pack: SemContextPack, checks: readonly StupifyCheck[], request?: Readonly<{
7
- prompt: string;
8
- schema: unknown;
9
- }>): Promise<AuditReviewResult>;
10
- export declare function findingsAuditRequest(changeSet: SemChangeSet, contexts: readonly SemContext[], pack: SemContextPack, checks: readonly StupifyCheck[], promptName?: AuditPromptName): Readonly<{
2
+ import type { SearchMatch, SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
3
+ export declare function runSearch(model: LocalModel, request: SearchRequest): Promise<readonly SearchMatch[]>;
4
+ export type SearchRequest = Readonly<{
11
5
  prompt: string;
12
6
  schema: unknown;
7
+ contexts: readonly SemContext[];
13
8
  }>;
9
+ export declare function searchRequest(input: Readonly<{
10
+ changeSet: SemChangeSet;
11
+ contexts: readonly SemContext[];
12
+ pack: SemContextPack;
13
+ patterns: readonly StupifyCheck[];
14
+ includeCounterReasonInPrompt?: boolean;
15
+ }>): SearchRequest;
14
16
  export declare function countPromptTokens(model: LocalModel, prompt: string): Promise<number>;
package/dist/analysis.js CHANGED
@@ -1,34 +1,17 @@
1
- import { auditPrompt, findingsAuditPrompt, scoutPrompt, semScoutPrompt } from "./prompts.js";
2
1
  import { cachedJson, fingerprint } from "./cache.js";
3
- export async function scoutBatch(model, batch, checks, sourceLabel) {
4
- const raw = await runJsonPrompt(model, scoutPrompt(batch, checks, sourceLabel), scoutSchema(batch), 0);
5
- return uncheckedCandidates(raw);
6
- }
7
- export async function auditCandidates(model, diff, contexts, checks) {
8
- if (contexts.length === 0)
9
- return { findings: [], summary: "No candidate regions found." };
10
- const raw = await runJsonPrompt(model, auditPrompt(contexts, checks, diff.label), auditSchema(contexts), 0);
11
- return uncheckedRawAuditResult(raw, diff.id);
12
- }
13
- export async function scoutSemChanges(model, changeSet, checks, maxCandidates) {
14
- const raw = await runJsonPrompt(model, semScoutPrompt(changeSet, checks, maxCandidates), semScoutSchema(changeSet, checks, maxCandidates), 0);
15
- return uncheckedSemCandidates(raw, changeSet.id);
16
- }
17
- export async function runFindingsAudit(model, changeSet, contexts, pack, checks, request = findingsAuditRequest(changeSet, contexts, pack, checks)) {
18
- if (contexts.length === 0) {
19
- return {
20
- findings: [],
21
- summary: "No candidate entities found.",
22
- stats: { totalTargets: 0, finding: 0, clean: 0, uncertain: 0, invalid: 0 },
23
- };
24
- }
2
+ import { searchPrompt } from "./prompts.js";
3
+ export async function runSearch(model, request) {
25
4
  const raw = await runJsonPrompt(model, request.prompt, request.schema, 0);
26
- return uncheckedFindingsAuditResult(raw, changeSet.id, contexts);
5
+ return uncheckedSearchMatches(raw, request.contexts);
27
6
  }
28
- export function findingsAuditRequest(changeSet, contexts, pack, checks, promptName = "strict") {
7
+ export function searchRequest(input) {
29
8
  return {
30
- prompt: findingsAuditPrompt(contexts, pack, checks, changeSet.label, promptName),
31
- schema: findingsAuditSchema(contexts),
9
+ prompt: searchPrompt({
10
+ ...input,
11
+ includeCounterReason: input.includeCounterReasonInPrompt ?? false,
12
+ }),
13
+ schema: searchSchema(input.contexts),
14
+ contexts: input.contexts,
32
15
  };
33
16
  }
34
17
  export async function countPromptTokens(model, prompt) {
@@ -53,171 +36,45 @@ export async function countPromptTokens(model, prompt) {
53
36
  });
54
37
  return cached.count;
55
38
  }
56
- function findingsAuditSchema(contexts) {
57
- const targetIds = contexts.map((context) => context.targetId);
58
- const findingItem = {
59
- type: "object",
60
- properties: {
61
- targetId: { type: "string", enum: targetIds },
62
- why: { type: "string" },
63
- proof: { type: "string" },
64
- },
65
- required: ["targetId", "why", "proof"],
66
- additionalProperties: false,
67
- };
68
- const uncertainItem = {
69
- type: "object",
70
- properties: {
71
- targetId: { type: "string", enum: targetIds },
72
- why: { type: "string" },
73
- },
74
- required: ["targetId", "why"],
75
- additionalProperties: false,
76
- };
77
- return {
78
- type: "object",
79
- properties: {
80
- findings: {
81
- type: "array",
82
- items: findingItem,
83
- },
84
- uncertain: {
85
- type: "array",
86
- items: uncertainItem,
87
- },
88
- },
89
- additionalProperties: false,
90
- };
91
- }
92
- function auditSchema(contexts) {
93
- return auditSchemaFromProofs(contexts.map((context) => context.pointer));
94
- }
95
- function auditSchemaFromProofs(proofs) {
96
- return {
97
- type: "object",
98
- properties: {
99
- findings: {
100
- type: "array",
101
- items: {
102
- type: "object",
103
- properties: {
104
- checkId: { type: "string" },
105
- why: { type: "string" },
106
- proof: { type: "string", enum: proofs },
107
- },
108
- required: ["checkId", "why", "proof"],
109
- additionalProperties: false,
110
- },
111
- },
112
- summary: { type: "string" },
113
- },
114
- required: ["findings", "summary"],
115
- additionalProperties: false,
116
- };
117
- }
118
- function semScoutSchema(changeSet, checks, maxCandidates) {
39
+ function searchSchema(contexts) {
119
40
  return {
120
41
  type: "object",
121
42
  properties: {
122
- targets: {
43
+ matches: {
123
44
  type: "array",
124
- maxItems: maxCandidates,
45
+ maxItems: 5,
125
46
  items: {
126
47
  type: "object",
127
48
  properties: {
128
- entityId: { type: "string", enum: changeSet.changes.map((change) => change.entityId) },
129
- checkId: { type: "string", enum: checks.map((check) => check.id) },
49
+ targetId: { type: "string", enum: contexts.map((context) => context.targetId) },
130
50
  reason: { type: "string" },
51
+ proof: { type: "string" },
131
52
  },
132
- required: ["entityId", "checkId", "reason"],
53
+ required: ["targetId", "reason", "proof"],
133
54
  additionalProperties: false,
134
55
  },
135
56
  },
136
57
  },
137
- required: ["targets"],
58
+ required: ["matches"],
138
59
  additionalProperties: false,
139
60
  };
140
61
  }
141
- function scoutSchema(batch) {
142
- return {
143
- type: "object",
144
- properties: {
145
- candidates: {
146
- type: "array",
147
- maxItems: 3,
148
- items: { type: "string", enum: batch.hunks.map((hunk) => hunk.pointer) },
149
- },
150
- },
151
- required: ["candidates"],
152
- additionalProperties: false,
153
- };
154
- }
155
- function uncheckedCandidates(value) {
156
- return [...(value.candidates ?? [])];
157
- }
158
- function uncheckedRawAuditResult(value, sourceId) {
159
- const output = value;
160
- const findings = (output.findings ?? []).map((finding) => ({
161
- sourceId,
162
- checkId: (finding.checkId ?? ""),
163
- why: finding.why ?? "",
164
- proof: finding.proof ?? "",
165
- }));
166
- return { findings, summary: output.summary ?? defaultSummary(findings.length) };
167
- }
168
- function uncheckedSemCandidates(value, sourceId) {
62
+ function uncheckedSearchMatches(value, contexts) {
169
63
  const output = value;
170
- const rawTargets = output.targets ?? output.candidates ?? [];
171
- return rawTargets.flatMap((candidate) => {
172
- if (candidate.checkId) {
173
- return [{
174
- sourceId,
175
- targetId: candidate.targetId ?? "",
176
- entityId: candidate.entityId ?? "",
177
- checkId: candidate.checkId,
178
- reason: candidate.reason ?? "",
179
- }];
180
- }
181
- return (candidate.checkIds ?? []).map((checkId) => ({
182
- sourceId,
183
- targetId: candidate.targetId ?? "",
184
- entityId: candidate.entityId ?? "",
185
- checkId: checkId,
186
- reason: candidate.reason ?? "",
187
- }));
64
+ const contextsByTargetId = new Map(contexts.map((context) => [context.targetId, context]));
65
+ return (output.matches ?? []).flatMap((match) => {
66
+ const targetId = match.targetId ?? "";
67
+ const context = contextsByTargetId.get(targetId);
68
+ if (!context)
69
+ return [];
70
+ return [{
71
+ targetId,
72
+ patternId: context.checkId,
73
+ reason: match.reason ?? "",
74
+ proof: match.proof ?? "",
75
+ }];
188
76
  });
189
77
  }
190
- function uncheckedFindingsAuditResult(value, sourceId, contexts) {
191
- const output = value;
192
- const targetsById = new Map(contexts.map((context) => [context.targetId, context]));
193
- const findings = (output.findings ?? []).map((finding) => {
194
- const target = targetsById.get(finding.targetId ?? "");
195
- return {
196
- sourceId,
197
- checkId: (target?.checkId ?? ""),
198
- why: finding.why ?? "",
199
- proof: finding.proof ?? "",
200
- };
201
- });
202
- const uncertain = output.uncertain?.length ?? 0;
203
- const totalTargets = contexts.length;
204
- return {
205
- findings,
206
- summary: defaultSummary(findings.length),
207
- stats: {
208
- totalTargets,
209
- finding: findings.length,
210
- clean: Math.max(0, totalTargets - findings.length - uncertain),
211
- uncertain,
212
- invalid: 0,
213
- },
214
- };
215
- }
216
- function defaultSummary(findingCount) {
217
- return findingCount === 0
218
- ? "No clear judgment-offload signal found."
219
- : `${findingCount} finding review${findingCount === 1 ? "" : "s"} accepted.`;
220
- }
221
78
  async function runJsonPrompt(model, prompt, schema, temperature) {
222
79
  return cachedJson("model-json", fingerprint({
223
80
  version: 1,
package/dist/checks.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { type StupifyCheck } from "./types.ts";
2
2
  export declare const defaultChecks: readonly StupifyCheck[];
3
3
  export declare function enabledChecks(checkIds: readonly string[] | null): readonly StupifyCheck[];
4
+ export declare function searchChecks(checkIds: readonly string[] | null): readonly StupifyCheck[];
package/dist/checks.js CHANGED
@@ -10,7 +10,19 @@ export const defaultChecks = [
10
10
  ],
11
11
  ignoreWhen: [
12
12
  "test fixture, mock, or intentional external contract",
13
- ],
13
+ "public API DTO filters, omits, protects, renames, or versions fields",
14
+ ],
15
+ hookMode: "warn",
16
+ searchPrompt: "Find only local/private payload or schema shapes that clearly copy another local shape one-for-one without creating a boundary. Do not match ordinary Input/Output/Request/Response types by name alone, public DTOs, external contracts, client types, or types that omit/protect private fields.",
17
+ searchExamples: {
18
+ match: [
19
+ "LocalUserPayload repeats User fields and maps id/email/displayName one-for-one.",
20
+ ],
21
+ nonMatch: [
22
+ "PublicWebhookDto omits privateNotes from InternalJob.",
23
+ "A client type describes an external dependency boundary.",
24
+ ],
25
+ },
14
26
  },
15
27
  {
16
28
  id: checkId("unnecessary_complexity"),
@@ -22,6 +34,35 @@ export const defaultChecks = [
22
34
  ignoreWhen: [
23
35
  "isolates dependency, removes duplication, or improves testability",
24
36
  ],
37
+ hookMode: "warn",
38
+ searchPrompt: `Find staged changes where a locally simple decision is made harder to understand by new indirection.
39
+ Only match when the staged diff clearly shows:
40
+ - a new named helper, wrapper, service, adapter, boundary, or abstraction
41
+ - and the surrounding change still appears locally simple
42
+ - and the new structure makes the decision harder to see
43
+ Do not match:
44
+ - plain conditionals, guard clauses, skip paths, or error handling
45
+ - normal feature structure
46
+ - exported utilities that are part of a real feature
47
+ - command plumbing
48
+ - prompt/instruction files
49
+ - domain configuration
50
+ - refactors that make ownership clearer
51
+ - changes where the payoff is unclear from the diff
52
+ Prefer no match over a weak match.`,
53
+ searchExamples: {
54
+ match: [
55
+ "A small inline operation becomes a helper/service/wrapper with one obvious caller.",
56
+ "A straightforward flow is split across files in a way that hides the decision.",
57
+ "A new abstraction appears before there is evidence it buys clarity, correctness, reuse, or isolation.",
58
+ ],
59
+ nonMatch: [
60
+ "A real external dependency boundary is isolated.",
61
+ "A security/auth boundary becomes clearer.",
62
+ "A refactor removes larger complexity elsewhere.",
63
+ "Framework-required structure is added.",
64
+ ],
65
+ },
25
66
  },
26
67
  {
27
68
  id: checkId("fake_precision_windowing"),
@@ -67,6 +108,22 @@ export const defaultChecks = [
67
108
  ignoreWhen: [
68
109
  "comment explains intent, constraint, workaround, or public API behavior",
69
110
  ],
111
+ hookMode: "warn",
112
+ searchPrompt: "Find staged changes where comments appear to substitute for judgment rather than clarify it.",
113
+ searchExamples: {
114
+ match: [
115
+ "New comments narrate obvious code instead of explaining tradeoffs.",
116
+ "A simple change gains multiple generic comments that restate control flow.",
117
+ "Comments make the code look more deliberate without adding useful reasoning.",
118
+ ],
119
+ nonMatch: [
120
+ "Comments explain a real domain constraint.",
121
+ "Comments document an external API quirk.",
122
+ "Comments clarify a surprising edge case.",
123
+ "Comments are sparse and specific.",
124
+ "Comments explain provider, finance, reconciliation, timezone, or ledger behavior.",
125
+ ],
126
+ },
70
127
  },
71
128
  {
72
129
  id: checkId("lint_bypass"),
@@ -80,6 +137,16 @@ export const defaultChecks = [
80
137
  "type-level test",
81
138
  "generated file convention",
82
139
  ],
140
+ hookMode: "warn",
141
+ searchPrompt: "Find only broad lint/type bypasses that hide useful feedback. Match bare @ts-ignore, bare @ts-expect-error, broad casts, any, or eslint/biome suppressions without a concrete inline reason. Do not match targeted suppressions that include a reason for a known framework, test, mock, or external-library limitation.",
142
+ searchExamples: {
143
+ match: [
144
+ "A bare // @ts-ignore hides property access on unknown input.",
145
+ ],
146
+ nonMatch: [
147
+ "// @ts-expect-error explains a known external library typing gap.",
148
+ ],
149
+ },
83
150
  },
84
151
  {
85
152
  id: checkId("inconsistent_patterns"),
@@ -103,7 +170,19 @@ export const defaultChecks = [
103
170
  ignoreWhen: [
104
171
  "existing utility has wrong contract",
105
172
  "new helper is clearer as a tiny private expression",
106
- ],
173
+ "helper is domain-specific or used by multiple local call sites",
174
+ ],
175
+ hookMode: "warn",
176
+ searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify, group, sort, pick, omit, uniq, or shuffle without domain-specific behavior. Do not match resolve/parse/format helpers, domain formatting, feature constants, or helpers with multiple obvious call sites.",
177
+ searchExamples: {
178
+ match: [
179
+ "clampValue returns min, max, or value.",
180
+ ],
181
+ nonMatch: [
182
+ "formatCurrencyHelper is used by invoice and refund labels.",
183
+ "Subscription tier constants encode domain configuration.",
184
+ ],
185
+ },
107
186
  },
108
187
  {
109
188
  id: checkId("operator_style_mismatch"),
@@ -121,6 +200,14 @@ export const defaultChecks = [
121
200
  export function enabledChecks(checkIds) {
122
201
  if (!checkIds)
123
202
  return defaultChecks.filter((check) => check.enabledByDefault !== false);
203
+ return checksById(checkIds);
204
+ }
205
+ export function searchChecks(checkIds) {
206
+ if (!checkIds)
207
+ return defaultChecks.filter((check) => check.hookMode === "warn");
208
+ return checksById(checkIds);
209
+ }
210
+ function checksById(checkIds) {
124
211
  const checksById = new Map(defaultChecks.map((check) => [check.id, check]));
125
212
  return checkIds.map((id) => {
126
213
  const check = checksById.get(id);