@stupify/cli 0.0.1 → 0.0.3

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 (63) hide show
  1. package/README.md +60 -0
  2. package/dist/analysis.d.ts +14 -0
  3. package/dist/analysis.js +276 -0
  4. package/dist/batcher.d.ts +3 -0
  5. package/dist/batcher.js +142 -0
  6. package/dist/cache.d.ts +2 -0
  7. package/dist/cache.js +59 -0
  8. package/dist/candidate-context.d.ts +2 -0
  9. package/dist/candidate-context.js +40 -0
  10. package/dist/checks.d.ts +3 -0
  11. package/dist/checks.js +131 -0
  12. package/dist/command.d.ts +2 -0
  13. package/dist/command.js +183 -0
  14. package/dist/constants.d.ts +4 -0
  15. package/dist/constants.js +53 -0
  16. package/dist/counter-scout.d.ts +14 -0
  17. package/dist/counter-scout.js +97 -0
  18. package/dist/diff.d.ts +1 -0
  19. package/dist/diff.js +10 -0
  20. package/dist/experiment.d.ts +1 -0
  21. package/dist/experiment.js +225 -0
  22. package/dist/git.d.ts +8 -0
  23. package/dist/git.js +219 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/model.d.ts +24 -0
  27. package/dist/model.js +281 -0
  28. package/dist/prompts.d.ts +5 -0
  29. package/dist/prompts.js +197 -0
  30. package/dist/render.d.ts +3 -0
  31. package/dist/render.js +101 -0
  32. package/dist/repomix-provider.d.ts +4 -0
  33. package/dist/repomix-provider.js +145 -0
  34. package/dist/sem-provider.d.ts +2 -0
  35. package/dist/sem-provider.js +221 -0
  36. package/dist/stupify.d.ts +2 -0
  37. package/dist/stupify.js +387 -0
  38. package/dist/trace.d.ts +29 -0
  39. package/dist/trace.js +64 -0
  40. package/dist/types.d.ts +236 -0
  41. package/dist/types.js +6 -0
  42. package/package.json +42 -5
  43. package/src/analysis.ts +408 -0
  44. package/src/batcher.ts +198 -0
  45. package/src/cache.ts +65 -0
  46. package/src/candidate-context.ts +43 -0
  47. package/src/checks.ts +132 -0
  48. package/src/command.ts +218 -0
  49. package/src/constants.ts +56 -0
  50. package/src/counter-scout.ts +119 -0
  51. package/src/diff.ts +9 -0
  52. package/src/experiment.ts +317 -0
  53. package/src/git.ts +228 -0
  54. package/src/index.ts +1 -0
  55. package/src/model.ts +360 -0
  56. package/src/prompts.ts +234 -0
  57. package/src/render.ts +107 -0
  58. package/src/repomix-provider.ts +163 -0
  59. package/src/sem-provider.ts +255 -0
  60. package/src/stupify.ts +598 -0
  61. package/src/trace.ts +103 -0
  62. package/src/types.ts +264 -0
  63. package/bin/stupify.mjs +0 -3
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @stupify/cli
2
+
3
+ Local-only diagnostic CLI for checking whether AI is making you dumber.
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.
9
+
10
+ ```sh
11
+ npx @stupify/cli --commit HEAD
12
+ ```
13
+
14
+ ```sh
15
+ npx @stupify/cli --engine sem --commit HEAD
16
+ ```
17
+
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.
24
+
25
+ ```sh
26
+ stupify experiment experiments/bevyl-last-week.json
27
+ stupify experiment experiments/bevyl-per-check.json
28
+ ```
29
+
30
+ The experiment runner shells out to the CLI and writes local JSON plus
31
+ manual-label markdown files under `experiments/results/`.
32
+
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.
40
+
41
+ ```sh
42
+ npx @stupify/cli --commits 20
43
+ ```
44
+
45
+ Recent-commits mode analyzes the selected range as one change. Findings are
46
+ range-level for now, not per-commit blame.
47
+
48
+ ```sh
49
+ git diff HEAD~1..HEAD | npx @stupify/cli --stdin
50
+ ```
51
+
52
+ ```sh
53
+ stupify --help
54
+ ```
55
+
56
+ The package is prepared for the public `@stupify` npm scope. Publishing should
57
+ run the TypeScript build first so the executable points at `dist/stupify.js`.
58
+
59
+ This iteration intentionally does not compare baselines, share data, call hosted
60
+ LLM APIs, integrate with Ollama, or scan the whole repo.
@@ -0,0 +1,14 @@
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<{
11
+ prompt: string;
12
+ schema: unknown;
13
+ }>;
14
+ export declare function countPromptTokens(model: LocalModel, prompt: string): Promise<number>;
@@ -0,0 +1,276 @@
1
+ import { auditPrompt, findingsAuditPrompt, scoutPrompt, semScoutPrompt } from "./prompts.js";
2
+ 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
+ }
25
+ const raw = await runJsonPrompt(model, request.prompt, request.schema, 0);
26
+ return uncheckedFindingsAuditResult(raw, changeSet.id, contexts);
27
+ }
28
+ export function findingsAuditRequest(changeSet, contexts, pack, checks, promptName = "strict") {
29
+ return {
30
+ prompt: findingsAuditPrompt(contexts, pack, checks, changeSet.label, promptName),
31
+ schema: findingsAuditSchema(contexts),
32
+ };
33
+ }
34
+ export async function countPromptTokens(model, prompt) {
35
+ const cached = await cachedJson("prompt-tokens", fingerprint({
36
+ version: 1,
37
+ modelId: model.id,
38
+ profile: model.profile,
39
+ prompt,
40
+ }), async () => {
41
+ const response = await fetch(`${model.baseUrl}/tokenize`, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify({ content: prompt }),
45
+ });
46
+ if (!response.ok) {
47
+ throw new Error(`llama-server tokenize failed: HTTP ${response.status} ${await response.text()}`);
48
+ }
49
+ const body = await response.json();
50
+ if (!Array.isArray(body.tokens))
51
+ throw new Error("llama-server tokenize returned no tokens.");
52
+ return { count: body.tokens.length };
53
+ });
54
+ return cached.count;
55
+ }
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) {
119
+ return {
120
+ type: "object",
121
+ properties: {
122
+ targets: {
123
+ type: "array",
124
+ maxItems: maxCandidates,
125
+ items: {
126
+ type: "object",
127
+ properties: {
128
+ entityId: { type: "string", enum: changeSet.changes.map((change) => change.entityId) },
129
+ checkId: { type: "string", enum: checks.map((check) => check.id) },
130
+ reason: { type: "string" },
131
+ },
132
+ required: ["entityId", "checkId", "reason"],
133
+ additionalProperties: false,
134
+ },
135
+ },
136
+ },
137
+ required: ["targets"],
138
+ additionalProperties: false,
139
+ };
140
+ }
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) {
169
+ 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
+ }));
188
+ });
189
+ }
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
+ async function runJsonPrompt(model, prompt, schema, temperature) {
222
+ return cachedJson("model-json", fingerprint({
223
+ version: 1,
224
+ modelId: model.id,
225
+ profile: model.profile,
226
+ prompt,
227
+ schema,
228
+ temperature,
229
+ }), () => runJsonPromptUncached(model, prompt, schema, temperature));
230
+ }
231
+ async function runJsonPromptUncached(model, prompt, schema, temperature) {
232
+ const first = await complete(model, prompt, schema, temperature);
233
+ const parsed = parseJson(first);
234
+ if (parsed.ok)
235
+ return parsed.value;
236
+ const retry = await complete(model, `${prompt}
237
+
238
+ Your previous response was not valid JSON. Return the requested JSON object only.`, schema, temperature);
239
+ const retryParsed = parseJson(retry);
240
+ if (retryParsed.ok)
241
+ return retryParsed.value;
242
+ console.error("Raw model output:");
243
+ console.error(retry);
244
+ throw new Error("Model returned invalid JSON.");
245
+ }
246
+ async function complete(model, prompt, schema, temperature) {
247
+ const response = await fetch(`${model.baseUrl}/v1/chat/completions`, {
248
+ method: "POST",
249
+ headers: { "content-type": "application/json" },
250
+ body: JSON.stringify({
251
+ model: model.id,
252
+ messages: [{ role: "user", content: prompt }],
253
+ temperature,
254
+ response_format: {
255
+ type: "json_object",
256
+ schema,
257
+ },
258
+ }),
259
+ });
260
+ if (!response.ok)
261
+ throw new Error(`llama-server request failed: HTTP ${response.status} ${await response.text()}`);
262
+ const body = await response.json();
263
+ const content = body.choices?.[0]?.message?.content;
264
+ if (typeof content !== "string")
265
+ throw new Error("llama-server returned no message content.");
266
+ return content;
267
+ }
268
+ function parseJson(raw) {
269
+ try {
270
+ const value = JSON.parse(raw);
271
+ return { ok: true, value };
272
+ }
273
+ catch {
274
+ return { ok: false };
275
+ }
276
+ }
@@ -0,0 +1,3 @@
1
+ import type { DiffBatch, DiffHunk } from "./types.ts";
2
+ export declare function batchDiff(diff: string, linesPerBatch?: number): readonly DiffBatch[];
3
+ export declare function allHunks(batches: readonly DiffBatch[]): readonly DiffHunk[];
@@ -0,0 +1,142 @@
1
+ const DEFAULT_BATCH_LINES = 1_000;
2
+ export function batchDiff(diff, linesPerBatch = DEFAULT_BATCH_LINES) {
3
+ const hunks = parseHunks(diff).flatMap((item) => splitLargeHunk(item, linesPerBatch));
4
+ const initialState = {
5
+ batches: [],
6
+ current: [],
7
+ currentLines: 0,
8
+ batchNumber: 1,
9
+ };
10
+ const finalized = hunks.reduce((state, hunk) => {
11
+ const needsFlush = state.current.length > 0 &&
12
+ state.currentLines + hunk.lineCount > linesPerBatch;
13
+ const flushed = needsFlush ? flush(state) : state;
14
+ return {
15
+ ...flushed,
16
+ current: [...flushed.current, toDiffHunk(hunk, flushed.batchNumber)],
17
+ currentLines: flushed.currentLines + hunk.lineCount,
18
+ };
19
+ }, initialState);
20
+ return finalized.current.length > 0
21
+ ? [...finalized.batches, toBatch(finalized.batchNumber, finalized.current)]
22
+ : finalized.batches;
23
+ function flush(state) {
24
+ return {
25
+ batches: [...state.batches, toBatch(state.batchNumber, state.current)],
26
+ current: [],
27
+ currentLines: 0,
28
+ batchNumber: state.batchNumber + 1,
29
+ };
30
+ }
31
+ }
32
+ export function allHunks(batches) {
33
+ return batches.flatMap((batch) => batch.hunks);
34
+ }
35
+ function parseHunks(diff) {
36
+ const lines = diff.split(/\r?\n/);
37
+ const initialState = {
38
+ hunks: [],
39
+ filePath: "unknown",
40
+ fileIndex: 0,
41
+ hunkIndex: 0,
42
+ fileHeader: [],
43
+ hunkLines: null,
44
+ };
45
+ const finalState = lines.reduce((state, line) => {
46
+ const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
47
+ if (fileMatch) {
48
+ const flushed = flush(state);
49
+ return {
50
+ ...flushed,
51
+ fileIndex: flushed.fileIndex + 1,
52
+ hunkIndex: 0,
53
+ filePath: fileMatch[1],
54
+ fileHeader: [line],
55
+ hunkLines: null,
56
+ };
57
+ }
58
+ if (line.startsWith("@@ ")) {
59
+ const flushed = flush(state);
60
+ return {
61
+ ...flushed,
62
+ hunkIndex: flushed.hunkIndex + 1,
63
+ hunkLines: [...flushed.fileHeader, line],
64
+ };
65
+ }
66
+ if (state.hunkLines)
67
+ return { ...state, hunkLines: [...state.hunkLines, line] };
68
+ if (state.fileHeader.length > 0)
69
+ return { ...state, fileHeader: [...state.fileHeader, line] };
70
+ return state;
71
+ }, initialState);
72
+ return flush(finalState).hunks;
73
+ function flush(state) {
74
+ if (!state.hunkLines)
75
+ return state;
76
+ const fileId = `file-${pad(state.fileIndex)}`;
77
+ const hunkId = `hunk-${pad(state.hunkIndex)}`;
78
+ const text = state.hunkLines.join("\n").trimEnd();
79
+ const nextHunk = {
80
+ fileId,
81
+ hunkId,
82
+ filePath: state.filePath,
83
+ text,
84
+ lineCount: countLines(text),
85
+ };
86
+ return { ...state, hunks: [...state.hunks, nextHunk], hunkLines: null };
87
+ }
88
+ }
89
+ function splitLargeHunk(hunk, linesPerBatch) {
90
+ if (hunk.lineCount <= linesPerBatch)
91
+ return [hunk];
92
+ const lines = hunk.text.split(/\r?\n/);
93
+ const chunkCount = Math.ceil(lines.length / linesPerBatch);
94
+ return Array.from({ length: chunkCount }, (_, chunkIndex) => {
95
+ const start = chunkIndex * linesPerBatch;
96
+ const text = lines.slice(start, start + linesPerBatch).join("\n");
97
+ return {
98
+ ...hunk,
99
+ hunkId: `${hunk.hunkId}-part-${pad(chunkIndex + 1)}`,
100
+ text,
101
+ lineCount: countLines(text),
102
+ };
103
+ });
104
+ }
105
+ function toDiffHunk(hunk, batchNumber) {
106
+ const batchId = `batch-${pad(batchNumber)}`;
107
+ return {
108
+ ...hunk,
109
+ batchId,
110
+ pointer: `${batchId}:${hunk.fileId}:${hunk.hunkId}`,
111
+ };
112
+ }
113
+ function toBatch(batchNumber, hunks) {
114
+ const id = `batch-${pad(batchNumber)}`;
115
+ return {
116
+ id,
117
+ hunks,
118
+ text: hunks.map(formatHunkForSearch).join("\n\n"),
119
+ };
120
+ }
121
+ function formatHunkForSearch(hunk) {
122
+ return `POINTER ${hunk.pointer}
123
+ FILE ${hunk.fileId}
124
+ PATH ${hunk.filePath}
125
+ ${searchView(hunk.text)}`;
126
+ }
127
+ function searchView(text) {
128
+ return text
129
+ .split(/\r?\n/)
130
+ .filter((line) => line.startsWith("diff --git ") ||
131
+ line.startsWith("--- ") ||
132
+ line.startsWith("+++ ") ||
133
+ line.startsWith("@@ ") ||
134
+ (line.startsWith("+") && !line.startsWith("+++")))
135
+ .join("\n");
136
+ }
137
+ function countLines(value) {
138
+ return value.length === 0 ? 0 : value.split(/\r?\n/).length;
139
+ }
140
+ function pad(value) {
141
+ return String(value).padStart(3, "0");
142
+ }
@@ -0,0 +1,2 @@
1
+ export declare function fingerprint(value: unknown): string;
2
+ export declare function cachedJson<T>(namespace: string, key: string, compute: () => Promise<T>): Promise<T>;
package/dist/cache.js ADDED
@@ -0,0 +1,59 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { homedir, platform } from "node:os";
4
+ import path from "node:path";
5
+ export function fingerprint(value) {
6
+ const text = typeof value === "string" ? value : stableStringify(value);
7
+ return createHash("sha256").update(text).digest("hex");
8
+ }
9
+ export async function cachedJson(namespace, key, compute) {
10
+ const filePath = cachePath(namespace, key);
11
+ try {
12
+ const value = JSON.parse(await readFile(filePath, "utf8"));
13
+ console.error(`cache hit ${namespace} ${key.slice(0, 12)}`);
14
+ return value;
15
+ }
16
+ catch {
17
+ console.error(`cache miss ${namespace} ${key.slice(0, 12)}`);
18
+ }
19
+ const value = await compute();
20
+ await writeCache(filePath, value).catch(() => undefined);
21
+ return value;
22
+ }
23
+ function cachePath(namespace, key) {
24
+ return path.join(cacheRoot(), "intermediate-v1", safeNamespace(namespace), `${key}.json`);
25
+ }
26
+ async function writeCache(filePath, value) {
27
+ await mkdir(path.dirname(filePath), { recursive: true });
28
+ const tempPath = `${filePath}.${process.pid}.tmp`;
29
+ try {
30
+ await writeFile(tempPath, JSON.stringify(value), "utf8");
31
+ await rename(tempPath, filePath);
32
+ }
33
+ catch (error) {
34
+ await rm(tempPath, { force: true });
35
+ throw error;
36
+ }
37
+ }
38
+ function cacheRoot() {
39
+ if (process.env.STUPIFY_CACHE_DIR)
40
+ return process.env.STUPIFY_CACHE_DIR;
41
+ if (process.env.XDG_CACHE_HOME)
42
+ return path.join(process.env.XDG_CACHE_HOME, "stupify");
43
+ if (platform() === "darwin")
44
+ return path.join(homedir(), "Library", "Caches", "stupify");
45
+ if (platform() === "win32" && process.env.LOCALAPPDATA)
46
+ return path.join(process.env.LOCALAPPDATA, "stupify", "Cache");
47
+ return path.join(homedir(), ".cache", "stupify");
48
+ }
49
+ function safeNamespace(value) {
50
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
51
+ }
52
+ function stableStringify(value) {
53
+ if (value === null || typeof value !== "object")
54
+ return JSON.stringify(value);
55
+ if (Array.isArray(value))
56
+ return `[${value.map(stableStringify).join(",")}]`;
57
+ const record = value;
58
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
59
+ }
@@ -0,0 +1,2 @@
1
+ import type { CandidateContext, DiffBatch } from "./types.ts";
2
+ export declare function candidateContexts(batches: readonly DiffBatch[], candidatePointers: readonly string[]): readonly CandidateContext[];
@@ -0,0 +1,40 @@
1
+ import { allHunks } from "./batcher.js";
2
+ const MAX_CONTEXT_LINES = 80;
3
+ export function candidateContexts(batches, candidatePointers) {
4
+ const hunks = allHunks(batches);
5
+ const byPointer = new Map(hunks.map((hunk) => [hunk.pointer, hunk]));
6
+ const uniquePointers = [...new Set(candidatePointers)]
7
+ .sort((left, right) => hunkPriority(byPointer.get(right)) - hunkPriority(byPointer.get(left)));
8
+ return uniquePointers.flatMap((pointer) => {
9
+ const hunk = byPointer.get(pointer);
10
+ if (!hunk)
11
+ return [];
12
+ return [{ pointer, text: formatHunk(hunk) }];
13
+ });
14
+ }
15
+ function formatHunk(hunk) {
16
+ return `PATH ${hunk.filePath}
17
+ ${shorten(hunk.text)}`;
18
+ }
19
+ function shorten(text) {
20
+ const lines = text.split(/\r?\n/);
21
+ if (lines.length <= MAX_CONTEXT_LINES)
22
+ return text;
23
+ return `${lines.slice(0, MAX_CONTEXT_LINES).join("\n")}
24
+ [stupify: hunk shortened after ${MAX_CONTEXT_LINES} lines]`;
25
+ }
26
+ function hunkPriority(hunk) {
27
+ if (!hunk)
28
+ return 0;
29
+ const text = hunk.text;
30
+ let priority = 0;
31
+ if (/^\+export\s+type\s|\+export\s+interface\s|\+type\s|\+interface\s/m.test(text))
32
+ priority += 3;
33
+ if (/^\+export\s+function\s|\+function\s/m.test(text))
34
+ priority += 2;
35
+ if (/\.map\(|=>\s*\(\{|=>\s*\{/m.test(text))
36
+ priority += 2;
37
+ if (/payload|schema|dto|response|result/i.test(text))
38
+ priority += 1;
39
+ return priority;
40
+ }
@@ -0,0 +1,3 @@
1
+ import { type StupifyCheck } from "./types.ts";
2
+ export declare const defaultChecks: readonly StupifyCheck[];
3
+ export declare function enabledChecks(checkIds: readonly string[] | null): readonly StupifyCheck[];