@gobing-ai/ts-rule-engine 0.5.4 → 0.5.5
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/fixers/fixers.d.ts.map +1 -1
- package/dist/fixers/fixers.js +28 -7
- package/package.json +5 -5
- package/src/fixers/fixers.ts +26 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAGH,KAAK,UAAU,EAGf,KAAK,eAAe,EAGvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAkB,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAIhG,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAEhF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAIjD,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACjC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI,CAcxF;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,GAAG,EAAE,EACrB,MAAM,UAAQ,EACd,EAAE,GAAE,UAAmC,GACxC,OAAO,CAAC,oBAAoB,CAAC,CA0D/B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzF;AAiDD;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IACxD,kDAAkD;IAC5C,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAoCtF;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IACvD,0DAA0D;IACpD,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAoBtF"}
|
package/dist/fixers/fixers.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
|
-
import { createNodeFileSystem, isAbsolutePath, joinPath, relativePath, resolvePath, } from '@gobing-ai/ts-runtime';
|
|
7
|
+
import { createNodeFileSystem, dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath, } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import { TestStubFixer } from './test-stub-fixer.js';
|
|
9
9
|
/** Numeric ordering for fix authority; higher means more write authority. */
|
|
10
10
|
export const FIX_MODE_RANK = {
|
|
@@ -42,7 +42,7 @@ export async function applyFixes(workdir, fixes, dryRun = false, fs = createNode
|
|
|
42
42
|
const diffs = [];
|
|
43
43
|
for (const [filePath, fileFixes] of byFile) {
|
|
44
44
|
const absPath = resolveWorkdirPath(workdir, filePath);
|
|
45
|
-
if (!isInsideWorkdir(workdir, absPath)) {
|
|
45
|
+
if (!(await isInsideWorkdir(workdir, absPath, fs))) {
|
|
46
46
|
deferred.push(...fileFixes);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
@@ -105,10 +105,31 @@ function selectNonOverlappingFixes(fixes) {
|
|
|
105
105
|
}
|
|
106
106
|
return { applied, deferred };
|
|
107
107
|
}
|
|
108
|
-
/** Return true when absPath is at or below workdir. */
|
|
109
|
-
function isInsideWorkdir(workdir, absPath) {
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
/** Return true when absPath is at or below workdir after symlink resolution. */
|
|
109
|
+
async function isInsideWorkdir(workdir, absPath, fs) {
|
|
110
|
+
// Task 0087 R3: fail closed when symlink resolution is unavailable — without
|
|
111
|
+
// realPath the containment proof is unsound, so refuse instead of degrading.
|
|
112
|
+
if (fs.realPath === undefined)
|
|
113
|
+
return false;
|
|
114
|
+
const real = (p) => fs.realPath?.(p) ?? p;
|
|
115
|
+
const root = real(resolvePath(workdir));
|
|
116
|
+
// Task 0086 R16: resolve through the nearest existing ancestor (ADR-022
|
|
117
|
+
// walk approach) — a not-yet-existing file can still escape the workdir
|
|
118
|
+
// through a symlinked parent directory, which plain path math misses.
|
|
119
|
+
const target = resolvePath(absPath);
|
|
120
|
+
let prefix = target;
|
|
121
|
+
const tail = [];
|
|
122
|
+
while (!(await fs.exists(prefix))) {
|
|
123
|
+
const parent = dirnamePath(prefix);
|
|
124
|
+
if (parent === prefix)
|
|
125
|
+
break; // filesystem root
|
|
126
|
+
tail.unshift(prefix.slice(parent.length + 1));
|
|
127
|
+
prefix = parent;
|
|
128
|
+
}
|
|
129
|
+
const resolved = tail.length > 0 ? joinPath(real(prefix), ...tail) : real(prefix);
|
|
130
|
+
const rel = relativePath(root, resolved);
|
|
131
|
+
const first = rel.split(/[\\/]/)[0];
|
|
132
|
+
return rel === '' || (first !== '..' && !isAbsolutePath(rel));
|
|
112
133
|
}
|
|
113
134
|
/** Return true when [start, end] is a valid byte range for a string of contentLength bytes. */
|
|
114
135
|
function isValidRange(start, end, contentLength) {
|
|
@@ -140,7 +161,7 @@ export class RegexFixerProvider {
|
|
|
140
161
|
if (!finding.filePath)
|
|
141
162
|
continue;
|
|
142
163
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
143
|
-
if (!isInsideWorkdir(context.workdir, absPath) || finding.line == null)
|
|
164
|
+
if (!(await isInsideWorkdir(context.workdir, absPath, fs)) || finding.line == null)
|
|
144
165
|
continue;
|
|
145
166
|
if (!(await fs.exists(absPath)))
|
|
146
167
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-rule-engine",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@gobing-ai/ts-ai-runner": "^0.5.
|
|
53
|
-
"@gobing-ai/ts-infra": "^0.5.
|
|
54
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
55
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
52
|
+
"@gobing-ai/ts-ai-runner": "^0.5.5",
|
|
53
|
+
"@gobing-ai/ts-infra": "^0.5.5",
|
|
54
|
+
"@gobing-ai/ts-runtime": "^0.5.5",
|
|
55
|
+
"@gobing-ai/ts-db": "^0.5.5",
|
|
56
56
|
"yaml": "^2.7.0",
|
|
57
57
|
"zod": "^4.1.0"
|
|
58
58
|
},
|
package/src/fixers/fixers.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
createNodeFileSystem,
|
|
10
|
+
dirnamePath,
|
|
10
11
|
type FileSystem,
|
|
11
12
|
isAbsolutePath,
|
|
12
13
|
joinPath,
|
|
@@ -83,7 +84,7 @@ export async function applyFixes(
|
|
|
83
84
|
|
|
84
85
|
for (const [filePath, fileFixes] of byFile) {
|
|
85
86
|
const absPath = resolveWorkdirPath(workdir, filePath);
|
|
86
|
-
if (!isInsideWorkdir(workdir, absPath)) {
|
|
87
|
+
if (!(await isInsideWorkdir(workdir, absPath, fs))) {
|
|
87
88
|
deferred.push(...fileFixes);
|
|
88
89
|
continue;
|
|
89
90
|
}
|
|
@@ -155,10 +156,29 @@ function selectNonOverlappingFixes(fixes: readonly Fix[]): { applied: Fix[]; def
|
|
|
155
156
|
return { applied, deferred };
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
/** Return true when absPath is at or below workdir. */
|
|
159
|
-
function isInsideWorkdir(workdir: string, absPath: string): boolean {
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
/** Return true when absPath is at or below workdir after symlink resolution. */
|
|
160
|
+
async function isInsideWorkdir(workdir: string, absPath: string, fs: FileSystem): Promise<boolean> {
|
|
161
|
+
// Task 0087 R3: fail closed when symlink resolution is unavailable — without
|
|
162
|
+
// realPath the containment proof is unsound, so refuse instead of degrading.
|
|
163
|
+
if (fs.realPath === undefined) return false;
|
|
164
|
+
const real = (p: string) => fs.realPath?.(p) ?? p;
|
|
165
|
+
const root = real(resolvePath(workdir));
|
|
166
|
+
// Task 0086 R16: resolve through the nearest existing ancestor (ADR-022
|
|
167
|
+
// walk approach) — a not-yet-existing file can still escape the workdir
|
|
168
|
+
// through a symlinked parent directory, which plain path math misses.
|
|
169
|
+
const target = resolvePath(absPath);
|
|
170
|
+
let prefix = target;
|
|
171
|
+
const tail: string[] = [];
|
|
172
|
+
while (!(await fs.exists(prefix))) {
|
|
173
|
+
const parent = dirnamePath(prefix);
|
|
174
|
+
if (parent === prefix) break; // filesystem root
|
|
175
|
+
tail.unshift(prefix.slice(parent.length + 1));
|
|
176
|
+
prefix = parent;
|
|
177
|
+
}
|
|
178
|
+
const resolved = tail.length > 0 ? joinPath(real(prefix), ...tail) : real(prefix);
|
|
179
|
+
const rel = relativePath(root, resolved);
|
|
180
|
+
const first = rel.split(/[\\/]/)[0];
|
|
181
|
+
return rel === '' || (first !== '..' && !isAbsolutePath(rel));
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
/** Return true when [start, end] is a valid byte range for a string of contentLength bytes. */
|
|
@@ -191,7 +211,7 @@ export class RegexFixerProvider implements RuleFixerProvider {
|
|
|
191
211
|
for (const finding of findings) {
|
|
192
212
|
if (!finding.filePath) continue;
|
|
193
213
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
194
|
-
if (!isInsideWorkdir(context.workdir, absPath) || finding.line == null) continue;
|
|
214
|
+
if (!(await isInsideWorkdir(context.workdir, absPath, fs)) || finding.line == null) continue;
|
|
195
215
|
if (!(await fs.exists(absPath))) continue;
|
|
196
216
|
const source = await fs.readFile(absPath);
|
|
197
217
|
const line = getLineRange(source, finding.line);
|