gsd-pi 2.10.0 → 2.10.2
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/node_modules/@gsd/native/dist/ast/index.d.ts +4 -0
- package/node_modules/@gsd/native/dist/ast/index.js +7 -0
- package/node_modules/@gsd/native/dist/ast/types.d.ts +69 -0
- package/node_modules/@gsd/native/dist/ast/types.js +1 -0
- package/node_modules/@gsd/native/{src/clipboard/index.ts → dist/clipboard/index.d.ts} +3 -15
- package/node_modules/@gsd/native/dist/clipboard/index.js +33 -0
- package/node_modules/@gsd/native/dist/clipboard/types.d.ts +7 -0
- package/node_modules/@gsd/native/dist/clipboard/types.js +1 -0
- package/node_modules/@gsd/native/dist/diff/index.d.ts +33 -0
- package/node_modules/@gsd/native/dist/diff/index.js +38 -0
- package/node_modules/@gsd/native/dist/diff/types.d.ts +23 -0
- package/node_modules/@gsd/native/dist/diff/types.js +1 -0
- package/node_modules/@gsd/native/{src/fd/index.ts → dist/fd/index.d.ts} +2 -12
- package/node_modules/@gsd/native/dist/fd/index.js +26 -0
- package/node_modules/@gsd/native/dist/fd/types.d.ts +29 -0
- package/node_modules/@gsd/native/dist/fd/types.js +1 -0
- package/node_modules/@gsd/native/{src/glob/index.ts → dist/glob/index.d.ts} +3 -19
- package/node_modules/@gsd/native/dist/glob/index.js +31 -0
- package/node_modules/@gsd/native/dist/glob/types.d.ts +50 -0
- package/node_modules/@gsd/native/dist/glob/types.js +1 -0
- package/node_modules/@gsd/native/dist/grep/index.d.ts +20 -0
- package/node_modules/@gsd/native/dist/grep/index.js +23 -0
- package/node_modules/@gsd/native/dist/grep/types.d.ts +99 -0
- package/node_modules/@gsd/native/dist/grep/types.js +1 -0
- package/node_modules/@gsd/native/dist/gsd-parser/index.d.ts +45 -0
- package/node_modules/@gsd/native/dist/gsd-parser/index.js +54 -0
- package/node_modules/@gsd/native/dist/gsd-parser/types.d.ts +55 -0
- package/node_modules/@gsd/native/dist/gsd-parser/types.js +7 -0
- package/node_modules/@gsd/native/{src/highlight/index.ts → dist/highlight/index.d.ts} +3 -19
- package/node_modules/@gsd/native/dist/highlight/index.js +33 -0
- package/node_modules/@gsd/native/dist/highlight/types.d.ts +25 -0
- package/node_modules/@gsd/native/dist/highlight/types.js +1 -0
- package/node_modules/@gsd/native/{src/html/index.ts → dist/html/index.d.ts} +1 -10
- package/node_modules/@gsd/native/dist/html/index.js +16 -0
- package/node_modules/@gsd/native/dist/html/types.d.ts +7 -0
- package/node_modules/@gsd/native/dist/html/types.js +1 -0
- package/node_modules/@gsd/native/{src/image/index.ts → dist/image/index.d.ts} +1 -14
- package/node_modules/@gsd/native/dist/image/index.js +18 -0
- package/node_modules/@gsd/native/dist/image/types.d.ts +35 -0
- package/node_modules/@gsd/native/dist/image/types.js +26 -0
- package/node_modules/@gsd/native/{src/index.ts → dist/index.d.ts} +12 -60
- package/node_modules/@gsd/native/dist/index.js +28 -0
- package/node_modules/@gsd/native/dist/native.d.ts +44 -0
- package/node_modules/@gsd/native/dist/native.js +34 -0
- package/node_modules/@gsd/native/dist/ps/index.d.ts +38 -0
- package/node_modules/@gsd/native/{src/ps/index.ts → dist/ps/index.js} +8 -13
- package/node_modules/@gsd/native/{src/ps/types.ts → dist/ps/types.d.ts} +2 -2
- package/node_modules/@gsd/native/dist/ps/types.js +1 -0
- package/node_modules/@gsd/native/{src/text/index.ts → dist/text/index.d.ts} +6 -76
- package/node_modules/@gsd/native/dist/text/index.js +66 -0
- package/node_modules/@gsd/native/dist/text/types.d.ts +27 -0
- package/node_modules/@gsd/native/dist/text/types.js +10 -0
- package/node_modules/@gsd/native/{src/ttsr/index.ts → dist/ttsr/index.d.ts} +3 -15
- package/node_modules/@gsd/native/dist/ttsr/index.js +32 -0
- package/node_modules/@gsd/native/{src/ttsr/types.ts → dist/ttsr/types.d.ts} +4 -5
- package/node_modules/@gsd/native/dist/ttsr/types.js +1 -0
- package/node_modules/@gsd/native/package.json +24 -23
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
- package/package.json +4 -2
- package/packages/native/dist/ast/index.d.ts +4 -0
- package/packages/native/dist/ast/index.js +7 -0
- package/packages/native/dist/ast/types.d.ts +69 -0
- package/packages/native/dist/ast/types.js +1 -0
- package/packages/native/dist/clipboard/index.d.ts +28 -0
- package/packages/native/dist/clipboard/index.js +33 -0
- package/packages/native/dist/clipboard/types.d.ts +7 -0
- package/packages/native/dist/clipboard/types.js +1 -0
- package/packages/native/dist/diff/index.d.ts +33 -0
- package/packages/native/dist/diff/index.js +38 -0
- package/packages/native/dist/diff/types.d.ts +23 -0
- package/packages/native/dist/diff/types.js +1 -0
- package/packages/native/dist/fd/index.d.ts +25 -0
- package/packages/native/dist/fd/index.js +26 -0
- package/packages/native/dist/fd/types.d.ts +29 -0
- package/packages/native/dist/fd/types.js +1 -0
- package/packages/native/dist/glob/index.d.ts +28 -0
- package/packages/native/dist/glob/index.js +31 -0
- package/packages/native/dist/glob/types.d.ts +50 -0
- package/packages/native/dist/glob/types.js +1 -0
- package/packages/native/dist/grep/index.d.ts +20 -0
- package/packages/native/dist/grep/index.js +23 -0
- package/packages/native/dist/grep/types.d.ts +99 -0
- package/packages/native/dist/grep/types.js +1 -0
- package/packages/native/dist/gsd-parser/index.d.ts +45 -0
- package/packages/native/dist/gsd-parser/index.js +54 -0
- package/packages/native/dist/gsd-parser/types.d.ts +55 -0
- package/packages/native/dist/gsd-parser/types.js +7 -0
- package/packages/native/dist/highlight/index.d.ts +28 -0
- package/packages/native/dist/highlight/index.js +33 -0
- package/packages/native/dist/highlight/types.d.ts +25 -0
- package/packages/native/dist/highlight/types.js +1 -0
- package/packages/native/dist/html/index.d.ts +15 -0
- package/packages/native/dist/html/index.js +16 -0
- package/packages/native/dist/html/types.d.ts +7 -0
- package/packages/native/dist/html/types.js +1 -0
- package/packages/native/dist/image/index.d.ts +15 -0
- package/packages/native/dist/image/index.js +18 -0
- package/packages/native/dist/image/types.d.ts +35 -0
- package/packages/native/dist/image/types.js +26 -0
- package/packages/native/dist/index.d.ts +40 -0
- package/packages/native/dist/index.js +28 -0
- package/packages/native/dist/native.d.ts +44 -0
- package/packages/native/dist/native.js +34 -0
- package/packages/native/dist/ps/index.d.ts +38 -0
- package/packages/native/dist/ps/index.js +47 -0
- package/packages/native/dist/ps/types.d.ts +5 -0
- package/packages/native/dist/ps/types.js +1 -0
- package/packages/native/dist/text/index.d.ts +55 -0
- package/packages/native/dist/text/index.js +66 -0
- package/packages/native/dist/text/types.d.ts +27 -0
- package/packages/native/dist/text/types.js +10 -0
- package/packages/native/dist/ttsr/index.d.ts +27 -0
- package/packages/native/dist/ttsr/index.js +32 -0
- package/packages/native/dist/ttsr/types.d.ts +9 -0
- package/packages/native/dist/ttsr/types.js +1 -0
- package/packages/native/package.json +24 -23
- package/packages/native/src/__tests__/diff.test.mjs +189 -0
- package/packages/native/src/__tests__/ttsr.test.mjs +135 -0
- package/packages/native/src/diff/index.ts +61 -0
- package/packages/native/src/diff/types.ts +24 -0
- package/packages/native/src/gsd-parser/index.ts +98 -0
- package/packages/native/src/gsd-parser/types.ts +62 -0
- package/packages/native/src/index.ts +23 -0
- package/packages/native/src/native.ts +8 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +11 -5
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +19 -142
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +23 -157
- package/src/resources/extensions/gsd/files.ts +9 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +135 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +86 -0
- package/node_modules/@gsd/native/src/__tests__/clipboard.test.mjs +0 -79
- package/node_modules/@gsd/native/src/__tests__/fd.test.mjs +0 -164
- package/node_modules/@gsd/native/src/__tests__/glob.test.mjs +0 -237
- package/node_modules/@gsd/native/src/__tests__/grep.test.mjs +0 -162
- package/node_modules/@gsd/native/src/__tests__/highlight.test.mjs +0 -156
- package/node_modules/@gsd/native/src/__tests__/html.test.mjs +0 -98
- package/node_modules/@gsd/native/src/__tests__/image.test.mjs +0 -137
- package/node_modules/@gsd/native/src/__tests__/ps.test.mjs +0 -109
- package/node_modules/@gsd/native/src/__tests__/text.test.mjs +0 -262
- package/node_modules/@gsd/native/src/ast/index.ts +0 -12
- package/node_modules/@gsd/native/src/ast/types.ts +0 -75
- package/node_modules/@gsd/native/src/clipboard/types.ts +0 -7
- package/node_modules/@gsd/native/src/fd/types.ts +0 -31
- package/node_modules/@gsd/native/src/glob/types.ts +0 -53
- package/node_modules/@gsd/native/src/grep/index.ts +0 -48
- package/node_modules/@gsd/native/src/grep/types.ts +0 -105
- package/node_modules/@gsd/native/src/highlight/types.ts +0 -25
- package/node_modules/@gsd/native/src/html/types.ts +0 -7
- package/node_modules/@gsd/native/src/image/types.ts +0 -41
- package/node_modules/@gsd/native/src/native.ts +0 -94
- package/node_modules/@gsd/native/src/text/types.ts +0 -29
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
|
|
20
20
|
import { checkExistingEnvKeys } from '../get-secrets-from-user.ts';
|
|
21
21
|
import { parseRoadmapSlices } from './roadmap-slices.ts';
|
|
22
|
+
import { nativeParseRoadmap, nativeExtractSection, NATIVE_UNAVAILABLE } from './native-parser-bridge.ts';
|
|
22
23
|
|
|
23
24
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -130,6 +131,10 @@ export function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
|
|
|
130
131
|
|
|
131
132
|
/** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
|
|
132
133
|
export function extractSection(body: string, heading: string, level: number = 2): string | null {
|
|
134
|
+
// Try native parser first for better performance on large files
|
|
135
|
+
const nativeResult = nativeExtractSection(body, heading, level);
|
|
136
|
+
if (nativeResult !== NATIVE_UNAVAILABLE) return nativeResult as string | null;
|
|
137
|
+
|
|
133
138
|
const prefix = '#'.repeat(level) + ' ';
|
|
134
139
|
const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm');
|
|
135
140
|
const match = regex.exec(body);
|
|
@@ -182,6 +187,10 @@ export function extractBoldField(text: string, key: string): string | null {
|
|
|
182
187
|
// ─── Roadmap Parser ────────────────────────────────────────────────────────
|
|
183
188
|
|
|
184
189
|
export function parseRoadmap(content: string): Roadmap {
|
|
190
|
+
// Try native parser first for better performance
|
|
191
|
+
const nativeResult = nativeParseRoadmap(content);
|
|
192
|
+
if (nativeResult) return nativeResult;
|
|
193
|
+
|
|
185
194
|
const lines = content.split('\n');
|
|
186
195
|
|
|
187
196
|
const h1 = lines.find(l => l.startsWith('# '));
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Native GSD Parser Bridge
|
|
2
|
+
// Provides drop-in replacements for the JS parsing functions in files.ts,
|
|
3
|
+
// backed by the Rust native parser for better performance on large projects.
|
|
4
|
+
//
|
|
5
|
+
// Functions fall back to JS implementations if the native module is unavailable.
|
|
6
|
+
|
|
7
|
+
import type { Roadmap, BoundaryMapEntry, RoadmapSliceEntry, RiskLevel } from './types.ts';
|
|
8
|
+
|
|
9
|
+
let nativeModule: {
|
|
10
|
+
parseFrontmatter: (content: string) => { metadata: string; body: string };
|
|
11
|
+
extractSection: (content: string, heading: string, level?: number) => { content: string; found: boolean };
|
|
12
|
+
extractAllSections: (content: string, level?: number) => string;
|
|
13
|
+
batchParseGsdFiles: (directory: string) => { files: Array<{ path: string; metadata: string; body: string; sections: string }>; count: number };
|
|
14
|
+
parseRoadmapFile: (content: string) => {
|
|
15
|
+
title: string;
|
|
16
|
+
vision: string;
|
|
17
|
+
successCriteria: string[];
|
|
18
|
+
slices: Array<{ id: string; title: string; risk: string; depends: string[]; done: boolean; demo: string }>;
|
|
19
|
+
boundaryMap: Array<{ fromSlice: string; toSlice: string; produces: string; consumes: string }>;
|
|
20
|
+
};
|
|
21
|
+
} | null = null;
|
|
22
|
+
|
|
23
|
+
let loadAttempted = false;
|
|
24
|
+
|
|
25
|
+
function loadNative(): typeof nativeModule {
|
|
26
|
+
if (loadAttempted) return nativeModule;
|
|
27
|
+
loadAttempted = true;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Dynamic import to avoid hard dependency - fails gracefully if native module not built
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const mod = require('@gsd/native');
|
|
33
|
+
if (mod.parseFrontmatter && mod.extractSection && mod.batchParseGsdFiles) {
|
|
34
|
+
nativeModule = mod;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Native module not available - all functions fall back to JS
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return nativeModule;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Native-backed frontmatter splitting.
|
|
45
|
+
* Returns [parsedMetadata, body] where parsedMetadata is the parsed key-value map.
|
|
46
|
+
*/
|
|
47
|
+
export function nativeSplitFrontmatter(content: string): { metadata: Record<string, unknown>; body: string } | null {
|
|
48
|
+
const native = loadNative();
|
|
49
|
+
if (!native) return null;
|
|
50
|
+
|
|
51
|
+
const result = native.parseFrontmatter(content);
|
|
52
|
+
return {
|
|
53
|
+
metadata: JSON.parse(result.metadata) as Record<string, unknown>,
|
|
54
|
+
body: result.body,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Sentinel value indicating the native module is not available. */
|
|
59
|
+
const NATIVE_UNAVAILABLE = Symbol('native-unavailable');
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Native-backed section extraction.
|
|
63
|
+
* Returns section content, null if not found, or NATIVE_UNAVAILABLE symbol
|
|
64
|
+
* if the native module isn't loaded.
|
|
65
|
+
*/
|
|
66
|
+
export function nativeExtractSection(content: string, heading: string, level: number = 2): string | null | typeof NATIVE_UNAVAILABLE {
|
|
67
|
+
const native = loadNative();
|
|
68
|
+
if (!native) return NATIVE_UNAVAILABLE;
|
|
69
|
+
|
|
70
|
+
const result = native.extractSection(content, heading, level);
|
|
71
|
+
return result.found ? result.content : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { NATIVE_UNAVAILABLE };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Native-backed roadmap parsing.
|
|
78
|
+
* Returns a Roadmap object or null if native module unavailable.
|
|
79
|
+
*/
|
|
80
|
+
export function nativeParseRoadmap(content: string): Roadmap | null {
|
|
81
|
+
const native = loadNative();
|
|
82
|
+
if (!native) return null;
|
|
83
|
+
|
|
84
|
+
const result = native.parseRoadmapFile(content);
|
|
85
|
+
return {
|
|
86
|
+
title: result.title,
|
|
87
|
+
vision: result.vision,
|
|
88
|
+
successCriteria: result.successCriteria,
|
|
89
|
+
slices: result.slices.map(s => ({
|
|
90
|
+
id: s.id,
|
|
91
|
+
title: s.title,
|
|
92
|
+
risk: s.risk as RiskLevel,
|
|
93
|
+
depends: s.depends,
|
|
94
|
+
done: s.done,
|
|
95
|
+
demo: s.demo,
|
|
96
|
+
})),
|
|
97
|
+
boundaryMap: result.boundaryMap.map(b => ({
|
|
98
|
+
fromSlice: b.fromSlice,
|
|
99
|
+
toSlice: b.toSlice,
|
|
100
|
+
produces: b.produces,
|
|
101
|
+
consumes: b.consumes,
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BatchParsedFile {
|
|
107
|
+
path: string;
|
|
108
|
+
metadata: Record<string, unknown>;
|
|
109
|
+
body: string;
|
|
110
|
+
sections: Record<string, string>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Batch-parse all .md files in a .gsd/ directory tree using the native parser.
|
|
115
|
+
* Returns null if native module unavailable.
|
|
116
|
+
*/
|
|
117
|
+
export function nativeBatchParseGsdFiles(directory: string): BatchParsedFile[] | null {
|
|
118
|
+
const native = loadNative();
|
|
119
|
+
if (!native) return null;
|
|
120
|
+
|
|
121
|
+
const result = native.batchParseGsdFiles(directory);
|
|
122
|
+
return result.files.map(f => ({
|
|
123
|
+
path: f.path,
|
|
124
|
+
metadata: JSON.parse(f.metadata) as Record<string, unknown>,
|
|
125
|
+
body: f.body,
|
|
126
|
+
sections: JSON.parse(f.sections) as Record<string, string>,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if the native parser is available.
|
|
132
|
+
*/
|
|
133
|
+
export function isNativeParserAvailable(): boolean {
|
|
134
|
+
return loadNative() !== null;
|
|
135
|
+
}
|
|
@@ -4,9 +4,34 @@
|
|
|
4
4
|
* Manages rules that get injected mid-stream when their condition pattern matches
|
|
5
5
|
* the agent's output. When a match occurs, the stream is aborted, the rule is
|
|
6
6
|
* injected as a system reminder, and the request is retried.
|
|
7
|
+
*
|
|
8
|
+
* The regex hot-path is delegated to a native Rust RegexSet engine when
|
|
9
|
+
* available, testing all patterns in a single DFA pass. Falls back to
|
|
10
|
+
* per-rule JS RegExp iteration when the native module is not loaded.
|
|
7
11
|
*/
|
|
8
12
|
import picomatch from "picomatch";
|
|
9
13
|
|
|
14
|
+
// ── Native TTSR engine (optional) ─────────────────────────────────────
|
|
15
|
+
let nativeTtsr: {
|
|
16
|
+
ttsrCompileRules: (rules: { name: string; conditions: string[] }[]) => number;
|
|
17
|
+
ttsrCheckBuffer: (handle: number, buffer: string) => string[];
|
|
18
|
+
ttsrFreeRules: (handle: number) => void;
|
|
19
|
+
} | null = null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Dynamic import to avoid hard dependency — gracefully degrades to JS.
|
|
23
|
+
const native = await import("@gsd/native");
|
|
24
|
+
if (native.ttsrCompileRules && native.ttsrCheckBuffer && native.ttsrFreeRules) {
|
|
25
|
+
nativeTtsr = {
|
|
26
|
+
ttsrCompileRules: native.ttsrCompileRules,
|
|
27
|
+
ttsrCheckBuffer: native.ttsrCheckBuffer,
|
|
28
|
+
ttsrFreeRules: native.ttsrFreeRules,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Native module not available — JS fallback will be used.
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
export type TtsrMatchSource = "text" | "thinking" | "tool";
|
|
11
36
|
|
|
12
37
|
/** Context about the stream content currently being checked against TTSR rules. */
|
|
@@ -86,6 +111,8 @@ export class TtsrManager {
|
|
|
86
111
|
readonly #injectionRecords = new Map<string, InjectionRecord>();
|
|
87
112
|
readonly #buffers = new Map<string, string>();
|
|
88
113
|
#messageCount = 0;
|
|
114
|
+
#nativeHandle: number | null = null;
|
|
115
|
+
#nativeDirty = false;
|
|
89
116
|
|
|
90
117
|
constructor(settings?: TtsrSettings) {
|
|
91
118
|
this.#settings = { ...DEFAULT_SETTINGS, ...settings };
|
|
@@ -245,6 +272,40 @@ export class TtsrManager {
|
|
|
245
272
|
return false;
|
|
246
273
|
}
|
|
247
274
|
|
|
275
|
+
/** Compile (or recompile) the native RegexSet from all current rules. */
|
|
276
|
+
#compileNative(): void {
|
|
277
|
+
if (!nativeTtsr || !this.#nativeDirty) return;
|
|
278
|
+
|
|
279
|
+
// Free previous handle if any.
|
|
280
|
+
if (this.#nativeHandle !== null) {
|
|
281
|
+
try {
|
|
282
|
+
nativeTtsr.ttsrFreeRules(this.#nativeHandle);
|
|
283
|
+
} catch { /* ignore */ }
|
|
284
|
+
this.#nativeHandle = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const ruleInputs: { name: string; conditions: string[] }[] = [];
|
|
288
|
+
for (const [, entry] of this.#rules) {
|
|
289
|
+
ruleInputs.push({
|
|
290
|
+
name: entry.rule.name,
|
|
291
|
+
conditions: entry.rule.condition,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (ruleInputs.length === 0) {
|
|
296
|
+
this.#nativeDirty = false;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
this.#nativeHandle = nativeTtsr.ttsrCompileRules(ruleInputs);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.warn(`[ttsr] Native compilation failed, using JS fallback: ${(err as Error).message}`);
|
|
304
|
+
this.#nativeHandle = null;
|
|
305
|
+
}
|
|
306
|
+
this.#nativeDirty = false;
|
|
307
|
+
}
|
|
308
|
+
|
|
248
309
|
/** Add a TTSR rule to be monitored. */
|
|
249
310
|
addRule(rule: Rule): boolean {
|
|
250
311
|
if (this.#rules.has(rule.name)) return false;
|
|
@@ -257,6 +318,7 @@ export class TtsrManager {
|
|
|
257
318
|
|
|
258
319
|
const globalPathMatchers = this.#compileGlobalPathMatchers(rule.globs);
|
|
259
320
|
this.#rules.set(rule.name, { rule, conditions, scope, globalPathMatchers });
|
|
321
|
+
this.#nativeDirty = true;
|
|
260
322
|
return true;
|
|
261
323
|
}
|
|
262
324
|
|
|
@@ -265,6 +327,10 @@ export class TtsrManager {
|
|
|
265
327
|
*
|
|
266
328
|
* Buffers are isolated by source/tool key so matches don't bleed across
|
|
267
329
|
* assistant prose, thinking text, and unrelated tool argument streams.
|
|
330
|
+
*
|
|
331
|
+
* When the native Rust engine is available, all regex conditions are tested
|
|
332
|
+
* in a single DFA pass via RegexSet. Scope, glob, and repeat-gate checks
|
|
333
|
+
* remain in JS as they are lightweight and context-dependent.
|
|
268
334
|
*/
|
|
269
335
|
checkDelta(delta: string, context: TtsrMatchContext): Rule[] {
|
|
270
336
|
const bufferKey = this.#bufferKey(context);
|
|
@@ -275,6 +341,26 @@ export class TtsrManager {
|
|
|
275
341
|
}
|
|
276
342
|
this.#buffers.set(bufferKey, nextBuffer);
|
|
277
343
|
|
|
344
|
+
// Lazily compile native engine if rules changed.
|
|
345
|
+
if (this.#nativeDirty) this.#compileNative();
|
|
346
|
+
|
|
347
|
+
// ── Native path: single-pass RegexSet match ───────────────────────
|
|
348
|
+
if (nativeTtsr && this.#nativeHandle !== null) {
|
|
349
|
+
const regexMatchedNames = nativeTtsr.ttsrCheckBuffer(this.#nativeHandle, nextBuffer);
|
|
350
|
+
const regexMatchedSet = new Set(regexMatchedNames);
|
|
351
|
+
|
|
352
|
+
const matches: Rule[] = [];
|
|
353
|
+
for (const [name, entry] of this.#rules) {
|
|
354
|
+
if (!regexMatchedSet.has(name)) continue;
|
|
355
|
+
if (!this.#canTrigger(name)) continue;
|
|
356
|
+
if (!this.#matchesScope(entry, context)) continue;
|
|
357
|
+
if (!this.#matchesGlobalPaths(entry, context)) continue;
|
|
358
|
+
matches.push(entry.rule);
|
|
359
|
+
}
|
|
360
|
+
return matches;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── JS fallback: per-rule regex iteration ─────────────────────────
|
|
278
364
|
const matches: Rule[] = [];
|
|
279
365
|
for (const [name, entry] of this.#rules) {
|
|
280
366
|
if (!this.#canTrigger(name)) continue;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { test, describe } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
|
|
10
|
-
const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon");
|
|
11
|
-
const platformTag = `${process.platform}-${process.arch}`;
|
|
12
|
-
const candidates = [
|
|
13
|
-
path.join(addonDir, `gsd_engine.${platformTag}.node`),
|
|
14
|
-
path.join(addonDir, "gsd_engine.dev.node"),
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
let native;
|
|
18
|
-
for (const candidate of candidates) {
|
|
19
|
-
try {
|
|
20
|
-
native = require(candidate);
|
|
21
|
-
break;
|
|
22
|
-
} catch {
|
|
23
|
-
// try next
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!native) {
|
|
28
|
-
console.error("Native addon not found. Run build:native first.");
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("native clipboard: copyToClipboard()", () => {
|
|
33
|
-
test("copies text without throwing", () => {
|
|
34
|
-
assert.doesNotThrow(() => {
|
|
35
|
-
native.copyToClipboard("GSD clipboard test");
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("accepts empty string", () => {
|
|
40
|
-
assert.doesNotThrow(() => {
|
|
41
|
-
native.copyToClipboard("");
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("accepts unicode text", () => {
|
|
46
|
-
assert.doesNotThrow(() => {
|
|
47
|
-
native.copyToClipboard("Hello 世界");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("native clipboard: readTextFromClipboard()", () => {
|
|
53
|
-
test("reads back text that was copied", () => {
|
|
54
|
-
const testText = `GSD clipboard roundtrip ${Date.now()}`;
|
|
55
|
-
native.copyToClipboard(testText);
|
|
56
|
-
const result = native.readTextFromClipboard();
|
|
57
|
-
assert.equal(result, testText);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("returns a string or null", () => {
|
|
61
|
-
const result = native.readTextFromClipboard();
|
|
62
|
-
assert.ok(result === null || typeof result === "string");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("native clipboard: readImageFromClipboard()", () => {
|
|
67
|
-
test("returns a promise", () => {
|
|
68
|
-
const result = native.readImageFromClipboard();
|
|
69
|
-
assert.ok(result instanceof Promise);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("resolves to ClipboardImage or null", async () => {
|
|
73
|
-
const result = await native.readImageFromClipboard();
|
|
74
|
-
if (result !== null) {
|
|
75
|
-
assert.ok(result.data instanceof Uint8Array, "data should be Uint8Array");
|
|
76
|
-
assert.equal(result.mimeType, "image/png");
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { test, describe } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
|
-
|
|
9
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
|
|
12
|
-
// Load the native addon directly
|
|
13
|
-
const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon");
|
|
14
|
-
const platformTag = `${process.platform}-${process.arch}`;
|
|
15
|
-
const candidates = [
|
|
16
|
-
path.join(addonDir, `gsd_engine.${platformTag}.node`),
|
|
17
|
-
path.join(addonDir, "gsd_engine.dev.node"),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
let native;
|
|
21
|
-
for (const candidate of candidates) {
|
|
22
|
-
try {
|
|
23
|
-
native = require(candidate);
|
|
24
|
-
break;
|
|
25
|
-
} catch {
|
|
26
|
-
// try next
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!native) {
|
|
31
|
-
console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first.");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("native fd: fuzzyFind()", () => {
|
|
36
|
-
test("finds files matching a query", (t) => {
|
|
37
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
38
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
39
|
-
|
|
40
|
-
fs.writeFileSync(path.join(tmpDir, "main.rs"), "fn main() {}");
|
|
41
|
-
fs.writeFileSync(path.join(tmpDir, "lib.rs"), "pub mod lib;");
|
|
42
|
-
fs.writeFileSync(path.join(tmpDir, "utils.ts"), "export {}");
|
|
43
|
-
fs.mkdirSync(path.join(tmpDir, "src"));
|
|
44
|
-
fs.writeFileSync(path.join(tmpDir, "src", "helper.rs"), "fn helper() {}");
|
|
45
|
-
|
|
46
|
-
const result = native.fuzzyFind({ query: "main", path: tmpDir });
|
|
47
|
-
|
|
48
|
-
assert.ok(result.matches.length > 0, "Should find at least one match");
|
|
49
|
-
assert.equal(result.matches[0].path, "main.rs");
|
|
50
|
-
assert.equal(result.matches[0].isDirectory, false);
|
|
51
|
-
assert.ok(result.matches[0].score > 0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("returns empty results for non-matching query", (t) => {
|
|
55
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
56
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
57
|
-
|
|
58
|
-
fs.writeFileSync(path.join(tmpDir, "hello.txt"), "hello");
|
|
59
|
-
|
|
60
|
-
const result = native.fuzzyFind({
|
|
61
|
-
query: "zzzznotexist",
|
|
62
|
-
path: tmpDir,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
assert.equal(result.matches.length, 0);
|
|
66
|
-
assert.equal(result.totalMatches, 0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("respects maxResults limit", (t) => {
|
|
70
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
71
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < 10; i++) {
|
|
74
|
-
fs.writeFileSync(path.join(tmpDir, `file${i}.txt`), "content");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = native.fuzzyFind({
|
|
78
|
-
query: "file",
|
|
79
|
-
path: tmpDir,
|
|
80
|
-
maxResults: 3,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
assert.equal(result.matches.length, 3);
|
|
84
|
-
assert.ok(result.totalMatches >= 3);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("directories have trailing slash and bonus score", (t) => {
|
|
88
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
89
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
90
|
-
|
|
91
|
-
fs.mkdirSync(path.join(tmpDir, "models"));
|
|
92
|
-
fs.writeFileSync(path.join(tmpDir, "models.ts"), "export {}");
|
|
93
|
-
|
|
94
|
-
const result = native.fuzzyFind({ query: "models", path: tmpDir });
|
|
95
|
-
|
|
96
|
-
const dirMatch = result.matches.find((m) => m.isDirectory);
|
|
97
|
-
const fileMatch = result.matches.find((m) => !m.isDirectory);
|
|
98
|
-
|
|
99
|
-
assert.ok(dirMatch, "Should find a directory match");
|
|
100
|
-
assert.ok(fileMatch, "Should find a file match");
|
|
101
|
-
assert.ok(dirMatch.path.endsWith("/"), "Directory should have trailing slash");
|
|
102
|
-
assert.ok(dirMatch.score > fileMatch.score, "Directory should score higher");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("empty query returns all entries", (t) => {
|
|
106
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
107
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
108
|
-
|
|
109
|
-
fs.writeFileSync(path.join(tmpDir, "a.txt"), "a");
|
|
110
|
-
fs.writeFileSync(path.join(tmpDir, "b.txt"), "b");
|
|
111
|
-
fs.writeFileSync(path.join(tmpDir, "c.txt"), "c");
|
|
112
|
-
|
|
113
|
-
const result = native.fuzzyFind({ query: "", path: tmpDir });
|
|
114
|
-
|
|
115
|
-
assert.equal(result.matches.length, 3);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("errors on non-existent path", () => {
|
|
119
|
-
assert.throws(
|
|
120
|
-
() => native.fuzzyFind({ query: "test", path: "/nonexistent/path" }),
|
|
121
|
-
{ message: /Path not found/ },
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("fuzzy subsequence matching works", (t) => {
|
|
126
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
127
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
128
|
-
|
|
129
|
-
fs.writeFileSync(path.join(tmpDir, "MyComponentFile.tsx"), "export {}");
|
|
130
|
-
fs.writeFileSync(path.join(tmpDir, "other.txt"), "other");
|
|
131
|
-
|
|
132
|
-
// "mcf" should fuzzy-match "MyComponentFile" via subsequence
|
|
133
|
-
const result = native.fuzzyFind({ query: "mcf", path: tmpDir });
|
|
134
|
-
|
|
135
|
-
assert.ok(result.matches.length > 0, "Fuzzy subsequence should match");
|
|
136
|
-
assert.ok(
|
|
137
|
-
result.matches.some((m) => m.path.includes("MyComponentFile")),
|
|
138
|
-
"Should find MyComponentFile via fuzzy match",
|
|
139
|
-
);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("results are sorted by score descending", (t) => {
|
|
143
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-fd-test-"));
|
|
144
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
145
|
-
|
|
146
|
-
fs.writeFileSync(path.join(tmpDir, "main.ts"), "");
|
|
147
|
-
fs.writeFileSync(path.join(tmpDir, "my_main.ts"), "");
|
|
148
|
-
fs.mkdirSync(path.join(tmpDir, "src"));
|
|
149
|
-
fs.writeFileSync(path.join(tmpDir, "src", "main.rs"), "");
|
|
150
|
-
|
|
151
|
-
const result = native.fuzzyFind({
|
|
152
|
-
query: "main",
|
|
153
|
-
path: tmpDir,
|
|
154
|
-
maxResults: 100,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
for (let i = 1; i < result.matches.length; i++) {
|
|
158
|
-
assert.ok(
|
|
159
|
-
result.matches[i - 1].score >= result.matches[i].score,
|
|
160
|
-
`Match ${i - 1} (score ${result.matches[i - 1].score}) should be >= match ${i} (score ${result.matches[i].score})`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|