コンテンツにスキップ

ontary — API リファレンス

English · 日本語 · ← README

リファレンス — ontary の公開名を調べるための一覧ページで、使い方ははじめにとオントロジーのテストで確認できます。

ontary のキュレーションされたフロントドア: __all__ の 46 個の名前。 残りのエンジン API は、ontary.meta、ontary.store などの 定義元サブモジュールから利用します。

これは調べ物のためのドキュメントです。「宣言する → バインドする → 読む → 配信する」 という流れの解説は README から始めてください。

目次


フロントドア

__all__ はソート済み・重複なし・import 可能で、ちょうど 46 個です。オントロジーの 作者がエンジンの名前空間を選ばずに使う名前だけをここに置きます。

Authoring vocabulary / 宣言用語彙

ActionContext、ActionParams、BoundQuery、CapabilityHandle、Cardinality、 Consumer、CustomResolver、DirectProperty、Event、FunctionParams、LinkHandle、Ontology、 OntologyObject、RowVisibilityStore、SelfScope、Sensitivity、Source、 Store、ViaLink、prop、ref、scope_ref、target。

Runtime entries / ランタイム項目

Declarations、EventRecord、Finding、InMemoryStore、MCPServer、ObjectStore、OntologyClient、 Page、PostgresStore、ScopePolicy、TypedPage、 __version__、build_mcp_server、declarations。

MCPServer は mcp SDK のサーバークラスです。MCP ビルダーの戻り値なので再エクスポートしています。 利用には [mcp] extra が必要です(pip install 'ontary[mcp]')。 extra がなくても import ontary は動きます。MCPServer に触れたときだけ、 インストールコマンドを示す ImportError になります。core のみの環境では from ontary import * も同じ ImportError になります。__all__ の全名前を取得するためです。

Error classes / 例外クラス

ActionError、AuthorityError、ConflictError、InternalError、OntaryError、 PermissionDenied、PreconditionFailed、ValidationFailed、VisibilityError。

この 46 個という個数は tests/test_docs.py が厳密に検証するため、root export の増加を 見落としません。

from ontary import Ontology, OntologyObject, Consumer, prop, target, Cardinality

Python 3.12+。コアパッケージの依存は pydantic のみ。エクストラ: [mcp](MCP サーバー)、[postgres](PostgresStore バックエンド)。


エンジン API

以下の名前は意図的にフロントドアへ平坦化していません。エンジンを拡張したり高度な 統合を行ったりするときは、定義元サブモジュールから import してください。

ontary.actions

ActionExecutor。

ontary.audit

CapabilityAccessRecord。

ontary.client

OntologyRuntime。

ontary.errors

ERROR_CODES、ErrorCodeInfo、Kind。

ontary.functions

FunctionHandler、FunctionRegistry。

ontary.ingest

IngestError、IngestReport、bulk_link、bulk_upsert。

ontary.mcp_server

ConsumerResolver、build_multi_consumer_mcp_server。

ontary.meta

ActionParameterDef、ActionTypeDef、FunctionDef、LinkTypeDef、 ObjectTypeDef、OntologyRegistry、PropertyDef、StructFieldDef、TransitionDef、RuleDef、PropertyType、ScopeLevel。

ontary.ontology

OntologyDef。

ontary.query

GuardedQuery。

ontary.scope

Direction、RowVisibilityFn、ScopeRule、resolve_contributor、 resolve_owning_scope。

ontary.security

ConsumerKind、covers_scope。

ontary.store

AuditEntry、DEFAULT_BATCH、DEFAULT_TENANT、Lineage、 SCHEMA_VERSION、StoredObject、WriteRecord。

ontary.testing

SDK 利用者向けのテストヘルパーは make_store、consumer、raises_code、 FixedClock、SequentialIds、Scenario、scenario です。make_store(ontology) は空の InMemoryStore を新しく作り、consumer(...) は有効な Consumer を組み立て、 raises_code(code) はメッセージではなく機械可読なコードでエラーを検証します (ontary.ingest.IngestError を含む任意の OntaryError に加え、安定した文字列 .code を公開する構造的に互換な作者定義のコード付き例外にも一致します)。 FixedClock(start) はタイムゾーン付きの同じ 日時を毎回返し、naive な start は拒否します。SequentialIds(prefix) は prefix-1、prefix-2、…という決定的な ID を返します。

scenario(ontology, *, store=None, clock=None, id_factory=None, capabilities=None) は、即時実行でチェーン可能な Scenario を返します。ストア、クロック、ID ファクトリは 準備の書き込みより前に一度だけバインドされます。既定は、新しい InMemoryStore、 2026-01-01T00:00:00Z の FixedClock、SequentialIds("id") です。上書きするときは 空のストアを渡してください。すべてのメソッドは同じ Scenario を返します。

メソッド 意味
given(*objects) 型付きの OntologyObject インスタンスを準備します。最初の when の前でのみ使えます。ストアが拒否した場合は、オブジェクトとエラーコードを示す AssertionError として再送出されます。
given_link(handle, from_, to) 型付きの LinkHandle でリンクを準備します。端点はオブジェクトまたは id 文字列です。最初の when の前でのみ使えます。
when(params, *, by) 1 つの ActionParams を、コンシューマー by(必須)として権限管理下で実行します。直前のステップが失敗し、then_error で検証されていない場合は、先に AssertionError を送出します。
then(cls, pk, **fields) 直前のステップが成功していることを要求し、指定した各プロパティを、マスキングされていない現在の行と == で比較します。宣言されていないフィールドは INVALID_RECORD になります。
then_result(expected) 成功していることと、戻り値が expected と等しいことを要求します。
then_error(code) code で失敗していることと、現在のオブジェクトとリンクがステップ前の状態と等しいことを要求します。失敗を検証済みにするのはこのメソッドだけです。
then_absent(cls, pk) 現在の行が存在しないことを要求します。成功後でも失敗後でも使えます。
then_link(handle, from_, to) リンクが現在の状態に存在することを要求します。
then_no_link(handle, from_, to) リンクが現在の状態に存在しないことを要求します。

検証に失敗すると AssertionError になります。すべてのシナリオは then* で終わります。 when で終わるシナリオは何も検証しません。


オントロジーを宣言する

Ontology(name, scope_levels, min_n=3)

宣言のファサード。OntologyRegistry、ScopePolicy、宣言されたハンドラを蓄積し、 ランタイムを払い出します。

引数 型 備考
name str MCP サーバー名の既定値にもなります。
scope_levels list[str] 自分で決める階層(粗い方を後ろに)。例: ["queue", "org"]。組み込みのレベルは存在しません。
min_n int = 3 集計に必要な異なる寄与者数の下限。

空または重複した scope_levels、1 未満の min_n は、.definition を最初に 構築した時点で ValidationFailed(ONTOLOGY_INVALID)として拒否されます。

宣言用メソッド(link 以外はデコレータ):

メソッド 役割
@ontology.object(...) OntologyObject サブクラスをオブジェクト型として登録
ontology.link(api_name, from_cls, to_cls, cardinality, ...) リンク型を登録し LinkHandle を返す
@ontology.action(params_cls, ...) 型付き Action ハンドラを登録
@ontology.function(...) 導出値 Function を登録
ontology.capability(proto, ...) Capability を宣言し CapabilityHandle を返す
ontology.validate(store=None) 検証して登録を凍結。ストアを渡すと、もう復元できない保存済み行も拒否
ontology.diagnose(store=None) 例外も凍結もせず全 Finding を返す。ストアを渡すと行もスイープ
ontology.bind(store, ...) OntologyRuntime を構築

validate()(および .definition への接触)はオントロジーを凍結します。以降の object/link/action/function 呼び出しは例外になります。すべての宣言を終えた あとに 1 回だけ呼んでください。

教えてくれるチェック。 以前は validate() を通って後で失敗していた 2 つの モデリングミスを、書いた場所で検出します。

  • target(...) パラメータがすべて target= と別の型を指す Action は、 @ontology.action(...) の時点で ONTOLOGY_INVALID として拒否されます。 メッセージには Action、パラメータ、両方の型が入ります。以前はスコープゲートが パラメータ側のオブジェクトを検査し、監査エントリと MCP は target= を示していました。 target() パラメータのうち 1 つは target= の型を指す必要があります。 別の型を指す追加の target() パラメータは許されます。手組みの OntologyRegistry にも validate() と diagnose() が同じ規則を適用します。
  • オントロジー編集(プロパティの必須化、型の変更、choices の絞り込み)より前に 書かれた行は、最初に読んだときにだけ失敗します。ストアはオントロジーの 指紋を記録しないためです。diagnose(store=store) は Store.read_all で現在の 行をすべて読み、型とプロパティごとに INVALID_RECORD の Finding を 1 件返します。 Finding には復元に失敗する行数が入ります。validate(store=store) はそれらを INVALID_RECORD として送出します。全行を読むため、スイープはストアを渡したとき だけ動き、bind() では動きません。
ontology.validate(store=store)  # 編集したオントロジーを提供する前に

@ontology.object(*, layer, owned=False, api_name=None, description=None, display_name=None, scope: Literal["unscoped"] | Sequence[ScopeRule] | None = None, contributor: Sequence[ScopeRule] | None = None, row_visibility: RowVisibilityFn | None = None)

デコレート対象の OntologyObject サブクラスを登録します。

  • layer — 自分で決めるグルーピング文字列("L0"、"core"、何でも)。 エンジンの挙動には一切影響せず、MCP のイントロスペクション (list_object_types)にそのまま載るので、エージェントやツールから「作者が どうグルーピングしたか」が見えます。
  • owned — True で型全体をオントロジー所有に(どのソースも書き込めません)。 dict を渡すと特定プロパティを既定値付きで所有扱いに(例 owned={"escalated": False})。
  • scope — "unscoped"、またはスコープルールのリスト(スコープポリシー 参照)。解決は宣言されたレベルごとに走ります。各レベルについて、ルールは宣言順に 試され、そのレベルを対象としていて かつ 値が解決できた最初のルールが採用されます。
  • contributor — 行の背後にいる人物を特定するスコープルール。min-N の計数に使われます。
  • row_visibility — (store, consumer, obj_id, payload) -> bool。スコープの上に 重ねる行単位の追加ゲート。
  • accept — この型が受け入れる lint コード(FORBIDDEN_TYPE_NAME、AUDIT_TYPE) 1 つ、またはそのシーケンス。snapshot — True でスナップショット型として宣言。 どちらもアドバイザリーな findingで説明します。

scope・contributor・row_visibility はデコレート時に形を検査します。 "unscoped" の綴り間違い、リストで包んでいない単一ルール、callable でない row_visibility は、その場で ValidationFailed(ONTOLOGY_INVALID)になります。 メッセージにはクラス名・引数名・渡された値・受け付ける形が入ります。Literal 注釈により、綴り間違いの文字列は mypy でもエラーになります。

ontology.link(api_name, from_cls, to_cls, cardinality: Cardinality | Literal["ONE_TO_ONE", "ONE_TO_MANY", "MANY_TO_ONE", "MANY_TO_MANY"], *, description=None, identity_revealing=False, owned=False) -> LinkHandle

cardinality は Cardinality 列挙体のメンバー、またはその名前文字列: ONE_TO_ONE、ONE_TO_MANY、MANY_TO_ONE、MANY_TO_MANY。それ以外の文字列は ValidationFailed(ONTOLOGY_INVALID)で拒否され、メッセージにこの 4 つの名前が 列挙されます。Literal 注釈により、綴り間違いは mypy でもエラーになります。

identity_revealing=True は、人間コンシューマーからの traverse を拒否することを 意味します(AI コンシューマーは辿れます)。返される LinkHandle を client.traverse(link_cls, from_obj_or_id) の第 1 引数に渡すと型付きの結果が得られます。

OntologyObject

宣言するオブジェクト型の基底クラス。pydantic.BaseModel のサブクラスなので、 バリデータも computed field も通常の注釈付き属性もそのまま使えます。

読み取り時にエンジンが 2 つの属性を付与します。これらはモデルのフィールドでは ありません(宣言したプロパティと衝突しません)。

属性 型 意味
lineage Lineage \| None 行の出自と有効期間
redacted_fields frozenset[str] このコンシューマーに対して伏せられたプロパティ

redacted_fields は「あなたには見えない」と「実際に None が入っている」を区別する ためのものです。可視だが値が無い optional フィールドも None になりますが、 こちらには決して現れません。

prop(*, primary_key=False, sensitivity=None, scope_level=None, required=None, property_type=None, choices=None, transitions=None, **field_kwargs)

pydantic.Field(...) にオントロジーのメタデータを足したもの。認識されない kwargs は そのまま Field に渡るので、prop(default=None, description="...") は期待どおりに 動きます。prop() は並行するフィールド体系では決してありません。同じクラス上で素の 注釈付きフィールドや Field(...) もそのまま機能します。

sensitivity を制限したプロパティは X | None で宣言する必要があります。 そうでない場合、クラス登録時にコード付きのバリデーションエラーになります — リダクションされた読み取りが None を返せる必要があるためです。

choices=["open", "closed"] は str プロパティの値をその集合に限定します。 それ以外の値は、どの書き込み経路でも拒否されます。

accept="STORED_DERIVABLE"(または "FREE_TEXT_STATUS"、あるいはそれらのシーケンス)は、 そのプロパティに対するアドバイザリーな finding を accept します。 アドバイザリーな findingを参照してください。

transitions=TransitionDef(initial=(...), moves={...}) は choice プロパティで 許可する状態遷移を宣言します。文字列、または値が文字列の Enum メンバー (通常の Enum または StrEnum)を状態として指定できます。 主キーには transitions を宣言できません。

グラフは prop(transitions=...) で指定し、TransitionDef は ontary.meta から import します。ontary.__all__ からは公開されません。

Ontology.rule(cls, name, *, message)

@ontology.object で cls を登録した後、型付きの述語をデコレートします。

@ontology.rule(Order, "shipped_needs_payment", message="payment required")
def shipped_needs_payment(order: Order) -> bool:
    return order.status != OrderStatus.SHIPPED or order.paid_at is not None

述語は保存済みの行全体から復元されたオブジェクトを受け取り、そのオブジェクトだけを 読み取る必要があります。名前とメッセージは型宣言に表示され、関数本体はエクスポート されるスキーマデータには含まれません。ontology.definition でオントロジーが固定 されるまでルールを登録できます。

選択肢プロパティ: Enum と Literal

プロパティに、値が文字列の Enum(StrEnum など)か、文字列の Literal[...] を 注釈します。これは str と choices の組と同じ形を宣言します。PropertyDef は type="str" になり、choices は宣言順のメンバー値になります。

class Status(StrEnum):
    OPEN = "open"
    CLOSED = "closed"


@ontology.object(layer="L0", scope="unscoped")
class Ticket(OntologyObject):
    id: str = prop(primary_key=True)
    status: Status
    kind: Literal["bug", "task"]
  • 保存。 ストアはメンバーの文字列値を保持します。すべての書き込み経路が Enum メンバーとその値の両方を受け付けます。対象は ingest、ctx.create、 ctx.save、Action パラメータ、where フィルタです。それ以外の値は拒否されます (書き込みでは INVALID_RECORD、Action では INVALID_PARAMS)。where フィルタは 選択肢外の値を拒否しません。どの行にも一致しないだけです。
  • choices を宣言した場所だけ。 Enum メンバーを値に展開するのは宣言であり、 Python の型ではありません。prop(choices=...) のプロパティは、値が選択肢に含まれる メンバーを受け付けます。同じ宣言だからです。choices の無い素の str プロパティは、 従来どおり Enum メンバーを拒否します(書き込みでは INVALID_RECORD、where フィルタでは OPERATOR_TYPE_MISMATCH)。
  • 読み取り。 型付きの読み取りは Enum メンバー(Literal なら文字列)を返すので、 mypy がフィールドの型を絞り込みます。dict の読み取り、MCP の結果、 aggregate_by のキーは素の文字列を返します。
  • Action パラメータ。 ActionParams のフィールドに同じ注釈を付けると、その ActionParameterDef に choices が付きます。型付きハンドラはメンバーを受け取ります。 str のパラメータに prop(choices=[...]) を付けても、プロパティと同じく choices が付きます。ハンドラは文字列を受け取ります。
  • MCP。 list_object_types と list_action_types は、許される値を choices に 列挙します(無い場合は null)。
  • 宣言時に拒否されるもの(ONTOLOGY_INVALID): IntEnum や Literal[1, 2] の ような文字列でないメンバー、選択肢の注釈と prop(choices=...) の併用、主キーへの 選択肢の注釈です。

Struct プロパティとパラメーター

プロパティまたは ActionParams のフィールドに、フラットな Pydantic BaseModel を 注釈します。たとえば amount: Money は struct プロパティを宣言します。Action パラメーター にも同じ注釈を付けられます。

class Money(BaseModel):
    value: float
    currency: str


class Order(OntologyObject):
    amount: Money

生成される PropertyDef または ActionParameterDef は type="struct" と、 StructFieldDef の空でない fields tuple を持ちます。各内側の宣言には name、 scalar の type(str、int、float、bool、date、datetime)、任意の文字列 choices、および required が含まれます。struct 以外の宣言では fields=None です。 外側を Money | None と注釈するとプロパティまたはパラメーターが optional になり、 内側の optional な注釈はそのフィールドを optional にします。dict で省略した optional な 内側のフィールドは明示的な null として保存され、モデルにデフォルトがなくても型付き読み取りでは None になります。

書き込みにはモデルのインスタンスか、それと同等の dict を渡せます。内側の宣言に対して 検証されます。不正なオブジェクトの書き込みは INVALID_RECORD、不正な Action パラメーターは INVALID_PARAMS になり、メッセージは amount.currency のような内側の パスを示します。型付き読み取りはモデルを返し、文字列および MCP の読み取りは通常の オブジェクトを返します。struct の更新は値全体を置き換えます。sensitivity は プロパティ全体に適用されます。

struct はフラットです。入れ子のモデル、モデルの list や map、内側の alias、内側の プロパティメタデータ、RootModel、struct の主キーは宣言時に拒否されます。フラットな BaseModel 注釈に prop(property_type="json") を指定すると不透明な JSON として保存されますが、その他の 明示的な property type は拒否されます。内側のフィールドによる 検索はサポートされません。struct プロパティに対する where 条件は OPERATOR_TYPE_MISMATCH、order_by は INVALID_PARAMS、group_by は INVALID_GROUP_BY になります。数値集計関数は NON_NUMERIC_AGGREGATE になります (count は対象外です)。MCP の list_object_types と list_action_types は、すべての プロパティとパラメーターに fields を含めます。struct 以外は null、struct は name、type、required、choices を持つオブジェクトの list です。

フィールドマーカー: ref / target / scope_ref

いずれも OntologyObject サブクラスを取り、追加の kwargs は Field に渡します。

マーカー 宣言内容
ref(cls) このパラメータは cls のオブジェクトを指す
target(cls) …かつ Action の対象である(スコープはこれに対して強制される)。unscoped な型には強制するスコープがないため、roles= だけがゲートになる。target() パラメータのうち 1 つは Action の target= の型を指す
scope_ref(cls) …かつ Action が作成する先のスコープを指す。unscoped な型は指せない(SCOPE_POLICY_ERROR)

これらが生成される ActionParameterDef の refers_to / scope_semantics を決めます。 つまりスコープ強制は、エンジンのハードコードではなく作者が宣言するものです。

ActionParams

型付き Action パラメータモデルの基底クラス。フィールドには上記マーカーを使います。

class EscalateTicketParams(ActionParams):
    ticket_id: str = target(Ticket)
    reason: str | None = None

date / datetime 型のパラメータは date と datetime の値 のプロパティの規則に従い、 拒否されると INVALID_PARAMS になります。MCP の execute_action では値が JSON 文字列なので、文字列の規則が適用されます。

イベント: Event・@ontology.event・emits=・ctx.emit・client.events

イベントは、Action が記録する業務上の事実です(たとえば「注文が出荷された」)。監査では ありません。監査は「誰が何をしたか」を記録し、イベントは「何が起きたか」を記録します。 イベントは Event のサブクラスとして宣言します。フィールドの型規則は ActionParams と 同じで、スカラー・選択肢・フラットな struct を使えます。Sensitivity もプロパティと同様に 使え、制限付きフィールドは省略可能にします。イベントには primary key、transitions、 scope_level はありません。

from ontary import Event

@ontology.event(description="An order has shipped.")
class OrderShipped(Event):
    carrier: str

@ontology.event(*, description=None, api_name=None, accept=()) は、クラスを EventTypeDef として登録します。クラスは Event のサブクラスである必要があり、1 つの クラスに付けられるのは 1 回だけです。違反すると ONTOLOGY_INVALID を送出します。 accept= は "EVENT_NEVER_EMITTED" を受け取ります。

Action は、送出してよいイベントを emits=[...] に列挙します。ActionTypeDef.emits には その api_name が入り、MCP の list_action_types に emits として表示されます。同じ Ontology に登録されたイベントではないクラスを emits に渡すと、宣言時に ONTOLOGY_INVALID になります。

@ontology.action(ShipOrder, target=Order, roles=["ops"], emits=[OrderShipped])
def ship(ctx: ActionContext, p: ShipOrder) -> dict[str, Any]:
    order = ctx.get(Order, p.order_id)
    ...
    ctx.emit(OrderShipped(carrier=p.carrier))
    return {}

ctx.emit(event, *, about=None) は、Action のトランザクション内でイベントを記録します。 対象(subject)の既定値は、Action が解決した対象オブジェクトです。作成系の Action のように 対象 id がない場合は、about=<object> を渡します。そのオブジェクトは、このコンテキスト (get・all・create・traverse)が渡したもので、Action の対象型でなければなりません。 1 回の呼び出しで複数のイベントを送出でき、同じ型を 2 回送出することもできます。イベントは 送出順に保持されます。イベントの ts は ctx.now() です。拒否はハンドラ内で送出される ため、Action 全体がロールバックされ、error として監査されます。

ケース コード
イベントクラスがこの Ontology に登録されていない UNKNOWN_NAME
イベントが Action の emits にない UNDECLARED_EVENT
対象を解決できない、対象型ではない、このコンテキストが渡していない、またはまだ行がない EVENT_SUBJECT_INVALID

client.events(event_type=None, /, *, about=None, since=None, until=None) は、クライアントの コンシューマーが見られるイベントを、送出順の list[EventRecord[E]] で返します。about は オブジェクトか (cls, id) のタプルです。since は含み、until は含みません。どちらも タイムゾーン付きでなければなりません。ページングはありません。イベントクラスを渡すと 各ペイロードがそのクラスになり、渡さない場合は保存されたフィールドを保持する素の Event になります。

records = client.events(OrderShipped, about=(Order, "o-1"))
records[0].payload.carrier

対象の最新の行がコンシューマーのスコープと row_visibility を通るとき、イベントは 見えます。退役した対象では、その行は退役前の最後の行です。対象がスコープを移ると、新しい スコープは履歴全体を見られ、古いスコープは何も見られません。宣言が削除されたイベント型は 隠れます。コンシューマーの種類に対して Sensitivity で制限されたペイロードフィールドは、 ペイロードでは None になり、redacted_fields に載ります。

EventRecord は frozen なジェネリッククラスです。フィールドは event_type、 about_type、about_id、ts、invocation_id、payload、 redacted_fields: frozenset[str] です。

イベントは呼び出しの監査行に保存されるため、Action と一緒にコミットまたはロールバックされます。 AuditEntry.events はそれらをマスクせずに列挙します。AuditEntry を 参照してください。

アドバイザリーな finding: Finding.guide・accept・snapshot

ontology.diagnose() は、アドバイザリーなコード(モデリングの lint とセキュリティの lint。一覧は CLI リファレンス)ごとに Finding を返します。Finding の フィールドは code、severity、location、message、fix_hint、guide です。

  • Finding.guide(str | None、デフォルトは None)は、その finding を説明する 公開済み英語デザインガイドの該当セクションの URL です。ontary.diagnose.GUIDE_URL と ontary.diagnose.GUIDE_ANCHORS から GUIDE_URL + "#" + anchor として作られます。 ontary.diagnose.ADVISORY_CODES のすべてのコードが設定します。ガイドのセクションが ない finding(ONTOLOGY_INVALID、SCOPE_POLICY_ERROR、INVALID_RECORD、 RULE_VIOLATED、DIAGNOSE_RULE_FAILED)は None のままです。
  • accept は、finding の原因になった宣言の場所で「これは意図したものです」と 伝えます。lint コードを 1 つ、またはコードのシーケンスを渡します。accept した finding は diagnose()、ontary validate のテキスト出力、--json のどれにも 現れません。下の表にないコードは拒否されます(最後の項目を参照)。
宣言 accept 引数 受け付けるコード
prop(...) accept= STORED_DERIVABLE, FREE_TEXT_STATUS
@ontology.object(...) accept= FORBIDDEN_TYPE_NAME, AUDIT_TYPE
@ontology.action(...) accept= CRUD_ACTION_NAME, MICRO_ACTION
@ontology.function(...) accept= CRUD_ACTION_NAME
@ontology.event(...) accept= EVENT_NEVER_EMITTED

受け付ける名前は、ontary.meta の Literal エイリアス PropertyLint、 ObjectLint、ActionLint、FunctionLint、EventLint です。ontary からは export されません が、誤ったコードを呼び出し側の mypy エラーにします。同じ accept フィールド (tuple[str, ...]、デフォルトは ())は PropertyDef、ObjectTypeDef、 ActionTypeDef、FunctionDef にもあり、手で組み立てた記述子もデコレートした クラスと同じように動きます。 - snapshot(bool、デフォルトは False)は @ontology.object(...) の引数で、 その型を時点のスナップショットとして宣言します。免除されるのは 2 つの finding です。 その型のプロパティに対する STORED_DERIVABLE と、Snapshot という名前の接尾辞に 対する FORBIDDEN_TYPE_NAME です。V<数字>、年、History の名前は免除されません。 snapshot=True のない *Snapshot の型は、これまでどおり警告されます。 description の「declared snapshot」という文字列には効果がありません。 - 拒否されるコード。 宣言が受け付けられないコードを渡すと、宣言を組み立てる 時点で ValidationFailed(コード ONTOLOGY_INVALID)が発生します。メッセージには、 宣言、拒否されたコード、その宣言が受け付けるコードが含まれます。未知のコード、 別の種類の宣言向けのコード、エラーコード、セキュリティ lint のコード (UNSCOPED_SENSITIVE、MIN_N_UNSET)が対象です。lint コードは finding のコード であり、raise されないため、ERROR_CODES にもエラーコード表にも ありません。

@ontology.object(layer="L0", scope="unscoped", snapshot=True)
class AccountSnapshot(OntologyObject):
    id: str = prop(primary_key=True)
    health_score: int  # STORED_DERIVABLE は出ない: 宣言済みのスナップショット型

@ontology.object(layer="L0", scope="unscoped")
class Applicant(OntologyObject):
    id: str = prop(primary_key=True)
    credit_score: int = prop(accept="STORED_DERIVABLE")  # 外部の信用情報機関から記録

スコープポリシー

スコープは宣言するものであり、推論されません。4 種類のルールが ScopePolicy を 構成します。通常は手で組み立てる必要はなく、@ontology.object(scope=[...]) が行います。

ルール フィールド レベルの解決元
SelfScope level オブジェクト自身の id
DirectProperty level, property_name 行のプロパティ
ViaLink link_api_name, direction("from"/"to"), parent_type リンクを辿って親へ(再帰的に)
CustomResolver level, fn(store, obj_type, obj_id) -> str \| None 任意の呼び出し側ロジック

ScopePolicy

フィールド 型 備考
levels list[str]
unscoped_types set[str] ここに挙げた型を rules にも書くことはできません。重複は validate() と全ての読み取りが SCOPE_POLICY_ERROR で拒否します。
rules dict[str, list[ScopeRule]]
contributor_rules dict[str, list[ScopeRule]]
row_visibility dict[str, RowVisibilityFn]
min_n int

解決ヘルパー

resolve_owning_scope(policy, store, obj_type, obj_id) -> dict[str, str | None]
resolve_contributor(policy, store, obj_type, obj_id) -> str | None
covers_scope(policy, consumer, resolved) -> bool

resolve_owning_scope は宣言されたレベルごとに 1 エントリを返します。covers_scope は コンシューマーのレベルが未解決なら拒否します — スコープ強制はフェイルオープンしません。

ルールが未宣言のオブジェクト型・リンク型・レベルを参照している場合は ValidationFailed(SCOPE_POLICY_ERROR)になります。


ランタイムとクライアント

runtime = ontology.bind(store, capabilities={...})                  # 1 回だけ
client  = runtime.for_consumer(consumer)                            # リクエストごとに安価に

Ontology.bind(store, *, clock=None, id_factory=None, capabilities=None)

clock はタイムゾーン付き datetime を返す callable で、デフォルトは datetime.now(timezone.utc) です。id_factory は str を返す callable で、デフォルトは UUID 形式の ID です。どちらも共有ランタイムに保存され、すべての for_consumer() ビューに引き継がれます。

clock=c を付けてバインドすると、c がストアに設定されます。以降、そのストアへの すべての書き込みが c を使います。対象は、アクションの書き込み、 ontary.ingest.bulk_upsert / bulk_link、ストアの直接呼び出しです。 valid_from / valid_to はこのクロックから決まります。

  • 1 回の呼び出しにつき 1 つの時刻。 アクションはクロックをちょうど 1 回読みます。 その時刻が、ctx.now() の戻り値、アクションが書き込むすべての行とリンクの valid_from / valid_to(retire が閉じるリンクを含む)、その呼び出しのすべての 監査エントリ(ok、denied、error)の ts になります。
  • CLOCK_CONFLICT。 同じストアを別のクロックオブジェクトでバインドすると、 PreconditionFailed が発生します。同じクロックオブジェクト、または clock= なしなら 問題ありません。clock= なしでバインドしたランタイムは、ストアに設定済みのクロックを使います。
  • CLOCK_REGRESSION。 閉じる行やリンクの valid_from より前の時刻での書き込みは、 PreconditionFailed になります。アクション内ではアクション全体がロールバックされ、 error 監査エントリがコードを記録します。同じ時刻は許可されます。
  • CLOCK_NOT_TIMEZONE_AWARE。 naive な datetime を返すクロックは ValidationFailed になり、何も保存されません。

先にバインドしてから、シードしてください。別のクロックでシードしたデータがあると、 過去に設定したクロックでの最初の更新や retire で CLOCK_REGRESSION が発生することがあります。

OntologyRuntime(ontology, store, handlers=None, *, clock=None, id_factory=None, capabilities=None)

1 つの (ontology, store) ペアに対する、コンシューマー非依存の共有機構 — クエリ層、 Action 実行器、バインド済みハンドラ — をちょうど 1 回だけ配線します。

  • .for_consumer(consumer, *, capabilities=None) -> OntologyClient — 安価なビュー。1 プロセスで多数のコンシューマーを捌いても、再配線は起きません。

OntologyClient(ontology, store, consumer, *, capabilities=None)

ちょうど 1 つの (ontology, store, consumer) に束縛されます。直接構築しても動作し、 その場合は内部で使い捨てのランタイムを構築します。

メソッド 戻り値
.get(obj_type, obj_id) T \| StoredObject \| None
.list(obj_type, where=None, *, limit=DEFAULT_READ_LIMIT, after=None, order_by=None) list[T] \| list[StoredObject] \| TypedPage[T] \| Page
.traverse(obj_type, link, from_id, *, reverse=False) または .traverse(link_cls, from_obj_or_id, *, reverse=False) list[T] \| list[StoredObject]
.aggregate(obj_type, value_field=None, where=None, *, func="mean") float \| int
.aggregate_by(obj_type, value_field, group_by, where=None, *, func="mean") dict[str, float \| int]
.count(obj_type, where=None) int
.exists(obj_type, where=None) bool
.count_contributors(obj_type, where=None) int
.execute(params) または .execute(action, params) dict[str, Any]
.call_function(params: FunctionParams) または .call_function(api_name: str, params: dict[str, Any] \| None = None) Any
.ingest(obj_type, records, source, *, on_error="raise") IngestReport(失敗があれば IngestError)
.ingest_links(link_api_name, pairs, source, *, on_error="raise") IngestReport(失敗があれば IngestError)

型付き surface と動的 surface。 作者のクラスを渡すと型付きインスタンスが返ります (client.get(Ticket, id) -> Ticket | None。mypy が推論し、キャストは不要)。文字列を 渡すと StoredObject が返ります — こちらが動的な形で、MCP のワイヤ形式でもあり、汎用 ツールが使う形です。.traverse は型付きの場合、LinkHandle を第 1 引数に取ります (client.traverse(link_cls, from_obj_or_id))。

すべての呼び出しの where / order_by / group_by / value_field のキーは、 型の宣言済みプロパティと照合されます。文字列形式でも型付き形式でも同じです。 未知のキーは、黙って何にもマッチしたり数値を返したりせず UNKNOWN_FIELD に なります。保存された行がたまたま持っているキーも同じです。宣言されていない payload キーは書き込みでは許されますが、読み取りで名前指定はできません。 宣言済みだが隠されているキーは、値を開示する 操作では VisibilityError と VISIBILITY_DENIED で拒否されます。スコープルーティングと Function 集計の狭い例外は、下記の「フィルター」と「集計」で説明します。

オブジェクト読み取りで limit を省略すると DEFAULT_READ_LIMIT=1000 が適用され、 Page(型付き呼び出しでは TypedPage)が返ります。limit=None を明示すると 上限なしのリスト形式になります。正の整数を指定するとページになります。 order_by は宣言済み payload フィールド(デフォルトは昇順)、または (field, "asc"|"desc") の組を受け付けます。未知のフィールドは UNKNOWN_FIELD、隠されたフィールドは VISIBILITY_DENIED で拒否されます。 DirectProperty のスコープルーティングキーであっても同じです。等価一致の where にある例外とは異なり、order_by は常に非除外の隠しフィールド検査を使います。

surface 間の非対称性が 2 つあります。どちらも見落としやすい点です。

ハイドレーション。 型付き読み取りは datetime 型のプロパティを実際の datetime にパースします(Pydantic 経由)。文字列 surface は保存されたままの ISO-8601 文字列を返します。ストアが永続化する内容を書き換えることはありません — 型付きクライアントの取り出し時のハイドレーションだけがパースします。書き込みで 受け付ける値は date と datetime の値 を参照してください。

リダクションの形。 リダクションされたフィールドは、型付き surface では None として返り、名前が redacted_fields に載ります。しかし文字列 surface と MCP では そのキー自体が payload から消えます — redacted_fields に相当するものもありません。 MCP 越しに読むときは防御的に(payload["email"] ではなく payload.get("email"))、 そしてキーが無いことを「保存されていない」と解釈しないでください。単にあなたから 隠されているだけかもしれません。


読み取り

StoredObject

フィールド 型
payload dict[str, Any]
lineage Lineage

payload と lineage を分離した凍結オブジェクトです。_object_type のようなキーを 紛れ込ませた素の dict では決してありません。

Lineage

object_type、object_id、valid_from、valid_to、source_system、source_id、 extracted_at。

フィルター、順序、読み取り上限

where は等価一致の素の scalar、または gt、gte、lt、lte、in、ne、 contains をキーとする mapping を受け付けます。複数のキーを持つ mapping は、その 1 つのフィールドに対する AND 条件です。{"gte": a, "lt": b} は半開区間の範囲指定、 {"contains": "x", "ne": "x"} は 1 つの値を除いた部分文字列一致になります。空の mapping は拒否されます。比較演算子は宣言済みの int、float、date、datetime プロパティで使えます。date と datetime の operand は、プロパティが保存するのと 同じ ISO-8601 文字列か、date 値・オフセット付きの datetime 値(先にその綴りへ 変換されます)です。datetime には時刻部分が必要です。日付 だけの文字列は、オフセット付きの列に決して一致しない naive な深夜 0 時として扱われる のではなく、OPERATOR_TYPE_MISMATCH になります。4 つの比較演算子は datetime を瞬間 として比較するため、別の UTC オフセットで書いた operand も同じ時刻なら一致します。 保存値と operand のオフセット有無が異なる(naive と aware)行は一致しません。 datetime に対する eq、ne、in は従来どおり保存された文字列そのものと一致 させます。in は宣言済み型の値の リストを取り、ne はすべての宣言済み型に使え、contains は str の部分文字列 検査です。未知の 演算子は UNKNOWN_OPERATOR、プロパティ型に合わない演算子または operand は OPERATOR_TYPE_MISMATCH になります。この operand の型検査は、等価比較の唯一の 書き方であるベアのスカラーにも適用されます。例外は None で、これは null 判定と して扱われ、そのプロパティを持たない行を選びます。宣言型が float のプロパティに int の operand を渡すのは型の拡大であり、mismatch ではありません。mapping 形式の where は、型付き、文字列、aggregate、Function、MCP のすべての読み取り surface で 共通です。

struct プロパティに対する where 条件は OPERATOR_TYPE_MISMATCH になります。 メッセージは where is not supported on struct property 'amount' のようにプロパティ名を 示します。amount.currency のような内側のパスでは検索できません。内側のフィールドは トップレベルのプロパティではありません。

mapping 値は演算子構文なので、宣言済み json プロパティの等価比較を where={"data": {"kind": "a"}} とは書けません — その mapping は値ではなく演算子として 解釈されます。1 要素の in リストで包んでください: where={"data": {"in": [{"kind": "a"}]}} が等価比較の回避策です。

DirectProperty として宣言されたスコープルーティングキーが隠しフィールドである 場合、スコープキーの例外は、素の eq 値による等価一致と、明示的なリストを operand とする単独の in に限られます。gt、gte、lt、lte、ne、contains、 リストでない in、不正な shape、および複数キーの mapping(in を含むものも)は、 呼び出し側に値を学習させるため VISIBILITY_DENIED で拒否されます。operand は実際に値を供給する必要があります。 str、int、float、bool、date のいずれかです。したがって None、None を 含むリスト、空の in リストも拒否されます。素の null は事前知識を必要とせず、 行ごとの null 探索になるためです。読める フィールドに対する null 絞り込みは 従来どおりです。where の mapping と、その中の各条件は、公開境界で一度だけ 読み取ります。すべての gate と行マッチャーはその一つのスナップショットを使うため、 二度目の読み取りで別の答えを返す mapping が、ある述語として分類されながら別の 述語として実行されることはありません。

where は mapping であるか、省略されるかのどちらかです。文字列・リスト・数値 などはすべて、どの読み取り surface でも INVALID_PARAMS になります。偽値も 同じです。""、0、[]、False は「フィルターなし」として扱わず拒否します。 空の mapping {} は従来どおりフィルターなしを意味し、None も同じです。

order_by は宣言済み payload フィールドを受け付け、デフォルトでは昇順になります。 (field, "asc"|"desc") の組で方向を明示できます。未知のフィールドは UNKNOWN_FIELD、隠されたフィールドは VISIBILITY_DENIED で拒否されます。 隠されたスコープキーも対象です。返される順位が値を開示するためです。 group_by にもスコープキーの例外はありません。返される dict のキーがグループ値を 開示するためです。ページカーソルとも組み合わせられます。

consumer surface(OntologyClient と GuardedQuery)では、limit を省略すると DEFAULT_READ_LIMIT=1000 が適用され、Page(型付き呼び出しでは TypedPage)が返ります。 limit=None を明示する場合は上限なしのリスト形式になります。宣言された Function 内では BoundQuery.list は limit を省略すると上限なしで bare list を返し、正の limit を指定した 場合に Page が返ります。

ページネーション — Page / TypedPage[T]

どちらも items と不透明な next_cursor: str | None を持ちます。consumer surface では limit の省略で上記の既定上限を使ったページになり、limit=None は明示的な上限なしリストの 指定です。宣言された Function 内では BoundQuery.list は limit の省略または None で bare list を返し、正の limit を渡すとページを返します。

契約:

  • ページサイズは正確。 可視な行がまだ limit 件残っている限り、ページはちょうど limit 件を持ちます — ストア読み取りより下流のフィルタがページを短くすることは ありません。短いページは常に「もう行が無い」を意味し、「一部が隠された」ではありません。
  • カーソルは不透明。 行ごとのランダムなトークンです。行 id でも件数でも順序でも ありません。パースせず、保存して after= に返すだけにしてください。
  • next_cursor is None は「埋まる前に尽きた」の意味。 最後の行でちょうど埋まった ページもカーソルを持ち、次の呼び出しが最後の空ページを返します。よって while next_cursor is not None のループは 1 ページ早く止まることなく正しく終了します。
  • 順序なしの walk では、途中の更新で行が重複することはありますが、取りこぼしは 起こりません。 ストアは close-old / insert-new です。順序付き walk では、ソート値が カーソルより前に移動した行は静かに取りこぼされ、後ろに移動した行は重複します。 順序付き walk が欠落も重複も起こさないのは、ストアが静止している場合だけです。
  • limit なしの after → AFTER_WITHOUT_LIMIT。limit < 1 → INVALID_LIMIT。 不正または未知のカーソル → INVALID_CURSOR。順序付き walk は、自身のカーソル行への 書き込み(retire や行を差し替える update を含む)の後は再開できず、STALE_CURSOR になります。先頭ページからやり直してください。

順序付きページのスケール上の注意。 順序付きページは現状、limit や where の 絞り込みに関わらず、毎回そのオブジェクト型全体を展開してソートします。実測した 20,000 行のケースでは、order_by ありの limit=10 が 20,000 行すべてを読み、 なしでは 500 行でした。1 ページあたり O(N log N)、順序付き walk 全体で O(N² log N) です。

traverse

型付き形式は client.traverse(link_cls, from_obj_or_id) です。文字列形式は client.traverse("Comment", "commentOnTicket", comment_id) のように、ソース型・ リンク API 名・ソース id をこの順で渡します。Function 内の BoundQuery も同じ ハンドル先頭の型付き形式を受け付けます。

返される対象行にも通常どおり可視性チェックが適用されます。identity-revealing な リンクの traverse は、human コンシューマーに対しては対象を返す前に VisibilityError (VISIBILITY_DENIED)になります。AI コンシューマーには対象行への通常のスコープと sensitivity の強制が適用されます。reverse=True はリンクの対象側から辿り、 identity-revealing の拒否は双方向で対称です。

可視行のカウント

count(obj_type, where=None) は、コンシューマーのスコープと行可視性のチェックを 適用した後に一致する行数を返します。exists(...) は同じ post-visibility の選択が 空でないかを返します。どちらも list と同じ where 演算子文法を受け付けます。 一致する行すべてからスコープ外に置かれたコンシューマーは、プライバシー拒否ではなく count から 0、exists から False を受け取ります。

この 2 つの操作は意図的に min-N の対象外です。開示するのは post-visibility フィルター後の行集合のサイズだけであり、コンシューマーは list(..., limit=None) で同じ行を列挙できるため、min-N 拒否を加えても開示保護は 増えません。count_contributors は引き続き唯一のプライバシー計数プリミティブです。 可視行のカウントとは異なり、集計の背後にある distinct な貢献者母集団を解決するため、 集計と同じ min-N のリリース規律を保ちます。

集計

aggregate(...) と aggregate_by(..., group_by=...) は func="mean"|"count"|"sum"|"min"|"max" を受け付けます。デフォルトは従来どおり "mean" です。グループなしでは count が int、その他の関数が float の値を返し、 グループ付きでは各グループに対応する値を dict で返します。

func="count" は宣言済みのどのフィールド型でも受け付けます。そのフィールドを 持つ行数を数えるだけで、値を float に変換することはありません。value_field も 省略(または None を渡す)でき、その場合は選択範囲内の可視な行すべてをカウント します。min-N はそれらの行の寄与者に対して(グループ付きならグループごとに) 適用されます。count 以外の func で value_field を省略すると INVALID_PARAMS になり、メッセージは func 名を挙げて value_field が必須であると伝えます。

両者は where/group_by の隠しフィールド検査、異なる寄与者に対する min-N、 型が宣言していない value_field に対する UNKNOWN_FIELD、そして mean/sum/min/max の下で宣言済みだが非数値の value_field に対する NON_NUMERIC_AGGREGATE (count は対象外です。どちらの検査も行を 1 つも読む前に行われます)を強制します。 隠された value_field を集計できるのは、contributor_rules を宣言した型に 対する、著者が宣言した Function から、func="mean" または func="count" を使う場合だけです。 コンシューマーの surface — client、型付き、MCP — からこの操作を行うと VisibilityError と VISIBILITY_DENIED になります。sum、min、max は引き続き拒否されます。 すべての関数に同じ min-N の公開判定が適用されます。可視な選択が 空の場合は、グループなしでもグループ付きでも MIN_N_VIOLATION になります。グループ付き の空集合 {} も同じように拒否され、空の dict は返りません。

group_by は、この surface の他のフィールド名と同じように検証されます。型付き surface だけでなく、すべての surface が対象です。型が宣言していない名前は where のキーや order_by と同じく UNKNOWN_FIELD になります。どの行にも一致せず、 グループなしの値をキー "None" で返すことはありません。falsy な group_by が黙って 非グループ経路に落ちることもありません。ただしそちらのコードは surface によって 異なります。文字列 surface と BoundQuery では INVALID_GROUP_BY、型付き surface では(クラスプロパティ検査が先に走るため)UNKNOWN_FIELD です。

json として宣言された group_by は INVALID_GROUP_BY になります。その値は dict や list になり得るため、ハッシュ可能とは限らないからです。検査対象は保存された値では なく宣言された型です。したがって、たまたまスカラーしか保持していない json プロパティも拒否されます。最初の dict が届くまで動いてしまうことはありません。 struct プロパティを group_by に指定した場合も INVALID_GROUP_BY になります。宣言済みの struct 値はグループキーとしてサポートされません。

異なる 2 つのグループ値が同じ dict キーとして公開される場合は GROUP_KEY_COLLISION になります。多くはオプショナルなプロパティが原因です。値を持たない行のキーが None に なり、文字列リテラル "None" を持つ行と衝突します。公開されるセル 1 つが記述できる 母集団は 1 つだけです。この検査は各グループの公開時に、そのグループの min-N 判定の あとで走ります。min-N が差し止める選択は引き続き MIN_N_VIOLATION になります。

aggregate、aggregate_by、count_contributors は、未登録のオブジェクト型を UNKNOWN_OBJECT_TYPE で拒否します。これは where= を渡したときの従来の挙動と 同じです。

GuardedQuery(store, registry, policy)

呼び出しごとに明示的な consumer を取る、エンジンレベルの読み取り経路: .get_object、.get_objects、.traverse、.aggregate、.aggregate_by、 .count、.exists、.count_contributors。通常の呼び出し側は代わりに OntologyClient を使います。


Action

Action は、型付きパラメータクラスと、 @ontology.action(params_cls, target=..., roles=[...], capabilities=()) でデコレートしたハンドラの組です。

execute はすべて同じパイプラインを通ります。

登録済みか → ロールは許可されているか → 宣言された target/scope パラメータを スコープがカバーするか → 事前条件 → トランザクション内の副作用 → 追記専用の監査

すべての試行が監査されます — ok、denied、error のいずれも。

ActionContext

ハンドラが生のストアハンドルの代わりに受け取るもの。書き込みには Action 自身の Source が自動で刻印されます。

型付きメンバー。 オントロジー自身のクラスと LinkHandle を受け取ります。そのため mypy がクラス、戻り値の型、リンクの各端点の型、save 前にハンドラが代入する属性を 検査します。create のキーワード名は実行時にだけ検査されます。

メンバー 用途
.get(cls, obj_id) -> T \| None 現在のオブジェクトを 1 件読む
.all(cls) -> list[T] cls の現在のオブジェクトすべて
.create(cls, **values) -> T 作成し、そのオブジェクトを返す。primary key を省略するとランタイムの id_factory から採番する。すでに有効な primary key は OBJECT_ALREADY_EXISTS で拒否される。宣言されていないプロパティ名は INVALID_RECORD で拒否される
.save(obj) このコンテキストが obj を渡した時点から変わった宣言済みプロパティだけを書く。変更がなければ何も書かない。get・all・create・traverse で得たオブジェクトだけを保存できる(それ以外は OBJECT_NOT_LOADED)。primary key の変更は PRIMARY_KEY_IMMUTABLE で拒否される
.link(handle, from_, to) リンク作成。各端はオブジェクトかその id で、型は handle が決める。両端はリンク型が宣言する端点型の有効なオブジェクトである必要があり、そうでなければ LINK_ENDPOINT_NOT_FOUND で拒否。有効なリンクと同一なら no-op
.unlink(handle, from_, to) 1 本の live link を閉じる。端の渡し方は link と同じ
.traverse(handle, anchor) -> list[To] anchor からリンクされた To オブジェクト。reverse=True ならそこへリンクしている From オブジェクト
.retire(obj) / .retire(cls, obj_id) オブジェクトをリタイアし、接続するすべての有効なリンクを閉じる

create に渡す値や save 前に代入する値が date / datetime の場合は date と datetime の値 に従います。拒否された値は INVALID_RECORD になります。

型付きハンドラは、オブジェクトを読み、変更し、保存します。

order = ctx.get(Order, params.order_id)
order.status = "shipped"          # mypy がフィールド名と型を検査する
ctx.save(order)                   # `status` だけを書く

0.18.0 で削除されました。 以下の型付きメンバーへ移行してください。 文字列形式の retire と unlink は ValidationFailed を送出します。 code は INVALID_PARAMS です。 unlink は位置引数専用です。キーワード引数で呼ぶと TypeError が発生します。

削除された呼び出し 型付きの移行先
.insert(obj_type, payload) .create(cls, **values)
.update(obj_type, obj_id, changes) .get(cls, obj_id) + 代入 + .save(obj)
.create_link(link_api_name, from_id, to_id) .link(handle, from_, to)
.retire(obj_type, obj_id) .retire(obj) / .retire(cls, obj_id)
.unlink(link_api_name, from_id, to_id) .unlink(handle, from_, to)
.read_current(obj_type, obj_id) .get(cls, obj_id)
.read_all(obj_type) .all(cls)
.links_from(link_api_name, from_id) .traverse(handle, anchor)
.links_to(link_api_name, to_id) .traverse(handle, anchor, reverse=True)

その他のメンバー。

メンバー 用途
.capability(handle) -> P 宣言済み Capability の取得
.consumer 呼び出し元の Consumer
.emit(event, *, about=None) この呼び出しに、宣言済みのイベントを記録する。「オントロジーを宣言する」のイベントの節を参照
.now() -> datetime 呼び出しの唯一の時刻(Ontology.bind を参照)。datetime.now() や時刻用 Capability の代わりに使います

get と all は信頼されたハンドラ向けの読み取りです。生データを返し、redaction と scope の制限を適用しません。 consumer 向けの guarded query ではありません。scope や sensitivity で列挙を絞ると、id allocator から既存行が隠れます。 その結果、id が再利用される可能性があります。

retire と unlink がハンドラーから使う SDK の唯一の除去操作です。これらはエンジンが所有する Action transaction の内側でだけ呼び出せます。retire はオブジェクトの current row を 閉じ、そのオブジェクト型についてリンク型が宣言している側でそのオブジェクトを参照するすべての live link を同じ transaction で cascade-close します。オブジェクトと各リンクの closure は AuditEntry.writes に記録されます。 unlink は一致する 1 本の live link を閉じます。

事前条件の失敗には ActionError を送出します。0.6.0 から code は必須です — PRECONDITION_FAILED か、独自の安定コードを指定してください。

権限(Authority)

宣言されていない書き込みは拒否されます。型全体が owned=True でないオブジェクト型を Action が作成しようとすると SOURCE_CREATE_REFUSED、オントロジー所有と宣言されていない ソース由来プロパティの更新やソース由来リンクの作成は UNDECLARED_SOURCE_WRITE に なります。ソースデータとオントロジー所有の状態は分離されたままです。

削除にも同じ authority 境界が適用されます。retire にはオブジェクト型全体の owned=True 宣言が、unlink にはリンク型の owned=True 宣言が必要です。そうでなければ UNDECLARED_SOURCE_REMOVAL になります。オブジェクトの retirement cascade がソース由来 リンクに達した場合は、先に閉じたリンクも含めて Action transaction 全体がロールバック されます。

呼び出し側がすでにストアトランザクションを保持している状態での execute() は拒否 されます(CALLER_TRANSACTION_REFUSED、監査対象外)。さもないと、適用され監査された Action が監査ログの下でロールバックされうるためです。

AuditEntry

ts、actor、role、action、target_type、target_id、params、outcome、 invocation_id、error_code、および完全性レコード: writes: list[WriteRecord]、 capability_accesses: list[CapabilityAccessRecord]、events: list[EmittedEvent]。

kind: Literal["action", "function"] — このエントリを生成したもの。Action と Function は 1 つのログを共有するため、読み手が両者を区別する手段が kind です(同じ api_name の Action と Function を宣言することを妨げるものは何もありません)。 function エントリでは action に Function の api_name が入り、target_type は "" (Function に対象オブジェクト型はありません)、writes は常に空です。

invocation_id: str | None — execute()(および監査対象の call_function()) 呼び出しごとに 1 つの id で、その呼び出しが書き込むすべてのエントリに刻印されます。 エントリを対応づけるときは、フィールド一致と追記順に頼らずこの値を使ってください — 同じ Action を 同じパラメータで 2 回呼ぶと、それ以外では区別できません。None はこのフィールドが存在 しなかった頃のエントリ(古いエンジンが書いたストアファイル)を意味し、後から捏造される ことはありません。

events: list[EmittedEvent] — ok の action エントリが送出したイベントを、送出順に マスクせず列挙します。監査は管理者向けのビューだからです。denied と error のエントリでは 常に空です。

unscoped_params: list[str] — scope="unscoped" の型を指すため、Action の スコープゲートを通らなかった target(...) パラメータの一覧です。これらのパラメータでは、 Action の roles= だけがゲートでした。スコープを持つパラメータがすべてスコープ検査を 受けた場合は空です。ゲートより前に書かれたエントリ(ロールによる拒否など)でも空です。

target_id: str | None — action エントリでは、呼び出し側が Action の宣言済み 対象パラメータに渡した id です。denied と error を含むすべての結果で記録されます。 その id が存在しない場合も記録します。Action が対象パラメータを宣言していない場合と、 すべての function エントリでは None です。

error_code: str | None — denied または error のエントリが失敗した理由です。 送出された例外のカタログ済み code(PERMISSION_DENIED、SCOPE_DENIED、 INVALID_PARAMS、ハンドラ独自の ActionError のコードなど)が入ります。ハンドラが 送出した素の KeyError のように code を持たない例外は、MCP サーバーと同じく INTERNAL_ERROR として記録します。ok エントリでは None です。

  • WriteRecord — op(create/update/link)、object_type、link_type、 object_id、from_id、to_id
  • CapabilityAccessRecord — api_name、count
  • EmittedEvent — event_type、about_type、about_id、payload(保存形式)

Function

Function ハンドラは BoundQuery を受け取ります。型付き Function は FunctionParams の サブクラスも @ontology.function(params_cls, ...) で宣言します。FunctionParams は未知のフィールドを 拒否します。その型注釈が入力検証と MCP のパラメータスキーマを定義します。ストアハンドルはハンドラに一切届かず、 すでにガードされた読み取りだけが渡ります。Function は導出値を返し、書き込みは行いません。

class TicketStatsParams(FunctionParams):
    queue_id: str

@ontology.function(TicketStatsParams, api_name="ticketStats")
def ticket_stats(query: BoundQuery, params: TicketStatsParams) -> float:
    mean = query.aggregate("Ticket", "age_hours", where={"queue_id": params.queue_id})
    assert isinstance(mean, float)
    return mean

client.call_function(TicketStatsParams(queue_id="q1"))

Function の宣言には 2 つの形式があります。型付き Function では FunctionParams の サブクラスを宣言します。上の例のようにインスタンスを渡すか、関数名と dict を渡せます (例: client.call_function("ticketStats", {"queue_id": "q1"}))。dict は検証されます。 ハンドラには TicketStatsParams のインスタンスが渡されます。 入力がない Function は params クラスを省略し、query だけを受け取ります。

@ontology.function(api_name="health")
def health(query: BoundQuery) -> bool:
    return query.exists("Ticket")

client.call_function("health")
client.call_function("health", {})

params クラスを指定しない場合、ハンドラは既定値なしの query をちょうど 1 つだけ受け取る必要があります。 それ以外のハンドラは、宣言時に ValidationFailed と code="ONTOLOGY_INVALID" で拒否されます。 (query, params: dict) などの dict 形式のハンドラは 0.20.0 で削除しました。 代わりに FunctionParams サブクラスを宣言してください。

未知のフィールド、必須フィールドの不足、不正な型、宣言した選択肢にない値は、ハンドラの実行前に ValidationFailed と code="INVALID_PARAMS" を送出します。入力がない Function に空でない params を 渡した場合も、同じコードで拒否します。

FunctionDef.parameters と MCP の list_functions は、型付き入力を name、type、choices、 fields、required、refers_to を持つパラメータとして公開します。これは scope_semantics を 除いた Action パラメータと同じ形式です。入力がない Function では [] になります。 client.call_function は、関数名でも FunctionParams のインスタンスでもない引数を ValidationFailed と code="INVALID_PARAMS" で拒否します。

BoundQuery

コンシューマーを固定した GuardedQuery: .get、.list、.count、.exists、.traverse、 .aggregate、.aggregate_by、.count_contributors、.capability(handle)。型付きオーバーロードは OntologyClient と同様に機能します(query.get(Ticket, id) -> Ticket | None)。

入力がない Function は、引数が 1 つのハンドラ (query) として宣言し、params なしまたは {} で呼び出します。

PreconditionFailed(FUNCTION_ERROR)は、未宣言の api_name、重複登録、ハンドラ未バインドを カバーします。

Function の監査境界

監査対象となる call では、OntologyClient.call_function が kind="function" の監査エントリを 1 件追加します — invocation id、params、outcome、handler の capability_accesses を記録します。writes は構造上空です。

Function を監査するかは FunctionDef.audited による条件付きです。

宣言 監査されるか
capability を宣言 ✅ yes(デフォルト)
何も宣言しない ❌ no(デフォルト)
@ontology.function(audit=True) ✅ yes、常に
@ontology.function(audit=False) ❌ no — ただし release の場合を除く

Function は Action よりはるかに頻繁に実行されます。Capability を宣言しない Function は プロセス外に到達できず、読むものはすべて guarded query layer によってすでに制限されます。 すべての call を監査すると、得られるものに対して write amplification が大きすぎるため、 デフォルトでは監査人が実際に確認する意味のあるケースを記録します。

エラーパスも監査されます。ハンドラが外界に到達してから raise した場合、その時点ですでに 世界に影響を与えているためです。

hidden field の release は宣言にかかわらず監査されます。 guarded query layer の境界には AC10 の contributor exemption が含まれるため、Capability のない Function でも、その consumer が自分では読めない field に対する mean や count を返せます。実際にそれを 行った call では call_function がエントリを追加します — audit=False も例外ではありません。 個人に結びつく数値をリリースしたことを、ontology が監査対象外にできないためです。

監査の trace は宣言ではなくreleaseに従います。

call 監査されるか
exemption を通じて hidden field をリリースした ✅ yes、宣言にかかわらず
consumer がもともと読める field を集計した 上表どおり
exemption を開いたが min-N で拒否された 上表どおり — 何もリリースされない
ontology 内のどこかで contributor_rules を宣言した 上表どおり — 宣言だけでは release ではない

1 つの call に監査理由が 2 つあっても、追加されるエントリは 1 件です。

裸の FunctionRegistry.call には境界がありません — この guarantee は、write gate が store ではなく execute() に属するのと同じく、client surface に属します。


Capabilities

ハンドラが外界に求めるものはすべて宣言し、バインド時に提供する必要があります。 未宣言の利用は拒否され、利用はすべて監査されます。Capability はハンドラが読む/呼ぶ ものです。

Mailer = ontology.capability(MailerProto, name="mailer")

@ontology.action(P, target=T, roles=["Agent"], capabilities=[Mailer])
def handler(ctx, params):
    ctx.capability(Mailer).send(...)

現在時刻には Capability ではなく ctx.now() を使います。

未宣言の Capability を要求すると UNDECLARED_CAPABILITY、宣言済みでもプロバイダが バインドされていなければ CAPABILITY_NOT_PROVIDED になります。

プロバイダは ontology.bind(store, capabilities={...})、またはクライアント単位で for_consumer(...) にバインドします。


セキュリティ

Consumer

フィールド 型
actor_id str
role str
scope_level str — オントロジーが宣言したレベルのいずれか
scope_id str
kind Literal["human", "ai"]

エンジンが強制する 4 つの仕組み:

  1. スコープ可視性 — 宣言された ScopePolicy を通じて解決。未解決のレベルは拒否。
  2. 機微度リダクション — プロパティごとの Sensitivity(ai_usable, human_visible)。 隠されたフィールドは None として読め、redacted_fields に名前が載ります。
  3. min-N — 異なる寄与者が min_n 未満の集計は、コード MIN_N_VIOLATION の VisibilityError。寄与者は宣言された contributor ルールから求められるため、 同一人物の複数行では閾値を満たしません。計数の対象は選択された全行ではなく、 value_field を持つ行だけです。min_n 人のうち一人だけが回答した集団では、 その回答は公開されません。寄与者が解決できない行は、一行ごとではなく全体で 一つの未知の識別子として数えます。リンクを閉じても寄与者を retire しても、 一人の複数行が公開可能な集団に変わることはありません。
  4. 本人特定リンク — 人間コンシューマーには、対象を解決する前に拒否されます。

covers_scope(policy, consumer, resolved) -> bool が唯一のカバー判定ルールであり、 読み取り経路と書き込み経路が共有します。


ストア

Store プロトコル

OntologyClient / GuardedQuery / ActionExecutor は具体的なバックエンドではなく、 このプロトコルに対して型付けされています。

insert(obj_type, payload, source) -> str
update(obj_type, obj_id, payload_changes, source) -> None
read_current(obj_type, obj_id) -> StoredObject | None
read_last(obj_type, obj_id) -> StoredObject | None
read_all(obj_type) -> list[StoredObject]
read_page(obj_type, after_key=None, batch=500) -> list[PagedRow]
retire_object(object_type, obj_id) -> StoredObject
create_link(link_type, from_id, to_id) -> None
close_link(link_type, from_id, to_id) -> bool
links_from(link_type, from_id) -> list[str]
links_to(link_type, to_id) -> list[str]
links_from_asof(link_type, from_id, asof) -> list[str]
links_to_asof(link_type, to_id, asof) -> list[str]
append_audit(entry) -> None
audit_entries() -> list[AuditEntry]
transaction() -> ContextManager
capture_action_writes() -> ContextManager[list[WriteRecord]]

insert は、同じ object type に有効な行がすでにある primary key を受け付けません。 ConflictError(code OBJECT_ALREADY_EXISTS)で拒否し、ストアは変更しません。 1 つのオブジェクトが持つ有効な行は最大 1 行です。SQL バックエンドは部分ユニーク インデックスでもこれを保証します。retire 済みのオブジェクトの id は再び insert でき、新しい有効な行が始まります。

insert と update は date / datetime の値を、ISO-8601 文字列または date / オフセット付き datetime オブジェクトとして受け取ります。date と datetime の値 を参照してください。

create_link は両端に有効なオブジェクトを必要とします。各 id はリンク型が宣言する from_type / to_type で検索します。存在しない id、retire 済みの id、別の型としてのみ 有効な id は ValidationFailed(code LINK_ENDPOINT_NOT_FOUND)で拒否し、リンク行は 書き込みません。端点の検査はカーディナリティより先に行います。同じアクション トランザクション内で先に insert したオブジェクトは有効として扱います。

create_link は冪等です。有効なリンクと同一のリンクを作成する呼び出しは、どの カーディナリティでも no-op になります。何も書き込まず、WriteRecord も記録せず、 正常に返ります。以前は MANY_TO_MANY のリンクが重複し、MANY_TO_ONE のリンクは自分自身に 対して CARDINALITY_VIOLATION で拒否されていました。同一リンクの検査はカーディナリティ より先に行います。対象は有効なリンクだけです。close_link の後は同じペアを再び リンクでき、閉じた行は履歴として残ります。SQL バックエンドは有効なリンクに対する 部分ユニークインデックスでもこれを保証します。

primary key は変更できません。update は、primary key を別の値に変える変更を ValidationFailed(code PRIMARY_KEY_IMMUTABLE)で拒否し、ストアは変更しません。 これにより、ストアの id と payload の primary key は常に一致します。現在と同じ値を 渡す場合は変更とみなさず、受け付けます。新しいキーが必要なときは、オブジェクトを retire してから新しく insert してください。

read_current は有効な行だけを返します。read_last は、その行が有効かどうかに 関わらず最新の行を返します。links_from/links_to は有効なリンクだけを返します。 _asof の 2 つは、指定した時刻以降に閉じられたリンクも返します。

履歴を見るこの 3 つの read は、呼び出し元が 1 つだけです。action executor の target gate です。gate は「スコープ外」と「retire 済み」を区別する必要があります。 retire されたオブジェクトも、属していたスコープを持ち続けます。ActionContext.retire はリンクを閉じるため、gate はオブジェクトの最後の行と、その行が閉じた時点で 持っていたリンクからスコープを解決します。

consumer の read は、あえてこれを使いません。retire 済みの行のスコープを解決すると、 その子オブジェクトが再び可視集合に入ります。すると母集団が min_n を超え、 retire 対象自身の行を含む集計値が返ってしまいます。read_last から retire 済みの行を除外したり、_asof から閉じたリンクを除外したりするバックエンドを 書くと、retire されたオブジェクトの正当な所有者が拒否されます。

gate はこの連鎖を、target 自身の行が閉じた時刻の断面で解決します。連鎖上のリンクは 上下どちらの境界もその時刻で読みます。そのため、target が既に離れていた辺が target の ために答えることはありません。

その時刻に ViaLink の hop が複数の親を見つけた場合は、連鎖が解決できた最初の親が 勝ちます。その順序は各バックエンドの行順ではなく、store が宣言する順序です。すなわち、 リンクの valid_from が最も早いものから、次にバイト順で最も小さい親 id からです。 作成順ではありません。links は単調増加のキーを宣言していないため、同じ tick で作られた 2 本のリンクは、どちらを先に書いたかではなく id で並びます。この順序が、どのスコープが そのオブジェクトを所有するか、したがってこの gate がどの操作者を通すかを決めます。その ため、すべてのバックエンドがこの順序で答えます。

祖先のオブジェクトは、その時刻に retire 済みでなければ採用します。ただし読むのは最新行です。 したがって payload、および payload から読む DirectProperty のキーは、その祖先が「いま」 持っている値です。その時刻より後に更新された祖先は、当時のスコープではなく現在のスコープを 答えます。オブジェクト側も断面にするには履歴を時刻指定で読む必要がありますが、Store に その read はありません。生きている target には閉じた時刻がないため、consumer の read と まったく同じ現在時刻で解決します。まだ存在するオブジェクトについて、この gate が緩むことは ありません。

「retire 済みの行も許す」ではなく断面で解決するのは、素直な 2 つの解釈がそれぞれ逆向きに 壊れるからです。祖先を常に最新行から解決すると、ずっと前に retire された祖先が生きている 祖先に競り勝ち、もう所有権を持たない操作者を通してしまいます。逆に祖先を現在時刻だけで 解決すると、target が閉じた時点では生きていて、いま生きているスコープへの唯一の経路である 祖先を拒否します。解散したチームは元に戻せないため、正当な所有者が恒久的に拒否されます。 target の valid_to の時点では、先に retire された祖先は既に閉じており、後まで残った祖先は 閉じていません。断面を 1 つ選べば、両方が同時に解決します。

連鎖が解決でき、consumer がそのスコープを覆う場合、gate は handler 自身の拒否 (OBJECT_ALREADY_RETIRED)まで到達します。SCOPE_DENIED の意味は 1 つではありません。 raise は 2 か所にあります。1 つは、スコープを担うパラメータが str でない場合の多層防御の 拒否です。型の不一致は先にパラメータ検証が弾くため、通常の経路ではなく型混同に対する床です。 もう 1 つは、覆えることを示せないときに必ず発火します。これは 1 つではなく 2 つの状況です。 連鎖は解決できたが、この consumer がその外にいる場合。これはこの gate が変えていない従来 どおりの拒否です。あるいは、その時刻に連鎖が解決できない場合で、既定の deny が働きます。 この断面が扱うのは後者だけです。宣言されているルールの種類は 4 つで、それぞれが自身の hop で retire に出会います。

  • SelfScope はオブジェクト自身の id を返します。retire はこの id を奪いません。
  • DirectProperty はスコープキーを payload から読みます。この gate が読むのは有効な行では なく最新行のため、retire は payload を変えません。
  • ViaLink は links テーブルをたどります。たどり着いた親のどれも順に解決できないとき、 この hop は何も解決しません。 target が閉じるより前に retire された親もその一つです。その時刻に辺そのものは読める ことがありますが、親の行が採用されないため、この gate が尋ねる 1 つの時刻にその親は 答えられません。target より後に retire された 親は、同じ cascade の tick で閉じたものも含めて、いまも target のために解決します。
  • CustomResolver は生の Store を受け取る作者のコードで、engine はその中に立ち入り ません。したがって、retire 済みのオブジェクトが何に解決するかは、この断面 ではなく resolver 自身の責任です。素直な実装は read_current を読みますが、これは retire 済みのオブジェクトには None を返すため、そう書かれた resolver は拒否します。 retire 後も precondition の拒否が必要な型は、2 つ目のルールを宣言してください。スコープ キーの列に置いた DirectProperty は retire 後も残ります。

各項目は 1 つの hop についての説明であり、1 つの target についての保証ではありません。 また、この 4 つが連鎖のすべてでもありません。ScopePolicy.rules は型ごとに順序付きの リストを持つため、target は宣言した数だけ hop を持ち、最初に解決できた hop が答えます。 さらに、自身のルールでは答えられない level は、より狭い level の canonical な インスタンスを宣言する型があれば、そこへ登ります。これも 4 つのどれでもありません。engine 自身の hop に共通する のは断面です。engine が読まなければならないオブジェクトが、target が閉じるより前に retire されていれば、そのどの hop からたどり着いたかによらず、そこで拒否されます。したがって、 target の level に答える hop は、その target の他の hop がまったく触れないオブジェクトで 失敗することがあります。CustomResolver がこの断面の外にあるのは、その callable 自身が行う read に ついてだけです。engine は作者のコードにその時刻を渡さないため、callable が自分でたどる祖先 は、retire 済みかどうかに関わらず callable の読み方どおりに読まれます。その答えは engine に戻ります。そこから engine が読むオブジェクトは、この断面の規則どおりに拒否されます。より 狭い level の答えが指す canonical なインスタンスも、engine が続けてルールを尋ねるオブジェクト も同じで、後者の行はその resolver が動く前に検査されます。

この一覧の拒否はすべて fail closed です。そのオブジェクト自身の所有者が拒否されるだけで 情報は出ません。

3 つの実装が同梱され、すべて同じ適合性テストスイートで検証されています。

  • ObjectStore — SQLite。履歴(close-old / insert-new)、リンク、監査ログ。
  • InMemoryStore — 純 Python。ファイルも SQL も無し。テストやドッグフーディング向け。
  • PostgresStore — PostgreSQL ベース。postgres extra で利用可能。

最外層の transaction() は、同じストア上の並行 writer に対して、読み取りに続く 書き込みを直列化します。再入可能であり、ネストした呼び出しは最外層の トランザクションを共有します。

ObjectStore(registry, path, *, busy_timeout=5.0) は、SQLite がデータベース ロックを待つ秒数を設定します。ActionExecutor は、外部サービスを呼ぶ ctx.capability() を含む handler 本体全体で最外層の transaction、したがって SQLite の write lock を保持します。競合する writer がその handler を待てる時間は、 自身の store の busy timeout までです。期限を超えると code STORE_BUSY (kind="conflict")が送出されます。正当な handler が既定値より長く実行される 場合は busy_timeout を増やすか、capability 呼び出しを短く保ってください。

Source — source_system、source_id、extracted_at。

retire_object は置き換え行を挿入せず、オブジェクトの current row を閉じます。リンクの cascade は行いません。close_link は 3 つの ID が一致する 1 本の live link を閉じます。 3 つのバックエンドがすべてこれらの verb を実装します。 ActionContext.retire の cascade は、ストアのオブジェクト retirement primitive の上位にあります。

date と datetime の値

date / datetime 型のプロパティの値は、どの書き込み経路でも同じ方法で検査します。 対象は Store.insert / update、ActionContext.create / save、bulk_upsert / client.ingest、Action パラメータ(MCP の execute_action を含む)です。

書き込む値 date プロパティ datetime プロパティ
date オブジェクト YYYY-MM-DD として保存 拒否
オフセット付き(aware)の datetime オブジェクト 拒否 自身の isoformat() の綴りで、オフセットを保って保存
naive な datetime オブジェクト(tzinfo なし) 拒否 拒否
"YYYY-MM-DD" 文字列 そのまま保存 拒否(時刻部分が必要)
"20261005" などの別の ISO-8601 日付表記 拒否 —
時刻を含む ISO-8601 文字列(オフセットの有無を問わず、Z を含む) — そのまま保存
その他の文字列 拒否 拒否
  • 正規化はしません。 ストアは綴りをそのまま保存します。オフセットを UTC に 変換せず、Z も Z のままです。eq と in のフィルターはこの綴りで一致を 判定します。比較演算子は時点(instant)で比較します (フィルター、順序、読み取り上限を参照)。
  • naive な文字列は受け付け、naive なオブジェクトは拒否します。 naive な ISO 文字列 は書いたとおりに保存します。naive な datetime オブジェクト は拒否 します。どの時点を指すかは tzinfo= を付けて示す必要があるためです。naive な値と オフセット付きの値を 1 つのプロパティに混在させないでください。両者の間に順序は 定義されていません。
  • 拒否時のコード。 拒否された値は、Store.insert / update と ActionContext.create / save では ValidationFailed(INVALID_RECORD)を 送出します。取り込みではレコードごとにレポートへ INVALID_RECORD が載ります。 Action パラメータはハンドラの実行前に INVALID_PARAMS で拒否されます。where の operand は OPERATOR_TYPE_MISMATCH で拒否されます。
from datetime import datetime, timedelta, timezone

from ontary import InMemoryStore, Ontology, OntologyObject, Source, prop
from ontary.errors import ValidationFailed

ontology = Ontology("shifts", scope_levels=["org"], min_n=1)


@ontology.object(layer="L0", scope="unscoped")
class Shift(OntologyObject):
    id: str = prop(primary_key=True)
    starts_at: datetime


ontology.validate()
store = InMemoryStore(ontology.registry)
source = Source(source_system="roster")
jst = timezone(timedelta(hours=9))

store.insert("Shift", {"id": "s1", "starts_at": datetime(2026, 10, 5, 9, tzinfo=jst)}, source)
store.insert("Shift", {"id": "s2", "starts_at": "2026-10-05T00:00:00Z"}, source)
assert store.read_current("Shift", "s1").payload["starts_at"] == "2026-10-05T09:00:00+09:00"
assert store.read_current("Shift", "s2").payload["starts_at"] == "2026-10-05T00:00:00Z"

try:
    store.insert("Shift", {"id": "s3", "starts_at": datetime(2026, 10, 5, 9)}, source)
except ValidationFailed as exc:
    assert exc.code == "INVALID_RECORD"  # a naive datetime object
else:
    raise AssertionError("a naive datetime object must be refused")

スキーマバージョニング

すべての SQLite ファイルは、作成時にエンジンの SCHEMA_VERSION を PRAGMA user_version へ刻印します。Postgres も同じ番号を schema_meta に記録します。 どちらのバックエンドも移行のはしご(migration ladder)を持ちません。そのため、刻印が 一致しないストアはすべて、構築時点で STORE_VERSION_UNSUPPORTED として両方のバージョンを 挙げて拒否します。刻印が高い場合も低い場合も、未刻印で objects テーブルをすでに 持つ場合も同じです。スキーマバージョンをまたぐ移行はオペレーターの明示的な手順です。 対応する ontary バージョンで開くか、新しいストアにデータを移してください。 drop して作り直す手順は storage.md にあります。


バルク取り込み

bulk_upsert(store, registry, obj_type, records, source) -> IngestReport
bulk_link(store, registry, link_type, pairs, source) -> IngestReport

client.ingest(
    obj_type, records, source, *,
    on_error: Literal["raise", "report"] = "raise",
) -> IngestReport
client.ingest_links(
    link_api_name, pairs, source, *,
    on_error: Literal["raise", "report"] = "raise",
) -> IngestReport

bulk_upsert と bulk_link はエンジン層であり、常に IngestReport を返します。 クライアントのメソッドはバッチを最後まで処理するため、別のレコードが失敗しても 有効なレコードはコミットされたままです。クライアントはデフォルトで失敗時に IngestError を送出します。.report に完全なレポートが入り、メッセージには コミット済みと失敗したレコード数が含まれます。on_error="report" を渡すと、 送出せずにレポートを返す従来の動作になります。

IngestReport — inserted_ids: list[str]、errors: list[IngestError]。レコードは 宣言された形状に対して検証されます。主キーの欠落、必須プロパティの欠落、未知の プロパティ、型の不一致は INVALID_RECORD になります。オントロジー所有の型への 書き込みは OWNED_TYPE_REFUSED、オントロジー所有プロパティの指定は OWNED_PROPERTY_REFUSED になります。端点に有効な行がないリンクのペアは、 カーディナリティ違反と同様にペア単位で LINK_ENDPOINT_NOT_FOUND として拒否します。 他のペアはそのまま登録されるため、オブジェクトをリンクより先に ingest してください。 リンクの読み込みの再実行は冪等です。有効なリンクと同一のペアは no-op になり、 inserted_ids にはそのまま含まれるため、2 回目の実行は 1 回目と同じレポートを返します。

date / datetime の値は、ISO-8601 文字列または date / オフセット付き datetime オブジェクトとして書き込みます。naive な datetime オブジェクトは INVALID_RECORD で拒否されます。date と datetime の値 を参照してください。


MCP サーバー

from ontary.mcp_server import build_mcp_server

server = build_mcp_server(ontology, store, consumer, *, name=None,
                          capabilities=None)  # -> MCPServer

1 サーバープロセスにつき 1 つの Consumer アイデンティティ。宣言済みハンドラは バインド済みで届きます — 登録用コールバックはありません。

12 個のツール。いずれも Python surface と同じガードの対象です。読み取り専用ツールには ToolAnnotations(readOnlyHint=True)、execute_action には ToolAnnotations(destructiveHint=True) が付きます。

ツール 用途 Annotation
list_object_types イントロスペクション readOnlyHint=True
list_link_types イントロスペクション readOnlyHint=True
list_action_types イントロスペクション(パラメータ定義を含む) readOnlyHint=True
list_functions イントロスペクション readOnlyHint=True
get_declarations 宣言された契約のバンドル readOnlyHint=True
get_object 単一読み取り readOnlyHint=True
query_objects フィルタ/ページ付き読み取り readOnlyHint=True
count_objects 可視行の件数 readOnlyHint=True
aggregate_objects 集計読み取り readOnlyHint=True
traverse_links リンクを辿る readOnlyHint=True
execute_action Action の実行 destructiveHint=True
call_function Function の呼び出し readOnlyHint=True

list_object_types はすべてのプロパティに transitions キーを含めます。グラフがない場合は null、ある場合は完全な initial 状態リストと moves の対応を返します。各オブジェクト型には rules リストもあり、各ルールの name と message を含みます。ルールのコードは含まれません。

query_objects(obj_type, where=None, order_by=None, limit=None, after=None) は MCP surface では常に 上限付きです。limit を省略するとサーバーのデフォルト上限 100 行を使い、明示する 場合の最大値は 1000 です。内部のページ付き読み取りが不透明な next_cursor を返し、 成功時のレスポンスは従来どおり行を result に置いたまま、同じ階層に next_cursor を追加します(消化済みなら null)。同じ明示的な limit とともにカーソルを返して 次ページを取得してください。明示的な limit なしの after は AFTER_WITHOUT_LIMIT、1 未満または 1000 超の値は INVALID_LIMIT になります。

where の文法は、素の scalar を等価一致として使うか、gt、gte、lt、lte、 in、ne、contains を指定する mapping 形式です。1 つの mapping に複数の演算子を 書くと AND 条件になり、{"gte": a, "lt": b} は範囲指定です。演算子は宣言済み プロパティ型に対して検証され、未知の演算子は UNKNOWN_OPERATOR、型に合わない演算子または operand は OPERATOR_TYPE_MISMATCH になります。date の比較は保存される ISO 日付の順序を使い、 datetime の比較は UTC オフセットをまたいで瞬間で行います。 lineage フィールドは対象外で、未知のキーは UNKNOWN_FIELD になります。 order_by は宣言済み payload フィールド(デフォルトは昇順)、または (field, "asc"|"desc") の組を受け付け、ページカーソルと組み合わせられます。 count_objects はコンシューマーに可視な行の件数を返し、min-N の対象外です。 開示するのは query_objects が既に一覧する内容だけであり、min-N でリリース された件数が必要な場合は value_field 不要の aggregate_objects(func="count") を使ってください。 aggregate_objects は func="mean"|"count"|"sum"|"min"|"max" を受け付け、 デフォルトは "mean" です。すべての関数に同じ min-N の公開判定が適用され、 グループ付きの空集合 {} も拒否されます。value_field は func="count" では 省略可能で、省略すると可視な行すべてをカウントします。それ以外の func では必須 であり、指定がなければ INVALID_PARAMS になります。 traverse_links は、委譲先の OntologyClient.traverse/GuardedQuery.traverse に limit/after とカーソルの API がないため、今回もページなしのリストです。 reverse=true を渡すとリンクの target 側から辿って source 側のオブジェクトを返します。 本人特定リンクの拒否は両方向で対称です。

オブジェクトは {"payload": {...}, "lineage": {...}} としてシリアライズされます (None は None のまま)。エラーは下表のコードを返し、分類できないものは内部情報を 呼び出し側に漏らさないよう INTERNAL_ERROR になります。

mcp エクストラが必要です。

マルチコンシューマー配信

from ontary.mcp_server import build_multi_consumer_mcp_server, ConsumerResolver

server = build_multi_consumer_mcp_server(
    ontology, store, *, resolve_consumer, name=None,
    capabilities=None,
    token_verifier=None, auth=None,
)  # -> MCPServer

1 サーバープロセスで多数の証明済みアイデンティティを提供します — consumer 引数は ありません。1 つの OntologyRuntime(1 つの GuardedQuery、1 つの ActionExecutor、 一度だけバインドされた宣言済みハンドラ)だけを構築し、各呼び出しは軽量な runtime.for_consumer(...) ビューを取得します。

呼び出しごとに、その request の MCP 検証済み AccessToken をトランスポート自身の contextvar から読み取り、呼び出し元が渡した ConsumerResolver (resolve_consumer(token) -> Consumer | None)を呼び、resolver が返ったあとで トークン(subject、なければ client_id)から Consumer.principal をスタンプします — そのため resolver は誰が認証したかを偽装できません。解決結果はキャッシュされません。 失効・再スコープされたトークンが古いバインディングのまま提供されることはありません。

build_mcp_server と同じ 12 個のツールで、イントロスペクションを含め同じ 3 通りの fail-closed 挙動をします。

条件 コード
request に検証済み AccessToken がない(get_access_token() が None を返す — トークン未提示、または token_verifier 未設定の HTTP。stdio では常にこれに該当) UNAUTHENTICATED
検証済みプリンシパルに対して resolve_consumer が None を返す CONSUMER_UNRESOLVED
resolve_consumer が意図的な OntaryError 以外の例外を送出する 汎用の INTERNAL_ERROR — トークン材料を含みうるため、送出されたメッセージは呼び出し側に届きません

この SDK はトークンを検証も発行もしません — token_verifier と auth は MCP 自身の 型です(mcp.server.auth.provider.TokenVerifier / mcp.server.auth.settings. AuthSettings)。デプロイヤーが設定し、内部の MCPServer(...) 呼び出しへそのまま 渡されます — MCPServer は構築後にどちらを設定する public なセッターも公開して いないため、ここが唯一の配線ポイントです。どちらも渡さないのは stdio 専用、 または意図的に認証なしのデプロイとして正当ですが、どちらか片方だけを渡すのは 実行時の状態ですらありません — MCPServer.__init__ がその場で ValueError を 送出する(構築時点での fail-fast)ため、サーバーは構築されず、呼び出しも一切 発生しません。stdio には認証コンテキストが全くないため、この 2 引数の値に 関わらず stdio 上のマルチコンシューマーサーバーは常にすべての呼び出しを UNAUTHENTICATED で拒否します。1 コンシューマー・stdio プロセスには build_mcp_server(ontology, store, consumer) を使ってください。

トランスポートのオプションは run()/streamable_http_app() に渡します。 ビルダーはセッションモードを強制しません。 mcp 2.x では、stateless_http、json_response、transport_security、host は run() と streamable_http_app() のキーワード引数です。 ASGI アプリはソケットを bind しないため、port は run() のキーワード引数です。 stateful セッションでも、各 request はその request 自身のトークンを解決します。 tests/test_mcp_multi_consumer.py は ASGI 境界で両方のモードを固定しています。 build_mcp_server は構築時に束縛された 1 つの Consumer だけを持ち、request ごとに 解決するアイデンティティはありません。

mcp エクストラが必要です。


記述子による宣言

クラス宣言が構築されている、より低レベルの surface です。生成された、あるいはデータ 駆動のオントロジーに有用ですが、たいていの作者は Ontology を使うべきです。

型 主なフィールド
ObjectTypeDef api_name, display_name, description, layer, properties, primary_key, rules, owned
PropertyDef name, type, choices, fields, transitions, required, sensitivity, scope_level
LinkTypeDef api_name, from_type, to_type, cardinality, description, identity_revealing, owned
ActionTypeDef api_name, display_name, target_type, executable_by_roles, description, parameters, capabilities
ActionParameterDef name, type, choices, fields, required, refers_to, scope_semantics
StructFieldDef name, type, choices, required
TransitionDef initial, moves
RuleDef name, message, check
FunctionDef api_name, description, input_description, output_description, parameters, capabilities
Sensitivity ai_usable, human_visible

PropertyType は Literal["str", "int", "float", "bool", "date", "datetime", "json", "struct"]。

StructFieldDef はフラットな内側のフィールドを 1 つ記述します。type は scalar の プロパティ型で、choices は str フィールドに使う任意の文字列 tuple、required の 既定値は True です。PropertyDef.fields と ActionParameterDef.fields は type="struct" では空でない tuple、それ以外の型では None です。MCP の schema は tuple を list として出力し、常に fields キーを含めます。

TransitionDef は choice プロパティで許可する状態を記述します。initial は空でない 開始状態の tuple で、moves はすべての choice から許可する遷移先への対応です。 終端状態には空の tuple を使います。すべての状態は宣言済みの choice に含めます。 PropertyDef.transitions に設定します。

RuleDef は新しい行全体を受け取る名前付き述語を check: Callable[[dict[str, Any]], bool] として記述します。name と message は 空にできず、同じ ObjectTypeDef.rules tuple 内の名前は一意である必要があります。 rule の check は渡されたオブジェクトだけを読み取るようにしてください。 model_dump() は check を除外するため、公開する宣言には rule の名前とメッセージ だけが含まれます。

OntologyRegistry が記述子を保持し、相互参照を検証します。validate() は、 リンク端点の参照切れ、Action 対象の参照切れ、api_name の重複、properties に無い主キーに 対して ValidationFailed(ONTOLOGY_INVALID)を送出します。

OntologyDef は registry + スコープポリシー + 設定をまとめたもので、クライアントや MCP サーバーがバインドする単位です。この SDK にはモジュールグローバルなレジストリが 意図的に存在しません。したがって 2 つのオントロジーが 1 プロセス内で干渉せず共存できます。

Declarations / declarations(...) は宣言された契約をデータとして公開します — MCP の get_declarations が返すものです: authority(モデル宣言・実行時チェック)、 capabilities(action/function ごとに宣言され、未提供・未宣言なら fail-closed。 provider 自体はサンドボックス化されない作者コード)、writeback (オントロジーへの書き込みはすべてオントロジー所有。実行時自身の外部書き込み経路は 持たないが、capability provider はインラインで外部書き込みもできるため、 これは宣言された規約であって強制された境界ではない)、reingest (upsert-merge。所有プロパティは残り、削除はない)、visibility_default (deny-by-default — 未解決のスコープは隠れる)、transaction_ownership (実行時所有 — 呼び出し元が開いたトランザクションを拒否)、 idempotency(なし — 再試行は別個の監査済み試行になる)、audit_scope (テナントスコープの管理者向けビュー。action は常に監査され、function は capability を 宣言した場合に限り監査される(function 単位の上書きがない限り)。ただし contributor exemption を通じて hidden field をリリースする call は常に監査される)、そして tenancy(構築時にバインドされた ストアインスタンスごとに 1 テナント)。identity を含みます: マルチコンシューマー MCP サーバーでは、この実行時ではなくトランスポートによって証明され — 検証器が 設定されていないデプロイはデフォルトのアイデンティティを仮定するのではなく、 すべての呼び出しを拒否します。1 コンシューマーサーバー、あるいは直接 Python から 使う場合は、Consumer は構築時にオペレーターが主張するものであり、何もそれを 証明しません。検証済みプリンシパル(マルチコンシューマーのみ)は、呼び出し元が 渡した resolver(サンドボックス化されない信頼された作者コード)によって Consumer にマッピングされ、トランスポートが証明した principal と解決された actor の どちらも監査されるため、resolver がすべてのプリンシパルを 1 つの特権的な actor に マッピングした場合、それはログ上で可視化されます。 監査された行のすべてが principal を持つわけではありません。min_n はオントロジー固有の唯一の答えで、オントロジー自身の ScopePolicy.min_n から読み取られます。


エラーコード

送出されるすべてのエラーは安定したコードを持ちます。OntaryError が基底で、 ERROR_CODES: dict[str, ErrorCodeInfo] が機械可読なレジストリです。

以下のコード表は、ソースの ERROR_CODES から自動生成しています。翻訳による 乖離を避けるため、説明文は原文(英語)のままです。

authority

Code Meaning
AUTHORITY_ERROR Fallback code for the authority-refusal family (AuthorityError); every concrete refusal a captured write can trigger carries its own more specific code instead (e.g. SOURCE_CREATE_REFUSED, UNDECLARED_SOURCE_WRITE). It is the base for ObjectStore.capture_action_writes refusals where a write inside an action's capture context crosses the source-backed/ontology-owned line.
OWNED_PROPERTY_REFUSED A bulk_upsert record supplied a value for a property declared ontology-owned on an otherwise source-backed object type.
OWNED_TYPE_REFUSED A bulk_upsert/bulk_link record targeted an object or link type that is declared whole-type ontology-owned; no source may supply its rows.
SOURCE_CREATE_REFUSED A captured insert targeted an object type that is not declared whole-type ontology-owned (ObjectTypeDef.owned is True).
UNDECLARED_SOURCE_REMOVAL A captured retirement or link closure targeted an object or link type that is not declared ontology-owned.
UNDECLARED_SOURCE_WRITE A captured update touched a property, or a create_link targeted a link type, that is not declared ontology-owned.

conflict

Code Meaning
CALLER_TRANSACTION_REFUSED Raised when ActionExecutor.execute() (or an ingest entry point, a later task) is called while the caller has already opened a store.transaction() block (declared-contracts §3 AC9). transaction() is reentrant, so a caller-owned outer transaction could roll back an action after the executor reported success and audited ok. The engine must own the transaction/audit boundary and refuses to nest inside the caller's. Deliberately NOT audited (spec §5): an audit row inside the caller's transaction could itself be rolled back, so the refusal is raised before any audit write.
CARDINALITY_VIOLATION A link creation would violate its LinkTypeDef cardinality.
OBJECT_ALREADY_EXISTS An insert used a primary key that already has a live row of the same object type; update that object instead, or retire it first.
OBJECT_ALREADY_RETIRED A retirement targeted an object whose current row is already closed.
STORE_VERSION_UNSUPPORTED Raised at store construction when the store's schema stamp is not this engine's SCHEMA_VERSION -- a SQLite file's PRAGMA user_version, or a Postgres database's schema_meta row. Neither backend carries a migration ladder: a store written by a different ontary schema shape is REFUSED, never migrated in place and never adopted. An unstamped store that already has an objects table is refused for the same reason -- stamping a shape this engine cannot read would be a lying stamp, and every later query would fail as a confusing uncoded SQL error instead. The message names BOTH the store's and the engine's versions, so an operator knows exactly what to upgrade; the way forward is a matching ontary version, or a fresh store the data is migrated into.
STORE_BUSY A SQLite transaction could not acquire or retain its database lock within ObjectStore's configured busy timeout; retry after the competing writer finishes or increase busy_timeout. This is a conflict, not a precondition: retrying is the remedy, and the kind travels on the MCP wire so callers can branch on retryability.

internal

Code Meaning
INTERNAL_ERROR An unclassified failure the MCP surface refuses to describe further, to avoid leaking internals to the caller.
STORE_ERROR Fallback code for an unclassified store-layer error.

permission

Code Meaning
PERMISSION_DENIED The consumer's role is not permitted to execute the action (code PERMISSION_DENIED). The permission kind also covers scope refusals under SCOPE_DENIED; each raise site supplies the specific code.
SCOPE_DENIED The consumer's scope does not cover the action's declared target/scope parameter (code SCOPE_DENIED). Role refusals use PERMISSION_DENIED; both are kind permission and each raise site supplies the specific code.
UNAUTHENTICATED A request carried no verified identity at all -- kind permission. build_multi_consumer_mcp_server raises this for every tool call, including introspection, that reaches it with no authenticated AccessToken: stdio (which has no auth context) or HTTP with no token_verifier configured. The fix is: configure authentication. It is deliberately separate from CONSUMER_UNRESOLVED: a missing credential; mapping the principal fixes a missing consumer, so callers can distinguish the two from .code alone.
CONSUMER_UNRESOLVED A verified principal existed, but the author's resolve_consumer callback returned no Consumer for it -- kind permission. build_multi_consumer_mcp_server raises this when the callback returns None for an otherwise verified AccessToken; the fix is: map this principal. It remains distinct from UNAUTHENTICATED, where no credential was presented at all, so the two failures are distinguishable from .code alone.

precondition

Code Meaning
CAPABILITY_NOT_PROVIDED A declared capability had no provider bound for this call.
CLOCK_CONFLICT A store already has a different clock installed; a store has one clock. Bind with the same clock object, or with no clock to use the one already installed.
CLOCK_REGRESSION The store clock reads earlier than the valid_from of the version a write would close; the clock went backwards. Fix the clock (it must never run behind the data it wrote) and retry.
FUNCTION_ERROR Registering/calling a Function failed: undeclared api_name, duplicate registration, or no handler bound.
PRECONDITION_FAILED An action's precondition failed; the message names it. The conventional code for ActionError (kind precondition); an author may attach their own stable code instead (AC7), e.g. raise ActionError("...", code="GAP_NOT_ACKNOWLEDGED"). It is also used with overridden codes for unregistered/unhandled actions (UNKNOWN_ACTION) and parameter-validation failures (INVALID_PARAMS) -- see the code= overrides at those raise sites.
TRANSITION_NOT_ALLOWED A governed property changed to a state not allowed by its declared transition graph; action starts must be initial states.

validation

Code Meaning
CLOCK_NOT_TIMEZONE_AWARE A clock returned a naive datetime; an instant must be timezone-aware. Return datetime values with a tzinfo, such as datetime.now(timezone.utc).
AFTER_WITHOUT_LIMIT GuardedQuery.get_objects's (or OntologyClient.list's) after was given without limit (pagination-hardening T2 review P1) -- the unpaginated Store.read_all path has no page to resume, so ignoring after would let a caller that lost track of its limit silently re-read every visible row and duplicate work; a caller that genuinely wants everything passes no after at all.
INVALID_BATCH Store.read_page's batch was < 1 (SQLite's LIMIT -1 means unlimited and InMemoryStore's negative slice drops rows -- both the opposite of a bounded read).
INVALID_CURSOR Raised when Store.read_page's after_key is malformed OR simply unknown. after_key is UNTRUSTED input: it reaches the store from an MCP client via a later page-filling loop, round-tripped from a previous page's cursor without any guarantee the caller did not tamper with it. As amended 2026-07-25 (T2 review, spec §5), it is a random per-row PAGE TOKEN (objects.page_token, uuid4 hex), not a decimal row id. Resolving token to row id through the unique index is the ONLY way to turn a cursor into row identity, so every string never issued for a real row (malformed, tampered, or made up) raises this same error on both backends. There is no distinct well-formed but out-of-range case from the old integer design's OverflowError/silent-empty-page divergence. A token issued for a row since superseded by update still resolves because lookup uses row_id independently of valid_to, so an in-flight cursor remains a valid resume point (spec §8).
GROUP_KEY_COLLISION Two distinct group_by values in one selection release as the same dictionary key, so one cell would have to describe two populations. The released shape is dict[str, ...] -- a public return type and MCP's wire shape -- and str() is not injective over the values a group key can take: an optional property keys None on the rows that lack it, which collides with a row carrying the literal string "None". The populations did not merge; the later one overwrote the earlier, so the released value (and, under func="count", the released size) described whichever rows were inserted last, decided by nothing the caller supplied or could observe. Raised per group as each is released, AFTER that group's min-N check, so the release floor keeps precedence over a shape refusal.
INVALID_GROUP_BY GuardedQuery.aggregate_by's (or BoundQuery's/OntologyClient's) group_by cannot be a group key. Either it was falsy (e.g. "") -- the shared aggregation body branches on group_by's truthiness, so a falsy-but-non-None value would otherwise silently collapse to the ungrouped path and return a float instead of a dict[str, float] -- or it names a property whose declared PropertyType is not groupable (json, whose values may be a dict or list and so need not be hashable; grouping by one used to raise a bare TypeError from inside the grouping loop, and INTERNAL_ERROR once it crossed the MCP boundary). The declared type is checked, not the stored values, so a json column that happens to hold only scalars refuses too rather than working until the first dict arrives. Both are checked in aggregate_by, where the GuardedQuery, BoundQuery, and client surfaces converge, before _aggregate runs, rather than relying on an assert removed by python -O.
struct プロパティもグループ化できません。group_by に指定すると、行を読む前にこのコードになります。
PAGE_NOT_ITERABLE A Page/TypedPage was iterated, indexed or measured directly instead of through .items. Both are pydantic models, so the inherited BaseModel.__iter__ would otherwise yield (field_name, value) pairs -- for row in page hands back ('items', [...]) and ('next_cursor', ...), and the failure surfaces later as AttributeError: 'tuple' object has no attribute 'payload' at whatever touched the row. This refuses at the iteration itself and names .items and limit=None.
INVALID_LIMIT GuardedQuery.get_objects's (or OntologyClient.list's) limit was < 1 -- a silently empty page would hide that the call was malformed rather than legitimately paginated.
STALE_CURSOR An ordered walk's cursor resolved to a row that is no longer current; restart the ordered walk from the first page.
INVALID_PARAMS A call's parameters failed declared-shape validation: an action's params, or a read parameter whose SHAPE is wrong -- an order_by that is neither a field name nor a (field, direction) pair, or a where= that is not a mapping of field name to condition. A parameter naming something that does not exist is UNKNOWN_FIELD instead; this code is about the shape, not the name.
INVALID_RECORD A bulk_upsert record failed declared-shape validation (missing primary key, missing required property, unknown property, or a value that does not match its declared type). The validation kind carries the SAME INVALID_RECORD code that bulk_upsert already reports: from a caller's point of view, a record not matching the declaration is one failure regardless of which write path noticed. This closes the M9 hole where only ingest checked: Store.insert/update and therefore ActionContext.insert/update could commit a row missing a required property or carrying a wrong-typed value, report success, and leave the typed reader unable to hydrate it. The same code wraps a Pydantic ValidationError while hydrating a stored OntologyObject payload (for example, a non-ISO datetime string), never surfacing a bare traceback; a stored row failing declared-shape validation on read-back is the same failure class ingest carries on write. Ontology.diagnose(store=...) reports, per type and property, the stored rows that would fail hydration under the current ontology, and Ontology.validate(store=...) raises this code for them.
LINK_ENDPOINT_NOT_FOUND A link creation named an endpoint id with no live row of the link type's declared endpoint type -- missing or retired; a link needs a live object at both ends.
LINK_NOT_FOUND A link closure found no matching live link.
NON_NUMERIC_AGGREGATE GuardedQuery.aggregate's value_field is declared a non-numeric PropertyType (anything other than int/float, such as str/json/datetime/bool). It is checked against the declared type before rows are iterated or coerced, so values that merely look numeric cannot bypass the type contract (spec m35-sdk-refactor §6 AC7). func="count" is exempt and accepts any declared type.
OBJECT_NOT_FOUND An update targeted a non-existent object.
OBJECT_NOT_LOADED ActionContext.save got an object this action context did not hand out; load it with ctx.get(...) or ctx.create(...) first, so only the fields the handler changed are written.
OBJECT_RETIRE_NOT_FOUND A retirement targeted an object with no stored row.
PRIMARY_KEY_IMMUTABLE An update tried to change an object's primary key; a primary key is immutable, so retire the object and insert a new one instead.
RULE_VIOLATED A declared object rule returned false or raised while checking the full new row.
ONTOLOGY_INVALID A declaration was rejected: validate() found invalid cross-references, or an authoring call (@ontology.object(...), ontology.link(...), .definition) refused a kwarg of the wrong shape: a misspelled scope/cardinality literal, a rule not wrapped in a list, a non-callable row_visibility, empty scope_levels, or min_n below 1.
SCOPE_POLICY_ERROR A ScopePolicy declaration is unusable: a rule references an undeclared object type, link type, or scope level; a type declares an empty contributor rule list; a type is listed in unscoped_types while also declaring scope rules; or an action's scope parameter refers to an unscoped type.
UNDECLARED_CAPABILITY A handler requested a capability its action or function did not declare.
UNDECLARED_EVENT An action emitted an event type it did not declare.
EVENT_SUBJECT_INVALID An emitted event's subject could not be resolved to a valid target object.
UNKNOWN_ACTION An action name is unregistered on the OntologyRegistry, or has no handler bound to it.
UNKNOWN_FIELD A typed get/list call named a key that is not one of the target class's declared properties (spec typed-authoring AC7). The existence-only check runs client-side before the guarded read layer; a hidden-but-declared key still reaches the visibility kind unchanged, and the string-form surface keeps its silent-non-match behavior (AC8). The error lives here since C3 of the staged refactor (previously ontary.functions, which re-exports it).
UNKNOWN_LINK_TYPE An operation referenced an unregistered link type.
UNKNOWN_NAME A typed BoundQuery/OntologyClient call named an unregistered object, link, action, or function -- e.g. an undecorated class, a class/LinkHandle registered on a different Ontology, or a link api_name absent from this registry (typed-authoring AC7 / typed-actions AC8). Typed lookup failures use the validation kind and live here since C3 of the staged refactor so ontary._typed_api can raise them below the runtime modules.
UNKNOWN_OBJECT_TYPE An operation referenced an unregistered object type.
UNKNOWN_OPERATOR A mapping-form where clause named an operator outside the declared set: gt, gte, lt, lte, in, ne, or contains -- or was an empty mapping. A mapping with several operators is validated key by key, so one unknown key refuses the whole clause.
OPERATOR_TYPE_MISMATCH A mapping-form where operator is not valid for the property's declared type (comparisons need int, float, date, or datetime; contains needs str), or its operand is not a declared-type scalar.
struct プロパティに対する where 条件もこのコードになります。メッセージは where is not supported on struct property 'amount' のように示され、内側のフィールドパスは使えません。

visibility

Code Meaning
MIN_N_VIOLATION An aggregate would be computed over fewer than min_n distinct contributors.
VISIBILITY_DENIED A single-object read/write targeted an object outside the consumer's scope.

全 57 コード / 7 種別。


例外階層

公開されている kind class は次の 9 型の階層です。IngestError は、レコード単位または クライアント単位の取り込み失敗に使う、追加のコード付き OntaryError サブクラスです。 その code はレポート内の失敗に対応します。すべてのエラーは ERROR_CODES の安定した code を持ちます。特定の種別を捕捉するには except <KindClass> as e: e.code を使い、 IngestError を含むすべてのコード付きエラーを捕捉するには except OntaryError as e: e.code を使います。

例外 親クラス 送出される場面
OntaryError Exception すべてのコード付きエラーの根。コード付きエラーをすべて捕捉するために使います。
VisibilityError OntaryError 可視性ルールが読み書きまたは隠しフィールド操作を拒否したとき、または集計の寄与者が min_n 未満のとき。
PermissionDenied OntaryError 呼び出し元に必要な role、scope、または認証済み identity がないとき。
PreconditionFailed OntaryError 必要な operation または action の前提条件を満たさないとき。
ValidationFailed OntaryError 呼び出し元の入力、宣言、レコード、その他の値の検証に失敗したとき。
AuthorityError OntaryError source または呼び出し元が、宣言された権限の範囲外へ書き込もうとしたとき。
ConflictError OntaryError 要求された操作が store、ontology、schema、または link の状態と衝突したとき。
InternalError OntaryError 失敗について、これ以上具体的なコード分類がないとき。
ActionError PreconditionFailed action handler の前提条件に失敗したとき。code="PRECONDITION_FAILED" か独自の安定コードを指定します。
IngestError OntaryError レポート内の失敗に対応する安定コードを持つ、レコード単位またはクライアント単位の取り込み失敗。