@gobing-ai/ts-rule-engine 0.5.2 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEH,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;AA8BD;;;;;;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"}
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"}
@@ -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
- const rel = relativePath(resolvePath(workdir), resolvePath(absPath));
111
- return rel === '' || (!rel.startsWith('..') && !isAbsolutePath(rel));
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.2",
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.2",
53
- "@gobing-ai/ts-infra": "^0.5.2",
54
- "@gobing-ai/ts-runtime": "^0.5.2",
55
- "@gobing-ai/ts-db": "^0.5.2",
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
  },
@@ -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
- const rel = relativePath(resolvePath(workdir), resolvePath(absPath));
161
- return rel === '' || (!rel.startsWith('..') && !isAbsolutePath(rel));
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);