@fusengine/harness 0.1.80 → 0.1.83

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 (74) hide show
  1. package/dist/adapters/claude/index.d.mts +1 -1
  2. package/dist/adapters/claude/index.mjs +1 -1
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +11 -12
  5. package/dist/adapters/codex/index.mjs +3 -3
  6. package/dist/adapters/cursor/index.d.mts +29 -29
  7. package/dist/adapters/cursor/index.mjs +1 -1
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/adapters/hermes/index.d.mts +22 -23
  10. package/dist/adapters/hermes/index.mjs +1 -1
  11. package/dist/adapters/kimi/index.d.mts +54 -46
  12. package/dist/adapters/kimi/index.mjs +2 -2
  13. package/dist/cache/index.d.mts +1 -1
  14. package/dist/{claude-51RSspJ1.mjs → claude-D93HY8dq.mjs} +2 -2
  15. package/dist/cli/bin.d.mts +1 -1
  16. package/dist/cli/bin.mjs +129 -38
  17. package/dist/cli/index.d.mts +3 -3
  18. package/dist/cli/index.mjs +1 -1
  19. package/dist/config/index.d.mts +2 -2
  20. package/dist/config/index.mjs +4 -4
  21. package/dist/detect/index.d.mts +2 -1
  22. package/dist/{doc-helpers-CEKzGg2u.d.mts → doc-helpers-BGCzP9VF.d.mts} +12 -12
  23. package/dist/{dotenv-B26N_uGr.mjs → dotenv-DoqNlALG.mjs} +29 -2
  24. package/dist/{evaluate-ClfbCDiY.mjs → evaluate-B4-MhOy_.mjs} +322 -32
  25. package/dist/freshness/index.d.mts +2 -2
  26. package/dist/{handle-B7YufFz9.mjs → handle-F35Gx-QS.mjs} +596 -174
  27. package/dist/{hermes-rTmVdhUL.mjs → hermes-1PUEoY8q.mjs} +2 -2
  28. package/dist/{home-state-BXf38Zi1.mjs → home-state-oUGFB4ds.mjs} +4 -2
  29. package/dist/index-84heAAm_.d.mts +12 -0
  30. package/dist/index-CaxrMD1A.d.mts +1 -0
  31. package/dist/index-CigBu4K9.d.mts +14 -0
  32. package/dist/{index-DGxNjXW7.d.mts → index-D4C2-IIc.d.mts} +6 -8
  33. package/dist/index-DBgnIZn9.d.mts +756 -0
  34. package/dist/{index-mmRF3KNp.d.mts → index-DEfz82BP.d.mts} +12 -13
  35. package/dist/{index-BUwEmIK-.d.mts → index-DhmV2MPh.d.mts} +31 -31
  36. package/dist/index-oXXPs2xl.d.mts +119 -0
  37. package/dist/index-p9fLrSOD.d.mts +27 -0
  38. package/dist/index.d.mts +11 -11
  39. package/dist/index.mjs +8 -8
  40. package/dist/init/index.d.mts +8 -9
  41. package/dist/init/index.mjs +1 -1
  42. package/dist/{kimi-C7FGprQz.mjs → kimi-ChbcU2uI.mjs} +17 -3
  43. package/dist/{layout-C0jaaCQC.mjs → layout-KWoE_Mqn.mjs} +1 -0
  44. package/dist/limits-DG5pl-5s.mjs +59 -0
  45. package/dist/memory/index.mjs +1 -1
  46. package/dist/policy/index.d.mts +2 -3
  47. package/dist/policy/index.mjs +4 -4
  48. package/dist/prompt/index.d.mts +2 -1
  49. package/dist/refs/index.d.mts +1 -1
  50. package/dist/{registry-CymilZiZ.mjs → registry-IhHk2KlT.mjs} +1 -1
  51. package/dist/{run-3tyTW9Ud.mjs → run-D_WchZ3R.mjs} +1 -1
  52. package/dist/{run-Do2JltgU.mjs → run-TAXNRSpD.mjs} +1 -1
  53. package/dist/runtime/index.d.mts +453 -451
  54. package/dist/runtime/index.mjs +3 -3
  55. package/dist/{session-state-CY4iohn_.d.mts → session-state-D5gLr66m.d.mts} +29 -30
  56. package/dist/{skill-path-CDvR4bbF.mjs → skill-path-DhBGlQe3.mjs} +2 -2
  57. package/dist/store-Dw08Amqh.mjs +845 -0
  58. package/dist/tracking/index.d.mts +23 -13
  59. package/dist/tracking/index.mjs +2 -2
  60. package/dist/{types-DVbIl9md.d.mts → types-C0-igRKr.d.mts} +8 -8
  61. package/dist/types-DvXTI71X.d.mts +15 -0
  62. package/dist/util/index.d.mts +1 -1
  63. package/dist/{validate-CkmquRDV.mjs → validate-Bf-EsbDh.mjs} +279 -20
  64. package/package.json +163 -163
  65. package/dist/apex-Wdi1nq_w.d.mts +0 -73
  66. package/dist/harness-BQ5_Wnva.d.mts +0 -27
  67. package/dist/index-BEMumjOw.d.mts +0 -27
  68. package/dist/index-BIGVNQB8.d.mts +0 -576
  69. package/dist/index-BKZ67WMa.d.mts +0 -1
  70. package/dist/index-BOBXQ91y.d.mts +0 -12
  71. package/dist/index-BVVgDSdq.d.mts +0 -1
  72. package/dist/index-B_VHbsaJ.d.mts +0 -96
  73. package/dist/limits-CHn8AIL1.mjs +0 -31
  74. package/dist/store-nDZ3eLBQ.mjs +0 -349
@@ -1,5 +1,5 @@
1
- import { a as parseEnvInt, i as splitTarget, n as MAX_LINES_ENV_KEY, r as resolveMaxLines, t as DEFAULT_MAX_LINES } from "../limits-CHn8AIL1.mjs";
2
- import { a as DEFAULT_TTL_SEC, c as ttlLabel, i as parseEnvFile, n as envCandidates, o as TTL_ENV_KEY, r as loadDotenv, s as resolveTtlSec, t as HOME_DIR } from "../dotenv-B26N_uGr.mjs";
3
- import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "../layout-C0jaaCQC.mjs";
1
+ import { a as resolveMaxLines, c as storeBudget, i as hookBudget, l as parseEnvInt, n as DEFAULT_STDIN_MAX_BYTES, o as resolveStdinMaxBytes, r as MAX_LINES_ENV_KEY, s as splitTarget, t as DEFAULT_MAX_LINES } from "../limits-DG5pl-5s.mjs";
2
+ import { a as DEFAULT_TTL_SEC, c as ttlLabel, i as parseEnvFile, n as envCandidates, o as TTL_ENV_KEY, r as loadDotenv, s as resolveTtlSec, t as HOME_DIR } from "../dotenv-DoqNlALG.mjs";
3
+ import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "../layout-KWoE_Mqn.mjs";
4
4
  import "../config-la_KkjCS.mjs";
5
- export { DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, HOME_DIR, MAX_LINES_ENV_KEY, STATE_GITIGNORE, STATE_ROOT, TTL_ENV_KEY, envCandidates, loadDotenv, parseEnvFile, parseEnvInt, projectLayout, resolveMaxLines, resolveTtlSec, splitTarget, ttlLabel };
5
+ export { DEFAULT_MAX_LINES, DEFAULT_STDIN_MAX_BYTES, DEFAULT_TTL_SEC, HOME_DIR, MAX_LINES_ENV_KEY, STATE_GITIGNORE, STATE_ROOT, TTL_ENV_KEY, envCandidates, hookBudget, loadDotenv, parseEnvFile, parseEnvInt, projectLayout, resolveMaxLines, resolveStdinMaxBytes, resolveTtlSec, splitTarget, storeBudget, ttlLabel };
@@ -1,2 +1,3 @@
1
- import { a as HarnessInfo, i as HarnessId, n as detectMode, o as HarnessMode, r as modeFor, s as HarnessVia, t as detectHarness } from "../harness-BQ5_Wnva.mjs";
1
+ import { i as HarnessVia, n as HarnessInfo, r as HarnessMode, t as HarnessId } from "../types-DvXTI71X.mjs";
2
+ import { n as detectMode, r as modeFor, t as detectHarness } from "../index-CigBu4K9.mjs";
2
3
  export { type HarnessId, type HarnessInfo, type HarnessMode, type HarnessVia, detectHarness, detectMode, modeFor };
@@ -1,9 +1,9 @@
1
1
  //#region src/freshness/doc-helpers.d.ts
2
2
  /**
3
- * Documentation-consultation freshness: was ANY documentation source consulted
4
- * this session — Context7, Exa, or WebSearch/WebFetch — via a live call or a
5
- * Read on a cached MCP result file?
6
- */
3
+ * Documentation-consultation freshness: was ANY documentation source consulted
4
+ * this session — Context7, Exa, or WebSearch/WebFetch — via a live call or a
5
+ * Read on a cached MCP result file?
6
+ */
7
7
  /** Authorization entry from APEX state (legacy session + new sessions[]). */
8
8
  interface AuthEntry {
9
9
  source?: string;
@@ -25,14 +25,14 @@ interface DocSatisfactionStatus {
25
25
  viaCache: boolean;
26
26
  }
27
27
  /**
28
- * True when documentation research is satisfied for the session. Primary path
29
- * is parity with the Python `mcp_research_done`
30
- * (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
31
- * "exa:" in content`, a strict AND (the Python source has no web-alone path).
32
- * `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
33
- * it alone satisfies the gate, since fuse-browser is this harness's preferred
34
- * doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
35
- */
28
+ * True when documentation research is satisfied for the session. Primary path
29
+ * is parity with the Python `mcp_research_done`
30
+ * (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
31
+ * "exa:" in content`, a strict AND (the Python source has no web-alone path).
32
+ * `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
33
+ * it alone satisfies the gate, since fuse-browser is this harness's preferred
34
+ * doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
35
+ */
36
36
  declare function isDocConsulted(authorizations: Record<string, AuthEntry> | undefined, sessionId: string): boolean;
37
37
  /** Report how each doc source was satisfied for a session. */
38
38
  declare function formatDocSatisfactionStatus(authorizations: Record<string, AuthEntry> | undefined, sessionId: string): DocSatisfactionStatus;
@@ -1,4 +1,4 @@
1
- import { a as parseEnvInt } from "./limits-CHn8AIL1.mjs";
1
+ import { l as parseEnvInt } from "./limits-DG5pl-5s.mjs";
2
2
  import { join } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
@@ -26,6 +26,33 @@ function ttlLabel(sec) {
26
26
  return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
27
27
  }
28
28
  //#endregion
29
+ //#region src/config/home-dir.ts
30
+ /**
31
+ * Harness home-dir resolution with env overrides. `dotenv.ts`'s static
32
+ * `HOME_DIR` table knows every harness's default config dir but cannot move
33
+ * with the user's data root — Kimi Code CLI documents `KIMI_CODE_HOME` for
34
+ * exactly that (isolated data roots get isolated skills/agents/hooks), so the
35
+ * override is honored here instead of hardcoding `~/.kimi-code`.
36
+ *
37
+ * Deliberately consumed only where a USER-HOME path is built (`envCandidates`
38
+ * today): `harnessHomeSegment` (`policy/apex-target.ts`) also serves
39
+ * project-relative joins (`<project>/.kimi-code/apex`), which an absolute
40
+ * override would corrupt — those sites stay segment-based.
41
+ */
42
+ /** Documented env var relocating a harness home dir, when one exists. */
43
+ const HOME_ENV = { kimi: "KIMI_CODE_HOME" };
44
+ /**
45
+ * Resolve a harness's home config dir as an absolute path.
46
+ * @param id - Harness id.
47
+ * @param env - Environment (defaults to `process.env`).
48
+ * @param home - OS home dir (injectable for tests).
49
+ * @returns The override path when set, else `join(home, HOME_DIR[id])`.
50
+ */
51
+ function harnessHome(id, env = process.env, home = homedir()) {
52
+ const key = HOME_ENV[id];
53
+ return (key ? env[key] : void 0) ?? join(home, HOME_DIR[id] ?? ".claude");
54
+ }
55
+ //#endregion
29
56
  //#region src/config/dotenv.ts
30
57
  /**
31
58
  * Native .env loader. Ports the claude-plugins `services/env-file.ts`
@@ -64,7 +91,7 @@ function parseEnvFile(path) {
64
91
  }
65
92
  /** The `.env` paths probed for a harness: home `.env` then `<cwd>/.env`. */
66
93
  function envCandidates(id, home = homedir(), cwd = process.cwd()) {
67
- return [join(home, HOME_DIR[id] ?? ".claude", ".env"), join(cwd, ".env")];
94
+ return [join(harnessHome(id, process.env, home), ".env"), join(cwd, ".env")];
68
95
  }
69
96
  /**
70
97
  * Load `.env` files into `env` without overwriting existing keys. Reads the
@@ -1,5 +1,5 @@
1
- import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
- import { f as countLines, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-BXf38Zi1.mjs";
1
+ import { a as resolveMaxLines } from "./limits-DG5pl-5s.mjs";
2
+ import { f as countLines, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-oUGFB4ds.mjs";
3
3
  import { dirname, join, normalize, resolve } from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
@@ -83,6 +83,106 @@ function detectProjectType(dir) {
83
83
  return "generic";
84
84
  }
85
85
  //#endregion
86
+ //#region src/policy/conventions/strip.ts
87
+ /** Mask PHP heredoc/nowdoc blocks (`<<<TAG` … terminator line), offsets kept. */
88
+ function maskHeredocs(content) {
89
+ return content.replace(/<<<['"]?(\w+)['"]?\r?\n[\s\S]*?^\1;?$/gm, (block) => block.replace(/[^\n]/g, " "));
90
+ }
91
+ /**
92
+ * Mask comments and strings of `content` according to its lexical profile.
93
+ * @param content - Source text.
94
+ * @param profile - Lexical family (see `langs.ts` `lexProfileOf`).
95
+ * @returns Same-length text with comment/string interiors blanked.
96
+ */
97
+ function maskCommentsAndStrings(content, profile) {
98
+ return maskScan(content, profile, false);
99
+ }
100
+ /**
101
+ * Mask COMMENTS only (strings kept) — for detectors whose signal IS a string
102
+ * literal (module imports like `from "zustand"`): a `// import …` comment
103
+ * cannot match, while the real import line stays readable.
104
+ * @param content - Source text.
105
+ * @param profile - Lexical family.
106
+ * @returns Same-length text with comment interiors blanked.
107
+ */
108
+ function maskCommentsOnly(content, profile) {
109
+ return maskScan(content, profile, true);
110
+ }
111
+ /** Shared scanner; `keepStrings` skips the string-masking branch. */
112
+ function maskScan(content, profile, keepStrings) {
113
+ if (profile === "php") content = maskHeredocs(content);
114
+ const out = content.split("");
115
+ const blank = (i) => {
116
+ if (out[i] !== "\n") out[i] = " ";
117
+ };
118
+ const isPy = profile === "py" || profile === "rb";
119
+ let i = 0;
120
+ while (i < out.length) {
121
+ const two = content.slice(i, i + 2);
122
+ if (!isPy && two === "//") {
123
+ while (i < out.length && out[i] !== "\n") blank(i++);
124
+ continue;
125
+ }
126
+ if (isPy && out[i] === "#") {
127
+ while (i < out.length && out[i] !== "\n") blank(i++);
128
+ continue;
129
+ }
130
+ if (profile === "php" && out[i] === "#") {
131
+ while (i < out.length && out[i] !== "\n") blank(i++);
132
+ continue;
133
+ }
134
+ if (!isPy && two === "/*") {
135
+ const end = content.indexOf("*/", i + 2);
136
+ const stop = end === -1 ? out.length : end + 2;
137
+ while (i < stop) blank(i++);
138
+ continue;
139
+ }
140
+ if (isPy && (content.startsWith("'''", i) || content.startsWith("\"\"\"", i))) {
141
+ const q = content.slice(i, i + 3);
142
+ const end = content.indexOf(q, i + 3);
143
+ const stop = end === -1 ? out.length : end + 3;
144
+ while (i < stop) blank(i++);
145
+ continue;
146
+ }
147
+ if (!keepStrings && (out[i] === "'" || out[i] === "\"" || profile === "c" && out[i] === "`")) {
148
+ const q = out[i];
149
+ let j = i + 1, closed = -1;
150
+ while (j < out.length && out[j] !== "\n") {
151
+ if (out[j] === "\\") {
152
+ j += 2;
153
+ continue;
154
+ }
155
+ if (out[j] === q) {
156
+ closed = j;
157
+ break;
158
+ }
159
+ j++;
160
+ }
161
+ if (q === "`") {
162
+ if (closed !== -1) {
163
+ while (i <= closed) blank(i++);
164
+ continue;
165
+ }
166
+ const end = content.indexOf("`", i + 1);
167
+ if (end !== -1) {
168
+ while (i <= end) blank(i++);
169
+ continue;
170
+ }
171
+ i++;
172
+ continue;
173
+ }
174
+ if (closed === -1) {
175
+ i++;
176
+ continue;
177
+ }
178
+ while (i <= closed) blank(i++);
179
+ continue;
180
+ }
181
+ i++;
182
+ }
183
+ return out.join("");
184
+ }
185
+ //#endregion
86
186
  //#region src/policy/nearest-manifest.ts
87
187
  /** Manifest filenames that anchor a project root (any one marks the dir). */
88
188
  const MANIFESTS = [
@@ -137,6 +237,12 @@ function projectCaps(dir) {
137
237
  if ("@angular/core" in deps) caps.add("angular");
138
238
  if ("nuxt" in deps) caps.add("nuxt");
139
239
  if ("tailwindcss" in deps) caps.add("tailwind");
240
+ if ("@tanstack/react-start" in deps) caps.add("tanstack-start");
241
+ if ("@tanstack/react-router" in deps) caps.add("tanstack-router");
242
+ if ("@tanstack/react-query" in deps) caps.add("tanstack-query");
243
+ if ("@tanstack/vue-query" in deps) caps.add("vue-query");
244
+ if ("zustand" in deps) caps.add("zustand");
245
+ if ("pinia" in deps) caps.add("pinia");
140
246
  }
141
247
  const type = detectProjectType(dir);
142
248
  if (type === "laravel") caps.add("laravel");
@@ -153,6 +259,8 @@ const REACT_CONTENT = /from ['"]react['"]|\buse(State|Effect|Context|Ref|Memo|Ca
153
259
  const NEXT_CONTENT = /use client|use server|NextRequest|NextResponse|from ['"]next|getServerSideProps|getStaticProps/;
154
260
  /** next.js route-file conventions. */
155
261
  const NEXT_ROUTE = /(page|layout|loading|error|route|middleware)\.(ts|tsx|js|jsx)$/;
262
+ /** TanStack Start markers (file routes, server fns, start/router imports). */
263
+ const TANSTACK_CONTENT = /createFileRoute|createServerFn|from ['"]@tanstack\/react-(start|router)/;
156
264
  /** Derive the raw signal an extension + content carry (no filesystem access). */
157
265
  function fileSignal(filePath, content) {
158
266
  if (/\.swift$/.test(filePath)) return { definitive: "swift" };
@@ -162,11 +270,14 @@ function fileSignal(filePath, content) {
162
270
  if (/\.rb$/.test(filePath)) return { definitive: "ruby" };
163
271
  if (/\.rs$/.test(filePath)) return { definitive: "rust" };
164
272
  if (/\.css$/.test(filePath) || /@tailwind|@apply/.test(content)) return { definitive: "tailwind" };
165
- const isNext = NEXT_ROUTE.test(filePath) || NEXT_CONTENT.test(content);
166
- if (/\.(tsx|jsx)$/.test(filePath)) return { jsHint: isNext ? "nextjs" : "react" };
273
+ const masked = maskCommentsAndStrings(content, "c");
274
+ const isTs = TANSTACK_CONTENT.test(masked);
275
+ const isNext = !isTs && (NEXT_ROUTE.test(filePath) || NEXT_CONTENT.test(masked));
276
+ if (/\.(tsx|jsx)$/.test(filePath)) return { jsHint: isTs ? "tanstack-start" : isNext ? "nextjs" : "react" };
167
277
  if (/\.(ts|js)$/.test(filePath)) {
278
+ if (isTs) return { jsHint: "tanstack-start" };
168
279
  if (isNext) return { jsHint: "nextjs" };
169
- if (REACT_CONTENT.test(content)) return { jsHint: "react" };
280
+ if (REACT_CONTENT.test(masked)) return { jsHint: "react" };
170
281
  }
171
282
  return {};
172
283
  }
@@ -174,6 +285,7 @@ function fileSignal(filePath, content) {
174
285
  function reconcile(caps, hint) {
175
286
  const nextCap = caps.has("nextjs");
176
287
  const reactCap = caps.has("react") || nextCap;
288
+ if (hint === "tanstack-start") return caps.has("tanstack-start") || caps.has("tanstack-router") ? "tanstack-start" : reactCap ? "react" : "generic";
177
289
  if (hint === "nextjs") return nextCap ? "nextjs" : reactCap ? "react" : "generic";
178
290
  return reactCap ? "react" : "generic";
179
291
  }
@@ -182,13 +294,14 @@ function reconcile(caps, hint) {
182
294
  * manifest's capabilities) and the file's own extension/content signal. A
183
295
  * backend `.ts` in a react project is `generic` (no react signal) — fixing the
184
296
  * old extension-only default that flagged every `.ts` as react. Fail-open: any
185
- * error yields `"generic"`, never throws. Return values stay within the existing
186
- * union ("react" | "nextjs" | "laravel" | "swift" | "tailwind" | "java" | "go" |
187
- * "ruby" | "rust" | "generic") — no new labels.
297
+ * error yields `"generic"`, never throws. Return values stay within the label
298
+ * union ("react" | "nextjs" | "tanstack-start" | "laravel" | "swift" |
299
+ * "tailwind" | "java" | "go" | "ruby" | "rust" | "generic") — `tanstack-start`
300
+ * added 2026-07 (owner spec: first-class framework, Next.js-equivalent).
188
301
  * @param filePath - Path of the file being written/edited.
189
302
  * @param content - Its (incoming) content, for JS content signals.
190
303
  * @param cwd - Root to resolve a relative `filePath` against (default cwd).
191
- * @returns A framework label from the existing union.
304
+ * @returns A framework label from the union.
192
305
  */
193
306
  function detectFramework(filePath, content, cwd = process.cwd()) {
194
307
  try {
@@ -886,6 +999,179 @@ function moduleAwarePath(filePath, subpath, fallback) {
886
999
  return name ? `modules/${name}/${subpath}` : fallback;
887
1000
  }
888
1001
  //#endregion
1002
+ //#region src/policy/conventions/langs.ts
1003
+ const EXT_LANG = {
1004
+ ts: "ts",
1005
+ tsx: "ts",
1006
+ js: "ts",
1007
+ jsx: "ts",
1008
+ mts: "ts",
1009
+ mjs: "ts",
1010
+ astro: "ts",
1011
+ svelte: "ts",
1012
+ py: "py",
1013
+ go: "go",
1014
+ rs: "rs",
1015
+ java: "java",
1016
+ kt: "java",
1017
+ php: "php",
1018
+ swift: "swift",
1019
+ rb: "rb",
1020
+ vue: "vue"
1021
+ };
1022
+ /** Language family for a file path (by extension), or null when unsupported. */
1023
+ function langOfPath(filePath) {
1024
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
1025
+ return EXT_LANG[ext] ?? null;
1026
+ }
1027
+ /** Lexical profile for masking, derived from the language family. */
1028
+ function lexProfileOf(lang) {
1029
+ if (lang === "py") return "py";
1030
+ if (lang === "rb") return "rb";
1031
+ if (lang === "php") return "php";
1032
+ return "c";
1033
+ }
1034
+ /** True for dotfiles/dependency dirs never subject to conventions. */
1035
+ function isVendorPath(filePath) {
1036
+ return /(^|\/)(node_modules|vendor|dist|build|\.next|coverage)\//.test(filePath);
1037
+ }
1038
+ /** Path predicates — canonical convention directories. */
1039
+ const isInterfacesPath = (p) => /\/(interfaces|Contracts|Protocols|traits)\//.test(p) || p.endsWith("traits.rs");
1040
+ const isTypesPath = (p) => p.includes("/types/") || p.endsWith(".d.ts");
1041
+ const isHooksPath = (p) => p.includes("/hooks/") || p.includes("/composables/");
1042
+ const isStoresPath = (p) => p.includes("/stores/");
1043
+ const isQueryPath = (p) => p.includes("/query/");
1044
+ const isComponentsPath = (p) => p.includes("/components/");
1045
+ //#endregion
1046
+ //#region src/policy/conventions/interfaces.ts
1047
+ /**
1048
+ * Interface/type-contract declaration detection, per language, over MASKED
1049
+ * content (see `strip.ts` — comments and strings never match). Single source
1050
+ * consumed by every interface gate; the four historical layers each carried
1051
+ * their own regexes (alias `type X =` false positives, unanchored
1052
+ * `class.*ABC`, missed `public protocol`/`sealed interface`/generics).
1053
+ *
1054
+ * Rule symmetry (owner spec): an EXPORTED contract belongs to its directory
1055
+ * (interfaces/ resp. types/); a non-exported local declaration is allowed.
1056
+ *
1057
+ * Each detection carries a LEVEL: `"legacy"` (the exact historical semantics,
1058
+ * hard deny byte-identical) vs `"extended"` (syntax widenings — Go
1059
+ * `interface{` without the space, Python `Protocol` — advisory-first via
1060
+ * `FUSE_CONVENTIONS_MODE`, owner Amendment 5). Go anchors at column 0: an
1061
+ * interface declared INSIDE a function (indented, a true false positive — a
1062
+ * local type cannot move to internal/interfaces/) never matches.
1063
+ */
1064
+ const LEGACY_RE = {
1065
+ ts: /^\s*export\s+(?:declare\s+)?interface\s+\w+/m,
1066
+ py: /^\s*class\s+\w+\s*\([^)]*\b(?:ABC|ABCMeta)\b/m,
1067
+ go: /^type\s+\w+\s+interface\s+\{/m,
1068
+ rs: /^\s*(?:pub\s+)?trait\s+\w+/m,
1069
+ java: /^\s*(?:(?:public|sealed|final|abstract)\s+)*(?:interface|@FunctionalInterface)\s*\w*|^\s*fun\s+interface\s+\w+/m,
1070
+ php: /^\s*(?:abstract\s+class|interface)\s+\w+/m,
1071
+ swift: /^\s*(?:(?:public|open|internal|private|fileprivate)\s+)?protocol\s+\w+/m
1072
+ };
1073
+ const EXTENDED_RE = {
1074
+ py: /^\s*class\s+\w+\s*\([^)]*\bProtocol\b/m,
1075
+ go: /^type\s+\w+\s+interface\b/m
1076
+ };
1077
+ /** Exported TS type alias (`export type X = …`) — belongs in `src/types/`. */
1078
+ const TYPE_ALIAS_RE = /^\s*export\s+type\s+\w+\s*[=<]/m;
1079
+ /**
1080
+ * Classify the interface declaration level of a file (masked scan), or null.
1081
+ * @param filePath - File path (extension selects the language).
1082
+ * @param content - Raw file content (masked internally).
1083
+ */
1084
+ function interfaceDeclLevel(filePath, content) {
1085
+ const lang = langOfPath(filePath);
1086
+ if (!lang) return null;
1087
+ const masked = maskCommentsAndStrings(content, lexProfileOf(lang));
1088
+ if (LEGACY_RE[lang]?.test(masked)) return "legacy";
1089
+ if (EXTENDED_RE[lang]?.test(masked)) return "extended";
1090
+ return null;
1091
+ }
1092
+ /**
1093
+ * True when the file declares an interface-like contract at any level.
1094
+ * @param filePath - File path (extension selects the language).
1095
+ * @param content - Raw file content (masked internally).
1096
+ */
1097
+ function declaresInterface(filePath, content) {
1098
+ return interfaceDeclLevel(filePath, content) !== null;
1099
+ }
1100
+ /**
1101
+ * True when a TS-family file declares an EXPORTED type alias (masked scan).
1102
+ * @param content - Raw file content.
1103
+ */
1104
+ function declaresExportedTypeAlias(content) {
1105
+ return TYPE_ALIAS_RE.test(maskCommentsAndStrings(content, "c"));
1106
+ }
1107
+ //#endregion
1108
+ //#region src/policy/conventions/verdict.ts
1109
+ /**
1110
+ * Current rollout mode: `advisory` ONLY on the exact explicit value —
1111
+ * anything else (absent, typo, empty) is `deny` (owner decision: deny-first).
1112
+ * @param env - Environment (defaults to `process.env`).
1113
+ */
1114
+ function conventionsMode(env = process.env) {
1115
+ return env.FUSE_CONVENTIONS_MODE === "advisory" ? "advisory" : "deny";
1116
+ }
1117
+ /**
1118
+ * Resolve a convention verdict under the rollout flag: a NEW rule yields an
1119
+ * `inform` prompt in advisory mode (explicit opt-out), the blocking prompt
1120
+ * otherwise; an EXISTING rule always yields the blocking prompt unchanged.
1121
+ * @param prompt - The blocking prompt the rule would emit.
1122
+ * @param isNew - True when the rule is new (post-conventions module).
1123
+ * @param env - Environment (defaults to `process.env`).
1124
+ */
1125
+ function rolloutVerdict(prompt, isNew, env = process.env) {
1126
+ if (!isNew || conventionsMode(env) === "deny") return prompt;
1127
+ return {
1128
+ kind: "inform",
1129
+ title: prompt.title,
1130
+ reason: prompt.reason,
1131
+ actions: prompt.actions
1132
+ };
1133
+ }
1134
+ //#endregion
1135
+ //#region src/policy/guards/interface-separation-ext.ts
1136
+ /**
1137
+ * Extended interface-syntax violations (owner E3, advisory-first): the
1138
+ * syntaxes the guard's legacy regexes miss — Swift `public protocol`,
1139
+ * Java/Kotlin `sealed`/`fun interface`, Go unexported top-level interface
1140
+ * (`type foo interface {` at column 0 — indented locals stay excluded, D0.2
1141
+ * invariant). Detection rides the conventions module on MASKED content; the
1142
+ * caller wraps the verdict with `rolloutVerdict`, so these surface as
1143
+ * `inform` until `FUSE_CONVENTIONS_MODE=deny`. Legacy cases never reach this
1144
+ * file (the guard's hard-deny branches return first).
1145
+ */
1146
+ /** Legacy anchors (kept in sync with interface-separation.ts by test). */
1147
+ const SWIFT_LEGACY_RE = /^\s*protocol\s+[A-Z]/m;
1148
+ const JAVA_LEGACY_RE = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:interface|record)\s+[A-Z]/m;
1149
+ const GO_LEGACY_RE = /^\s*type\s+[A-Z]\w*\s+interface\b/m;
1150
+ const inAny = (path, ...frags) => frags.some((f) => path.includes(f));
1151
+ /**
1152
+ * The extended-syntax violation for a file, or null when clean/legacy.
1153
+ * @param path - The written/edited file's path.
1154
+ * @param content - Its raw content (masked internally).
1155
+ * @returns Message + action for the advisory prompt.
1156
+ */
1157
+ function extendedInterfaceViolation(path, content) {
1158
+ if (!declaresInterface(path, content)) return null;
1159
+ const masked = maskCommentsAndStrings(content, lexProfileOf(langOfPath(path) ?? "ts"));
1160
+ if (/\.swift$/.test(path) && inAny(path, "View/", "Views/", "Component/", "Components/") && !SWIFT_LEGACY_RE.test(masked)) return {
1161
+ msg: "Protocol in view file. Move to Sources/Interfaces/",
1162
+ action: "Move the protocol to Sources/Interfaces/"
1163
+ };
1164
+ if (/\.(java|kt)$/.test(path) && inAny(path, "controller/", "controllers/", "handler/", "handlers/") && !JAVA_LEGACY_RE.test(masked)) return {
1165
+ msg: "Interface in controller file. Move to interfaces/ package",
1166
+ action: "Move the interface to the interfaces/ package"
1167
+ };
1168
+ if (/\.go$/.test(path) && inAny(path, "handler/", "handlers/", "controller/", "controllers/") && !GO_LEGACY_RE.test(masked)) return {
1169
+ msg: "Interface in handler file. Move to internal/interfaces/",
1170
+ action: "Move the interface to internal/interfaces/"
1171
+ };
1172
+ return null;
1173
+ }
1174
+ //#endregion
889
1175
  //#region src/policy/guards/interface-separation.ts
890
1176
  /** TS/JS component files: top-level `interface`/`type Foo`. */
891
1177
  const TS_DECL_RE = /^\s*(export\s+)?(interface|type)\s+[A-Z]/m;
@@ -907,45 +1193,49 @@ const JAVA_DECL_RE = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:i
907
1193
  const RUST_DECL_RE = /^\s*(?:pub(?:\(\w+\))?\s+)?trait\s+([A-Z]\w*)/m;
908
1194
  /**
909
1195
  * Blocks top-level interface/type/protocol declarations in component, view or
910
- * controller files (Interface Segregation). Fires only when BOTH the path
911
- * category AND the content pattern match.
912
- *
913
- * Destination text for TS/JS/Vue/Svelte, PHP and Swift matches the user's own
914
- * `claude-rules/rules/04-solid-dry-rules.md` ("SOLID Skill per Stack" table),
915
- * the current authoritative convention NOT the older `enforce-interfaces.py`
916
- * text, which this guard originally ported. Go/Python/Java/Kotlin aren't
917
- * covered by that table, so their destinations stay as a reasonable default.
918
- *
919
- * Parity note: enforce-interfaces.py only inspects `Write` (tool_input.content).
920
- * We deliberately also fire on `Edit` an in-place edit can introduce the same
921
- * violation — and the path fragments accept singular *and* plural directory
922
- * names (`view/` + `views/`), mirroring the Python `s?` regexes.
1196
+ * controller files (Interface Segregation), when BOTH the path category AND
1197
+ * the content pattern match. Content is MASKED first (conventions/strip.ts):
1198
+ * declarations in comments/strings/templates/heredocs never trigger. Legacy
1199
+ * regexes keep their exact values (parity tests) every case that blocked
1200
+ * before still blocks with the same message (byte-parity). Two extension
1201
+ * classes ship ADVISORY via {@link rolloutVerdict} (`FUSE_CONVENTIONS_MODE`,
1202
+ * Amendment 5): exported type aliases redirected to `src/types/` (Amendment 2),
1203
+ * and the widenings of `interface-separation-ext.ts` (public protocol,
1204
+ * sealed/fun interface, unexported Go top-level). Rust `impl<T>` stays a
1205
+ * known FN. Parity: the Python port inspected `Write` only; we also fire on
1206
+ * `Edit`, and fragments accept singular+plural dirs (`view/` + `views/`).
923
1207
  */
924
1208
  function interfaceSeparationGuard(ctx) {
925
1209
  if (ctx.tool !== "Write" && ctx.tool !== "Edit") return null;
926
1210
  const path = ctx.filePath;
927
1211
  const content = ctx.content;
928
1212
  if (!path || !content) return null;
1213
+ const masked = maskCommentsAndStrings(content, lexProfileOf(langOfPath(path) ?? "ts"));
929
1214
  const blockWith = (msg, action) => ({
930
1215
  kind: "block",
931
1216
  title: "Separate the interface",
932
1217
  reason: `SOLID VIOLATION: ${msg}`,
933
1218
  actions: [action]
934
1219
  });
935
- const inAny = (...frags) => frags.some((f) => path.includes(f));
936
- if (/\.(tsx|jsx|vue|svelte)$/.test(path) && TS_DECL_RE.test(content)) return blockWith("Interface/type in component file. Move to modules/[feature]/src/interfaces/", "Move the interface/type to modules/[feature]/src/interfaces/");
937
- if (/\.py$/.test(path) && inAny("view/", "views/", "controller/", "controllers/", "route/", "routes/") && PY_MODEL_RE.test(content)) return blockWith("Type class in view file. Move to src/interfaces/", "Move the type class to src/interfaces/");
938
- if (/\.go$/.test(path) && inAny("handler/", "handlers/", "controller/", "controllers/") && GO_DECL_RE.test(content)) return blockWith("Interface in handler file. Move to internal/interfaces/", "Move the interface to internal/interfaces/");
939
- if (/\.(java|kt)$/.test(path) && inAny("controller/", "controllers/", "handler/", "handlers/") && JAVA_DECL_RE.test(content)) return blockWith("Interface in controller file. Move to interfaces/ package", "Move the interface to the interfaces/ package");
940
- if (/\.php$/.test(path) && inAny("Controller/", "Controllers/", "Handler/", "Handlers/") && PHP_DECL_RE.test(content)) return blockWith("Interface in controller file. Move to app/Contracts/", "Move the interface to app/Contracts/");
941
- if (/\.swift$/.test(path) && inAny("View/", "Views/", "Component/", "Components/") && SWIFT_PROTO_RE.test(content)) return blockWith("Protocol in view file. Move to Sources/Interfaces/", "Move the protocol to Sources/Interfaces/");
1220
+ const inAny = (...frags) => frags.some((f) => path.includes(`/${f}`) || path.startsWith(f));
1221
+ if (/\.(tsx|jsx|vue|svelte)$/.test(path) && TS_DECL_RE.test(masked)) {
1222
+ if (!/^\s*(export\s+)?(declare\s+)?interface\s+\w/.test(masked) && declaresExportedTypeAlias(content)) return rolloutVerdict(blockWith("Type alias in component file. Move to modules/[feature]/src/types/", "Move the type alias to modules/[feature]/src/types/"), true);
1223
+ return blockWith("Interface/type in component file. Move to modules/[feature]/src/interfaces/", "Move the interface/type to modules/[feature]/src/interfaces/");
1224
+ }
1225
+ if (/\.py$/.test(path) && inAny("view/", "views/", "controller/", "controllers/", "route/", "routes/") && PY_MODEL_RE.test(masked)) return blockWith("Type class in view file. Move to src/interfaces/", "Move the type class to src/interfaces/");
1226
+ if (/\.go$/.test(path) && inAny("handler/", "handlers/", "controller/", "controllers/") && GO_DECL_RE.test(masked)) return blockWith("Interface in handler file. Move to internal/interfaces/", "Move the interface to internal/interfaces/");
1227
+ if (/\.(java|kt)$/.test(path) && inAny("controller/", "controllers/", "handler/", "handlers/") && JAVA_DECL_RE.test(masked)) return blockWith("Interface in controller file. Move to interfaces/ package", "Move the interface to the interfaces/ package");
1228
+ if (/\.php$/.test(path) && inAny("Controller/", "Controllers/", "Handler/", "Handlers/") && PHP_DECL_RE.test(masked)) return blockWith("Interface in controller file. Move to app/Contracts/", "Move the interface to app/Contracts/");
1229
+ if (/\.swift$/.test(path) && inAny("View/", "Views/", "Component/", "Components/") && SWIFT_PROTO_RE.test(masked)) return blockWith("Protocol in view file. Move to Sources/Interfaces/", "Move the protocol to Sources/Interfaces/");
942
1230
  if (/\.rs$/.test(path) && !inAny("traits/", "interfaces/") && !path.endsWith("traits.rs")) {
943
- const name = RUST_DECL_RE.exec(content)?.[1];
944
- if (name && new RegExp(`impl\\s+${name}\\s+for\\s+\\w+`).test(content)) {
1231
+ const name = RUST_DECL_RE.exec(masked)?.[1];
1232
+ if (name && new RegExp(`impl\\s+${name}\\s+for\\s+\\w+`).test(masked)) {
945
1233
  const dest = moduleAwarePath(path, "traits.rs", "a dedicated traits.rs / interfaces module");
946
1234
  return blockWith(`Trait declared alongside its implementation. Move the trait to ${dest}`, `Move the trait to ${dest}`);
947
1235
  }
948
1236
  }
1237
+ const ext = extendedInterfaceViolation(path, content);
1238
+ if (ext) return rolloutVerdict(blockWith(ext.msg, ext.action), true);
949
1239
  return null;
950
1240
  }
951
1241
  //#endregion
@@ -1196,4 +1486,4 @@ function evaluate(ctx) {
1196
1486
  };
1197
1487
  }
1198
1488
  //#endregion
1199
- export { GIT_ASK as A, detectProjectType as B, SESSION_STATE_FRAGMENT as C, ASK_PATTERNS as D, protectedPathGuard as E, isRalphMode as F, requiredArchSkill as H, matchPatterns as I, detectFramework as L, PROJECT_INSTALL as M, RALPH_SAFE as N, CRITICAL_PATTERNS as O, SYSTEM_INSTALL as P, DEV_KEYWORDS as R, SAFE_PREFIXES as S, PROTECTED_GIT_RE as T, isApexCommand as V, bashWriteGuard as _, registerGuard as a, CODE_REDIRECT as b, GO_DECL_RE as c, PY_MODEL_RE as d, RUST_DECL_RE as f, moduleAwarePath as g, interfaceSeparationGuard as h, clearUserGuards as i, GIT_BLOCKED as j, securityGuard as k, JAVA_DECL_RE as l, TS_DECL_RE as m, FAIL_CLOSED as n, runGuards as o, SWIFT_PROTO_RE as p, GUARDS as r, installGuard as s, evaluate as t, PHP_DECL_RE as u, ASK_WRITERS as v, PROTECTED_FRAGMENTS as w, FILE_REDIRECT as x, CODE_MUTATORS as y, detectModularArchitecture as z };
1489
+ export { matchPatterns as $, moduleAwarePath as A, protectedPathGuard as B, isInterfacesPath as C, isVendorPath as D, isTypesPath as E, FILE_REDIRECT as F, isFileSizeScoped as G, CRITICAL_PATTERNS as H, SAFE_PREFIXES as I, GIT_BLOCKED as J, resolveSolidRefFramework as K, SESSION_STATE_FRAGMENT as L, ASK_WRITERS as M, CODE_MUTATORS as N, langOfPath as O, CODE_REDIRECT as P, isRalphMode as Q, PROTECTED_FRAGMENTS as R, isHooksPath as S, isStoresPath as T, securityGuard as U, ASK_PATTERNS as V, computeEditResultLines as W, RALPH_SAFE as X, PROJECT_INSTALL as Y, SYSTEM_INSTALL as Z, rolloutVerdict as _, registerGuard as a, DEV_KEYWORDS as at, interfaceDeclLevel as b, GO_DECL_RE as c, isApexCommand as ct, PY_MODEL_RE as d, detectFramework as et, RUST_DECL_RE as f, conventionsMode as g, interfaceSeparationGuard as h, clearUserGuards as i, maskCommentsOnly as it, bashWriteGuard as j, lexProfileOf as k, JAVA_DECL_RE as l, requiredArchSkill as lt, TS_DECL_RE as m, FAIL_CLOSED as n, projectCaps as nt, runGuards as o, detectModularArchitecture as ot, SWIFT_PROTO_RE as p, GIT_ASK as q, GUARDS as r, maskCommentsAndStrings as rt, installGuard as s, detectProjectType as st, evaluate as t, nearestManifestDir as tt, PHP_DECL_RE as u, declaresExportedTypeAlias as v, isQueryPath as w, isComponentsPath as x, declaresInterface as y, PROTECTED_GIT_RE as z };
@@ -1,3 +1,3 @@
1
- import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "../doc-helpers-CEKzGg2u.mjs";
2
- import { t as incrementTrivialEditCounter } from "../index-BOBXQ91y.mjs";
1
+ import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "../doc-helpers-BGCzP9VF.mjs";
2
+ import { t as incrementTrivialEditCounter } from "../index-84heAAm_.mjs";
3
3
  export { AuthEntry, DocSatisfactionStatus, formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };