@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/hooks.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { chmod, readFile, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { gitPath, gitRoot } from "./git.ts";
7
+ import type { HookAction } from "./types.ts";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const START = "# stupify hook start";
11
+ const END = "# stupify hook end";
12
+
13
+ export async function runHookCommand(action: HookAction): Promise<string> {
14
+ if (action === "status") return hookStatus();
15
+ if (action === "install") return installHook();
16
+ return uninstallHook();
17
+ }
18
+
19
+ export function hookSnippet(): string {
20
+ return managedBlock("stupify --staged");
21
+ }
22
+
23
+ async function hookStatus(): Promise<string> {
24
+ const hookPath = await preCommitHookPath();
25
+ if (!existsSync(hookPath)) return "Stupify hook: not installed";
26
+
27
+ const content = await readFile(hookPath, "utf8");
28
+ if (hasManagedBlock(content)) return "Stupify hook: installed";
29
+ return "Stupify hook: existing non-Stupify pre-commit hook found";
30
+ }
31
+
32
+ async function installHook(): Promise<string> {
33
+ const hookPath = await preCommitHookPath();
34
+ const block = await managedBlockForInstall();
35
+ if (!existsSync(hookPath)) {
36
+ await writeFile(hookPath, `#!/bin/sh\n${block}\n`, "utf8");
37
+ await chmod(hookPath, 0o755);
38
+ return "Stupify hook: installed";
39
+ }
40
+
41
+ const content = await readFile(hookPath, "utf8");
42
+ if (hasManagedBlock(content)) {
43
+ await writeFile(hookPath, `${replaceManagedBlock(content, block).trimEnd()}\n`, "utf8");
44
+ await chmod(hookPath, 0o755);
45
+ return "Stupify hook: updated";
46
+ }
47
+
48
+ if (isEffectivelyEmptyHook(content)) {
49
+ await writeFile(hookPath, `#!/bin/sh\n${block}\n`, "utf8");
50
+ await chmod(hookPath, 0o755);
51
+ return "Stupify hook: installed";
52
+ }
53
+
54
+ return `Stupify hook: existing non-Stupify pre-commit hook found; not modified.
55
+ Add this snippet manually if you want Stupify in that hook:
56
+ ${block}`;
57
+ }
58
+
59
+ async function uninstallHook(): Promise<string> {
60
+ const hookPath = await preCommitHookPath();
61
+ if (!existsSync(hookPath)) return "Stupify hook: not installed";
62
+
63
+ const content = await readFile(hookPath, "utf8");
64
+ if (!hasManagedBlock(content)) return "Stupify hook: not installed";
65
+
66
+ const next = replaceManagedBlock(content, "").trim();
67
+ if (isEffectivelyEmptyHook(next)) {
68
+ await rm(hookPath, { force: true });
69
+ return "Stupify hook: uninstalled";
70
+ }
71
+
72
+ await writeFile(hookPath, `${next}\n`, "utf8");
73
+ await chmod(hookPath, 0o755);
74
+ return "Stupify hook: uninstalled";
75
+ }
76
+
77
+ async function preCommitHookPath(): Promise<string> {
78
+ const [root, hook] = await Promise.all([gitRoot(), gitPath("hooks/pre-commit")]);
79
+ return path.isAbsolute(hook) ? hook : path.join(root, hook);
80
+ }
81
+
82
+ function hasManagedBlock(content: string): boolean {
83
+ return content.includes(START) && content.includes(END);
84
+ }
85
+
86
+ async function managedBlockForInstall(): Promise<string> {
87
+ if (await commandExists("stupify")) return managedBlock("stupify --staged");
88
+
89
+ const root = await gitRoot();
90
+ const localEntrypoint = path.join(root, "packages", "cli", "src", "stupify.ts");
91
+ if (existsSync(localEntrypoint) && await commandExists("bun")) {
92
+ return managedBlock(`bun ${shellQuote(localEntrypoint)} --staged`);
93
+ }
94
+
95
+ return managedBlock("stupify --staged");
96
+ }
97
+
98
+ function managedBlock(command: string): string {
99
+ return `${START}
100
+ ${command} || true
101
+ ${END}`;
102
+ }
103
+
104
+ function replaceManagedBlock(content: string, replacement: string): string {
105
+ const pattern = new RegExp(`${escapeRegExp(START)}[\\s\\S]*?${escapeRegExp(END)}`);
106
+ return content.replace(pattern, replacement);
107
+ }
108
+
109
+ function isEffectivelyEmptyHook(content: string): boolean {
110
+ return content
111
+ .split(/\r?\n/)
112
+ .map((line) => line.trim())
113
+ .filter((line) => line && line !== "#!/bin/sh" && line !== "#!/usr/bin/env sh")
114
+ .length === 0;
115
+ }
116
+
117
+ function escapeRegExp(value: string): string {
118
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
119
+ }
120
+
121
+ async function commandExists(command: string): Promise<boolean> {
122
+ try {
123
+ await execFileAsync("sh", ["-c", `command -v ${shellQuote(command)}`], {
124
+ maxBuffer: 1024 * 1024,
125
+ });
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ function shellQuote(value: string): string {
133
+ return `'${value.replace(/'/g, "'\\''")}'`;
134
+ }
package/src/model.ts CHANGED
@@ -24,14 +24,22 @@ import type { ModelId } from "./types.ts";
24
24
  const execFileAsync = promisify(execFile);
25
25
  const LLAMA_SERVER_HOST = "127.0.0.1";
26
26
 
27
- export type ModelProfile = "scout" | "audit";
27
+ export type ModelProfile = "scout";
28
28
 
29
29
  type ModelRuntime = Readonly<{
30
30
  profile: ModelProfile;
31
31
  baseUrl: string;
32
32
  port: string;
33
+ contextSize: number;
33
34
  reasoning: "on" | "off" | "auto";
34
35
  reasoningBudget?: number;
36
+ gpuLayers?: number;
37
+ batchSize?: number;
38
+ ubatchSize?: number;
39
+ parallel?: number;
40
+ threads?: number;
41
+ threadsBatch?: number;
42
+ flashAttention?: boolean;
35
43
  }>;
36
44
 
37
45
  export type LocalModel = Readonly<{
@@ -41,13 +49,6 @@ export type LocalModel = Readonly<{
41
49
  profile: ModelProfile;
42
50
  }>;
43
51
 
44
- export async function loadLocalModels(modelId: ModelId) {
45
- const modelPath = await firstRunModelBootstrap(modelId);
46
- const scoutModel = await loadLocalModel(modelPath, modelId, "scout");
47
- const auditModel = await loadLocalModel(modelPath, modelId, "audit");
48
- return { scoutModel, auditModel };
49
- }
50
-
51
52
  export async function firstRunModelBootstrap(
52
53
  modelId: ModelId,
53
54
  ): Promise<string> {
@@ -107,18 +108,6 @@ export async function loadLocalModel(
107
108
  }
108
109
 
109
110
  function modelRuntime(profile: ModelProfile): ModelRuntime {
110
- if (profile === "audit") {
111
- const baseUrl =
112
- process.env.STUPIFY_AUDIT_LLAMA_SERVER_URL ?? "http://127.0.0.1:8092";
113
- return {
114
- profile,
115
- baseUrl,
116
- port: new URL(baseUrl).port || "8092",
117
- reasoning: "on",
118
- reasoningBudget: 4_096,
119
- };
120
- }
121
-
122
111
  const baseUrl =
123
112
  process.env.STUPIFY_SCOUT_LLAMA_SERVER_URL ??
124
113
  process.env.STUPIFY_LLAMA_SERVER_URL ??
@@ -127,7 +116,15 @@ function modelRuntime(profile: ModelProfile): ModelRuntime {
127
116
  profile,
128
117
  baseUrl,
129
118
  port: new URL(baseUrl).port || "8091",
119
+ contextSize: envInteger("STUPIFY_LLAMA_CONTEXT") ?? 65_536,
130
120
  reasoning: "off",
121
+ gpuLayers: envInteger("STUPIFY_LLAMA_GPU_LAYERS") ?? 999,
122
+ batchSize: envInteger("STUPIFY_LLAMA_BATCH") ?? 2_048,
123
+ ubatchSize: envInteger("STUPIFY_LLAMA_UBATCH") ?? 512,
124
+ parallel: envInteger("STUPIFY_LLAMA_PARALLEL") ?? 2,
125
+ threads: envInteger("STUPIFY_LLAMA_THREADS"),
126
+ threadsBatch: envInteger("STUPIFY_LLAMA_THREADS_BATCH"),
127
+ flashAttention: envBoolean("STUPIFY_LLAMA_FLASH_ATTN"),
131
128
  };
132
129
  }
133
130
 
@@ -182,11 +179,18 @@ async function startLlamaServer(
182
179
  "--port",
183
180
  runtime.port,
184
181
  "-c",
185
- "65536",
182
+ String(runtime.contextSize),
186
183
  "--reasoning",
187
184
  runtime.reasoning,
188
185
  "--no-warmup",
189
186
  ];
187
+ if (runtime.gpuLayers !== undefined) args.push("-ngl", String(runtime.gpuLayers));
188
+ if (runtime.batchSize !== undefined) args.push("-b", String(runtime.batchSize));
189
+ if (runtime.ubatchSize !== undefined) args.push("-ub", String(runtime.ubatchSize));
190
+ if (runtime.parallel !== undefined) args.push("-np", String(runtime.parallel));
191
+ if (runtime.threads !== undefined) args.push("-t", String(runtime.threads));
192
+ if (runtime.threadsBatch !== undefined) args.push("-tb", String(runtime.threadsBatch));
193
+ if (runtime.flashAttention !== undefined) args.push("-fa", runtime.flashAttention ? "on" : "off");
190
194
  if (runtime.reasoningBudget !== undefined) {
191
195
  args.push("--reasoning-budget", String(runtime.reasoningBudget));
192
196
  }
@@ -242,11 +246,20 @@ async function managedServerPid(runtime: ModelRuntime): Promise<number | null> {
242
246
  }
243
247
 
244
248
  function pidPath(runtime: ModelRuntime): string {
245
- const filename =
246
- runtime.profile === "scout"
247
- ? "llama-server.pid"
248
- : `llama-server-${runtime.profile}.pid`;
249
- return path.join(cacheDir(), filename);
249
+ return path.join(cacheDir(), "llama-server.pid");
250
+ }
251
+
252
+ function envInteger(name: string, fallback?: number): number | undefined {
253
+ const raw = process.env[name];
254
+ if (raw === undefined || raw === "") return fallback;
255
+ const value = Number(raw);
256
+ return Number.isInteger(value) && value > 0 ? value : fallback;
257
+ }
258
+
259
+ function envBoolean(name: string): boolean | undefined {
260
+ const raw = process.env[name];
261
+ if (raw === undefined || raw === "") return undefined;
262
+ return /^(1|true|yes|on)$/i.test(raw);
250
263
  }
251
264
 
252
265
  async function waitForServer(baseUrl: string, modelId: ModelId): Promise<void> {
package/src/prompts.ts CHANGED
@@ -1,234 +1,98 @@
1
- import type {
2
- AuditPromptName,
3
- CandidateContext,
4
- DiffBatch,
5
- SemChangeSet,
6
- SemContext,
7
- SemContextPack,
8
- StupifyCheck,
9
- } from "./types.ts";
10
-
11
- export function scoutPrompt(batch: DiffBatch, checks: readonly StupifyCheck[], sourceLabel: string): string {
12
- return `Pick diff hunks that match enabled checks.
13
- Return JSON only:
14
- { "candidates": ["exact POINTER"] }
15
-
16
- Rules:
17
- - Use POINTER values exactly as shown.
18
- - Return at most 3 candidates.
19
- - Return { "candidates": [] } if clean.
20
- - Pick definitions over usage sites.
21
-
22
- ${formatCompactChecks(checks)}
23
-
24
- SOURCE:
25
- ${sourceLabel}
26
-
27
- DIFF BATCH ${batch.id}:
28
- ${batch.text}`;
29
- }
30
-
31
- export function auditPrompt(
32
- contexts: readonly CandidateContext[],
33
- checks: readonly StupifyCheck[],
34
- sourceLabel: string,
35
- ): string {
36
- return `Audit candidate diff regions against enabled checks.
1
+ import type { SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
2
+
3
+ export function searchPrompt(input: Readonly<{
4
+ changeSet: SemChangeSet;
5
+ contexts: readonly SemContext[];
6
+ pack: SemContextPack;
7
+ patterns: readonly StupifyCheck[];
8
+ includeCounterReason: boolean;
9
+ }>): string {
10
+ return `You are Stupify's local search model.
11
+ Stupify checks whether AI-assisted coding may be replacing developer judgment.
12
+ You will receive:
13
+ 1. Semantic changed entities selected by a fast local counter.
14
+ 2. Compressed local file context from Repomix.
15
+ 3. A list of search targets. Each target has exactly one assigned pattern.
16
+
17
+ Your job:
18
+ Evaluate each target only against its assigned pattern.
19
+ False positives are expensive.
20
+ Only emit a match if the assigned pattern clearly applies to that exact target.
21
+ Do not perform general code review.
22
+ Do not suggest improvements.
23
+ Do not choose a pattern.
24
+ Do not apply other patterns.
25
+ Do not report issues for unlisted targets.
26
+ Do not emit clean results.
27
+ Omitted target = clean.
37
28
  Return JSON only:
38
29
  {
39
- "findings": [{ "checkId": "check_id", "why": "one sentence", "proof": "exact POINTER" }],
40
- "summary": "one short sentence"
41
- }
42
-
43
- Rules:
44
- - Use only checks listed below.
45
- - checkId must be a check ID, never a POINTER.
46
- - proof must be one exact POINTER from candidate regions.
47
- - why describes the suspicious structure, not an identifier.
48
- - Do not describe an issue in summary unless it is also in findings.
49
- - If no findings, return { "findings": [], "summary": "No clear judgment-offload signal found." }.
50
-
51
- Allowed proof pointers:
52
- ${contexts.map((context) => `- ${context.pointer}`).join("\n")}
53
-
54
- ${formatFullChecks(checks)}
55
-
56
- SOURCE:
57
- ${sourceLabel}
58
-
59
- CANDIDATE REGIONS:
60
- ${contexts.map(formatContext).join("\n\n")}`;
61
- }
62
-
63
- export function semScoutPrompt(
64
- changeSet: SemChangeSet,
65
- checks: readonly StupifyCheck[],
66
- maxCandidates: number,
67
- ): string {
68
- return `Pick changed entity/check targets worth auditing.
69
- Return JSON only:
70
- { "targets": [{ "entityId": "exact entityId", "checkId": "check_id", "reason": "short scout reason" }] }
71
-
72
- Rules:
73
- - Use entityId values exactly as shown.
74
- - Each target has exactly one checkId.
75
- - Return at most ${maxCandidates} targets.
76
- - Return { "targets": [] } if clean.
77
- - Pick definitions over usage sites.
78
- - Prefer high recall, but do not attach unrelated checks.
79
-
80
- ${formatCompactChecks(checks)}
81
-
82
- SOURCE:
83
- ${changeSet.label}
84
-
85
- SEM CHANGE SUMMARY:
86
- ${JSON.stringify(changeSet.summary, null, 2)}
87
-
88
- SEM ENTITY CHANGES:
89
- ${changeSet.changes.map(formatSemChange).join("\n\n")}`;
90
- }
91
-
92
- export function findingsAuditPrompt(
93
- contexts: readonly SemContext[],
94
- pack: SemContextPack,
95
- checks: readonly StupifyCheck[],
96
- sourceLabel: string,
97
- promptName: AuditPromptName,
98
- ): string {
99
- const task =
100
- promptName === "high_bar"
101
- ? `You are Stupify's audit model.
102
- You are reviewing candidate/check targets for signs that AI-assisted coding may have replaced engineering judgment.
103
- Only emit a finding if it is clearly useful to a developer.
104
- A useful finding must:
105
- - match the target's check exactly
106
- - point to a concrete change pattern
107
- - explain why the change may reflect judgment-offload
108
- - avoid generic code-review commentary
109
- If the target is normal engineering work, omit it.
110
- If the target is merely plausible but not strong, omit it.
111
- If the target does not exactly match its assigned check, omit it.`
112
- : `You are Stupify's auditor.
113
- Audit only the listed target/check pairs.
114
- Emit only exceptions.`;
115
-
116
- const highBarRules =
117
- promptName === "high_bar"
118
- ? `- Prefer clean over weak.
119
- - Prefer no finding over generic finding.
120
- - Do not emit style feedback unless the assigned check is truly about style.
121
- - Do not turn functional refactors into style mismatch findings.`
122
- : "";
123
-
124
- return `${task}
125
- Return JSON only:
126
- {
127
- "findings": [
30
+ "matches": [
128
31
  {
129
32
  "targetId": "t001",
130
- "why": "one sentence",
33
+ "reason": "one sentence",
131
34
  "proof": "short pointer"
132
35
  }
133
- ],
134
- "uncertain": [
135
- {
136
- "targetId": "t002",
137
- "why": "one sentence"
138
- }
139
36
  ]
140
37
  }
141
38
 
142
39
  Rules:
143
- - Inspect every target.
144
- - Each target has exactly one check.
145
- - Emit a finding only when the target clearly matches its check.
146
- - Emit uncertain only when the target may match, but evidence is insufficient.
147
- - If a target is clean, emit nothing for it.
148
- - Omitted target means clean.
149
- - Do not output clean reviews.
150
- - Do not explain clean targets.
151
- - Do not write "no evidence" as a finding.
152
- - Do not put negative statements in findings.
153
- - Prefer omission over weak findings.
154
- - Use only provided targetIds.
155
- - Do not search for other checks.
40
+ - Use only targetIds from the input.
41
+ - Emit at most 5 matches.
42
+ - Prefer omission over a weak match.
156
43
  - Do not quote source code.
157
- - Use packed file context only as supporting evidence for these candidate entities.
158
- ${highBarRules}
159
-
160
- Targets:
161
- ${contexts.map((context) => formatAuditTarget(context, checks)).join("\n\n")}
44
+ - Do not write generic feedback.
45
+ - Do not emit "no evidence" or "does not apply."
46
+ - Proof must point to concrete changed product code that implements the pattern.
47
+ - Proof must not be a file header or start with "diff --git".
48
+ - Do not use pattern registry text, prompt text, docs, tests, or examples as proof.
49
+ - Do not treat pattern or prompt wording as the code being evaluated.
50
+ - Do not treat plain conditionals, guard clauses, skip paths, or error handling as indirection.
51
+ - For unnecessary_complexity, identify the exact new named abstraction in proof.
52
+ - If unnecessary_complexity proof would only be a file, hunk, or conditional block, omit it.
53
+ - If nothing clearly matches, return { "matches": [] }.
162
54
 
163
55
  SOURCE:
164
- ${sourceLabel}
165
-
166
- CANDIDATE ENTITY DELTAS:
167
- ${contexts.map(formatSemContext).join("\n\n")}
168
-
169
- PACKED FILE CONTEXT (${pack.provider}, ${pack.filePaths.length} files, ${pack.totalTokens} tokens):
170
- ${pack.text || "(none)"}`;
171
- }
56
+ ${input.changeSet.label}
172
57
 
173
- function formatCompactChecks(checks: readonly StupifyCheck[]): string {
174
- return `Checks:
175
- ${checks.map((check) => `- ${check.id}: ${check.lookFor.join("; ")}`).join("\n")}`;
176
- }
58
+ SEARCH TARGETS:
59
+ ${input.contexts.map((context) => formatSearchTarget(context, patternForContext(context, input.patterns), input.includeCounterReason)).join("\n\n") || "(none)"}
177
60
 
178
- function formatFullChecks(checks: readonly StupifyCheck[]): string {
179
- return checks.map(formatCheck).join("\n\n");
61
+ REPOMIX CONTEXT (${input.pack.filePaths.length} files, ${input.pack.totalTokens} tokens):
62
+ ${input.pack.text || "(none)"}`;
180
63
  }
181
64
 
182
- function formatCheck(check: StupifyCheck): string {
183
- return `# ${check.name}
184
- ID: ${check.id}
185
- Q: ${check.question}
65
+ function formatSearchPattern(check: StupifyCheck): string {
66
+ return `Pattern: ${check.id} (${check.name})
67
+ Question: ${check.searchPrompt ?? check.question}
186
68
  Look for:
187
69
  ${check.lookFor.map((signal) => `- ${signal}`).join("\n")}
188
70
  Ignore when:
189
71
  ${check.ignoreWhen.map((signal) => `- ${signal}`).join("\n")}
190
72
  Match examples:
191
- ${(check.examples?.match ?? []).map((example) => `- ${example}`).join("\n")}
192
- No-match examples:
193
- ${(check.examples?.noMatch ?? []).map((example) => `- ${example}`).join("\n")}`;
73
+ ${(check.searchExamples?.match ?? check.examples?.match ?? []).map((example) => `- ${example}`).join("\n")}
74
+ Non-match examples:
75
+ ${(check.searchExamples?.nonMatch ?? check.examples?.noMatch ?? []).map((example) => `- ${example}`).join("\n")}`;
194
76
  }
195
77
 
196
- function formatContext(context: CandidateContext): string {
197
- return `POINTER ${context.pointer}
198
- ${context.text}`;
199
- }
200
-
201
- function formatSemChange(change: SemChangeSet["changes"][number]): string {
202
- return `ENTITY ${change.entityId}
203
- TYPE ${change.entityType}
204
- CHANGE ${change.changeType}
205
- PATH ${change.filePath}`;
206
- }
207
-
208
- function formatSemContext(context: SemContext): string {
78
+ function formatSearchTarget(context: SemContext, pattern: StupifyCheck, includeCounterReason: boolean): string {
209
79
  return `TARGET ${context.targetId}
80
+ ASSIGNED ${formatSearchPattern(pattern)}
81
+ SEM TARGET:
210
82
  ENTITY ${context.entityId}
211
83
  NAME ${context.entityName}
212
84
  KIND ${context.entityKind}
213
85
  CHANGE ${context.changeKind}
214
- CHECK ${context.checkId}
215
- SCOUT_REASON ${context.reason}
216
- CONTEXT:
217
- ${context.text}`;
218
- }
219
-
220
- function formatAuditTarget(context: SemContext, checks: readonly StupifyCheck[]): string {
221
- const check = checks.find((item) => item.id === context.checkId);
222
- return `- targetId=${context.targetId} checkId=${context.checkId} entityId=${context.entityId}
223
- scoutReason=${context.reason}
224
- ${check ? formatCheck(check) : ""}`;
86
+ FILE ${context.filePath ?? "(unknown)"}
87
+ ${includeCounterReason ? `COUNTER_REASON ${context.reason}` : ""}`.trim();
225
88
  }
226
89
 
227
- function shortenCode(value: string | null): string {
228
- if (!value) return "(none)";
229
- const lines = value.split(/\r?\n/);
230
- const limit = 80;
231
- if (lines.length <= limit) return value;
232
- return `${lines.slice(0, limit).join("\n")}
233
- [stupify: sem entity content shortened after ${limit} lines]`;
90
+ function patternForContext(context: SemContext, patterns: readonly StupifyCheck[]): StupifyCheck {
91
+ return patterns.find((pattern) => pattern.id === context.checkId) ?? {
92
+ id: context.checkId,
93
+ name: context.checkId,
94
+ question: `Does this target match ${context.checkId}?`,
95
+ lookFor: [],
96
+ ignoreWhen: [],
97
+ };
234
98
  }