@factbag/jev 0.0.0-alpha.5 → 0.0.0-alpha.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @factbag/jev
2
2
 
3
+ ## 0.0.0-alpha.6
4
+
5
+ ### Minor Changes
6
+
7
+ - 6433c6b: Classify open-ended `String`, `Number`, and `Date` fields, and entity fields with no registered entities, over the values each passage offers. Pass `createChunkSymbolExtractor` to `classifier(...)` from `@factbag/jev` or `@factbag/clef` to build an extractor per chunk whose `query` answers candidate values for the open fields of the predicates it classifies. Both packages export the `ChunkSymbolExtractor`, `CreateChunkSymbolExtractor`, and `SymbolAnswer` types.
8
+
9
+ Extracted values are candidates only. An extracted entity is registered in `run.kb.entities` when asserted or denied evidence mentions it; entities the model does not admit evidence for are discarded.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [7225a51]
14
+ - Updated dependencies [7f26b45]
15
+ - Updated dependencies [b7c85ff]
16
+ - Updated dependencies [ed6ec84]
17
+ - Updated dependencies [5114b2f]
18
+ - Updated dependencies [15391f2]
19
+ - Updated dependencies [eab3679]
20
+ - Updated dependencies [ecbd8d9]
21
+ - Updated dependencies [e4b343e]
22
+ - factbag@0.0.0-alpha.6
23
+
3
24
  ## 0.0.0-alpha.5
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -102,14 +102,14 @@ field that cannot be classified. A nonempty metadata description is required.
102
102
 
103
103
  Supported field domains:
104
104
 
105
- | Schema field | Candidate values |
106
- | -------------------------- | ---------------------------------------- |
107
- | `Boolean` | `false`, `true` |
108
- | `enumeration(...)` | Explicit string, number, or Date members |
109
- | Entity constructor | Instances registered in `kb.entities` |
110
- | `String`, `Number`, `Date` | Unsupported open-ended domains |
111
-
112
- An entity domain must contain registered instances. Existing scalar facts do not
105
+ | Schema field | Candidate values |
106
+ | -------------------------- | ---------------------------------------------------- |
107
+ | `Boolean` | `false`, `true` |
108
+ | `enumeration(...)` | Explicit string, number, or Date members |
109
+ | Entity constructor | Registered instances, plus each chunk's extracted |
110
+ | `String`, `Number`, `Date` | Each chunk's extracted values; unsupported otherwise |
111
+
112
+ Without `createChunkSymbolExtractor`, an entity domain must contain registered instances. Existing scalar facts do not
113
113
  turn an open-ended schema into an enum. To model a fixed value, declare it explicitly
114
114
  with `enumeration(value)` rather than asking the classifier to discover it.
115
115
 
@@ -132,6 +132,49 @@ proposition. Every candidate gets an independent `Choice(ASSERT, DENY, IRRELEVAN
132
132
  question. Assert and deny permissions for the same predicate share one question.
133
133
  No enum exclusivity or Boolean equivalence is assumed; those belong to the ruleset.
134
134
 
135
+ ### Passage symbols
136
+
137
+ Pass `createChunkSymbolExtractor` to fill open-ended and entity fields from the text
138
+ being classified. The classifier builds one extractor per job over that job's chunk,
139
+ and asks it for the open fields of the predicates it classifies:
140
+
141
+ ```ts
142
+ const run = classifier({
143
+ corpus,
144
+ input: access,
145
+ createChunkSymbolExtractor: (chunks) => {
146
+ const found = scan(chunks);
147
+ return {
148
+ query: (requests) =>
149
+ new Map(
150
+ [...requests].map(([predicate, fields]) => [
151
+ predicate,
152
+ Object.fromEntries(fields.map((name) => [name, found.for(predicate, name)])),
153
+ ]),
154
+ ),
155
+ };
156
+ },
157
+ });
158
+ ```
159
+
160
+ - Open fields are `String`, `Number`, `Date`, and entity fields. Boolean and
161
+ enumeration fields keep their declared domains.
162
+ - `query` receives a map from each predicate to its open field names, and answers with
163
+ a map from each predicate to the values for each field, possibly asynchronously. How
164
+ it finds them, and what it caches, is up to the extractor.
165
+ - The answer must cover exactly the requested predicates and fields, with values each
166
+ field can hold: strings, finite numbers, valid dates, or entities of the field's
167
+ type. Otherwise the job rejects before anything is classified.
168
+ - Scalar fields take exactly the values answered. Entity fields take them alongside
169
+ their registered instances. Extracted entities are only candidates: those that
170
+ asserted or denied evidence mentions are registered in `run.kb.entities`, and the
171
+ rest are discarded.
172
+ - A predicate with a field left empty is skipped for that chunk.
173
+
174
+ An entity's type and `id` are its identity, so the extractor decides which mentions
175
+ are the same entity by the ids it mints. `new Person("Alice")` from two chunks is one
176
+ entity.
177
+
135
178
  ## Jobs and execution
136
179
 
137
180
  `run` is a single-cursor `IterableIterator`. Each yielded job classifies one chunk
@@ -1,5 +1,6 @@
1
1
  import { GuardedKB, KnowledgeBase } from "factbag/kb";
2
- import { Corpus, Metadata } from "factbag/corpus";
2
+ import { Chunk, Corpus, Metadata } from "factbag/corpus";
3
+ import { Entity, PredicateDeclaration, Scalar } from "factbag/dsl";
3
4
  import { TypeSafeClient } from "@typesafe-ai/sdk";
4
5
  /** Default minimum probability for admitting signed evidence. @beta */
5
6
  export declare const DEFAULT_MINIMUM_PROBABILITY = .4;
@@ -32,6 +33,25 @@ type Settings = {
32
33
  };
33
34
  /** Settings with every default applied. @beta */
34
35
  type Normalized = Readonly<Required<Settings>>;
36
+ /**
37
+ * Candidate values for each requested field, by predicate and then by field name. Open
38
+ * fields are `String`, `Number`, `Date`, and entity fields. The same value may be offered
39
+ * to any number of fields.
40
+ * @beta
41
+ */
42
+ type SymbolAnswer = ReadonlyMap<PredicateDeclaration, Readonly<Record<string, readonly (Scalar | Entity)[]>>>;
43
+ /**
44
+ * Candidate symbols found in a fixed set of chunks. How and when it reads them, and what
45
+ * it caches between queries, is up to the implementation. An entity's type and id are its
46
+ * identity, so the ids it mints decide which mentions are the same entity.
47
+ * @beta
48
+ */
49
+ type ChunkSymbolExtractor = {
50
+ /** Candidate values for exactly the requested fields of each requested predicate. */
51
+ readonly query: (requests: ReadonlyMap<PredicateDeclaration, readonly string[]>) => SymbolAnswer | Promise<SymbolAnswer>;
52
+ };
53
+ /** Build an extractor over the chunks it will be queried about. @beta */
54
+ type CreateChunkSymbolExtractor = (chunks: readonly Chunk[]) => ChunkSymbolExtractor;
35
55
  /** One opaque unit of work. Its effects are written to the run’s knowledge base. @beta */
36
56
  type Job = {
37
57
  readonly execute: () => Promise<void>;
@@ -57,12 +77,15 @@ export declare function createConfig(config?: Config): Normalized & Config;
57
77
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
58
78
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
59
79
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
80
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
81
+ * each chunk offers, so they need no finite domain up front.
60
82
  * @beta
61
83
  */
62
84
  export declare function classifier<M extends Metadata = Metadata>(input: {
63
85
  corpus: Corpus<M>;
64
86
  input: KnowledgeBase | GuardedKB;
65
87
  config?: Config;
88
+ createChunkSymbolExtractor?: CreateChunkSymbolExtractor;
66
89
  }): Run;
67
90
  /**
68
91
  * Settle contradictions in a knowledge base by reading each one's evidence together.
@@ -76,4 +99,4 @@ export declare function classifier<M extends Metadata = Metadata>(input: {
76
99
  * @beta
77
100
  */
78
101
  export declare function resolver<M extends Metadata = Metadata>(corpus: Corpus<M>, kb: KnowledgeBase, config?: Config): Run;
79
- export type { Job, Normalized, Run, Settings };
102
+ export type { ChunkSymbolExtractor, CreateChunkSymbolExtractor, Job, Normalized, Run, Settings, SymbolAnswer };
@@ -11,7 +11,9 @@ import { TypeSafeClient } from "@typesafe-ai/sdk";
11
11
  * Query-only predicates are excluded.
12
12
  *
13
13
  * Called once when constructing a run, so unsupported predicates fail before reading
14
- * the corpus or calling Jev. Candidate combinations are built later, after filtering.
14
+ * the corpus or calling Jev. Each field starts with the values known before any passage
15
+ * is read: open-ended fields, and entity fields with no registered entities, start empty.
16
+ * Candidate combinations are built later, after filtering.
15
17
  */
16
18
  function classifiablePredicates(access) {
17
19
  const actions = /* @__PURE__ */ new Map();
@@ -30,10 +32,12 @@ function classifiablePredicates(access) {
30
32
  };
31
33
  });
32
34
  }
33
- /** Validate a finite field domain and collect its values. */
35
+ /** Validate a field's type and collect the values known before reading any passage. */
34
36
  function fieldValues({ predicate, name, kb }) {
35
37
  const type = predicate.params[name];
36
- if (type === String || type === Number || type === Date || type === void 0) throw new UnsupportedPredicateError(`Cannot classify predicate "${predicate.name}": field "${name}" has an unsupported open-ended domain. Use Boolean, enumeration(...), or registered entities.`, predicate, name);
38
+ if (type === void 0) throw new UnsupportedPredicateError(`Invalid schema for ${predicate.name}: field "${name}" has no type`, predicate, name);
39
+ /** Open-ended fields take values only from passages. */
40
+ if (type === String || type === Number || type === Date) return [];
37
41
  if (type === Boolean) return [false, true];
38
42
  if (typeof type === "object") {
39
43
  const kind = type.values[0] instanceof Date ? "date" : typeof type.values[0];
@@ -41,14 +45,57 @@ function fieldValues({ predicate, name, kb }) {
41
45
  if (type.values.length === 0 || kind !== "string" && kind !== "number" && kind !== "date" || type.values.some((value) => (value instanceof Date ? "date" : typeof value) !== kind) || keys.some((key) => typeof key === "number" && Number.isNaN(key)) || new Set(keys).size !== type.values.length) throw new UnsupportedPredicateError(`Invalid enum domain for ${predicate.name}.${name}`, predicate, name);
42
46
  return [...type.values];
43
47
  }
44
- const ids = kb.entities.ids(type);
45
- if (ids.length === 0) throw new UnsupportedPredicateError(`No registered entities for ${predicate.name}.${name}`, predicate, name);
46
- return ids.map((id) => {
48
+ return kb.entities.ids(type).map((id) => {
47
49
  const entity = kb.entities.get(type, id);
48
50
  if (entity === void 0) throw new Error("A registered entity domain was modified");
49
51
  return entity;
50
52
  });
51
53
  }
54
+ // ../../packages/system-one/src/utils.ts
55
+ /** Snapshot JSON data, including nested dates and entities identified by a string id. */
56
+ function encode(value) {
57
+ const entities = /* @__PURE__ */ new WeakMap();
58
+ const json = JSON.stringify(value, (_, item) => {
59
+ if (!isRecord(item) || typeof item["id"] !== "string") return item;
60
+ let entity = entities.get(item);
61
+ if (entity === void 0) {
62
+ entity = {
63
+ ...item,
64
+ type: typeof item.constructor === "function" ? item.constructor.name : "Object"
65
+ };
66
+ entities.set(item, entity);
67
+ }
68
+ return entity;
69
+ });
70
+ if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
71
+ return JSON.parse(json);
72
+ }
73
+ /** A compact diagnostic label, using entity ids and the predicate's declared field order. */
74
+ function formatAtom({ predicate, fields }) {
75
+ return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
76
+ }
77
+ function format(value) {
78
+ if (value instanceof Date) return value.toISOString();
79
+ return isRecord(value) && "id" in value ? String(value["id"]) : String(value);
80
+ }
81
+ /**
82
+ * Fail fast when an invariant breaks: a bug in Factbag, never bad input from the caller,
83
+ * which gets a descriptive error of its own.
84
+ */
85
+ function assert(condition, message) {
86
+ if (!condition) throw new Error(`Assertion failed: ${message}`);
87
+ }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null && !Array.isArray(value);
90
+ }
91
+ /** An entity value: an object with a string id, other than a Date. */
92
+ function isEntity(value) {
93
+ return typeof value === "object" && value !== null && !(value instanceof Date) && "id" in value && typeof value.id === "string";
94
+ }
95
+ /** A finite probability, including both endpoints. */
96
+ function isProbability(value) {
97
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
98
+ }
52
99
  // ../../packages/system-one/src/classifier/extract-evidence.ts
53
100
  /**
54
101
  * Assess the retained propositions against one passage and add admitted evidence to
@@ -68,6 +115,12 @@ async function extractEvidence({ chunk, predicates, symbols, config, kb }) {
68
115
  })))).flat();
69
116
  for (const { sign, atom, metadata } of evidence) if (sign === "+") kb.asserts(atom, metadata);
70
117
  else kb.denies(atom, metadata);
118
+ /**
119
+ * Register the entities that written evidence mentions, so later readers of the KB can
120
+ * enumerate them. Extracted candidates that no evidence mentions stay unregistered.
121
+ * Identity is type and id, so an entity from several chunks is registered once.
122
+ */
123
+ kb.entities.add(...evidence.flatMap(({ atom }) => Object.values(atom.fields)).filter((value) => isEntity(value)));
71
124
  }
72
125
  /**
73
126
  * Enumerate one predicate's candidate facts, have the model judge each against the passage,
@@ -124,6 +177,59 @@ function enumerateCandidateFacts({ predicate, symbols }) {
124
177
  fields: values
125
178
  }));
126
179
  }
180
+ // ../../packages/system-one/src/classifier/extract-symbols.ts
181
+ /**
182
+ * Widen each retained predicate's open fields with the values this chunk offers. This is
183
+ * the second pipeline step, after predicate filtering, so only fields of predicates the
184
+ * passage may bear on are queried. Boolean and enumeration fields keep their declared
185
+ * domains; entity fields keep their registered entities. Extracted entities are only
186
+ * candidates: nothing is registered here. Fields may still be empty afterwards.
187
+ */
188
+ async function extractSymbols({ chunk, predicates, createExtractor }) {
189
+ const requests = new Map(predicates.map(({ declaration }) => [declaration, openFields(declaration)]).filter(([, fields]) => fields.length > 0));
190
+ /** Nothing to ask builds no extractor. */
191
+ if (createExtractor === void 0 || requests.size === 0) return predicates;
192
+ const answer = await createExtractor([chunk]).query(requests);
193
+ assertAnswers(requests, answer);
194
+ return predicates.map((predicate) => ({
195
+ ...predicate,
196
+ roles: Object.fromEntries(Object.entries(predicate.roles).map(([name, domain]) => [name, unique([...domain, ...answer.get(predicate.declaration)?.[name] ?? []])]))
197
+ }));
198
+ }
199
+ /** The fields whose values come from the passage: every field but Boolean and enumeration ones. */
200
+ function openFields(predicate) {
201
+ return predicate.paramNames.filter((name) => {
202
+ const type = predicate.params[name];
203
+ return typeof type === "function" && type !== Boolean;
204
+ });
205
+ }
206
+ /**
207
+ * The answer must cover exactly the requested predicates and fields, with values each
208
+ * field can hold, so an extractor bug fails the job before anything is asked.
209
+ */
210
+ function assertAnswers(requests, answer) {
211
+ for (const predicate of answer.keys()) if (!requests.has(predicate)) throw new TypeError(`Extracted symbols answer unrequested predicate "${predicate.name}"`);
212
+ for (const [predicate, fields] of requests) {
213
+ const values = answer.get(predicate);
214
+ if (values === void 0) throw new TypeError(`Extracted symbols omit requested predicate "${predicate.name}"`);
215
+ if (Object.keys(values).length !== fields.length || !fields.every((name) => Object.hasOwn(values, name))) throw new TypeError(`Extracted symbols for "${predicate.name}" must answer exactly its fields: ${fields.join(", ")}`);
216
+ for (const name of fields) {
217
+ const type = predicate.params[name];
218
+ if (!(values[name] ?? []).every((value) => holds(type, value))) throw new TypeError(`Extracted symbols for ${predicate.name}.${name} must be valid values of its type`);
219
+ }
220
+ }
221
+ }
222
+ /** Whether a field of this type can hold the value: a finite number, valid date, or own entity. */
223
+ function holds(type, value) {
224
+ if (type === String) return typeof value === "string";
225
+ if (type === Number) return typeof value === "number" && Number.isFinite(value);
226
+ if (type === Date) return value instanceof Date && !Number.isNaN(value.getTime());
227
+ return isEntity(value) && value.constructor === type;
228
+ }
229
+ /** Drop structural duplicates, keeping the first occurrence. */
230
+ function unique(values) {
231
+ return values.filter((value, index) => values.findIndex((other) => fieldEquals(value, other)) === index);
232
+ }
127
233
  // ../../packages/system-one/src/classifier/filter-predicates.ts
128
234
  /** Retain the predicates relevant to this passage. This is the first pipeline step. */
129
235
  async function filterPredicates({ chunk, predicates, config }) {
@@ -162,11 +268,15 @@ function needsFiltering(predicates, config) {
162
268
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
163
269
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
164
270
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
271
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
272
+ * each chunk offers, so they need no finite domain up front. Without one, a predicate
273
+ * with a field that has no values fails here.
165
274
  */
166
- function classifier$1({ corpus, input, config }) {
275
+ function classifier$1({ corpus, input, config, createChunkSymbolExtractor }) {
167
276
  const baselineKB = (input instanceof GuardedKB ? input : guard(input).permit("*")).fork();
168
277
  const outputKB = baselineKB.kb.fork();
169
278
  const predicates = classifiablePredicates(baselineKB);
279
+ if (createChunkSymbolExtractor === void 0) assertAskable(predicates);
170
280
  const chunker = chunkers.recursive({
171
281
  size: 8e3,
172
282
  overlap: 200
@@ -174,16 +284,21 @@ function classifier$1({ corpus, input, config }) {
174
284
  const jobs = corpus.documents().values().filter(() => predicates.length > 0).flatMap((document) => corpus.chunk(document, chunker)).map((chunk) => ({ execute: async () => await processChunk({
175
285
  chunk,
176
286
  predicates,
287
+ createExtractor: createChunkSymbolExtractor,
177
288
  config,
178
289
  kb: outputKB
179
290
  }) }));
180
291
  return Object.assign(jobs, { kb: outputKB });
181
292
  }
182
- async function processChunk({ chunk, predicates: inputPredicates, config, kb }) {
183
- const predicates = await filterPredicates({
293
+ async function processChunk({ chunk, predicates: inputPredicates, createExtractor, config, kb }) {
294
+ const predicates = await extractSymbols({
184
295
  chunk,
185
- predicates: inputPredicates,
186
- config
296
+ predicates: await filterPredicates({
297
+ chunk,
298
+ predicates: inputPredicates,
299
+ config
300
+ }),
301
+ createExtractor
187
302
  });
188
303
  await extractEvidence({
189
304
  chunk,
@@ -197,39 +312,25 @@ async function processChunk({ chunk, predicates: inputPredicates, config, kb })
197
312
  kb
198
313
  });
199
314
  }
200
- // ../../packages/system-one/src/utils.ts
201
- /** Snapshot JSON data, including nested dates and entities identified by a string id. */
202
- function encode(value) {
203
- const entities = /* @__PURE__ */ new WeakMap();
204
- const json = JSON.stringify(value, (_, item) => {
205
- if (!isRecord(item) || typeof item["id"] !== "string") return item;
206
- let entity = entities.get(item);
207
- if (entity === void 0) {
208
- entity = {
209
- ...item,
210
- type: typeof item.constructor === "function" ? item.constructor.name : "Object"
211
- };
212
- entities.set(item, entity);
213
- }
214
- return entity;
215
- });
216
- if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
217
- return JSON.parse(json);
218
- }
219
- /** A compact diagnostic label, using entity ids and the predicate's declared field order. */
220
- function formatAtom({ predicate, fields }) {
221
- return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
222
- }
223
- function format(value) {
224
- if (value instanceof Date) return value.toISOString();
225
- return isRecord(value) && "id" in value ? String(value["id"]) : String(value);
226
- }
227
- function isRecord(value) {
228
- return typeof value === "object" && value !== null && !Array.isArray(value);
315
+ /**
316
+ * Throw for the first predicate with a field that has no values. Without a symbol
317
+ * extractor, fields hold only the values known up front, so such a predicate could never
318
+ * be asked about.
319
+ */
320
+ function assertAskable(predicates) {
321
+ for (const { declaration, roles } of predicates) {
322
+ const name = emptyField({
323
+ declaration,
324
+ roles
325
+ });
326
+ if (name === void 0) continue;
327
+ const type = declaration.params[name];
328
+ throw new UnsupportedPredicateError(type === String || type === Number || type === Date ? `Cannot classify predicate "${declaration.name}": field "${name}" has an unsupported open-ended domain. Use Boolean, enumeration(...), registered entities, or a symbol extractor.` : `No registered entities for ${declaration.name}.${name}`, declaration, name);
329
+ }
229
330
  }
230
- /** A finite probability, including both endpoints. */
231
- function isProbability(value) {
232
- return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
331
+ /** The first field, in declared order, with no values. */
332
+ function emptyField({ declaration, roles }) {
333
+ return declaration.paramNames.find((name) => (roles[name] ?? []).length === 0);
233
334
  }
234
335
  // ../../packages/system-one/src/config.ts
235
336
  /** Default minimum probability for admitting signed evidence. @beta */
@@ -412,7 +513,7 @@ function systemOneModel(endpoint) {
412
513
  judgement: choice(response, OUTCOMES, id, name)
413
514
  })
414
515
  })),
415
- adjudicate: async ({ atom, passages }) => await ask({
516
+ adjudicate: async ({ atom, passages }) => passages.length === 0 ? [] : await ask({
416
517
  endpoint,
417
518
  prefix: "passage",
418
519
  items: passages,
@@ -434,10 +535,12 @@ async function inBatches(items, size, request) {
434
535
  * Ask one question per item in a single request and turn each answer into a result.
435
536
  * Question keys are positional, so they are minted and matched only here. Validation
436
537
  * happens while building results, so one bad answer rejects the whole request.
437
- * Nothing to ask sends nothing; more than the provider allows sends nothing and throws.
538
+ * Every request holds at least one question: callers send nothing when there is nothing
539
+ * to ask, so an empty request is a bug. More than the provider allows sends nothing and
540
+ * throws.
438
541
  */
439
542
  async function ask({ endpoint: { name, maxBatchSize, transport }, prefix, items, state, question, answer }) {
440
- if (items.length === 0) return [];
543
+ assert(items.length > 0, `Asked ${name} an empty ${prefix} request`);
441
544
  if (items.length > maxBatchSize) throw new RangeError(`Cannot ask ${items.length} ${prefix} questions in one request: ${name} accepts at most ${maxBatchSize}`);
442
545
  const keyed = items.map((item, index) => ({
443
546
  id: `${prefix}_${index}`,
@@ -584,10 +687,11 @@ function systemOne(provider) {
584
687
  pipelines.set(normalized, resolved);
585
688
  return normalized;
586
689
  },
587
- classifier: ({ corpus, input, config }) => classifier$1({
690
+ classifier: ({ corpus, input, config, createChunkSymbolExtractor }) => classifier$1({
588
691
  corpus,
589
692
  input,
590
- config: pipeline(config)
693
+ config: pipeline(config),
694
+ createChunkSymbolExtractor
591
695
  }),
592
696
  resolver: (corpus, kb, config) => resolver$1(corpus, kb, pipeline(config))
593
697
  };
@@ -613,6 +717,8 @@ function createConfig(config) {
613
717
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
614
718
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
615
719
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
720
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
721
+ * each chunk offers, so they need no finite domain up front.
616
722
  * @beta
617
723
  */
618
724
  function classifier(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factbag/jev",
3
- "version": "0.0.0-alpha.5",
3
+ "version": "0.0.0-alpha.6",
4
4
  "description": "Lazy AI-assisted classification and evidence resolution for Factbag.",
5
5
  "keywords": [
6
6
  "classification",
@@ -89,7 +89,7 @@
89
89
  "vitest": "5.0.5"
90
90
  },
91
91
  "peerDependencies": {
92
- "factbag": "0.0.0-alpha.5",
92
+ "factbag": "0.0.0-alpha.6",
93
93
  "@typesafe-ai/sdk": "^0.7.0"
94
94
  },
95
95
  "scripts": {