@fusengine/harness 0.1.79 → 0.1.81
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.
- package/README.md +14 -1
- package/dist/adapters/claude/index.d.mts +1 -1
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +11 -12
- package/dist/adapters/codex/index.mjs +3 -3
- package/dist/adapters/cursor/index.d.mts +29 -29
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.d.mts +22 -23
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/adapters/kimi/index.d.mts +87 -0
- package/dist/adapters/kimi/index.mjs +2 -0
- package/dist/cache/index.d.mts +1 -1
- package/dist/{claude-51RSspJ1.mjs → claude-D93HY8dq.mjs} +2 -2
- package/dist/cli/bin.d.mts +1 -1
- package/dist/cli/bin.mjs +130 -39
- package/dist/cli/index.d.mts +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +4 -4
- package/dist/detect/index.d.mts +2 -1
- package/dist/detect/index.mjs +1 -1
- package/dist/{doc-helpers-CEKzGg2u.d.mts → doc-helpers-BGCzP9VF.d.mts} +12 -12
- package/dist/{dotenv-Jj8aL1FL.mjs → dotenv-DoqNlALG.mjs} +31 -3
- package/dist/{evaluate-ClfbCDiY.mjs → evaluate-B4-MhOy_.mjs} +322 -32
- package/dist/freshness/index.d.mts +2 -2
- package/dist/{handle-Dthtlp7Z.mjs → handle-J5r2f_O_.mjs} +665 -164
- package/dist/{harness-Cb9xR8dC.mjs → harness-BMuLJ9lm.mjs} +4 -2
- package/dist/{hermes-rTmVdhUL.mjs → hermes-1PUEoY8q.mjs} +2 -2
- package/dist/{home-state-BXf38Zi1.mjs → home-state-oUGFB4ds.mjs} +4 -2
- package/dist/index-84heAAm_.d.mts +12 -0
- package/dist/index-CaxrMD1A.d.mts +1 -0
- package/dist/index-CigBu4K9.d.mts +14 -0
- package/dist/{index-DGxNjXW7.d.mts → index-D4C2-IIc.d.mts} +6 -8
- package/dist/index-DBgnIZn9.d.mts +756 -0
- package/dist/{index-mmRF3KNp.d.mts → index-DEfz82BP.d.mts} +12 -13
- package/dist/{index-BUwEmIK-.d.mts → index-DhmV2MPh.d.mts} +31 -31
- package/dist/index-oXXPs2xl.d.mts +119 -0
- package/dist/index-p9fLrSOD.d.mts +27 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +9 -9
- package/dist/init/index.d.mts +8 -9
- package/dist/init/index.mjs +1 -1
- package/dist/kimi-ChbcU2uI.mjs +94 -0
- package/dist/{layout-C0jaaCQC.mjs → layout-KWoE_Mqn.mjs} +1 -0
- package/dist/limits-DG5pl-5s.mjs +59 -0
- package/dist/memory/index.mjs +1 -1
- package/dist/policy/index.d.mts +2 -3
- package/dist/policy/index.mjs +4 -4
- package/dist/prompt/index.d.mts +2 -1
- package/dist/refs/index.d.mts +1 -1
- package/dist/{registry-CymilZiZ.mjs → registry-IhHk2KlT.mjs} +1 -1
- package/dist/{run-3tyTW9Ud.mjs → run-D_WchZ3R.mjs} +1 -1
- package/dist/{run-Do2JltgU.mjs → run-TAXNRSpD.mjs} +1 -1
- package/dist/runtime/index.d.mts +455 -446
- package/dist/runtime/index.mjs +3 -3
- package/dist/{session-state-CY4iohn_.d.mts → session-state-D5gLr66m.d.mts} +29 -30
- package/dist/{skill-path-BO2N0XvB.mjs → skill-path-DhBGlQe3.mjs} +3 -3
- package/dist/{store-Bcn08y9m.mjs → store-yr_QFsZY.mjs} +101 -13
- package/dist/tracking/index.d.mts +22 -13
- package/dist/tracking/index.mjs +2 -2
- package/dist/{types-DVbIl9md.d.mts → types-C0-igRKr.d.mts} +8 -8
- package/dist/types-DvXTI71X.d.mts +15 -0
- package/dist/util/index.d.mts +1 -1
- package/dist/{validate-DhOX5hDK.mjs → validate-Bf-EsbDh.mjs} +348 -32
- package/package.json +163 -158
- package/dist/apex-Wdi1nq_w.d.mts +0 -73
- package/dist/harness-s1Fce_dL.d.mts +0 -27
- package/dist/index-BEMumjOw.d.mts +0 -27
- package/dist/index-BIGVNQB8.d.mts +0 -576
- package/dist/index-BKZ67WMa.d.mts +0 -1
- package/dist/index-BOBXQ91y.d.mts +0 -12
- package/dist/index-BVVgDSdq.d.mts +0 -1
- package/dist/index-jj_EzZRv.d.mts +0 -96
- package/dist/limits-CHn8AIL1.mjs +0 -31
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as ttlLabel, t as HOME_DIR } from "./dotenv-
|
|
3
|
-
import {
|
|
4
|
-
import { d as countFrameworkCodeLines, l as PLUGINS_DIR, u as SOLID_REF } from "./home-state-
|
|
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-
|
|
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
|
|
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
|
|
233
|
-
* they define. Exempt by exact filename
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,9 +519,132 @@ 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(
|
|
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.");
|
|
523
|
+
return v;
|
|
524
|
+
}
|
|
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
|
+
}
|
|
399
637
|
return v;
|
|
400
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
|
+
}
|
|
401
648
|
//#endregion
|
|
402
649
|
//#region src/policy/framework-solid.ts
|
|
403
650
|
/** Next.js detection: directive, runtime types, or a `next` import. */
|
|
@@ -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
|
-
|
|
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
|
|
@@ -1208,24 +1467,29 @@ function harnessHomeSegment(id) {
|
|
|
1208
1467
|
return HOME_DIR[id] ?? ".claude";
|
|
1209
1468
|
}
|
|
1210
1469
|
/**
|
|
1211
|
-
* Root instructions doc name for a harness target: "AGENTS.md" for Codex
|
|
1212
|
-
* "CLAUDE.md" for every other
|
|
1470
|
+
* Root instructions doc name for a harness target: "AGENTS.md" for Codex and
|
|
1471
|
+
* Kimi Code CLI (both documented conventions), "CLAUDE.md" for every other
|
|
1472
|
+
* target (including the default/unknown fallback).
|
|
1213
1473
|
* @param id - Harness id.
|
|
1214
1474
|
* @returns The doc file name.
|
|
1215
1475
|
*/
|
|
1216
1476
|
function apexDocName(id) {
|
|
1217
|
-
return id === "codex" ? "AGENTS.md" : "CLAUDE.md";
|
|
1477
|
+
return id === "codex" || id === "kimi" ? "AGENTS.md" : "CLAUDE.md";
|
|
1218
1478
|
}
|
|
1219
1479
|
/**
|
|
1220
|
-
* Native plan/track tool name for a harness target: Codex's `update_plan
|
|
1221
|
-
*
|
|
1222
|
-
* tool references (`TaskList`,
|
|
1223
|
-
*
|
|
1480
|
+
* Native plan/track tool name for a harness target: Codex's `update_plan`,
|
|
1481
|
+
* Kimi Code CLI's `TodoList`, or Claude's `TaskCreate` (default). Covers only
|
|
1482
|
+
* the PLAN-step line — other target-specific tool references (`TaskList`,
|
|
1483
|
+
* `TaskUpdate`) are resolved via `apexTaskSteps`/`apexAgentSteps` in
|
|
1484
|
+
* `apex-target-steps.ts` since their surrounding prose differs structurally,
|
|
1485
|
+
* not just by token.
|
|
1224
1486
|
* @param id - Harness id.
|
|
1225
1487
|
* @returns The plan-tool name.
|
|
1226
1488
|
*/
|
|
1227
1489
|
function apexPlanTool(id) {
|
|
1228
|
-
|
|
1490
|
+
if (id === "codex") return "update_plan";
|
|
1491
|
+
if (id === "kimi") return "TodoList";
|
|
1492
|
+
return "TaskCreate";
|
|
1229
1493
|
}
|
|
1230
1494
|
//#endregion
|
|
1231
1495
|
//#region src/policy/detect-claude-md-project-type.ts
|
|
@@ -1291,6 +1555,61 @@ function buildClaudeMdContext(prompt, cwd, id = "claude-code") {
|
|
|
1291
1555
|
return `${buildApexInstruction(detectClaudeMdProjectType(cwd), resolveMaxLines(), id)}\n\n# ${docName}\n${claudeContent}`;
|
|
1292
1556
|
}
|
|
1293
1557
|
//#endregion
|
|
1558
|
+
//#region src/policy/apex-target-steps.ts
|
|
1559
|
+
/** Codex/Kimi step-trio overrides; any other target falls back to {@link DEFAULT_TASK_STEPS}. */
|
|
1560
|
+
const TASK_STEPS = {
|
|
1561
|
+
codex: {
|
|
1562
|
+
step3: "update_plan → review the current plan",
|
|
1563
|
+
step4: "update_plan → mark the active step in_progress before starting",
|
|
1564
|
+
step7: "update_plan → mark the step completed when done"
|
|
1565
|
+
},
|
|
1566
|
+
kimi: {
|
|
1567
|
+
step3: "TodoList → review the current list (omit todos to query)",
|
|
1568
|
+
step4: "TodoList → mark the active item in_progress before starting",
|
|
1569
|
+
step7: "TodoList → mark the item done when finished"
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
/** Default (Claude Code) step trio — zero-regression fallback for any unlisted target. */
|
|
1573
|
+
const DEFAULT_TASK_STEPS = {
|
|
1574
|
+
step3: "TaskList → see pending tasks",
|
|
1575
|
+
step4: "TaskUpdate(in_progress) → before starting",
|
|
1576
|
+
step7: "TaskUpdate(completed) → triggers auto-commit"
|
|
1577
|
+
};
|
|
1578
|
+
/**
|
|
1579
|
+
* Resolve the step3/step4/step7 phrasing for a harness target (APEX
|
|
1580
|
+
* task-context injection). Replaces the former `isCodex` binary ternary.
|
|
1581
|
+
* @param id - Harness id.
|
|
1582
|
+
* @returns The {@link ApexTaskSteps} for that target.
|
|
1583
|
+
*/
|
|
1584
|
+
function apexTaskSteps(id) {
|
|
1585
|
+
return TASK_STEPS[id] ?? DEFAULT_TASK_STEPS;
|
|
1586
|
+
}
|
|
1587
|
+
/** Codex/Kimi before/done overrides; any other target falls back to {@link DEFAULT_AGENT_STEPS}. */
|
|
1588
|
+
const AGENT_STEPS = {
|
|
1589
|
+
codex: {
|
|
1590
|
+
beforeStart: "Use update_plan → mark the active step in_progress before starting",
|
|
1591
|
+
whenDone: "update_plan → mark the step completed when done"
|
|
1592
|
+
},
|
|
1593
|
+
kimi: {
|
|
1594
|
+
beforeStart: "Use TodoList → mark the active item in_progress before starting",
|
|
1595
|
+
whenDone: "TodoList → mark the item done when finished"
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
/** Default (Claude Code) before/done pair — zero-regression fallback for any unlisted target. */
|
|
1599
|
+
const DEFAULT_AGENT_STEPS = {
|
|
1600
|
+
beforeStart: "Use TaskUpdate(taskId, status: in_progress) before starting",
|
|
1601
|
+
whenDone: "TaskUpdate(taskId, status: completed) triggers auto-commit"
|
|
1602
|
+
};
|
|
1603
|
+
/**
|
|
1604
|
+
* Resolve the beforeStart/whenDone phrasing for a harness target (APEX
|
|
1605
|
+
* sub-agent injection). Replaces the former `isCodex` binary ternary.
|
|
1606
|
+
* @param id - Harness id.
|
|
1607
|
+
* @returns The {@link ApexAgentSteps} for that target.
|
|
1608
|
+
*/
|
|
1609
|
+
function apexAgentSteps(id) {
|
|
1610
|
+
return AGENT_STEPS[id] ?? DEFAULT_AGENT_STEPS;
|
|
1611
|
+
}
|
|
1612
|
+
//#endregion
|
|
1294
1613
|
//#region src/policy/apex-task-context.ts
|
|
1295
1614
|
/**
|
|
1296
1615
|
* Read the current task state from `task.json`, reproducing the legacy Python
|
|
@@ -1330,10 +1649,7 @@ function loadApexTaskState(taskFile) {
|
|
|
1330
1649
|
*/
|
|
1331
1650
|
function buildApexTaskContext(state, maxLines, id = "claude-code") {
|
|
1332
1651
|
const seg = harnessHomeSegment(id);
|
|
1333
|
-
const
|
|
1334
|
-
const step3 = isCodex ? "update_plan → review the current plan" : "TaskList → see pending tasks";
|
|
1335
|
-
const step4 = isCodex ? "update_plan → mark the active step in_progress before starting" : "TaskUpdate(in_progress) → before starting";
|
|
1336
|
-
const step7 = isCodex ? "update_plan → mark the step completed when done" : "TaskUpdate(completed) → triggers auto-commit";
|
|
1652
|
+
const { step3, step4, step7 } = apexTaskSteps(id);
|
|
1337
1653
|
return `⚠️ APEX MODE - Read ${seg}/apex/AGENTS.md for rules\n\nCurrent: Task #${state.id} - ${state.subject} (Phase: ${state.phase})\nDocs consulted: ${state.docs}\n\nAgent must:\n1. Read task.json → find last 3 completed tasks\n2. Read their notes in docs/ (task-{ID}-{subject}.md)\n3. ${step3}\n4. ${step4}\n5. Apply SOLID (files < ${maxLines} lines)\n6. Write notes to docs/task-{ID}-{subject}.md\n7. ${step7}`;
|
|
1338
1654
|
}
|
|
1339
1655
|
/**
|
|
@@ -1591,4 +1907,4 @@ function missingSeoElements(html) {
|
|
|
1591
1907
|
return missing;
|
|
1592
1908
|
}
|
|
1593
1909
|
//#endregion
|
|
1594
|
-
export {
|
|
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 };
|