AI 科技

Without Palantir: What a Contract, a Test and an Approval List Cover

In August 2024, a Palantir forum user asked for a code-based way to define the Ontology; an employee said the tooling was not ready. This piece checks how much of four lenses — semantic definition, action, permission, lineage — a contract, a test, and an approval list in three public repos cover, and which slices are not the same thing as Palantir's.

2026.09.02 · 作者 dvdmaru · 約 13 分鐘 · 5,117 字

本文另有中文版:沒有 Palantir:一頁契約、一支測試、一份核准清單,各做到了 Ontology 的哪一格

In August 2024, a user on Palantir’s own community forum asked whether there was an Infrastructure-as-Code or API way to define the Ontology, to rely less on the GUI. On August 20, a Palantir employee posting under the forum’s “Palantirians” group label, account aparson, replied: “Currently there is no way for you to use this tooling.” The same reply added that the infrastructure was still under development and testing. An earlier reply from the same account, posted August 19, said the direction would roughly let a user define an ontology in code and then push that definition to Foundry through CI/CD as a marketplace product — early days, by Palantir’s own account at the time.

Two years later, read in September 2026, Palantir’s documentation describes a full path called SuperRepo: object types, links, and actions declared in TypeScript, with code as the source of truth. A text file as the source of truth for an ontology is not a self-build trick — Palantir built that road too. This piece organizes the rest of its argument around four lenses — semantic, action, permission, lineage — that are this article’s own comparison frame for lining three repos up against Palantir’s documentation, not Palantir’s own classification. Palantir’s own split is data, logic, action, security, and Palantir states separately that the Ontology is not a semantic layer. This piece continues part 1 and part 2 of this series. Part 1 walked through what each of the four terms maps to in Palantir’s own documentation; part 2 walked through one action’s full path, from definition to submission, write-back, and logging. What this piece checks is not whether a text file can define an ontology — it is who enforces that definition, and when, once it has been written.

The test case is three public repos the author maintains: basketball-tools (GitHub dvdmaru/basketball-tools, public), health-tools (GitHub dvdmaru/health-tools, public), and racing-tools (GitHub dvdmaru/racing-tools, public). One person, no platform, three static sites — the semantic side runs on a one-page MODEL.md plus a test, the lineage side on a citation-receipt gate, the action side on an approval list. Each covers a slice of what Palantir’s documentation defines; some pieces are missing entirely, and some that exist are not the same thing as Palantir’s version. The Palantir documentation cited here was read in September 2026.

Five Officially Defined Building Blocks Frame Palantir’s Semantic Side

Palantir’s core-concepts page and its Create object type page give each of five semantic concepts a one-sentence definition: an object type is the schema definition of a real-world entity or event, a property is the schema definition of a characteristic of that entity or event, a link type is the schema definition of a relationship between two object types, a primary key is the property that acts as a unique identifier for each instance, and a backing datasource is the source of the data used as property values. These are five building blocks Palantir defines separately — Palantir’s documentation does not state that every object type must carry all five as one minimum set.

Palantir describes the relationship between objects and data tables with a direct comparison: “The definition of an object type in the Ontology is analogous to that of a dataset, while the definition of an object is analogous to that of a row in the dataset.” A row in a data table corresponds to an object. The primary key definition is equally plain: “The property that acts as a unique identifier for each instance of an object type. Each row in the backing datasource must have a different value for this property.” How the semantic side gets built, Palantir’s documentation states, is by mapping existing datasources into objects, properties, and links.

Beyond these five building blocks, Palantir’s documentation lays out a code-based path for defining an ontology: SuperRepo. Palantir’s documentation states: “If you define your Ontology in code rather than in the UI, consider creating a SuperRepo”. Under SuperRepo, Ontology-as-code compiles TypeScript definitions into real platform entities: “Ontology-as-code provides a pro-code way for you to define your Ontology entities in a SuperRepo. TypeScript definitions of your object types, interfaces, actions, and other entities are compiled and materialized into real entities on your enrollment after your product is deployed. Ontology-as-code acts as the source of truth for your entities, so you should manage all changes from your code definitions.” Change the definitions, and the OSDK regenerates.

A MODEL.md Page and a Test Lock In basketball-tools’s Key Set

basketball-tools’s MODEL.md lists a field-shape contract; this piece treats it as a slice of the semantic side, not the semantic side itself. Its acceptance rule is stated at the end of the file: “驗收判準:正文裡每一個欄位級事實,都要指得出來源欄位。指不出來就是模型知識 → 砍。” (MODEL.md, line 201) — the acceptance rule is that every field-level fact in an article must trace back to a source field; a fact it cannot trace back to is model knowledge and gets cut. Section 1’s table lists what fields each seasonal snapshot must and may carry; the NBA standings[] rows are explicitly marked as never carrying division (MODEL.md, line 28). That prohibition has a documented incident behind it: MODEL.md’s own account states that a writer once wrote in an article that a team finished first in the Pacific Division, even though the NBA snapshot carries only conference, not division — the fact came from model knowledge. The Lakers really did have the best record in that division, so the sentence happened to be correct, which is exactly what made it more dangerous (the incident itself is the repo’s own account, not independently verified for this piece).

The gatekeeper is tests/test_snapshot_schema.py. It stores Section 1’s table as a separate Python set — for example NBA_ROW_FORBIDDEN = {"division"} (line 38) — and enforces three invariants: required fields must not disappear, an unregistered field must fail, and NBA rows may never carry division (test_no_division_field_in_nba, line 156). The test file also carries positive controls: it injects a fake division field and an unregistered top-level field into a copy of the data and asserts that the checker actually catches them (test_gate_itself_catches_injected_field, line 167); a second positive control removes a required field and confirms the checker catches that too (test_gate_itself_catches_missing_required, line 182); and it guards against an empty set — finding no snapshot files at all is itself a failure, not a silent pass (line 138). This test runs on every PR, every push, and before every daily fetch (.github/workflows/tests.yml, line 28; basketball-daily.yml, lines 43-44, the Unit tests step).

Against MODEL.md itself, the test file does exactly one thing: it confirms the file exists and contains three anchor strings — for anchor in ("資料契約", "division", "final_four"): (line 198). It does not parse Section 1’s table, and it does not compare that table against the Python set. The contract therefore lives in two copies — the table in MODEL.md, and the set in the test file — and CI catches drift between the snapshot and the test set, but not drift between the MODEL.md table and the test set; the two copies stay in sync only because a human keeps them in sync in the same pull request. This is the inverse of Palantir’s SuperRepo path: TypeScript definitions compile and materialize into real platform entities, and the OSDK regenerates automatically whenever they change — MODEL.md generates nothing at all.

The test itself also checks key sets only — a set-difference operation across “required,” “optional,” and “forbidden” keys — not value types, not object identity or a primary key, not link types, not a mapping from datasource to property. That resembles a very small slice of what a Palantir property schema defines. It is not equivalent to a full object type, and it is not equivalent to the semantic side as a whole.

Palantir Records Lineage Automatically; health-tools’s Receipts Only Point Back to Registered Rows

Palantir’s documentation describes lineage as something captured automatically: when a decision was made, against which version of enterprise data, and through which application, is automatically recorded and made securely accessible to both human developers and agents (the why-ontology page). Data Lineage is a separate tool that lets someone see how data flows through the whole Foundry platform. Palantir’s fiscal year 2024 10-K, filed with the SEC in 2025, turns this into a statement to shareholders: “Our platforms automatically maintain complete records of both data provenance and all transformations applied to data in the system, allowing users to assess the reliability of the data and facilitate the review and correction of inaccuracies when necessary.”

health-tools does not implement a narrower version of that complete provenance promise — it does something with a different scope and a different direction: a citation receipt for already-registered criteria rows. MODEL.md states the rule: “頁面上的每個數字都必須回指到某一列,該列的 quote 必須 grep 得回該來源的快照” (MODEL.md, line 19) — every number on the page must point back to a row, and that row’s quote must be grep-matched back to the source snapshot. A “normal” or “elevated” reading is positioned as the result of filtering a detail row, never a field stored on its own (MODEL.md, line 21); sources are sorted into three licensing buckets, and for the most restrictive one, snapshots stay local, are excluded from version control, and only a sha256 hash is checked in (MODEL.md, line 42).

The gatekeeper, scripts/check-receipts.py, does four things for every row in data/criteria/*.json: the doc_id must exist in the manifest, the quote must be non-empty, the quote must grep-match when the snapshot exists on disk, and it prints SKIP when the snapshot does not exist (scripts/check-receipts.py, lines 5-16). The comparison rule only strips whitespace — full-width versus half-width characters, uppercase versus lowercase, and versus are deliberately left untouched, because those differences are exactly the fingerprints that let someone trace a claim back to the original text (line 69). A mismatch is a FAIL; it is not permitted to add a row to an exceptions list, and it is not permitted to edit the quote to match the snapshot instead (lines 18, 23). What happens when the snapshot is not on disk is stated directly: “快照檔不存在時印 SKIP,不是 PASS。SKIP 代表「還沒驗過」,退出碼仍為 0” (lines 15-16) — SKIP is printed, not PASS, and the exit code stays 0. It does not block CI.

A handwritten table in the body markdown fails the build outright, because the criteria table is generated from data/ and a second, hand-written copy would create a second source of truth (“正文含手寫表格。指標頁的判準表由 data/criteria/ 生成,md 不得再寫一份(雙源=資料改了頁面不會叫)。”, scripts/gen-indicator.py, lines 342-343). The generator has to run twice and produce identical SHA-256 hashes both times; that check is a shell step in CI, not an independent program in the repo (.github/workflows/tests.yml, lines 31, 41). One example of a registered quote, from the hba1c criteria row: “A1C ≥6.5% (≥48 mmol/mol).” (data/criteria/hba1c.json, line 14)

What this produces is build-time, value-to-source lineage — not Palantir’s run-time decision lineage, which runs decision to data version to application. The receipt gate only proves that an already-registered criteria row’s citation can be checked; it does not scan backward through the built pages to confirm every number on them has a supporting row, and a row missing its snapshot is skipped, not blocked. The history file’s change events are gated by a status enum that mechanically prevents unverified rows from rendering — “未證實的列不得渲染” (data/criteria/history-schema.json, line 106) — but there is no structured field for who made that judgment call or when.

Palantir’s Action Layer Binds Five Capability Facets; racing-tools’s Approval List Only Guards a SHA256 Match

Palantir’s documentation defines an action as a single transaction (the action types overview page). What makes up an action type are five capability facets: submission criteria, parameter validation, Ontology edits or writeback, side effects — which are optional — and an action log, which exists only once configured. Once configured, an action log by default records the Action and Action type identifiers and version, a timestamp, the submitting user, and the primary keys of edited objects; the audit log is a separate mechanism, whose export dataset carries an optional retention policy capped at 730 days — the two should not be conflated as the same kind of log. Palantir’s documentation does not state that every action type must carry all five facets, but it binds them into one definition and one submission — part 2 of this series already walked through each of these five mechanisms in detail.

racing-tools’s action-layer work is a pre-publish gate. config/approved.json states its own purpose at the top of the file: “default-deny 發布核准清單。只有 article_sha256 完全命中的文章才會進輸出。” (config/approved.json, line 2) — a default-deny publish approval list; only an article whose article_sha256 matches exactly makes it into the build output. When the list is empty, the build removes every already-live article — that is the correct behavior for default-deny, not a bug. One approval record carries seven fields: slug, article_sha256, facts_sha256, check_report_sha256, approved_by, approved_at, note — but build-articles.py compares only article_sha256; the other two hashes are retained as evidence, not entry conditions.

When the sha256 does not match, build-articles.py prints a warning and skips that article: approval invalidated (article_sha256 mismatch) (scripts/build-articles.py, line 772), and the script itself does not return a build failure. But that is only the first layer: the PR and weekly-update pipeline run unit tests first, and ApprovedArticlesAreStillApproved in tests/test_site_facts.py turns the same mismatch into a failing test — the test file’s own comment puts it plainly: “所以把它變成機械擋線:sha 對不上就讓測試紅,PR 合不進去。” (tests/test_site_facts.py, line 176) — so a mismatch fails the test mechanically, the PR does not merge, and the weekly run does not proceed to deployment (.github/workflows/racing-weekly.yml, lines 95, 102).

The approved.json comment says the approver cannot be the same person as the author, but that rule lives only in the comment — the publish gate never reads or checks approved_by; build-articles.py reads only the article_sha256 field, start to finish (scripts/build-articles.py, lines 765-775). One script writes to the approval list, approve-season-intro.py, and its scope is limited to season-intro migrations (scripts/approve-season-intro.py, line 3); this round’s search across hits on approved.json found no other writer script. How other articles get approved — by hand-editing the JSON and merging a PR — is an inference from the note field and from that absence, not a directly observed fact.

Errata is a third, separate file, data/errata.json, with fields for slug, at, was, now, what, and credit, maintained by hand and read into the build to render the /errata/ page. The approval note field carries the narrative of one re-approval after a correction: an article’s sha256 changed from 7d23f22f… to 2c629835… (config/approved.json, line 21). Approval, publication, and errata sit in three separate places, connected only by slug and sha256 naming conventions, with no shared type definition binding them into one action: racing-tools has no Ontology object or link edits, no mechanical check on submitter permission or identity, and no action log auto-generated on every pass or failure; approved.json and errata.json are both hand-maintained status and narrative files, not an action log.

Palantir’s permission layer has its own machinery. Object security policies are set independently of the backing data source: “Object security policies allow you to configure view permissions on an object instance by configuring security policies on the object type, independently of the permissions on the backing data source.” Property-level permissions run on the same machinery, restricted to a subset of properties; Roles are the central permissioning model for the Ontology. A third-party client acting on behalf of an end user through Ontology MCP uses the OAuth authorization code grant: the user explicitly consents to the requested scopes, and the access token is scoped to that user’s permissions in Foundry; the same page lists a separate client credentials grant for non-interactive, service-to-service clients, so this is not a single statement covering every third-party client or the AIP agent permission model. Palantir’s own words: “Each user explicitly consents to the requested scopes, and the resulting access token is scoped to that user’s permissions in Foundry.” A January 22, 2026 Palantir blog post adds that an agent’s actual permissions are a function of three things together — the user who configured and owns it, the agent’s own service account and OSDK scope, and whoever it is executing on behalf of at a given moment; an April 28, 2026 post adds that all agent activity is governed by the same security policies that govern human use.

All three repos did the same thing: search a bounded scope for permission, role, acl, and auth. basketball-tools searched .py, .json, .md, and .yml files (excluding .git, node_modules, and worktrees), and the only hits were the workflow permissions: key, the Permissions-Policy header, and substrings such as author or authoredrole and acl had zero hits. health-tools searched the same scope, with hits limited to the workflow permissions: key, the word role in a document heading, HTML’s role="img", and author. racing-tools searched inside scripts/, tests/, and .github/, and found nothing beyond two instances of HTML’s role="img". All three are bounded-scope negative results — they do not mean the whole repo has nothing, and they do not mean self-built permissions are impossible.

That the three repos did not build access control does not mean Palantir is the only place that facet exists. The open-source authorization engine Cerbos puts conditional rules into a resource policy: “A resource policy contains all rules governing access to a single resource kind. Rules are evaluated per action and can reference roles, derived roles, and conditions.” Auditing is built in too — audit logs capture access records and the engine’s decisions along with their context. The difference is not capability; it is that these three repos did not wire in an external engine like this, while Palantir’s permission policies are bound directly into the ontology’s own definition.

A Side-by-Side Table of the Four Lenses

The table below lines up this piece’s own four lenses against Palantir’s official mechanism, and the slice each repo actually implements.

LensPalantir’s mechanism (official term)What the docs say (paraphrased)The repo’s slice (file + line)What’s missing or not the same thing
Semantic definitionobject type / property / link type / primary key / backing datasourceFive building blocks each carry an official schema definition; Palantir compares an object type to a dataset and an object to a dataset row — this piece’s repo snapshot rows are not Palantir Ontology objectsbasketball-tools: MODEL.md’s field-contract table plus the test file’s separately stored Python key set (MODEL.md:28; tests/test_snapshot_schema.py:30-40)Checks key sets only — no value types, no object identity or primary key, no link type, no datasource-to-property mapping; MODEL.md itself generates nothing, and the two copies are synced by hand in the same PR (tests/test_snapshot_schema.py:193-199)
Actionaction type (submission criteria / validation / side effect / writeback / action log)Submitting is a single transaction; this piece examines five capability facets, of which side effects are optional and the action log exists only once configured — no claim that every action type carries all fiveracing-tools: config/approved.json’s pre-publish gate — an article only reaches the build once article_sha256 matches (scripts/build-articles.py:769-775)No Ontology edits, no mechanical check on the submitter’s permission, no action log auto-generated on every submission; approved.json and errata.json are both hand-maintained files (config/approved.json:2)
Permissionobject / property security policy, Roles, Ontology MCP token scopeObject-level view permission can be configured independently of the backing data sourceAll three repos searched a bounded scope and found nothingAn open-source authorization engine (such as Cerbos) can put rules and audit logging into a policy file — that approach exists; the difference is an external engine, not something bound into the ontology’s own definition
Traceability (lineage)Data Lineage, decision lineage auto-capture (audit logs are a separate mechanism)The time, data version, and application behind a decision are captured automatically and made securely accessiblehealth-tools: scripts/check-receipts.py checks doc_id and quote for each registered row in data/criteria/*.json; the quote must match when the snapshot is on disk, and is skipped when it is not (scripts/check-receipts.py:5-16)Not the run-time lineage of decision to data version to application — only build-time, value-to-source lineage; who judged a history row’s status, and when, has no structured field (data/criteria/history-schema.json:106)

What dbt, Cube, LookML, and Three Open-Source Projects Each Define

The words “ontology” and “semantic layer” carry different meanings across several companies’ pages. dbt’s Semantic Layer documentation describes it as a way to “define metrics on top of existing models” — only describing the definition and querying of metrics; this one page, as snapshotted here, does not describe write-back, and that is not a claim about the whole dbt product. Cube’s documentation states: “In Cube, cubes are used to organize tables and connections between tables.” — organizing tables and the connections between them, again with no description of write-back on this page. Looker’s documentation states that LookML is used to “describe dimensions, aggregates, calculations, and data relationships”; Databricks metric views separate measure definitions from grouping and filter fields, in Databricks’s own phrase, “separating measure definitions from the fields”. dbt also has model contracts, which govern the shape of every column in a model, not who can see which column.

A vendor comparison page, Timbr, self-reports against Palantir: “Timbr does not provide a native equivalent to Palantir Actions.” (Timbr’s vendor comparison page, self-dated as verified July 2026)

Building an ontology yourself is not a new idea. A GitHub project, operational-ontology, creates one action instance per call and records it in the audit log, whether the call was applied or refused: “Every call, applied or refused, creates one action instance” (github.com/gura105/operational-ontology, a personal GitHub project). foundry-ontology-open, another personal project, “mirrors Foundry’s Object Types, Link Types, Action Types, and Functions”; openfoundry describes itself as “a fully functional, local emulator of the Palantir Foundry platform.” These are all personal projects; they demonstrate that prior art exists, not that it is mature or widely adopted.

Palantir’s own 10-K, in its Competition section, states that its potential customers’ internal software development is a primary competitor, that organizations frequently try to build their own data platform before buying one, and that in doing so they generally rely on a patchwork of custom solutions, outside consultants, IT services companies, and packaged and open-source software — this is a risk disclosure written for shareholders, not marketing language.

Three Repos’ Gatekeeping Shapes, Side by Side

Contract fileGatekeeper programWhere it runs (CI step)WritersHow it tracesRule the program does not enforce and that remains manual
basketball-toolsMODEL.md §1tests/test_snapshot_schema.py.github/workflows/tests.yml (line 28) plus basketball-daily.yml (lines 43-44)3 fetch scripts; NBA and Taiwan basketball go through snapshot_guard.guarded_write, the HBL fetch script also writes directly with write_textconfig/season-facts.json’s free-text verified field, filled in by handThe acceptance rule — cut anything that cannot point to a source field — is a human review rule; the program does not check it
health-toolsMODEL.md §1scripts/check-receipts.py.github/workflows/tests.yml (lines 31, 41, 50)scripts/fetch-health-source.py writes manifest.json; no script writing data/criteria/*.json was found in the repoEach manifest entry carries a sha256 and fetched_at; history rows carry a status enum, and unverified rows do not renderWho judged a history row’s status, and when, has no structured field — only a human record
racing-toolsconfig/approved.jsonscripts/build-articles.py’s sha256 comparison plus tests/test_site_facts.py’s ApprovedArticlesAreStillApproved.github/workflows/racing-weekly.yml (lines 95, 102)approve-season-intro.py, scoped only to season intros; other articles are inferred, from the note field, to be approved by hand-editing the JSON and merging a PRdata/errata.json as a third file, plus the free-text narrative in approved.json’s note fieldThe rule that the approver cannot be the author is only a comment; build-articles.py never reads or checks approved_by

When a Lightweight Gatekeeping Mechanism Is Worth Building

What the three repos did was take a handful of the building blocks in Palantir’s documentation and rebuild each of them, in one small, verifiable slice, using a document plus a program or a list. Whether it is worth doing depends on a few conditions: do multiple processes share the same entities, does anything need a write-back action, is permission or compliance a hard requirement, does something need to answer where a number came from, and is the data spread across multiple systems. The more of these hold true, the more value there is in binding these slices into one runtime-enforced system. A read-only, single-writer static site, where the schema answers to no one but itself, only needs what this piece covers: a field-shape contract, a citation receipt, and a pre-publish gate.

Back to the forum question this piece opened with. Whether an ontology can be defined in code now has an answer on both sides: Palantir has SuperRepo and Ontology-as-code; self-building runs on MODEL.md, criteria JSON, and an approval list. The difference is not whether a text file exists after the definition — it is who enforces that definition, and when, once it has been written. The open-source and self-built prior art found here is enough to prove that each mechanism can be implemented separately; it does not prove that integrating these mechanisms, maintaining them over time, and enforcing them consistently is not a technical problem. The verifiable difference between these three repos and Palantir is that the former spread the mechanism across documents, tests, JSON, and CI, while the latter has one platform carrying Ontology entities, Actions, permission policies, and lineage capability together; whether they all belong to a single definition is not a claim this piece makes.

Next in this series: what dbt, Cube, LookML, knowledge graphs and Palantir each mean by “ontology” or “semantic layer”.

Frequently Asked Questions

Q: Does a one-page MODEL.md count as Palantir’s object type definition? This is an analogy, not an equivalence. In the SuperRepo path, Ontology-as-code, TypeScript object type definitions compile and materialize into platform entities after deployment, and the OSDK regenerates whenever those definitions change. MODEL.md generates nothing; the test file keeps a separate Python key set, checks MODEL.md only for file existence and three anchor strings, and the two copies are synced by hand in the same PR.

Q: Without Palantir, can write-back validation and audit be self-built? racing-tools’s approval list implements default-deny: only an article whose article_sha256 matches exactly makes it into the build. But the rule that the approver cannot be the author lives only in a comment; the publish gate never reads or checks approved_by. Errata is a separate file, maintained by hand.

Q: How does self-built lineage differ from Palantir’s decision lineage? health-tools’s receipt gate checks value to source: for a registered criteria row whose snapshot is on disk, the quote must be grepped back to it; a row with no snapshot on disk is skipped and does not block CI. Palantir’s decision lineage is decision to data version to application — when a decision was made, against which version of data, through which application. The two are not the same layer.

Q: When is a lightweight gatekeeping mechanism worth building? The more of these hold true — multiple processes sharing the same entities, actions that need write-back, permissions or compliance as a hard requirement, needing to answer where a number came from, data spread across multiple systems — the more value there is in binding the mechanisms into one runtime-enforced system. A read-only, single-writer static site only needs the three slices this piece covers: a field-shape contract, a citation receipt, and a pre-publish gate.