@fusengine/harness 0.1.80 → 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/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 +54 -46
- package/dist/adapters/kimi/index.mjs +2 -2
- 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 +129 -38
- 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/{doc-helpers-CEKzGg2u.d.mts → doc-helpers-BGCzP9VF.d.mts} +12 -12
- package/dist/{dotenv-B26N_uGr.mjs → dotenv-DoqNlALG.mjs} +29 -2
- package/dist/{evaluate-ClfbCDiY.mjs → evaluate-B4-MhOy_.mjs} +322 -32
- package/dist/freshness/index.d.mts +2 -2
- package/dist/{handle-B7YufFz9.mjs → handle-J5r2f_O_.mjs} +637 -159
- 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 +8 -8
- package/dist/init/index.d.mts +8 -9
- package/dist/init/index.mjs +1 -1
- package/dist/{kimi-C7FGprQz.mjs → kimi-ChbcU2uI.mjs} +17 -3
- 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 +453 -451
- 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-CDvR4bbF.mjs → skill-path-DhBGlQe3.mjs} +2 -2
- package/dist/{store-nDZ3eLBQ.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-CkmquRDV.mjs → validate-Bf-EsbDh.mjs} +279 -20
- package/package.json +163 -163
- package/dist/apex-Wdi1nq_w.d.mts +0 -73
- package/dist/harness-BQ5_Wnva.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-B_VHbsaJ.d.mts +0 -96
- package/dist/limits-CHn8AIL1.mjs +0 -31
package/dist/config/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
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-
|
|
3
|
-
import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "../layout-
|
|
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 };
|
package/dist/detect/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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(
|
|
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 {
|
|
2
|
-
import { f as countLines, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-
|
|
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
|
|
166
|
-
|
|
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(
|
|
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
|
|
186
|
-
* union ("react" | "nextjs" | "
|
|
187
|
-
* "ruby" | "rust" | "generic") —
|
|
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
|
|
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)
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* Parity
|
|
920
|
-
*
|
|
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(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if (/\.
|
|
941
|
-
if (/\.
|
|
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(
|
|
944
|
-
if (name && new RegExp(`impl\\s+${name}\\s+for\\s+\\w+`).test(
|
|
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 {
|
|
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-
|
|
2
|
-
import { t as incrementTrivialEditCounter } from "../index-
|
|
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 };
|