@factbag/clef 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/clef
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
@@ -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
  /** Default minimum probability for admitting signed evidence. @beta */
4
5
  export declare const DEFAULT_MINIMUM_PROBABILITY = .4;
5
6
  /** Default maximum probability gap from the strongest signed outcome. @beta */
@@ -31,6 +32,25 @@ type Settings = {
31
32
  };
32
33
  /** Settings with every default applied. @beta */
33
34
  type Normalized = Readonly<Required<Settings>>;
35
+ /**
36
+ * Candidate values for each requested field, by predicate and then by field name. Open
37
+ * fields are `String`, `Number`, `Date`, and entity fields. The same value may be offered
38
+ * to any number of fields.
39
+ * @beta
40
+ */
41
+ type SymbolAnswer = ReadonlyMap<PredicateDeclaration, Readonly<Record<string, readonly (Scalar | Entity)[]>>>;
42
+ /**
43
+ * Candidate symbols found in a fixed set of chunks. How and when it reads them, and what
44
+ * it caches between queries, is up to the implementation. An entity's type and id are its
45
+ * identity, so the ids it mints decide which mentions are the same entity.
46
+ * @beta
47
+ */
48
+ type ChunkSymbolExtractor = {
49
+ /** Candidate values for exactly the requested fields of each requested predicate. */
50
+ readonly query: (requests: ReadonlyMap<PredicateDeclaration, readonly string[]>) => SymbolAnswer | Promise<SymbolAnswer>;
51
+ };
52
+ /** Build an extractor over the chunks it will be queried about. @beta */
53
+ type CreateChunkSymbolExtractor = (chunks: readonly Chunk[]) => ChunkSymbolExtractor;
34
54
  /** One opaque unit of work. Its effects are written to the run’s knowledge base. @beta */
35
55
  type Job = {
36
56
  readonly execute: () => Promise<void>;
@@ -66,12 +86,15 @@ export declare function createConfig(config?: Config): Normalized & Config;
66
86
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
67
87
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
68
88
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
89
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
90
+ * each chunk offers, so they need no finite domain up front.
69
91
  * @beta
70
92
  */
71
93
  export declare function classifier<M extends Metadata = Metadata>(input: {
72
94
  corpus: Corpus<M>;
73
95
  input: KnowledgeBase | GuardedKB;
74
96
  config?: Config;
97
+ createChunkSymbolExtractor?: CreateChunkSymbolExtractor;
75
98
  }): Run;
76
99
  /**
77
100
  * Settle contradictions in a knowledge base by reading each one's evidence together.
@@ -87,4 +110,4 @@ export declare function classifier<M extends Metadata = Metadata>(input: {
87
110
  * @beta
88
111
  */
89
112
  export declare function resolver<M extends Metadata = Metadata>(corpus: Corpus<M>, kb: KnowledgeBase, config?: Config): Run;
90
- export type { Config, Job, Normalized, Run, Settings };
113
+ export type { ChunkSymbolExtractor, Config, CreateChunkSymbolExtractor, Job, Normalized, Run, Settings, SymbolAnswer };
@@ -10,7 +10,9 @@ import { fieldEquals } from "factbag/dsl";
10
10
  * Query-only predicates are excluded.
11
11
  *
12
12
  * Called once when constructing a run, so unsupported predicates fail before reading
13
- * the corpus or calling Jev. Candidate combinations are built later, after filtering.
13
+ * the corpus or calling Jev. Each field starts with the values known before any passage
14
+ * is read: open-ended fields, and entity fields with no registered entities, start empty.
15
+ * Candidate combinations are built later, after filtering.
14
16
  */
15
17
  function classifiablePredicates(access) {
16
18
  const actions = /* @__PURE__ */ new Map();
@@ -29,10 +31,12 @@ function classifiablePredicates(access) {
29
31
  };
30
32
  });
31
33
  }
32
- /** Validate a finite field domain and collect its values. */
34
+ /** Validate a field's type and collect the values known before reading any passage. */
33
35
  function fieldValues({ predicate, name, kb }) {
34
36
  const type = predicate.params[name];
35
- 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);
37
+ if (type === void 0) throw new UnsupportedPredicateError(`Invalid schema for ${predicate.name}: field "${name}" has no type`, predicate, name);
38
+ /** Open-ended fields take values only from passages. */
39
+ if (type === String || type === Number || type === Date) return [];
36
40
  if (type === Boolean) return [false, true];
37
41
  if (typeof type === "object") {
38
42
  const kind = type.values[0] instanceof Date ? "date" : typeof type.values[0];
@@ -40,14 +44,57 @@ function fieldValues({ predicate, name, kb }) {
40
44
  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);
41
45
  return [...type.values];
42
46
  }
43
- const ids = kb.entities.ids(type);
44
- if (ids.length === 0) throw new UnsupportedPredicateError(`No registered entities for ${predicate.name}.${name}`, predicate, name);
45
- return ids.map((id) => {
47
+ return kb.entities.ids(type).map((id) => {
46
48
  const entity = kb.entities.get(type, id);
47
49
  if (entity === void 0) throw new Error("A registered entity domain was modified");
48
50
  return entity;
49
51
  });
50
52
  }
53
+ // ../../packages/system-one/src/utils.ts
54
+ /** Snapshot JSON data, including nested dates and entities identified by a string id. */
55
+ function encode(value) {
56
+ const entities = /* @__PURE__ */ new WeakMap();
57
+ const json = JSON.stringify(value, (_, item) => {
58
+ if (!isRecord$1(item) || typeof item["id"] !== "string") return item;
59
+ let entity = entities.get(item);
60
+ if (entity === void 0) {
61
+ entity = {
62
+ ...item,
63
+ type: typeof item.constructor === "function" ? item.constructor.name : "Object"
64
+ };
65
+ entities.set(item, entity);
66
+ }
67
+ return entity;
68
+ });
69
+ if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
70
+ return JSON.parse(json);
71
+ }
72
+ /** A compact diagnostic label, using entity ids and the predicate's declared field order. */
73
+ function formatAtom({ predicate, fields }) {
74
+ return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
75
+ }
76
+ function format(value) {
77
+ if (value instanceof Date) return value.toISOString();
78
+ return isRecord$1(value) && "id" in value ? String(value["id"]) : String(value);
79
+ }
80
+ /**
81
+ * Fail fast when an invariant breaks: a bug in Factbag, never bad input from the caller,
82
+ * which gets a descriptive error of its own.
83
+ */
84
+ function assert(condition, message) {
85
+ if (!condition) throw new Error(`Assertion failed: ${message}`);
86
+ }
87
+ function isRecord$1(value) {
88
+ return typeof value === "object" && value !== null && !Array.isArray(value);
89
+ }
90
+ /** An entity value: an object with a string id, other than a Date. */
91
+ function isEntity(value) {
92
+ return typeof value === "object" && value !== null && !(value instanceof Date) && "id" in value && typeof value.id === "string";
93
+ }
94
+ /** A finite probability, including both endpoints. */
95
+ function isProbability(value) {
96
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
97
+ }
51
98
  // ../../packages/system-one/src/classifier/extract-evidence.ts
52
99
  /**
53
100
  * Assess the retained propositions against one passage and add admitted evidence to
@@ -67,6 +114,12 @@ async function extractEvidence({ chunk, predicates, symbols, config, kb }) {
67
114
  })))).flat();
68
115
  for (const { sign, atom, metadata } of evidence) if (sign === "+") kb.asserts(atom, metadata);
69
116
  else kb.denies(atom, metadata);
117
+ /**
118
+ * Register the entities that written evidence mentions, so later readers of the KB can
119
+ * enumerate them. Extracted candidates that no evidence mentions stay unregistered.
120
+ * Identity is type and id, so an entity from several chunks is registered once.
121
+ */
122
+ kb.entities.add(...evidence.flatMap(({ atom }) => Object.values(atom.fields)).filter((value) => isEntity(value)));
70
123
  }
71
124
  /**
72
125
  * Enumerate one predicate's candidate facts, have the model judge each against the passage,
@@ -123,6 +176,59 @@ function enumerateCandidateFacts({ predicate, symbols }) {
123
176
  fields: values
124
177
  }));
125
178
  }
179
+ // ../../packages/system-one/src/classifier/extract-symbols.ts
180
+ /**
181
+ * Widen each retained predicate's open fields with the values this chunk offers. This is
182
+ * the second pipeline step, after predicate filtering, so only fields of predicates the
183
+ * passage may bear on are queried. Boolean and enumeration fields keep their declared
184
+ * domains; entity fields keep their registered entities. Extracted entities are only
185
+ * candidates: nothing is registered here. Fields may still be empty afterwards.
186
+ */
187
+ async function extractSymbols({ chunk, predicates, createExtractor }) {
188
+ const requests = new Map(predicates.map(({ declaration }) => [declaration, openFields(declaration)]).filter(([, fields]) => fields.length > 0));
189
+ /** Nothing to ask builds no extractor. */
190
+ if (createExtractor === void 0 || requests.size === 0) return predicates;
191
+ const answer = await createExtractor([chunk]).query(requests);
192
+ assertAnswers(requests, answer);
193
+ return predicates.map((predicate) => ({
194
+ ...predicate,
195
+ roles: Object.fromEntries(Object.entries(predicate.roles).map(([name, domain]) => [name, unique([...domain, ...answer.get(predicate.declaration)?.[name] ?? []])]))
196
+ }));
197
+ }
198
+ /** The fields whose values come from the passage: every field but Boolean and enumeration ones. */
199
+ function openFields(predicate) {
200
+ return predicate.paramNames.filter((name) => {
201
+ const type = predicate.params[name];
202
+ return typeof type === "function" && type !== Boolean;
203
+ });
204
+ }
205
+ /**
206
+ * The answer must cover exactly the requested predicates and fields, with values each
207
+ * field can hold, so an extractor bug fails the job before anything is asked.
208
+ */
209
+ function assertAnswers(requests, answer) {
210
+ for (const predicate of answer.keys()) if (!requests.has(predicate)) throw new TypeError(`Extracted symbols answer unrequested predicate "${predicate.name}"`);
211
+ for (const [predicate, fields] of requests) {
212
+ const values = answer.get(predicate);
213
+ if (values === void 0) throw new TypeError(`Extracted symbols omit requested predicate "${predicate.name}"`);
214
+ 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(", ")}`);
215
+ for (const name of fields) {
216
+ const type = predicate.params[name];
217
+ if (!(values[name] ?? []).every((value) => holds(type, value))) throw new TypeError(`Extracted symbols for ${predicate.name}.${name} must be valid values of its type`);
218
+ }
219
+ }
220
+ }
221
+ /** Whether a field of this type can hold the value: a finite number, valid date, or own entity. */
222
+ function holds(type, value) {
223
+ if (type === String) return typeof value === "string";
224
+ if (type === Number) return typeof value === "number" && Number.isFinite(value);
225
+ if (type === Date) return value instanceof Date && !Number.isNaN(value.getTime());
226
+ return isEntity(value) && value.constructor === type;
227
+ }
228
+ /** Drop structural duplicates, keeping the first occurrence. */
229
+ function unique(values) {
230
+ return values.filter((value, index) => values.findIndex((other) => fieldEquals(value, other)) === index);
231
+ }
126
232
  // ../../packages/system-one/src/classifier/filter-predicates.ts
127
233
  /** Retain the predicates relevant to this passage. This is the first pipeline step. */
128
234
  async function filterPredicates({ chunk, predicates, config }) {
@@ -161,11 +267,15 @@ function needsFiltering(predicates, config) {
161
267
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
162
268
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
163
269
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
270
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
271
+ * each chunk offers, so they need no finite domain up front. Without one, a predicate
272
+ * with a field that has no values fails here.
164
273
  */
165
- function classifier$1({ corpus, input, config }) {
274
+ function classifier$1({ corpus, input, config, createChunkSymbolExtractor }) {
166
275
  const baselineKB = (input instanceof GuardedKB ? input : guard(input).permit("*")).fork();
167
276
  const outputKB = baselineKB.kb.fork();
168
277
  const predicates = classifiablePredicates(baselineKB);
278
+ if (createChunkSymbolExtractor === void 0) assertAskable(predicates);
169
279
  const chunker = chunkers.recursive({
170
280
  size: 8e3,
171
281
  overlap: 200
@@ -173,16 +283,21 @@ function classifier$1({ corpus, input, config }) {
173
283
  const jobs = corpus.documents().values().filter(() => predicates.length > 0).flatMap((document) => corpus.chunk(document, chunker)).map((chunk) => ({ execute: async () => await processChunk({
174
284
  chunk,
175
285
  predicates,
286
+ createExtractor: createChunkSymbolExtractor,
176
287
  config,
177
288
  kb: outputKB
178
289
  }) }));
179
290
  return Object.assign(jobs, { kb: outputKB });
180
291
  }
181
- async function processChunk({ chunk, predicates: inputPredicates, config, kb }) {
182
- const predicates = await filterPredicates({
292
+ async function processChunk({ chunk, predicates: inputPredicates, createExtractor, config, kb }) {
293
+ const predicates = await extractSymbols({
183
294
  chunk,
184
- predicates: inputPredicates,
185
- config
295
+ predicates: await filterPredicates({
296
+ chunk,
297
+ predicates: inputPredicates,
298
+ config
299
+ }),
300
+ createExtractor
186
301
  });
187
302
  await extractEvidence({
188
303
  chunk,
@@ -196,39 +311,25 @@ async function processChunk({ chunk, predicates: inputPredicates, config, kb })
196
311
  kb
197
312
  });
198
313
  }
199
- // ../../packages/system-one/src/utils.ts
200
- /** Snapshot JSON data, including nested dates and entities identified by a string id. */
201
- function encode(value) {
202
- const entities = /* @__PURE__ */ new WeakMap();
203
- const json = JSON.stringify(value, (_, item) => {
204
- if (!isRecord$1(item) || typeof item["id"] !== "string") return item;
205
- let entity = entities.get(item);
206
- if (entity === void 0) {
207
- entity = {
208
- ...item,
209
- type: typeof item.constructor === "function" ? item.constructor.name : "Object"
210
- };
211
- entities.set(item, entity);
212
- }
213
- return entity;
214
- });
215
- if (typeof json !== "string") throw new TypeError("Cannot encode a value without a JSON representation");
216
- return JSON.parse(json);
217
- }
218
- /** A compact diagnostic label, using entity ids and the predicate's declared field order. */
219
- function formatAtom({ predicate, fields }) {
220
- return `${predicate.name}(${predicate.paramNames.map((name) => format(fields[name])).join(", ")})`;
221
- }
222
- function format(value) {
223
- if (value instanceof Date) return value.toISOString();
224
- return isRecord$1(value) && "id" in value ? String(value["id"]) : String(value);
225
- }
226
- function isRecord$1(value) {
227
- return typeof value === "object" && value !== null && !Array.isArray(value);
314
+ /**
315
+ * Throw for the first predicate with a field that has no values. Without a symbol
316
+ * extractor, fields hold only the values known up front, so such a predicate could never
317
+ * be asked about.
318
+ */
319
+ function assertAskable(predicates) {
320
+ for (const { declaration, roles } of predicates) {
321
+ const name = emptyField({
322
+ declaration,
323
+ roles
324
+ });
325
+ if (name === void 0) continue;
326
+ const type = declaration.params[name];
327
+ 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);
328
+ }
228
329
  }
229
- /** A finite probability, including both endpoints. */
230
- function isProbability(value) {
231
- return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
330
+ /** The first field, in declared order, with no values. */
331
+ function emptyField({ declaration, roles }) {
332
+ return declaration.paramNames.find((name) => (roles[name] ?? []).length === 0);
232
333
  }
233
334
  // ../../packages/system-one/src/config.ts
234
335
  /** Default minimum probability for admitting signed evidence. @beta */
@@ -411,7 +512,7 @@ function systemOneModel(endpoint) {
411
512
  judgement: choice(response, OUTCOMES, id, name)
412
513
  })
413
514
  })),
414
- adjudicate: async ({ atom, passages }) => await ask({
515
+ adjudicate: async ({ atom, passages }) => passages.length === 0 ? [] : await ask({
415
516
  endpoint,
416
517
  prefix: "passage",
417
518
  items: passages,
@@ -433,10 +534,12 @@ async function inBatches(items, size, request) {
433
534
  * Ask one question per item in a single request and turn each answer into a result.
434
535
  * Question keys are positional, so they are minted and matched only here. Validation
435
536
  * happens while building results, so one bad answer rejects the whole request.
436
- * Nothing to ask sends nothing; more than the provider allows sends nothing and throws.
537
+ * Every request holds at least one question: callers send nothing when there is nothing
538
+ * to ask, so an empty request is a bug. More than the provider allows sends nothing and
539
+ * throws.
437
540
  */
438
541
  async function ask({ endpoint: { name, maxBatchSize, transport }, prefix, items, state, question, answer }) {
439
- if (items.length === 0) return [];
542
+ assert(items.length > 0, `Asked ${name} an empty ${prefix} request`);
440
543
  if (items.length > maxBatchSize) throw new RangeError(`Cannot ask ${items.length} ${prefix} questions in one request: ${name} accepts at most ${maxBatchSize}`);
441
544
  const keyed = items.map((item, index) => ({
442
545
  id: `${prefix}_${index}`,
@@ -583,10 +686,11 @@ function systemOne(provider) {
583
686
  pipelines.set(normalized, resolved);
584
687
  return normalized;
585
688
  },
586
- classifier: ({ corpus, input, config }) => classifier$1({
689
+ classifier: ({ corpus, input, config, createChunkSymbolExtractor }) => classifier$1({
587
690
  corpus,
588
691
  input,
589
- config: pipeline(config)
692
+ config: pipeline(config),
693
+ createChunkSymbolExtractor
590
694
  }),
591
695
  resolver: (corpus, kb, config) => resolver$1(corpus, kb, pipeline(config))
592
696
  };
@@ -657,6 +761,8 @@ function createConfig(config) {
657
761
  * Validate available predicates eagerly and return a lazy iterator of executable jobs.
658
762
  * Bare KBs expose all known predicates; explicit guards retain their permission policy.
659
763
  * Only job.execute() makes a remote call. The consumer owns pacing and concurrency.
764
+ * With `createChunkSymbolExtractor`, open-ended and entity fields also take the values
765
+ * each chunk offers, so they need no finite domain up front.
660
766
  * @beta
661
767
  */
662
768
  function classifier(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factbag/clef",
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, using Cloudflare's Clef models on Workers AI.",
5
5
  "keywords": [
6
6
  "classification",
@@ -87,7 +87,7 @@
87
87
  "vitest": "5.0.5"
88
88
  },
89
89
  "peerDependencies": {
90
- "factbag": "0.0.0-alpha.5"
90
+ "factbag": "0.0.0-alpha.6"
91
91
  },
92
92
  "scripts": {
93
93
  "api-report": "api-extractor run --local",