@smeltjs/core 0.2.0 → 0.4.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.
Files changed (348) hide show
  1. package/README.md +3 -3
  2. package/dist/agents/guide.d.ts +68 -0
  3. package/dist/agents/guide.d.ts.map +1 -0
  4. package/dist/agents/guide.js +72 -0
  5. package/dist/agents/guide.js.map +1 -0
  6. package/dist/agents/instructions.d.ts +133 -0
  7. package/dist/agents/instructions.d.ts.map +1 -0
  8. package/dist/agents/instructions.js +212 -0
  9. package/dist/agents/instructions.js.map +1 -0
  10. package/dist/agents/lint.d.ts +154 -0
  11. package/dist/agents/lint.d.ts.map +1 -0
  12. package/dist/agents/lint.js +761 -0
  13. package/dist/agents/lint.js.map +1 -0
  14. package/dist/agents/split.d.ts +113 -0
  15. package/dist/agents/split.d.ts.map +1 -0
  16. package/dist/agents/split.js +234 -0
  17. package/dist/agents/split.js.map +1 -0
  18. package/dist/apply.d.ts.map +1 -1
  19. package/dist/apply.js.map +1 -1
  20. package/dist/cache/prefix.d.ts.map +1 -1
  21. package/dist/cache/prefix.js.map +1 -1
  22. package/dist/cli/agents.d.ts +39 -0
  23. package/dist/cli/agents.d.ts.map +1 -0
  24. package/dist/cli/agents.js +121 -0
  25. package/dist/cli/agents.js.map +1 -0
  26. package/dist/cli/args.d.ts +17 -70
  27. package/dist/cli/args.d.ts.map +1 -1
  28. package/dist/cli/args.js +18 -338
  29. package/dist/cli/args.js.map +1 -1
  30. package/dist/cli/bin.d.ts.map +1 -1
  31. package/dist/cli/bin.js.map +1 -1
  32. package/dist/cli/config.d.ts +60 -0
  33. package/dist/cli/config.d.ts.map +1 -1
  34. package/dist/cli/config.js +89 -2
  35. package/dist/cli/config.js.map +1 -1
  36. package/dist/cli/hooks.d.ts +55 -51
  37. package/dist/cli/hooks.d.ts.map +1 -1
  38. package/dist/cli/hooks.js +206 -658
  39. package/dist/cli/hooks.js.map +1 -1
  40. package/dist/cli/init.d.ts +30 -15
  41. package/dist/cli/init.d.ts.map +1 -1
  42. package/dist/cli/init.js +167 -38
  43. package/dist/cli/init.js.map +1 -1
  44. package/dist/cli/report.d.ts +23 -0
  45. package/dist/cli/report.d.ts.map +1 -1
  46. package/dist/cli/report.js +142 -4
  47. package/dist/cli/report.js.map +1 -1
  48. package/dist/cli/run.d.ts +29 -90
  49. package/dist/cli/run.d.ts.map +1 -1
  50. package/dist/cli/run.js +34 -342
  51. package/dist/cli/run.js.map +1 -1
  52. package/dist/cli/shell.d.ts +106 -0
  53. package/dist/cli/shell.d.ts.map +1 -0
  54. package/dist/cli/shell.js +100 -0
  55. package/dist/cli/shell.js.map +1 -0
  56. package/dist/cli/subcommands/agents.d.ts +72 -0
  57. package/dist/cli/subcommands/agents.d.ts.map +1 -0
  58. package/dist/cli/subcommands/agents.js +162 -0
  59. package/dist/cli/subcommands/agents.js.map +1 -0
  60. package/dist/cli/subcommands/flags.d.ts +144 -0
  61. package/dist/cli/subcommands/flags.d.ts.map +1 -0
  62. package/dist/cli/subcommands/flags.js +220 -0
  63. package/dist/cli/subcommands/flags.js.map +1 -0
  64. package/dist/cli/subcommands/hooks.d.ts +18 -0
  65. package/dist/cli/subcommands/hooks.d.ts.map +1 -0
  66. package/dist/cli/subcommands/hooks.js +80 -0
  67. package/dist/cli/subcommands/hooks.js.map +1 -0
  68. package/dist/cli/subcommands/init.d.ts +19 -0
  69. package/dist/cli/subcommands/init.d.ts.map +1 -0
  70. package/dist/cli/subcommands/init.js +44 -0
  71. package/dist/cli/subcommands/init.js.map +1 -0
  72. package/dist/cli/subcommands/map.d.ts +75 -0
  73. package/dist/cli/subcommands/map.d.ts.map +1 -0
  74. package/dist/cli/subcommands/map.js +132 -0
  75. package/dist/cli/subcommands/map.js.map +1 -0
  76. package/dist/cli/subcommands/registry.d.ts +81 -0
  77. package/dist/cli/subcommands/registry.d.ts.map +1 -0
  78. package/dist/cli/subcommands/registry.js +141 -0
  79. package/dist/cli/subcommands/registry.js.map +1 -0
  80. package/dist/cli/subcommands/retrieve.d.ts +74 -0
  81. package/dist/cli/subcommands/retrieve.d.ts.map +1 -0
  82. package/dist/cli/subcommands/retrieve.js +84 -0
  83. package/dist/cli/subcommands/retrieve.js.map +1 -0
  84. package/dist/cli/subcommands/smelt.d.ts +107 -0
  85. package/dist/cli/subcommands/smelt.d.ts.map +1 -0
  86. package/dist/cli/subcommands/smelt.js +304 -0
  87. package/dist/cli/subcommands/smelt.js.map +1 -0
  88. package/dist/cli/subcommands/stats.d.ts +43 -0
  89. package/dist/cli/subcommands/stats.d.ts.map +1 -0
  90. package/dist/cli/subcommands/stats.js +62 -0
  91. package/dist/cli/subcommands/stats.js.map +1 -0
  92. package/dist/cli/subcommands/subcommand.d.ts +113 -0
  93. package/dist/cli/subcommands/subcommand.d.ts.map +1 -0
  94. package/dist/cli/subcommands/subcommand.js +3 -0
  95. package/dist/cli/subcommands/subcommand.js.map +1 -0
  96. package/dist/cli/usage.d.ts +3 -0
  97. package/dist/cli/usage.d.ts.map +1 -0
  98. package/dist/cli/usage.js +95 -0
  99. package/dist/cli/usage.js.map +1 -0
  100. package/dist/detect.d.ts.map +1 -1
  101. package/dist/detect.js.map +1 -1
  102. package/dist/errors.d.ts +21 -1
  103. package/dist/errors.d.ts.map +1 -1
  104. package/dist/errors.js +33 -1
  105. package/dist/errors.js.map +1 -1
  106. package/dist/harness/aider.d.ts +10 -0
  107. package/dist/harness/aider.d.ts.map +1 -0
  108. package/dist/harness/aider.js +21 -0
  109. package/dist/harness/aider.js.map +1 -0
  110. package/dist/harness/claude-code.d.ts +3 -0
  111. package/dist/harness/claude-code.d.ts.map +1 -0
  112. package/dist/harness/claude-code.js +68 -0
  113. package/dist/harness/claude-code.js.map +1 -0
  114. package/dist/harness/cline.d.ts +3 -0
  115. package/dist/harness/cline.d.ts.map +1 -0
  116. package/dist/harness/cline.js +54 -0
  117. package/dist/harness/cline.js.map +1 -0
  118. package/dist/harness/codex.d.ts +3 -0
  119. package/dist/harness/codex.d.ts.map +1 -0
  120. package/dist/harness/codex.js +92 -0
  121. package/dist/harness/codex.js.map +1 -0
  122. package/dist/harness/cursor.d.ts +3 -0
  123. package/dist/harness/cursor.d.ts.map +1 -0
  124. package/dist/harness/cursor.js +54 -0
  125. package/dist/harness/cursor.js.map +1 -0
  126. package/dist/harness/gemini.d.ts +3 -0
  127. package/dist/harness/gemini.d.ts.map +1 -0
  128. package/dist/harness/gemini.js +60 -0
  129. package/dist/harness/gemini.js.map +1 -0
  130. package/dist/harness/grok.d.ts +3 -0
  131. package/dist/harness/grok.d.ts.map +1 -0
  132. package/dist/harness/grok.js +47 -0
  133. package/dist/harness/grok.js.map +1 -0
  134. package/dist/harness/hermes.d.ts +3 -0
  135. package/dist/harness/hermes.d.ts.map +1 -0
  136. package/dist/harness/hermes.js +72 -0
  137. package/dist/harness/hermes.js.map +1 -0
  138. package/dist/harness/kilocode.d.ts +3 -0
  139. package/dist/harness/kilocode.d.ts.map +1 -0
  140. package/dist/harness/kilocode.js +35 -0
  141. package/dist/harness/kilocode.js.map +1 -0
  142. package/dist/harness/opencode.d.ts +3 -0
  143. package/dist/harness/opencode.d.ts.map +1 -0
  144. package/dist/harness/opencode.js +97 -0
  145. package/dist/harness/opencode.js.map +1 -0
  146. package/dist/harness/paths.d.ts +17 -0
  147. package/dist/harness/paths.d.ts.map +1 -0
  148. package/dist/harness/paths.js +45 -0
  149. package/dist/harness/paths.js.map +1 -0
  150. package/dist/harness/profile.d.ts +209 -0
  151. package/dist/harness/profile.d.ts.map +1 -0
  152. package/dist/harness/profile.js +41 -0
  153. package/dist/harness/profile.js.map +1 -0
  154. package/dist/harness/registry.d.ts +100 -0
  155. package/dist/harness/registry.d.ts.map +1 -0
  156. package/dist/harness/registry.js +144 -0
  157. package/dist/harness/registry.js.map +1 -0
  158. package/dist/harness/snippet.d.ts +19 -0
  159. package/dist/harness/snippet.d.ts.map +1 -0
  160. package/dist/harness/snippet.js +41 -0
  161. package/dist/harness/snippet.js.map +1 -0
  162. package/dist/hash.d.ts.map +1 -1
  163. package/dist/hash.js.map +1 -1
  164. package/dist/hooks/guard-core.d.ts +14 -14
  165. package/dist/hooks/guard-core.d.ts.map +1 -1
  166. package/dist/hooks/guard-core.js +15 -37
  167. package/dist/hooks/guard-core.js.map +1 -1
  168. package/dist/hooks/shim.d.ts +95 -4
  169. package/dist/hooks/shim.d.ts.map +1 -1
  170. package/dist/hooks/shim.js +87 -2
  171. package/dist/hooks/shim.js.map +1 -1
  172. package/dist/hooks/shims/claude-code.d.ts +5 -18
  173. package/dist/hooks/shims/claude-code.d.ts.map +1 -1
  174. package/dist/hooks/shims/claude-code.js +9 -56
  175. package/dist/hooks/shims/claude-code.js.map +1 -1
  176. package/dist/hooks/shims/cline.d.ts +5 -12
  177. package/dist/hooks/shims/cline.d.ts.map +1 -1
  178. package/dist/hooks/shims/cline.js +9 -34
  179. package/dist/hooks/shims/cline.js.map +1 -1
  180. package/dist/hooks/shims/codex.d.ts +5 -18
  181. package/dist/hooks/shims/codex.d.ts.map +1 -1
  182. package/dist/hooks/shims/codex.js +9 -51
  183. package/dist/hooks/shims/codex.js.map +1 -1
  184. package/dist/hooks/shims/cursor.d.ts +5 -14
  185. package/dist/hooks/shims/cursor.d.ts.map +1 -1
  186. package/dist/hooks/shims/cursor.js +9 -42
  187. package/dist/hooks/shims/cursor.js.map +1 -1
  188. package/dist/hooks/shims/gemini.d.ts +5 -18
  189. package/dist/hooks/shims/gemini.d.ts.map +1 -1
  190. package/dist/hooks/shims/gemini.js +9 -48
  191. package/dist/hooks/shims/gemini.js.map +1 -1
  192. package/dist/hooks/shims/grok.d.ts +5 -13
  193. package/dist/hooks/shims/grok.d.ts.map +1 -1
  194. package/dist/hooks/shims/grok.js +9 -32
  195. package/dist/hooks/shims/grok.js.map +1 -1
  196. package/dist/hooks/shims/hermes.d.ts +5 -17
  197. package/dist/hooks/shims/hermes.d.ts.map +1 -1
  198. package/dist/hooks/shims/hermes.js +9 -45
  199. package/dist/hooks/shims/hermes.js.map +1 -1
  200. package/dist/index.d.ts +43 -76
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +21 -67
  203. package/dist/index.js.map +1 -1
  204. package/dist/lang/bash.d.ts.map +1 -1
  205. package/dist/lang/bash.js.map +1 -1
  206. package/dist/lang/c.d.ts.map +1 -1
  207. package/dist/lang/c.js.map +1 -1
  208. package/dist/lang/c_sharp.d.ts.map +1 -1
  209. package/dist/lang/c_sharp.js.map +1 -1
  210. package/dist/lang/cpp.d.ts.map +1 -1
  211. package/dist/lang/cpp.js.map +1 -1
  212. package/dist/lang/go.d.ts.map +1 -1
  213. package/dist/lang/go.js.map +1 -1
  214. package/dist/lang/java.d.ts.map +1 -1
  215. package/dist/lang/java.js.map +1 -1
  216. package/dist/lang/javascript.d.ts.map +1 -1
  217. package/dist/lang/javascript.js.map +1 -1
  218. package/dist/lang/kotlin.d.ts.map +1 -1
  219. package/dist/lang/kotlin.js.map +1 -1
  220. package/dist/lang/php.d.ts.map +1 -1
  221. package/dist/lang/php.js.map +1 -1
  222. package/dist/lang/profile.d.ts.map +1 -1
  223. package/dist/lang/profile.js.map +1 -1
  224. package/dist/lang/python.d.ts.map +1 -1
  225. package/dist/lang/python.js.map +1 -1
  226. package/dist/lang/registry.d.ts.map +1 -1
  227. package/dist/lang/registry.js.map +1 -1
  228. package/dist/lang/ruby.d.ts.map +1 -1
  229. package/dist/lang/ruby.js.map +1 -1
  230. package/dist/lang/rust.d.ts.map +1 -1
  231. package/dist/lang/rust.js.map +1 -1
  232. package/dist/lang/swift.d.ts.map +1 -1
  233. package/dist/lang/swift.js.map +1 -1
  234. package/dist/lang/tsx.d.ts.map +1 -1
  235. package/dist/lang/tsx.js.map +1 -1
  236. package/dist/lang/typescript.d.ts.map +1 -1
  237. package/dist/lang/typescript.js.map +1 -1
  238. package/dist/net/policy.d.ts +18 -1
  239. package/dist/net/policy.d.ts.map +1 -1
  240. package/dist/net/policy.js +6 -4
  241. package/dist/net/policy.js.map +1 -1
  242. package/dist/ops/index.d.ts +39 -0
  243. package/dist/ops/index.d.ts.map +1 -0
  244. package/dist/ops/index.js +37 -0
  245. package/dist/ops/index.js.map +1 -0
  246. package/dist/ops/inputs.d.ts +170 -0
  247. package/dist/ops/inputs.d.ts.map +1 -0
  248. package/dist/ops/inputs.js +142 -0
  249. package/dist/ops/inputs.js.map +1 -0
  250. package/dist/ops/verbs.d.ts +136 -0
  251. package/dist/ops/verbs.d.ts.map +1 -0
  252. package/dist/ops/verbs.js +76 -0
  253. package/dist/ops/verbs.js.map +1 -0
  254. package/dist/plan/auto.d.ts +69 -0
  255. package/dist/plan/auto.d.ts.map +1 -0
  256. package/dist/plan/auto.js +70 -0
  257. package/dist/plan/auto.js.map +1 -0
  258. package/dist/plan/budget.d.ts +21 -0
  259. package/dist/plan/budget.d.ts.map +1 -0
  260. package/dist/plan/budget.js +26 -0
  261. package/dist/plan/budget.js.map +1 -0
  262. package/dist/plan/grammar.d.ts +14 -0
  263. package/dist/plan/grammar.d.ts.map +1 -1
  264. package/dist/plan/grammar.js +47 -6
  265. package/dist/plan/grammar.js.map +1 -1
  266. package/dist/plan/lexical.d.ts.map +1 -1
  267. package/dist/plan/lexical.js +1 -7
  268. package/dist/plan/lexical.js.map +1 -1
  269. package/dist/plan/planners.d.ts +35 -2
  270. package/dist/plan/planners.d.ts.map +1 -1
  271. package/dist/plan/planners.js +35 -2
  272. package/dist/plan/planners.js.map +1 -1
  273. package/dist/plan/structural.d.ts +19 -5
  274. package/dist/plan/structural.d.ts.map +1 -1
  275. package/dist/plan/structural.js +180 -33
  276. package/dist/plan/structural.js.map +1 -1
  277. package/dist/repomap/cache.d.ts +48 -0
  278. package/dist/repomap/cache.d.ts.map +1 -1
  279. package/dist/repomap/cache.js +112 -13
  280. package/dist/repomap/cache.js.map +1 -1
  281. package/dist/repomap/io.d.ts +24 -0
  282. package/dist/repomap/io.d.ts.map +1 -0
  283. package/dist/repomap/io.js +34 -0
  284. package/dist/repomap/io.js.map +1 -0
  285. package/dist/repomap/map.d.ts +69 -6
  286. package/dist/repomap/map.d.ts.map +1 -1
  287. package/dist/repomap/map.js +104 -22
  288. package/dist/repomap/map.js.map +1 -1
  289. package/dist/repomap/rank.d.ts +26 -2
  290. package/dist/repomap/rank.d.ts.map +1 -1
  291. package/dist/repomap/rank.js +20 -0
  292. package/dist/repomap/rank.js.map +1 -1
  293. package/dist/repomap/reader.d.ts +73 -0
  294. package/dist/repomap/reader.d.ts.map +1 -0
  295. package/dist/repomap/reader.js +30 -0
  296. package/dist/repomap/reader.js.map +1 -0
  297. package/dist/repomap/tags.d.ts.map +1 -1
  298. package/dist/repomap/tags.js.map +1 -1
  299. package/dist/retrieve.d.ts +8 -0
  300. package/dist/retrieve.d.ts.map +1 -1
  301. package/dist/retrieve.js +12 -1
  302. package/dist/retrieve.js.map +1 -1
  303. package/dist/smelter.d.ts +79 -0
  304. package/dist/smelter.d.ts.map +1 -0
  305. package/dist/smelter.js +65 -0
  306. package/dist/smelter.js.map +1 -0
  307. package/dist/stages.d.ts.map +1 -1
  308. package/dist/stages.js +2 -2
  309. package/dist/stages.js.map +1 -1
  310. package/dist/stats.d.ts +2 -1
  311. package/dist/stats.d.ts.map +1 -1
  312. package/dist/stats.js +2 -1
  313. package/dist/stats.js.map +1 -1
  314. package/dist/store-dir.d.ts +43 -4
  315. package/dist/store-dir.d.ts.map +1 -1
  316. package/dist/store-dir.js +48 -5
  317. package/dist/store-dir.js.map +1 -1
  318. package/dist/store.d.ts +6 -0
  319. package/dist/store.d.ts.map +1 -1
  320. package/dist/store.js +6 -0
  321. package/dist/store.js.map +1 -1
  322. package/dist/text/json-edit.d.ts +71 -0
  323. package/dist/text/json-edit.d.ts.map +1 -0
  324. package/dist/text/json-edit.js +219 -0
  325. package/dist/text/json-edit.js.map +1 -0
  326. package/dist/types.d.ts +35 -14
  327. package/dist/types.d.ts.map +1 -1
  328. package/dist/types.js.map +1 -1
  329. package/grammars/tree-sitter-bash.wasm +0 -0
  330. package/grammars/tree-sitter-c.wasm +0 -0
  331. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  332. package/grammars/tree-sitter-cpp.wasm +0 -0
  333. package/grammars/tree-sitter-go.wasm +0 -0
  334. package/grammars/tree-sitter-java.wasm +0 -0
  335. package/grammars/tree-sitter-javascript.wasm +0 -0
  336. package/grammars/tree-sitter-kotlin.wasm +0 -0
  337. package/grammars/tree-sitter-php.wasm +0 -0
  338. package/grammars/tree-sitter-python.wasm +0 -0
  339. package/grammars/tree-sitter-ruby.wasm +0 -0
  340. package/grammars/tree-sitter-rust.wasm +0 -0
  341. package/grammars/tree-sitter-swift.wasm +0 -0
  342. package/grammars/tree-sitter-tsx.wasm +0 -0
  343. package/grammars/tree-sitter-typescript.wasm +0 -0
  344. package/package.json +11 -11
  345. package/dist/cli/resolve.d.ts +0 -103
  346. package/dist/cli/resolve.d.ts.map +0 -1
  347. package/dist/cli/resolve.js +0 -106
  348. package/dist/cli/resolve.js.map +0 -1
@@ -0,0 +1,154 @@
1
+ import type { RepoReader } from '../repomap/reader.ts';
2
+ import type { ElisionReason } from '../types.ts';
3
+ /**
4
+ * `smelt agents lint` — the audit of the blob an agent loads on **every** request.
5
+ *
6
+ * smelt's whole subject is what a context window is spent on, and an instruction file
7
+ * is the one blob every request pays for whether or not it is relevant. So the lint is
8
+ * the same three moves smelt makes everywhere else, aimed at a file nobody measures:
9
+ *
10
+ * 1. **Measure, never threshold** (ruling R2). Bytes per level, the most any single
11
+ * request loads, and the repository-wide surface — three numbers, each labelled
12
+ * with the question it answers, because a monorepo makes the last two differ and
13
+ * printing one under the other's heading is a lie about a cost. Plus an imperative
14
+ * count that says out loud it is a heuristic. The only number that can fail a run
15
+ * is `agents.budgetBytes` in `smelt.config.json` — *the user's own*. There is no
16
+ * built-in budget, for exactly the reason `--budget` has none.
17
+ * 2. **Explain every finding** (Law 2, in the {@link ElisionReason} discipline). A
18
+ * finding is a stable `rule` id plus a sentence, and the sentence ends in a phrase
19
+ * from the guide it is applying, attributed — see `./guide.ts`.
20
+ * 3. **Resolve against the real tree** (ruling R3). `dead-path` and `dead-link` are
21
+ * the checks nobody else makes, because everyone else is linting Markdown while
22
+ * the thing that has rotted is the *repository the Markdown describes*. A renamed
23
+ * `src/auth/handlers.ts` does not make the file invalid; it makes it a lie that
24
+ * the agent believes on every request.
25
+ *
26
+ * **Advisory by default.** Findings exit 0. `--strict` turns any finding into exit 1
27
+ * for CI, because a check that cannot be enforced is a check nobody runs, and a check
28
+ * that is enforced by default is smelt deciding somebody's house style for them.
29
+ *
30
+ * The heuristics here are heuristics, and the report says so where it matters: the
31
+ * imperative count carries "(heuristic)" in its own label and in every receipt,
32
+ * `generated-boilerplate` calls itself the softest rule in its own explanation, and the
33
+ * closing line of every run states that findings are advisory and exit 0 unless
34
+ * `--strict` was asked for. A rule that fires on a file the guide would call minimal is
35
+ * not automatically a bug in the file — it may be a bug in the rule, or in the guide,
36
+ * and the answer is worth writing down either way (ruling R9; see this repository's own
37
+ * `AGENTS.md`).
38
+ */
39
+ /** A path-like token in the prose that resolves to nothing in the tree. The flagship. */
40
+ export declare const DEAD_PATH_RULE = "dead-path";
41
+ /** A Markdown link whose relative target is not in the tree. */
42
+ export declare const DEAD_LINK_RULE = "dead-link";
43
+ /** "always", "never", ALL-CAPS forcing. */
44
+ export declare const FORCING_LANGUAGE_RULE = "forcing-language";
45
+ /** A directory tree, or a run of bare path lines. */
46
+ export declare const STRUCTURE_DUMP_RULE = "structure-dump";
47
+ /** The fingerprints an init script leaves. The softest rule here, and it says so. */
48
+ export declare const GENERATED_BOILERPLATE_RULE = "generated-boilerplate";
49
+ /** A code-style rule that loads on every request to be relevant on some of them. */
50
+ export declare const LANGUAGE_RULE_RULE = "language-rule";
51
+ /** A mirror (CLAUDE.md / GEMINI.md) that has diverged from its AGENTS.md. */
52
+ export declare const MIRROR_DRIFT_RULE = "mirror-drift";
53
+ /** The same instruction present at a level and at one of its ancestors. */
54
+ export declare const RESTATED_AT_LEVEL_RULE = "restated-at-level";
55
+ /**
56
+ * The rules a finding can carry, in report order.
57
+ *
58
+ * `Object.freeze`-flat on purpose: the ids are a wire surface. They go into `--json`,
59
+ * into CI greps and into whatever a user filters on, so they are declared once and
60
+ * never spelled again in prose.
61
+ */
62
+ export declare const AGENTS_LINT_RULES: readonly ["dead-path", "dead-link", "forcing-language", "structure-dump", "generated-boilerplate", "language-rule", "mirror-drift", "restated-at-level"];
63
+ /**
64
+ * The imperative counter's rule id (ruling R6).
65
+ *
66
+ * **Deliberately not in {@link AGENTS_LINT_RULES}.** An imperative is not a defect —
67
+ * an instruction file is *made* of imperatives — so counting them is a measurement,
68
+ * like `outputBytes`, and putting them among the findings would make `--strict` red on
69
+ * every real AGENTS.md and therefore useless. Each counted line still carries a
70
+ * receipt naming the verb that matched, because a heuristic whose matches you cannot
71
+ * inspect is a number nobody can check.
72
+ */
73
+ export declare const IMPERATIVE_LINE_RULE = "imperative-line";
74
+ /** One thing the lint noticed, at one place, with its reason. */
75
+ export interface AgentsFinding {
76
+ /** Root-relative path of the instruction file. */
77
+ readonly file: string;
78
+ /** 1-based line within that file. */
79
+ readonly line: number;
80
+ /** Stable `rule` id plus the sentence explaining it — Law 2's shape. */
81
+ readonly reason: ElisionReason;
82
+ }
83
+ /** What one level of the merged set costs, and what stands beside it. */
84
+ export interface AgentsLevelReport {
85
+ /** Root-relative directory; `''` is the repository root. */
86
+ readonly dir: string;
87
+ /** The file this level contributes — see {@link InstructionLevel}. */
88
+ readonly path: string;
89
+ readonly bytes: number;
90
+ /** The mirrors at this level, and how each one stands. */
91
+ readonly mirrors: readonly AgentsMirrorReport[];
92
+ }
93
+ /** A `CLAUDE.md`/`GEMINI.md` beside an `AGENTS.md`, and whether it can drift. */
94
+ export interface AgentsMirrorReport {
95
+ readonly path: string;
96
+ readonly bytes: number;
97
+ /**
98
+ * `'symlink'` — the arrangement the guide recommends, and the only one in which
99
+ * drift is impossible. `'copy'` — byte-identical today. `'drift'` — diverged, and a
100
+ * {@link MIRROR_DRIFT_RULE} finding.
101
+ */
102
+ readonly standing: 'symlink' | 'copy' | 'drift';
103
+ }
104
+ /** Everything one `smelt agents lint` run measured and found. */
105
+ export interface AgentsLintReport {
106
+ /** The directory that was linted, as the caller spelled it. */
107
+ readonly root: string;
108
+ /** Root level first. Empty when the tree holds no instruction file at all. */
109
+ readonly levels: readonly AgentsLevelReport[];
110
+ /**
111
+ * The repository-wide instruction surface: every level's primary, summed.
112
+ *
113
+ * **Not a per-request cost** — see {@link perRequestBytes}, and the note in
114
+ * `./instructions.ts` on why the two differ in any monorepo. This is the number the
115
+ * user's `agents.budgetBytes` is compared against, deliberately: a ceiling on the
116
+ * whole surface is one that cannot be met by moving bytes into a second package.
117
+ */
118
+ readonly totalBytes: number;
119
+ /**
120
+ * What the most expensive single request loads: the heaviest level plus its
121
+ * ancestors. Siblings never merge, so they are never summed into this.
122
+ */
123
+ readonly perRequestBytes: number;
124
+ /** Present only when `smelt.config.json` set one. There is no default (R2). */
125
+ readonly budgetBytes?: number;
126
+ /**
127
+ * Lines counted as instructions, each with the verb that matched. The *count* is the
128
+ * headline (`imperatives (heuristic)`); the receipts make it checkable.
129
+ */
130
+ readonly imperatives: readonly AgentsFinding[];
131
+ /** Every advisory finding, grouped by rule in {@link AGENTS_LINT_RULES} order. */
132
+ readonly findings: readonly AgentsFinding[];
133
+ }
134
+ /** What `lintAgents` needs. Everything but the root has a default. */
135
+ export interface AgentsLintOptions {
136
+ /** The repository root to lint. */
137
+ readonly root: string;
138
+ /** The tree seam. Defaults to {@link nodeFsReader}. */
139
+ readonly reader?: RepoReader;
140
+ /** Replaces the built-in ignore list when given. */
141
+ readonly ignore?: readonly string[];
142
+ /** The user's budget, from `smelt.config.json`. Absent means unbudgeted (R2). */
143
+ readonly budgetBytes?: number;
144
+ }
145
+ /**
146
+ * Lint the merged set under `root`.
147
+ *
148
+ * Pure over its inputs and its reader: nothing is written, and every filesystem touch
149
+ * goes through {@link RepoReader}, which has no writer on it.
150
+ */
151
+ export declare function lintAgents(options: AgentsLintOptions): AgentsLintReport;
152
+ /** How far over the user's budget the merged set is, or `undefined` when it fits. */
153
+ export declare function overBudgetBytes(report: AgentsLintReport): number | undefined;
154
+ //# sourceMappingURL=lint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/agents/lint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAMH,yFAAyF;AACzF,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,gEAAgE;AAChE,eAAO,MAAM,cAAc,cAAc,CAAC;AAC1C,2CAA2C;AAC3C,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,qFAAqF;AACrF,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAClE,oFAAoF;AACpF,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAClD,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAChD,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,0JASpB,CAAC;AAEX;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AAMtD,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACjD;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;CACjD;AAED,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,SAAS,aAAa,EAAE,CAAC;IAC/C,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C;AAED,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAyDvE;AAED,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAI5E","sourcesContent":["import { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\nimport type { ElisionReason } from '../types.ts';\n\nimport { citing, GUIDE } from './guide.ts';\nimport { ancestorDirs, readInstructionSet, resolvesInTree } from './instructions.ts';\nimport type { InstructionFile, InstructionSet } from './instructions.ts';\n\n/**\n * `smelt agents lint` — the audit of the blob an agent loads on **every** request.\n *\n * smelt's whole subject is what a context window is spent on, and an instruction file\n * is the one blob every request pays for whether or not it is relevant. So the lint is\n * the same three moves smelt makes everywhere else, aimed at a file nobody measures:\n *\n * 1. **Measure, never threshold** (ruling R2). Bytes per level, the most any single\n * request loads, and the repository-wide surface — three numbers, each labelled\n * with the question it answers, because a monorepo makes the last two differ and\n * printing one under the other's heading is a lie about a cost. Plus an imperative\n * count that says out loud it is a heuristic. The only number that can fail a run\n * is `agents.budgetBytes` in `smelt.config.json` — *the user's own*. There is no\n * built-in budget, for exactly the reason `--budget` has none.\n * 2. **Explain every finding** (Law 2, in the {@link ElisionReason} discipline). A\n * finding is a stable `rule` id plus a sentence, and the sentence ends in a phrase\n * from the guide it is applying, attributed — see `./guide.ts`.\n * 3. **Resolve against the real tree** (ruling R3). `dead-path` and `dead-link` are\n * the checks nobody else makes, because everyone else is linting Markdown while\n * the thing that has rotted is the *repository the Markdown describes*. A renamed\n * `src/auth/handlers.ts` does not make the file invalid; it makes it a lie that\n * the agent believes on every request.\n *\n * **Advisory by default.** Findings exit 0. `--strict` turns any finding into exit 1\n * for CI, because a check that cannot be enforced is a check nobody runs, and a check\n * that is enforced by default is smelt deciding somebody's house style for them.\n *\n * The heuristics here are heuristics, and the report says so where it matters: the\n * imperative count carries \"(heuristic)\" in its own label and in every receipt,\n * `generated-boilerplate` calls itself the softest rule in its own explanation, and the\n * closing line of every run states that findings are advisory and exit 0 unless\n * `--strict` was asked for. A rule that fires on a file the guide would call minimal is\n * not automatically a bug in the file — it may be a bug in the rule, or in the guide,\n * and the answer is worth writing down either way (ruling R9; see this repository's own\n * `AGENTS.md`).\n */\n\n/* ------------------------------------------------------------------------------------\n * Rule ids — stable, and the whole machine-readable surface of a finding\n * ---------------------------------------------------------------------------------- */\n\n/** A path-like token in the prose that resolves to nothing in the tree. The flagship. */\nexport const DEAD_PATH_RULE = 'dead-path';\n/** A Markdown link whose relative target is not in the tree. */\nexport const DEAD_LINK_RULE = 'dead-link';\n/** \"always\", \"never\", ALL-CAPS forcing. */\nexport const FORCING_LANGUAGE_RULE = 'forcing-language';\n/** A directory tree, or a run of bare path lines. */\nexport const STRUCTURE_DUMP_RULE = 'structure-dump';\n/** The fingerprints an init script leaves. The softest rule here, and it says so. */\nexport const GENERATED_BOILERPLATE_RULE = 'generated-boilerplate';\n/** A code-style rule that loads on every request to be relevant on some of them. */\nexport const LANGUAGE_RULE_RULE = 'language-rule';\n/** A mirror (CLAUDE.md / GEMINI.md) that has diverged from its AGENTS.md. */\nexport const MIRROR_DRIFT_RULE = 'mirror-drift';\n/** The same instruction present at a level and at one of its ancestors. */\nexport const RESTATED_AT_LEVEL_RULE = 'restated-at-level';\n\n/**\n * The rules a finding can carry, in report order.\n *\n * `Object.freeze`-flat on purpose: the ids are a wire surface. They go into `--json`,\n * into CI greps and into whatever a user filters on, so they are declared once and\n * never spelled again in prose.\n */\nexport const AGENTS_LINT_RULES = [\n DEAD_PATH_RULE,\n DEAD_LINK_RULE,\n FORCING_LANGUAGE_RULE,\n STRUCTURE_DUMP_RULE,\n GENERATED_BOILERPLATE_RULE,\n LANGUAGE_RULE_RULE,\n MIRROR_DRIFT_RULE,\n RESTATED_AT_LEVEL_RULE,\n] as const;\n\n/**\n * The imperative counter's rule id (ruling R6).\n *\n * **Deliberately not in {@link AGENTS_LINT_RULES}.** An imperative is not a defect —\n * an instruction file is *made* of imperatives — so counting them is a measurement,\n * like `outputBytes`, and putting them among the findings would make `--strict` red on\n * every real AGENTS.md and therefore useless. Each counted line still carries a\n * receipt naming the verb that matched, because a heuristic whose matches you cannot\n * inspect is a number nobody can check.\n */\nexport const IMPERATIVE_LINE_RULE = 'imperative-line';\n\n/* ------------------------------------------------------------------------------------\n * The report\n * ---------------------------------------------------------------------------------- */\n\n/** One thing the lint noticed, at one place, with its reason. */\nexport interface AgentsFinding {\n /** Root-relative path of the instruction file. */\n readonly file: string;\n /** 1-based line within that file. */\n readonly line: number;\n /** Stable `rule` id plus the sentence explaining it — Law 2's shape. */\n readonly reason: ElisionReason;\n}\n\n/** What one level of the merged set costs, and what stands beside it. */\nexport interface AgentsLevelReport {\n /** Root-relative directory; `''` is the repository root. */\n readonly dir: string;\n /** The file this level contributes — see {@link InstructionLevel}. */\n readonly path: string;\n readonly bytes: number;\n /** The mirrors at this level, and how each one stands. */\n readonly mirrors: readonly AgentsMirrorReport[];\n}\n\n/** A `CLAUDE.md`/`GEMINI.md` beside an `AGENTS.md`, and whether it can drift. */\nexport interface AgentsMirrorReport {\n readonly path: string;\n readonly bytes: number;\n /**\n * `'symlink'` — the arrangement the guide recommends, and the only one in which\n * drift is impossible. `'copy'` — byte-identical today. `'drift'` — diverged, and a\n * {@link MIRROR_DRIFT_RULE} finding.\n */\n readonly standing: 'symlink' | 'copy' | 'drift';\n}\n\n/** Everything one `smelt agents lint` run measured and found. */\nexport interface AgentsLintReport {\n /** The directory that was linted, as the caller spelled it. */\n readonly root: string;\n /** Root level first. Empty when the tree holds no instruction file at all. */\n readonly levels: readonly AgentsLevelReport[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost** — see {@link perRequestBytes}, and the note in\n * `./instructions.ts` on why the two differ in any monorepo. This is the number the\n * user's `agents.budgetBytes` is compared against, deliberately: a ceiling on the\n * whole surface is one that cannot be met by moving bytes into a second package.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus its\n * ancestors. Siblings never merge, so they are never summed into this.\n */\n readonly perRequestBytes: number;\n /** Present only when `smelt.config.json` set one. There is no default (R2). */\n readonly budgetBytes?: number;\n /**\n * Lines counted as instructions, each with the verb that matched. The *count* is the\n * headline (`imperatives (heuristic)`); the receipts make it checkable.\n */\n readonly imperatives: readonly AgentsFinding[];\n /** Every advisory finding, grouped by rule in {@link AGENTS_LINT_RULES} order. */\n readonly findings: readonly AgentsFinding[];\n}\n\n/** What `lintAgents` needs. Everything but the root has a default. */\nexport interface AgentsLintOptions {\n /** The repository root to lint. */\n readonly root: string;\n /** The tree seam. Defaults to {@link nodeFsReader}. */\n readonly reader?: RepoReader;\n /** Replaces the built-in ignore list when given. */\n readonly ignore?: readonly string[];\n /** The user's budget, from `smelt.config.json`. Absent means unbudgeted (R2). */\n readonly budgetBytes?: number;\n}\n\n/**\n * Lint the merged set under `root`.\n *\n * Pure over its inputs and its reader: nothing is written, and every filesystem touch\n * goes through {@link RepoReader}, which has no writer on it.\n */\nexport function lintAgents(options: AgentsLintOptions): AgentsLintReport {\n const reader = options.reader ?? nodeFsReader();\n const set = readInstructionSet({\n root: options.root,\n reader,\n ...(options.ignore === undefined ? {} : { ignore: options.ignore }),\n });\n\n const findings: AgentsFinding[] = [];\n const imperatives: AgentsFinding[] = [];\n\n for (const level of set.levels) {\n // The imperative count is a companion to the byte total, so it is counted over\n // exactly what the byte total is counted over: the primaries. A mirror is an\n // alternative spelling of a level, not a second level — counting it would make\n // the headline number describe a request nobody makes.\n imperatives.push(...countImperatives(level.primary, scanLines(level.primary.text)));\n\n // The rules, though, run over the primary **and every mirror that has actually\n // diverged**: a drifted CLAUDE.md is what Claude Code loads, so its own dead paths\n // are real. A symlink or a byte-identical copy is skipped — it would mint a\n // duplicate of every finding on the primary and say nothing new.\n const linted = [\n level.primary,\n ...level.mirrors.filter((mirror) => standingOf(level.primary, mirror) === 'drift'),\n ];\n for (const file of linted) {\n const lines = scanLines(file.text);\n findings.push(...findDeadLinks(file, lines, options.root, reader));\n findings.push(...findDeadPaths(file, lines, options.root, reader));\n findings.push(...findForcingLanguage(file, lines));\n findings.push(...findStructureDumps(file, lines));\n findings.push(...findGeneratedBoilerplate(file, lines));\n findings.push(...findLanguageRules(file, lines));\n }\n findings.push(...findMirrorDrift(level.primary, level.mirrors));\n }\n findings.push(...findRestatedAcrossLevels(set));\n\n return {\n root: options.root,\n levels: set.levels.map((level) => ({\n dir: level.dir,\n path: level.primary.path,\n bytes: level.primary.bytes,\n mirrors: level.mirrors.map((mirror) => ({\n path: mirror.path,\n bytes: mirror.bytes,\n standing: standingOf(level.primary, mirror),\n })),\n })),\n totalBytes: set.totalBytes,\n perRequestBytes: set.perRequestBytes,\n ...(options.budgetBytes === undefined ? {} : { budgetBytes: options.budgetBytes }),\n imperatives,\n findings: findings.toSorted(byRuleThenPlace),\n };\n}\n\n/** How far over the user's budget the merged set is, or `undefined` when it fits. */\nexport function overBudgetBytes(report: AgentsLintReport): number | undefined {\n if (report.budgetBytes === undefined) return undefined;\n const over = report.totalBytes - report.budgetBytes;\n return over > 0 ? over : undefined;\n}\n\n/** Findings in report order: rule first, then where they were found. */\nfunction byRuleThenPlace(a: AgentsFinding, b: AgentsFinding): number {\n const rank =\n ruleOrder(a.reason.rule) - ruleOrder(b.reason.rule) ||\n (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||\n a.line - b.line;\n return rank;\n}\n\nfunction ruleOrder(rule: string): number {\n const index = (AGENTS_LINT_RULES as readonly string[]).indexOf(rule);\n return index === -1 ? AGENTS_LINT_RULES.length : index;\n}\n\n/* ------------------------------------------------------------------------------------\n * Scanning: prose versus fences\n * ---------------------------------------------------------------------------------- */\n\n/** One line of an instruction file, with the one fact every rule branches on. */\ninterface ScannedLine {\n /** 1-based. */\n readonly number: number;\n readonly text: string;\n /** True inside a fenced code block, including the fence lines themselves. */\n readonly fenced: boolean;\n /** True for the ``` or ~~~ line that opens a block. */\n readonly opensFence: boolean;\n}\n\n/**\n * Split a file into lines, marking fenced code.\n *\n * The distinction matters in both directions: a `const x = 1` inside a fence is an\n * *example*, not a `language-rule`, and a tree drawing is only a `structure-dump`\n * because it is a fence full of paths. Rules that read prose skip fences; the one\n * rule that reads fences skips prose.\n */\nfunction scanLines(text: string): readonly ScannedLine[] {\n const out: ScannedLine[] = [];\n let fence: string | undefined;\n text.split('\\n').forEach((line, index) => {\n const marker = /^\\s{0,3}(`{3,}|~{3,})/.exec(line);\n const opensFence = marker !== null && fence === undefined;\n if (marker !== null) {\n if (fence === undefined) fence = marker[1]!.slice(0, 1);\n else if (marker[1]!.startsWith(fence)) fence = undefined;\n out.push({ number: index + 1, text: line, fenced: true, opensFence });\n return;\n }\n out.push({ number: index + 1, text: line, fenced: fence !== undefined, opensFence: false });\n });\n return out;\n}\n\n/** A line stripped of list bullets, heading hashes, blockquote marks and bold runs. */\nfunction bareText(text: string): string {\n return text\n .replace(/^\\s*(?:[-*+]\\s+|\\d+[.)]\\s+|#{1,6}\\s+|>\\s*)+/, '')\n .replace(/\\*\\*/g, '')\n .trim();\n}\n\n/* ------------------------------------------------------------------------------------\n * imperative-line — a labelled heuristic (R6)\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The openers counted as an instruction.\n *\n * A closed list, deliberately: an open-ended part-of-speech guess would be a number\n * nobody could reproduce, and this figure is reported beside a byte count that *is*\n * exact. The modal openers the guide itself names — always / never / do not / must /\n * should — are here alongside the verbs an instruction file actually opens with.\n */\nconst IMPERATIVE_OPENERS: readonly string[] = [\n 'add',\n 'always',\n 'avoid',\n 'build',\n 'check',\n 'commit',\n 'create',\n 'do',\n 'document',\n 'ensure',\n 'follow',\n 'format',\n 'ignore',\n 'implement',\n 'install',\n 'keep',\n 'lint',\n 'make',\n 'must',\n 'name',\n 'never',\n 'place',\n 'prefer',\n 'put',\n 'read',\n 'refuse',\n 'remove',\n 'return',\n 'run',\n 'should',\n 'skip',\n 'test',\n 'throw',\n 'treat',\n 'update',\n 'use',\n 'verify',\n 'write',\n];\n\n/**\n * Count the lines that read as instructions, one receipt each.\n *\n * Reported as `imperatives (heuristic)` and never as a precise figure, because it is\n * not one: \"Run `pnpm verify`\" counts and \"The gate is `pnpm verify`\" does not, and\n * both are the same instruction. The number is useful as a *scale* — the guide cites\n * ~150-200 as what a frontier thinking model follows consistently — and useless as a\n * threshold, which is why nothing here compares it to anything.\n */\nfunction countImperatives(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n const bare = bareText(line.text);\n if (bare === '') continue;\n const opener = /^(do not|[A-Za-z']+)/.exec(bare.toLowerCase())?.[1];\n if (opener === undefined) continue;\n const matched = opener === 'do not' ? 'do not' : opener === \"don't\" ? \"don't\" : opener;\n const counted =\n matched === 'do not' ||\n matched === \"don't\" ||\n IMPERATIVE_OPENERS.includes(matched.replace(/'.*$/, ''));\n if (!counted) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: IMPERATIVE_LINE_RULE,\n explanation:\n `opens with \"${matched}\", so it is counted as one instruction (heuristic)` +\n citing(GUIDE.instructionCeiling),\n },\n });\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * dead-link — a Markdown link whose target left the tree\n * ---------------------------------------------------------------------------------- */\n\n/** `[text](target)`, with the target captured. */\nconst MARKDOWN_LINK = /\\[[^\\]]*\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g;\n\n/**\n * CommonMark lets a link destination be wrapped in angle brackets — `[x](<a b.md>)`,\n * the only way to write one containing a space. The brackets are delimiters, not part\n * of the path, and statting `<src/kept.ts>` never resolves: the rule would report a\n * live file as dead, which is the exact failure this rule exists to avoid making.\n */\nfunction stripAngles(target: string): string {\n return target.startsWith('<') && target.endsWith('>') ? target.slice(1, -1) : target;\n}\n\nfunction findDeadLinks(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n root: string,\n reader: RepoReader,\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const match of line.text.matchAll(MARKDOWN_LINK)) {\n const target = stripFragment(stripAngles(match[1]!));\n if (target === '' || isExternal(target)) continue;\n const resolved = resolveAgainst(file.dir, target);\n if (resolved === undefined) continue;\n if (resolvesInTree(root, reader, resolved)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: DEAD_LINK_RULE,\n explanation:\n `links to \\`${target}\\`, which is not in the tree — the pointer the root ` +\n `file exists to be goes nowhere` +\n citing(GUIDE.pointsElsewhere),\n },\n });\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * dead-path — the flagship (R3)\n * ---------------------------------------------------------------------------------- */\n\n/** A token with a directory separator: `src/auth`, `./scripts/build.mjs`, `docs/`. */\nconst SLASHED = /^\\.{0,2}\\/?[\\w.+-]+(?:\\/[\\w.+-]+)*\\/?$/;\n/** A bare file name whose extension says \"this is a file in this repo\". */\nconst CODE_FILE = /^[\\w.-]+\\.(?:[cm]?[jt]sx?|json|md|ya?ml|toml|py|rs|go|rb|java|sh|sql|css|html)$/;\n/**\n * A host name: what `example.com/guide` is, and what a repository path never is.\n *\n * A scheme-less URL is the one thing that is shaped exactly like a relative path —\n * dotted word, slash, more words — and the guide this very tool cites is written\n * `aihero.dev/a-complete-guide-to-agents-md` in smelt's own help text. Only the\n * segment before the first slash is tested, so `scripts/build.sh` is untouched: `sh`\n * is a TLD *and* an extension, and which one it is depends entirely on where the dot\n * sits relative to the separator.\n */\nconst DOMAIN_HOST =\n /^[\\w-]+(?:\\.[\\w-]+)*\\.(?:com|org|net|io|dev|ai|app|co|me|sh|so|to|xyz|gg|cloud|page|info|blog)$/i;\n/**\n * `Node.js`, `Vue.js`, `Bun.sh`, `Three.js` — a product, not a file in this tree.\n *\n * Narrow on purpose: one capitalised word, then one of the five suffixes products are\n * actually named with. It costs a bare mention of a PascalCase `Button.js`, which is\n * a real filename — but that is a finding not made, and this is a false accusation not\n * made, and on the flagship rule those two are not worth the same.\n */\nconst PRODUCT_NAME = /^[A-Z][A-Za-z]*\\.(?:js|sh|ai|dev|io)$/;\n\nfunction findDeadPaths(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n root: string,\n reader: RepoReader,\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n const seen = new Set<string>();\n for (const line of lines) {\n if (line.fenced) continue;\n // Markdown link targets belong to `dead-link`; blank them so one dead pointer is\n // never reported twice under two rules.\n const withoutLinks = line.text.replace(MARKDOWN_LINK, '[]()');\n for (const token of pathCandidates(withoutLinks)) {\n const key = `${String(line.number)}\u0000${token}`;\n if (seen.has(key)) continue;\n seen.add(key);\n const resolved = resolveAgainst(file.dir, token);\n // A token that climbed out of the tree with `..` is not a path this lint can\n // rule on: it names something outside the repository, and both \"dead\" and\n // \"alive\" would be guesses about a directory nobody handed us.\n if (resolved === undefined) continue;\n if (resolvesInTree(root, reader, resolved)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: DEAD_PATH_RULE,\n explanation:\n `names \\`${token}\\`, which resolves to nothing in this tree — an agent ` +\n `reads this on every request and looks there anyway` +\n citing(GUIDE.stalenessPoisons),\n },\n });\n }\n }\n return out;\n}\n\n/**\n * The path-like tokens on one line.\n *\n * Inline code spans first, because a path in an instruction file is nearly always in\n * backticks; then bare words, filtered hard. The filters are the interesting part —\n * every one of them is a false positive this rule made before it had them:\n *\n * - `https://…`, `mailto:` — not tree paths.\n * - `@smeltjs/core`, `@types/node` — package names, which look exactly like paths.\n * - `src/**\\/*.ts` — a glob describes a set, and a set does not resolve.\n * - `pnpm run build`, `and/or` — anything with whitespace, and anything whose\n * segments carry no extension and no separator worth trusting.\n * - `v1.2/v2` style version prose, caught by requiring a real segment shape.\n * - `example.com/guide`, `aihero.dev/…` — a URL somebody wrote without its scheme.\n * - `Node.js`, `Vue.js`, `Bun.sh` — products whose names end in an extension.\n *\n * The last two are why a token *without* a separator is a candidate only when it came\n * out of a code span. In running prose a dotted bare word is far more often a product\n * than a file, and this rule's whole value is that a reader believes it: one confident\n * sentence accusing `Node.js` of having left the tree costs more trust than a dozen\n * real findings earn. In backticks the author has said \"this is a thing in my\n * repository\", and the rule takes them at their word.\n */\nfunction pathCandidates(text: string): readonly string[] {\n const tokens: string[] = [];\n const add = (raw: string, fromCodeSpan: boolean): void => {\n const token = raw.replace(/[),.:;]+$/, '').trim();\n if (token === '' || !isPathLike(token, fromCodeSpan)) return;\n tokens.push(token);\n };\n const withoutCode = text.replace(/`([^`]+)`/g, (_whole, inner: string) => {\n add(inner, true);\n return ' ';\n });\n for (const word of withoutCode.split(/\\s+/)) add(word, false);\n return tokens;\n}\n\n/**\n * Is `token` a path into this tree?\n *\n * `fromCodeSpan` is the author's own signal, and it decides the one ambiguous case: a\n * dotted word with no separator. See {@link pathCandidates}.\n */\nfunction isPathLike(token: string, fromCodeSpan: boolean): boolean {\n if (isExternal(token)) return false;\n if (token.startsWith('@')) return false;\n if (/[*?[\\]{}<>|\"'`\\\\]/.test(token)) return false;\n if (token.startsWith('#')) return false;\n if (!SLASHED.test(token)) return false;\n if (token.includes('/')) {\n // `example.com/guide` — a URL with its scheme left off, not a directory.\n return !DOMAIN_HOST.test(token.split('/')[0] ?? '');\n }\n return fromCodeSpan && !PRODUCT_NAME.test(token) && CODE_FILE.test(token);\n}\n\n/** True for anything that is not a path into this tree. */\nfunction isExternal(target: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('//') || target.startsWith('#');\n}\n\nfunction stripFragment(target: string): string {\n const cut = target.indexOf('#');\n return cut === -1 ? target : target.slice(0, cut);\n}\n\n/**\n * A token in a nested instruction file is relative to *that* file's directory, which\n * is the whole reason a nested file can hold a link the root one cannot. `../` is\n * resolved rather than refused, so a nested file may point back up the tree.\n *\n * `undefined` when the token climbs past the repository root. It used to clamp there —\n * `stack.pop()` on an empty stack is a no-op — which silently turned `../sibling/x.ts`\n * into `sibling/x.ts` and then answered a question about the wrong file, in whichever\n * direction happened to be wrong. Outside the tree, this lint has nothing to say.\n */\nfunction resolveAgainst(dir: string, token: string): string | undefined {\n const base = dir === '' ? [] : dir.split('/');\n const parts = token.replace(/\\/+$/, '').split('/');\n const stack = [...base];\n for (const part of parts) {\n if (part === '' || part === '.') continue;\n if (part === '..') {\n if (stack.length === 0) return undefined;\n stack.pop();\n } else stack.push(part);\n }\n return stack.join('/');\n}\n\n/* ------------------------------------------------------------------------------------\n * forcing-language\n * ---------------------------------------------------------------------------------- */\n\n/** ALL-CAPS words a reader meets as shouting rather than as an acronym. */\nconst SHOUTED = /\\b(ALWAYS|NEVER|MUST|DO NOT|DON'T|REQUIRED|MANDATORY|CRITICAL|IMPORTANT)\\b/;\n/** The two words the guide names in its own example of what *not* to write. */\nconst FORCING_WORDS = /\\b(always|never)\\b/i;\n\nfunction findForcingLanguage(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n const shouted = SHOUTED.exec(line.text)?.[1];\n const forcing = shouted ?? FORCING_WORDS.exec(line.text)?.[1];\n if (forcing === undefined) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: FORCING_LANGUAGE_RULE,\n explanation:\n `forces with \"${forcing}\" — forcing language spends tokens on emphasis ` +\n `rather than on information` +\n citing(GUIDE.lightTouch),\n },\n });\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * structure-dump\n * ---------------------------------------------------------------------------------- */\n\n/**\n * The characters a generated tree listing is drawn with.\n *\n * Both charsets: `tree` draws box-drawing by default and `|--` / `` `-- `` under\n * `--charset=ascii`, and the ASCII form is what lands in a file written on a machine\n * whose terminal was not UTF-8. Missing it made the whole rule silent on a fenced\n * ASCII tree — and because fenced lines skip `dead-path` too, that block produced no\n * finding of any kind.\n */\nconst TREE_DRAWING = /[├└│─]|^\\s*(?:\\|--|`--|\\|\\s{3}|\\+--)/;\n/** How many path-ish lines in a row read as a dump rather than as an example. */\nconst DUMP_RUN = 3;\n\nfunction findStructureDumps(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n\n // A fenced block whose body is a tree drawing.\n let fenceStart: ScannedLine | undefined;\n let drawn = 0;\n for (const line of lines) {\n if (line.opensFence) {\n fenceStart = line;\n drawn = 0;\n continue;\n }\n if (!line.fenced) {\n fenceStart = undefined;\n continue;\n }\n if (fenceStart === undefined) continue;\n if (TREE_DRAWING.test(line.text)) drawn += 1;\n if (drawn === 2) {\n out.push(structureFinding(file, fenceStart.number, 'a directory tree'));\n fenceStart = undefined;\n }\n }\n\n // A run of bare path lines in prose — the same dump without the box characters.\n let run = 0;\n let runStart = 0;\n for (const line of lines) {\n if (line.fenced) {\n run = 0;\n continue;\n }\n const bare = bareText(line.text);\n // A line whose whole content is one token is a path line, not prose — so the\n // code-span rule `dead-path` needs against running text has nothing to guard here.\n const isPathLine =\n bare !== '' && isPathLike(bare.replace(/`/g, '').split(/\\s+/)[0] ?? '', true);\n if (isPathLine && bare.split(/\\s+/).length <= 2) {\n if (run === 0) runStart = line.number;\n run += 1;\n if (run === DUMP_RUN) out.push(structureFinding(file, runStart, 'a run of path lines'));\n } else {\n run = 0;\n }\n }\n\n return out;\n}\n\nfunction structureFinding(file: InstructionFile, line: number, what: string): AgentsFinding {\n return {\n file: file.path,\n line,\n reason: {\n rule: STRUCTURE_DUMP_RULE,\n explanation:\n `spends the every-request budget on ${what} — layout is the fact in a ` +\n `repository that changes most often, so it is also the fact that rots first` +\n citing(GUIDE.describeCapabilities),\n },\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * generated-boilerplate — the softest rule here, and it says so\n * ---------------------------------------------------------------------------------- */\n\n/** The fingerprints an init script leaves behind, with what each one is. */\nconst BOILERPLATE_SIGNATURES: readonly (readonly [RegExp, string])[] = [\n [/\\bauto-?generated\\b/i, 'an \"auto-generated\" marker'],\n [/\\bgenerated by\\b/i, 'a \"generated by\" credit'],\n [/<!--\\s*generated/i, 'a generated-block comment'],\n [/\\b(?:claude|codex|gemini|cursor|agents?)\\s+init\\b/i, 'an init-command credit'],\n [/\\bthis file was (?:created|generated)\\b/i, 'a \"this file was generated\" line'],\n [/\\bdo not edit\\b/i, 'a \"do not edit\" banner'],\n];\n\nfunction findGeneratedBoilerplate(\n file: InstructionFile,\n lines: readonly ScannedLine[],\n): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const [pattern, what] of BOILERPLATE_SIGNATURES) {\n if (!pattern.test(line.text)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: GENERATED_BOILERPLATE_RULE,\n explanation:\n `carries ${what}, which suggests this file was generated rather than ` +\n `written. This is the softest rule here: a signature is circumstantial, ` +\n `and a hand-written file may honestly carry one, so it never means more ` +\n `than \"read this file again\"` +\n citing(GUIDE.neverGenerate),\n },\n });\n break;\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * language-rule\n * ---------------------------------------------------------------------------------- */\n\n/** Style rules that pay their every-request cost only when the agent writes code. */\nconst LANGUAGE_RULE_SIGNATURES: readonly (readonly [RegExp, string])[] = [\n [/\\bconst\\b[^\\n]*\\blet\\b|\\blet\\b[^\\n]*\\bconst\\b/, 'a const/let rule'],\n [/\\binterface\\b[^\\n]*\\btype\\b|\\btype\\b[^\\n]*\\binterface\\b/, 'an interface-vs-type rule'],\n [/\\bstrict[- ]?null(?:checks)?\\b/i, 'a strict-null rule'],\n [/\\bsemi-?colons?\\b/i, 'a semicolon rule'],\n [/\\b(?:single|double) quotes\\b/i, 'a quote-style rule'],\n [/\\barrow functions?\\b/i, 'an arrow-function rule'],\n [/\\bnamed exports?\\b|\\bdefault exports?\\b/i, 'an export-style rule'],\n [\n /\\btabs? (?:over|versus|vs\\.?) spaces?\\b|\\bspaces? (?:over|versus|vs\\.?) tabs?\\b/i,\n 'an indentation rule',\n ],\n];\n\nfunction findLanguageRules(file: InstructionFile, lines: readonly ScannedLine[]): AgentsFinding[] {\n const out: AgentsFinding[] = [];\n for (const line of lines) {\n if (line.fenced) continue;\n for (const [pattern, what] of LANGUAGE_RULE_SIGNATURES) {\n if (!pattern.test(line.text)) continue;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: LANGUAGE_RULE_RULE,\n explanation:\n `states ${what}, which is paid for on every request and is relevant on ` +\n `few of them — move it behind a link and it costs only the tasks it applies to` +\n citing(GUIDE.loadWhenRelevant),\n },\n });\n break;\n }\n }\n return out;\n}\n\n/* ------------------------------------------------------------------------------------\n * mirror-drift (R4)\n * ---------------------------------------------------------------------------------- */\n\nfunction standingOf(\n primary: InstructionFile,\n mirror: InstructionFile,\n): AgentsMirrorReport['standing'] {\n if (mirror.symlink) return 'symlink';\n return mirror.text === primary.text ? 'copy' : 'drift';\n}\n\n/**\n * A mirror that has diverged from its `AGENTS.md`.\n *\n * A byte-identical copy is **not** a finding: it is not drift, and calling it one\n * would be smelt enforcing the guide's suggestion rather than reporting a fact. What\n * the report does say, beside every copy, is that a symlink cannot drift — which is\n * the guide's suggestion offered, exactly as `smelt hooks` offers rather than does.\n */\nfunction findMirrorDrift(\n primary: InstructionFile,\n mirrors: readonly InstructionFile[],\n): AgentsFinding[] {\n return mirrors\n .filter((mirror) => standingOf(primary, mirror) === 'drift')\n .map((mirror) => ({\n file: mirror.path,\n line: 1,\n reason: {\n rule: MIRROR_DRIFT_RULE,\n explanation:\n `has diverged from \\`${primary.path}\\` (${String(mirror.bytes)} bytes against ` +\n `${String(primary.bytes)}) — two harnesses are now reading two different sets ` +\n `of instructions from one repository` +\n citing(GUIDE.symlinkMirror),\n },\n }));\n}\n\n/* ------------------------------------------------------------------------------------\n * restated-at-level (R8)\n * ---------------------------------------------------------------------------------- */\n\n/** Below this, a repeated line is a heading or a bullet marker, not an instruction. */\nconst RESTATEMENT_MIN_CHARS = 40;\n\n/**\n * The same instruction present at a level **and at one of its ancestors**.\n *\n * The guide's rule is that a nested file *merges with* the root, so a line written in\n * both is a line the agent is handed twice — paid for twice, and the second copy\n * carrying the risk that only one of them is ever updated. Reported on the deeper file,\n * because that is the copy the ancestor already covers.\n *\n * **Only ancestors.** A merge runs up the tree, never across it: an agent working in\n * `pkg/a` loads the root file and `pkg/a`'s, and never `pkg/b`'s. So a line two\n * siblings happen to share is not a line anybody is handed twice, and reporting it as\n * one would print an explanation about a merge that does not happen — a finding whose\n * sentence is false, which is worse than no finding at all (Law 2).\n */\nfunction findRestatedAcrossLevels(set: InstructionSet): AgentsFinding[] {\n /** dir → the lines that level states, each with the file that states them. */\n const stated = new Map<string, Map<string, string>>();\n const out: AgentsFinding[] = [];\n\n // Levels arrive root-first (`readInstructionSet` sorts by depth), so every ancestor\n // of a level has already been recorded by the time the level is read.\n for (const level of set.levels) {\n const file = level.primary;\n const local = new Map<string, string>();\n stated.set(level.dir, local);\n const ancestors = ancestorDirs(level.dir);\n\n scanLines(file.text).forEach((line) => {\n if (line.fenced) return;\n const normalized = bareText(line.text).toLowerCase().replace(/\\s+/g, ' ');\n if (normalized.length < RESTATEMENT_MIN_CHARS) return;\n if (local.has(normalized)) return;\n local.set(normalized, file.path);\n const earlier = ancestors\n .map((dir) => stated.get(dir)?.get(normalized))\n .find((path) => path !== undefined);\n if (earlier === undefined) return;\n out.push({\n file: file.path,\n line: line.number,\n reason: {\n rule: RESTATED_AT_LEVEL_RULE,\n explanation:\n `repeats a line already in \\`${earlier}\\`, which is above it — the levels ` +\n `merge, so the agent is handed this twice and only one copy will be kept ` +\n `up to date` +\n citing(GUIDE.nestedMerge),\n },\n });\n });\n }\n return out;\n}\n"]}