Skip to content

Changelog

All notable changes to this project are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Built against engine main 95caf5925. No breaking change.

  • OSFQL editor tokens (reasoninglayer-ts-sdk#60): client.osfql.tokens(query) calls POST /api/v1/osfql/tokens and answers OsfqlTokensResponse, the editor tokens of the source in UTF-16 coordinates, each with an OsfqlTokenKind.
  • Resumable RDF uploads (reasoninglayer-ts-sdk#60), every route under /api/v1/ingest/rdf/uploads: client.ingestion.createRdfUpload, listRdfUploads, getRdfUpload, uploadRdfChunk, completeRdfUpload and pauseRdfUpload, with the types RdfUpload, CreateRdfUploadRequest, RdfUploadChunkReceipt and RdfUploadPauseReceipt. uploadRdfChunk(id, offset, chunk) sends the chunk as raw application/octet-stream bytes. A resent chunk with the same bytes answers the same receipt; different bytes at an offset already stored are refused.
  • PatternDiscoveryRequest.continuation on the generated transport (discoverPatternProposals, exportPatternContext): send the continuation of the previous complete-mode page, with the same scope and filters, to receive the next page. The engine answers 409 when the scope or filters changed.
  • The transport layer and its doc comments follow the engine spec at 95caf5925: attribute-exploration completion registers its appropriateness sorts before the terminal commit and answers the committed receipt again on retry; a confirm/refute answer commits only on the session version it was checked against; a pipeline stage’s metrics count what its basis names.

Built against engine main feafe936f (reasoninglayer#584 E0-E11, engine !400-!411, and the engine changes merged since cfd780622). Two parts: the #584 S1 resync (truthful pipeline stages, completeness fields) and the governed controlled vocabulary resource, client.vocabulary (#584 S2).

Each item names one BREAKING change. The items marked (run time) change behaviour that the compiler does not report: grep for them.

Pipeline stages are assessed, not counted

  • Stop reading PipelineStageStatus.complete. The engine removed it (engine #584 C0.2): it was derived from counts, so a flat sort list with two levels read as a “complete taxonomy”. There is no replacement flag and no alias. Read state ('not_assessed' | 'stale' | 'accepted'), what it rests on in basis (discriminated by kind: 'legacy_sort_metrics', or 'vocabulary_release' with releaseId, releaseNumber, assessmentId, contextId, assessedAt), and the reason in words. Only the vocabulary stage can leave not_assessed, from an active governed vocabulary release whose bound assessment is complete and passed; the other five stages are always not_assessed. metrics are unchanged and decide nothing.

    // 4.x
    const done = status.stages.filter((s) => s.complete).length;
    // 5.0.0
    const accepted = status.stages.filter((s) => s.state === 'accepted');
    for (const s of status.stages) console.log(s.stage, s.state, s.reason);
  • OntologyPipelineStage is a closed union of the six stages; the open string arm is gone, because the engine now declares the stage as an enum.

  • Handle a 503 from ontology.pipelineStatus() (run time): a VocabularyRefusedError with code: 'vocabulary_not_committed' means the vocabulary store could not be read; nothing is served partially. It is not a ServiceUnavailableError: the body is a vocabulary refusal, and every such body is read as one.

Results that were not complete

  • Handle OptimizationResult status: 'undetermined' (run time). optimize.solve() and fromKnowledgeBase() reported infeasible for every empty backward-chain answer, including one from a bounded search. infeasible now requires the engine to report a complete derivation; an empty answer from a bounded or pending derivation is undetermined (UndeterminedResult). An exhaustive switch on status needs the new arm.
  • Read the completeness before reading an empty answer (run time): BackwardChainResponse.derivationComplete / openWorldSearch, FuzzyProveResponse.derivationComplete / openWorldSearch, NafProveResponse.derivationComplete, BayesianPredictResponse.derivationComplete, AuthzCheckResponse.derivationComplete, NlQueryResponse.derivationComplete, ForwardChainResponse.completion, OsfqlResponse.chainCompletions / proveCompletions, EvaluateCompetencyQuestionsResponse.complete / incomplete, CheckDocumentResponse.complete / passes, SortDiscoveryResponse.incompleteReason / inputComplete / search, the exploration search reports, SolveConstraintResponse.unsatisfiable, CoherenceReport.unfinishedDerivationRuns and TemporalSeriesResponse.selectionComplete. The SDK dropped all of them before.

Unions that gained an arm (an exhaustive switch needs it)

  • ProofKind 'truncated', CoherenceVerdict 'undetermined' (with checked and chainIterations; never a certificate), CompetencyQuestionVerdict 'undetermined', CronJobStatus 'executing', CronJobRunStatus 'interrupted', AudioJobStatus 'interrupted', RememberOutcome 'unresolved_contradiction', FindingKind 'scope_incomplete', OutputPredicate { TracePredicate }, DocumentSource / AudioSource { stored: { id } }, and the code unions TermWriteErrorCode / SortCreateErrorCode 'governed_sort_write_refused'.

Request and response types that tightened

  • Set DiscoveryConfigDto.changeMode ('absolute' | 'relative') whenever you send a discovery config: the engine’s published request contract requires it, and the SDK does not pick a change quantity for you. Send no config to use the engine defaults. This requirement comes from a defect in the engine’s spec, not from the engine’s behaviour: the engine accepts a config without change_mode and applies absolute (reasoninglayer#593). A later release makes changeMode optional again once the spec is fixed.
  • Read DiscoverEffectsResponse.discoveredHorizons as camelCase (supportWeight, sampleCount); HorizonDto.confidence and EffectPredictionDto.confidence are optional and always null for this estimator.
  • Add the new required fields to every object you build by hand (test fixtures): PipelineStageStatus.state / basis / reason; OntologyQualityReport.competencyAssessment; EvaluateCompetencyQuestionsResponse.complete / incomplete; OsfqlResponse.chainCompletions / proveCompletions; ValidatedUnifyResponse.witnessesFailed; AuthzCheckResponse.decisionTime; CheckDocumentResponse.complete / passes; SortDiscoveryResponse.incompleteReason / inputComplete / inputStopReason / search / registrations; the exploration responses’ search; IntegratedCycleOutcomeDto.goalsDeferred / simulationResults; CycleOutcomeSummaryDto.goalsDeferred; AgentBeliefDto.shared; CronJobRun.origin; RunCronJobNowResponse.runId; InstallSummary.bundleId; GlbResponse.maximalLowerBounds; LubResponse.minimalUpperBounds; SolveConstraintResponse.unsatisfiable / violatedConstraintIds / suspendedConstraintIds; CoherenceLink.scope; CoherenceReport.unfinishedDerivationRuns; ConversationMessageResponse.frameAmbiguities / frameRetries / gateAbstained; TemporalSeriesResponse.selector / selectionComplete; DetectCommunitiesResponse.graphObservation / levels / config / hierarchyStopReason and graphObservation on the centrality, cohesion, path and link-prediction responses; the DiscoverEffectsResponse and PredictFromDiscoveryResponse accounting fields; and VerdictKind ProvenSafe.binding. Code that only reads them is unaffected.
  • Read SortDiscoveryResponse.fuzzyLevels as FuzzyConceptLevel[]; it was unknown[].
  • Catch ResponseShapeError where a schema-erased field breaks the engine’s documented shape (run time): the discovery accounting, the community graph observation, the echoed term-set selector, the certificate binding and the extraction-worker execution / graph are now read field by field, and a body that does not match fails loudly instead of reaching you half-read.

The governed controlled vocabulary: client.vocabulary (engine #584 E9/E10, every route under /api/v1/vocabulary)

  • A vocabulary change is a candidate of immutable revisions of knowledge-organization records (schemes, concepts, definitions, label uses, memberships, evidence, source groups, authority references, identity decisions, aboutness). It is assessed, submitted, reviewed and approved by a steward who did not author it, released as an immutable release, and activated: activation swaps the tenant’s active-release pointer (compare-and-set) and materializes the records as Psi-terms of the com.kortexya.knowledge-organization plugin in one transaction. Approval and release change neither the active release nor the terms; nothing creates a sort, a parent edge or a rule.
  • 31 methods, one per route: status, active, history, listSchemes, listConcepts, getConcept, conceptHistory, search, createCandidate, listCandidates, getCandidate, saveRevision, getRevision, diffRevision, rebase, assess, getAssessment, submit, review, approve, reject, withdraw, listDecisions, release, listReleases, getRelease, listReleaseRecords, activate, getRequestOutcome, getContext, migrationPreview. Every 2xx is decoded through decodeResponse.
  • Typed discriminated unions for the change set: VocabularyRecordInput (by kind), VocabularyRecordRef (by by: record or client_ref), VocabularyRecordOperation (by op: upsert or remove) and the stored VocabularyRecordView. Reads take a VocabularyViewSelector (active, release + releaseId, candidate + revisionId); a candidate view says draftNotActive: true. Closed literal unions for every code, state and severity (VocabularyRefusalCode, VocabularyFindingCode, VocabularyFindingSeverity, VocabularyCandidateState, VocabularyReleaseState, VocabularyRecordErrorCode, …). The types are also under the Vocabulary namespace.
  • Idempotency. Every mutation takes a required idempotencyKey in its options (VocabularyMutationOptions); the SDK never generates one, so bind it to a variable before the call and pass the same variable on every retry. An identical repeat answers the committed outcome with replayed: true. Save and rebase also take a required ifMatch (the head revision id or its ETag; VocabularyRevisionMutationOptions). New idempotencyParams(key, options?, ifMatch?) in request-params.ts, beside ifMatchParams.
  • Acting user. Every mutation is made by the client’s authenticatedUser (X-Authenticated-User: user:<uuid>). An author and an approver are two clients.
  • Refusals (engine: every vocabulary 4xx except 404 and 412 answers VocabularyRefusalResponse): VocabularyRefusedError with the closed code, current, findings, conflicts, authority, plugin and the whole typed refusal (recordErrors, findingIds, missingRelations, storedRequestHash, rebaseUrl, recovery). Subclasses IdempotencyKeyRequiredError (428), IdempotencyKeyReusedError (422, storedRequestHash) and MutationOutcomeUnconfirmedError (503, recovery: never conclude that nothing was written; read getRequestOutcome or repeat with the same key). getRequestOutcome decodes the stored body as the operation’s own typed result (VocabularyCommittedResult, discriminated by operation); a stored body that is not that response throws ResponseShapeError. invalid_request arrives with 400, 413, 415 or 422; the activation 500 materialized_vocabulary_drift is a VocabularyRefusedError too. A 404 for an unknown record stays a NotFoundError, and a stale If-Match stays a PreconditionFailedError (currentEtag names the head). vocabularyRefusalOfBody(body) reads a refusal from any error body.
  • VocabularyFindingDispositionInput.rationale is required (engine E9).
  • Contract suite tests/contract/vocabulary.contract.test.ts runs the M1 first slice (base_create, v11_definition_change) on a fresh, seeded tenant and erases it; pnpm run capture-fixtures --vocabulary records verbatim engine bodies for the unit tests.

The S1 resync

  • ontology.theoryFingerprint() (GET /api/v1/ontology/theory/fingerprint, engine #584 C0.2): digests of the tenant’s own sort declarations and rules (definition, declarationSet, ruleSet, excluded). Plugin and engine contributions are excluded and only counted, so a plugin install changes neither digest.
  • OntologyQualityReport.competencyAssessment (state: 'none_stated' | 'not_run', acceptedQuestions): why competencyPassRate is absent. The report runs no question.
  • EvaluateCompetencyQuestionsResponse.complete and incomplete, and the undetermined verdict.
  • Typed WorkerProposal on ExtractEntitiesResponse.workerProposal (engine #584 W1.0), with execution and graph as Readonly<Record<string, unknown>>, and ExtractEntitiesRequest.worker ({ sourceId, sourceRevision }) to request one. The proposal is advisory: nativeAdmission is always false.
  • Completeness: OpenWorldSearchCompletion / OpenWorldLimit, ForwardChainCompletion / FactIdentityIssue, OsfqlChainCompletion / OsfqlProveCompletion / OsfqlRowVerdict, WitnessFailure, AggregationEvidence on ProofDto.aggregation, ExplorationSearchReport, FcaSearchSummary, SortRegistration.
  • OsfqlRequest.inference: recompute deductions in a private read snapshot without publishing a change.
  • AuthzCheckRequest.decisionTime and AuthzCheckResponse.decisionTime.
  • analysis.advanceExploration(sessionId, bounds?): resume a suspended exploration search. Start, confirm and refute take maxWork, maxCandidates and searchTimeoutMs; sort discovery takes maxWork, maxVisitedConcepts and maxSourceScan.
  • ingestion.resumeAudioJob(jobId) for an interrupted audio job; originalFileId on parsed-document, audio and image-extraction metadata; { stored: { id } } document and audio sources that reuse a retained original.
  • Discovery: targetUnit, candidateHorizons, minHorizonScore, asOf, and the full window, observation, outcome and support accounting (ChangeAccounting, EffectAccounting, DiscoveryWindow, DiscoveryObservation, ForwardChange, PredictionSupport).
  • Communities: hierarchical detection (hierarchical, maxLevels, seed, graphView), CommunityDto.parentCommunityId, CommunityLevel, HierarchyStopReason, ResolvedCommunityDetectionConfig, and the ReferenceGraphObservation every graph metric now reports.
  • Conversation frame mode: ConversationMessageRequest.queryMode / queryModel / contextTermIds, and frameMode, frameAmbiguities, frameRecordId, frameRetries, gateAbstained, queryFrame on the response.
  • GlbResponse.maximalLowerBounds and LubResponse.minimalUpperBounds; SolveConstraintResponse refutation fields; CoherenceLink.scope (ClauseComparisonScope); CronJobRun origin and interruption instants; RunCronJobNowResponse.runId; RememberResult.contradictionCheckIncomplete; AgentBeliefDto.shared and AddBeliefRequest.shared; InstallSummary.bundleId; EnrichedHealthResponse.sovereign; artifactFilesDeleted on the tenant clear and factory reset; EvaluateFunctionRequest.expectedResult; CertificateBinding / ClaimKind on a ProvenSafe verdict.
  • Marketplace.MarketplaceErrorCode: plugin_not_persisted, plugin_has_governed_data, governed_data_unverified, plugin_sort_identity_changed.
  • pnpm run check-normalizer-drops runs in test:ci.
  • Doc comments follow the engine: @governed sorts refuse every client write and delete door with governed_sort_write_refused (term routes 409, OSFQL 422 refusal code); a system-wide marketplace operation is durable-first and answers 503 plugin_not_persisted when the registry did not store it; disable and uninstall refuse a plugin whose @governed sorts hold records.
  • Cron docs follow the engine’s current scheduler (run time): timezone accepts only UTC (any other zone is a 400), and jitterMs and maxRetries are reserved (a nonzero value is a 400; retries default to 0). cron.runNow refuses a job whose agent is missing, paused or stopped, whose source is not registered, or that has no program registration, with 409.
  • A cognitive cycle (basic or integrated) on a tenant that holds a native agent belief is refused with 422 agent_belief_operation_unsupported and changes nothing (run time). New Cognitive.AgentBeliefErrorCode names the four belief refusal codes.
  • The reason a bulk action-review decision gives for an unknown review is Not found: Review <id> not found; another tenant’s sort as a parent answers Sort not found: SortId(<id>), byte-identical to an unknown id.
  • The cognitive-agent contract suite runs on its own tenant and deletes it afterwards; the g6a cron tests create a real agent for runNow.

The regenerated transport layer carries these engine routes; no SDK method wraps them in this change: query models, the authoring context and application plan, the inbound-webhook hook programs, chunked file upload, the heartbeat simulator, the OSFQL capabilities / token / query-generation routes, the frame execute and frame schema routes, the pattern-proposal routes, completeJoin / completeMeet, model discovery and validation, the temporal profile, UI validation, the egress and sovereignty reads, the source-file download, and the approver-role revocation.

Built against engine main cfd780622 (engine !321: #475-#480, !322: #481-#487).

  • conversation.getConversation(id) (engine #478): reads one conversation’s summary, with its kind and title, without reading the whole listing.
  • ConversationMessageResponse.notices (engine #477): what the engine did to the question before it answered, typed by kind (pruned_features, value_grounding, retrieval_rescue). New types ConversationNotice, SkippedFeature and ValueSubstitution. The notice sentences no longer appear in suggestions, which holds follow-up suggestions only. repaired and originalOsfql are now also set by a prune, a grounding, the retrieval rescue and the no-code builder.
  • ProofDto.conclusion and SolutionDto.conclusion (engine #476): the conclusion as a structured term, ProofConclusionDto (sortId, sortName, termId, tagged features). Read the sort and features here, not from goalDisplay.
  • UIAssemblyStatsDto.dataLoadError and UIGenerateResponse.dataLoadErrors (engine #475): a failed data load as a typed DataLoadError (code, message, query).
  • SessionDocument.errorCode (engine #479): the typed reason a session document failed, SessionDocumentFailureCode.
  • DocumentStatsDto.entitiesRefused (engine #480): the entities the declaration verdict refused. It is on the session stats, the document stats and the markdown receipt.
  • BulkSortErrorKind gains amendment_refused (engine #482): an amendment the lattice refused (a contradicted subsort refinement, a subtree over the cap, a parent edge closing a cycle). It answers sortCreated: true.
  • DocumentResult.summary doc follows engine #487: the summary is deterministic engine text, not LLM prose.
  • ConversationMessageResponse.notices and DocumentStatsDto.entitiesRefused are required, as on the wire. A fixture you build for either type must carry them.
  • UIAssemblyStatsDto.osfqlExecuted is absent on a failed data load. It no longer starts with FAILED:; read dataLoadError instead.
  • Doc comments follow the engine behaviour of !321 and !322: ui.executeAction update_form edits the term in place (#481), affectedPreview counts a refused statement as 0 (#485), and the rule derivationsRemoved and derivationsWeakened count resident conclusions (#484).
  • inference.addRule, validateRule, bulkAddRules and replaceRule throw a ValidationError naming the field (antecedents, or rules[k].antecedents) for a request without an antecedents array, not a raw TypeError (SDK #58). An empty array stays the engine’s decision.

Built against engine main 308ed2a25 (engine !320: #473, #474).

  • AddRuleResponse.etag and ReplaceRuleResponse.etag (engine #473): inference.addRule and replaceRule fold the ETag response header into the result. replaceRule’s tag is the REPLACEMENT’s. The field is absent when the engine sends no header, which it does only when the read that tags the response fails after the write committed.
  • RuleEntryDto.etag on inference.getRules (engine #473): the tag of each withdrawable rule; absent on a rule no route withdraws.
  • GeneratedScenario.sorts is optional (engine #474). No scenario key is required now: every collection defaults to empty, so a vocabulary import sends sorts alone.
  • GeneratedScenario: the engine no longer reads the snake_case spellings webhook_actions, temporal_constraints, guard_constraints, pipeline_stages and sensor_points (engine #474). It ignores them like any unlisted key, without an error. Send webhookActions, temporalConstraints, guardConstraints, pipelineStages and sensorPoints. materializeScenario sends the scenario verbatim, so the SDK does not rewrite a key in either direction; ontology.generate() returns the camelCase spellings.
  • MaterializeScenarioRequestFromFrontToApi passes the scenario through without a type assertion.
  • inference.deleteRule and replaceRule docs: read a rule’s ifMatch tag from addRule, replaceRule or the getRules listing. They said to read it with terms.getTerm(ruleId), which answers 404 for a rule since engine #456. TermResponse.etag and terms.getTerm docs are corrected the same way. This closes the 4.0.0 known engine gap reasoninglayer#473.
  • Contract test g5a reads the rule’s tag from the addRule result and the listing, not from terms.getTerm.

Built against engine main 5cef4fa34 (v0.1.151-58). 3.1.0 and 3.2.0 shipped without changelog entries; the entries below cover every change since 3.2.0.

Each item names one BREAKING change listed under Changed — BREAKING or Removed — BREAKING below, and what to change in your code. The items marked run time change behaviour that the compiler does not report: grep for them.

Removed calls and fields

  • Drop platform.listBranches and platform.forkBranch, and the imports of ConversationBranch, BranchListResponse and ForkBranchRequest. There is no replacement: the engine removed both routes.
  • Stop reading DeleteGoalResponse.referencesDeleted and SortDeletePreviewResponse.orphanedForeignChildren: the engine no longer sends them. orphanedChildren lists every child a sort delete would orphan.
  • Delete UIActionRequest.originalValues from every UI action, and always send the row’s term UUID as termId.
  • Delete IngestionConfigDto.batchSize and maxSchemaTokens from every ingest call. The engine refuses either key with 422.
  • Delete tenantId and ownerId from AddPendingReviewRequest, tenantId from ReExtractRequest, and reviewedBy from the bulk review requests. Send each review candidate as { termId, similarity }.
  • Stop reading ErrorResponse.details.

Changed signatures

  • authz.deleteTuple(tupleId, options?, requestOptions?): move request options from the second argument to the third.
  • conversation.listConversations(query?, requestOptions?): move request options from the first argument to the second.
  • analysis.confirmImplication(sessionId, { questionId }, requestOptions?): pass the pending question’s ExplorationQuestion.questionId, and move request options to the third argument. Set RefuteRequest.questionId too.
  • reviews.getSummary() takes no tenant id: call getSummary() or getSummary(requestOptions).

Request bodies the engine now checks

  • Give every rule at least one antecedent (addRule, validateRule, replaceRule, bulkAddRules): antecedents is required. Write a rule with no antecedents as a fact, with terms.createTerm.
  • A not(bind('?T', …)) needs a positive antecedent that binds ?T (run time): drop the bind to keep the old “no such term exists” meaning. A nested bind() in a positive feature value now joins on that identity; drop it there too if you did not mean the join.
  • Write each scenario rule as a Scenarios.ScenarioRule ({ head, antecedents?, certainty? }, each term { sort, features?, negated?, binding? }) in GeneratedScenario.rules and UpdateScenarioRequest.addRules.
  • Set RecordTurnRequest.conversationKind to 'guided_ontology' or 'external_agent' only.
  • Use FlowRelOp ('le' | 'ge' | 'eq' | 'lt' | 'gt' | 'ne') in the compare arm of a feasibility BoolExpr, not the old RelOp values.
  • Use SymbolVisibilityDto ('public' | 'private' | 'protected') for a module symbol’s visibility.
  • Narrow the strings you assign to Argument.label (ArgumentLabel), Attack.kind (ArgumentAttackKind), ReExtractRequest.extractionStrategy (ExtractionStrategy), CertificateDetail.verdict / method, UIResultShape, TurnFeedback.rating / ConversationAnswerFeedbackResponse.rating (AnswerRating) and the residuation kind filter (ResiduationKindFilter).
  • Annotate a rename body with PatchSortFeatureRename where you extended or indexed PatchSortFeatureRequest, and narrow with 'name' in req before you read .name.

Response shapes

  • Re-read these responses through their new types, because the old types named keys the engine never sent: OsfSearchResponse.entities / relations (OsfSearchEntityDto, OsfSearchRelationDto), AteEstimateResponse.certificate (DoublyRobustCertificateDto), FormalJudgeResponse.violations (FormalJudgeViolationDto), ExecutionSessionStatsResponse, and the flat PendingReviewDto (now the ingestion one).
  • Read UIDescribeResponse.data as a UIDataPayload: switch total_count / term_ids to totalCount / termIds.
  • Parse CreateConstraintSessionResponse.createdAt as an ISO 8601 string, not a number.
  • Handle null in DocumentAnalysisReport.overallCompliance and DocumentResult.complianceScore: null means no applicable rule was evaluated, or (for a document) that it was read only in part.
  • Filter conversations on c.kind === 'assistant', not on an absent kind: ConversationSummaryDto.kind is required.
  • Add the new arms to every exhaustive switch: the twelve new fuzzy EffectDto arms, ReviewReason 're_extraction', OsfqlErrorType 'lock_timeout' and WaitingConditionType 'variable_binding'. Remove the 'suspended' and 'ready' residuation states, and ResiduationStats.ready / suspended.
  • Move imports of ViolationDto from the Oversight namespace to Generation, and of SortSuggestionDto from Reviews to Cognitive. Read Ingestion.PendingReviewDto where you read Reviews.PendingReviewDto. The flat exports are unchanged.
  • Name IngestionSessionDetailResponse where you named the return type of ingestion.getSession in a position that needs the exact type.
  • Add the new required fields to every object you build by hand (test fixtures): sourceFilesDeleted, inboxItemsDeleted and dmPairingStateDeleted (tenant clear, delete and factory reset); entitiesRefused (IngestionStatsDto); turnId (ConversationMessageResponse); complete (SparqlTranslation); entityTermId and entityOrigin (PendingReviewEntity); documentId (ReExtractResponse); inventionTermId (TrizNoveltyScore); and all seven ConformanceViolationDto fields. Code that only reads them is unaffected.
  • Catch ResponseShapeError and ReasoningLayerError where a malformed response used to pass through (run time): an unknown fuzzy effect arm, an off-scale rating, and an unreadable TRIZ novelty score now throw.

Errors and status codes (run time)

  • Branch on OsfqlError with errorType === 'lock_timeout', not on ServiceUnavailableError / InternalServerError, for a 503 from osfql.execute.
  • Branch on errorType === 'refused' / 400, not 'execution' / 422, for an OSFQL DROP SORT or ALTER SORT … PARENTS beside another statement, or a reparent onto an unknown parent.
  • Move review-decision error branches: BadRequestError / 400 for a decided review becomes ConstraintViolationError / 409, and ForbiddenError / 403 for another tenant’s review becomes NotFoundError / 404.
  • Read a sendMessageStream refusal of an unknown or foreign conversation from its error event (status 404 or 409), not from a thrown error.
  • query.osfSearch entities and relations take the route’s real shape (engine !309). OsfSearchResponse.entities is OsfSearchEntityDto[] (termId, sortId, matchDegree, communityId, features, name?, matchReason?) and relations is OsfSearchRelationDto[] (sourceId, targetId, relationType, confidence, discoveryMethod). The query-area MatchedEntityDto and DiscoveredRelationDto are removed: they named keys the route never sends, so every field read undefined. The flat MatchedEntityDto export is now the conversation one.

  • AteEstimateResponse.certificate is a DoublyRobustCertificateDto (productBiasFactorization, consistentUnderCorrectPropensity, consistentUnderCorrectOutcome, allCertified) on causal.estimateAte and its ATT/DML/TMLE/clustered siblings. It was typed as the oversight CertificateDto, so every field read undefined.

  • FormalJudgeResponse.violations is FormalJudgeViolationDto[] (layer, description, factId, constraintType?) on oversight.judge, refine and session finalize. The old severity and relatedFactId never existed on this route. ViolationDto moves from the Oversight namespace to Generation, the area that sends it; the flat export is unchanged.

  • CreateConstraintSessionResponse.createdAt is an ISO 8601 string, not a number: the engine serializes a UTC date-time.

  • ExecutionSessionStatsResponse is { memorySessionCount, curriedFunctionCount, dbAvailable }. The old { documents, sessionId, total } was the ingestion shape and never matched the route.

  • Module-symbol visibility is SymbolVisibilityDto ('public' | 'private' | 'protected'). The namespace VisibilityDto it used admitted internal and restricted, which the module routes refuse.

  • Solver RelOp is replaced by FlowRelOp ('le' | 'ge' | 'eq' | 'lt' | 'gt' | 'ne') in the compare arm of a feasibility BoolExpr. The old spelled-out values ('less_than_or_equal', …) belong to the constraint solver and were refused by beginFeasibilitySession.

  • Fuzzy EffectDto has all fifteen membership-function arms of the engine’s FuzzyEffectDto (sigmoid, bell, sigmoid_difference, gaussian_product, sigmoid_product, cosine, spike, cauchy, s_shape, z_shape, pi_shape, piecewise_linear, besides gaussian, triangular and trapezoidal). Code that switches exhaustively on PredictEffectResponse.effect must add the new arms. An arm the SDK does not know now throws ResponseShapeError (with the status, method, path and body); before, any arm but the first three was reported as a fabricated { gaussian: { mean: 0, stdDev: 0 } }.

  • UIDescribeResponse.data is a UIDataPayload (columns, rows, totalCount, termIds?), mapped like ui.generate’s payload. It was an untyped object passed through raw, so callers read snake_case total_count / term_ids. Switch those reads to totalCount / termIds.

  • UIResultShape is the closed union of the engine’s Returns enum ('Bindings' | 'TermIds' | 'Scalar' | 'List' | 'Path' | 'Boolean' | 'None'); the open string widening is gone.

  • ConformanceViolationDto fields (subject, object, feature, subjectSort, objectSort, kind, suggestedFixes) are required: the engine publishes the component with all seven required. Only code that constructs one is affected.

  • The cognitive cycle’s SortSuggestionDto moves from the Reviews namespace to Cognitive, the area that returns it. The flat export is unchanged.

  • CertificateDetail.verdict is typed VerdictKindTag ('ProvenSafe' | 'Falsified' | 'EmpiricallySafe' | 'Residuated') and method is ProofMethod; both were string.

  • ErrorResponse loses details: no engine error body carries it.

  • authz.deleteTuple(tupleId, options?, requestOptions?): the second argument is now { ifMatch?: string }; per-call request options move to the third. A caller that passed request options second must move them.

  • The flat PendingReviewDto is the ingestion one (candidates: CandidateMatchDto[] of { termId, similarity }, entityId, reason, sort): the shape the ingest responses carry in pendingReview. The Reviews.PendingReviewDto is removed: it carried ReviewCandidateMatchDto candidates, matched no wire shape, and no normalizer produced it. Read Ingestion.PendingReviewDto, or the flat name.

  • UIActionRequest.originalValues is removed (engine #172). The engine refuses an original_values key with 422, and update_form no longer reads it: it identifies the row by termId alone, which must be a term UUID (else 400). Drop the field, and always send the row’s term id.

  • reviews.getSummary() takes no tenant id (engine !304, #87 #366). The route is now GET /api/v1/reviews/summary, with the tenant taken from the principal; the old /reviews/summary/{tenant_id} is gone. Call getSummary() (or getSummary(requestOptions)). ReviewSummary adds totalApproved, totalRejected, totalCorrected, totalMerged, avgPendingConfidence (0 when nothing is pending) and oldestPending? (RFC 3339), and byReason is now Partial<Record<ReviewReason, number>>; a key outside ReviewReason throws ValidationError. The confidence bands are now computed, no longer a constant 0.

  • Review request bodies lose the fields the engine now refuses with 422 (engine !304). AddPendingReviewRequest drops tenantId and ownerId, adds reasonDetail?, and takes candidates: ReviewCandidateInput[] ({ termId, similarity }; the old full ReviewCandidateMatchDto is refused). ReExtractRequest drops tenantId, and extractionStrategy is an ExtractionStrategy (it was a string); documentId must be a registry document id. BulkApproveRequest, BulkRejectRequest and BulkMergeRequest drop reviewedBy: the reviewer is the principal. Migration: delete those fields from every call, and send each candidate as { termId, similarity }.

  • ReviewReason adds 're_extraction' (engine !304), the reason of an entity a re-extraction found. An exhaustive switch over ReviewReason needs the new arm. Ingestion.PendingReviewDto.reason (the pendingReview entries of the ingest responses) is now a ReviewReason, not a string, and adds reasonDetail?.

  • PendingReviewEntity adds the required entityTermId and entityOrigin (ReviewEntityOrigin: 'proposed' | 'provisional' | 'existing'), and reasonDetail?; ReExtractResponse adds the required documentId, the registry id of the new document version. Only code that builds these objects by hand is affected.

  • An OSFQL DROP SORT or ALTER SORT … PARENTS beside another statement is refused (HTTP 400), not execution (HTTP 422) (engine !305, #365). osfql.execute throws OsfqlError with errorType: 'refused' and the new atomicRefusal (code: drop_sort_not_alone / alter_sort_not_alone, message), the same code osfql.preview reports. An ALTER SORT … PARENTS that names an unknown or foreign parent is also a refused 400 (Cannot reparent sort 'S': parent sort X does not exist); an unknown subject sort stays an execution 422. The type surface does not break, but code that branches on errorType === 'execution' or on status 422 for these programs must move to 'refused' / 400.

  • DeleteGoalResponse.referencesDeleted is removed (engine !306, #288). The engine dropped the delete_references query parameter and the always-0 references_deleted count. inference.deleteGoal(goalId) deletes the goal with every term it owns (its clause, query, constraint and variable terms) and never a tenant fact the goal only references. The response is { deleted, goalId }.

  • ClearTenantResponse, DeleteTenantResponse and FactoryResetResponse add the required sourceFilesDeleted, inboxItemsDeleted and dmPairingStateDeleted (engine !308, #381, #358): the uploaded source files, unified-inbox items and DM pairing records (policies, allowlisted senders and pairing requests, summed) the wipe erased. The mapper dropped all three. rowsDeletedTotal does not count the inbox or pairing records: the engine erases them through their stores before the table sweep. Only code that builds one of these objects by hand must add the fields.

  • Argument.label is ArgumentLabel ('in' | 'out' | 'undecided') and Attack.kind is ArgumentAttackKind ('value_conflict' | 'polarity_conflict' | 'deontic_conflict' | 'rule_derived_conflict'), engine !306 (#76). Both were string; the wire values did not change. Only code that assigns an arbitrary string to them stops compiling.

  • ConversationSummaryDto.kind is a required ConversationKind ('assistant' | 'guided_ontology' | 'generative_ui' | 'external_agent', engine !310). Every conversation carries its kind: an assistant chat carries 'assistant', so a test for an absent kind (!c.kind) now matches nothing. Filter on c.kind === 'assistant', or list one kind with listConversations({ kind }). The engine no longer lists a conversation without a valid kind.

  • conversation.listConversations(query?, requestOptions?) (engine !310): the first argument is now a ListConversationsQuery ({ kind?: ConversationKind }), sent as ?kind=<kind>; per-call request options move to the second. A caller that passed request options first must move them. An unknown kind is a BadRequestError.

  • RecordTurnRequest.conversationKind is a RecordableConversationKind ('guided_ontology' | 'external_agent'), not a free string (engine !310). The engine refuses assistant and generative_ui with 400, an unknown kind with 422, and a kind other than the stored one with 409 (ConstraintViolationError). An absent kind on create now means external_agent.

  • IngestionConfigDto.batchSize and maxSchemaTokens are removed (engine !311, #352). The engine refuses either key with 422 on /ingest/markdown, /ingest/markdown/batch, /ingest/document, /ingest/document/batch and /ingest/audio. Neither had an effect. The LTN and source-ingest batchSize fields are different DTOs and stay.

  • ingestion.getSession and IngestionSessionHandle.getSession return IngestionSessionDetailResponse (engine !311, #400): the session fields and documents: SessionDocument[]. IngestionFailedError.session is the detail response too. The result still satisfies IngestionSessionResponse; only code that names the return type in a position that needs the exact type is affected.

  • analysis.confirmImplication(sessionId, request, requestOptions?) (engine !314, #410): the second argument is now a required ConfirmRequest ({ questionId }), sent as { "question_id": … }; per-call request options move to the third. RefuteRequest.questionId is required too. Pass the pending question’s ExplorationQuestion.questionId (new on every question the start, confirm, refute and status calls return). An id that is not the pending question — a replayed answer, a stale id, an id after completion — answers 409 with code exploration_question_not_pending (ConstraintViolationError; read the code with errorBodyCode) and nothing is applied. An older client gets 415 (confirm) or 422 (refute).

  • AddRuleRequest.antecedents is required (engine !312, #375, #390), and so is antecedents in the request of inference.addRule, validateRule, replaceRule and every bulkAddRules row. The engine refuses a rule with no antecedents (422 rule rejected: a rule needs at least one antecedent; a term with no antecedents is a fact — write it through POST /api/v1/terms): write a fact with terms.createTerm. validateRule with an empty list answers ok: false with a refused issue. In bulkAddRules an empty row is refused per row in rejected[] and not counted; a row without the key fails the whole batch with 422. Every rejected[].reason is now the single route’s 422 text, prefixed rule rejected: (the old rule head term did not resolve: … and antecedent N could not be resolved texts are gone).

  • The residuation listing drops the states it never held (engine !314, #93). ResiduationStateDto is 'pending' | 'completed' | 'failed' and ResiduationStateFilter is 'pending' | 'completed' | 'failed' | 'all' ('suspended' and 'ready' are gone; 'failed' is a new filter). ResiduationStats loses ready and suspended, and its counts describe the returned rows. ResiduationKind loses 'all'; the request’s kind is the new ResiduationKindFilter (ResiduationKind | 'all'). WaitingConditionType adds 'variable_binding': an exhaustive switch needs the arm.

  • TrizNoveltyScore.inventionTermId is required, and a malformed novelty score throws (engine !313, #383). Entry i of NlQueryResponse.noveltyScores scores the i-th triz_inventive_proposal of results, so the mapper no longer skips an entry it cannot read (that shifted every later score onto the wrong proposal): it throws ReasoningLayerError naming the index.

  • TurnFeedback.rating and ConversationAnswerFeedbackResponse.rating are typed AnswerRating (-1 | 0 | 1), not number (sdk#55). The mapper checks the value: a rating off the scale is reported as a ResponseShapeError, not passed through. Only code that builds one of these objects with a plain number stops compiling.

  • A 503 lock_timeout from osfql.execute is now an OsfqlError, not a ServiceUnavailableError / InternalServerError (engine !306, #288). OsfqlError extends ApiError only, so an existing instanceof ServiceUnavailableError or instanceof InternalServerError branch for /osfql stops matching, and no compiler reports it: branch on errorType === 'lock_timeout'. OsfqlErrorType adds 'lock_timeout': other writers kept the tenant’s knowledge base locked until the request deadline; nothing ran, so retry it as is or send a larger timeoutMs. The engine can answer it before a read-only MATCH too; osfql.preview never does. The transport’s retryOn503 still applies first. An exhaustive switch over OsfqlErrorType needs the new arm.

  • PatchSortFeatureRequest is now a union type alias (PatchSortFeatureRename | PatchSortFeatureAmendment, engine !305, #389). Rename calls compile unchanged, but code that reads or extends the type breaks: interface X extends PatchSortFeatureRequest no longer compiles, and req.name / PatchSortFeatureRequest['name'] is string | undefined. Extend or annotate with PatchSortFeatureRename for a rename body, and narrow with 'name' in req before you read .name.

  • Review decisions answer 409 and 404 where they answered 400 and 403 (engine !304). The type surface does not break, but approveEntity, correctEntity, mergeEntity and rejectEntity now throw ConstraintViolationError (409) for a review that is no longer pending (it was BadRequestError, 400), and NotFoundError (404) for another tenant’s review (it was ForbiddenError, 403). Code that branches on BadRequestError / 400 for a decided review, or on ForbiddenError / 403 for another tenant’s review, must move to ConstraintViolationError / 409 and NotFoundError / 404.

  • DocumentAnalysisReport.overallCompliance and DocumentResult.complianceScore are number | null on documents.analyze (engine !315, #411). null means that no applicable rule was evaluated, so nothing is shown about compliance. A truncated document has a null score, and its rules do not count toward overallCompliance: no pass can stand on a partial read. The SDK reported an absent score as 0, which reads as “fails every rule”.

  • IngestionStatsDto.entitiesRefused is required (RefusedEntityDto[]: termId, sortName, message, feature?, chunkIndex?; engine !315, #433). Ingestion judges each extracted entity by the same declaration verdict as POST /api/v1/terms and lists every entity it refused, so it was not written. An older engine that omits the list reads as [].

  • ConversationMessageResponse.turnId is required (string, engine !317, #466): the assistant turn the answer was stored as, the id submitAnswerFeedback rates. Every answer is stored, the certify-or-abstain refusal and the could-not-process reply included, so every answer carries it. sendMessage, the streamed done event and decodeConversationMessage all carry it; a payload without turn_id does not decode (decodeConversationMessage returns null, and the stream throws NetworkError).

  • SparqlTranslation.complete is required (boolean, engine !317, #464). It says whether osfql answers the query when run verbatim (true, one statement) or holds plan fragments that only the engine’s bridge joins (false). The mapper dropped it before. Check it before you run any of osfql.

  • bind() on a nested term keeps its binder (engine #438). In 3.2.0 the nested-term conversion dropped it by design, and the same call now sends a different body with a different meaning. On the clause of not(), not(bind('?T', psi('meat_topping'))) changes from NOT EXISTS meat_topping() (“no such term exists”) to NOT EXISTS ?T: meat_topping() (“?T is not one”); when no positive antecedent binds ?T, the engine now refuses the rule with 422. A nested bind() in a positive feature value now adds an identity join.

  • GeneratedScenario.rules and UpdateScenarioRequest.addRules are ScenarioRule[] (engine !319, #470), the shape the engine publishes: head (required) and antecedents are ScenarioRuleTerms (sort, features?, negated?, binding?) and certainty is optional (default 0.8). They were Record<string, unknown>[] and unknown[], so a rule of the wrong shape (a conclusion in place of head) compiled and was refused only by the engine. addRules is now mapped field by field, so a key outside the published shape no longer goes out; only the term features go out as written. The other add* collections and GeneratedScenario.rules go out unchanged.

  • platform.listBranches and platform.forkBranch are removed, with the types ConversationBranch, BranchListResponse and ForkBranchRequest (flat and under Platform). The engine removed GET /api/v1/branches/{conversation_id} and POST /api/v1/branches/{branch_id}/fork (engine !308, #370); both answer 404, and there is no replacement.
  • SortDeletePreviewResponse.orphanedForeignChildren is removed (engine !315, #435). A caller-chosen sort id can no longer name a sort outside the caller’s scope, so a delete preview no longer counts another tenant’s children. orphanedChildren lists every child that would be left without a parent.
  • ingestion.searchWeb(request) (POST /api/v1/web/search, engine #413): searches the web through the deployment’s providers and returns WebSearchResponse (query, the trimmed query, and results: WebSearchHit[] of { url, title, snippet, provider, score? }, best first). An empty results is a search that found nothing. The hits are candidates for ingestUrl; the search writes nothing for the tenant. limit is left out when unset (the engine refuses null). A deployment with no provider answers 503 web_search_not_configured (ServiceUnavailableError, permanent: retryOn503 retries it for nothing); failed providers answer 502 web_search_providers_failed. New types: WebSearchRequest, WebSearchResponse, WebSearchHit, WebSearchErrorCode.

  • getWebCapabilities limits (engine #414): urlIngestion.maxCrawlDelayMs, maxRedirects and requestTimeoutMs, and webSearch.maxResultsLimit and maxQueryChars. The mapper dropped all five. UrlIngestionCapability documents the worst case of one crawl they bound. Code that builds a WebCapabilitiesResponse literal must add the five fields.

  • UrlIngestStoreFailedError (engine #415): ingestion.ingestUrl throws it for the 500 web_ingest_store_failed answer, when the crawl ran and storing its pages failed part way. It extends InternalServerError and carries report (the pages stored, as a 200 would report them), failed (WebIngestStoreFailure: step, page?, message) and notWritten (WebPageRef[]). Nothing is rolled back and a re-run is safe. Such a failure was a 400 Domain error before; a 400 now means a malformed body only. New types: IngestUrlStoreFailure, WebIngestStoreFailure, WebIngestStoreStep, WebPageRef, WebIngestErrorCode.

  • conversation.submitAnswerFeedback(conversationId, request) (POST /api/v1/conversation/{id}/feedback, engine #84): rates one assistant answer -1, 0 or 1 (AnswerRating), with an optional correction and comment (each at most 10,000 bytes), and returns ConversationAnswerFeedbackResponse (feedbackId, conversationId, turnId, rating, replaced). One rating per turn: a re-rating replaces it and keeps the feedbackId. There is no delete; rate 0 to withdraw a judgement. A user turn is refused with 422. An accepted rating is also recorded in the compliance ledger under /api/v1/conversation/answer-feedback.

  • TurnDto.feedback (TurnFeedback: feedbackId, rating, correction?, comment?, recordedAt) on a rated assistant turn of getTurns.

  • ConversationTrainingConfigDto.rewardUserFeedback (default 0.5): a chat answer rated helpful earns rewardUserFeedback × satisfaction; one rated unhelpful takes the failed-goal penalty. It is left out when unset (the engine refuses null).

  • RlTrainResponse.transcripts (RlTranscriptSummary: cognitiveAgent, chat, synthetic, ratedTurns) on a conversation run. A nonzero synthetic with useRealConversations says the tenant had no conversation to train on.

  • verification.runVerification(request) (POST /api/v1/verification/run): discharges a SafetyProperty against a neural network, an agent trace or a family of LLM outputs and returns a VerifyRunResult (certificateId, kind, method?, verdict). New types: VerifyRunRequest, SafetyProperty, VerificationTarget, OutputPredicate, AbstractIntervalVec, AbstractInterval, AbstractIntervalBound, TraceBundle, TraceProperty, AgentTraceStep, VerifyRunResult, VerdictKind, VerdictKindTag, ProofMethod, EmpiricalMethod. The PascalCase variant keys are sent verbatim.

  • verification.health() (GET /api/v1/verification/health): the plain-text "ok".

  • CertificateDetail.verdictDetail: the whole verdict, so the falsification witness and description, the empirical confidence and sample count, the residuation wake triggers and the proof are no longer dropped.

  • Typed lifecycle refusal readers: termRefusalOf(error) → TermRefusal (status, code: TermRefusalCode, message, termId, referrers, repairs, ruleIds, derivedBy?), ruleRefusalOf(error) → RuleRefusal (code: RuleRefusalCode, ruleId, derivations?, derivationsAtLeast?) and functionRefusalOf(error) → FunctionRefusal (code: FunctionRefusalCode, function, callers). They read the engine’s TermRefusalResponse, RuleRefusalResponse and FunctionRefusalResponse out of a thrown ApiError, like sortDeleteRefusalOf. RuleNotWithdrawable.code is typed RuleRefusalCode.

  • AuthzTuple.etag: authz.writeTuple folds the ETag response header into the tuple; pass it as deleteTuple(id, { ifMatch }) for a stale-write-safe revoke (a stale tag throws PreconditionFailedError with currentEtag).

  • visualization.termList(request?) (POST /api/v1/visualization/term-list) → TermListVisualizationResponse (graph, totalCount, offset, limit, emittedCount, coreferences?), and visualization.unifyTerms(request) (POST /api/v1/visualization/unify-terms, op: 'unify' | 'anti_unify') → TermUnifyVisualizationResponse (graph, success, bindings?, resultTermId?). Both routes now publish their own components.

  • ProofTraceNodeDto.substitutions: the variable bindings applied at a conversation proof step, keyed verbatim by variable name.

  • SortRefSelectProps and isSortRefSelectProps(props) (engine #172): ui.describe renders a feature declared by an entity range as a select node whose props carry sortName, valueKey: 'id' and labelKey: 'label', and whose load_data action lists the options. The guard narrows a node’s props without a cast.

  • ReviewEntityOrigin, ReviewCandidateInput, ReviewAddErrorCode (review_sort_unknown, review_candidate_unknown, review_confidence_out_of_range, review_similarity_out_of_range, the 422 codes of reviews.addPendingReview) and ReExtractErrorCode (document_source_not_retained, the 409 of reviews.reExtract). errorBodyCode reads both.

  • ingestion.retryDlqEntry(id) (POST /api/v1/ingest/dlq/{id}/retry, engine #87): re-runs the entry’s whole document and answers 202 with DlqRetryResponse (entry, now retrying; entriesRetrying; sessionId; documentId; statusUrl). Every pending entry of the document moves to retrying and settles to succeeded or back to pending. 404 for an unknown or foreign id; 409 with a DlqRetryErrorCode (dlq_entry_not_pending, dlq_retries_exhausted, dlq_document_in_flight, dlq_source_not_retained); 503 without a background queue throws IngestionQueueUnavailableError, as sync: false ingestion does.

  • IngestionStatsDto.alreadyIngested and DocumentStatsDto.alreadyIngested (engine #387): true when the bytes were already ingested in the tenant. Nothing is re-extracted, documentTermId names the earlier registry document, and the request’s collectionPath / previousDocumentId apply to that document. Code that builds either object by hand must add the field.

  • IngestionSessionHandle.getDocumentStats(documentId) (GET /api/v1/ingest/sessions/{session_id}/documents/{document_id}/stats): one document’s stats, null while it still runs (204); 404 and 409 (failed or cancelled) throw. The engine publishes DocumentStatsDto as the 200 body (engine #464).

  • DocumentVersionErrorCode (engine #387): ingestMarkdown and ingestDocument answer 404 previous_document_not_found (NotFoundError) for a previousDocumentId that names no document, which was silently dropped before, and 409 duplicate_document_version_conflict (ConstraintViolationError) when duplicate bytes already belong to another version family. With sync: false the same refusal ends the session document failed. Read the code with errorBodyCode.

  • ontology.listUpperOntologies() (GET /api/v1/ontology/upper, engine !297): every vendored upper ontology, in registry order, as UpperOntologySeedsResponse (seeds: UpperOntologySeed[] of name, title, ontologyIri, versionIri, license (SPDX), sourceUrl, retrievedOn, byteLength). A seed is third-party data served unmodified under its licence. ontology.getUpperOntology(name) now takes any listed name (string); UpperOntologyName stays as the set this SDK version knows. Neither read registers a tenant.

  • sorts.patchSortFeature amends a declaration’s description and index (engine !305, #389). PatchSortFeatureRequest is now the union PatchSortFeatureRename (name, cascadeTerms?) | PatchSortFeatureAmendment (description and/or index). Per key, an absent key keeps, null clears, a value replaces; index: null returns the declaration to silence, which is not not_indexed. This is the only way to clear either field. The union makes the engine’s new 400s (a rename combined with an amendment, cascadeTerms on an amendment, an empty body) compile errors; a blank description is still a 400. The alias change is listed under Changed — BREAKING.

  • OsfqlFailure.atomicRefusal and OsfqlError.atomicRefusal (engine #365): the code of a program refused because a DROP SORT / ALTER SORT is not alone.

  • EnrichedHealthResponse.bootId, startedAt and uptimeSeconds (engine !306, #287) on health.check(): the identity of the engine process. A new bootId means the engine restarted and every derived conclusion a client cached is reset. They are optional only because a gateway can answer with an empty or partial body; the mapper never invents them. BuildInfoDto.version is now the engine’s git describe without v and -dirty (0.1.150-76-g8cb95b7c8), and gitHash / buildTimestamp are present in development builds too.

  • ArgumentLabel and ArgumentAttackKind (flat and under Argumentation).

  • ConversationKind, RecordableConversationKind and ListConversationsQuery (engine !310), flat and under Conversation.

  • TurnDto.proofTrace, reasoningTrace, validityCertificateId, validityCertificateHash, validityCertificateRuleProgramHash and validityCertificateRuleRevision (engine !310): a reloaded assistant turn of getTurns carries the proof tree, the reasoning trace and the certificate the live answer carried.

  • ingestion.getDocumentStats(sessionId, documentId) (GET /api/v1/ingest/sessions/{id}/documents/{doc}/stats, engine !311, #400): the stats of one document, or null while it runs (204). An RDF document is a NotFoundError (its receipt is on getSession); a failed or cancelled document is a ConstraintViolationError.

  • IngestionSessionHandle.waitForSession(options?) (engine #400): polls like waitForCompletion and returns the completed IngestionSessionDetailResponse, whose documents carry their receipts. Use it for an ingestRdf session queued with sync: false: it has no markdown stats.

  • IngestionSessionDetailResponse, SessionDocument, IngestionDocumentStatus, DocumentReceipt and RdfImportReceipt (engine #400): documents[].error gives the reason a document failed, and documents[].receipt what a completed one wrote, tagged by kind (markdown: the document stats; rdf: sortsCreated, termsCreated, shapesParsed, repairedIriCount, repairedIris).

  • BulkCreateSortsRequest.prefixes (engine !311, #404, #454): prefix bindings (prefix → namespace IRI, '' = the default prefix) for the batch’s CURIE names. The keys go out verbatim. Pass translateRdf(...).prefixes. A malformed binding is a BadRequestError; rebinding a bound prefix is a ConstraintViolationError.

  • BulkSortErrorKind adds 'unbound_prefix' and 'non_canonical_prefix' (both with sortCreated: false). An exhaustive switch over the kind must handle them; prefer sortCreated.

  • DocumentType adds 'text' (engine #405): ingestDocument and ingestDocumentBatch accept base64 .txt, .md and .markdown files, and the parse stats name the text parser. A file that is not UTF-8 is a 422 on ingestDocument and on ingestDocumentBatch({ sync: false }), and a failed results entry on an inline batch; either way the message names the file and the byte offset.

  • engine.getStats() (GET /api/v1/engine/stats, engine !314, #93), also system.engine: the caller’s tenant’s termCount, ruleCount, tenantSortCount and sharedSortCount (with tenantId), each equal to the total of the listing it summarises. New EngineClient and types EngineStatsResponse (flat and under the new Engine namespace).

  • reasoning.listOperations({ limit? }) (GET /api/v1/reasoning/operations, engine !314, #93): the subsumption, merge, entailment and disentailment verdicts the engine answered for the tenant, newest first, as ReasoningOperationListResponse (operations: ReasoningOperation[] of sequence, name, goal, goalTruncated, verdict, degree?, computationTimeMs, createdAt; total; retention of scope: 'process', capacity, goalBudgetBytes). The log is process-scoped and bounded; limit: 0 is a BadRequestError. New types: ReasoningOperationName, ReasoningVerdict, ReasoningOperationLogScope, ReasoningOperationRetention.

  • RuleTermWriteError (engine !312, #376): terms.updateTerm, terms.deleteTerm and inference.bulkRetractTerms throw it for the 409 write_targets_rule answer — a write onto a rule’s own term (its conclusion template, an antecedent pattern or a variable). It extends ConstraintViolationError and carries ruleIds and repairs (replace_rule and withdraw_rule per rule the caller may edit; none for a plugin, system or bootstrap rule). A pattern or variable delete answered term_named_by_rules before; a conclusion edit or delete answered 200/204. bulkRetractTerms removes nothing. OsfqlRepair.kind / DerivedFactRepair.kind document replace_rule.

  • SortRefFeatureValue ({ sort_ref, sort_name? }) in FeatureValueDto (engine !312, #353): a sort named as a value in a listed rule, a chaining answer or a fact listing. It arrived as { term_id } before, indistinguishable from a term. sort_name is absent when the caller’s lattice does not hold the sort. FeatureValueDto also gains InlineTermFeatureValue, the whole Ψ-term the engine inlines for a hidden rule template. New helpers: isSortRefFeatureValue(value) and featureValueToInput(value), which converts a listed value into the write format ({ sortRef }, { termId }, the literal’s scalar) so a rule read from getRules() can be sent back to replaceRule.

  • ExplorationStatusResponse.question (engine #410): the pending question, null once complete — a client that lost an answer re-reads it here. ExplorationQuestion.questionId, ConfirmRequest and AttributeExplorationErrorCode ('exploration_question_not_pending', flat and under Analysis).

  • OsfqlResponse.computationTimeMs (engine !314, #93): the engine’s own run time of the program. Code that builds an OsfqlResponse literal must add it.

  • ConformanceRepairResponse.durabilityFailures (engine !314, #406): the repairs applied in memory but not persisted; empty on a clean pass. Code that builds the response by hand must add it.

  • UpdateScenarioResponse.warnings (engine !312, #403): a scenario rule the engine refused is named here (rules[i] (conclusion `s`) was not written: <reason>), not thrown. Code that builds the response by hand must add it.

  • DocumentResult.evaluatedText (DocumentEvaluatedText: evaluatedChars, totalChars, truncated) and DocumentRuleResult.withheldStatus (engine !314, #411) on documents.analyze: how much of the document the rules pass read, and the pass/fail the engine withheld (reported inconclusive) because it was not grounded in the text.

  • WaitingConditionDto.variable (the awaited variable, e.g. ?y) and ResiduatedTermDto.resolvedAt (epoch ms) on visualization.residuationState and execution.residuate (engine #93).

  • createApiError is exported from the package root (sdk#51). The error event of conversation.sendMessageStream documented it, but only the error classes were exported. createApiError(event.status, { error: event.error, message: event.error }, new Headers()) gives the same subclass, message and errorCode that sendMessage throws for the same failure (InternalServerError for 502, ServiceUnavailableError for 503, …). An error event without status is not an HTTP failure. The stream still yields the event and does not throw it.

  • ResponseShapeError (sdk#54): every resource method that reads a JSON body throws it for a 2xx response it cannot read, in place of the bare TypeError a normalizer raised (Cannot read properties of undefined (reading 'map')). It extends ReasoningLayerError, not ApiError, and carries status, method, path (after the base URL, without the query), body and field: the dotted path of the first published field the body lacks, holds as null or holds with the wrong kind (items, items[3].sort_suggestions). field is undefined when the body is not JSON (an HTML page served with 200; cause is the parse error) or when every published field is present. The check runs only after reading the body failed, so a body without a field the SDK defaults for an older engine is still read. Any other error a normalizer throws (ValidationError, ReasoningLayerError) is unchanged.

  • Sorts.SortCreateErrorCode (sort_id_unavailable, sort_id_held_under_another_name; engine !315, #435): the 409 codes sorts.createSort answers for a caller-chosen id. sort_id_unavailable is an id held outside the caller’s scope (another tenant’s sort or the shared lattice), or a version 5 UUID that names none of the caller’s own sorts; sort_id_held_under_another_name is an own id posted under a different name (rename with patchSort). Read the code with errorBodyCode.

  • DocumentRuleResult.basis (DocumentVerdictBasis: quoted_violation, quoted_compliance, stated_unknown, absence) and DocumentRuleResult.withheldConfidence on documents.analyze (engine !315, #411): what the evaluator said a verdict rests on, and its confidence in a withheld verdict. Both are absent from an older engine.

  • Documents.DocumentAnalysisErrorCode (llm_answer_unparseable; engine !315, #411): documents.analyze throws an InternalServerError with status 502 and this code when the LLM answered a step with no readable JSON.

  • Terms.TermWriteErrorCode (write_creates_rule; engine !315, #455): terms.createTerm, terms.updateTerm and inference.addFact answer 422 with this code for a non-empty list under the feature when, which would make the term a rule. bulkCreateTerms names such a row in BulkRefusedError.rows (feature: 'when'), and bulkAddFacts lists it in rejected. OSFQL writes answer the refusal code write_creates_rule with the repair use_derive.

  • OsfqlPreviewStatement.refusal (engine #449, #457): the refusal the run would raise for the statement, with the message osfql.execute answers: a stand-alone write naming an earlier statement’s variable, or a write onto an append-only fluent term, an inline MATCH write included.

  • GenerateOntologyRequest.language on ontology.generate: the language of the generated human-readable text, as a BCP-47 tag or a language name. Sort and feature names stay English snake_case; absent, the model writes in the language of the prompt.

  • FeatureInputInlineTerm.binding and FeatureInputInlineTermByName.binding (engine #438): a nested inline term names its own identity. On the clause of a negation it names the term the negation is about: not(bind('?T', psi('meat_topping'))) is NOT EXISTS ?T: meat_topping(), and a positive antecedent must bind ?T.

  • prefixes on scenarios.create, materializeScenario and update (CreateScenarioRequest, MaterializeScenarioRequest, UpdateScenarioRequest; engine !319, #470): prefix bindings (prefix → namespace IRI, '' = the default prefix) for the scenario’s CURIE names, with the contract of BulkCreateSortsRequest.prefixes. They are bound in the same hold of the lattice as the scenario’s sorts and released if the scenario is refused. The keys go out verbatim. Pass translateRdf(...).prefixes. A malformed binding is a BadRequestError; rebinding a bound prefix is a ConstraintViolationError.

  • ScenarioRule and ScenarioRuleTerm (under Scenarios): the typed scenario rule, with the antecedent binding the engine now publishes.

  • documents.analyze (engine #411): a withheld verdict reports confidence: 0, and the evaluator’s value moves to withheldConfidence. overallCompliance no longer counts not_applicable rules, so the number changes even where it is not null. documents.check adds a warning for a document on which the rules pass evaluated no applicable rule, and reports an unreadable LLM answer as its non-fatal KB-rules warning.

  • Review decisions change the knowledge base (engine !304): rejectEntity retracts a provisional entity with its edges, mergeEntity moves the edges and retracts, correctEntity patches the live term, and reExtract now runs: it creates a new document version and answers 404 for an unknown document.

  • Every UI action is atomic (engine #172): a failure keeps nothing, and its error ends with (the program was rolled back: …).

  • Saved goals live outside the fact store (engine !306, #288). inference.createGoal, backwardChain / fuzzyProve / nafProve / bayesianPredict with saveGoal: true, and bulkFuzzyProve with saveGoals: true store the goal apart from the tenant’s facts: it no longer appears in a MATCH or in getFacts(), and createGoal / bayesianPredict no longer write it into the live store. A sort-only backwardChain goal with saveGoal: true now answers a UUID goalId (it was null, and the save was dropped). nafProve with saveGoal: true also saves the goal’s own term. Goals that an earlier engine saved are stray facts in the fact store and listGoals does not list them: wipe them (for example with admin.clearMyTenantData()).

  • Reads never register a tenant (engine !306, #385). GET, HEAD and OPTIONS never create the tenant’s registry row; a served unsafe-method request (POST/PUT/PATCH/DELETE) or a WebSocket upgrade does, but a 401/403/404/405/501 answer never does. After reads only, admin.getMyTenant() and admin.deleteMyTenant() answer 404. osfql.preview, osfql.diagnose, argumentation.evaluate and ingestion.searchWeb never register the caller, their 400/415/422 answers included.

  • The unified inbox and the DM pairing state are durable (engine !308, #358): DM policies, allowlists, pending pairing requests and inbox items are stored in PostgreSQL and survive a restart. A tenant clear or delete, or a factory reset, erases them and counts them in inboxItemsDeleted / dmPairingStateDeleted.

  • A tenant delete or clear releases the tenant’s channel routes (engine !308, #369): another tenant’s channels.registerRoute for such an account now succeeds instead of answering 409.

  • argumentation.evaluate (engine !306, #76): the request body is closed (an undefined key is a plain-text 422); a preference or retraction naming no claim is a 400 (preference names no claim: "x" / retracted names no claim: "x"); a preference on a retracted claim is dropped with a warning. Argument.text is the claim’s prose, else the OSFQL pattern of the stored fact, else its term label; Argument.index excludes retracted claims; ArgumentQuestion.prompt uses typographic quotes (Which is true: “A” or “B”?); a rule-derived attack’s reason names the rendered fact.

  • coherence.analyze (engine !306, #76): CoherenceDerivationStep.derivedFactLabel, the rule-derived clash reason and the conflict-fact diagnostic carry the rendered OSFQL pattern (for example incoherence(motif: "two colours")), not term-xxxxxxxx.

  • platform.runDiagnostics (engine !306, #368): checks holds three entries, memory_usage, system_clock and disk_access, each once (it held four), so passCount is 3 on a healthy engine.

  • sendMessage and recordTurn raise ConstraintViolationError (409) for a conversation another surface owns, and sendMessageStream yields an error event with status: 409 (404 for an unknown conversation) (engine !310): the stream checks the conversation after it opens. sendMessage raises NotFoundError for an unknown conversation. The certify-or-abstain answer is now stored and carries turnId. On a rescued turn, suggestions[0] is the No exact match notice, and originalOsfql is always set when repaired is true.

  • ui.generate records a generative_ui conversation titled by the prompt (engine !310), listed by listConversations({ kind: 'generative_ui' }). A conversationId the caller does not own is a NotFoundError; a conversation of another kind is a ConstraintViolationError. The engine checks both before the LLM runs.

  • ingestAudio applies ingestionConfig (engine !311, #367), like /ingest/markdown, unless parseOnly is set. Without a config, the extraction strategy is the request default (LLM), not the deployment’s EXTRACTION_STRATEGY.

  • Markdown ingestion has no 100,000-byte limit (engine #405): ingestMarkdown, ingestMarkdownBatch and resumeIngestion are bounded only by the 50 MiB body limit (413, an ApiError).

  • OSFQL binds prefixes and reads CURIEs (engine #404): PREFIX ex: <iri>; binds a tenant prefix durably, and MATCH :Pizza(:size: ?S); reads unquoted CURIE names. A rebind, or a DEFINE of a CURIE whose prefix is not bound, is an OsfqlError execution failure (422). sorts.createSort, patchSort and patchSortFeature accept a CURIE under a bound prefix, and refuse an unbound one with 400. sorts.getSchema starts with the tenant’s PREFIX lines.

  • TranslateRdfResponse.prefixes is the binding map the names are compacted through: a document prefix that conflicts with a tenant binding is renamed, never rebound. termCount no longer counts stub terms for the objects of skipped schema declarations.

  • IngestRdfResponse.sortsCreated counts the sorts the OWL pass created (0 without parseOntology), not the lattice size.

  • A synchronous MATCH … SEMANTIC row carries its feature columns in rank order. A binder-less pattern keeps _semantic; a pattern with a named root has no _semantic column. Ranking obeys visibility and the bitemporal slice.

  • query.nlQuery in cognitive mode filters by every translated constraint, and generatedOsf is the executed OSFQL. A constraint it cannot carry is success: false with the reason in error; an empty results with success: true means no match. A filtered or structured question over a rule sort that has members only by derivation is success: false, and so is an aggregate over any sort a rule concludes (the aggregate reads only stored members); the reason is in error. NlQueryResultItem.id is the matched term id.

  • A rule head no longer carries certainty in features (engine !312, #377, #391): read RuleEntryDto.certainty. A fact’s authored certainty feature now shows in display. Every display / goalDisplay / ruleLabel text shows all data features sorted by name, with JSON-escaped strings, shortest round-trip reals and full lists (no [...N items]); ruleLabel no longer shows the aggregator.

  • A MATCH with an inline write classifies by that write (engine !313, #416): an inline INSERT, LINK or UPSERT LINK is additive_write; an inline UPDATE, REMOVE, UNLINK, DELETE or DETACH DELETE (or an IF/WITH that holds one) is targeted_destructive. ui.executeAction now needs confirm: true for those (else requiresConfirmation: true), its dry run answers affectedPreview.exact: false, and osfql.preview answers affectedError and no affected for an inline destructive MATCH. A writing query in a ui.describe, ui.generate or AG-UI data load is refused without an exception: stats.osfqlExecuted starts with FAILED:.

  • generation.generateDocument (engine !313, #360) is read-only (no derived fact is written) and covers only the root’s material (the terms it reaches plus the terms that reference it). A root that is absent, an engine built-in or rule scaffolding answers 404 (NotFoundError) before the LLM check; more than 500 terms of material answers 422.

  • fuzzy.searchTopK (engine !313, #378): exhaustive: true with no coverage when no retrieval cap fired; exhaustive: false with coverage when one did.

  • memory.remember (engine !313, #379): the tenant route no longer refuses text that contains code; the remaining 400s are an empty text, more than 500 characters and an importance outside 0..1.

  • Agent-queued action reviews (engine !313, #380) carry agentId and goalContextId, so actionReviews.list({ agentId }) finds them; approving one re-runs the agent’s integrated cycle.

  • Scenarios (engine !312, #403): scenarios.create, materializeScenario and update decode rule features with the REST rule encoding ("?X", {"name":"?X"}, {"_var":"?X"} are variables; a literal starting with ? is {"type":"String","value":"?x"}). Each rule runs the POST /api/v1/inference/rules checks; a refused rule is not written and is named in warnings. An unknown sort is refused, not widened to thing.

  • conformance.repair (engine !314, #406): an applied repair is durable — visible on GET /terms, kept across a restart, versioned in the history and announced on CDC.

  • The residuation listing (engine #93) keeps one row per term (id is the term id), keeps completed and failed rows, fills waitingFor, applies both filters and reports createdAt in epoch milliseconds. GET /terms/{id} reports failed when a later check failed.

  • osfql.preview counts the terms an inline MATCH … UPDATE / REMOVE / UNLINK / DELETE changes (engine #457). Such a statement carries affected (count, exact, sampleRows, one row per changed term) like a RETRACT, where it carried affectedError and no count. A term that several joined rows bind counts once, and an UNLINK counts a source only when it holds the edge. ui.executeAction dry runs report the same exact count.

  • ingestion.getDocumentStats and the session handle’s getDocumentStats call the generated route: the engine now publishes the DocumentStatsDto body of its 200 (engine !317, #464).

  • ui.generate binds a data query to its rows’ term ids (engine #471): a certified single-pattern data query gets the identity binding on its root pattern (noted in diagnostics), and the view that uses it names the id column in props.rowKey. Any other query names no rowKey.

  • scripts/check-spec-validity.js fails on a root tag declared more than once (sdk#2). The engine declares preferences once since #465.

  • Every resource method that reads a JSON 2xx body goes through decodeResponse (sdk#54). Twenty-two pass-through methods (statistical, causal, neuro-symbolic, cognitive, collections, control, proof-engine, snapshots) returned response.data raw, so a gateway’s 200 HTML page resolved null typed as the DTO. They now throw ResponseShapeError. pnpm run check-response-decode flags a bare return response.data; unless the route asks for text, blob or arrayBuffer.

  • ResponseShapeError messages use the right article: not an array (got an object), not (got a object).

  • fuzzy.predictEffect calls the generated route instead of a raw request.

  • ingestion.retryDlqEntry never retries the queueless 503, even with retryOn503, like the sync: false paths. It retried it up to maxRetries times before it threw IngestionQueueUnavailableError.

  • ingestion.ingestUrl raises UrlIngestStoreFailedError only for a 500 body that has the complete published shape (an error string, and pages and skipped entries with their required keys). A body with report.pages: [null] threw a bare TypeError from the mapper and lost the original InternalServerError; it now stays that InternalServerError.

  • featureValueToInput keeps a listed string that starts with ? a constant: it returns { type: 'String', value }. Returned bare, the engine’s rule write path read "?x" as a variable, so a rebuilt rule matched every value.

  • Generation GenerationVerificationDto.violations passed the wire objects through raw, so relatedFactId was always undefined (the snake_case related_fact_id leaked instead). Each violation is now mapped.

  • A conversation proof node with a null detail, rule or confidence reads undefined, like every other absent optional field.

  • ui.generate reads a null certificate_id, page_version, conversation_id, data, layout, descriptor, trace, intent statement_id/returns/reason or data-source returns as absent. It now calls the generated route against the published UIGenerateResponse instead of a raw request typed by hand-written wire shapes.

  • coherence.analyze calls the generated route, typed against the published CoherenceReport instead of hand-written wire shapes. A null optional report field (subsumption, clash, advisory, target, weakest_boundary, …) stays absent, as an omitted one does.

  • marketplace.searchCatalog and conformance.check are typed against their own published components (MarketplaceSearchRequest/MarketplaceSearchResponse, ConformanceViolationDto) instead of hand-written wire shapes; searchCatalog calls the generated route.

  • reviews.addPendingReview calls the generated route, and a pending review’s sort suggestions are typed against the published ReviewSortSuggestionDto. A malformed suggestion is no longer refused with a ValidationError at read time; the published DTO is the contract.

  • ConstraintViolationError.termId, .feature and .constraint are read from the engine’s flat 409 body (ConstraintViolationResponse: { error, message, term_id, feature, constraint }). The SDK read them from a details object no engine sends, so all three were always undefined.

  • Verification errors: the engine now answers { error: <reason phrase>, message: <text> }, so ApiError.message is the engine’s sentence (it was the synthesized HTTP 404) and errorCode the reason phrase. verification.getCertificate calls the published route; a non-UUID id is a BadRequestError (400), an unknown id a NotFoundError (404).

  • sorts.patchSortFeature sent only name and cascade_terms: the mapper dropped description and index. It now copies every key that is set, and sends null as null.

  • OsfqlFailureFromApiToFront dropped the error body’s atomic_refusal; it now reads it, and refuses a malformed one like the other details.

  • discriminateFeatureValue threw on every object, so it could not read a variable, a literal wrapper or a term reference. It now narrows all five object arms (var_name, literal, an inlined term, sort_ref, term_id).

  • inference.bulkAddRules dropped a row’s aggregator; it now forwards it, as addRule does.

  • sparql.translateQuery and translateUpdate are now typed against the published SparqlExplainDto / SparqlUpdateExplainDto (engine !317, #464), and a plan node’s attrs keeps only string entries.

  • UI actions (engine #172): a "sort_ref" field takes a term UUID in values, which the engine stores as a Reference; a non-UUID, an undeclared entity range, or a non-UUID sort_ref placeholder in a run_osfql / feature-based delete_record template answers 400 (BadRequestError), and a UUID that names no visible term answers success: false. UIAssemblyStatsDto.includedFeatures counts only fields with a value control.
  • Review decisions (engine !304): approveEntity, correctEntity, mergeEntity and rejectEntity document 403 for a reserved or unwritable sort; rejectEntity, mergeEntity, bulkReject and bulkMerge document the 503 for terms that could not be made resident. Each method documents its knowledge-base effect, and that notes and reason are kept as the decision note.
  • functions.registerFunction / replaceFunction (engine !314, #388) document the 422 codomain codes function_codomain_empty, function_codomain_non_string_label, function_codomain_blank_label and function_codomain_repeats_label, checked before the conformalTau check; evaluateFunction documents the error for a stored mixed codomain such as ["a", 3]. A Neural codomain must be non-blank, distinct string labels.
  • The CheckDocumentResponse.warnings of documents/check include KB rules pass read only the first N of M characters of <name>; … for a partly read document, and kbRuleReport carries the same evaluated_text / withheld_status fields.
  • The Ψ-terms concept page documents the read format of a listed rule’s features and the featureValueToInput round trip; the workshop guide says that a rule needs at least one antecedent.
  • ConversationTrainingConfigDto: with useRealConversations a conversation run now trains on the cognitive agent’s conversations and the tenant’s chat conversations together, numTranscripts of each (selected by conversationIds and agentId), and falls back to synthetic transcripts only when neither source has one (engine #84). The field docs say so.
  • ingestion.ingestUrl documents the redirect cap as urlIngestion.maxRedirects, and an unknown body key as 422, not 400.
  • WASM plugin methods document 500 (plugin store failure) beside 403/404/501.
  • rag.ontologyRag documents the 422 an unknown featureConfig key now answers (the engine reads it with deny_unknown_fields); the SDK sends only the seven declared keys.
  • Term writes (engine #172): a scalar literal in a feature whose declared range is an entity sort is refused with 422 on terms.createTerm, updateTerm, bulkCreateTerms (a BulkRefusedError row), OSFQL writes (OsfqlError, execution), actions.applyAction and inference.addFact. Write Value.reference(termId). The terms guide documents the OSFQL nested designator <sort>(__osf_term_id: "<id>"), which references an existing term and mints nothing.
  • CreateSortRequest.parents and sorts.createSort (engine !305, #355): a parent id must name a sort of the caller’s tenant or a shared system sort. Another tenant’s sort id is a 400 parent sort(s) do not exist, byte-identical to an unknown id (it was accepted with 201).
  • FeatureDescriptorDto.cardinalityOrigin (engine !305, #290): createSort and bulkCreateSorts accept inferred only as an unchanged echo of a bound the tenant already stores as inferred on the sort or an ancestor, with the sort’s own record declaring none; anything else is a 400 / unsatisfiable_declaration that starts sort 'S': feature 'F':. OSFQL @card(0, n) required (or required @card(0)) is a parse OsfqlError on execute and preview; it was stored as a minimum of 1.
  • inference.forwardChain / ForwardChainRequest.keepDerived (engine #290): a deadline-cut keepDerived: false run (200 or 504) leaves nothing stored; a deadline-cut keepDerived: true run records provenance for every conclusion it stored and announces it.
  • sorts.deleteSort: the sort_would_orphan_children message now ends Pass ?descendants=delete to delete them with it.; an OSFQL DROP SORT refusal names the OSFQL clauses (CASCADE TERMS, ORPHAN RULES, CASCADE DESCENDANTS) and the sort (engine #374). Messages are carried verbatim.
  • OsfqlPreviewStatement.sorts (engine #412): a feature-type word of a DEFINE (string, integer, …) is listed only when a sort of that name exists in the program or the tenant.
  • FeatureDescriptorDto.description / index point at patchSortFeature with null as the way to clear them.
  • Tenant erasure (engine !308, #381): admin.clearAllData, clearTenantData, clearMyTenantData, deleteTenant and deleteMyTenant document the 500 (InternalServerError) whose message contains uploaded source files were not erased: the wipe is incomplete, so retry it. A delete that fails this way writes no tombstone, so the tenant stays live.
  • Channels, cron, inbox and pairing (engine !308, #358) document their new 500s: channels.receiveWebhook when the DM policy or inbox store cannot be reached (the message was not admitted, so the relay must deliver it again), cron.runNow when the job fired but its inbox item was not stored (the run is not undone), inbox.list / markRead when the inbox store cannot be reached, and every pairing method when the pairing store cannot be reached.
  • authz.writeTuple and authz.deleteTuple (engine !306, #371): a caller that is not exempt gets 403 in every case, before any validation. For an exempt caller, deleteTuple answers 404 for a missing id or for an id that names a term that is not a can / has_role tuple (the term survives), also with ifMatch.
  • inference.deleteGoal documents the 404 for an unknown goal and what the delete removes; createGoal, listGoals and the goalId fields say that a saved goal is not a fact.
  • sorts.createSort and CreateSortRequest.id (engine !315, #435): an id must be unused or name one of the caller’s own sorts; a re-post of an own id updates that sort and keeps its name. The shared-lattice id the engine derives from the same name is a 400 (BadRequestError), not a 409; only a shared-lattice id posted under another name is sort_id_unavailable.
  • terms.createTerm and terms.updateTerm document NamedConstraintViolationError for a 422 refusal by a named constraint: both routes now publish TermWriteUnprocessableBody (DeclarationViolationResponse | CodedErrorResponse | ErrorResponse) as the 422 body, where PUT /api/v1/terms/{id} published only ErrorResponse before (engine !315/!317).
  • DocumentVerdictBasis: a pass or fail whose basis does not agree with it (a stated_unknown or missing basis included) is withheld and reported inconclusive.
  • terms.updateTerm, terms.deleteTerm and RuleTermWriteError (engine #456): a ground sub-term that rule authoring minted inside a rule is part of the rule, so a write onto it is refused 409 write_targets_rule; getTerm answers 404 for it and termExists answers false.
  • terms.createTerm, updateTerm and inference.addFact document the 422 for an engine-owned feature such as _derived_by (engine #249).
  • analysis.refuteImplication and RefuteRequest.features (engine #462): a counterexample must also respect every confirmed implication, else 400. The exploration poses one question for each pseudo-intent.
  • scenarios.materializeScenario and scenarios.update (engine !319, #470) document the 400 (BadRequestError) for a minted sort, feature, fluent, computed feature or axiom property name the creation gate of sorts.createSort refuses (a name OSFQL cannot read, or a CURIE whose prefix the tenant does not bind canonically); nothing is written. scenarios.create answers the same case with a 422 Generated scenario refused: …. An inferred relation edge name the gate refuses is skipped and named in warnings.
  • scenarios.create and materializeScenario: a sort a rule names that the scenario does not declare is created under thing, not refused. The JSDoc said the opposite; only update’s addRules refuses an unknown sort.
  • ui.describe (engine #471) documents the 400 for a termId that is not a UUID and the 404 for a termId that names no term of the sort; a detail or form view with termId loads that term’s own values. A data view names in props.rowKey the column in which each loaded row carries its term’s id: __osf_term_id for the engine-generated query, and the query’s own identity variable for a custom query. A custom query whose rows cannot carry an id (an aggregate, a WITH, a projection without it, a DISTINCT, an inline write) names no rowKey. Read the column rowKey names; do not hard-code __osf_term_id. UIDataPayload.termIds lists the terms the load program produced; it is not one id per row.
  • ui.executeAction and UIActionRequest.sortName (engine #471): submit_form, update_form and delete_record resolve the sort by its exact name, so a CURIE sort works. A 400 now answers an unknown sort, a sort name with a backtick, a value key the sort does not declare, and an update_form whose term is held under another sort.
  • TranslateRdfResponse.prefixes points at MaterializeScenarioRequest.prefixes too.
  • reasoninglayer#473: terms.getTerm(ruleId) answers 404 for a rule since engine #456, so the way the inference.deleteRule and replaceRule docs give to read a rule’s If-Match tag does not work. The contract test for a stale ifMatch on a rule delete fails against engine 5cef4fa34 for this reason.
  • Corrects the list of resolved issues in the 3.0.0 entry below. No code change.

Brings the SDK in line with engine main 6acb754ca (2026-09-27). The vendored spec went from 756 to 815 operations and from 1877 to 1996 schemas; every new operation is wrapped. Resolves SDK#17–#44 and #46–#48.

The engine carries no compatibility paths: old wire shapes are refused with 422, not aliased. This release therefore targets the current engine only. An SDK 2.x client talking to this engine breaks where the list below says so, and an SDK 3.0 client does not support an older engine.

  • Path parameters are percent-encoded by the SDK, exactly once. Stop pre-encoding ids (encodeURIComponent(sourceId)), or they are encoded twice (#38).
  • Opaque JSON is sent verbatim. sources.register() config, scenarios.materializeScenario() / update(), webhook action inputs and outputs, agent message content, actionReviews.modify() modifiedParams, oversight step input/output, UI customizations and action values, actions.applyAction inputs, compliance.append extra and review features are no longer snake_cased. Write the engine’s own key spellings (#17, #39).
  • osfql.execute / osfql.preview throw OsfqlError for every OSFQL error body, and every engine deadline (504, error_type: "timeout") is a DeadlineExceededError. A 400 parse error is no longer a BadRequestError: branch on error.errorType (#43).
  • sync: false ingestion returns an IngestionOutcome, not a bare session. Read outcome.kind (#22).
  • ReviewsClient returns typed camelCase objects, not the raw snake_case body typed unknown (#30).
  • Constructed fixtures need the new required fields listed below. Reading code is unaffected.

Transport and errors

  • Every path parameter of every generated route, and of the hand-written paths, is percent-encoded once (#38).
  • ApiError.message is the body’s message for bodies with no error field (OSFQL, SPARQL, property graph). It was HTTP <status>.
  • HTTP 503 is ServiceUnavailableError, a subclass of InternalServerError. instanceof InternalServerError still matches.
  • HTTP 504 with error_type: "timeout" is DeadlineExceededError on every route (was InternalServerError).
  • HTTP 409 write_targets_derived_fact is DerivedFactWriteError (a ConstraintViolationError); 412 is PreconditionFailedError; 428 is PreconditionRequiredError.
  • ValidationError.field paths name their container: rules[k].term, rules[k].antecedents[i], literals[i].term, goal, antecedent, consequent (#48).
  • A raw TermInputDto feature value that matches no variant throws ValidationError before the request is sent. It was sent as {} (#47).

Sources, scenarios, opaque bodies

  • sources.register() sends config verbatim; write engine keys (connection_string, file_path, …) (#17).
  • scenarios.materializeScenario() / update() send the scenario verbatim. MaterializeScenarioRequest.scenario is a typed GeneratedScenario whose keys are the engine’s own, mixed spellings (curriculum_stages, guardConstraints, …). A camelCase curriculumStages is ignored by the engine (#39).
  • webhookActions.register/invoke/completeInvocation, cognitive.sendMessage/broadcastMessage, actionReviews.modify, oversight.judge/refine/createSession/ingestStep, ui.describe, ui.executeAction, actions.applyAction, compliance.append, reviews.addPendingReview and reviews.correctEntity send the caller’s nested JSON verbatim. Keys in customizations, values, originalValues, inputs, extra, review features and the like are no longer snake_cased.
  • DiscoveredFeatureDto.constraints is a tagged DiscoveredConstraintDto union (was the engine’s Rust debug string).

Ingestion (#20, #21, #22)

  • ingestMarkdown(req, { sync: false }) and ingestDocument(req, { sync: false }) return IngestionOutcome ('queued' → session, 'inline' → result).
  • ingestMarkdownBatch(req) returns IngestionOutcome<IngestionBatchSession, IngestMarkdownResponse>; ingestDocumentBatch(req, { sync: false }) returns an IngestionBatchSession (documentIds, documentCount).
  • ingestRdf defaults to synchronous ingestion. IngestRdfRequest loses ownerId, tenantId and sync; pass { sync: false } as the second argument.
  • A ?sync=false document or batch refused by a deployment with no queue throws IngestionQueueUnavailableError (status 503), and is never retried, even with retryOn503.
  • IngestionSession’s polling methods moved to the base IngestionSessionHandle; batch handles are not instanceof IngestionSession.

OSFQL (#33, #34, #43)

  • OsfqlResponse.updatedTermIds is required and disjoint from producedTermIds. Count created and updated terms from the two lists directly.
  • osfql.diagnose(query, requestOptions) is osfql.diagnose(query, options?, requestOptions?).
  • OsfqlPreviewAffected.exact is required.
  • The atomic refusal code drop_sort_in_atomic_program is drop_sort_not_alone. A DROP SORT or ALTER SORT must be the only statement of its program, with atomic: false too.
  • RiskTier gains schema_destructive (DROP SORT).
  • UI dry-run affectedPreview: sampleRows values are tagged OsfqlValues keyed by feature plus term_id; count and statement cover the whole program; exact, mutates and statements are new required fields.

Terms, rules and sorts (#32, #35, #40, #41, #46)

  • terms.termExists(id) returns the engine’s answer. It returned true for every id.
  • FeatureInputValueDto gains the tagged { type, value } member.
  • RuleTermDraftDto.features is Record<string, string | number | boolean>.
  • Offset iterators: when a page reports total, a short page no longer ends the walk.
  • TermVersionDto.seeded is required; versions are listed newest first.
  • RegisterFunctionRequest.tenantId and EvaluateFunctionRequest.tenantId are removed (#35). FunctionDraftDto is an alias of RegisterFunctionRequest.
  • SortReferenceKind gains term_reference. DeleteSortResponse.sortsDeleted is required. deleteSort’s options are the named DeleteSortOptions. The sort-delete refusal body is coded; read it with sortDeleteRefusalOf(error).

Agents, reviews, conversation (#25, #30)

  • Every ReviewsClient method returns a typed camelCase result; failedIds entries are { reviewId, reason }.
  • AddPendingReviewRequest.sortSuggestions is ReviewSortSuggestion[] ({ sortName, confidence, reason? }). The old shape was refused with 422.
  • ConversationClient.getTurns narrows queryResults rows through the OSFQL value mapper.
  • ActionReviewsClient.listPending and the agent lifecycle methods raise ValidationError for an origin or status outside their unions.

UI, checks, reasoning (#28, #37, #44)

  • Removed: constraints.getGraph (use visualization.constraintGraph), fuzzy.fuzzyMerge and fuzzy.fuzzySubsumption (use reasoning.merge / reasoning.subsumption), execution.getTrail and execution.getChoicePoints (no engine route; read trailLength / choicePointCount from execution.getSession). All five called paths no engine serves.
  • UIDescribeRequest.sessionId is removed; the engine refuses it with 422.
  • UIGenerateResponse.data is UIDataPayload; diagnostics and suggestions are string[]; verdict is UIGenerateVerdict.
  • CoherenceAnalyzeResponse.report is a typed camelCase CoherenceReport; CoherenceSummaryDto.verdict is a union.
  • ConformanceViolationDto.suggestedFixes is SuggestedFix[]; an unknown action arrives as { action: 'unrecognized', entry }.
  • CheckFinding.proofTrace is CheckProofStep[] | null.
  • TemporalPlanResponse.diversityScore is optional: absent without a diversity objective.
  • FuzzyProveResponse.certificate is a DiagnosisCertificate with the engine’s snake_case keys, kept verbatim because it is re-verified by deserializing it exactly as sent.

New required fields on response types (fixtures and mocks must add them)

  • SourceSummaryDto.lastSyncAt, SourceDetailResponse.lastSyncAt, IngestFromSourceResponse.warnings, OntologyBridge.BindingSummaryDto.sortName (#29).
  • TranslateRdfResponse / IngestRdfResponse repairedIriCount, repairedIris (and sortPreviews on translate); DocumentStatsDto.relationsIntegrated; EncodeClipResponse.disposition, timingsUs; four FrameSummary arrays.
  • ActionReviewResponse.producedTermIds, PendingActionReviewDto.origin, ten IntegratedCycleOutcomeDto fields.
  • SubscriptionDto.maxRetries, backoffMs.
  • MaterializeScenarioResponse.goalCreated.
  • papersPooled / totalPapersPooled on research cycles, sessions, summaries and statistics.
  • SendMessageResponse.timestamp; EntailmentResponse / DisentailmentResponse degree, antecedentDegree, consequentDegree; MaterializationSummaryDto temporalConstraintsCreated, webhookActionsCreated, toolRegistrationsCreated, warnings; CycleOutcomeSummaryDto.rulesLearned; SortCompareResponse sortA, sortB, operator; LearnedSimilarityDto.meetIsBottom; SimilaritySearchResponse.exhaustive; CreateModuleResponse.success; ImportModuleResponse importerModule, success; AgentSubVerdictDto.stepIndices; OversightSessionStatusResponse.userIntent; SoftUnifyResponse.embeddingsUsed.

Engine behaviour that reaches SDK callers

  • memory.remember answers 400 for an out-of-range importance or a policy-refused write. setActionReviewConfig answers 400 for a zero timeout. A timed-out review decision answers 409.
  • subscriptions.create / update refuse loopback and private targets with 400.
  • Speech: an unreachable sidecar is 502; 503 is always the permanent, coded speech_synthesis_not_configured.
  • compliance.list refuses undeclared query keys with 400 (the SDK sends only declared keys).
  • A DELETE of a rule’s conclusion answers write_targets_derived_fact, not term_named_by_rules.
  • New clients: client.cron, client.metrics, client.inbox, client.pairing, client.channels, client.platform (backups, branches, config sections, connector health, diagnostics, identities, onboarding, skills), client.plugins (WASM plugins), and client.memory (#26, #31). Type namespaces Cron, Metrics, Inbox, Pairing, Channels, Platform, Plugins, Memory.
  • Sources: uploadFile, listFiles, deleteFile (#18).
  • Ingestion: queueMetrics, listDlq, countDlq, abandonDlqEntry, recoverStuck, reprocessFailed (#23); ingestUrl, getWebCapabilities; ingestAudio, getAudioJob; the full IngestionConfigDto (#21); RDF repairIris, computeEmbeddings, keepDeclarations (#20); merge-audit evidence and the other stats fields; the V-JEPA encodeClip receipt and per-frame verdicts.
  • Cognitive: getActionReviewConfig, pauseAgent, resumeAgent, stopAgent (#24); actionReviews.expire; reviewQueueRefusals and nine cycle counters.
  • Conversation: sendMessageStream, injection, includePromptTrace, promptTrace, matchedTemplates; root exports decodeConversationMessage and USER_DATA_FIELDS (#25).
  • OSFQL: OsfqlRequest.semantics ('osf' | 'rdf') (#33); OsfqlError (errorType, refusal with referrers and cascadeLimit, maxRows) and DeadlineExceededError (#43); diagnose options timeoutMs / maxRows; OsfqlResponse.sortDelete / sortReparent; OsfqlPreviewStatement.affectedError / sortDelete.
  • Terms and rules: TermResponse.etag; ifMatch on updateTerm, deleteTerm, deleteRule, replaceRule (#32); rejected on bulk rule and fact writes (#46); iterateTerms offset and onPage (#40).
  • Sorts: previewSortDelete, subtree delete (descendants: 'delete'), reparentSort, sortDeleteRefusalOf, feature description and index.
  • Ontology: checkOwl2, pipelineStatus, qualityReport, competency questions, enrich (#19). UI: listPages (#28), UIGenerateResponse.conversationId / dataSources / intents / trace, UIActionResponse.retractedTermIds. NL query TRIZ fields (#27). Typed coherence report and suggested fixes (#37).
  • Request fields that unlock response fields: fuzzyProve seal and kbDataVersion, enrollSpeaker variant, visualization.lattice limit / offset, visualization.hypergraph includeProvenance / includeRelations.
  • OsfqlError.sortDeleteRefusal: the coded refusal of a DROP SORT.
  • Compliance: getRecord, getCausalChain, listScopes, getInclusionProof, list filters actor / q; RequestOptions.causal and the x-osfkb-* header helpers (readAuditHeaders, readCausalRefusal, sealPreimage, …).
  • subscriptions.update; admin.getMyTenant; authz.writeTuple / listTuples / deleteTuple / check; errorBodyCode.
  • sources.register() rewrote user-keyed maps in config, including MCP transport.env / transport.headers, which lost credentials (#17).
  • A path id holding /, ?, #, % or a space addressed the wrong route (#38).
  • The sources, bridge, RDF, ingestion, conversation, nlQuery, ui.generate, action-review, cycle and subscription mappers dropped fields the engine sends (#20, #21, #25, #27, #28, #29).
  • Async ingestion returned sessions with undefined status and discarded inline results (#22).
  • reviews.addPendingReview with sort suggestions was refused with 422 (spec collision, engine #224).
  • iterateTerms stopped after one page when pageSize exceeded the engine’s 10 000-row cap (#40).
  • terms.termExists returned true for every id.
  • Five methods called unserved paths (#44).
  • 73 response fields in 27 DTOs that the engine sends were dropped by the normalizers; all are mapped. scripts/check-normalizer-drops.mjs runs in the unit suite, so a field added to the spec and not mapped fails the tests.
  • A Gaussian, CyclicGaussian or GaussianProduct FuzzyNumber read stdDev: undefined; std_dev is mapped in both directions.
  • ui.describe customizations lost camelCase component props (dataKey became data_key, which the engine’s remove_component / reorder lookups then missed).
  • compliance.append rewrote the keys of extra, which the engine hashes into the audit chain.
  • A 503 that no retry can change (a queueless sync: false ingestion, speech synthesis not configured) is never retried, even with retryOn503.
  • updatedTermIds and producedTermIds are disjoint (#34); every OsfqlAtomicRefusal.code, OsfqlRefusal.code, repair kind and diagnostic kind is listed.
  • terms.listVersions is newest first (#36); BigIntegerValue states the engine’s current type-hint rules (#41); ProofDto.ruleHeadTermId groups by rule (#42).
  • The engine’s current status codes and refusals on the changed routes: cron timezone, DLQ abandon, document check, derived conclusions, function registration, sort delete, generate citations, visualization constraints, ontology transpile, TRIZ, action-review deadlines.
  • reasoninglayer#363 (duplicate operationId), #364 (undeclared statuses, headers and refusal bodies), #365 (OSFQL sort-statement run errors), #366 (review stores), #367 (audio ingestion_config), #368 (duplicate diagnostic), #369 (channel routes outlive their tenant), #370 (branch scoping), #371 (tuple write refusal order), #372 (untyped coherence report), and a new collision on #224. IngestionConfigDto.batchSize / maxSchemaTokens are deprecated no-ops until engine #352 removes them.

These entries were collected under an “Unreleased” heading from 1.2.1 through 2.1.2, and were not split per release.

  • ClientConfig.auth is required. new ReasoningLayerClient({ baseUrl, tenantId, timeoutMs }) was valid in 1.27 and now throws at construction: ClientConfig.auth is required — pass { mode: 'bearer', token } or { mode: 'cookie' }.

    It is called out here because of the failure mode, not the change: the throw is at RUN TIME, so a clean typecheck of an app says nothing about the scripts, seeds and ad-hoc measurement code that also construct a client. Grep for new ReasoningLayerClient rather than trusting the build.

  • osfql.execute() carries updatedTermIds. The engine puts an updated term’s id in both produced_term_ids and updated_term_ids, and the mapper read only the first — so a client could not tell an insert from an update, or tell whether an UPDATE pattern matched anything. undefined means the engine does not report it; [] means the program updated nothing. (Superseded in 3.0.0: the engine now keeps the two lists disjoint, and updatedTermIds is always present.)
  • forwardChain() sends timeoutMs. The field was typed and documented (a server-side deadline, 504 when it passes) and the mapper never emitted it, so a caller setting a deadline silently got the 300 s backstop. backwardChain() beside it always sent it.
  • compliance.summary() carries the risk block and nOversightDecisions. The mapper named six fields and stopped, dropping fourteen risk counters and the oversight count — the EU AI Act Article 15 and Article 14 evidence. risk.fabricationRate stays nullable: null is “not measured”, and its denominator is checked responses only, so folding it to 0 would report a clean rate for a ledger nobody checked.
  • conformance.check() returns the violations the route actually sends. They were mapped through ViolationDto — a different struct sharing no key with the response — so every violation arrived with description, relatedFactId and severity all undefined: a 200, an array of the right length, and no detail. conformance.repair() was never affected.
  • The sources mappers carry four fields they dropped: DiscoverableTypeDto.columnCount, DiscoveredFeatureDto.featureName, DiscoveredFeatureDto.identifierProvenance, and SourceTypeDto.configFields. identifierProvenance is the one that mattered most — it separates a declared primary key from a sampled guess, and a key that is not unique makes a re-ingest duplicate facts rather than update them.
  • osfql.preview() no longer loses the plan over one sample row. An unrecognised value tag in one of ten ILLUSTRATIVE sample rows used to fail the affected block, the statement, and the whole response, and preview() threw a bare ReasoningLayerError carrying no status. The row is now dropped and counted in affected.unparsedSampleRows; the count, the risk and mutates survive.
  • A malformed term request raises ValidationError, not TypeError. TermInputDtoFromFrontToApi discriminates with in, which throws on undefined, so a request missing term produced an error from library internals that escaped every instanceof branch over the SDK hierarchy. It now names the field, down to the row and the feature (antecedents[1], term.features.tier).
  • OntologyBridgeClient.executeTranspiled() — POST /api/v1/ontology/query. It was typed on the generated api and never lifted onto the client. The second half of the transpile pair: transpile emits the native query and stops, this runs it — and for a transpile-mode source, whose bound sorts are live views over the operator’s own database, it is the only route that answers at all. rowCount / sourceId are camelCased; rows is one object per record.
  • ConstraintDto gains RealRange — { type: 'RealRange', value: { min, max } }, the Real twin of IntegerRange. The engine has always defined it; consumers were casting past the published union.
  • New types: RiskTotalsDto, ConformanceViolationDto, ConfigFieldDto, ExecuteTranspiledRequest, ExecuteTranspiledResponse.
  • ReferenceDesignator says which spelling works. Pass sortName; the sort_name spelling shown for the wire is refused, because a designator sits inside a features map and features is carried verbatim. The compiler does not catch the swap — ReferenceValue.value is string | ReferenceDesignator, so an object with the wrong key stays assignable — and the engine’s refusal names a byte offset and no feature.
  • OsfqlClient.preview’s remarks no longer contradict OsfqlPreviewAffected.count. A destructive statement matching zero rows answers {count: 0, exact: true}; it is the ABSENCE of the whole affected block that means the statement has no affected-row set. The two are distinguishable, which is what the block was separated for.
  • OsfqlPreviewAffected.exact narrowed to what the engine gives. true for every top-level statement; a nested statement is always an upper bound, even when its enclosing guard names no variable. Do not derive it from the shape of the program.
  • BulkRefusedError is right about dryRun with partial. The two together answer 200 with termsAdded: 0 and every row in errors — they do not throw, because a dry run that wrote nothing has nothing to refuse. That pair is the only shape that answers both halves of a forecast in one call.
  • ProofDto.goalTermId is now optional, and it MEANS something different. It used to name the rule’s instantiated head: one id shared by every solution of a goal, and an id getTerm() answered 404 for. It now names the CONCLUSION the node proved, which joins to client.query.findBySort() and client.terms.getTerm(), and two solutions carry different ids when they prove different conclusions. It is ABSENT when no conclusion was materialised — a goal proved with no preceding forward chain stores nothing. Assigning it to a string no longer compiles, which is the point.

    The old id has not gone away; it moved to the new ProofDto.ruleHeadTermId.

  • ProofDto.ruleHeadTermId — the rule’s instantiated head, present on a rule application when it differs from the conclusion. A grouping key, NOT a fetchable id: the term routes refuse it deliberately.
  • SortsClient.getQuotientOrder() — the Definition IV.9 quotient order over the caller’s own lattice: the equivalence classes with their degrees, and the fuzzy partial order between them. Prefer it over getEquivalenceClasses(), which computes process-wide and then filters.
  • granularity on getPreorderDegree() — read the degree from either of the paper’s two preorders. similarity_deleted (Definition IV.5) is the default here and is what the graded GLB is computed from; combined (Definition IV.1) treats a similarity edge as a step, so a directly-similar pair answers its similarity degree instead of 0. The response always echoes the granularity back. ⚠️ getQuotientOrder() takes the same two spellings and defaults to combined.
  • New types: SortPreorderGranularity, GetQuotientOrderOptions, GetQuotientOrderResponse, QuotientClassDto, QuotientOrderEdgeDto.
  • nafProve() / proveWithNegation() accept a psi() term. Every other term-carrying method did; these two demanded a hand-built wire TermInputDto, so the documented way to build a term did not work there.

  • The preorder-degree documentation wrote Definition IV.5 as ((similarity - subsumption) union subsumption)^+, reading .- as an arithmetic difference. It is pair DELETION, and the operands were wrong too.

  • FlowNetworksClient (client.flowNetworks) — generic capacitated directed-graph resource. Persist a graph once, then create / get / clone / commit / solve it under any of five algorithms: max_flow, min_cost_max_flow, classify_edges, classify_edges_optimal, min_cut. Mirrors the lifecycle of the /spaces resource (create, clone, commit, solve) and exposes the same Dulmage–Mendelsohn edge trichotomy (always_used / never_used / sometimes_used) the scheduling engine returns over (agent, day, shift) cells, generalised to arbitrary edges.

  • New types: FlowAlgorithm, EdgeSpec, EdgeFlow, EdgeClass(ification), MinCutReport, EdgeCapacityUpdate, CreateFlowNetworkRequest, FlowNetworkResponse, CommitFlowNetworkRequest, SolveFlowNetworkRequest, SolveFlowNetworkResponse (exported under the FlowNetworks namespace).

  • New guide: Flow Networks.

  • BackwardChainRequest.expandBindings and BackwardChainResponse.referencedTerms (the Record<string, TermDto> sidecar map). These existed only to support a prior backend feature where scheduling was solved as a backward-chain query returning deeply-nested Ψ-term lists; the backend has dropped that path in favour of the dedicated scheduling and flow-network endpoints, and the SDK fields are no longer wired to anything.

    Note: TermDto.referencedTerms (a different field — Record<string, ReferencedTermSummary> on the term-retrieval surface) is unaffected.

Initial release of the @kortexya/reasoninglayer TypeScript SDK.

  • ReasoningLayerClient main client with resource-as-property access pattern
  • ClientConfig with baseUrl, tenantId, and optional auth headers
  • HTTP layer with automatic retry (429, 503), exponential backoff with jitter, and configurable timeouts
  • Error hierarchy: ReasoningLayerError, ApiError, ConstraintViolationError, RateLimitError, TimeoutError, ValidationError, NetworkError
  • .withMetadata() on all resource clients for access to HTTP status, headers, and rate limit info — this never shipped. The entry was written from the design and no resource client grew the accessor. Use an interceptor for a successful response’s status and headers, and ApiError’s own status / headers on a failure.
  • WebSocket client with auto-reconnect for real-time event subscriptions
  • ValueDto tagged discriminated union (10 variants: String, Integer, Real, Boolean, Uninstantiated, Reference, List, FuzzyScalar, FuzzyNumber, Set)
  • FeatureValueDto untagged union for homoiconic inference endpoints
  • FuzzyShapeDto with "kind" discriminator (Triangular, Trapezoidal, Gaussian, CyclicGaussian)
  • TermInputDto structural union (Reference, Inline by sort ID, Inline by sort name)
  • FeatureInputValueDto structural union (10 variants including ConstrainedVariable and Variable)
  • Full DTO types for all domains: sorts, terms, inference, query, fuzzy, cognitive, constraints, namespaces, collections, execution, causal, ingestion, reviews, visualization, ILP, reasoning, statistical, control, spaces, row, sources, communities, utilities, scenarios, action-reviews, discovery, extract, scheduling
  • Value namespace: tagged value construction (Value.string(), Value.integer(), Value.fuzzyNumber(), etc.)
  • FuzzyShape namespace: membership function shapes (FuzzyShape.triangular(), FuzzyShape.gaussian(), etc.)
  • FeatureInput namespace: untagged value construction for inference (FeatureInput.string(), FeatureInput.variable(), FeatureInput.constrainedVar(), etc.)
  • TermInput namespace: term input construction (TermInput.byName(), TermInput.byId(), TermInput.ref())
  • guard() function: shorthand for guard constraints with 6 operators (lt, lte, gt, gte, eq, ne)
  • allen() function: Allen temporal interval constraints with 13 relations
  • SortBuilder fluent API: chained sort definition with parents, features, bound constraints, and descriptions
  • psi() shorthand for inline Psi-term construction with auto-coercion
  • SortsClient: createSort, getSort, deleteSort, listSorts, bulkCreateSorts, isSubtype, computeGlb, computeLub, decodeGlb, compareSorts, getChildren, getParents, getAncestors, getDescendants, getCompatible, indexSorts, updateReviewStatus
  • TermsClient: createTerm, getTerm, updateTerm, deleteTerm, termExists, bulkCreateTerms
  • InferenceClient: addRule, addFact, bulkAddRules, bulkAddFacts, getFacts, clearFacts, backwardChain, forwardChain, forwardChainTagged, fuzzyProve, bayesianPredict, nafProve, createGoal, getGoals, getGoal, deleteGoal, getMetaSorts
  • QueryClient: findUnifiable, findBySort, osfSearch, validateTerm, validatedUnify, nlQuery
  • FuzzyClient: fuzzyUnify, fuzzyMerge, fuzzySubsumption, searchTopK, predictEffect
  • CognitiveClient: createAgent, getAgent, deleteAgent, getState, runCycle, addBelief, addGoal, addRule, addSort, recallEpisodes, getEpisodeStats, addHtnMethod, sendMessage, broadcastMessage, markMessagesRead, provideFeedback, subscribeToEvents (WebSocket)
  • ConstraintsClient: constraint sessions, one-shot solving, graph visualization
  • NamespacesClient: namespace hierarchy, imports, metadata
  • CollectionsClient: collection CRUD, tagging
  • ExecutionClient: Execution sessions, backtracking, goal stack
  • CausalClient: Pearl’s causal hierarchy, interventions, counterfactuals, root cause analysis
  • IngestionClient: document ingestion, markdown, RDF, session management
  • ReviewsClient: human-in-the-loop review operations (approve, reject, merge, re-extract)
  • VisualizationClient: lattice, hypergraph, GLB/LUB trace, residuation state, trigger dependency
  • IlpClient: inductive logic programming (pattern learning, evaluation, GFlowNet, synthesis)
  • ReasoningClient: entailment, evidence assessment, residuation, resource coordination, temporal planning
  • StatisticalClient: correlation, partial correlation, conditional independence, causal discovery
  • ControlClient: Prolog-style proof control (cut, findall, forall, NAF, conditionals, global state)
  • SpacesClient: computation spaces (create, clone, commit, search)
  • RowClient: row polymorphism (schema-flexible search, unification, integration, dynamic queries)
  • SourcesClient: data source registration, schema discovery, ingestion
  • CommunitiesClient: community detection, memberships, search
  • UtilitiesClient: string operations, relational arithmetic, term copying
  • ScenariosClient: scenario CRUD and verification
  • ActionReviewsClient: autonomous action approval (approve, reject, modify, bulk operations)
  • DiscoveryClient: causal effect discovery and prediction
  • ExtractClient: entity extraction from text
  • SchedulingClient: capacitated bipartite b-matching for staff rostering and assignment problems — feasibility() returns the per-cell trichotomy (confirmed_true / confirmed_false / free) under hard rules; optimize() adds soft preferences and returns the trichotomy across every optimal rota
  • SDK_VERSION exported constant (0.1.0)
  • X-SDK-Version header sent on every HTTP request for backend diagnostics
  • scripts/generate-route-manifest.ts — generates endpoint manifest from OpenAPI spec
  • scripts/check-completeness.ts — reports SDK endpoint coverage vs. OpenAPI spec
  • Dual ESM/CJS output via tsup
  • TypeScript strict mode with zero any types
  • Zero runtime dependencies
  • ES2022 target (Node 18+, modern browsers, Deno, Bun)
  • Vitest test framework
  • pnpm package manager with strict resolution