@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/src/analysis.ts CHANGED
@@ -1,95 +1,36 @@
1
- import { auditPrompt, findingsAuditPrompt, scoutPrompt, semScoutPrompt } from "./prompts.ts";
2
1
  import { cachedJson, fingerprint } from "./cache.ts";
3
2
  import type { LocalModel } from "./model.ts";
4
- import type {
5
- CandidateContext,
6
- CheckId,
7
- DiffBatch,
8
- AuditPromptName,
9
- AuditReviewResult,
10
- Finding,
11
- FindingsResult,
12
- NetDiff,
13
- SemCandidate,
14
- SemChangeSet,
15
- SemContext,
16
- SemContextPack,
17
- SourceId,
18
- StupifyCheck,
19
- } from "./types.ts";
3
+ import { searchPrompt } from "./prompts.ts";
4
+ import type { SearchMatch, SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
20
5
 
21
- export async function scoutBatch(
6
+ export async function runSearch(
22
7
  model: LocalModel,
23
- batch: DiffBatch,
24
- checks: readonly StupifyCheck[],
25
- sourceLabel: string,
26
- ): Promise<readonly string[]> {
27
- const raw = await runJsonPrompt(model, scoutPrompt(batch, checks, sourceLabel), scoutSchema(batch), 0);
28
- return uncheckedCandidates(raw);
8
+ request: SearchRequest,
9
+ ): Promise<readonly SearchMatch[]> {
10
+ const raw = await runJsonPrompt(model, request.prompt, request.schema, 0);
11
+ return uncheckedSearchMatches(raw, request.contexts);
29
12
  }
30
13
 
31
- export async function auditCandidates(
32
- model: LocalModel,
33
- diff: NetDiff,
34
- contexts: readonly CandidateContext[],
35
- checks: readonly StupifyCheck[],
36
- ): Promise<FindingsResult> {
37
- if (contexts.length === 0) return { findings: [], summary: "No candidate regions found." };
38
-
39
- const raw = await runJsonPrompt(model, auditPrompt(contexts, checks, diff.label), auditSchema(contexts), 0);
40
- return uncheckedRawAuditResult(raw, diff.id);
41
- }
42
-
43
- export async function scoutSemChanges(
44
- model: LocalModel,
45
- changeSet: SemChangeSet,
46
- checks: readonly StupifyCheck[],
47
- maxCandidates: number,
48
- ): Promise<readonly SemCandidate[]> {
49
- const raw = await runJsonPrompt(
50
- model,
51
- semScoutPrompt(changeSet, checks, maxCandidates),
52
- semScoutSchema(changeSet, checks, maxCandidates),
53
- 0,
54
- );
55
- return uncheckedSemCandidates(raw, changeSet.id);
56
- }
57
-
58
- export async function runFindingsAudit(
59
- model: LocalModel,
60
- changeSet: SemChangeSet,
61
- contexts: readonly SemContext[],
62
- pack: SemContextPack,
63
- checks: readonly StupifyCheck[],
64
- request = findingsAuditRequest(changeSet, contexts, pack, checks),
65
- ): Promise<AuditReviewResult> {
66
- if (contexts.length === 0) {
67
- return {
68
- findings: [],
69
- summary: "No candidate entities found.",
70
- stats: { totalTargets: 0, finding: 0, clean: 0, uncertain: 0, invalid: 0 },
71
- };
72
- }
73
-
74
- const raw = await runJsonPrompt(
75
- model,
76
- request.prompt,
77
- request.schema,
78
- 0,
79
- );
80
- return uncheckedFindingsAuditResult(raw, changeSet.id, contexts);
81
- }
14
+ export type SearchRequest = Readonly<{
15
+ prompt: string;
16
+ schema: unknown;
17
+ contexts: readonly SemContext[];
18
+ }>;
82
19
 
83
- export function findingsAuditRequest(
84
- changeSet: SemChangeSet,
85
- contexts: readonly SemContext[],
86
- pack: SemContextPack,
87
- checks: readonly StupifyCheck[],
88
- promptName: AuditPromptName = "strict",
89
- ): Readonly<{ prompt: string; schema: unknown }> {
20
+ export function searchRequest(input: Readonly<{
21
+ changeSet: SemChangeSet;
22
+ contexts: readonly SemContext[];
23
+ pack: SemContextPack;
24
+ patterns: readonly StupifyCheck[];
25
+ includeCounterReasonInPrompt?: boolean;
26
+ }>): SearchRequest {
90
27
  return {
91
- prompt: findingsAuditPrompt(contexts, pack, checks, changeSet.label, promptName),
92
- schema: findingsAuditSchema(contexts),
28
+ prompt: searchPrompt({
29
+ ...input,
30
+ includeCounterReason: input.includeCounterReasonInPrompt ?? false,
31
+ }),
32
+ schema: searchSchema(input.contexts),
33
+ contexts: input.contexts,
93
34
  };
94
35
  }
95
36
 
@@ -119,214 +60,53 @@ export async function countPromptTokens(model: LocalModel, prompt: string): Prom
119
60
  return cached.count;
120
61
  }
121
62
 
122
- function findingsAuditSchema(contexts: readonly SemContext[]): unknown {
123
- const targetIds = contexts.map((context) => context.targetId);
124
- const findingItem = {
125
- type: "object",
126
- properties: {
127
- targetId: { type: "string", enum: targetIds },
128
- why: { type: "string" },
129
- proof: { type: "string" },
130
- },
131
- required: ["targetId", "why", "proof"],
132
- additionalProperties: false,
133
- };
134
- const uncertainItem = {
135
- type: "object",
136
- properties: {
137
- targetId: { type: "string", enum: targetIds },
138
- why: { type: "string" },
139
- },
140
- required: ["targetId", "why"],
141
- additionalProperties: false,
142
- };
143
- return {
144
- type: "object",
145
- properties: {
146
- findings: {
147
- type: "array",
148
- items: findingItem,
149
- },
150
- uncertain: {
151
- type: "array",
152
- items: uncertainItem,
153
- },
154
- },
155
- additionalProperties: false,
156
- };
157
- }
158
-
159
- function auditSchema(contexts: readonly CandidateContext[]): unknown {
160
- return auditSchemaFromProofs(contexts.map((context) => context.pointer));
161
- }
162
-
163
- function auditSchemaFromProofs(proofs: readonly string[]): unknown {
164
- return {
165
- type: "object",
166
- properties: {
167
- findings: {
168
- type: "array",
169
- items: {
170
- type: "object",
171
- properties: {
172
- checkId: { type: "string" },
173
- why: { type: "string" },
174
- proof: { type: "string", enum: proofs },
175
- },
176
- required: ["checkId", "why", "proof"],
177
- additionalProperties: false,
178
- },
179
- },
180
- summary: { type: "string" },
181
- },
182
- required: ["findings", "summary"],
183
- additionalProperties: false,
184
- };
185
- }
186
-
187
- function semScoutSchema(
188
- changeSet: SemChangeSet,
189
- checks: readonly StupifyCheck[],
190
- maxCandidates: number,
191
- ): unknown {
63
+ function searchSchema(contexts: readonly SemContext[]): unknown {
192
64
  return {
193
65
  type: "object",
194
66
  properties: {
195
- targets: {
67
+ matches: {
196
68
  type: "array",
197
- maxItems: maxCandidates,
69
+ maxItems: 5,
198
70
  items: {
199
71
  type: "object",
200
72
  properties: {
201
- entityId: { type: "string", enum: changeSet.changes.map((change) => change.entityId) },
202
- checkId: { type: "string", enum: checks.map((check) => check.id) },
73
+ targetId: { type: "string", enum: contexts.map((context) => context.targetId) },
203
74
  reason: { type: "string" },
75
+ proof: { type: "string" },
204
76
  },
205
- required: ["entityId", "checkId", "reason"],
77
+ required: ["targetId", "reason", "proof"],
206
78
  additionalProperties: false,
207
79
  },
208
80
  },
209
81
  },
210
- required: ["targets"],
211
- additionalProperties: false,
212
- };
213
- }
214
-
215
- function scoutSchema(batch: DiffBatch): unknown {
216
- return {
217
- type: "object",
218
- properties: {
219
- candidates: {
220
- type: "array",
221
- maxItems: 3,
222
- items: { type: "string", enum: batch.hunks.map((hunk) => hunk.pointer) },
223
- },
224
- },
225
- required: ["candidates"],
82
+ required: ["matches"],
226
83
  additionalProperties: false,
227
84
  };
228
85
  }
229
86
 
230
- type RawScoutOutput = Readonly<{ candidates?: readonly string[] }>;
231
- type RawAuditOutput = Readonly<{
232
- findings?: readonly RawFinding[];
233
- summary?: string;
87
+ type RawSearchOutput = Readonly<{
88
+ matches?: readonly RawSearchMatch[];
234
89
  }>;
235
- type RawSemScoutOutput = Readonly<{
236
- targets?: readonly RawSemCandidate[];
237
- candidates?: readonly RawSemCandidate[];
238
- }>;
239
- type RawSemCandidate = Readonly<{
90
+ type RawSearchMatch = Readonly<{
240
91
  targetId?: string;
241
- entityId?: string;
242
- checkId?: string;
243
- checkIds?: readonly string[];
244
92
  reason?: string;
245
- }>;
246
- type RawFinding = Readonly<{
247
- checkId?: string;
248
- why?: string;
249
93
  proof?: string;
250
94
  }>;
251
- type RawFindingReview = RawFinding & Readonly<{ targetId?: string }>;
252
- type RawFindingsAuditOutput = Readonly<{
253
- findings?: readonly RawFindingReview[];
254
- uncertain?: readonly RawFindingReview[];
255
- }>;
256
-
257
- function uncheckedCandidates(value: unknown): readonly string[] {
258
- return [...((value as RawScoutOutput).candidates ?? [])];
259
- }
260
95
 
261
- function uncheckedRawAuditResult(value: unknown, sourceId: SourceId): FindingsResult {
262
- const output = value as RawAuditOutput;
263
- const findings = (output.findings ?? []).map((finding): Finding => ({
264
- sourceId,
265
- checkId: (finding.checkId ?? "") as CheckId,
266
- why: finding.why ?? "",
267
- proof: finding.proof ?? "",
268
- }));
269
- return { findings, summary: output.summary ?? defaultSummary(findings.length) };
270
- }
271
-
272
- function uncheckedSemCandidates(value: unknown, sourceId: SourceId): readonly SemCandidate[] {
273
- const output = value as RawSemScoutOutput;
274
- const rawTargets = output.targets ?? output.candidates ?? [];
275
- return rawTargets.flatMap((candidate) => {
276
- if (candidate.checkId) {
277
- return [{
278
- sourceId,
279
- targetId: candidate.targetId ?? "",
280
- entityId: candidate.entityId ?? "",
281
- checkId: candidate.checkId as CheckId,
282
- reason: candidate.reason ?? "",
283
- }];
284
- }
285
- return (candidate.checkIds ?? []).map((checkId) => ({
286
- sourceId,
287
- targetId: candidate.targetId ?? "",
288
- entityId: candidate.entityId ?? "",
289
- checkId: checkId as CheckId,
290
- reason: candidate.reason ?? "",
291
- }));
292
- });
293
- }
294
-
295
- function uncheckedFindingsAuditResult(
296
- value: unknown,
297
- sourceId: SourceId,
298
- contexts: readonly SemContext[],
299
- ): AuditReviewResult {
300
- const output = value as RawFindingsAuditOutput;
301
- const targetsById = new Map(contexts.map((context) => [context.targetId, context]));
302
- const findings = (output.findings ?? []).map((finding): Finding => {
303
- const target = targetsById.get(finding.targetId ?? "");
304
- return {
305
- sourceId,
306
- checkId: (target?.checkId ?? "") as CheckId,
307
- why: finding.why ?? "",
308
- proof: finding.proof ?? "",
309
- };
96
+ function uncheckedSearchMatches(value: unknown, contexts: readonly SemContext[]): readonly SearchMatch[] {
97
+ const output = value as RawSearchOutput;
98
+ const contextsByTargetId = new Map(contexts.map((context) => [context.targetId, context]));
99
+ return (output.matches ?? []).flatMap((match): readonly SearchMatch[] => {
100
+ const targetId = match.targetId ?? "";
101
+ const context = contextsByTargetId.get(targetId);
102
+ if (!context) return [];
103
+ return [{
104
+ targetId,
105
+ patternId: context.checkId,
106
+ reason: match.reason ?? "",
107
+ proof: match.proof ?? "",
108
+ }];
310
109
  });
311
- const uncertain = output.uncertain?.length ?? 0;
312
- const totalTargets = contexts.length;
313
- return {
314
- findings,
315
- summary: defaultSummary(findings.length),
316
- stats: {
317
- totalTargets,
318
- finding: findings.length,
319
- clean: Math.max(0, totalTargets - findings.length - uncertain),
320
- uncertain,
321
- invalid: 0,
322
- },
323
- };
324
- }
325
-
326
- function defaultSummary(findingCount: number): string {
327
- return findingCount === 0
328
- ? "No clear judgment-offload signal found."
329
- : `${findingCount} finding review${findingCount === 1 ? "" : "s"} accepted.`;
330
110
  }
331
111
 
332
112
  async function runJsonPrompt(
package/src/checks.ts CHANGED
@@ -11,7 +11,19 @@ export const defaultChecks: readonly StupifyCheck[] = [
11
11
  ],
12
12
  ignoreWhen: [
13
13
  "test fixture, mock, or intentional external contract",
14
- ],
14
+ "public API DTO filters, omits, protects, renames, or versions fields",
15
+ ],
16
+ hookMode: "warn",
17
+ 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.",
18
+ searchExamples: {
19
+ match: [
20
+ "LocalUserPayload repeats User fields and maps id/email/displayName one-for-one.",
21
+ ],
22
+ nonMatch: [
23
+ "PublicWebhookDto omits privateNotes from InternalJob.",
24
+ "A client type describes an external dependency boundary.",
25
+ ],
26
+ },
15
27
  },
16
28
  {
17
29
  id: checkId("unnecessary_complexity"),
@@ -23,6 +35,35 @@ export const defaultChecks: readonly StupifyCheck[] = [
23
35
  ignoreWhen: [
24
36
  "isolates dependency, removes duplication, or improves testability",
25
37
  ],
38
+ hookMode: "warn",
39
+ searchPrompt: `Find staged changes where a locally simple decision is made harder to understand by new indirection.
40
+ Only match when the staged diff clearly shows:
41
+ - a new named helper, wrapper, service, adapter, boundary, or abstraction
42
+ - and the surrounding change still appears locally simple
43
+ - and the new structure makes the decision harder to see
44
+ Do not match:
45
+ - plain conditionals, guard clauses, skip paths, or error handling
46
+ - normal feature structure
47
+ - exported utilities that are part of a real feature
48
+ - command plumbing
49
+ - prompt/instruction files
50
+ - domain configuration
51
+ - refactors that make ownership clearer
52
+ - changes where the payoff is unclear from the diff
53
+ Prefer no match over a weak match.`,
54
+ searchExamples: {
55
+ match: [
56
+ "A small inline operation becomes a helper/service/wrapper with one obvious caller.",
57
+ "A straightforward flow is split across files in a way that hides the decision.",
58
+ "A new abstraction appears before there is evidence it buys clarity, correctness, reuse, or isolation.",
59
+ ],
60
+ nonMatch: [
61
+ "A real external dependency boundary is isolated.",
62
+ "A security/auth boundary becomes clearer.",
63
+ "A refactor removes larger complexity elsewhere.",
64
+ "Framework-required structure is added.",
65
+ ],
66
+ },
26
67
  },
27
68
  {
28
69
  id: checkId("fake_precision_windowing"),
@@ -68,6 +109,22 @@ export const defaultChecks: readonly StupifyCheck[] = [
68
109
  ignoreWhen: [
69
110
  "comment explains intent, constraint, workaround, or public API behavior",
70
111
  ],
112
+ hookMode: "warn",
113
+ searchPrompt: "Find staged changes where comments appear to substitute for judgment rather than clarify it.",
114
+ searchExamples: {
115
+ match: [
116
+ "New comments narrate obvious code instead of explaining tradeoffs.",
117
+ "A simple change gains multiple generic comments that restate control flow.",
118
+ "Comments make the code look more deliberate without adding useful reasoning.",
119
+ ],
120
+ nonMatch: [
121
+ "Comments explain a real domain constraint.",
122
+ "Comments document an external API quirk.",
123
+ "Comments clarify a surprising edge case.",
124
+ "Comments are sparse and specific.",
125
+ "Comments explain provider, finance, reconciliation, timezone, or ledger behavior.",
126
+ ],
127
+ },
71
128
  },
72
129
  {
73
130
  id: checkId("lint_bypass"),
@@ -81,6 +138,16 @@ export const defaultChecks: readonly StupifyCheck[] = [
81
138
  "type-level test",
82
139
  "generated file convention",
83
140
  ],
141
+ hookMode: "warn",
142
+ 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.",
143
+ searchExamples: {
144
+ match: [
145
+ "A bare // @ts-ignore hides property access on unknown input.",
146
+ ],
147
+ nonMatch: [
148
+ "// @ts-expect-error explains a known external library typing gap.",
149
+ ],
150
+ },
84
151
  },
85
152
  {
86
153
  id: checkId("inconsistent_patterns"),
@@ -104,7 +171,19 @@ export const defaultChecks: readonly StupifyCheck[] = [
104
171
  ignoreWhen: [
105
172
  "existing utility has wrong contract",
106
173
  "new helper is clearer as a tiny private expression",
107
- ],
174
+ "helper is domain-specific or used by multiple local call sites",
175
+ ],
176
+ hookMode: "warn",
177
+ 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.",
178
+ searchExamples: {
179
+ match: [
180
+ "clampValue returns min, max, or value.",
181
+ ],
182
+ nonMatch: [
183
+ "formatCurrencyHelper is used by invoice and refund labels.",
184
+ "Subscription tier constants encode domain configuration.",
185
+ ],
186
+ },
108
187
  },
109
188
  {
110
189
  id: checkId("operator_style_mismatch"),
@@ -123,6 +202,16 @@ export const defaultChecks: readonly StupifyCheck[] = [
123
202
  export function enabledChecks(checkIds: readonly string[] | null): readonly StupifyCheck[] {
124
203
  if (!checkIds) return defaultChecks.filter((check) => check.enabledByDefault !== false);
125
204
 
205
+ return checksById(checkIds);
206
+ }
207
+
208
+ export function searchChecks(checkIds: readonly string[] | null): readonly StupifyCheck[] {
209
+ if (!checkIds) return defaultChecks.filter((check) => check.hookMode === "warn");
210
+
211
+ return checksById(checkIds);
212
+ }
213
+
214
+ function checksById(checkIds: readonly string[]): readonly StupifyCheck[] {
126
215
  const checksById = new Map<string, StupifyCheck>(defaultChecks.map((check) => [check.id, check]));
127
216
  return checkIds.map((id) => {
128
217
  const check = checksById.get(id);