@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,19 +1,29 @@
1
- import { a as recordAgent, c as recordRefRead, d as trivialCount, i as emptyTrack, l as recordTarget, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as recordTrivialEdit } from "../session-state-CY4iohn_.mjs";
2
-
1
+ import { a as recordAgent, c as recordRefRead, d as trivialCount, i as emptyTrack, l as recordTarget, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as recordTrivialEdit } from "../session-state-D5gLr66m.mjs";
3
2
  //#region src/tracking/store.d.ts
3
+ /** Journal mode is the default; `FUSE_TRACK_JOURNAL=0` reverts to the legacy locked RMW (see the module NOTE: =0 ignores the uncompacted journal — recoverable, keep the `.log`). */
4
+ declare function trackJournalEnabled(): boolean;
4
5
  /**
5
- * Load and verify a session track from a signed envelope file.
6
- *
7
- * Returns {@link emptyTrack} (fail closed) when the file is absent, corrupt, or
8
- * fails MAC validation — so the gates re-require real agents rather than trust a
9
- * forged track. Only a MAC mismatch triggers fail-closed; the nonce is advisory
10
- * and is never checked during load.
11
- */
6
+ * Load a session track: the verified legacy snapshot (the migration base — an
7
+ * old `track-<sid>.json` with no log reads exactly as before) with the journal
8
+ * folded on top. Fail closed on tampering (envelope MAC, per-line MAC).
9
+ */
12
10
  declare function loadTrack(file: string): Promise<SessionTrack>;
13
11
  /**
14
- * Sign and persist a session track as a tamper-evident envelope, then write the
15
- * advisory nonce watermark.
16
- */
12
+ * Persist a session track. Journal mode appends the diff as signed events;
13
+ * legacy mode signs and persists the tamper-evident envelope, then writes the
14
+ * advisory nonce watermark.
15
+ */
17
16
  declare function saveTrack(file: string, track: SessionTrack): Promise<void>;
17
+ /**
18
+ * Mutate a session track. Journal mode: load (snapshot ⊕ log), run `mutate`,
19
+ * append the resulting events — lock-free, so a write is only ever lost to a
20
+ * genuine I/O error (reported as `false`). Legacy mode: locked RMW; on lock
21
+ * contention the write is SKIPPED (fail-open, stderr logged) and `false` is
22
+ * returned — a named lost write, never a crash.
23
+ * @param file - The session track file.
24
+ * @param mutate - Pure mutation over the freshly loaded track.
25
+ * @returns True when the write landed.
26
+ */
27
+ declare function withTrack(file: string, mutate: (track: SessionTrack) => SessionTrack | Promise<SessionTrack>): Promise<boolean>;
18
28
  //#endregion
19
- export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trivialCount };
29
+ export { AgentQuality, SessionTrack, agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
@@ -1,2 +1,2 @@
1
- import { c as recordAgent, d as recordRefRead, f as recordTarget, l as recordBrainstormRequired, m as trivialCount, n as saveTrack, o as agentsFresh, p as recordTrivialEdit, s as emptyTrack, t as loadTrack, u as recordDoc } from "../store-nDZ3eLBQ.mjs";
2
- export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trivialCount };
1
+ import { S as trivialCount, _ as recordBrainstormRequired, b as recordTarget, g as recordAgent, h as emptyTrack, i as withTrack, m as agentsFresh, n as saveTrack, r as trackJournalEnabled, t as loadTrack, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "../store-Dw08Amqh.mjs";
2
+ export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTarget, recordTrivialEdit, saveTrack, trackJournalEnabled, trivialCount, withTrack };
@@ -2,10 +2,10 @@
2
2
  /** How a policy outcome should surface in a harness. */
3
3
  type PromptKind = "ask" | "block" | "inform";
4
4
  /**
5
- * Portable, harness-agnostic representation of a policy outcome as a prompt.
6
- * Adapters render this into their harness's native shape (a Claude
7
- * `permissionDecision`, a Cursor block, a CLI exit + message, ...).
8
- */
5
+ * Portable, harness-agnostic representation of a policy outcome as a prompt.
6
+ * Adapters render this into their harness's native shape (a Claude
7
+ * `permissionDecision`, a Cursor block, a CLI exit + message, ...).
8
+ */
9
9
  interface Prompt {
10
10
  kind: PromptKind;
11
11
  /** Short title, e.g. "SOLID file-size limit". */
@@ -15,10 +15,10 @@ interface Prompt {
15
15
  /** Concrete next actions to proceed. */
16
16
  actions?: string[];
17
17
  /**
18
- * User-facing notice (Python `hook_output.allow_pass`/`post_pass` parity):
19
- * shown to the HUMAN — Claude Code's `systemMessage` — never to the agent,
20
- * unlike `reason`/additionalContext. Excluded from {@link formatPrompt}.
21
- */
18
+ * User-facing notice (Python `hook_output.allow_pass`/`post_pass` parity):
19
+ * shown to the HUMAN — Claude Code's `systemMessage` — never to the agent,
20
+ * unlike `reason`/additionalContext. Excluded from {@link formatPrompt}.
21
+ */
22
22
  userMessage?: string;
23
23
  }
24
24
  /** Render a {@link Prompt} as a consistent, agent-readable memo block. */
@@ -0,0 +1,15 @@
1
+ //#region src/detect/interfaces/types.d.ts
2
+ /** Known AI coding harnesses detectable at runtime. */
3
+ type HarnessId = "claude-code" | "codex" | "cursor" | "cline" | "gemini-cli" | "opencode" | "windsurf" | "copilot" | "aider" | "kiro" | "goose" | "amp" | "hermes" | "kimi" | "unknown";
4
+ /** Integration mode: `hook` = native lifecycle hooks; `cli` = run as an external step. */
5
+ type HarnessMode = "hook" | "cli";
6
+ /** How the harness was identified. */
7
+ type HarnessVia = "agent-std" | "ai-agent-std" | "env" | "fallback";
8
+ /** Result of {@link detectHarness}. */
9
+ interface HarnessInfo {
10
+ id: HarnessId;
11
+ mode: HarnessMode;
12
+ via: HarnessVia;
13
+ }
14
+ //#endregion
15
+ export { HarnessVia as i, HarnessInfo as n, HarnessMode as r, HarnessId as t };
@@ -1,2 +1,2 @@
1
- import { a as compactJson, i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "../index-BEMumjOw.mjs";
1
+ import { a as compactJson, i as walkUpFor, n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "../index-p9fLrSOD.mjs";
2
2
  export { compactJson, isCodeFile, projectRoot, projectRootOrNull, walkUpFor };
@@ -1,11 +1,11 @@
1
- import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
- import { c as ttlLabel, t as HOME_DIR } from "./dotenv-B26N_uGr.mjs";
3
- import { R as DEV_KEYWORDS, g as moduleAwarePath } from "./evaluate-ClfbCDiY.mjs";
4
- import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-BXf38Zi1.mjs";
1
+ import { a as resolveMaxLines, c as storeBudget, i as hookBudget } from "./limits-DG5pl-5s.mjs";
2
+ import { c as ttlLabel, t as HOME_DIR } from "./dotenv-DoqNlALG.mjs";
3
+ import { A as moduleAwarePath, S as isHooksPath, T as isStoresPath, _ as rolloutVerdict, at as DEV_KEYWORDS, it as maskCommentsOnly, nt as projectCaps, rt as maskCommentsAndStrings, tt as nearestManifestDir, x as isComponentsPath, y as declaresInterface } from "./evaluate-B4-MhOy_.mjs";
4
+ import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-oUGFB4ds.mjs";
5
5
  import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-CWZegVdR.mjs";
6
6
  import { t as routeReferences } from "./router-PKVNBHge.mjs";
7
- import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-CDvR4bbF.mjs";
8
- import { extname, join } from "node:path";
7
+ import { n as findMarketplacePlugins, r as readPluginMeta, t as resolveSkillPath } from "./skill-path-DhBGlQe3.mjs";
8
+ import { dirname, extname, join, resolve } from "node:path";
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  //#region src/refs/ref-key.ts
@@ -90,7 +90,7 @@ const solidReadGate = (ctx) => {
90
90
  if (missing.length === 0) return null;
91
91
  const optional = routed.optional.map((r) => r.meta.filePath);
92
92
  const reason = [
93
- `Read these before editing ${ctx.filePath} (expires every ${ttl}):`,
93
+ `Read ALL references below (including Optional), then retry — required before editing ${ctx.filePath} (expires every ${ttl}):`,
94
94
  ...optional.length ? [`Optional: ${optional.join(", ")}`] : [],
95
95
  `Full skill: ${routed.skillPath}`
96
96
  ].join("\n");
@@ -229,12 +229,13 @@ function isExcludedSwiftPath(filePath) {
229
229
  /**
230
230
  * This harness's own gate-detection source files. They hold the React/Next
231
231
  * detection literals as regex source text (client-directive string, hook/
232
- * event names) — scanning them as JS/TS would self-match the very heuristics
233
- * they define. Exempt by exact filename, not by directory, so a genuine SOLID
234
- * violation elsewhere in src/policy/ stays gated by the framework-agnostic
235
- * line-size check in policy/evaluate.ts, untouched by this exemption.
232
+ * event names, store/query/route signals) — scanning them as JS/TS would
233
+ * self-match the very heuristics they define. Exempt by exact filename or by
234
+ * the conventions/ detector directory, not by broad directory, so a genuine
235
+ * SOLID violation elsewhere in src/policy/ stays gated by the
236
+ * framework-agnostic line-size check in policy/evaluate.ts, untouched.
236
237
  */
237
- const SELF_GATE_EXCLUDE_RE = /(^|\/)src\/policy\/framework-solid-gates(-systems)?\.ts$/;
238
+ const SELF_GATE_EXCLUDE_RE = /(^|\/)src\/policy\/(framework-solid-gates(-systems)?\.ts|framework-solid-extended\.ts|detect-framework\.ts|conventions\/[^/]+\.ts|guards\/interface-separation-ext\.ts)$/;
238
239
  /**
239
240
  * Whether a JS/TS file path is this harness's own gate-detection source —
240
241
  * exempt from the pattern heuristics (not the line-size ceiling) to avoid
@@ -245,13 +246,125 @@ function isSelfGateSourcePath(filePath) {
245
246
  return SELF_GATE_EXCLUDE_RE.test(filePath);
246
247
  }
247
248
  //#endregion
249
+ //#region src/policy/conventions/stores.ts
250
+ /**
251
+ * State-store declaration detection over MASKED content — Zustand (React
252
+ * family) and Pinia (Vue family), per the owner's framework scoping: a rule
253
+ * only fires inside its own family (the gate checks the project deps, this
254
+ * module only reads content). Canonical home: `src/stores/`, `*.store.ts`,
255
+ * one store per domain, budget = ratio 0.4 of the global line limit.
256
+ *
257
+ * Signature-strict (audit lesson): `Object.create(` and home-made
258
+ * `create(` factories never match — Zustand requires the `zustand` import
259
+ * (or the v5 curried `create<T>()(...)` call), Pinia requires the `pinia`
260
+ * import alongside `defineStore(`.
261
+ */
262
+ /** Import line of the store package — comments masked (a `// import …` line
263
+ * never matches), strings KEPT (masking would blank the module literal). */
264
+ const STORE_IMPORT_RE = {
265
+ zustand: /^\s*import\s+[^;]*from\s+['"]zustand/m,
266
+ pinia: /^\s*import\s+[^;]*from\s+['"]pinia/m
267
+ };
268
+ /** Zustand v5 curried declaration: `export const useX = create<S>()(`. */
269
+ const ZUSTAND_CURRIED_RE = /^\s*export\s+const\s+\w+\s*=\s*create\s*<[^=]*>\s*\(\s*\)\s*\(/m;
270
+ /** Zustand declaration with import context: `export const useX = create(`. */
271
+ const ZUSTAND_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*create(?:Store)?\s*(?:<[^(]+>)?\s*\(/m;
272
+ /** Pinia declaration: `export const useX = defineStore(`. */
273
+ const PINIA_DECL_RE = /^\s*export\s+const\s+\w+\s*=\s*defineStore\s*\(/m;
274
+ /**
275
+ * True when the file declares a store of the given library (masked scan,
276
+ * signature-strict).
277
+ * @param content - Raw file content.
278
+ * @param lib - Store library to detect.
279
+ */
280
+ function declaresStore(content, lib) {
281
+ const masked = maskCommentsAndStrings(content, "c");
282
+ const importable = maskCommentsOnly(content, "c");
283
+ if (lib === "pinia") return STORE_IMPORT_RE.pinia.test(importable) && PINIA_DECL_RE.test(masked);
284
+ return ZUSTAND_CURRIED_RE.test(masked) || STORE_IMPORT_RE.zustand.test(importable) && ZUSTAND_DECL_RE.test(masked);
285
+ }
286
+ /**
287
+ * Count store declarations in one file (multi-store advisory: one per domain).
288
+ * @param content - Raw file content.
289
+ * @param lib - Store library to count.
290
+ */
291
+ function countStores(content, lib) {
292
+ const masked = maskCommentsAndStrings(content, "c");
293
+ const re = lib === "pinia" ? PINIA_DECL_RE : ZUSTAND_DECL_RE;
294
+ const curried = masked.match(new RegExp(ZUSTAND_CURRIED_RE.source, "gm"))?.length ?? 0;
295
+ const plain = masked.match(new RegExp(re.source, "gm"))?.length ?? 0;
296
+ return lib === "pinia" ? plain : Math.max(curried, plain);
297
+ }
298
+ //#endregion
299
+ //#region src/policy/conventions/query.ts
300
+ /**
301
+ * TanStack Query definition detection over MASKED content (React and Vue
302
+ * variants share the call names; the family scoping is the gate's job, via
303
+ * project deps). Canonical home: `src/query/` — one file per domain. Only
304
+ * DEFINITIONS are gated; inline consumption of a hook defined in `query/`
305
+ * stays allowed.
306
+ */
307
+ /** Import line of a TanStack Query package (raw scan: masking blanks the module string). */
308
+ const QUERY_IMPORT_RE = /^\s*import\s+[^;]*@tanstack\/[\w-]*query[\w-]*/m;
309
+ /** A TanStack Query definition call. */
310
+ const QUERY_CALL_RE = /\b(?:useQuery|useMutation|useInfiniteQuery|queryOptions)\s*(?:<[^()]*>)?\s*\(/;
311
+ /** Exported declaration (`export const|function`) — a defined query hook. */
312
+ const EXPORT_DECL_RE = /^\s*export\s+(?:const|function)\s+\w+/m;
313
+ /**
314
+ * True when the file imports a TanStack Query package (raw scan, anchored to
315
+ * real import lines so `// import …` comments cannot match).
316
+ * @param content - Raw file content.
317
+ */
318
+ function importsTanstackQuery(content) {
319
+ return QUERY_IMPORT_RE.test(content);
320
+ }
321
+ /**
322
+ * True when the file DEFINES an exported query/mutation hook (export +
323
+ * query call, masked scan) — as opposed to merely consuming one inline.
324
+ * @param content - Raw file content.
325
+ */
326
+ function declaresQueryHook(content) {
327
+ const masked = maskCommentsAndStrings(content, "c");
328
+ return QUERY_CALL_RE.test(masked) && EXPORT_DECL_RE.test(masked);
329
+ }
330
+ /**
331
+ * True when the nearest manifest of `filePath` declares a TanStack Query
332
+ * dependency (`@tanstack/react-query` or `@tanstack/vue-query`) — the
333
+ * cap gate of every query convention (owner: no cap, no query rule).
334
+ * @param filePath - The file being judged (its nearest manifest wins).
335
+ */
336
+ function queryCapActive(filePath) {
337
+ const caps = projectCaps(nearestManifestDir(dirname(resolve(filePath))));
338
+ return caps.has("tanstack-query") || caps.has("vue-query");
339
+ }
340
+ //#endregion
341
+ //#region src/policy/conventions/store-or-query.ts
342
+ /**
343
+ * Shared store/query-definition exemption (owner F0.2): a file is governed
344
+ * by the store/query conventions — NOT by the hook-location rule — ONLY when
345
+ * its signature is validated AND its ecosystem is confirmed:
346
+ * - store: the real `zustand`/`pinia` import (or the v5 curried signature);
347
+ * - query: a TanStack Query definition AND the `@tanstack/*-query` cap in
348
+ * the nearest manifest. Without the cap, a fake `useQuery(` caller is just
349
+ * a hook — the legacy hook rule applies, byte-identical.
350
+ */
351
+ /**
352
+ * True when the file is a store or query definition (exempt from hook rules).
353
+ * @param filePath - The file being judged (manifest resolution for the cap).
354
+ * @param content - Its RAW content (detectors mask internally).
355
+ */
356
+ function isStoreOrQueryDefinition(filePath, content) {
357
+ if (declaresStore(content, "zustand") || declaresStore(content, "pinia")) return true;
358
+ return declaresQueryHook(content) && queryCapActive(filePath);
359
+ }
360
+ //#endregion
248
361
  //#region src/policy/framework-solid-gates.ts
249
362
  /** Custom hook export (React): `export function/const use[A-Z]`. */
250
363
  const HOOK_RE = /^export (function|const) use[A-Z]/m;
251
364
  /** Top-level TS interface/type declaration. */
252
365
  const TS_DECL_RE = /^(export )?(interface|type) [A-Z]/m;
253
366
  /** Client-only React hooks that require the `'use client'` directive. */
254
- const CLIENT_HOOK_RE = /(useState|useEffect|useRef|onClick|onChange)/;
367
+ const CLIENT_HOOK_RE = /\b(useState|useEffect|useRef|onClick|onChange)\b/;
255
368
  /** PHP top-level `interface` declaration. */
256
369
  const PHP_INTERFACE_RE = /^interface /m;
257
370
  /** Swift top-level `protocol` declaration. */
@@ -261,6 +374,8 @@ const SWIFT_TYPE_RE = /^(class|struct) [^\n{]* \{/m;
261
374
  /** React: line limit, interface separation, custom hooks under `/hooks/`. */
262
375
  function reactGate(filePath, content, fileLines) {
263
376
  const v = [];
377
+ const raw = content;
378
+ content = maskCommentsAndStrings(content, "c");
264
379
  const max = resolveMaxLines();
265
380
  const lines = fileLines ?? countFrameworkCodeLines(content);
266
381
  if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split to hooks/, components/, or utils/.`);
@@ -268,13 +383,16 @@ function reactGate(filePath, content, fileLines) {
268
383
  const dest = moduleAwarePath(filePath, "src/interfaces/", "src/interfaces/ or src/types/");
269
384
  v.push(`Interface/type in component. Move to ${dest}.`);
270
385
  }
271
- if (HOOK_RE.test(content) && !filePath.includes("/hooks/")) v.push("Custom hook defined outside hooks/ directory. Move to hooks/.");
386
+ if (HOOK_RE.test(content) && !filePath.includes("/hooks/") && !isStoreOrQueryDefinition(filePath, raw)) v.push("Custom hook defined outside hooks/ directory. Move to hooks/.");
272
387
  return v;
273
388
  }
274
389
  /** Next.js: adaptive line limit, interface separation, `'use client'`. */
275
390
  function nextGate(filePath, content, fileLines) {
276
391
  const v = [];
277
- const max = /(page|layout|loading|error|not-found)\.(tsx|ts)$/.test(filePath) ? 150 : 100;
392
+ const raw = content;
393
+ content = maskCommentsAndStrings(content, "c");
394
+ const base = resolveMaxLines();
395
+ const max = /(page|layout|loading|error|not-found)\.(tsx|ts)$/.test(filePath) ? base + 50 : base;
278
396
  const lines = fileLines ?? countFrameworkCodeLines(content);
279
397
  if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split to lib/, hooks/, or components/.`);
280
398
  if (/\/(app|components|modules)\//.test(filePath) && !filePath.includes("/interfaces/") && TS_DECL_RE.test(content)) {
@@ -282,7 +400,7 @@ function nextGate(filePath, content, fileLines) {
282
400
  v.push(`Interface/type in component. Move to ${dest}.`);
283
401
  }
284
402
  if (CLIENT_HOOK_RE.test(content)) {
285
- const head = content.split("\n").slice(0, 5).join("\n");
403
+ const head = maskCommentsOnly(raw, "c").split("\n").slice(0, 5).join("\n");
286
404
  if (!head.includes("'use client'") && !head.includes("\"use client\"")) v.push("Client hooks detected but 'use client' directive missing at top.");
287
405
  }
288
406
  return v;
@@ -290,6 +408,7 @@ function nextGate(filePath, content, fileLines) {
290
408
  /** Laravel/PHP: line limit, interface outside `/Contracts/`, fat controller (>80). */
291
409
  function laravelGate(filePath, content, fileLines) {
292
410
  const v = [];
411
+ content = maskCommentsAndStrings(content, "php");
293
412
  const lines = fileLines ?? countFrameworkCodeLines(content);
294
413
  const max = resolveMaxLines();
295
414
  if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split using Services, Actions, or Traits.`);
@@ -303,7 +422,9 @@ function laravelGate(filePath, content, fileLines) {
303
422
  /** Swift: adaptive limit, protocol separation, @MainActor, Sendable. */
304
423
  function swiftGate(filePath, content, fileLines) {
305
424
  const v = [];
306
- const max = /(View|Screen)\.swift$/.test(filePath) ? 150 : 100;
425
+ content = maskCommentsAndStrings(content, "c");
426
+ const base = resolveMaxLines();
427
+ const max = /(View|Screen)\.swift$/.test(filePath) ? base + 50 : base;
307
428
  const lines = fileLines ?? countFrameworkCodeLines(content);
308
429
  if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Extract to ViewModels, Services, or subviews.`);
309
430
  if (SWIFT_PROTOCOL_RE.test(content) && !filePath.includes("/Protocols/")) {
@@ -347,6 +468,7 @@ const RUST_DERIVE_PARSER_RE = /#\[derive\([^)]*Parser[^)]*\)\]/;
347
468
  /** Go: line limit, interface co-located with its (pointer-receiver) implementation. */
348
469
  function goGate(filePath, content, fileLines) {
349
470
  const v = [];
471
+ content = maskCommentsAndStrings(content, "c");
350
472
  const max = resolveMaxLines();
351
473
  const lines = fileLines ?? countFrameworkCodeLines(content);
352
474
  if (lines > max) {
@@ -381,6 +503,8 @@ function isClapArgsOnly(content) {
381
503
  /** Rust: line limit, trait co-location, god-main.rs, unsafe without `// SAFETY:`. */
382
504
  function rustGate(filePath, content, fileLines) {
383
505
  const v = [];
506
+ const raw = content;
507
+ content = maskCommentsAndStrings(content, "c");
384
508
  const max = resolveMaxLines();
385
509
  const lines = fileLines ?? countFrameworkCodeLines(content);
386
510
  if (lines > max) {
@@ -395,10 +519,133 @@ function rustGate(filePath, content, fileLines) {
395
519
  }
396
520
  }
397
521
  if ((RUST_MAIN_RE.test(filePath) || RUST_BIN_RE.test(filePath)) && (RUST_TYPE_DECL_RE.test(content) || RUST_IMPL_RE.test(content)) && !isClapArgsOnly(content)) v.push("Entry point contains business logic — keep main.rs to wiring only (parse args, init, call lib.rs/app::run()).");
398
- if (RUST_UNSAFE_RE.test(content) && !RUST_SAFETY_COMMENT_RE.test(content)) v.push("unsafe without a // SAFETY: comment. Justify the invariant above each unsafe block.");
522
+ if (RUST_UNSAFE_RE.test(content) && !RUST_SAFETY_COMMENT_RE.test(raw)) v.push("unsafe without a // SAFETY: comment. Justify the invariant above each unsafe block.");
399
523
  return v;
400
524
  }
401
525
  //#endregion
526
+ //#region src/policy/js-gate-route.ts
527
+ /**
528
+ * Per-process memo of manifest-dir → capabilities. Hook invocations are
529
+ * short-lived processes, so a stale manifest is impossible in production; the
530
+ * memo only saves a re-read when one process judges several files (tests).
531
+ */
532
+ const capsCache = /* @__PURE__ */ new Map();
533
+ /** Resolve the nearest manifest's capabilities for `filePath` (memoized per dir). */
534
+ function capsFor(filePath, cwd) {
535
+ const dir = nearestManifestDir(dirname(resolve(cwd, filePath)));
536
+ if (!dir) return /* @__PURE__ */ new Set();
537
+ const hit = capsCache.get(dir);
538
+ if (hit) return hit;
539
+ const caps = projectCaps(dir);
540
+ capsCache.set(dir, caps);
541
+ return caps;
542
+ }
543
+ /**
544
+ * Pick the JS gate from the PROJECT's declared framework, not the file's
545
+ * content markers (H1): a bare client component (hooks but no `next` marker)
546
+ * in a Next.js app must still hit nextGate's `'use client'` rule — content
547
+ * routing sent it to reactGate and the file landed, crashing at runtime.
548
+ * `nextjs` wins over `react` (every Next app ships react); a TanStack
549
+ * Start/Router or plain-react project routes to reactGate. Fail-open: any
550
+ * error, or no JS-framework capability (no manifest, backend, vue), yields
551
+ * `null` so the legacy NEXT_RE content routing applies, byte-identical.
552
+ * @param filePath - Path of the file being written/edited.
553
+ * @param cwd - Root to resolve a relative `filePath` against (default cwd).
554
+ * @returns The gate route, or null for the legacy content-based fallback.
555
+ */
556
+ function jsGateRoute(filePath, cwd = process.cwd()) {
557
+ try {
558
+ const caps = capsFor(filePath, cwd);
559
+ if (caps.has("nextjs")) return "nextjs";
560
+ if (caps.has("react") || caps.has("tanstack-start") || caps.has("tanstack-router")) return "react";
561
+ return null;
562
+ } catch {
563
+ return null;
564
+ }
565
+ }
566
+ //#endregion
567
+ //#region src/policy/conventions/react-hooks.ts
568
+ /**
569
+ * Custom React hook declaration detection over MASKED content. Historical
570
+ * rule (`framework-solid-gates.ts`): an exported custom hook belongs in
571
+ * `hooks/` — the regex was naive (`^export (function|const) use[A-Z]`,
572
+ * missing `export default`, `async`, and matching inside comments/strings).
573
+ */
574
+ /** Exported custom hook: `export [default] [async] function|const useX`. */
575
+ const HOOK_DECL_RE = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|const)\s+use[A-Z]\w*/m;
576
+ /**
577
+ * True when the file declares an exported custom `use*` hook (masked scan).
578
+ * @param content - Raw file content.
579
+ */
580
+ function declaresCustomHook(content) {
581
+ return HOOK_DECL_RE.test(maskCommentsAndStrings(content, "c"));
582
+ }
583
+ //#endregion
584
+ //#region src/policy/framework-solid-extended.ts
585
+ /**
586
+ * Extended convention violations — the NEW rules of the conventions module
587
+ * (owner Amendments 3-5), all advisory-first: the caller wraps them with
588
+ * {@link rolloutVerdict}, so they surface as non-blocking `inform` until
589
+ * `FUSE_CONVENTIONS_MODE=deny`. Only fires when the legacy gates found
590
+ * nothing (no double-reporting of cases the legacy reactGate already blocks).
591
+ *
592
+ * Rules: (1) custom hook outside `hooks/` — widened to `export default`/
593
+ * `async` syntaxes and to the nextjs/tanstack family (legacy covered react
594
+ * only); (2) hook file over its line budget — `hookBudget`, ratio 0.3 of the
595
+ * global `FUSE_SOLID_MAX_LINES` limit (default 30; one variable drives every
596
+ * budget, owner decision — no per-file-kind env vars); (3) TanStack Start
597
+ * route files kept routing-only (no interface declarations in `src/routes/`);
598
+ * (4) store declared outside `stores/` or over `storeBudget` (ratio 0.4);
599
+ * (5) TanStack Query hook defined in a component/page (only when the
600
+ * `@tanstack/*-query` cap is detected in the nearest manifest — `query/` or
601
+ * `hooks/` are both accepted homes); (6) component file misplaced inside
602
+ * `src/hooks|stores|query|interfaces|types/` (deterministic case only).
603
+ */
604
+ /** TanStack Start content signal (file routes, server fns, start/router imports). */
605
+ const TANSTACK_RE = /createFileRoute|createServerFn|from ['"]@tanstack\/react-(start|router)/;
606
+ /** A custom-hook source file under a hooks directory. */
607
+ const HOOK_FILE_RE = /(^|\/)use[A-Z][^/]*\.(ts|tsx|js|jsx)$/;
608
+ /**
609
+ * Collect the extended convention violations for a JS/TS-family file.
610
+ * @param filePath - The written/edited file's path.
611
+ * @param content - Its raw content (masked internally).
612
+ * @returns The violation messages (empty when clean or out of scope).
613
+ */
614
+ function extendedViolations(filePath, content) {
615
+ const v = [];
616
+ if (!/\.(tsx|ts|jsx|js|vue)$/.test(filePath)) return v;
617
+ const masked = maskCommentsAndStrings(content, "c");
618
+ if (declaresCustomHook(content) && !isHooksPath(filePath) && !isStoreOrQueryDefinition(filePath, content)) v.push("Custom hook defined outside hooks/ directory. Move to modules/[feature]/src/hooks/.");
619
+ if (isHooksPath(filePath) && HOOK_FILE_RE.test(filePath)) {
620
+ const max = hookBudget(resolveMaxLines());
621
+ const lines = countFrameworkCodeLines(masked);
622
+ if (lines > max) v.push(`Hook file has ${lines} lines (limit: ${max}). Extract smaller hooks.`);
623
+ }
624
+ if (filePath.includes("/routes/") && TANSTACK_RE.test(masked) && declaresInterface(filePath, content)) v.push("Route files are routing-only. Move the interface to src/modules/[feature]/src/interfaces/.");
625
+ const storeLib = declaresStore(content, "zustand") ? "zustand" : declaresStore(content, "pinia") ? "pinia" : null;
626
+ if (storeLib && !isStoresPath(filePath)) v.push("Store defined outside stores/ directory. Move to modules/[feature]/src/stores/.");
627
+ if (storeLib && isStoresPath(filePath)) {
628
+ const max = storeBudget(resolveMaxLines());
629
+ const lines = countFrameworkCodeLines(masked);
630
+ if (lines > max) v.push(`Store file has ${lines} lines (limit: ${max}).`);
631
+ }
632
+ if (isComponentOrPagePath(filePath) && declaresQueryHook(content) && queryCapActive(filePath)) v.push("Query hook defined outside query/ directory. Move to modules/[feature]/src/query/.");
633
+ if (/\.(tsx|vue)$/.test(filePath)) {
634
+ const m = /\/src\/(hooks|stores|query|interfaces|types)\//.exec(filePath);
635
+ if (m && declaresComponent(masked)) v.push(`Component does not belong in ${m[1]}/. Move to modules/[feature]/components/.`);
636
+ }
637
+ return v;
638
+ }
639
+ /** Component/page directories where a store or query DEFINITION is a violation. */
640
+ function isComponentOrPagePath(filePath) {
641
+ return isComponentsPath(filePath) || /\/(pages|app|routes)\//.test(filePath);
642
+ }
643
+ /** True when the file declares an exported component (function/const, Vue SFC
644
+ * `export default {}` or `defineComponent(` included — audit F0 option). */
645
+ function declaresComponent(masked) {
646
+ return /^\s*export\s+(?:default\s+)?(?:function|const)\s+[A-Z]\w*/m.test(masked) || /^\s*export\s+default\s*\{/m.test(masked) || /defineComponent\s*\(/.test(masked);
647
+ }
648
+ //#endregion
402
649
  //#region src/policy/framework-solid.ts
403
650
  /** Next.js detection: directive, runtime types, or a `next` import. */
404
651
  const NEXT_RE = /(use client|use server|NextRequest|NextResponse|from ['"]next)/;
@@ -414,6 +661,9 @@ function block(filePath, violations) {
414
661
  /** Run the JS/TS gate (React or Next.js), honoring build-artifact exclusions. */
415
662
  function jsViolations(filePath, content, fileLines) {
416
663
  if (isExcludedJsPath(filePath) || isSelfGateSourcePath(filePath)) return [];
664
+ const route = jsGateRoute(filePath);
665
+ if (route === "nextjs") return nextGate(filePath, content, fileLines);
666
+ if (route === "react") return reactGate(filePath, content, fileLines);
417
667
  return NEXT_RE.test(content) ? nextGate(filePath, content, fileLines) : reactGate(filePath, content, fileLines);
418
668
  }
419
669
  /**
@@ -431,6 +681,12 @@ function jsViolations(filePath, content, fileLines) {
431
681
  */
432
682
  function frameworkSolidGate(filePath, content, fileLines) {
433
683
  if (!filePath || !content) return null;
684
+ if (filePath.endsWith("routeTree.gen.ts")) return {
685
+ kind: "block",
686
+ title: "Generated file",
687
+ reason: `SOLID VIOLATION in ${filePath}: routeTree.gen.ts is generated by TanStack Router — never edit it directly.`,
688
+ actions: ["Edit the source route files instead, then regenerate"]
689
+ };
434
690
  let violations = [];
435
691
  if (filePath.endsWith(".php")) {
436
692
  if (isExcludedPhpPath(filePath)) return null;
@@ -441,7 +697,10 @@ function frameworkSolidGate(filePath, content, fileLines) {
441
697
  } else if (/\.(tsx|ts|jsx|js)$/.test(filePath)) violations = jsViolations(filePath, content, fileLines);
442
698
  else if (filePath.endsWith(".go")) violations = goGate(filePath, content, fileLines);
443
699
  else if (filePath.endsWith(".rs")) violations = rustGate(filePath, content, fileLines);
444
- return violations.length ? block(filePath, violations) : null;
700
+ if (violations.length) return block(filePath, violations);
701
+ if (!/\.(tsx|ts|jsx|js|vue)$/.test(filePath) || isExcludedJsPath(filePath) || isSelfGateSourcePath(filePath)) return null;
702
+ const extended = extendedViolations(filePath, content);
703
+ return extended.length ? rolloutVerdict(block(filePath, extended), true) : null;
445
704
  }
446
705
  //#endregion
447
706
  //#region src/policy/skill-patterns/shadcn.ts
@@ -1648,4 +1907,4 @@ function missingSeoElements(html) {
1648
1907
  return missing;
1649
1908
  }
1650
1909
  //#endregion
1651
- export { MAX_EXA_RESULTS as A, freshnessGate as B, detectRequiredSkills as C, frameworkSolidGate as D, SKILL_TRIGGERS as E, POST_AUTH_GATES as F, PRE_AUTH_GATES as I, brainstormGate as L, capVerbosity as M, detectCreationIntent as N, isExcludedJsPath as O, APEX_GATES as P, docConsultedGate as R, parseField as S, usesTailwindUtilities as T, solidReadGate as V, detectClaudeMdProjectType as _, firstHeading as a, scanPlugin as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, buildClaudeMdContext as g, buildApexInstruction as h, firstComment as i, MAX_TOKENS as j, isExcludedSwiftPath as k, PROJECT_INDICATORS as l, DEV_VERBS as m, missingSeoElements as n, parseEnrichment as o, apexAgentSteps as p, descFromText as r, parseEntry as s, isHtmlLike as t, buildApexTaskContext as u, apexDocName as v, skillTriggerGate as w, parseBodyDesc as x, harnessHomeSegment as y, evaluateApex as z };
1910
+ export { importsTanstackQuery as A, APEX_GATES as B, detectRequiredSkills as C, frameworkSolidGate as D, SKILL_TRIGGERS as E, isExcludedSwiftPath as F, evaluateApex as G, PRE_AUTH_GATES as H, MAX_EXA_RESULTS as I, freshnessGate as K, MAX_TOKENS as L, countStores as M, declaresStore as N, declaresCustomHook as O, isExcludedJsPath as P, capVerbosity as R, parseField as S, usesTailwindUtilities as T, brainstormGate as U, POST_AUTH_GATES as V, docConsultedGate as W, detectClaudeMdProjectType as _, firstHeading as a, scanPlugin as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, buildClaudeMdContext as g, buildApexInstruction as h, firstComment as i, queryCapActive as j, declaresQueryHook as k, PROJECT_INDICATORS as l, DEV_VERBS as m, missingSeoElements as n, parseEnrichment as o, apexAgentSteps as p, solidReadGate as q, descFromText as r, parseEntry as s, isHtmlLike as t, buildApexTaskContext as u, apexDocName as v, skillTriggerGate as w, parseBodyDesc as x, harnessHomeSegment as y, detectCreationIntent as z };