@kortexya/reasoninglayer 1.27.0 → 2.0.0

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/dist/index.cjs CHANGED
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
  };
8
8
 
9
9
  // src/config.ts
10
- var SDK_VERSION = "1.27.0";
10
+ var SDK_VERSION = "2.0.0";
11
11
  function resolveConfig(config) {
12
12
  if (!config.baseUrl) {
13
13
  throw new Error("ClientConfig.baseUrl is required");
@@ -102,6 +102,20 @@ var ConstraintViolationError = class extends ApiError {
102
102
  this.constraint = constraint;
103
103
  }
104
104
  };
105
+ var BulkRefusedError = class extends ApiError {
106
+ name = "BulkRefusedError";
107
+ /**
108
+ * The refused rows, each naming its index in the request's `terms` array.
109
+ *
110
+ * @remarks
111
+ * Never empty — the engine answers this shape only when it refused something.
112
+ */
113
+ rows;
114
+ constructor(message, body, headers, rows, errorCode) {
115
+ super(message, 422, body, headers, errorCode);
116
+ this.rows = rows;
117
+ }
118
+ };
105
119
  var RateLimitError = class extends ApiError {
106
120
  name = "RateLimitError";
107
121
  /** Seconds to wait before retrying, or null if not specified. */
@@ -191,6 +205,13 @@ function createApiError(status, body, headers) {
191
205
  constraint
192
206
  );
193
207
  }
208
+ case 422: {
209
+ const rows = bulkRefusalRows(body);
210
+ if (rows !== null) {
211
+ return new BulkRefusedError(message, body, headers, rows, errorCode);
212
+ }
213
+ return new ApiError(message, status, body, headers, errorCode);
214
+ }
194
215
  case 429: {
195
216
  const rl = parseRateLimitHeaders(headers);
196
217
  return new RateLimitError(message, body, headers, errorCode, rl.retryAfter, rl.limit, rl.remaining);
@@ -207,6 +228,23 @@ function isErrorBody(body) {
207
228
  const obj = body;
208
229
  return typeof obj.error === "string" && typeof obj.message === "string";
209
230
  }
231
+ function bulkRefusalRows(body) {
232
+ if (typeof body !== "object" || body === null) return null;
233
+ const obj = body;
234
+ if (!Array.isArray(obj.errors)) return null;
235
+ const rows = [];
236
+ for (const entry of obj.errors) {
237
+ if (typeof entry !== "object" || entry === null) return null;
238
+ const row = entry;
239
+ if (typeof row.index !== "number" || typeof row.message !== "string") return null;
240
+ rows.push({
241
+ index: row.index,
242
+ message: row.message,
243
+ ...typeof row.feature === "string" ? { feature: row.feature } : {}
244
+ });
245
+ }
246
+ return rows.length > 0 ? rows : null;
247
+ }
210
248
  function isConstraintViolationBody(body) {
211
249
  if (!isErrorBody(body)) return false;
212
250
  if (!("details" in body)) return false;
@@ -1126,11 +1164,11 @@ var Sorts = class {
1126
1164
  ...params
1127
1165
  });
1128
1166
  /**
1129
- * @description Per Definition IV.5 (Milanese & Pasi, IEEE TFS 2024 — CC-BY manuscript in reasoninglayer-sources/pdf_sources/), verbatim: ≺∼· ≝ ((≺∼ .− ∼) ⊍ ⪯)⊕ The combined chain preorder ≺∼ of Definition IV.1 with every DIRECTLY-similar pair deleted (`.−` zeroes the pair — it is not an arithmetic difference), the crisp order unioned back, and the result re-closed. A chain survives when its ENDPOINTS are not directly similar: slasher ⪯ horror ∼₀.₅ thriller keeps 0.5 while (horror, thriller) itself answers 0 — two similar sorts meet through their GLB instead (Fig. 4c: horror ⩏ thriller = slasher). The combined ≺∼ — where a direct ∼ edge IS a step; the coarse retrieval mode of Example V.4 — backs equivalence classes and term substitutability internally. (History: the differencing form here is ORIGINAL and faithful; #203 swapped in the combined semantics, and a 2026-08-23 pass re-documented that as correct from secondary sources. The accepted manuscript settled it the other way.)
1167
+ * @description Omitted, the answer is Def. IV.5 `≾̇` — the default because that is the relation the graded GLB and term substitutability are computed from. `granularity: "combined"` answers Def. IV.1 `≺∼`, where a similarity edge IS a step, so a directly-similar pair reads its similarity degree instead of the `0` the pair deletion gives it. A caller asking "how close are these two sorts" wants the second; a caller asking "does this sort substitute for that one" wants the first (#282). Per Definition IV.5 (Milanese & Pasi, IEEE TFS 2024 — CC-BY manuscript in reasoninglayer-sources/pdf_sources/), verbatim: ≺∼· ≝ ((≺∼ .− ∼) ⊍ ⪯)⊕ The combined chain preorder ≺∼ of Definition IV.1 with every DIRECTLY-similar pair deleted (`.−` zeroes the pair — it is not an arithmetic difference), the crisp order unioned back, and the result re-closed. A chain survives when its ENDPOINTS are not directly similar: slasher ⪯ horror ∼₀.₅ thriller keeps 0.5 while (horror, thriller) itself answers 0 — two similar sorts meet through their GLB instead (Fig. 4c: horror ⩏ thriller = slasher). The combined ≺∼ — where a direct ∼ edge IS a step; the coarse retrieval mode of Example V.4 — backs equivalence classes and term substitutability internally. (History: the differencing form here is ORIGINAL and faithful; #203 swapped in the combined semantics, and a 2026-08-23 pass re-documented that as correct from secondary sources. The accepted manuscript settled it the other way.)
1130
1168
  *
1131
1169
  * @tags sorts
1132
1170
  * @name GetPreorderDegree
1133
- * @summary Get preorder degree ≾̇(s₁, s₂) between two sorts
1171
+ * @summary Get the preorder degree between two sorts, in either of the paper's two readings — `granularity` selects which, exactly as `GET /api/v1/sorts/quotient-order` does, and the answer echoes it back.
1134
1172
  * @request POST:/api/v1/sorts/preorder-degree
1135
1173
  */
1136
1174
  getPreorderDegree = (data, params = {}) => this.http.request({
@@ -1641,7 +1679,7 @@ var Terms = class {
1641
1679
  ...params
1642
1680
  });
1643
1681
  /**
1644
- * @description Creates multiple terms in a single operation for efficiency. This is optimized for high-volume data loading scenarios and skips individual witness validation (constraint propagation runs once at the end). # Performance - Terms are added to the domain store in a single lock acquisition - Constraint propagation runs once after all terms are added - Much faster than calling add_term N times # Declarations and all-or-nothing (#238, #239) Every entry is held to the same declarations as `POST /terms`, with the same `422`. One refusal — a declaration violation or a bound-constraint violation — refuses the WHOLE batch and puts the store and the facade back: fresh ids removed, coreferenced entities restored to their pre-batch description. A batch may reference the client-minted ids it is itself creating: the resolver is the resident store unioned with the batch's own ids. When such an id then coreferences through a `@key`, every designator of it in the same batch is rewritten to the entity it merged into — the reference names what the write actually produced, never an id the coreference removed. `term_ids` carries the EFFECTIVE id per request position — the created id, or the existing entity's id when the entry coreferenced through a `@key`. # Authorization Requires X-Tenant-Id header. The tenant_id is taken from the header.
1682
+ * @description Creates multiple terms in a single operation for efficiency. This is optimized for high-volume data loading scenarios and skips individual witness validation (constraint propagation runs once at the end). # Performance - Terms are added to the domain store in a single lock acquisition - Constraint propagation runs once after all terms are added - Much faster than calling add_term N times # Declarations and all-or-nothing (#238, #239, #262) Every entry is held to the same declarations as `POST /terms`, with the same `422`. One refusal — a declaration violation or a bound-constraint violation — refuses the WHOLE batch and puts the store and the facade back: fresh ids removed, coreferenced entities restored to their pre-batch description. The refusal NAMES THE ROWS (#262): the body carries `errors[]`, one entry per refused row, each with its `index` in the request's `terms` array and the `feature` the refusal is about when it names one, so a client repairs the exact rows it sent instead of guessing which one a single-message refusal meant. `partial: true` (#271) writes the rows the checks did NOT refuse instead of refusing the batch for one of them. A real import holding one bad record had to be resent minus that record — a second full call, a second full round of constraint evaluation, and a window in which a survivor's reference target can be deleted between the two — although the engine already names every bad entry by index in one pass and therefore already knows which ones were fine. The answer is `201` when anything was written, with `term_ids` carrying one id per ACCEPTED row and the same `errors[]` beside it; `422` when every row was refused, because nothing was written and that is a refusal. ⛔ Two failures stay batch-wide under `partial`, because neither can be attributed to a row, and the flag does not pretend otherwise: the end-of-batch constraint propagation answers ONE `409` for the whole batch (`facade.process_events`), and a persistence failure reverts what the batch inserted (#239). `dry_run: true` (#262) runs every check the real write runs — conversion, declarations, coreference, events — and writes nothing: the store and the facade are put back inside the write lock, nothing persists, nothing is notified, no derivation is queued. A clean dry run answers `200` with the count the batch would have produced and NO `term_ids` (#268): the rollback has already discarded every id it minted for a fresh entry, and a second, real POST of the same body mints different ones — so the vector named nothing, in the very positions the field documents as storable. The ids that DO outlive a dry run are the entities a `@key` coreference would have merged into, and those are answered separately as `coreferenced_term_ids`. A refused dry run answers the same `errors[]` body the real write answers with. A batch may reference the client-minted ids it is itself creating: the resolver is the resident store unioned with the batch's own ids. When such an id then coreferences through a `@key`, every designator of it in the same batch is rewritten to the entity it merged into — the reference names what the write actually produced, never an id the coreference removed. On a real write `term_ids` carries the EFFECTIVE id per request position — the created id, or the existing entity's id when the entry coreferenced through a `@key`. It is ABSENT on a dry run. # Authorization Requires X-Tenant-Id header. The tenant_id is taken from the header.
1645
1683
  *
1646
1684
  * @tags terms
1647
1685
  * @name BulkAddTerms
@@ -1831,7 +1869,7 @@ var Inference = class {
1831
1869
  ...params
1832
1870
  });
1833
1871
  /**
1834
- * @description # TRUE HOMOICONIC API Request contains a goal term and optional constraints. Response returns solutions with term-based substitutions. ## Temporal Reasoning Use constraints to filter by temporal relations: ```json { "goal": {"sort_name": "Employment", "features": {"valid_to": {"name": "?EndTime"}}}, "constraints": [{"type": "Guard", "left": "?EndTime", "op": "lt", "right": "1583020800000"}] } ``` ## Reading a solution `solutions[].proof.kind` says how the goal was established, and is always present: `proved` (a rule was applied — either it fired in this search, or the goal was a conclusion a forward chain had materialised and the engine recovered the rule from its recorded derivation, so `rule_term_id` and `rule_label` are filled), `fact` (a stored fact with no recorded derivation), `residuated` (an open-world leaf: unknown, never false), `unattributed` (a sound answer whose derivation the engine could not report). A client never has to infer this from the node's shape. A goal that carries `constraints` is a CONJUNCTION: its root node holds one subproof per clause and reports the strongest kind the clauses carry — `proved` when any clause was proved by a rule, else `fact`. `solutions[].substitution.bindings` carries one entry per query variable the engine resolved. A variable left unresolved is ABSENT; it is never bound to itself. # Authorization Requires X-Tenant-Id header. Traced: this is the path the zanzibar gateway hits for every permission check, so it is where an end-to-end trace either explains a slow request or does not. `skip_all` because the request body can be large and has no business in a span attribute.
1872
+ * @description # TRUE HOMOICONIC API Request contains a goal term and optional constraints. Response returns solutions with term-based substitutions. ## Temporal Reasoning Use constraints to filter by temporal relations: ```json { "goal": {"sort_name": "Employment", "features": {"valid_to": {"name": "?EndTime"}}}, "constraints": [{"type": "Guard", "left": "?EndTime", "op": "lt", "right": "1583020800000"}] } ``` ## Reading a solution `solutions[].proof.kind` says how the goal was established, and is always present: `proved` (a rule was applied — either it fired in this search, or the goal was a conclusion a forward chain had materialised and the engine recovered the rule from its recorded derivation, so `rule_term_id` and `rule_label` are filled), `fact` (a stored fact with no recorded derivation), `residuated` (an open-world leaf: unknown, never false), `unattributed` (a sound answer whose derivation the engine could not report). A client never has to infer this from the node's shape. A goal that carries `constraints` is a CONJUNCTION: its root node holds one subproof per clause and reports the strongest kind the clauses carry — `proved` when any clause was proved by a rule, else `fact`. `solutions[].substitution.bindings` carries one entry per query variable the engine resolved. A variable left unresolved is ABSENT; it is never bound to itself. ## Joining a proof to the thing it proves `proof.goal_term_id` is the TermId of the CONCLUSION the node proved. It joins to `POST /api/v1/query/by-sort` and `GET /api/v1/terms/{id}`, and two solutions of one goal share it only when they prove the same conclusion. It is ABSENT when there is no such id — a goal proved without a preceding `CHAIN` materialised no conclusion, and a derivation replayed from a persistent store cannot be attributed to one. A client falls back deliberately there rather than by accident. `proof.rule_head_term_id` names the rule's instantiated HEAD on a rule application, when it differs from the conclusion. ⛔ It is rule scaffolding: `GET /api/v1/terms/{id}` refuses it deliberately (#192). It is a grouping key, not a fetchable id. # Authorization Requires X-Tenant-Id header. Traced: this is the path the zanzibar gateway hits for every permission check, so it is where an end-to-end trace either explains a slow request or does not. `skip_all` because the request body can be large and has no business in a span attribute.
1835
1873
  *
1836
1874
  * @tags inference
1837
1875
  * @name BackwardChain
@@ -1939,7 +1977,7 @@ var Inference = class {
1939
1977
  ...params
1940
1978
  });
1941
1979
  /**
1942
- * @description This drops the hydrated base facts, the forward-chain `persist_derived` facts and the residuation store, then forgets the hydration flag so the next request reloads the base facts from PostgreSQL. **It does not delete durable data.** The terms are the authority; this is their cache. That makes it the retraction primitive forward chaining otherwise lacks. Chaining is monotonic — delete a `blocks` edge and the derived "A blocks B" survives every later pass, so the KB keeps asserting a relationship the user removed. Clearing and re-chaining rebuilds the closure from the terms that actually exist. # History Until 2026-07-28 this handler enumerated every term in the tenant and deleted it from PostgreSQL, while calling itself "clear facts" and reporting `"Cleared N facts/rules"`. It is reachable with an ordinary tenant credential — unlike `/api/v1/admin/clear-tenant/{tenant_id}`, which gateways block — so a caller reading the name, the path or the response body had no way to know it was a tenant wipe. It destroyed a live tenant that way. Use `DELETE /api/v1/terms/{term_id}` to delete a term, and the admin route to wipe a tenant; deleting durable data must not be something an endpoint does as a side effect of its name. # Authorization Requires X-Tenant-Id header, and the path tenant must match it.
1980
+ * @description This drops the hydrated base facts, the forward-chain `keep_derived` facts and the residuation store, then forgets the hydration flag so the next request reloads the base facts from PostgreSQL. **It does not delete durable data.** The terms are the authority; this is their cache. That makes it the retraction primitive forward chaining otherwise lacks. Chaining is monotonic — delete a `blocks` edge and the derived "A blocks B" survives every later pass, so the KB keeps asserting a relationship the user removed. Clearing and re-chaining rebuilds the closure from the terms that actually exist. # History Until 2026-07-28 this handler enumerated every term in the tenant and deleted it from PostgreSQL, while calling itself "clear facts" and reporting `"Cleared N facts/rules"`. It is reachable with an ordinary tenant credential — unlike `/api/v1/admin/clear-tenant/{tenant_id}`, which gateways block — so a caller reading the name, the path or the response body had no way to know it was a tenant wipe. It destroyed a live tenant that way. Use `DELETE /api/v1/terms/{term_id}` to delete a term, and the admin route to wipe a tenant; deleting durable data must not be something an endpoint does as a side effect of its name. # Authorization Requires X-Tenant-Id header, and the path tenant must match it.
1943
1981
  *
1944
1982
  * @tags inference
1945
1983
  * @name ClearFacts
@@ -6681,7 +6719,7 @@ var Query = class {
6681
6719
  this.http = http;
6682
6720
  }
6683
6721
  /**
6684
- * @description Returns all terms with the specified sort OR any of its subtypes. This implements proper OSF polymorphic query semantics where querying a parent sort returns all instances of that sort and its descendants. ## Resolving `sort_name` A name can denote more than one id (see `sort_name_candidates`), and no cheap probe tells which of them the query can actually answer from: on the production adapter `get_sort` and `get_sort_ids_by_names` are bare reads of an in-memory cache with no persistence fallback, while the query's own `get_compatible_sorts` does fall back to Postgres. Confirming a candidate with `get_sort` would therefore 404 every tenant sort created before the last restart — a guard strictly stricter than the thing it guards. So the candidates are **tried** against the real query; only `SortNotFound` moves on to the next, every other failure is returned as-is. A phantom id (minted into the tenant lattice by ingestion and never persisted, #138) cannot be returned: the query authority refuses it and the loop skips past it. When no candidate answers, the sort is not queryable and the honest reply is 404 naming the sort the CALLER asked for — never a 400 leaking an internal `SortId` the caller never supplied. Every candidate that answers **contributes**; the answer is their union, deduplicated by term id. Registration now keeps a tenant to one sort per name (#139), so two answering candidates mean rows a pre-fix engine left behind: one name, two sorts, and the tenant's terms of that type divided between them. Stopping at the first — what this route did — returned one half and reported nothing about the other, because an id the caller never supplied going unqueried raises no error. In the ordinary case exactly one candidate answers and the route runs exactly one term query, as it always did.
6722
+ * @description Returns all terms with the specified sort OR any of its subtypes. This implements proper OSF polymorphic query semantics where querying a parent sort returns all instances of that sort and its descendants. ## Resolving `sort_name` A name can denote more than one id (see `sort_name_candidates`), and no cheap probe tells which of them the query can actually answer from: on the production adapter `get_sort` and `get_sort_ids_by_names` are bare reads of an in-memory cache with no persistence fallback, while the query's own `get_compatible_sorts` does fall back to Postgres. Confirming a candidate with `get_sort` would therefore 404 every tenant sort created before the last restart — a guard strictly stricter than the thing it guards. So the candidates are **tried** against the real query; only `SortNotFound` moves on to the next, every other failure is returned as-is. A phantom id (minted into the tenant lattice by ingestion and never persisted, #138) cannot be returned: the query authority refuses it and the loop skips past it. When no candidate answers, the sort is not queryable and the honest reply is 404 naming the sort the CALLER asked for — never a 400 leaking an internal `SortId` the caller never supplied. Every candidate that answers **contributes**; the answer is their union, deduplicated by term id. Registration now keeps a tenant to one sort per name (#139), so two answering candidates mean rows a pre-fix engine left behind: one name, two sorts, and the tenant's terms of that type divided between them. Stopping at the first — what this route did — returned one half and reported nothing about the other, because an id the caller never supplied going unqueried raises no error. In the ordinary case exactly one candidate answers and the route runs exactly one term query, as it always did. ## Conclusions, and what this route promises The answer is the tenant's durable extension of the browse closure UNIONED with the conclusions this process currently HOLDS — the same set OSFQL `MATCH` reads. That is what makes a rule sort readable here at all: no OSFQL path persists a conclusion, so the durable half is empty for a sort whose members exist only by derivation (#261). This route does NOT chain. It reports what the last chain left, so: * after a restart the derived half is empty until something chains again — `CHAIN`, `PROVE`, a retraction's re-chain, or `POST /api/v1/inference/forward-chain`; * a conclusion whose premise was withdrawn through a door that runs no truth maintenance is still answered until the next chain (`docs/OPEN_DEFECTS.md` #82). `total` is the size of the answer before `offset`/`limit`, counted — never a search bound. Rows are ordered by term id, so a page is stable. When the answer is EMPTY and the browsed sort is a rule conclusion sort, `note` says so and names the routes that materialise the conclusions. A bare `total: 0` reads as an authoritative count, and for a rule sort in a freshly-started process it is the one answer a caller must not take at face value — the same confusion #261 was filed about, one state later.
6685
6723
  *
6686
6724
  * @tags query
6687
6725
  * @name FindBySort
@@ -9594,7 +9632,7 @@ var Admin = class {
9594
9632
  ...params
9595
9633
  });
9596
9634
  /**
9597
- * @description Truncates the derived_facts table for the given tenant and queues a BootstrapAll event covering every analyzed rule. Asynchronous: returns the queue depth and current materialization LSN, not the final state. Concurrent rebuilds for the same tenant are rejected with 409 (per-tenant mutex). Full operational runbook (prerequisites, timings, monitoring, failure recovery, known limitations) lives in .claude/SUB_MS_FORWARD_CHAINING_STATUS.md, section 'Operator runbook — derived-facts rebuild'.
9635
+ * @description Re-materialises the tenant's conclusions SYNCHRONOUSLY: chains the resident store once, so every conclusion the rules prove and the store lacks comes back. `rematerialised` reports how many, and every read surface answers from that store — this is the one call that repairs a drifted tenant (#270). ONE DIRECTION: it does not remove a conclusion the rules no longer support, because the write doors' truth maintenance already does that on the write that changed the premise. It ALSO truncates the durable derived_facts table and queues a BootstrapAll event over every analyzed rule; that half is asynchronous, and `removed` / `rules_queued` / `materialization_lsn` describe it. Concurrent rebuilds for the same tenant are rejected with 409 (per-tenant mutex). Full operational runbook (prerequisites, timings, monitoring, failure recovery, known limitations) lives in .claude/SUB_MS_FORWARD_CHAINING_STATUS.md, section 'Operator runbook — derived-facts rebuild'.
9598
9636
  *
9599
9637
  * @tags admin
9600
9638
  * @name RebuildDerivedFacts
@@ -10040,6 +10078,24 @@ var Osfql = class {
10040
10078
  format: "json",
10041
10079
  ...params
10042
10080
  });
10081
+ /**
10082
+ * @description Takes the same body as `POST /api/v1/osfql` (the `query` matters; `atomic` decides whether the answer reports the atomic refusal) and answers: - `mutates` — whether ANY statement writes, folding every nested statement, so an `IF` whose THEN or ELSE branch writes reports `true`, and including an inline-write `MATCH` a first-statement classification would read as safe; - `atomic_refusal` — why the program cannot run as one atomic unit, with the same code the run would refuse with (`drop_sort_in_atomic_program`); - `statements` — one entry per statement with the ENGINE's classification (catalog id, risk tier, and a per-statement `mutates` — not a client-side tokenizer), the exact source text, the sorts named, nested entries for IF branches and `WITH` continuations, and — for each destructive statement — the affected-row count and up to 10 sample rows. The per-statement `mutates` is what a write gate reads: `CHAIN` and `RELEASE RESIDUATIONS` report `true` although they classify as `process_control`, while `MARK`, `CUT` and `SPACE` report `false`. No client has to keep its own table of which process-control statements write; `GET /api/v1/osfql/catalog` carries the same verdict per entry as `never` / `always` / `depends` (#266). The classification comes from the engine's own parse; the counts come from a read-only `MATCH` derived from the destructive statement's pattern and run against a CLONE of the tenant. Nothing runs against the live store, so nothing observable happens in the tenant afterwards. # Examples ```json { "query": "MATCH person(name: ?N); RETRACT person(name: \"Bob\");" } ``` ```json { "query": "DROP SORT person; INSERT person(name: \"Bob\");" } ```
10083
+ *
10084
+ * @tags osfql
10085
+ * @name PreviewOsfql
10086
+ * @summary Preview an OSFQL program: what each statement would do, decided without running anything (#257).
10087
+ * @request POST:/api/v1/osfql/preview
10088
+ * @secure
10089
+ */
10090
+ previewOsfql = (data, params = {}) => this.http.request({
10091
+ path: `/api/v1/osfql/preview`,
10092
+ method: "POST",
10093
+ body: data,
10094
+ secure: true,
10095
+ type: "application/json",
10096
+ format: "json",
10097
+ ...params
10098
+ });
10043
10099
  };
10044
10100
 
10045
10101
  // src/api-spec/generated/Context.ts
@@ -13229,6 +13285,45 @@ var Research = class {
13229
13285
  });
13230
13286
  };
13231
13287
 
13288
+ // src/normalizers/ontology-alignment.ts
13289
+ function AlignOntologyRequestFromFrontToApi(model) {
13290
+ return {
13291
+ domain_owl: model.domainOwl,
13292
+ targets: model.targets
13293
+ };
13294
+ }
13295
+ function AlignmentMatchDtoFromApiToFront(dto) {
13296
+ return {
13297
+ domainSort: dto.domain_sort,
13298
+ matchType: dto.match_type,
13299
+ targetCurie: dto.target_curie,
13300
+ targetLabel: dto.target_label
13301
+ };
13302
+ }
13303
+ function AlignmentConflictDtoFromApiToFront(dto) {
13304
+ return {
13305
+ domainSort: dto.domain_sort,
13306
+ targetA: dto.target_a,
13307
+ targetB: dto.target_b
13308
+ };
13309
+ }
13310
+ function ExternalMatchDtoFromApiToFront(dto) {
13311
+ return {
13312
+ matchType: dto.match_type,
13313
+ ontologyId: dto.ontology_id,
13314
+ source: dto.source
13315
+ };
13316
+ }
13317
+ function AlignOntologyResponseFromApiToFront(dto) {
13318
+ return {
13319
+ conflicts: dto.conflicts.map(AlignmentConflictDtoFromApiToFront),
13320
+ domainSorts: dto.domain_sorts,
13321
+ mappingTtl: dto.mapping_ttl,
13322
+ matches: dto.matches.map(AlignmentMatchDtoFromApiToFront),
13323
+ targetSorts: dto.target_sorts
13324
+ };
13325
+ }
13326
+
13232
13327
  // src/utils/records.ts
13233
13328
  function definedEntries(map) {
13234
13329
  return Object.entries(map).filter((entry) => entry[1] !== void 0);
@@ -13280,7 +13375,12 @@ function FeatureDescriptorDtoFromApiToFront(dto) {
13280
13375
  // `undefined` for a question the wire already answers.
13281
13376
  required: dto.required ?? false,
13282
13377
  constraint: dto.constraint ? ConstraintDtoFromApiToFront(dto.constraint) : void 0,
13283
- key: dto.key
13378
+ key: dto.key,
13379
+ expectedSortName: dto.expected_sort_name ?? void 0,
13380
+ minCount: dto.min_count ?? void 0,
13381
+ maxCount: dto.max_count ?? void 0,
13382
+ cardinalityOrigin: dto.cardinality_origin ?? void 0,
13383
+ annotations: definedRecord2(dto.annotations)
13284
13384
  };
13285
13385
  }
13286
13386
  function FeatureDescriptorDtoFromFrontToApi(model) {
@@ -13290,7 +13390,18 @@ function FeatureDescriptorDtoFromFrontToApi(model) {
13290
13390
  expected_type_hint: model.expectedTypeHint ?? void 0,
13291
13391
  required: model.required,
13292
13392
  constraint: model.constraint ? ConstraintDtoFromFrontToApi(model.constraint) : void 0,
13293
- key: model.key
13393
+ key: model.key,
13394
+ expected_sort_name: model.expectedSortName ?? void 0,
13395
+ min_count: model.minCount ?? void 0,
13396
+ max_count: model.maxCount ?? void 0,
13397
+ cardinality_origin: model.cardinalityOrigin ?? void 0,
13398
+ annotations: model.annotations
13399
+ };
13400
+ }
13401
+ function CoextensiveDefinitionDtoFromApiToFront(dto) {
13402
+ return {
13403
+ definition: dto.definition,
13404
+ exampleCount: dto.example_count
13294
13405
  };
13295
13406
  }
13296
13407
  function BoundConstraintDtoFromApiToFront(dto) {
@@ -13372,7 +13483,17 @@ function SortDtoFromApiToFront(dto) {
13372
13483
  worldMode: dto.world_mode,
13373
13484
  annotations: definedRecord2(dto.annotations),
13374
13485
  pluginId: dto.plugin_id ?? void 0,
13375
- pluginLocalName: dto.plugin_local_name ?? void 0
13486
+ pluginLocalName: dto.plugin_local_name ?? void 0,
13487
+ altLabels: dto.alt_labels,
13488
+ hiddenLabels: dto.hidden_labels,
13489
+ scopeNote: dto.scope_note ?? void 0,
13490
+ related: dto.related,
13491
+ definition: dto.definition ?? void 0,
13492
+ coextensive: dto.coextensive?.map(CoextensiveDefinitionDtoFromApiToFront),
13493
+ externalMatches: dto.external_matches?.map(
13494
+ (match) => ExternalMatchDtoFromApiToFront(match)
13495
+ ),
13496
+ featureEquations: dto.feature_equations
13376
13497
  };
13377
13498
  }
13378
13499
  function SortInfoDtoFromApiToFront(dto) {
@@ -13384,7 +13505,20 @@ function SortInfoDtoFromApiToFront(dto) {
13384
13505
  function SortListResponseFromApiToFront(dto) {
13385
13506
  return {
13386
13507
  sorts: dto.sorts.map(SortDtoFromApiToFront),
13387
- count: dto.count
13508
+ count: dto.count,
13509
+ total: dto.total,
13510
+ offset: dto.offset
13511
+ };
13512
+ }
13513
+ function ListSortsQueryFromFrontToApi(query) {
13514
+ if (query === void 0) return void 0;
13515
+ return {
13516
+ include_system: query.includeSystem,
13517
+ limit: query.limit,
13518
+ llm_extracted: query.llmExtracted,
13519
+ name_prefix: query.namePrefix?.join(","),
13520
+ needs_review: query.needsReview,
13521
+ offset: query.offset
13388
13522
  };
13389
13523
  }
13390
13524
  function CreateSortRequestFromFrontToApi(model) {
@@ -13406,6 +13540,8 @@ function BulkSortDefinitionFromFrontToApi(model) {
13406
13540
  parents: model.parents,
13407
13541
  features: model.features?.map(FeatureDescriptorDtoFromFrontToApi),
13408
13542
  alt_labels: model.altLabels,
13543
+ hidden_labels: model.hiddenLabels,
13544
+ scope_note: model.scopeNote ?? void 0,
13409
13545
  description: model.description ?? void 0,
13410
13546
  world_mode: model.worldMode
13411
13547
  };
@@ -13534,19 +13670,56 @@ function BulkSetSimilaritiesResponseFromApiToFront(dto) {
13534
13670
  errors: dto.errors
13535
13671
  };
13536
13672
  }
13673
+ var SORT_PREORDER_GRANULARITIES = [
13674
+ "similarity_deleted",
13675
+ "combined"
13676
+ ];
13677
+ function toSortPreorderGranularity(value) {
13678
+ const known = SORT_PREORDER_GRANULARITIES.find((candidate) => candidate === value);
13679
+ if (known === void 0) {
13680
+ throw new ValidationError(
13681
+ `Unknown preorder granularity "${value}" \u2014 expected ${SORT_PREORDER_GRANULARITIES.join(" or ")}.`
13682
+ );
13683
+ }
13684
+ return known;
13685
+ }
13537
13686
  function GetPreorderDegreeRequestFromFrontToApi(model) {
13538
13687
  return {
13539
13688
  sort1_id: model.sort1Id,
13540
- sort2_id: model.sort2Id
13689
+ sort2_id: model.sort2Id,
13690
+ granularity: model.granularity
13541
13691
  };
13542
13692
  }
13543
13693
  function GetPreorderDegreeResponseFromApiToFront(dto) {
13544
13694
  return {
13545
13695
  sort1Id: dto.sort1_id,
13546
13696
  sort2Id: dto.sort2_id,
13697
+ degree: dto.degree,
13698
+ granularity: toSortPreorderGranularity(dto.granularity)
13699
+ };
13700
+ }
13701
+ function QuotientClassFromApiToFront(dto) {
13702
+ return {
13703
+ sortIds: dto.sort_ids,
13704
+ size: dto.size,
13705
+ alpha: dto.alpha
13706
+ };
13707
+ }
13708
+ function QuotientOrderEdgeFromApiToFront(dto) {
13709
+ return {
13710
+ from: dto.from,
13711
+ to: dto.to,
13547
13712
  degree: dto.degree
13548
13713
  };
13549
13714
  }
13715
+ function GetQuotientOrderResponseFromApiToFront(dto) {
13716
+ return {
13717
+ granularity: toSortPreorderGranularity(dto.granularity),
13718
+ classes: dto.classes.map(QuotientClassFromApiToFront),
13719
+ count: dto.count,
13720
+ orderEdges: dto.order_edges.map(QuotientOrderEdgeFromApiToFront)
13721
+ };
13722
+ }
13550
13723
  function EquivalenceClassFromApiToFront(dto) {
13551
13724
  return {
13552
13725
  sortIds: dto.sort_ids,
@@ -14015,13 +14188,60 @@ var SortsClient = class {
14015
14188
  return response.data;
14016
14189
  }
14017
14190
  /**
14018
- * List all sorts.
14191
+ * List every sort the tenant owns.
14019
14192
  *
14020
- * @returns Array of sorts.
14193
+ * @param requestOptions - Per-call transport overrides.
14194
+ * @returns The sorts, each with its feature declarations.
14195
+ * @throws {ApiError} When the engine refuses the request.
14196
+ *
14197
+ * @remarks
14198
+ * This asks for the whole listing and keeps only the array. A production
14199
+ * tenant can own 1 M+ sorts (~500 MB uncompressed), which one response
14200
+ * cannot deliver — use {@link SortsClient.listSortsPage} to window it, to
14201
+ * filter it, or to read the `total` that says when to stop.
14202
+ *
14203
+ * @example
14204
+ * ```typescript
14205
+ * const sorts = await client.sorts.listSorts();
14206
+ * ```
14021
14207
  */
14022
14208
  async listSorts(requestOptions) {
14023
- const response = await this.sorts.listSorts(this.tenantId, void 0, toRequestParams(requestOptions));
14024
- return SortListResponseFromApiToFront(response.data).sorts;
14209
+ return (await this.listSortsPage(void 0, requestOptions)).sorts;
14210
+ }
14211
+ /**
14212
+ * List the tenant's sorts, keeping the envelope — `count`, `total` and
14213
+ * `offset` beside the page.
14214
+ *
14215
+ * @param query - The window and filters over the listing.
14216
+ * @param requestOptions - Per-call transport overrides.
14217
+ * @returns The page, its length, the tenant's filtered total and the
14218
+ * offset the page starts at.
14219
+ * @throws {ApiError} When the engine refuses the request.
14220
+ *
14221
+ * @remarks
14222
+ * `count` is the length of THIS page; `total` is what the tenant owns after
14223
+ * filters, across all pages. Measured against the engine on a tenant
14224
+ * holding three sorts, `GET /api/v1/sorts/tenant/{id}` answers
14225
+ * `{"count":3,"total":3,"offset":0}`.
14226
+ *
14227
+ * @example
14228
+ * ```typescript
14229
+ * let offset = 0;
14230
+ * for (;;) {
14231
+ * const page = await client.sorts.listSortsPage({ limit: 500, offset });
14232
+ * consume(page.sorts);
14233
+ * offset += page.count;
14234
+ * if (page.total === undefined || offset >= page.total) break;
14235
+ * }
14236
+ * ```
14237
+ */
14238
+ async listSortsPage(query, requestOptions) {
14239
+ const response = await this.sorts.listSorts(
14240
+ this.tenantId,
14241
+ ListSortsQueryFromFrontToApi(query),
14242
+ toRequestParams(requestOptions)
14243
+ );
14244
+ return SortListResponseFromApiToFront(response.data);
14025
14245
  }
14026
14246
  /**
14027
14247
  * Bulk-create sorts with name-based parent references.
@@ -14346,13 +14566,32 @@ var SortsClient = class {
14346
14566
  /**
14347
14567
  * Compute the preorder degree between two sorts.
14348
14568
  *
14349
- * @param request - Sort pair to compute preorder degree for.
14350
- * @returns The preorder degree response including sort IDs and degree.
14351
- * @throws {@link ApiError} If the sorts do not exist.
14352
- *
14353
- * @remarks
14354
- * Per Definition IV.5 (Milanese and Pasi 2024), the combined preorder is:
14355
- * `preorder_dot = ((similarity - subsumption) union subsumption)^+`
14569
+ * @param request - Sort pair to compute preorder degree for, and optionally
14570
+ * which of the two preorders to read it from.
14571
+ * @returns The preorder degree response including sort IDs, degree, and the
14572
+ * granularity the degree was read from.
14573
+ * @throws {@link ApiError} If the sorts do not exist, or if `granularity`
14574
+ * carries a spelling the engine does not accept.
14575
+ * @throws {@link ValidationError} If the engine answers a granularity this
14576
+ * SDK version does not know.
14577
+ *
14578
+ * @remarks
14579
+ * Per Definition IV.5 (Milanese and Pasi, IEEE TFS 2024), the dotted preorder
14580
+ * is `preorder_dot = ((combined_preorder .- similarity) union subsumption)^+`,
14581
+ * where `.-` DELETES each directly-similar pair — it is NOT an arithmetic
14582
+ * difference. So a directly-similar pair answers `0` under the default
14583
+ * granularity: two similar sorts meet through their GLB, not through each
14584
+ * other.
14585
+ *
14586
+ * `granularity` selects the reading, with the same two spellings
14587
+ * `GET /api/v1/sorts/quotient-order` uses:
14588
+ * - omitted or `similarity_deleted` — Definition IV.5, the default, and the
14589
+ * relation the graded GLB and term substitutability are computed from.
14590
+ * - `combined` — Definition IV.1, where a similarity edge IS a step, so a
14591
+ * directly-similar pair answers its similarity degree.
14592
+ *
14593
+ * The response always echoes the granularity back, so a `0.0` is never
14594
+ * ambiguous between "no path" and "the pair deletion zeroed it".
14356
14595
  *
14357
14596
  * Degree interpretation:
14358
14597
  * - 1.0 = subsumption (sort1 <= sort2)
@@ -14363,17 +14602,78 @@ var SortsClient = class {
14363
14602
  *
14364
14603
  * @example
14365
14604
  * ```typescript
14366
- * const result = await client.sorts.getPreorderDegree({
14605
+ * const strict = await client.sorts.getPreorderDegree({
14367
14606
  * sort1Id: 'uuid-1',
14368
14607
  * sort2Id: 'uuid-2',
14369
14608
  * });
14370
- * console.log(result.degree); // 0.72
14609
+ * console.log(strict.degree, strict.granularity); // 0 'similarity_deleted'
14610
+ *
14611
+ * const coarse = await client.sorts.getPreorderDegree({
14612
+ * sort1Id: 'uuid-1',
14613
+ * sort2Id: 'uuid-2',
14614
+ * granularity: 'combined',
14615
+ * });
14616
+ * console.log(coarse.degree, coarse.granularity); // 0.5 'combined'
14371
14617
  * ```
14372
14618
  */
14373
14619
  async getPreorderDegree(request, requestOptions) {
14374
14620
  const response = await this.sorts.getPreorderDegree(GetPreorderDegreeRequestFromFrontToApi(request), toRequestParams(requestOptions));
14375
14621
  return GetPreorderDegreeResponseFromApiToFront(response.data);
14376
14622
  }
14623
+ /**
14624
+ * Get the Definition IV.9 quotient order over the caller's own lattice.
14625
+ *
14626
+ * @param options - Which of the two fuzzy preorders to quotient. Omitted
14627
+ * means `combined`, the engine's default HERE.
14628
+ * @returns The equivalence classes with their degrees, and the fuzzy partial
14629
+ * order between them.
14630
+ * @throws {@link ApiError} 400 when `granularity` carries a spelling the
14631
+ * engine does not accept.
14632
+ * @throws {@link ValidationError} If the engine answers a granularity this
14633
+ * SDK version does not know.
14634
+ *
14635
+ * @remarks
14636
+ * This is the tenant-scoped companion of
14637
+ * {@link SortsClient.getEquivalenceClasses}: the classes, degrees and order
14638
+ * describe exactly the sorts the caller can see, where the older
14639
+ * equivalence-classes route computes process-wide and then filters. Prefer
14640
+ * this one.
14641
+ *
14642
+ * ⚠️ The default granularity here is `combined`, NOT the
14643
+ * `similarity_deleted` default of {@link SortsClient.getPreorderDegree}. The
14644
+ * two routes take the same two spellings and disagree on which is the
14645
+ * default, so state it when it matters. The response echoes it back either
14646
+ * way.
14647
+ *
14648
+ * `orderEdges` is SPARSE and indexes into `classes`: a pair with no edge has
14649
+ * degree `0`. The order is a partial order — antisymmetric, unlike either
14650
+ * preorder it is built from.
14651
+ *
14652
+ * Uses tagged serialization format.
14653
+ *
14654
+ * @example
14655
+ * ```typescript
14656
+ * const quotient = await client.sorts.getQuotientOrder({
14657
+ * granularity: 'similarity_deleted',
14658
+ * });
14659
+ *
14660
+ * for (const cls of quotient.classes) {
14661
+ * console.log(`class of ${cls.size} sorts, degree ${cls.alpha}`);
14662
+ * }
14663
+ * for (const edge of quotient.orderEdges) {
14664
+ * const lower = quotient.classes[edge.from];
14665
+ * const upper = quotient.classes[edge.to];
14666
+ * console.log(`${lower.sortIds} <= ${upper.sortIds} at ${edge.degree}`);
14667
+ * }
14668
+ * ```
14669
+ */
14670
+ async getQuotientOrder(options, requestOptions) {
14671
+ const response = await this.sorts.getQuotientOrder(
14672
+ { granularity: options?.granularity },
14673
+ toRequestParams(requestOptions)
14674
+ );
14675
+ return GetQuotientOrderResponseFromApiToFront(response.data);
14676
+ }
14377
14677
  /**
14378
14678
  * Get equivalence classes based on the combined preorder.
14379
14679
  *
@@ -14381,8 +14681,16 @@ var SortsClient = class {
14381
14681
  * @throws {@link ApiError} If the lattice cannot be computed.
14382
14682
  *
14383
14683
  * @remarks
14384
- * Per Definition IV.9 (Milanese and Pasi 2024):
14385
- * s1 ~ s2 iff preorder_dot(s1, s2) > 0 AND preorder_dot(s2, s1) > 0.
14684
+ * Per Definition IV.9 (Milanese and Pasi 2024), two sorts are equivalent when
14685
+ * each reaches the other: `s1 ~ s2` iff `preorder(s1, s2) > 0` AND
14686
+ * `preorder(s2, s1) > 0`. The preorder here is the COMBINED one, where a
14687
+ * similarity edge is itself a step — not the `similarity_deleted` default of
14688
+ * {@link SortsClient.getPreorderDegree}.
14689
+ *
14690
+ * ⚠️ This route computes PROCESS-WIDE and then filters, so its classes can
14691
+ * be shaped by sorts the caller cannot see. {@link SortsClient.getQuotientOrder}
14692
+ * computes on the tenant-visible hierarchy instead, returns the same classes
14693
+ * with their degrees, and adds the partial order between them. Prefer it.
14386
14694
  *
14387
14695
  * Uses tagged serialization format.
14388
14696
  *
@@ -14628,6 +14936,11 @@ function toUntaggedValue(value) {
14628
14936
  return value.value.map(toUntaggedValue);
14629
14937
  }
14630
14938
  if (value.type === "Reference") {
14939
+ if (typeof value.value !== "string") {
14940
+ throw new ValidationError(
14941
+ `A @key reference designator (sort "${value.value.sortName}") cannot be a FEATURE value on an untagged inference endpoint: the untagged feature-value family has no designator variant. Name a whole term with { designator: \u2026 } where a TermInputDto is expected, or resolve this one to a term id.`
14942
+ );
14943
+ }
14631
14944
  return { termId: value.value };
14632
14945
  }
14633
14946
  if (value.type === "SortId") {
@@ -14691,6 +15004,18 @@ function ValueDtoFromApiToFront(dto) {
14691
15004
  value: dto.value.map(ValueDtoFromApiToFront)
14692
15005
  };
14693
15006
  }
15007
+ if (dto.type === "Reference") {
15008
+ if (typeof dto.value === "string") {
15009
+ return { type: "Reference", value: dto.value };
15010
+ }
15011
+ return {
15012
+ type: "Reference",
15013
+ value: {
15014
+ sortName: dto.value.sort_name,
15015
+ features: featuresFromApiToFront(dto.value.features)
15016
+ }
15017
+ };
15018
+ }
14694
15019
  if (dto.type === "PsiTerm") {
14695
15020
  const features = dto.value.features;
14696
15021
  return {
@@ -14781,6 +15106,18 @@ function ValueDtoFromFrontToApi(value) {
14781
15106
  value: value.value.map(ValueDtoFromFrontToApi)
14782
15107
  };
14783
15108
  }
15109
+ if (value.type === "Reference") {
15110
+ if (typeof value.value === "string") {
15111
+ return { type: "Reference", value: value.value };
15112
+ }
15113
+ return {
15114
+ type: "Reference",
15115
+ value: {
15116
+ sort_name: value.value.sortName,
15117
+ features: featuresFromFrontToApi(value.value.features)
15118
+ }
15119
+ };
15120
+ }
14784
15121
  if (value.type === "PsiTerm") {
14785
15122
  const features = value.value.features;
14786
15123
  return {
@@ -14866,7 +15203,10 @@ function TermDtoFromApiToFront(dto) {
14866
15203
  sortName: dto.sort_name ?? void 0,
14867
15204
  displayName: dto.display_name ?? void 0,
14868
15205
  referencedTerms,
14869
- origin: dto.origin
15206
+ origin: dto.origin,
15207
+ // `derived_by` is a uuid or `null`; the shipped field is `string |
15208
+ // undefined`, so a null collapses rather than travelling as a third state.
15209
+ ...dto.derived_by == null ? {} : { derivedBy: dto.derived_by }
14870
15210
  };
14871
15211
  }
14872
15212
  function WitnessProofDtoFromApiToFront(dto) {
@@ -14895,9 +15235,21 @@ function CreateTermRequestFromFrontToApi(model) {
14895
15235
  return {
14896
15236
  sort_id: model.sortId,
14897
15237
  owner_id: model.ownerId,
14898
- features: featuresFromFrontToApi(model.features)
15238
+ features: featuresFromFrontToApi(model.features),
15239
+ ...model.id === void 0 ? {} : { id: model.id }
14899
15240
  };
14900
15241
  }
15242
+ function CreateTermInputFromFrontToApi(model) {
15243
+ if ("sortName" in model) {
15244
+ return {
15245
+ sort_name: model.sortName,
15246
+ owner_id: model.ownerId,
15247
+ features: featuresFromFrontToApi(model.features),
15248
+ ...model.id === void 0 ? {} : { id: model.id }
15249
+ };
15250
+ }
15251
+ return CreateTermRequestFromFrontToApi(model);
15252
+ }
14901
15253
  function UpdateTermRequestFromFrontToApi(model) {
14902
15254
  return {
14903
15255
  features: featuresFromFrontToApi(model.features)
@@ -14905,15 +15257,34 @@ function UpdateTermRequestFromFrontToApi(model) {
14905
15257
  }
14906
15258
  function BulkAddTermsRequestFromFrontToApi(model) {
14907
15259
  return {
14908
- terms: model.terms.map(CreateTermRequestFromFrontToApi)
15260
+ terms: model.terms.map(CreateTermInputFromFrontToApi),
15261
+ dry_run: model.dryRun,
15262
+ partial: model.partial
14909
15263
  };
14910
15264
  }
14911
15265
  function BulkAddTermsResponseFromApiToFront(dto) {
14912
15266
  return {
14913
- termIds: dto.term_ids,
15267
+ // `term_ids` is absent on a dry run and shorter than the request under
15268
+ // `partial`, so it is optional on the shipped type rather than defaulted to
15269
+ // an empty array — an empty array would read as "nothing was created", and
15270
+ // a dry run over a clean batch creates nothing while reporting a non-zero
15271
+ // `termsAdded`. The two facts are different and both matter.
15272
+ ...dto.term_ids ? { termIds: dto.term_ids } : {},
15273
+ termsAdded: dto.terms_added,
15274
+ dryRun: dto.dry_run,
15275
+ ...dto.refused === void 0 || dto.refused === null ? {} : { refused: dto.refused },
15276
+ ...dto.errors ? { errors: dto.errors.map(BulkRowRefusalFromApiToFront) } : {},
15277
+ ...dto.coreferenced_term_ids ? { coreferencedTermIds: dto.coreferenced_term_ids } : {},
14914
15278
  processingTimeMs: dto.processing_time_ms
14915
15279
  };
14916
15280
  }
15281
+ function BulkRowRefusalFromApiToFront(dto) {
15282
+ return {
15283
+ index: dto.index,
15284
+ message: dto.message,
15285
+ ...dto.feature ? { feature: dto.feature } : {}
15286
+ };
15287
+ }
14917
15288
  function ClearTermsResponseFromApiToFront(dto) {
14918
15289
  return {
14919
15290
  message: dto.message,
@@ -14958,7 +15329,12 @@ function ValidatedUnifyResponseFromApiToFront(dto) {
14958
15329
  function TermListResponseFromApiToFront(dto) {
14959
15330
  return {
14960
15331
  terms: dto.terms.map(TermDtoFromApiToFront),
14961
- count: dto.count
15332
+ count: dto.count,
15333
+ // `total` is `null` on a route that does not page, and `note` is `null`
15334
+ // whenever the engine has no remark. Both collapse to absent so a consumer
15335
+ // tests one thing — presence — rather than two.
15336
+ ...dto.total == null ? {} : { total: dto.total },
15337
+ ...dto.note == null ? {} : { note: dto.note }
14962
15338
  };
14963
15339
  }
14964
15340
  function TermReferrerDtoFromApiToFront(dto) {
@@ -15075,10 +15451,9 @@ var TermsClient = class {
15075
15451
  * ```
15076
15452
  */
15077
15453
  async createTerm(request, requestOptions) {
15078
- const wireRequest = CreateTermRequestFromFrontToApi({
15079
- ...request,
15080
- features: convertFeatures(request.features)
15081
- });
15454
+ const wireRequest = CreateTermInputFromFrontToApi(
15455
+ "sortName" in request ? { ...request, features: convertFeatures(request.features) } : { ...request, features: convertFeatures(request.features) }
15456
+ );
15082
15457
  const response = await this.api.addTerm(wireRequest, toRequestParams(requestOptions));
15083
15458
  return TermResponseFromApiToFront(response.data);
15084
15459
  }
@@ -15253,13 +15628,91 @@ var TermsClient = class {
15253
15628
  }
15254
15629
  }
15255
15630
  /**
15256
- * Bulk-create terms.
15631
+ * Create many terms in one request, all-or-nothing by default.
15632
+ *
15633
+ * @param request - The rows, plus `dryRun` to check without writing and
15634
+ * `partial` to keep the rows that passed. Features may be plain JS values
15635
+ * or `Value.*` output.
15636
+ * @param requestOptions - Per-call request options.
15637
+ * @returns What was written (or, on a dry run, what WOULD be written):
15638
+ * `termsAdded`, `termIds` on a real write, `errors` beside them under
15639
+ * `partial`, `coreferencedTermIds` on a dry run.
15640
+ * @throws {@link BulkRefusedError} 422 when the batch was refused and
15641
+ * NOTHING was written. Read `error.rows` — one entry per refused row, each
15642
+ * naming its `index` in `request.terms`. This is the answer for a refused
15643
+ * default batch, for a refused dry run, and for a `partial` batch in which
15644
+ * every row was refused.
15645
+ * @throws {@link ApiError} 409 when the end-of-batch constraint propagation
15646
+ * refuses the batch as a whole. That verdict cannot be attributed to a row,
15647
+ * so `partial` does not split it.
15648
+ *
15649
+ * @remarks
15650
+ * **Serialization format: Tagged (`ValueDto`).** Plain feature values are
15651
+ * converted exactly as {@link TermsClient.createTerm} converts them.
15652
+ *
15653
+ * There are three outcomes, and they are distinguishable without reading a
15654
+ * status code:
15655
+ *
15656
+ * 1. **A clean write** — `201`. `termIds` holds one id per request row, in
15657
+ * request order; `termsAdded` equals its length; `errors` is absent.
15658
+ * Measured 2026-09-18, a 2-row clean batch:
15659
+ * `{"terms_added":2,"term_ids":["3d936f37-…","f4a77a0b-…"],
15660
+ * "processing_time_ms":38,"dry_run":false}`.
15661
+ * 2. **A partial write** — `201`, and only with `partial: true`. Some rows
15662
+ * landed. `termIds` holds one id per ACCEPTED row, so it is SHORTER than
15663
+ * `request.terms`, and `errors` sits beside it naming the refused ones.
15664
+ * Map a refusal back with `errors[].index`, never with a position in
15665
+ * `termIds`. Measured, a 2-row batch whose second row violates a declared
15666
+ * range:
15667
+ * `{"terms_added":1,"term_ids":["d6ba85e6-…"],"errors":[{"index":1,
15668
+ * "feature":"price","message":"Constraint violation: Feature 'price'
15669
+ * value violates its declared range/constraint"}],"refused":1,
15670
+ * "processing_time_ms":37,"dry_run":false}`.
15671
+ * 3. **A refusal** — `422`, thrown as {@link BulkRefusedError}. Nothing was
15672
+ * written. Measured, the same bad batch WITHOUT `partial`:
15673
+ * `{"code":"bulk_refused","message":"1 of 2 entries were refused; the
15674
+ * whole batch was refused and nothing was written","errors":[{"index":1,
15675
+ * "feature":"price","message":"Constraint violation: …"}]}`. With
15676
+ * `partial: true` and BOTH rows bad, the same `422`:
15677
+ * `"2 of 2 entries were refused; the whole batch was refused and nothing
15678
+ * was written"`.
15679
+ *
15680
+ * **`dryRun` runs every check and writes nothing**, and it answers in the
15681
+ * same two shapes. A clean dry run is `200` and reports NO `termIds` —
15682
+ * measured:
15683
+ * `{"terms_added":2,"coreferenced_term_ids":[],"processing_time_ms":0,
15684
+ * "dry_run":true}`. The absence is deliberate: the rollback discarded every
15685
+ * id it minted, a later real write mints different ones, so the vector would
15686
+ * name nothing. The ids that DO outlive a dry run are the existing entities
15687
+ * a `@key` coreference would have merged into, and they come back as
15688
+ * `coreferencedTermIds`. A dry run over a BAD batch throws
15689
+ * {@link BulkRefusedError} with the same per-row refusals a real write throws
15690
+ * — measured, identical body to outcome 3 — so a caller can validate an
15691
+ * import with one call and never touch the store.
15692
+ *
15693
+ * @example
15694
+ * ```typescript
15695
+ * // Validate an import without writing.
15696
+ * try {
15697
+ * const check = await client.terms.bulkCreateTerms({ terms: rows, dryRun: true });
15698
+ * console.log(`${check.termsAdded} rows would be written`); // no check.termIds
15699
+ * } catch (e) {
15700
+ * if (e instanceof BulkRefusedError) {
15701
+ * for (const row of e.rows) console.error(`row ${row.index}: ${row.message}`);
15702
+ * }
15703
+ * }
15257
15704
  *
15258
- * @param request - Bulk creation request.
15259
- * @returns Bulk creation result with term UUIDs.
15705
+ * // Write what passes, and report what did not.
15706
+ * const result = await client.terms.bulkCreateTerms({ terms: rows, partial: true });
15707
+ * console.log(`${result.termsAdded} written`, result.termIds);
15708
+ * for (const bad of result.errors ?? []) {
15709
+ * console.warn(`row ${bad.index} (${bad.feature}): ${bad.message}`);
15710
+ * }
15711
+ * ```
15260
15712
  */
15261
15713
  async bulkCreateTerms(request, requestOptions) {
15262
15714
  const wireRequest = BulkAddTermsRequestFromFrontToApi({
15715
+ ...request,
15263
15716
  terms: request.terms.map((t) => ({
15264
15717
  ...t,
15265
15718
  features: convertFeatures(t.features)
@@ -15269,34 +15722,66 @@ var TermsClient = class {
15269
15722
  return BulkAddTermsResponseFromApiToFront(response.data);
15270
15723
  }
15271
15724
  /**
15272
- * List terms for the authenticated tenant.
15273
- *
15274
- * @param query - Optional paging and sort filter. Omit for every term.
15275
- * @returns The list of terms with total count.
15276
- * @throws {ApiError} If the request fails.
15725
+ * List one page of the tenant's terms, and say how many there are.
15277
15726
  *
15278
- * @remarks
15279
- * Terms are enriched with sort names, display names, and referenced term summaries.
15280
- * Requires X-Tenant-Id header (set via client configuration).
15281
- * Uses the tagged {@link ValueDto} serialization format.
15282
- *
15283
- * `sortName` filters on the sort's committed name. For a plugin-contributed
15284
- * sort that is the namespaced form (`plugin:<plugin-name>:<local>`), which
15285
- * {@link SortDto.name} carries and {@link SortDto.pluginLocalName} maps back
15286
- * to the name its author wrote.
15287
- *
15288
- * @example
15289
- * ```typescript
15290
- * const result = await client.terms.listTerms();
15291
- * console.log(`Found ${result.count} terms`);
15727
+ * @param query - Paging, the sort filter, and `includeDerived`. Omit for
15728
+ * every term.
15729
+ * @param requestOptions - Per-call request options.
15730
+ * @returns The page in `terms`, its length in `count`, the size of the whole
15731
+ * answer in `total`, and an engine remark in `note`.
15732
+ * @throws {@link ApiError} If the request fails.
15733
+ *
15734
+ * @remarks
15735
+ * **Serialization format: Tagged (`ValueDto`).** Terms are enriched with sort
15736
+ * names, display names, and referenced-term summaries. Requires the
15737
+ * `X-Tenant-Id` header, which the client configuration sets.
15738
+ *
15739
+ * **Page off `total`, not `count`.** `count` is this page's length and
15740
+ * nothing else. Measured 2026-09-18 on a tenant holding 3 terms:
15741
+ * `GET /api/v1/terms?limit=1&offset=2` answered
15742
+ * `{"terms":[…one…],"count":1,"total":3}`. `total` is counted before the
15743
+ * window, so it is the number to compare an offset against.
15744
+ *
15745
+ * **When the page is empty, read `note` before you report "no results".**
15746
+ * The route neither chains nor persists conclusions, so for a sort whose
15747
+ * members exist only by derivation the honest answer in a freshly started
15748
+ * process is zero rows — and `total: 0` reads like an authoritative "no
15749
+ * members", which is false. Measured 2026-09-18, a tenant with `widget`,
15750
+ * subsort `premium_widget`, one `widget` fact and the rule
15751
+ * `widget(name: ?N) → premium_widget(name: ?N)`:
15752
+ * `GET /api/v1/terms?sort_name=premium_widget` answered
15753
+ * `{"terms":[],"count":0,"total":0,"note":"`premium_widget` is a rule
15754
+ * conclusion sort: its members are derived … An empty answer here does not
15755
+ * mean the sort has no members."}`. Without the rule, the same empty answer
15756
+ * carried no note.
15757
+ *
15758
+ * `sortName` filters on the sort's committed name, and on that sort EXACTLY
15759
+ * — a member of a subsort is not answered. For a plugin-contributed sort the
15760
+ * committed name is the namespaced form (`plugin:<plugin-name>:<local>`),
15761
+ * which {@link SortDto.name} carries and {@link SortDto.pluginLocalName}
15762
+ * maps back to the name its author wrote.
15763
+ *
15764
+ * @example
15765
+ * ```typescript
15766
+ * const first = await client.terms.listTerms({ sortName: 'person', limit: 50 });
15767
+ * if (first.terms.length === 0 && first.note) console.info(first.note);
15768
+ * for (let offset = 50; offset < (first.total ?? 0); offset += 50) {
15769
+ * const page = await client.terms.listTerms({ sortName: 'person', limit: 50, offset });
15770
+ * // …
15771
+ * }
15292
15772
  *
15293
- * // Only the first page of one sort's terms.
15294
- * const page = await client.terms.listTerms({ sortName: 'person', limit: 50 });
15773
+ * // The asserted rows alone — the engine includes conclusions by default.
15774
+ * const asserted = await client.terms.listTerms({ includeDerived: false });
15295
15775
  * ```
15296
15776
  */
15297
15777
  async listTerms(query, requestOptions) {
15298
15778
  const response = await this.api.listTerms(
15299
- query ? { limit: query.limit, offset: query.offset, sort_name: query.sortName } : void 0,
15779
+ query ? {
15780
+ limit: query.limit,
15781
+ offset: query.offset,
15782
+ sort_name: query.sortName,
15783
+ include_derived: query.includeDerived
15784
+ } : void 0,
15300
15785
  toRequestParams(requestOptions)
15301
15786
  );
15302
15787
  return TermListResponseFromApiToFront(response.data);
@@ -15325,12 +15810,25 @@ var TermsClient = class {
15325
15810
  // ─── Friendly Aliases ─────────────────────────────────────────────
15326
15811
  /**
15327
15812
  * Create multiple records in a single request.
15328
- * Alias for {@link bulkCreateTerms}.
15813
+ * Alias for {@link TermsClient.bulkCreateTerms}.
15329
15814
  *
15330
- * @param request - Bulk creation request.
15331
- * @returns Bulk creation result with term UUIDs.
15815
+ * @param request - The rows, plus `dryRun` and `partial`.
15816
+ * @param requestOptions - Per-call request options.
15817
+ * @returns What was written, exactly as {@link TermsClient.bulkCreateTerms}
15818
+ * returns it.
15819
+ * @throws {@link BulkRefusedError} 422 when nothing was written.
15820
+ *
15821
+ * @remarks
15822
+ * **Serialization format: Tagged (`ValueDto`).** Same call, friendlier name
15823
+ * — read {@link TermsClient.bulkCreateTerms} for the three outcomes and for
15824
+ * what a dry run does and does not report.
15332
15825
  *
15333
- * @see bulkCreateTerms
15826
+ * @example
15827
+ * ```typescript
15828
+ * const result = await client.terms.createMany({ terms: rows, partial: true });
15829
+ * ```
15830
+ *
15831
+ * @see {@link TermsClient.bulkCreateTerms}
15334
15832
  */
15335
15833
  async createMany(request, requestOptions) {
15336
15834
  return this.bulkCreateTerms(request, requestOptions);
@@ -15361,7 +15859,7 @@ var TermsClient = class {
15361
15859
  return paginateByOffset(
15362
15860
  async (window, perCall) => {
15363
15861
  const page = await this.listTerms({ ...query, ...window }, perCall);
15364
- return { items: page.terms };
15862
+ return page.total === void 0 ? { items: page.terms } : { items: page.terms, total: page.total };
15365
15863
  },
15366
15864
  options,
15367
15865
  requestOptions
@@ -15369,10 +15867,6 @@ var TermsClient = class {
15369
15867
  }
15370
15868
  };
15371
15869
  function convertFeatures(features) {
15372
- const values = Object.values(features);
15373
- if (values.length > 0 && values.every(isTaggedValueDto)) {
15374
- return features;
15375
- }
15376
15870
  return toTaggedFeatures(features);
15377
15871
  }
15378
15872
 
@@ -15423,6 +15917,14 @@ function TermInputDtoFromFrontToApi(model) {
15423
15917
  if ("termId" in model) {
15424
15918
  return { term_id: model.termId };
15425
15919
  }
15920
+ if ("designator" in model) {
15921
+ return {
15922
+ designator: {
15923
+ sort_name: model.designator.sortName,
15924
+ features: model.designator.features
15925
+ }
15926
+ };
15927
+ }
15426
15928
  if ("sortId" in model) {
15427
15929
  const inline = {
15428
15930
  sort_id: model.sortId,
@@ -15495,6 +15997,7 @@ function ProofDtoFromApiToFront(dto) {
15495
15997
  kind: dto.kind,
15496
15998
  goalTermId: dto.goal_term_id,
15497
15999
  ruleTermId: dto.rule_term_id,
16000
+ ruleHeadTermId: dto.rule_head_term_id,
15498
16001
  goalDisplay: dto.goal_display,
15499
16002
  ruleLabel: dto.rule_label,
15500
16003
  substitution: HomoiconicSubstitutionDtoFromApiToFront(dto.substitution),
@@ -15593,7 +16096,7 @@ function ForwardChainResponseFromApiToFront(dto) {
15593
16096
  iterations: dto.iterations,
15594
16097
  totalFacts: dto.total_facts,
15595
16098
  materializationTimeMs: dto.materialization_time_ms,
15596
- persistedCount: dto.persisted_count,
16099
+ keptCount: dto.kept_count,
15597
16100
  provenanceTags: dto.provenance_tags?.map(ProvenanceTagDtoFromApiToFront)
15598
16101
  };
15599
16102
  }
@@ -16030,7 +16533,7 @@ function BackwardChainRequestFromFrontToApi(model) {
16030
16533
  function ForwardChainRequestFromFrontToApi(model) {
16031
16534
  return {
16032
16535
  initial_facts: model.initialFacts?.map(TermInputDtoFromFrontToApi),
16033
- persist_derived: model.persistDerived,
16536
+ keep_derived: model.keepDerived,
16034
16537
  enable_provenance_tags: model.enableProvenanceTags,
16035
16538
  max_iterations: model.maxIterations,
16036
16539
  max_facts: model.maxFacts,
@@ -16374,6 +16877,14 @@ var InferenceClient = class {
16374
16877
  *
16375
16878
  * The `timeout_ms` field on the request is a wall-clock timeout for the search.
16376
16879
  * When it fires, the backend returns whatever solutions have been found so far.
16880
+ *
16881
+ * To join a proof node to the thing it proves, read `proof.goalTermId` — the
16882
+ * conclusion's term ID, which `client.terms.getTerm()` and
16883
+ * `client.query.findBySort()` both answer for. It is OPTIONAL: a goal proved
16884
+ * without a preceding forward chain materialises no conclusion, so the node
16885
+ * carries no id and `proof.goalDisplay` renders it instead. Do not read
16886
+ * `proof.ruleHeadTermId` as a fetchable id — it names the rule's instantiated
16887
+ * head, which the term routes refuse.
16377
16888
  */
16378
16889
  async backwardChain(request, requestOptions) {
16379
16890
  const wireRequest = {
@@ -16393,7 +16904,19 @@ var InferenceClient = class {
16393
16904
  * Forward chaining starts from existing facts and applies rules to derive new facts,
16394
16905
  * repeating until no more new facts can be derived (fixpoint) or limits are reached.
16395
16906
  *
16396
- * If `persist_derived` is true, derived facts are permanently saved to the database.
16907
+ * `keepDerived: true` keeps the run's derivations RESIDENT so a later `MATCH`
16908
+ * reads them. A default run is rolled back, not merely unwritten — so this
16909
+ * route reports what it derived and leaves the store as it found it. Neither
16910
+ * setting is durable: to repair a tenant whose materialised set has drifted,
16911
+ * use OSFQL `CHAIN;` or {@link AdminClient.rebuildDerivedFacts}.
16912
+ *
16913
+ * `timeoutMs` is a server-side deadline in milliseconds, checked at every
16914
+ * fixpoint boundary and every rule application. Omitted means the engine's own
16915
+ * backstop (`OSFKB_FC_TIMEOUT_SECS`, 300 s by default); `0` opts out entirely.
16916
+ * The route answers `504` when the deadline passes, and the body says whether
16917
+ * a `keepDerived` run kept the partial derivation it had reached.
16918
+ *
16919
+ * @throws {ApiError} With status 504 when the derivation passed its deadline.
16397
16920
  */
16398
16921
  async forwardChain(request, requestOptions) {
16399
16922
  const wireRequest = {
@@ -16465,13 +16988,37 @@ var InferenceClient = class {
16465
16988
  /**
16466
16989
  * Run negation-as-failure (NAF) proof search.
16467
16990
  *
16468
- * @param request - NAF prove request.
16991
+ * @param request - NAF prove request. Each literal's `term` takes a
16992
+ * {@link psi} term or a wire {@link TermInputDto}, like every other
16993
+ * term-carrying method.
16469
16994
  * @returns NAF proof result.
16470
16995
  *
16996
+ * @remarks
16997
+ * Uses the untagged (homoiconic) serialization format: a literal's features
16998
+ * are plain scalars and `"?Var"` strings.
16999
+ *
17000
+ * @example
17001
+ * ```typescript
17002
+ * const result = await client.inference.nafProve({
17003
+ * literals: [
17004
+ * { term: psi('employee', { name: '?Name' }) },
17005
+ * { term: psi('senior_engineer', { name: '?Name' }), negated: true },
17006
+ * ],
17007
+ * maxSolutions: 10,
17008
+ * });
17009
+ * ```
17010
+ *
16471
17011
  * @see proveWithNegation — friendlier alias for this method.
16472
17012
  */
16473
17013
  async nafProve(request, requestOptions) {
16474
- const response = await this.api.nafProve(NafProveRequestFromFrontToApi(request), toRequestParams(requestOptions));
17014
+ const wireRequest = {
17015
+ ...request,
17016
+ literals: request.literals?.map((literal) => ({
17017
+ ...literal,
17018
+ term: convertTermArg(literal.term)
17019
+ }))
17020
+ };
17021
+ const response = await this.api.nafProve(NafProveRequestFromFrontToApi(wireRequest), toRequestParams(requestOptions));
16475
17022
  return NafProveResponseFromApiToFront(response.data);
16476
17023
  }
16477
17024
  /**
@@ -16693,6 +17240,7 @@ function FindBySortRequestFromFrontToApi(model) {
16693
17240
  sort_name: model.sortName ?? void 0,
16694
17241
  filter: model.filter ?? void 0,
16695
17242
  limit: model.limit ?? void 0,
17243
+ offset: model.offset ?? void 0,
16696
17244
  include_derived: model.includeDerived
16697
17245
  };
16698
17246
  }
@@ -16833,11 +17381,97 @@ var QueryClient = class {
16833
17381
  return response.data.results.map(TermDtoFromApiToFront);
16834
17382
  }
16835
17383
  /**
16836
- * Find terms by sort ID, sort name, or with optional filter.
17384
+ * Browse a sort and everything below it, one page at a time.
16837
17385
  *
16838
- * @param request - Query by sort request. Accepts sort_id (UUID),
16839
- * sort_name (human-readable), and optional filter for feature-based filtering.
16840
- * @returns Array of matching terms (tagged ValueDto format).
17386
+ * @param request - `sortId` (UUID) or `sortName`, an optional feature
17387
+ * `filter`, the `limit`/`offset` window, and `includeDerived`.
17388
+ * @param requestOptions - Per-call request options.
17389
+ * @returns The page in `terms`, its length in `count`, the size of the whole
17390
+ * answer in `total`, and an engine remark in `note`.
17391
+ * @throws {@link ApiError} 404 when no sort of that name is queryable for
17392
+ * the tenant.
17393
+ *
17394
+ * @remarks
17395
+ * **Serialization format: Tagged (`ValueDto`).** This is the polymorphic
17396
+ * browse: a query on a sort answers that sort AND every subsort of it, which
17397
+ * is what distinguishes it from {@link TermsClient.listTerms}'s exact
17398
+ * `sortName` filter.
17399
+ *
17400
+ * **Page off `total`.** `count` is this page's length. `total` is the number
17401
+ * of rows matched BEFORE `offset` and `limit`, counted rather than estimated.
17402
+ *
17403
+ * **A page is stable.** The answer is ordered by term id before the window
17404
+ * is applied, so page 2 neither repeats nor skips a row of page 1. Measured
17405
+ * 2026-09-18 against a 2-member sort:
17406
+ * `POST /api/v1/query/by-sort {"sort_name":"widget","include_derived":true}`
17407
+ * answered ids `829ef5dc-…` then `d4f7a4f8-…` with
17408
+ * `{"count":2,"total":2}`; the same request plus `{"limit":1,"offset":1}`
17409
+ * answered `d4f7a4f8-…` alone with `{"count":1,"total":2}` — the second row,
17410
+ * and the same total.
17411
+ *
17412
+ * **The route does not chain.** It answers the tenant's durable extension
17413
+ * UNIONED with the conclusions this process currently HOLDS. After a restart
17414
+ * the derived half is empty until something chains again. So when the page
17415
+ * is empty, read `note` rather than trusting `total: 0`: measured
17416
+ * 2026-09-18, a tenant with `widget`, subsort `premium_widget`, one `widget`
17417
+ * fact and the rule `widget(name: ?N) → premium_widget(name: ?N)`,
17418
+ * `{"sort_name":"premium_widget"}` answered
17419
+ * `{"terms":[],"count":0,"total":0,"note":"`premium_widget` is a rule
17420
+ * conclusion sort: its members are derived … Materialise them with OSFQL
17421
+ * CHAIN, POST /api/v1/inference/forward-chain, or POST
17422
+ * /api/v1/admin/derived-facts/rebuild/{tenant_id} … An empty answer here
17423
+ * does not mean the sort has no members."}`. Before the rule existed, the
17424
+ * same empty query carried no note.
17425
+ *
17426
+ * @example
17427
+ * ```typescript
17428
+ * const page = await client.query.findBySortPage({
17429
+ * sortName: 'sales_order',
17430
+ * includeDerived: true,
17431
+ * limit: 25,
17432
+ * offset: 0,
17433
+ * });
17434
+ * if (page.terms.length === 0 && page.note) console.info(page.note);
17435
+ * console.log(`${page.count} of ${page.total}`);
17436
+ * ```
17437
+ *
17438
+ * @see {@link QueryClient.findBySort} — the deprecated array-returning form.
17439
+ */
17440
+ async findBySortPage(request, requestOptions) {
17441
+ const response = await this.api.findBySort(
17442
+ FindBySortRequestFromFrontToApi(request),
17443
+ toRequestParams(requestOptions)
17444
+ );
17445
+ return TermListResponseFromApiToFront(response.data);
17446
+ }
17447
+ /**
17448
+ * Browse a sort and everything below it, discarding the envelope.
17449
+ *
17450
+ * @deprecated Use {@link QueryClient.findBySortPage}, which returns the
17451
+ * engine's envelope. This method drops `count`, `total` and `note`, so a
17452
+ * caller cannot tell a full answer from a truncated one, cannot page, and
17453
+ * reads an empty array for a rule-conclusion sort with no way to see the
17454
+ * engine's explanation. It is kept so 1.27 callers keep compiling.
17455
+ *
17456
+ * @param request - `sortId` or `sortName`, an optional feature `filter`, the
17457
+ * `limit`/`offset` window, and `includeDerived`.
17458
+ * @param requestOptions - Per-call request options.
17459
+ * @returns The page's terms alone.
17460
+ * @throws {@link ApiError} 404 when no sort of that name is queryable for
17461
+ * the tenant.
17462
+ *
17463
+ * @remarks
17464
+ * **Serialization format: Tagged (`ValueDto`).** Identical request,
17465
+ * identical rows, identical ordering — see
17466
+ * {@link QueryClient.findBySortPage} for the measured ordering and paging
17467
+ * contract. The only difference is what is thrown away.
17468
+ *
17469
+ * @example
17470
+ * ```typescript
17471
+ * const terms = await client.query.findBySort({ sortName: 'sales_order' });
17472
+ * ```
17473
+ *
17474
+ * @see {@link QueryClient.findBySortPage}
16841
17475
  */
16842
17476
  async findBySort(request, requestOptions) {
16843
17477
  const response = await this.api.findBySort(FindBySortRequestFromFrontToApi(request), toRequestParams(requestOptions));
@@ -16945,10 +17579,6 @@ var QueryClient = class {
16945
17579
  }
16946
17580
  };
16947
17581
  function convertPattern(pattern) {
16948
- const values = Object.values(pattern.features);
16949
- if (values.length > 0 && values.every(isTaggedValueDto)) {
16950
- return pattern;
16951
- }
16952
17582
  return {
16953
17583
  sortId: pattern.sortId,
16954
17584
  features: toTaggedFeatures(pattern.features)
@@ -31399,6 +32029,11 @@ function OsfqlValueFromApiToFront(value) {
31399
32029
  return typeof payload === "string" ? { type: "string", value: payload } : void 0;
31400
32030
  case "boolean":
31401
32031
  return typeof payload === "boolean" ? { type: "boolean", value: payload } : void 0;
32032
+ // The engine's own RFC 3339 rendering, passed through as the string it is.
32033
+ // Parsing it to a `Date` here would reformat a response, which this layer
32034
+ // never does — and would lose the offset the engine chose to send.
32035
+ case "datetime":
32036
+ return typeof payload === "string" ? { type: "datetime", value: payload } : void 0;
31402
32037
  case "term_ref":
31403
32038
  return typeof payload === "string" ? { type: "term_ref", value: payload } : void 0;
31404
32039
  case "list": {
@@ -31532,6 +32167,7 @@ function OsfqlCatalogEntryFromApiToFront(dto) {
31532
32167
  syntax: dto.syntax,
31533
32168
  examples: dto.examples,
31534
32169
  risk: dto.risk,
32170
+ mutates: dto.mutates,
31535
32171
  execution: OsfqlCatalogExecutionFromApiToFront(dto.execution),
31536
32172
  uiAffinity: {
31537
32173
  display: dto.ui_affinity.display ?? void 0,
@@ -31550,6 +32186,74 @@ function OsfqlCatalogExecutionFromApiToFront(dto) {
31550
32186
  if (dto === "PlanOnly") return { status: "planOnly" };
31551
32187
  return { status: "partial", note: dto.Partial };
31552
32188
  }
32189
+ function OsfqlPreviewSortCountFromApiToFront(dto) {
32190
+ return { sort: dto.sort, count: dto.count };
32191
+ }
32192
+ function OsfqlPreviewAffectedFromApiToFront(dto) {
32193
+ const affected = { count: dto.count };
32194
+ if (dto.exact !== void 0) {
32195
+ affected.exact = dto.exact;
32196
+ }
32197
+ if (dto.sample_rows !== void 0) {
32198
+ const rows = OsfqlBindingsFromApiToFront(dto.sample_rows);
32199
+ if (rows === void 0) {
32200
+ return void 0;
32201
+ }
32202
+ affected.sampleRows = rows;
32203
+ }
32204
+ if (dto.by_sort !== void 0) {
32205
+ affected.bySort = dto.by_sort.map(OsfqlPreviewSortCountFromApiToFront);
32206
+ }
32207
+ return affected;
32208
+ }
32209
+ function OsfqlAtomicRefusalFromApiToFront(dto) {
32210
+ return { code: dto.code, message: dto.message };
32211
+ }
32212
+ function OsfqlPreviewStatementFromApiToFront(dto) {
32213
+ const statement = {
32214
+ index: dto.index,
32215
+ id: dto.id,
32216
+ statement: dto.statement,
32217
+ risk: dto.risk,
32218
+ mutates: dto.mutates,
32219
+ sorts: dto.sorts,
32220
+ source: dto.source
32221
+ };
32222
+ if (dto.affected !== void 0) {
32223
+ const affected = OsfqlPreviewAffectedFromApiToFront(dto.affected);
32224
+ if (affected === void 0) {
32225
+ return void 0;
32226
+ }
32227
+ statement.affected = affected;
32228
+ }
32229
+ if (dto.nested !== void 0) {
32230
+ const nested = [];
32231
+ for (const child of dto.nested) {
32232
+ const parsed = OsfqlPreviewStatementFromApiToFront(child);
32233
+ if (parsed === void 0) {
32234
+ return void 0;
32235
+ }
32236
+ nested.push(parsed);
32237
+ }
32238
+ statement.nested = nested;
32239
+ }
32240
+ return statement;
32241
+ }
32242
+ function OsfqlPreviewResponseFromApiToFront(dto) {
32243
+ const statements = [];
32244
+ for (const entry of dto.statements) {
32245
+ const parsed = OsfqlPreviewStatementFromApiToFront(entry);
32246
+ if (parsed === void 0) {
32247
+ return void 0;
32248
+ }
32249
+ statements.push(parsed);
32250
+ }
32251
+ const response = { mutates: dto.mutates, statements };
32252
+ if (dto.atomic_refusal !== void 0) {
32253
+ response.atomicRefusal = dto.atomic_refusal ? OsfqlAtomicRefusalFromApiToFront(dto.atomic_refusal) : dto.atomic_refusal;
32254
+ }
32255
+ return response;
32256
+ }
31553
32257
 
31554
32258
  // src/resources/osfql.ts
31555
32259
  var OsfqlClient = class {
@@ -31625,6 +32329,107 @@ var OsfqlClient = class {
31625
32329
  }
31626
32330
  return parsed;
31627
32331
  }
32332
+ /**
32333
+ * Preview an OSFQL program: what each statement would do, decided without
32334
+ * running anything.
32335
+ *
32336
+ * @param query - The OSFQL program text (one or more statements separated by `;`).
32337
+ * @param options - Optional request options. Only `atomic` changes the answer:
32338
+ * it decides whether `atomicRefusal` is reported. `reactive`, `maxRows` and
32339
+ * `timeoutMs` are accepted for wire parity with {@link execute} and are
32340
+ * ignored by the route.
32341
+ * @param requestOptions - Per-call transport options (timeout, signal, headers).
32342
+ * @returns The per-statement classification, the affected-row reports, and the
32343
+ * atomic refusal when there is one.
32344
+ * @throws {ApiError} If the program does not parse (HTTP 400, `OsfqlErrorResponse`),
32345
+ * or the request otherwise fails.
32346
+ * @throws {ReasoningLayerError} If a sample row carries a value that does not satisfy
32347
+ * the published {@link OsfqlValue} contract.
32348
+ *
32349
+ * @remarks
32350
+ * **Why call this instead of classifying the program client-side.** Three
32351
+ * answers only the engine's own parse can give:
32352
+ *
32353
+ * 1. **Per-statement `mutates`.** A client keeps no table of which
32354
+ * process-control statements write. Measured on the dev engine: `CHAIN` and
32355
+ * `RELEASE RESIDUATIONS` report `mutates: true`, while `MARK ?m1`, `CUT` and
32356
+ * `SPACE CREATE ?S` report `false` — all five are `risk: "process_control"`.
32357
+ * In the other direction, `MATCH customer(name: ?N) INSERT vip(name: ?N)`
32358
+ * is `risk: "read"` with `mutates: true`. Read `mutates`, never `risk`.
32359
+ * 2. **`affected.count` and `affected.sampleRows`.** Up to 10 rows, each
32360
+ * identifying itself with a `term_id` column and the declaring sort's
32361
+ * `@key` features, so a confirm dialog can say WHICH rows go, not just how
32362
+ * many. Counted by running a derived read-only `MATCH` against a clone of
32363
+ * the tenant. Verified: four destructive previews (`RETRACT`, `CLEAR FACTS`,
32364
+ * `DROP SORT`, `DEFINE`) left the tenant's fact count and its lattice
32365
+ * unchanged.
32366
+ * 3. **`atomicRefusal`.** The same `drop_sort_in_atomic_program` verdict
32367
+ * `POST /api/v1/osfql` would refuse the program with, answered before the
32368
+ * program runs.
32369
+ *
32370
+ * ⚠️ **The one thing a caller must NOT do: treat `affected.count` as the
32371
+ * number of rows the run will remove.** It is the reach of the statement's
32372
+ * PATTERN. For `MATCH customer(name: ?N, spend: ?S); IF ?S > 60 THEN RETRACT
32373
+ * customer(name: ?N);` over three customers of which one has `spend > 60`, the
32374
+ * nested `RETRACT` answers `count: 3` — the derived `MATCH customer(name: ?N)`
32375
+ * applies neither the `IF` condition nor the binding the earlier statement
32376
+ * gives `?N`. Present it as "up to N rows" for a nested statement or one that
32377
+ * reads a variable from an earlier statement.
32378
+ *
32379
+ * Two further measured facts. The top-level `mutates` DOES fold nested
32380
+ * statements now: the `IF` program above answers `true`, and so does an `IF`
32381
+ * whose only writing branch is the `ELSE` — the earlier defect where an `IF`
32382
+ * with a writing branch answered `false` is fixed. And `affected` being absent
32383
+ * is ambiguous: `RETRACT customer(tier: "bronze")` matching zero rows omits
32384
+ * the field entirely rather than answering `count: 0`, exactly as a read does.
32385
+ *
32386
+ * `statement.index` is a single counter over the whole program: an `IF` at
32387
+ * index 1 carries nested entries at index 2 and 3.
32388
+ *
32389
+ * Request serialization matches {@link execute} — `query` plus the optional
32390
+ * `atomic` / `reactive` / `max_rows` / `timeout_ms` keys. The program TEXT is
32391
+ * a string value, so the request bridge's snake_case pass does not touch it;
32392
+ * a `camelCase` feature name inside the query survives verbatim. Sample-row
32393
+ * KEYS are data and are not camelCased on the way back.
32394
+ *
32395
+ * @example
32396
+ * ```typescript
32397
+ * const preview = await client.osfql.preview(
32398
+ * 'MATCH customer(name: ?N); RETRACT customer(tier: "gold");'
32399
+ * );
32400
+ *
32401
+ * console.log(preview.mutates); // true
32402
+ * const writes = preview.statements.filter((s) => s.mutates);
32403
+ * console.log(writes[0].id); // "retract"
32404
+ * console.log(writes[0].risk); // "targeted_destructive"
32405
+ * console.log(writes[0].affected?.count); // 2
32406
+ * console.log(writes[0].affected?.sampleRows); // [{ term_id: …, tier: … }, …]
32407
+ *
32408
+ * // A DROP SORT cannot share an atomic program
32409
+ * const refused = await client.osfql.preview('DROP SORT widget; INSERT widget(label: "x");');
32410
+ * console.log(refused.atomicRefusal?.code); // "drop_sort_in_atomic_program"
32411
+ *
32412
+ * // Ask the same question without the atomic constraint
32413
+ * const loose = await client.osfql.preview(
32414
+ * 'DROP SORT widget; INSERT widget(label: "x");',
32415
+ * { atomic: false },
32416
+ * );
32417
+ * console.log(loose.atomicRefusal); // undefined
32418
+ * ```
32419
+ */
32420
+ async preview(query, options, requestOptions) {
32421
+ const response = await this.api.previewOsfql(
32422
+ OsfqlRequestFromFrontToApi({ ...options, query }),
32423
+ toRequestParams(requestOptions)
32424
+ );
32425
+ const parsed = OsfqlPreviewResponseFromApiToFront(response.data);
32426
+ if (parsed === void 0) {
32427
+ throw new ReasoningLayerError(
32428
+ "osfql/preview returned an affected sample row that does not match the published OsfqlValue contract (expected tagged values with a lowercase `type` discriminator)"
32429
+ );
32430
+ }
32431
+ return parsed;
32432
+ }
31628
32433
  /**
31629
32434
  * Diagnose an OSFQL program for contradictions and inconsistencies.
31630
32435
  *
@@ -35442,38 +36247,6 @@ var SolverClient = class {
35442
36247
  }
35443
36248
  };
35444
36249
 
35445
- // src/normalizers/ontology-alignment.ts
35446
- function AlignOntologyRequestFromFrontToApi(model) {
35447
- return {
35448
- domain_owl: model.domainOwl,
35449
- targets: model.targets
35450
- };
35451
- }
35452
- function AlignmentMatchDtoFromApiToFront(dto) {
35453
- return {
35454
- domainSort: dto.domain_sort,
35455
- matchType: dto.match_type,
35456
- targetCurie: dto.target_curie,
35457
- targetLabel: dto.target_label
35458
- };
35459
- }
35460
- function AlignmentConflictDtoFromApiToFront(dto) {
35461
- return {
35462
- domainSort: dto.domain_sort,
35463
- targetA: dto.target_a,
35464
- targetB: dto.target_b
35465
- };
35466
- }
35467
- function AlignOntologyResponseFromApiToFront(dto) {
35468
- return {
35469
- conflicts: dto.conflicts.map(AlignmentConflictDtoFromApiToFront),
35470
- domainSorts: dto.domain_sorts,
35471
- mappingTtl: dto.mapping_ttl,
35472
- matches: dto.matches.map(AlignmentMatchDtoFromApiToFront),
35473
- targetSorts: dto.target_sorts
35474
- };
35475
- }
35476
-
35477
36250
  // src/resources/ontology-alignment.ts
35478
36251
  var OntologyAlignmentClient = class {
35479
36252
  /** @internal */
@@ -38529,15 +39302,15 @@ function TemporalSeriesPointFromApiToFront(value) {
38529
39302
  }
38530
39303
  const bucketStart = asNumber(value["bucket_start"]);
38531
39304
  const count = asNumber(value["count"]);
38532
- const aggregate = asNumber(value["aggregate"]);
38533
- if (bucketStart === void 0 || count === void 0 || aggregate === void 0) {
39305
+ const aggregate2 = asNumber(value["aggregate"]);
39306
+ if (bucketStart === void 0 || count === void 0 || aggregate2 === void 0) {
38534
39307
  return void 0;
38535
39308
  }
38536
39309
  return {
38537
39310
  bucketStart,
38538
39311
  bucketEnd: asNumber(value["bucket_end"]),
38539
39312
  count,
38540
- aggregate,
39313
+ aggregate: aggregate2,
38541
39314
  aggregateSecondary: asNumber(value["aggregate_secondary"])
38542
39315
  };
38543
39316
  }
@@ -42018,21 +42791,75 @@ var embeddings_exports = {};
42018
42791
  // src/builders/value.ts
42019
42792
  var Value = {
42020
42793
  /**
42021
- * Create a reference to another term by UUID.
42794
+ * Create a reference to another stored term by its UUID, or by the `@key`
42795
+ * values that name it.
42022
42796
  *
42023
- * @param id - The UUID of the referenced term.
42024
- * @returns A tagged `ReferenceValue`: `{"type": "Reference", "value": "uuid"}`.
42797
+ * @param target - The referenced term's UUID, or a {@link ReferenceDesignator}
42798
+ * naming it through the `@key` features of its sort.
42799
+ * @returns A tagged `ReferenceValue`: `{"type": "Reference", "value": "uuid"}`
42800
+ * for the id form, `{"type":"Reference","value":{"sort_name":…,"features":…}}`
42801
+ * for the designator form.
42802
+ * @throws {@link ValidationError} when the designator carries an empty
42803
+ * `sortName`, or names no feature — the engine refuses both, so the builder
42804
+ * refuses them before the round trip.
42025
42805
  *
42026
42806
  * @remarks
42027
- * Serialization format: Tagged (ValueDto). Use with term CRUD, queries, fuzzy operations.
42807
+ * **Serialization format: Tagged (`ValueDto`).** Use with term CRUD, queries
42808
+ * and fuzzy operations. The homoiconic inference endpoints take the untagged
42809
+ * format and have no designator form.
42810
+ *
42811
+ * The two forms differ in what they can be used for:
42812
+ *
42813
+ * - The **UUID form** is what a read answers, and the only form that can
42814
+ * point at a term whose sort declares no `@key`.
42815
+ * - The **designator form** is WRITE-side only. It RESOLVES to a term that
42816
+ * already exists and never mints one: measured on 2026-09-18, writing
42817
+ * a `payment` whose `invoice` feature was
42818
+ * `Value.reference({ sortName: 'invoice', features: { number: { type: 'String', value: 'INV-1' } } })`
42819
+ * stored `{"type":"Reference","value":"4641cbcb-…"}` — the id of the single
42820
+ * existing invoice — and the invoice extent still held exactly one term. A
42821
+ * designator matching nothing is refused `422 no term of sort 'invoice'
42822
+ * carries @key 'number' = "INV-NOPE"; a reference designator names an
42823
+ * existing term, it never creates one`. The same refusal arrives from
42824
+ * `POST /api/v1/terms/bulk` as a `BulkRefusedError` naming the entry index.
42825
+ *
42826
+ * The designator's `features` keys are OSF feature names — user data, not
42827
+ * schema — and survive the request bridge verbatim, so a feature declared
42828
+ * `invoiceNumber` must be spelled `invoiceNumber` here. Sending the
42829
+ * snake_cased spelling is refused: `422 feature 'invoice_number' of sort
42830
+ * 'invoice' is not a @key feature; a designator addresses a term only through
42831
+ * its @key`.
42028
42832
  *
42029
42833
  * @example
42030
42834
  * ```typescript
42835
+ * // By id:
42031
42836
  * Value.reference("550e8400-e29b-41d4-a716-446655440000")
42837
+ *
42838
+ * // By @key, when the caller has the business key and not the UUID:
42839
+ * Value.reference({
42840
+ * sortName: 'invoice',
42841
+ * features: { number: { type: 'String', value: 'INV-1' } },
42842
+ * })
42032
42843
  * ```
42033
42844
  */
42034
- reference(id) {
42035
- return { type: "Reference", value: id };
42845
+ reference(target) {
42846
+ if (typeof target === "string") {
42847
+ return { type: "Reference", value: target };
42848
+ }
42849
+ if (target.sortName.length === 0) {
42850
+ throw new ValidationError(
42851
+ "Value.reference() expects a designator with a non-empty sortName"
42852
+ );
42853
+ }
42854
+ if (Object.keys(target.features).length === 0) {
42855
+ throw new ValidationError(
42856
+ `Value.reference() expects a designator naming at least one @key feature of sort "${target.sortName}"; the engine refuses an empty designator`
42857
+ );
42858
+ }
42859
+ return {
42860
+ type: "Reference",
42861
+ value: { sortName: target.sortName, features: target.features }
42862
+ };
42036
42863
  },
42037
42864
  /**
42038
42865
  * Create a reference to a sort by UUID.
@@ -42771,6 +43598,7 @@ var FuzzyShape = {
42771
43598
  };
42772
43599
 
42773
43600
  // src/builders/psi.ts
43601
+ var NEGATION_SORT = "negation";
42774
43602
  function psi(sortOrName, features) {
42775
43603
  if (typeof sortOrName === "string") {
42776
43604
  if (!features) {
@@ -42799,6 +43627,38 @@ function bind(name, term) {
42799
43627
  }
42800
43628
  return { ...term, binding: name };
42801
43629
  }
43630
+ function not(clause) {
43631
+ if ("sortName" in clause && clause.sortName === NEGATION_SORT) {
43632
+ throw new ValidationError(
43633
+ `not() cannot negate a negation: the chainers read the "${NEGATION_SORT}" carrier one clause deep, so a nested negation is accepted and derives nothing`
43634
+ );
43635
+ }
43636
+ return {
43637
+ __psiTerm: true,
43638
+ sortName: NEGATION_SORT,
43639
+ features: { clause }
43640
+ };
43641
+ }
43642
+ function aggregate(spec) {
43643
+ if (spec.groupBy.length === 0) {
43644
+ throw new ValidationError(
43645
+ "aggregate() expects at least one groupBy feature; an aggregator with no group key has no group to fold into"
43646
+ );
43647
+ }
43648
+ const blank = spec.groupBy.find((name) => name.trim().length === 0);
43649
+ if (blank !== void 0) {
43650
+ throw new ValidationError("aggregate() expects every groupBy entry to be a feature name");
43651
+ }
43652
+ if (spec.target.trim().length === 0) {
43653
+ throw new ValidationError("aggregate() expects a target feature name");
43654
+ }
43655
+ if (spec.groupBy.includes(spec.target)) {
43656
+ throw new ValidationError(
43657
+ `aggregate() cannot aggregate "${spec.target}" and also group by it`
43658
+ );
43659
+ }
43660
+ return { groupBy: [...spec.groupBy], op: spec.op, target: spec.target };
43661
+ }
42802
43662
  function constrained(name, constraint) {
42803
43663
  return { __constrainedVar: true, name, constraint };
42804
43664
  }
@@ -43483,6 +44343,7 @@ exports.AuthenticationError = AuthenticationError;
43483
44343
  exports.Authz = authz_exports;
43484
44344
  exports.BadRequestError = BadRequestError;
43485
44345
  exports.Batch = batch_exports;
44346
+ exports.BulkRefusedError = BulkRefusedError;
43486
44347
  exports.CDL = cdl_exports;
43487
44348
  exports.Causal = causal_exports;
43488
44349
  exports.Chase = chase_exports;
@@ -43594,6 +44455,7 @@ exports.Visualization = visualization_exports;
43594
44455
  exports.WebSocketClient = WebSocketClient;
43595
44456
  exports.WebSocketConnection = WebSocketConnection;
43596
44457
  exports.WebhookActions = webhook_actions_exports;
44458
+ exports.aggregate = aggregate;
43597
44459
  exports.allen = allen;
43598
44460
  exports.bind = bind;
43599
44461
  exports.collect = collect;
@@ -43604,6 +44466,7 @@ exports.isConstrainedPlainVar = isConstrainedPlainVar;
43604
44466
  exports.isPsiTermInput = isPsiTermInput;
43605
44467
  exports.isTaggedValueDto = isTaggedValueDto;
43606
44468
  exports.isUuid = isUuid;
44469
+ exports.not = not;
43607
44470
  exports.paginateByOffset = paginateByOffset;
43608
44471
  exports.paginateByPage = paginateByPage;
43609
44472
  exports.psi = psi;