@mrclrchtr/supi-flow 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,161 +0,0 @@
1
- #!/usr/bin/env -S pnpm exec jiti
2
- /**
3
- * Vocabulary marker scanner — scans markdown files for AI-prose vocabulary markers.
4
- *
5
- * Reads vocabulary definitions from `../references/vocabulary.json` — the single
6
- * source of truth shared with SKILL.md documentation tables.
7
- *
8
- * Usage:
9
- * pnpm exec jiti scripts/slop-scan-vocab.ts <file> [<file>...]
10
- *
11
- * Cross-platform Node.js/TypeScript — runs wherever pi runs.
12
- * Output: JSON array with one result per file.
13
- */
14
-
15
- import { readFileSync } from "node:fs";
16
- import { outputJSON, stripCodeBlocks, stripInlineCode } from "./slop-helpers.ts";
17
-
18
- /** Resolve path relative to this script file. jiti provides __dirname at runtime. */
19
- function resolveAdjacent(filename: string): string {
20
- const dir = (typeof __dirname !== "undefined" ? __dirname : "").replace(/\+$/, "");
21
- return `${dir}/${filename}`;
22
- }
23
-
24
- interface VocabEntry {
25
- term: string;
26
- score: number;
27
- }
28
-
29
- interface VocabData {
30
- tier1: VocabEntry[];
31
- tier2: VocabEntry[];
32
- tier3: VocabEntry[];
33
- tier4: VocabEntry[];
34
- }
35
-
36
- const vocabulary: VocabData = JSON.parse(
37
- readFileSync(resolveAdjacent("../references/vocabulary.json"), "utf-8"),
38
- );
39
-
40
- /** Build flat list with tier annotation. */
41
- function allVocab(): Array<VocabEntry & { tier: 1 | 2 | 3 | 4 }> {
42
- return [
43
- ...vocabulary.tier1.map((e) => ({ ...e, tier: 1 as const })),
44
- ...vocabulary.tier2.map((e) => ({ ...e, tier: 2 as const })),
45
- ...vocabulary.tier3.map((e) => ({ ...e, tier: 3 as const })),
46
- ...vocabulary.tier4.map((e) => ({ ...e, tier: 4 as const })),
47
- ];
48
- }
49
-
50
- interface VocabHit {
51
- term: string;
52
- tier: number;
53
- score: number;
54
- count: number;
55
- context?: string;
56
- }
57
-
58
- interface VocabResult {
59
- file: string;
60
- wordCount: number;
61
- totalScore: number;
62
- normalizedScore: number;
63
- tierScores: { tier1: number; tier2: number; tier3: number; tier4: number };
64
- hits: VocabHit[];
65
- rating: "clean" | "light" | "moderate" | "heavy";
66
- recommendation: string;
67
- }
68
-
69
- function rate(normalizedScore: number): Pick<VocabResult, "rating" | "recommendation"> {
70
- if (normalizedScore <= 1.0) {
71
- return { rating: "clean", recommendation: "No action needed — vocabulary is clean." };
72
- }
73
- if (normalizedScore <= 2.5) {
74
- return {
75
- rating: "light",
76
- recommendation: "Spot remediation — fix individual markers found above.",
77
- };
78
- }
79
- if (normalizedScore <= 5.0) {
80
- return {
81
- rating: "moderate",
82
- recommendation: "Section rewrite recommended — review flagged areas.",
83
- };
84
- }
85
- return {
86
- rating: "heavy",
87
- recommendation: "Full document review — do not commit without addressing flagged markers.",
88
- };
89
- }
90
-
91
- /** Escape special regex characters in a string. */
92
- function escapeRegex(s: string): string {
93
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
94
- }
95
-
96
- /** Read a file as UTF-8 string. */
97
- function readFile(path: string): string {
98
- return readFileSync(path, "utf-8");
99
- }
100
-
101
- /** Scan a single file for vocabulary markers. */
102
- function scanFile(filePath: string): VocabResult {
103
- const content = readFile(filePath);
104
- // Strip code blocks and inline code to avoid false positives from quoted terms
105
- const prose = stripInlineCode(stripCodeBlocks(content));
106
- const lowerContent = prose.toLowerCase();
107
- const wordCount = prose.split(/[\s\n]+/).filter((w) => w.length > 0).length;
108
-
109
- const hits: VocabHit[] = [];
110
- let totalScore = 0;
111
-
112
- for (const entry of allVocab()) {
113
- const pattern = escapeRegex(entry.term.toLowerCase());
114
- const re = new RegExp(pattern, "gi");
115
- const matches = [...lowerContent.matchAll(re)];
116
-
117
- if (matches.length === 0) continue;
118
-
119
- const count = matches.length;
120
- totalScore += count * entry.score;
121
-
122
- // Context snippet around first occurrence (uses prose indices)
123
- const idx = matches[0].index;
124
- const start = Math.max(0, idx - 30);
125
- const end = Math.min(prose.length, idx + entry.term.length + 30);
126
- const context = prose.slice(start, end).replace(/\n/g, " ").trim();
127
-
128
- hits.push({ term: entry.term, tier: entry.tier, score: entry.score, count, context });
129
- }
130
-
131
- hits.sort((a, b) => b.count * b.score - a.count * a.score);
132
-
133
- const tierScores = {
134
- tier1: hits.filter((h) => h.tier === 1).reduce((s, h) => s + h.count * h.score, 0),
135
- tier2: hits.filter((h) => h.tier === 2).reduce((s, h) => s + h.count * h.score, 0),
136
- tier3: hits.filter((h) => h.tier === 3).reduce((s, h) => s + h.count * h.score, 0),
137
- tier4: hits.filter((h) => h.tier === 4).reduce((s, h) => s + h.count * h.score, 0),
138
- };
139
-
140
- const normalizedScore = wordCount > 0 ? (totalScore / wordCount) * 100 : 0;
141
-
142
- return {
143
- file: filePath,
144
- wordCount,
145
- totalScore,
146
- normalizedScore: Math.round(normalizedScore * 100) / 100,
147
- tierScores,
148
- hits,
149
- ...rate(normalizedScore),
150
- };
151
- }
152
-
153
- // --- CLI ---
154
- const files = process.argv.slice(2);
155
- if (files.length === 0) {
156
- process.stderr.write("Usage: pnpm exec jiti scripts/slop-scan-vocab.ts <file> [<file>...]\n");
157
- process.exit(1);
158
- }
159
-
160
- const results = files.map(scanFile);
161
- outputJSON(results);
@@ -1,209 +0,0 @@
1
- #!/usr/bin/env -S pnpm exec jiti
2
- /**
3
- * Combined slop scanner — runs vocabulary + structural detection and produces
4
- * the final density score matching the formula in SKILL.md.
5
- *
6
- * Usage:
7
- * pnpm exec jiti scripts/slop-scan.ts <file> [<file>...]
8
- * pnpm exec jiti scripts/slop-scan.ts <file> --json-only # raw JSON, no summary
9
- *
10
- * Cross-platform Node.js/TypeScript — runs wherever pi runs.
11
- * Output: Human-readable summary (default) or JSON (with --json-only).
12
- */
13
-
14
- import { execFileSync } from "node:child_process";
15
- import { type DocProfile, outputJSON } from "./slop-helpers.ts";
16
-
17
- interface VocabResult {
18
- file: string;
19
- wordCount: number;
20
- totalScore: number;
21
- normalizedScore: number;
22
- tierScores: { tier1: number; tier2: number; tier3: number; tier4: number };
23
- hits: Array<{ term: string; tier: number; score: number; count: number; context?: string }>;
24
- rating: string;
25
- recommendation: string;
26
- }
27
-
28
- interface StructuralResult {
29
- file: string;
30
- profile: DocProfile;
31
- adjustments: string[];
32
- wordCount: number;
33
- metrics: {
34
- emDashDensity: number;
35
- bulletRatio: number;
36
- participialTails: number;
37
- participialTailsPer500: number;
38
- arrowConnectors: number;
39
- technicalArrowConnectors: number;
40
- proseArrowConnectors: number;
41
- correlativePairs: number;
42
- plusSigns: number;
43
- colons: number;
44
- semicolons: number;
45
- sentenceClusterRatio: number;
46
- fromToRanges: number;
47
- emojiBullets: number;
48
- introBodyConclusion: boolean;
49
- conclusionMirroring: boolean;
50
- paragraphUniformity: number;
51
- };
52
- structuralScore: number;
53
- flags: string[];
54
- }
55
-
56
- interface CombinedReport {
57
- file: string;
58
- profile: DocProfile;
59
- adjustments: string[];
60
- wordCount: number;
61
- vocabScore: number;
62
- structuralScore: number;
63
- finalScore: number;
64
- rating: "clean" | "light" | "moderate" | "heavy";
65
- recommendation: string;
66
- vocab: VocabResult;
67
- structural: StructuralResult;
68
- }
69
-
70
- /** Get the directory of the currently running script. */
71
- function scriptDir(): string {
72
- const dir = (typeof __dirname !== "undefined" ? __dirname : "").replace(/\\+$/, "");
73
- return `${dir}/`;
74
- }
75
-
76
- function siblingPath(name: string): string {
77
- return `${scriptDir()}${name}`;
78
- }
79
-
80
- function runScript(script: string, files: string[]): string {
81
- return execFileSync("pnpm", ["exec", "jiti", script, ...files], {
82
- encoding: "utf-8",
83
- stdio: ["pipe", "pipe", "pipe"],
84
- });
85
- }
86
-
87
- function rate(finalScore: number): { rating: CombinedReport["rating"]; recommendation: string } {
88
- if (finalScore <= 1.0) return { rating: "clean", recommendation: "No action needed." };
89
- if (finalScore <= 2.5)
90
- return { rating: "light", recommendation: "Spot remediation — fix individual markers." };
91
- if (finalScore <= 5.0)
92
- return { rating: "moderate", recommendation: "Section rewrite recommended." };
93
- return {
94
- rating: "heavy",
95
- recommendation: "Full document review — do not commit without fixing.",
96
- };
97
- }
98
-
99
- function scanFile(filePath: string): CombinedReport {
100
- const vocabOut = runScript(siblingPath("slop-scan-vocab.ts"), [filePath]);
101
- const structOut = runScript(siblingPath("slop-scan-structural.ts"), [filePath]);
102
-
103
- const vocabResults = JSON.parse(vocabOut) as VocabResult[];
104
- const structResults = JSON.parse(structOut) as StructuralResult[];
105
-
106
- const vocab = vocabResults[0];
107
- const structural = structResults[0];
108
- const finalScore = Math.min(vocab.normalizedScore + structural.structuralScore, 10);
109
-
110
- return {
111
- file: filePath,
112
- profile: structural.profile,
113
- adjustments: structural.adjustments,
114
- wordCount: vocab.wordCount,
115
- vocabScore: vocab.normalizedScore,
116
- structuralScore: structural.structuralScore,
117
- finalScore: Math.round(finalScore * 100) / 100,
118
- vocab,
119
- structural,
120
- ...rate(finalScore),
121
- };
122
- }
123
-
124
- // --- CLI ---
125
- const args = process.argv.slice(2);
126
- const jsonOnly = args.includes("--json-only");
127
- const files = args.filter((a) => a !== "--json-only");
128
-
129
- if (files.length === 0) {
130
- console.error("Usage: pnpm exec jiti scripts/slop-scan.ts <file> [<file>...] [--json-only]");
131
- process.exit(1);
132
- }
133
-
134
- const results = files.map(scanFile);
135
-
136
- if (jsonOnly) {
137
- outputJSON(results);
138
- } else {
139
- console.log("╔══════════════════════════════════════════════════╗");
140
- console.log("║ Slop Detection Scan Report ║");
141
- console.log("╚══════════════════════════════════════════════════╝");
142
- console.log();
143
-
144
- for (const r of results) {
145
- console.log(`File: ${r.file}`);
146
- console.log(`Profile: ${r.profile}`);
147
- console.log(`Words: ${r.wordCount}`);
148
- if (r.adjustments.length > 0) {
149
- console.log(`Adjustments: ${r.adjustments.join(", ")}`);
150
- }
151
- console.log("───");
152
- console.log(` Vocab score: ${r.vocabScore.toFixed(2)} (per 100 words)`);
153
- console.log(` Structural score: ${r.structuralScore} (penalty points)`);
154
- console.log(` Final score: ${r.finalScore.toFixed(2)}`);
155
-
156
- const barLen = Math.round(r.finalScore);
157
- const bar = "█".repeat(Math.min(barLen, 10)) + "░".repeat(Math.max(0, 10 - barLen));
158
- console.log(` [${bar}]`);
159
-
160
- let statusColor = "🟢";
161
- if (r.rating === "light") statusColor = "🟡";
162
- else if (r.rating === "moderate") statusColor = "🟠";
163
- else if (r.rating === "heavy") statusColor = "🔴";
164
-
165
- console.log(` ${statusColor} ${r.rating.toUpperCase()} — ${r.recommendation}`);
166
- console.log();
167
-
168
- if (r.vocab.hits.length > 0) {
169
- console.log(" Vocabulary markers found:");
170
- for (const hit of r.vocab.hits.slice(0, 10)) {
171
- console.log(
172
- ` • Tier ${hit.tier} "${hit.term}" ×${hit.count} (score: ${hit.count * hit.score})`,
173
- );
174
- }
175
- if (r.vocab.hits.length > 10) {
176
- console.log(` ... and ${r.vocab.hits.length - 10} more`);
177
- }
178
- console.log();
179
- }
180
-
181
- if (r.structural.flags.length > 0) {
182
- console.log(" Structural flags:");
183
- for (const flag of r.structural.flags) {
184
- console.log(` • ${flag}`);
185
- }
186
- console.log();
187
- }
188
-
189
- console.log(" Structural metrics:");
190
- const m = r.structural.metrics;
191
- console.log(` Em dash density: ${m.emDashDensity.toFixed(1)}/1000 words`);
192
- console.log(` Sentence clustering: ${(m.sentenceClusterRatio * 100).toFixed(0)}%`);
193
- console.log(` Bullet ratio: ${(m.bulletRatio * 100).toFixed(0)}%`);
194
- console.log(` Paragraph uniformity: ${(m.paragraphUniformity * 100).toFixed(0)}%`);
195
- console.log(` Participial tails: ${m.participialTailsPer500.toFixed(1)}/500 words`);
196
- console.log(` Correlative pairs: ${m.correlativePairs}`);
197
- console.log(` Arrow connectors: ${m.arrowConnectors}`);
198
- console.log(` Technical arrow chains: ${m.technicalArrowConnectors}`);
199
- console.log(` Prose arrow shorthand: ${m.proseArrowConnectors}`);
200
- console.log(` Plus-sign conjunctions: ${m.plusSigns}`);
201
- console.log(` Emoji bullets: ${m.emojiBullets}`);
202
- console.log(` Colons vs semicolons: ${m.colons} / ${m.semicolons}`);
203
- console.log(` From→To ranges: ${m.fromToRanges}`);
204
- console.log(` Intro-body-conclusion: ${m.introBodyConclusion}`);
205
- console.log(` Conclusion mirroring: ${m.conclusionMirroring}`);
206
- console.log("───");
207
- console.log();
208
- }
209
- }