@gobing-ai/ts-rule-engine 0.5.29 → 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 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;AAsB7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAwBvF"}
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"}
@@ -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.29",
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.29",
55
- "@gobing-ai/ts-infra": "^0.5.29",
56
- "@gobing-ai/ts-runtime": "^0.5.29",
57
- "@gobing-ai/ts-db": "^0.5.29",
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
+ }
@@ -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
  }