@gobing-ai/ts-rule-engine 0.5.30 → 0.5.32
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 +20 -0
- package/dist/evaluators/hedge-phrases-evaluator.d.ts +28 -0
- package/dist/evaluators/hedge-phrases-evaluator.d.ts.map +1 -0
- package/dist/evaluators/hedge-phrases-evaluator.js +70 -0
- package/dist/host/builtins.d.ts.map +1 -1
- package/dist/host/builtins.js +2 -0
- package/package.json +5 -5
- package/rules/quality/no-hedged-claims.yaml +32 -0
- package/src/evaluators/hedge-phrases-evaluator.ts +89 -0
- package/src/host/builtins.ts +2 -0
package/README.md
CHANGED
|
@@ -372,6 +372,26 @@ Built-in fixer providers (`RegexFixerProvider`, `PathFixerProvider`, `TestStubFi
|
|
|
372
372
|
| `test-location` | Enforce test placement and matching source/test pairs. | Uses named test-path resolvers. |
|
|
373
373
|
| `schema-artifact` | Validate JSON schema artifact structure. | Checks existence, JSON validity, title, properties, defs, required array. |
|
|
374
374
|
| `sg` | Run an ast-grep pattern. | Requires the `sg` CLI in the execution environment. |
|
|
375
|
+
| `hedge-phrases` | Flag uncertainty phrasing ("it may", "presumably") in scoped files. | Literal phrases, one finding per file per phrase with its first matching line; `caseSensitive` (default false) and `wholeWord` (default true). |
|
|
376
|
+
|
|
377
|
+
Hedge phrases (a claim that hedges states nothing verifiable):
|
|
378
|
+
|
|
379
|
+
```yaml
|
|
380
|
+
rules:
|
|
381
|
+
- id: no-hedged-claims
|
|
382
|
+
severity: warning
|
|
383
|
+
include: ["docs/tasks*/**/*.md"]
|
|
384
|
+
evaluator:
|
|
385
|
+
type: hedge-phrases
|
|
386
|
+
config:
|
|
387
|
+
phrases: ["it may", "presumably"]
|
|
388
|
+
# wholeWord: true # default: "it may" does not match inside "it maybe"
|
|
389
|
+
# caseSensitive: false # default
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
A finding reads `uncertainty phrases detected: it may` and carries the first matching line.
|
|
393
|
+
Enable it only where evidence prose must stay factual — design docs may legitimately hedge.
|
|
394
|
+
Bundled as `rules/quality/no-hedged-claims.yaml` (shipped disabled).
|
|
375
395
|
|
|
376
396
|
### Common Evaluator Examples
|
|
377
397
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Flags uncertainty / hedge phrasing in the files a rule scopes in: a claim that
|
|
4
|
+
* hedges ("it may", "presumably") states no verifiable fact, so it reads as evidence
|
|
5
|
+
* while pinning nothing.
|
|
6
|
+
*
|
|
7
|
+
* Config (`evaluator.config`):
|
|
8
|
+
* - `phrases`: non-empty list of literal phrases to flag (required; a phrase is text,
|
|
9
|
+
* never a pattern)
|
|
10
|
+
* - `caseSensitive`: default `false` — "It May" is the same hedge as "it may"
|
|
11
|
+
* - `wholeWord`: default `true` — `it may` does not match inside `it maybe`
|
|
12
|
+
*
|
|
13
|
+
* One finding per file per phrase, carrying the first matching line, with the message
|
|
14
|
+
* `uncertainty phrases detected: <phrase>`. Scope comes from the rule's `include` /
|
|
15
|
+
* `exclude` (anchored globs), so the plane is the rule author's choice rather than a
|
|
16
|
+
* built-in assumption.
|
|
17
|
+
*/
|
|
18
|
+
export declare class HedgePhrasesEvaluator implements RuleEvaluator {
|
|
19
|
+
/**
|
|
20
|
+
* No collaborators: scope and filesystem come from the rule and the context. Explicit rather
|
|
21
|
+
* than implicit because Bun's coverage counts an implicit class constructor as uncovered, which
|
|
22
|
+
* would keep this file under the per-file function threshold for no real gap.
|
|
23
|
+
*/
|
|
24
|
+
constructor();
|
|
25
|
+
/** Scan the scoped files for each configured phrase. */
|
|
26
|
+
evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hedge-phrases-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hedge-phrases-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/hedge-phrases-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAWlB;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACvD;;;;OAIG;;IAGH,wDAAwD;IAClD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAiC5F"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createFinding, } from '../types.js';
|
|
2
|
+
import { stringArray } from './config-access.js';
|
|
3
|
+
import { scanFiles } from './file-discovery.js';
|
|
4
|
+
/**
|
|
5
|
+
* Flags uncertainty / hedge phrasing in the files a rule scopes in: a claim that
|
|
6
|
+
* hedges ("it may", "presumably") states no verifiable fact, so it reads as evidence
|
|
7
|
+
* while pinning nothing.
|
|
8
|
+
*
|
|
9
|
+
* Config (`evaluator.config`):
|
|
10
|
+
* - `phrases`: non-empty list of literal phrases to flag (required; a phrase is text,
|
|
11
|
+
* never a pattern)
|
|
12
|
+
* - `caseSensitive`: default `false` — "It May" is the same hedge as "it may"
|
|
13
|
+
* - `wholeWord`: default `true` — `it may` does not match inside `it maybe`
|
|
14
|
+
*
|
|
15
|
+
* One finding per file per phrase, carrying the first matching line, with the message
|
|
16
|
+
* `uncertainty phrases detected: <phrase>`. Scope comes from the rule's `include` /
|
|
17
|
+
* `exclude` (anchored globs), so the plane is the rule author's choice rather than a
|
|
18
|
+
* built-in assumption.
|
|
19
|
+
*/
|
|
20
|
+
export class HedgePhrasesEvaluator {
|
|
21
|
+
/**
|
|
22
|
+
* No collaborators: scope and filesystem come from the rule and the context. Explicit rather
|
|
23
|
+
* than implicit because Bun's coverage counts an implicit class constructor as uncovered, which
|
|
24
|
+
* would keep this file under the per-file function threshold for no real gap.
|
|
25
|
+
*/
|
|
26
|
+
constructor() { }
|
|
27
|
+
/** Scan the scoped files for each configured phrase. */
|
|
28
|
+
async evaluate(rule, context) {
|
|
29
|
+
const config = (rule.evaluator.config ?? {});
|
|
30
|
+
const phrases = stringArray(config.phrases) ?? [];
|
|
31
|
+
if (phrases.length === 0) {
|
|
32
|
+
throw new Error('hedge-phrases evaluator requires a non-empty "phrases" config');
|
|
33
|
+
}
|
|
34
|
+
const caseSensitive = config.caseSensitive === true;
|
|
35
|
+
const wholeWord = config.wholeWord !== false;
|
|
36
|
+
const matchers = phrases.map((phrase) => ({ phrase, pattern: toPattern(phrase, caseSensitive, wholeWord) }));
|
|
37
|
+
const files = await scanFiles({
|
|
38
|
+
workdir: context.workdir,
|
|
39
|
+
include: rule.include,
|
|
40
|
+
exclude: rule.exclude,
|
|
41
|
+
matchMode: 'glob',
|
|
42
|
+
...(context.fileSystem !== undefined ? { fs: context.fileSystem } : {}),
|
|
43
|
+
});
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const { file, content } of files) {
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
for (const { phrase, pattern } of matchers) {
|
|
48
|
+
const index = lines.findIndex((line) => pattern.test(line));
|
|
49
|
+
if (index < 0)
|
|
50
|
+
continue;
|
|
51
|
+
findings.push(createFinding(rule, `uncertainty phrases detected: ${phrase}`, file, {
|
|
52
|
+
line: index + 1,
|
|
53
|
+
code: 'hedge-phrases:matched',
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { findings, fixes: [] };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Compile one phrase as literal text, optionally whole-word and case-insensitive. The
|
|
62
|
+
* word boundary is applied only at an edge that is a word character, so a phrase like
|
|
63
|
+
* `(b) may?` still matches — a trailing `\\b` after `?` could never be satisfied.
|
|
64
|
+
*/
|
|
65
|
+
function toPattern(phrase, caseSensitive, wholeWord) {
|
|
66
|
+
const body = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
const lead = wholeWord && /^\w/.test(body) ? '\\b' : '';
|
|
68
|
+
const tail = wholeWord && /\w$/.test(body) ? '\\b' : '';
|
|
69
|
+
return new RegExp(`${lead}${body}${tail}`, caseSensitive ? 'u' : 'iu');
|
|
70
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAuB7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAyBvF"}
|
package/dist/host/builtins.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AgentDetectionEvaluator } from '../evaluators/agent-detection-evaluator
|
|
|
2
2
|
import { CoverageGateEvaluator } from '../evaluators/coverage-gate-evaluator.js';
|
|
3
3
|
import { ExitCodeEvaluator } from '../evaluators/exit-code-evaluator.js';
|
|
4
4
|
import { ForbiddenImportEvaluator } from '../evaluators/forbidden-import-evaluator.js';
|
|
5
|
+
import { HedgePhrasesEvaluator } from '../evaluators/hedge-phrases-evaluator.js';
|
|
5
6
|
import { ImportBoundaryEvaluator } from '../evaluators/import-boundary-evaluator.js';
|
|
6
7
|
import { PathEvaluator } from '../evaluators/path-evaluator.js';
|
|
7
8
|
import { RegexEvaluator } from '../evaluators/regex-evaluator.js';
|
|
@@ -37,6 +38,7 @@ export function registerBuiltins(host, executor) {
|
|
|
37
38
|
host.evaluators.register('sg', new SgEvaluator(executor), 'builtin');
|
|
38
39
|
host.evaluators.register('schema-artifact', new SchemaArtifactEvaluator(), 'builtin');
|
|
39
40
|
host.evaluators.register('import-boundary', new ImportBoundaryEvaluator(), 'builtin');
|
|
41
|
+
host.evaluators.register('hedge-phrases', new HedgePhrasesEvaluator(), 'builtin');
|
|
40
42
|
host.formatters.register('text', new TextFormatter(), 'builtin');
|
|
41
43
|
host.formatters.register('json', new JsonFormatter(), 'builtin');
|
|
42
44
|
}
|
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.32",
|
|
4
4
|
"description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"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"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@gobing-ai/ts-ai-runner": "^0.5.
|
|
55
|
-
"@gobing-ai/ts-infra": "^0.5.
|
|
56
|
-
"@gobing-ai/ts-runtime": "^0.5.
|
|
57
|
-
"@gobing-ai/ts-db": "^0.5.
|
|
54
|
+
"@gobing-ai/ts-ai-runner": "^0.5.32",
|
|
55
|
+
"@gobing-ai/ts-infra": "^0.5.32",
|
|
56
|
+
"@gobing-ai/ts-runtime": "^0.5.32",
|
|
57
|
+
"@gobing-ai/ts-db": "^0.5.32",
|
|
58
58
|
"yaml": "^2.7.0",
|
|
59
59
|
"zod": "^4.1.0"
|
|
60
60
|
},
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Uncertainty-phrase gate — a claim that hedges states nothing a reader can verify.
|
|
2
|
+
#
|
|
3
|
+
# SHIPPED DISABLED. Hedging is style, not a correctness contract for every consumer, and the
|
|
4
|
+
# scope below is this project's task-file plane: enable it per project (and adjust `include`)
|
|
5
|
+
# where evidence prose must stay factual. A runner whose engine predates `hedge-phrases` reports
|
|
6
|
+
# `Unknown evaluator: hedge-phrases` as an error finding, so keep it disabled there.
|
|
7
|
+
#
|
|
8
|
+
# Plane: task files carry the evidence a reviewer trusts, so a hedged claim there ("it may be
|
|
9
|
+
# stale") reads as proof while pinning nothing. Prose in design docs may legitimately hedge.
|
|
10
|
+
include:
|
|
11
|
+
- "docs/tasks*/**/*.md"
|
|
12
|
+
|
|
13
|
+
rules:
|
|
14
|
+
- id: no-hedged-claims
|
|
15
|
+
description: "Task evidence must state what was observed; an uncertainty phrase (\"it may\") is reported instead of read as proof."
|
|
16
|
+
severity: warning
|
|
17
|
+
enabled: false
|
|
18
|
+
evaluator:
|
|
19
|
+
type: hedge-phrases
|
|
20
|
+
config:
|
|
21
|
+
phrases:
|
|
22
|
+
- "it may"
|
|
23
|
+
- "it might"
|
|
24
|
+
- "might be"
|
|
25
|
+
- "may be"
|
|
26
|
+
- "possibly"
|
|
27
|
+
- "presumably"
|
|
28
|
+
- "probably"
|
|
29
|
+
- "seems to"
|
|
30
|
+
- "appears to"
|
|
31
|
+
wholeWord: true
|
|
32
|
+
caseSensitive: false
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConstraintFinding,
|
|
3
|
+
type ConstraintRule,
|
|
4
|
+
createFinding,
|
|
5
|
+
type RuleContext,
|
|
6
|
+
type RuleEvaluationResult,
|
|
7
|
+
type RuleEvaluator,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import { stringArray } from './config-access';
|
|
10
|
+
import { scanFiles } from './file-discovery';
|
|
11
|
+
|
|
12
|
+
/** Evaluator config shape extracted from `rule.evaluator.config`. */
|
|
13
|
+
interface HedgePhrasesConfig {
|
|
14
|
+
phrases?: unknown;
|
|
15
|
+
caseSensitive?: unknown;
|
|
16
|
+
wholeWord?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Flags uncertainty / hedge phrasing in the files a rule scopes in: a claim that
|
|
21
|
+
* hedges ("it may", "presumably") states no verifiable fact, so it reads as evidence
|
|
22
|
+
* while pinning nothing.
|
|
23
|
+
*
|
|
24
|
+
* Config (`evaluator.config`):
|
|
25
|
+
* - `phrases`: non-empty list of literal phrases to flag (required; a phrase is text,
|
|
26
|
+
* never a pattern)
|
|
27
|
+
* - `caseSensitive`: default `false` — "It May" is the same hedge as "it may"
|
|
28
|
+
* - `wholeWord`: default `true` — `it may` does not match inside `it maybe`
|
|
29
|
+
*
|
|
30
|
+
* One finding per file per phrase, carrying the first matching line, with the message
|
|
31
|
+
* `uncertainty phrases detected: <phrase>`. Scope comes from the rule's `include` /
|
|
32
|
+
* `exclude` (anchored globs), so the plane is the rule author's choice rather than a
|
|
33
|
+
* built-in assumption.
|
|
34
|
+
*/
|
|
35
|
+
export class HedgePhrasesEvaluator implements RuleEvaluator {
|
|
36
|
+
/**
|
|
37
|
+
* No collaborators: scope and filesystem come from the rule and the context. Explicit rather
|
|
38
|
+
* than implicit because Bun's coverage counts an implicit class constructor as uncovered, which
|
|
39
|
+
* would keep this file under the per-file function threshold for no real gap.
|
|
40
|
+
*/
|
|
41
|
+
constructor() {}
|
|
42
|
+
|
|
43
|
+
/** Scan the scoped files for each configured phrase. */
|
|
44
|
+
async evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult> {
|
|
45
|
+
const config = (rule.evaluator.config ?? {}) as HedgePhrasesConfig;
|
|
46
|
+
const phrases = stringArray(config.phrases) ?? [];
|
|
47
|
+
if (phrases.length === 0) {
|
|
48
|
+
throw new Error('hedge-phrases evaluator requires a non-empty "phrases" config');
|
|
49
|
+
}
|
|
50
|
+
const caseSensitive = config.caseSensitive === true;
|
|
51
|
+
const wholeWord = config.wholeWord !== false;
|
|
52
|
+
const matchers = phrases.map((phrase) => ({ phrase, pattern: toPattern(phrase, caseSensitive, wholeWord) }));
|
|
53
|
+
const files = await scanFiles({
|
|
54
|
+
workdir: context.workdir,
|
|
55
|
+
include: rule.include,
|
|
56
|
+
exclude: rule.exclude,
|
|
57
|
+
matchMode: 'glob',
|
|
58
|
+
...(context.fileSystem !== undefined ? { fs: context.fileSystem } : {}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const findings: ConstraintFinding[] = [];
|
|
62
|
+
for (const { file, content } of files) {
|
|
63
|
+
const lines = content.split('\n');
|
|
64
|
+
for (const { phrase, pattern } of matchers) {
|
|
65
|
+
const index = lines.findIndex((line) => pattern.test(line));
|
|
66
|
+
if (index < 0) continue;
|
|
67
|
+
findings.push(
|
|
68
|
+
createFinding(rule, `uncertainty phrases detected: ${phrase}`, file, {
|
|
69
|
+
line: index + 1,
|
|
70
|
+
code: 'hedge-phrases:matched',
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { findings, fixes: [] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compile one phrase as literal text, optionally whole-word and case-insensitive. The
|
|
81
|
+
* word boundary is applied only at an edge that is a word character, so a phrase like
|
|
82
|
+
* `(b) may?` still matches — a trailing `\\b` after `?` could never be satisfied.
|
|
83
|
+
*/
|
|
84
|
+
function toPattern(phrase: string, caseSensitive: boolean, wholeWord: boolean): RegExp {
|
|
85
|
+
const body = phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
|
+
const lead = wholeWord && /^\w/.test(body) ? '\\b' : '';
|
|
87
|
+
const tail = wholeWord && /\w$/.test(body) ? '\\b' : '';
|
|
88
|
+
return new RegExp(`${lead}${body}${tail}`, caseSensitive ? 'u' : 'iu');
|
|
89
|
+
}
|
package/src/host/builtins.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AgentDetectionEvaluator } from '../evaluators/agent-detection-evaluator
|
|
|
3
3
|
import { CoverageGateEvaluator } from '../evaluators/coverage-gate-evaluator';
|
|
4
4
|
import { ExitCodeEvaluator } from '../evaluators/exit-code-evaluator';
|
|
5
5
|
import { ForbiddenImportEvaluator } from '../evaluators/forbidden-import-evaluator';
|
|
6
|
+
import { HedgePhrasesEvaluator } from '../evaluators/hedge-phrases-evaluator';
|
|
6
7
|
import { ImportBoundaryEvaluator } from '../evaluators/import-boundary-evaluator';
|
|
7
8
|
import { PathEvaluator } from '../evaluators/path-evaluator';
|
|
8
9
|
import { RegexEvaluator } from '../evaluators/regex-evaluator';
|
|
@@ -45,6 +46,7 @@ export function registerBuiltins(host: RuleEngineHost, executor?: ProcessExecuto
|
|
|
45
46
|
host.evaluators.register('sg', new SgEvaluator(executor), 'builtin');
|
|
46
47
|
host.evaluators.register('schema-artifact', new SchemaArtifactEvaluator(), 'builtin');
|
|
47
48
|
host.evaluators.register('import-boundary', new ImportBoundaryEvaluator(), 'builtin');
|
|
49
|
+
host.evaluators.register('hedge-phrases', new HedgePhrasesEvaluator(), 'builtin');
|
|
48
50
|
host.formatters.register('text', new TextFormatter(), 'builtin');
|
|
49
51
|
host.formatters.register('json', new JsonFormatter(), 'builtin');
|
|
50
52
|
}
|