@crewhaus/tool-text 0.7.0 → 0.8.1

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,7 +1,13 @@
1
+ import type { PatternAnswer } from "./regex";
1
2
  export declare class TemplateError extends Error {
2
3
  readonly name = "TemplateError";
3
4
  }
4
- /** Walk a dotted path (`user.name`, `items.0.id`) through plain data. */
5
+ /**
6
+ * Walk a dotted path (`user.name`, `items.0.id`) through plain data. Only
7
+ * the data's own fields are read: `{{constructor}}` on data with no such
8
+ * field is missing, where 0.7.0 rendered Object's constructor as
9
+ * "function Object() { [native code] }" and strict mode let it through.
10
+ */
5
11
  export declare function lookupPath(data: unknown, path: string): unknown;
6
12
  /**
7
13
  * Render `{{path}}` placeholders against `data`.
@@ -31,6 +37,16 @@ export type ClassifyResult = {
31
37
  readonly score: number;
32
38
  readonly matched: ReadonlyArray<string>;
33
39
  readonly scores: Readonly<Record<string, number>>;
40
+ /**
41
+ * Regex patterns with no answer (the worker's deadline, the engine giving
42
+ * up). When any is listed, `label` is null: a pattern that might have
43
+ * matched could change the winner, so neither the top label nor the
44
+ * default is given.
45
+ */
46
+ readonly undetermined?: ReadonlyArray<{
47
+ readonly pattern: string;
48
+ readonly reason: string;
49
+ }>;
34
50
  };
35
51
  /**
36
52
  * Score text against operator-written rules and return the best label.
@@ -38,5 +54,10 @@ export type ClassifyResult = {
38
54
  * Lexical only — phrase and regex matching with per-rule weights. No model
39
55
  * and no embedding, so the same message always gets the same label, which is
40
56
  * what makes it safe to route on.
57
+ *
58
+ * A regex rule's pattern is never run here: `regexAnswers` holds its answer
59
+ * on this text, worked out in the regex worker beforehand (the tool does
60
+ * that with `answerPatterns`). A pattern with no definite answer leaves the
61
+ * label undetermined.
41
62
  */
42
- export declare function classifyByRules(text: string, rules: ReadonlyArray<ClassifyRule>, threshold: number, defaultLabel: string | null): ClassifyResult;
63
+ export declare function classifyByRules(text: string, rules: ReadonlyArray<ClassifyRule>, threshold: number, defaultLabel: string | null, regexAnswers?: ReadonlyMap<string, PatternAnswer>): ClassifyResult;
@@ -1,7 +1,12 @@
1
1
  export class TemplateError extends Error {
2
2
  name = "TemplateError";
3
3
  }
4
- /** Walk a dotted path (`user.name`, `items.0.id`) through plain data. */
4
+ /**
5
+ * Walk a dotted path (`user.name`, `items.0.id`) through plain data. Only
6
+ * the data's own fields are read: `{{constructor}}` on data with no such
7
+ * field is missing, where 0.7.0 rendered Object's constructor as
8
+ * "function Object() { [native code] }" and strict mode let it through.
9
+ */
5
10
  export function lookupPath(data, path) {
6
11
  if (path === ".")
7
12
  return data;
@@ -16,7 +21,7 @@ export function lookupPath(data, path) {
16
21
  cur = cur[i];
17
22
  continue;
18
23
  }
19
- if (typeof cur !== "object")
24
+ if (typeof cur !== "object" || !Object.hasOwn(cur, part))
20
25
  return undefined;
21
26
  cur = cur[part];
22
27
  }
@@ -59,18 +64,38 @@ export function renderTemplateString(template, data, strict) {
59
64
  * Lexical only — phrase and regex matching with per-rule weights. No model
60
65
  * and no embedding, so the same message always gets the same label, which is
61
66
  * what makes it safe to route on.
67
+ *
68
+ * A regex rule's pattern is never run here: `regexAnswers` holds its answer
69
+ * on this text, worked out in the regex worker beforehand (the tool does
70
+ * that with `answerPatterns`). A pattern with no definite answer leaves the
71
+ * label undetermined.
62
72
  */
63
- export function classifyByRules(text, rules, threshold, defaultLabel) {
73
+ export function classifyByRules(text, rules, threshold, defaultLabel, regexAnswers = new Map()) {
64
74
  const haystack = text.toLowerCase();
65
- const scores = {};
75
+ // Labels are the caller's text: a label named `constructor` or
76
+ // `__proto__` is a label, never an Object.prototype member.
77
+ const scores = Object.create(null);
66
78
  const matched = [];
79
+ const undetermined = [];
67
80
  for (const rule of rules) {
68
81
  const weight = rule.weight ?? 1;
69
82
  let hits = 0;
70
83
  for (const pattern of rule.patterns) {
71
- const found = rule.regex === true
72
- ? new RegExp(pattern, "i").test(text)
73
- : haystack.includes(pattern.toLowerCase());
84
+ let found;
85
+ if (rule.regex === true) {
86
+ const answer = regexAnswers.get(pattern);
87
+ if (typeof answer !== "boolean") {
88
+ undetermined.push({
89
+ pattern,
90
+ reason: answer?.undetermined ?? "the pattern was not answered before classifying",
91
+ });
92
+ continue;
93
+ }
94
+ found = answer;
95
+ }
96
+ else {
97
+ found = haystack.includes(pattern.toLowerCase());
98
+ }
74
99
  if (found) {
75
100
  hits += 1;
76
101
  matched.push(pattern);
@@ -79,6 +104,10 @@ export function classifyByRules(text, rules, threshold, defaultLabel) {
79
104
  if (hits > 0)
80
105
  scores[rule.label] = (scores[rule.label] ?? 0) + hits * weight;
81
106
  }
107
+ if (undetermined.length > 0) {
108
+ const best = Math.max(0, ...Object.values(scores));
109
+ return { label: null, score: best, matched, scores, undetermined };
110
+ }
82
111
  const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
83
112
  const top = ranked[0];
84
113
  if (top === undefined || top[1] < threshold) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/tool-text",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "Deterministic text tools: regex extraction, diffing, unified-diff parsing, budget truncation, log compaction, outlining, templating, rule classification, normalization, similarity, and escaping",
6
6
  "main": "dist/index.js",
@@ -8,16 +8,18 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
12
13
  }
13
14
  },
14
15
  "scripts": {
15
16
  "test": "bun test src"
16
17
  },
17
18
  "dependencies": {
18
- "@crewhaus/errors": "0.7.0",
19
- "@crewhaus/tool-builder": "0.7.0",
20
- "@crewhaus/tool-catalog": "0.7.0",
19
+ "@crewhaus/errors": "0.8.1",
20
+ "@crewhaus/tool-builder": "0.8.1",
21
+ "@crewhaus/tool-catalog": "0.8.1",
22
+ "@crewhaus/tool-safety": "0.8.1",
21
23
  "zod": "^3.23.8"
22
24
  },
23
25
  "devDependencies": {
@@ -41,5 +43,8 @@
41
43
  "publishConfig": {
42
44
  "access": "public"
43
45
  },
44
- "files": ["dist", "README.md", "LICENSE", "NOTICE"]
46
+ "files": ["dist", "README.md", "LICENSE", "NOTICE"],
47
+ "engines": {
48
+ "bun": ">=1.3.14"
49
+ }
45
50
  }