oh-my-opencode 5.1.54 → 5.1.56

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 (89) hide show
  1. package/dist/cli/index.js +29 -18
  2. package/dist/cli-node/index.js +29 -18
  3. package/dist/config/schema/agent-overrides.d.ts +64 -0
  4. package/dist/config/schema/categories.d.ts +8 -0
  5. package/dist/config/schema/oh-my-opencode-config.d.ts +64 -0
  6. package/dist/index.js +120 -48
  7. package/dist/oh-my-opencode.schema.json +112 -0
  8. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  9. package/dist/skills/programming/SKILL.md +2 -0
  10. package/dist/skills/programming/references/testing/README.md +2 -2
  11. package/dist/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
  12. package/dist/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
  13. package/dist/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
  14. package/dist/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
  15. package/dist/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
  16. package/dist/skills/remove-ai-slops/SKILL.md +32 -20
  17. package/dist/skills/ulw-research/scripts/source-commands.mjs +3 -1
  18. package/dist/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
  19. package/dist/tools/delegate-task/models-distribution.d.ts +1 -0
  20. package/dist/tui.js +28 -17
  21. package/package.json +16 -14
  22. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  23. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
  24. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  29. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  33. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  34. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  40. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  46. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  68. package/packages/omo-codex/plugin/package-lock.json +12 -12
  69. package/packages/omo-codex/plugin/package.json +1 -1
  70. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  71. package/packages/omo-codex/plugin/skills/programming/SKILL.md +2 -0
  72. package/packages/omo-codex/plugin/skills/programming/references/testing/README.md +2 -2
  73. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
  74. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
  75. package/packages/omo-codex/plugin/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
  76. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +32 -20
  77. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +3 -1
  78. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
  79. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  80. package/packages/shared-skills/skills/programming/SKILL.md +2 -0
  81. package/packages/shared-skills/skills/programming/references/testing/README.md +2 -2
  82. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk-rules.ts +140 -0
  83. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.test.ts +125 -0
  84. package/packages/shared-skills/skills/programming/scripts/typescript/check-test-junk.ts +148 -0
  85. package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.test.ts +43 -0
  86. package/packages/shared-skills/skills/programming/scripts/typescript/mutation-proof.ts +179 -0
  87. package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +32 -20
  88. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +3 -1
  89. package/packages/shared-skills/skills/ulw-research/scripts/source-integrity.test.ts +39 -2
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.1.54) Loading Project Rules",
10
+ "statusMessage": "(OmO 5.1.56) Loading Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook user-prompt-submit"
12
12
  }
13
13
  ]
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/omo-codex-plugin",
3
- "version": "5.1.54",
3
+ "version": "5.1.56",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@sisyphuslabs/omo-codex-plugin",
9
- "version": "5.1.54",
9
+ "version": "5.1.56",
10
10
  "workspaces": [
11
11
  "components/comment-checker",
12
12
  "components/git-bash",
@@ -100,7 +100,7 @@
100
100
  },
101
101
  "components/comment-checker": {
102
102
  "name": "@code-yeongyu/codex-comment-checker",
103
- "version": "5.1.54",
103
+ "version": "5.1.56",
104
104
  "license": "MIT",
105
105
  "bin": {
106
106
  "omo-comment-checker": "dist/cli.js"
@@ -122,7 +122,7 @@
122
122
  },
123
123
  "components/git-bash": {
124
124
  "name": "@sisyphuslabs/codex-git-bash-hook",
125
- "version": "5.1.54",
125
+ "version": "5.1.56",
126
126
  "bin": {
127
127
  "omo-git-bash-hook": "dist/cli.js"
128
128
  },
@@ -136,7 +136,7 @@
136
136
  },
137
137
  "components/lazycodex-executor-verify": {
138
138
  "name": "@code-yeongyu/codex-lazycodex-executor-verify",
139
- "version": "5.1.54",
139
+ "version": "5.1.56",
140
140
  "license": "MIT",
141
141
  "bin": {
142
142
  "lazycodex-executor-verify": "dist/cli.js"
@@ -154,7 +154,7 @@
154
154
  },
155
155
  "components/lsp": {
156
156
  "name": "@code-yeongyu/codex-lsp",
157
- "version": "5.1.54",
157
+ "version": "5.1.56",
158
158
  "license": "MIT",
159
159
  "dependencies": {
160
160
  "@code-yeongyu/lsp-daemon": "file:../../../../lsp-daemon",
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "components/rules": {
178
178
  "name": "@code-yeongyu/codex-rules",
179
- "version": "5.1.54",
179
+ "version": "5.1.56",
180
180
  "license": "MIT",
181
181
  "dependencies": {
182
182
  "picomatch": "^4.0.7"
@@ -199,7 +199,7 @@
199
199
  },
200
200
  "components/teammode": {
201
201
  "name": "@sisyphuslabs/codex-teammode",
202
- "version": "5.1.54",
202
+ "version": "5.1.56",
203
203
  "devDependencies": {
204
204
  "@types/node": "^26.6.2",
205
205
  "@vitest/coverage-v8": "5.0.1",
@@ -213,7 +213,7 @@
213
213
  },
214
214
  "components/telemetry": {
215
215
  "name": "@code-yeongyu/codex-telemetry",
216
- "version": "5.1.54",
216
+ "version": "5.1.56",
217
217
  "license": "MIT",
218
218
  "bin": {
219
219
  "omo-telemetry": "dist/cli.js"
@@ -232,7 +232,7 @@
232
232
  },
233
233
  "components/ultrawork": {
234
234
  "name": "@code-yeongyu/codex-ultrawork",
235
- "version": "5.1.54",
235
+ "version": "5.1.56",
236
236
  "license": "MIT",
237
237
  "bin": {
238
238
  "omo-ultrawork": "dist/cli.js"
@@ -251,7 +251,7 @@
251
251
  },
252
252
  "components/ulw-execute-continuation": {
253
253
  "name": "@code-yeongyu/codex-ulw-execute-continuation",
254
- "version": "5.1.54",
254
+ "version": "5.1.56",
255
255
  "license": "MIT",
256
256
  "bin": {
257
257
  "omo-ulw-execute-continuation": "dist/cli.js"
@@ -269,7 +269,7 @@
269
269
  },
270
270
  "components/ulw-loop": {
271
271
  "name": "@code-yeongyu/codex-ulw-loop",
272
- "version": "5.1.54",
272
+ "version": "5.1.56",
273
273
  "license": "MIT",
274
274
  "bin": {
275
275
  "omo-ulw-loop": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/omo-codex-plugin",
3
- "version": "5.1.54",
3
+ "version": "5.1.56",
4
4
  "description": "Aggregate Codex plugin root for OMO components.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -5,5 +5,5 @@
5
5
  "index.js": "8b14c1775f397e36967a0d094c67e2a31f8ae1e92add172925548723ca339046",
6
6
  "page-bundle.js": "bfd51e8383cf47200d57387bbbed258bce5a3a3d00c38fc108034d2888e23f0c"
7
7
  },
8
- "stagedAtUtc": "2026-10-31T15:59:33.751Z"
8
+ "stagedAtUtc": "2026-11-01T11:38:13.634Z"
9
9
  }
@@ -199,6 +199,8 @@ uv run scripts/python/check-no-excuse-rules.py <changed paths>
199
199
  bash scripts/rust/check-no-excuse-rules.sh <changed paths>
200
200
  # TypeScript
201
201
  bun run scripts/typescript/check-no-excuse-rules.ts <changed paths>
202
+ # Any language, test files you touched: the junk classes a regex can see (candidates for "test your test")
203
+ bun run scripts/typescript/check-test-junk.ts <changed test paths>
202
204
  ```
203
205
 
204
206
  ### Step 2 — interpret
@@ -56,12 +56,12 @@ When delegating test-writing, hand the child the behavior the test must distingu
56
56
 
57
57
  ## After writing: test your test
58
58
 
59
- A test earns its place by failing. Before the PR, for each new or reshaped test:
59
+ A test earns its place by failing. Before the PR, run `bun run scripts/typescript/check-test-junk.ts <test files>` for the classes a regex can see (source grep, prose pin, fixed sleep, `.only`/`.skip`, self-comparison, lower bound), then for each new or reshaped test:
60
60
 
61
61
  1. **Break the owner** (flip the condition, drop the branch, return a constant, skip the write) and run the test. Confirm the mutated bytes are actually in the file before reading the result; a no-op edit "passes" identically to the original.
62
62
  2. **Red for the intended reason?** Read the failure message: a loose pattern can match the base's generic error by coincidence (a false red), and a fake that simulates the failure by a different mechanism than the guard reads fails with the fix present.
63
63
  3. **Still green?** Then one of: the test is vacuous (fix the assertion); the mutant is equivalent (a `try/catch`, fallback, or normalization maps it back to the same outcome: retarget the mutation at the observable claim); a second rule already produces the same outcome (make the input exercise only the rule under test and assert the recorded reason); or the mutated code is dead (delete the code, not the test's teeth).
64
- 4. **Restore byte for byte.** Never commit a mutation.
64
+ 4. **Restore byte for byte.** Never commit a mutation. `bun run scripts/typescript/mutation-proof.ts --file <owner> --find <text> --replace <text> --test 'candidate=bun test <file>' --test 'keeper=bun test <file>'` does steps 1, 3 and 4 in one run and prints red/green per test.
65
65
 
66
66
  Assertion shapes that survive this step:
67
67
 
@@ -0,0 +1,140 @@
1
+ import { extname } from "node:path"
2
+
3
+ export type RuleId =
4
+ | "source-grep"
5
+ | "prose-pin"
6
+ | "fixed-sleep"
7
+ | "focused"
8
+ | "skipped"
9
+ | "self-compare"
10
+ | "lower-bound"
11
+
12
+ export type Language = "typescript" | "python" | "rust" | "go"
13
+
14
+ export type ReadKind = "prose" | "source"
15
+
16
+ export type LineRule = {
17
+ readonly rule: RuleId
18
+ readonly languages: readonly Language[]
19
+ readonly pattern: RegExp
20
+ readonly message: string
21
+ }
22
+
23
+ export const RULE_IDS: readonly RuleId[] = [
24
+ "source-grep",
25
+ "prose-pin",
26
+ "fixed-sleep",
27
+ "focused",
28
+ "skipped",
29
+ "self-compare",
30
+ "lower-bound",
31
+ ]
32
+
33
+ export function languageOf(file: string): Language | undefined {
34
+ switch (extname(file)) {
35
+ case ".ts":
36
+ case ".tsx":
37
+ case ".mts":
38
+ case ".cts":
39
+ case ".js":
40
+ case ".jsx":
41
+ case ".mjs":
42
+ case ".cjs":
43
+ return "typescript"
44
+ case ".py":
45
+ return "python"
46
+ case ".rs":
47
+ return "rust"
48
+ case ".go":
49
+ return "go"
50
+ default:
51
+ return undefined
52
+ }
53
+ }
54
+
55
+ const SLEEP_MESSAGE = "fixed sleep: wait on the event or state change, not on the clock"
56
+ const SKIP_MESSAGE = "skipped test: fix the test or delete it with its proof; never park it"
57
+ const LOWER_BOUND_MESSAGE = "lower bound on a cardinality: removing one item stays green; pin the exact set"
58
+
59
+ export const LINE_RULES: readonly LineRule[] = [
60
+ {
61
+ rule: "fixed-sleep",
62
+ languages: ["typescript"],
63
+ pattern: /\bsetTimeout\s*\(\s*(resolve|res|r|done|cb)\s*,\s*[1-9]|\bawait\s+(Bun\.)?sleep\s*\(\s*[1-9]|new Promise\s*\(\s*\(?\s*\w*\s*\)?\s*=>\s*setTimeout\s*\(\s*\w+\s*,\s*[1-9]/,
64
+ message: SLEEP_MESSAGE,
65
+ },
66
+ { rule: "fixed-sleep", languages: ["python"], pattern: /\btime\.sleep\s*\(|\bawait\s+(asyncio|anyio|trio)\.sleep\s*\(\s*[0-9]/, message: SLEEP_MESSAGE },
67
+ { rule: "fixed-sleep", languages: ["rust"], pattern: /\bthread::sleep\s*\(|\btokio::time::sleep\s*\(/, message: SLEEP_MESSAGE },
68
+ { rule: "fixed-sleep", languages: ["go"], pattern: /\btime\.Sleep\s*\(/, message: SLEEP_MESSAGE },
69
+ {
70
+ rule: "focused",
71
+ languages: ["typescript"],
72
+ pattern: /\b(test|it|describe)\.only\s*\(/,
73
+ message: "focused test: the rest of the suite is skipped silently",
74
+ },
75
+ { rule: "skipped", languages: ["typescript"], pattern: /\b(test|it|describe)\.skip\s*\(|\b(xit|xdescribe|xtest)\s*\(/, message: SKIP_MESSAGE },
76
+ { rule: "skipped", languages: ["python"], pattern: /@pytest\.mark\.skip\b|\bpytest\.skip\s*\(|\bunittest\.skip\b/, message: SKIP_MESSAGE },
77
+ { rule: "skipped", languages: ["rust"], pattern: /#\[ignore\b/, message: SKIP_MESSAGE },
78
+ { rule: "skipped", languages: ["go"], pattern: /\bt\.Skip(f|Now)?\s*\(/, message: SKIP_MESSAGE },
79
+ {
80
+ rule: "lower-bound",
81
+ languages: ["typescript"],
82
+ pattern: /\.(length|size|count)\s*\)\s*\.(toBeGreaterThan\s*\(\s*[1-9]|toBeGreaterThanOrEqual\s*\(\s*([2-9]|\d{2,}))/,
83
+ message: LOWER_BOUND_MESSAGE,
84
+ },
85
+ {
86
+ rule: "lower-bound",
87
+ languages: ["python"],
88
+ pattern: /\bassert\s+len\s*\(.+\)\s*(>\s*[1-9]|>=\s*([2-9]|\d{2,}))/,
89
+ message: LOWER_BOUND_MESSAGE,
90
+ },
91
+ ]
92
+
93
+ export const SELF_COMPARE: Readonly<Record<Language, RegExp>> = {
94
+ typescript: /\bexpect\s*\(\s*([^()]+?)\s*\)\s*\.(toBe|toEqual|toStrictEqual)\s*\(\s*\1\s*\)/,
95
+ python: /\bassert\s+([A-Za-z_][\w.]*)\s*==\s*\1\s*(#|$)/,
96
+ rust: /\bassert_eq!\s*\(\s*([^,()]+?)\s*,\s*\1\s*\)/,
97
+ go: /\b(Equal|Exactly)\s*\(\s*t\s*,\s*([^,()]+?)\s*,\s*\2\s*\)/,
98
+ }
99
+
100
+ const SOURCE_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rs|go)$/
101
+ const PROSE_EXTENSIONS = /\.(md|mdc|txt|prompt)$/
102
+ const READ_ASSIGNMENT = /\b(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:\w+\.)?(?:readFileSync|readFile|readTextSync|Bun\.file|open|read_text|read)\s*\(([^\n]*)$/
103
+ const PYTHON_READ_ASSIGNMENT = /^\s*(\w+)\s*=\s*(?:\w+\.)?(?:open|Path|read_text)\s*\(([^\n]*)$/
104
+ const STRING_LITERAL = /["'`]([^"'`]+)["'`]/g
105
+
106
+ function readKindOf(callArguments: string): ReadKind | undefined {
107
+ for (const literal of callArguments.matchAll(STRING_LITERAL)) {
108
+ const value = literal[1] ?? ""
109
+ if (PROSE_EXTENSIONS.test(value)) return "prose"
110
+ if (SOURCE_EXTENSIONS.test(value)) return "source"
111
+ }
112
+ return undefined
113
+ }
114
+
115
+ export function collectReadVariables(lines: readonly string[], language: Language): ReadonlyMap<string, ReadKind> {
116
+ const variables = new Map<string, ReadKind>()
117
+ const pattern = language === "python" ? PYTHON_READ_ASSIGNMENT : READ_ASSIGNMENT
118
+ for (const line of lines) {
119
+ const match = pattern.exec(line)
120
+ if (match === null) continue
121
+ const kind = readKindOf(match[2] ?? "")
122
+ if (kind !== undefined && match[1] !== undefined) variables.set(match[1], kind)
123
+ }
124
+ return variables
125
+ }
126
+
127
+ export function greppedVariable(line: string, language: Language): string | undefined {
128
+ if (language === "python") {
129
+ return /\bassert\s+(?:not\s+)?["'][^"']*["']\s+(?:not\s+)?in\s+(\w+)/.exec(line)?.[1]
130
+ }
131
+ return (
132
+ /\bexpect\s*\(\s*(\w+)\s*\)\s*(?:\.not)?\.(?:toContain|toMatch|toMatchSnapshot|toMatchInlineSnapshot|toInclude|toStartWith|toEndWith)\s*\(/.exec(line)?.[1] ??
133
+ /\bexpect\s*\(\s*(\w+)\.(?:includes|match|indexOf|search)\s*\(/.exec(line)?.[1]
134
+ )
135
+ }
136
+
137
+ export const READ_RULE_MESSAGES: Readonly<Record<ReadKind, { readonly rule: RuleId; readonly message: string }>> = {
138
+ prose: { rule: "prose-pin", message: "wording of a prompt/skill/doc is pinned; assert a machine-consumed value or write no test" },
139
+ source: { rule: "source-grep", message: "a source file is grepped as text; drive the module and assert behavior" },
140
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Flag test files for the junk classes a regex can see (the rest need a reader):
4
+ *
5
+ * source-grep - a value read from a source file (.ts/.js/.py/.rs/.go/...) is grepped
6
+ * (toContain / toMatch / includes / `in`); byte equality between two reads is not flagged
7
+ * prose-pin - a value read from a prompt/skill/markdown file has its wording pinned
8
+ * (toContain / not.toContain / toMatch / toMatchSnapshot / `in`)
9
+ * fixed-sleep - setTimeout-as-wait, sleep(N), time.sleep, thread::sleep, time.Sleep
10
+ * focused - test.only / it.only / describe.only (a focused test silently skips the suite)
11
+ * skipped - .skip / xit / xdescribe / @pytest.mark.skip / #[ignore] / t.Skip
12
+ * self-compare - expect(x).toBe(x) / assert x == x / assert_eq!(x, x)
13
+ * lower-bound - toBeGreaterThan(N>=1) / toBeGreaterThanOrEqual(N>=2) on a length/size/count
14
+ * (removing one item stays green; a plain non-empty check is not flagged)
15
+ *
16
+ * Opt out per line with a trailing comment naming the rule and the reason:
17
+ * // test-junk-ok: fixed-sleep debounce window is the behavior under test
18
+ *
19
+ * Usage:
20
+ * bun run scripts/typescript/check-test-junk.ts [--json] <file-or-dir>...
21
+ *
22
+ * Exit codes: 0 no findings, 1 findings, 2 input error.
23
+ * Each class is explained in references/testing/junk-classes.md; a finding here is a candidate
24
+ * to prove with references/testing/README.md "test your test", not an automatic deletion.
25
+ */
26
+
27
+ import { readdirSync, readFileSync, statSync } from "node:fs"
28
+ import { basename, extname, join } from "node:path"
29
+ import process from "node:process"
30
+
31
+ import {
32
+ collectReadVariables,
33
+ greppedVariable,
34
+ languageOf,
35
+ LINE_RULES,
36
+ READ_RULE_MESSAGES,
37
+ RULE_IDS,
38
+ SELF_COMPARE,
39
+ type RuleId,
40
+ } from "./check-test-junk-rules"
41
+
42
+ export type { RuleId } from "./check-test-junk-rules"
43
+
44
+ export type Finding = {
45
+ readonly file: string
46
+ readonly line: number
47
+ readonly rule: RuleId
48
+ readonly message: string
49
+ }
50
+
51
+ const EXCLUDED_DIRS: ReadonlySet<string> = new Set([".git", "node_modules", "dist", "build", "target", "__pycache__", ".venv", "vendor"])
52
+
53
+ const TEST_FILE_PATTERNS: readonly RegExp[] = [
54
+ /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/,
55
+ /_test\.go$/,
56
+ /(^|\/)test_[^/]*\.py$/,
57
+ /(^|\/)[^/]*_test\.py$/,
58
+ /\.rs$/,
59
+ ]
60
+
61
+ const OPT_OUT = /test-junk-ok:\s*([a-z-]+)/
62
+
63
+ function isTestFile(file: string): boolean {
64
+ if (!TEST_FILE_PATTERNS.some((pattern) => pattern.test(file))) return false
65
+ // Rust unit tests live inside source files; only files declaring a test module count.
66
+ return extname(file) !== ".rs" || /#\[cfg\(test\)\]|#\[test\]/.test(readFileSync(file, "utf8"))
67
+ }
68
+
69
+ function* walk(root: string): Generator<string> {
70
+ if (statSync(root).isFile()) {
71
+ yield root
72
+ return
73
+ }
74
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
75
+ if (entry.isDirectory()) {
76
+ if (!EXCLUDED_DIRS.has(entry.name)) yield* walk(join(root, entry.name))
77
+ } else if (entry.isFile()) {
78
+ yield join(root, entry.name)
79
+ }
80
+ }
81
+ }
82
+
83
+ function optedOut(line: string, rule: RuleId): boolean {
84
+ const match = OPT_OUT.exec(line)
85
+ return match !== null && match[1] === rule
86
+ }
87
+
88
+ function scanFile(file: string): readonly Finding[] {
89
+ const language = languageOf(file)
90
+ if (language === undefined) return []
91
+ const lines = readFileSync(file, "utf8").split("\n")
92
+ const readVariables = collectReadVariables(lines, language)
93
+ const findings: Finding[] = []
94
+ const report = (line: number, rule: RuleId, message: string): void => {
95
+ if (!optedOut(lines[line - 1] ?? "", rule)) findings.push({ file, line, rule, message })
96
+ }
97
+
98
+ lines.forEach((source, index) => {
99
+ const line = index + 1
100
+ for (const rule of LINE_RULES) {
101
+ if (rule.languages.includes(language) && rule.pattern.test(source)) report(line, rule.rule, rule.message)
102
+ }
103
+ if (SELF_COMPARE[language].test(source)) {
104
+ report(line, "self-compare", "the expected value is the value under test; derive it from the input")
105
+ }
106
+ const grepped = greppedVariable(source, language)
107
+ const kind = grepped === undefined ? undefined : readVariables.get(grepped)
108
+ if (kind !== undefined) report(line, READ_RULE_MESSAGES[kind].rule, READ_RULE_MESSAGES[kind].message)
109
+ })
110
+ return findings
111
+ }
112
+
113
+ function scan(paths: readonly string[]): readonly Finding[] {
114
+ const findings: Finding[] = []
115
+ for (const root of paths) {
116
+ for (const file of walk(root)) {
117
+ if (isTestFile(file)) findings.push(...scanFile(file))
118
+ }
119
+ }
120
+ return findings
121
+ }
122
+
123
+ function main(argv: readonly string[]): number {
124
+ const json = argv.includes("--json")
125
+ const paths = argv.filter((arg) => arg !== "--json")
126
+ if (paths.length === 0) {
127
+ console.error(`usage: ${basename(import.meta.path)} [--json] <file-or-dir>...`)
128
+ return 2
129
+ }
130
+ for (const path of paths) {
131
+ try {
132
+ statSync(path)
133
+ } catch {
134
+ console.error(`error: ${path} does not exist`)
135
+ return 2
136
+ }
137
+ }
138
+ const findings = scan(paths)
139
+ if (json) {
140
+ console.log(JSON.stringify({ findings, rules: RULE_IDS }, null, 2))
141
+ } else {
142
+ for (const finding of findings) console.log(`${finding.file}:${finding.line} ${finding.rule} ${finding.message}`)
143
+ console.log(findings.length === 0 ? "check-test-junk: no findings" : `check-test-junk: ${findings.length} finding(s)`)
144
+ }
145
+ return findings.length === 0 ? 0 : 1
146
+ }
147
+
148
+ if (import.meta.main) process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Prove that a test can fail: apply one exact-match edit to a production file, run each test
4
+ * command against the mutated owner, then restore the original bytes and verify their hash.
5
+ *
6
+ * Usage:
7
+ * bun run scripts/typescript/mutation-proof.ts --file <path> --find <text> --replace <text> \
8
+ * --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]
9
+ *
10
+ * --test takes "label=command words"; the command is split on spaces and quoting is not
11
+ * supported, so select a test by a pattern without spaces or run the whole file.
12
+ *
13
+ * Verdicts per test: red (exit != 0, the test caught the mutation) or green (exit 0, it did not).
14
+ * The owner is restored on success, on a thrown error, and on SIGINT/SIGTERM.
15
+ * Exit codes: 0 ran and restored, 1 the find text is empty or did not match exactly once or the
16
+ * restore hash differed (the file was still restored), 2 usage, 130/143 interrupted and restored.
17
+ * Read references/testing/junk-classes.md "Proof rule" for how to read a candidate/keeper pair.
18
+ */
19
+
20
+ import { createHash } from "node:crypto"
21
+ import { readFileSync, writeFileSync } from "node:fs"
22
+ import { resolve } from "node:path"
23
+ import process from "node:process"
24
+ import { parseArgs } from "node:util"
25
+
26
+ export type MutationTest = { readonly label: string; readonly command: readonly string[] }
27
+
28
+ export type MutationOutcome = {
29
+ readonly label: string
30
+ readonly command: readonly string[]
31
+ readonly exitCode: number
32
+ readonly verdict: "red" | "green"
33
+ readonly outputTail: string
34
+ }
35
+
36
+ export type MutationResult = {
37
+ readonly file: string
38
+ readonly find: string
39
+ readonly replace: string
40
+ readonly originalSha256: string
41
+ readonly restoredSha256: string
42
+ readonly outcomes: readonly MutationOutcome[]
43
+ }
44
+
45
+ export class MutationError extends Error {
46
+ constructor(
47
+ readonly kind: "FIND_EMPTY" | "FIND_NOT_UNIQUE" | "RESTORE_MISMATCH",
48
+ message: string,
49
+ ) {
50
+ super(message)
51
+ this.name = "MutationError"
52
+ }
53
+ }
54
+
55
+ const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex")
56
+
57
+ async function runCaptured(command: readonly string[], cwd: string): Promise<{ readonly exitCode: number; readonly outputTail: string }> {
58
+ const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" })
59
+ const [stdout, stderr, exitCode] = await Promise.all([
60
+ new Response(child.stdout).text(),
61
+ new Response(child.stderr).text(),
62
+ child.exited,
63
+ ])
64
+ const lines = `${stdout}${stderr}`.trimEnd().split("\n")
65
+ return { exitCode, outputTail: lines.slice(-30).join("\n") }
66
+ }
67
+
68
+ export async function runMutation(options: {
69
+ readonly cwd: string
70
+ readonly file: string
71
+ readonly find: string
72
+ readonly replace: string
73
+ readonly tests: readonly MutationTest[]
74
+ }): Promise<MutationResult> {
75
+ if (options.find.length === 0) throw new MutationError("FIND_EMPTY", `${options.file}: find text is empty`)
76
+ const full = resolve(options.cwd, options.file)
77
+ const original = readFileSync(full)
78
+ const text = original.toString("utf8")
79
+ const occurrences = text.split(options.find).length - 1
80
+ if (occurrences !== 1) {
81
+ throw new MutationError("FIND_NOT_UNIQUE", `${options.file}: find matched ${occurrences} times (need exactly 1)`)
82
+ }
83
+ const originalSha256 = sha256(original)
84
+ const outcomes: MutationOutcome[] = []
85
+ const restoreOnSignal = (signal: NodeJS.Signals): void => {
86
+ writeFileSync(full, original)
87
+ process.exit(signal === "SIGINT" ? 130 : 143)
88
+ }
89
+ process.on("SIGINT", restoreOnSignal)
90
+ process.on("SIGTERM", restoreOnSignal)
91
+ try {
92
+ writeFileSync(full, text.replace(options.find, () => options.replace))
93
+ for (const test of options.tests) {
94
+ const run = await runCaptured(test.command, options.cwd)
95
+ outcomes.push({
96
+ label: test.label,
97
+ command: test.command,
98
+ exitCode: run.exitCode,
99
+ verdict: run.exitCode === 0 ? "green" : "red",
100
+ outputTail: run.outputTail,
101
+ })
102
+ }
103
+ } finally {
104
+ writeFileSync(full, original)
105
+ process.off("SIGINT", restoreOnSignal)
106
+ process.off("SIGTERM", restoreOnSignal)
107
+ }
108
+ const restoredSha256 = sha256(readFileSync(full))
109
+ if (restoredSha256 !== originalSha256) {
110
+ throw new MutationError("RESTORE_MISMATCH", `${options.file} not restored byte for byte`)
111
+ }
112
+ return { file: options.file, find: options.find, replace: options.replace, originalSha256, restoredSha256, outcomes }
113
+ }
114
+
115
+ function parseTest(spec: string): MutationTest | undefined {
116
+ const separator = spec.indexOf("=")
117
+ if (separator <= 0) return undefined
118
+ const label = spec.slice(0, separator)
119
+ const command = spec
120
+ .slice(separator + 1)
121
+ .split(" ")
122
+ .filter((word) => word.length > 0)
123
+ return command.length === 0 ? undefined : { label, command }
124
+ }
125
+
126
+ function usage(): number {
127
+ console.error(
128
+ "usage: mutation-proof.ts --file <path> --find <text> --replace <text> --test '<label>=<argv...>' [--test ...] [--cwd <dir>] [--json]",
129
+ )
130
+ return 2
131
+ }
132
+
133
+ async function main(argv: readonly string[]): Promise<number> {
134
+ const { values } = parseArgs({
135
+ args: [...argv],
136
+ options: {
137
+ file: { type: "string" },
138
+ find: { type: "string" },
139
+ replace: { type: "string" },
140
+ test: { type: "string", multiple: true },
141
+ cwd: { type: "string", default: process.cwd() },
142
+ json: { type: "boolean", default: false },
143
+ },
144
+ strict: true,
145
+ })
146
+ if (values.file === undefined || values.find === undefined || values.replace === undefined || values.test === undefined) {
147
+ return usage()
148
+ }
149
+ const tests: MutationTest[] = []
150
+ for (const spec of values.test) {
151
+ const parsed = parseTest(spec)
152
+ if (parsed === undefined) return usage()
153
+ tests.push(parsed)
154
+ }
155
+ try {
156
+ const result = await runMutation({
157
+ cwd: values.cwd ?? process.cwd(),
158
+ file: values.file,
159
+ find: values.find,
160
+ replace: values.replace,
161
+ tests,
162
+ })
163
+ if (values.json) {
164
+ console.log(JSON.stringify(result, null, 2))
165
+ } else {
166
+ for (const outcome of result.outcomes) console.log(`${outcome.label}: ${outcome.verdict} (exit ${outcome.exitCode})`)
167
+ console.log(`restored ${result.file} (sha256 ${result.restoredSha256.slice(0, 12)})`)
168
+ }
169
+ return 0
170
+ } catch (error) {
171
+ if (error instanceof MutationError) {
172
+ console.error(`mutation-proof: ${error.kind}: ${error.message}`)
173
+ return 1
174
+ }
175
+ throw error
176
+ }
177
+ }
178
+
179
+ if (import.meta.main) process.exit(await main(process.argv.slice(2)))