From 473bcd6e78fa9b4c0c76b9ed44efcd2deefd2174 Mon Sep 17 00:00:00 2001 From: Kames Date: Tue, 30 Jun 2026 11:39:19 -0700 Subject: [PATCH 1/7] lockfile --- bun.lock | 90 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/bun.lock b/bun.lock index bf3fe2e..982c917 100644 --- a/bun.lock +++ b/bun.lock @@ -33,27 +33,27 @@ "name": "@0xintuition/classifications", "version": "0.1.0-alpha.0", "devDependencies": { - "@0xintuition/predicates": "workspace:*", - "@0xintuition/schema-org": "workspace:*", - "typescript": "catalog:", - "vitest": "catalog:", + "@0xintuition/predicates": "0.1.0-alpha.0", + "@0xintuition/schema-org": "0.1.0-alpha.0", + "typescript": "^5.9.3", + "vitest": "^4.0.16", }, }, "packages/curves": { "name": "@0xintuition/curves", "version": "0.1.0-alpha.0", "devDependencies": { - "typescript": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "vitest": "^4.0.16", }, }, "packages/deployments": { "name": "@0xintuition/deployments", "version": "0.1.0-alpha.0", "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -63,9 +63,9 @@ "name": "@0xintuition/ids", "version": "0.1.0-alpha.0", "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -75,12 +75,12 @@ "name": "@0xintuition/periphery", "version": "0.1.0-alpha.0", "dependencies": { - "@0xintuition/deployments": "workspace:*", + "@0xintuition/deployments": "0.1.0-alpha.0", }, "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -90,12 +90,12 @@ "name": "@0xintuition/predicates", "version": "0.1.0-alpha.0", "dependencies": { - "@0xintuition/ids": "workspace:*", + "@0xintuition/ids": "0.1.0-alpha.0", }, "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -105,14 +105,14 @@ "name": "@0xintuition/primitives", "version": "0.1.0-alpha.0", "dependencies": { - "@0xintuition/classifications": "workspace:*", - "@0xintuition/ids": "workspace:*", - "@0xintuition/predicates": "workspace:*", + "@0xintuition/classifications": "0.1.0-alpha.0", + "@0xintuition/ids": "0.1.0-alpha.0", + "@0xintuition/predicates": "0.1.0-alpha.0", }, "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -122,12 +122,12 @@ "name": "@0xintuition/protocol", "version": "3.0.0", "dependencies": { - "@0xintuition/curves": "workspace:*", + "@0xintuition/curves": "0.1.0-alpha.0", }, "devDependencies": { - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", }, "peerDependencies": { "viem": "^2.0.0", @@ -137,21 +137,21 @@ "name": "@0xintuition/react", "version": "0.1.0-alpha.0", "dependencies": { - "@0xintuition/deployments": "workspace:*", - "@0xintuition/ids": "workspace:*", - "@0xintuition/protocol": "workspace:*", + "@0xintuition/deployments": "0.1.0-alpha.0", + "@0xintuition/ids": "0.1.0-alpha.0", + "@0xintuition/protocol": "3.0.0", }, "devDependencies": { - "@tanstack/react-query": "catalog:", - "@testing-library/react": "catalog:", - "@types/react": "catalog:", - "jsdom": "catalog:", - "react": "catalog:", - "react-dom": "catalog:", - "typescript": "catalog:", - "viem": "catalog:", - "vitest": "catalog:", - "wagmi": "catalog:", + "@tanstack/react-query": "^5.90.2", + "@testing-library/react": "^16.3.0", + "@types/react": "19.1.10", + "jsdom": "^26.1.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "typescript": "^5.9.3", + "viem": "^2.31.4", + "vitest": "^4.0.16", + "wagmi": "^2.15.4", }, "peerDependencies": { "@tanstack/react-query": ">=5", @@ -164,8 +164,8 @@ "name": "@0xintuition/schema-org", "version": "0.1.0-alpha.0", "devDependencies": { - "typescript": "catalog:", - "vitest": "catalog:", + "typescript": "^5.9.3", + "vitest": "^4.0.16", }, }, }, From 2787b05b46f647e8051b6eb9ffc763957e415737 Mon Sep 17 00:00:00 2001 From: Kames Date: Tue, 30 Jun 2026 13:40:12 -0700 Subject: [PATCH 2/7] feat: predicate semantics --- .planning/predicate-fields/README.md | 72 ++++ .planning/predicate-fields/claim-type.md | 67 ++++ .planning/predicate-fields/contradicts.md | 71 ++++ .../predicate-fields/deferred-and-cut.md | 116 ++++++ .planning/predicate-fields/inverse.md | 67 ++++ .planning/predicate-fields/is-asymmetric.md | 51 +++ .planning/predicate-fields/is-symmetric.md | 55 +++ .planning/predicate-fields/is-transitive.md | 67 ++++ .planning/predicate-fields/market-pattern.md | 64 ++++ .planning/predicate-fields/object-kind.md | 70 ++++ .planning/predicate-fields/polarity.md | 66 ++++ .planning/predicate-fields/specializes.md | 61 ++++ .planning/predicate-fields/temporal-nature.md | 67 ++++ .planning/predicate-semantics-foundations.md | 344 ++++++++++++++++++ .planning/predicate-semantics.md | 280 ++++++++++++++ .planning/predicate-spec-decisions.md | 234 ++++++++++++ 16 files changed, 1752 insertions(+) create mode 100644 .planning/predicate-fields/README.md create mode 100644 .planning/predicate-fields/claim-type.md create mode 100644 .planning/predicate-fields/contradicts.md create mode 100644 .planning/predicate-fields/deferred-and-cut.md create mode 100644 .planning/predicate-fields/inverse.md create mode 100644 .planning/predicate-fields/is-asymmetric.md create mode 100644 .planning/predicate-fields/is-symmetric.md create mode 100644 .planning/predicate-fields/is-transitive.md create mode 100644 .planning/predicate-fields/market-pattern.md create mode 100644 .planning/predicate-fields/object-kind.md create mode 100644 .planning/predicate-fields/polarity.md create mode 100644 .planning/predicate-fields/specializes.md create mode 100644 .planning/predicate-fields/temporal-nature.md create mode 100644 .planning/predicate-semantics-foundations.md create mode 100644 .planning/predicate-semantics.md create mode 100644 .planning/predicate-spec-decisions.md diff --git a/.planning/predicate-fields/README.md b/.planning/predicate-fields/README.md new file mode 100644 index 0000000..8a2b884 --- /dev/null +++ b/.planning/predicate-fields/README.md @@ -0,0 +1,72 @@ +# Predicate Field Justifications + +One document per candidate field. Each makes the case for inclusion with multiple concrete +examples spanning the value axes, then assigns a **confidence score** under a fixed rubric. The +scoring is allowed to *overturn* earlier "ship" decisions — if a field can't clear the bar here, it +gets demoted. That's the point. + +## Scoring rubric (0–100) + +A field's score is a holistic judgment across four weighted dimensions: + +| Dimension | Weight | Asks | +|---|---|---| +| **Leverage** | 40% | How much does it unlock — querying, machine interpretation, frontend auto-generation, economics? Does it enable something otherwise impossible or expensive? | +| **Consumer readiness** | 25% | Does a consumer read it *today* (frontend, indexer), or only a hypothetical future engine? | +| **Simplicity / cost** | 20% | Authoring burden across 133 specs, reasoning cost at graph scale, maintenance. Cheaper is better. | +| **Non-redundancy** | 15% | Is it derivable from other fields, duplicated elsewhere, or merge-able? | + +### Bands + +| Score | Band | Action | +|---|---|---| +| 90–100 | Core | Ship now, foundational | +| 75–89 | Ship | Ship now | +| 60–74 | Soft-ship | Define now, populate opportunistically; don't block on it | +| 40–59 | Defer | Keep documented; add when a consumer appears | +| <40 | Cut | Do not add | + +## The value axes (what "leverage" is measured against) + +Every example in these docs is tagged with which axis it serves: + +- **[QUERY]** — better querying / indexer optimization / edge synthesis +- **[MACHINE]** — machine-readable interpretation, inference, consistency +- **[RENDER]** — frontend automatically generating components from the spec +- **[ECON]** — market design, reputation, staking semantics +- **[DATA]** — data quality, freshness, lifecycle + +## Summary + +| Field | Doc | Score | Band | +|---|---|---|---| +| `objectKind` | [object-kind.md](object-kind.md) | **92** | Core | +| `inverse` | [inverse.md](inverse.md) | **88** | Ship | +| `polarity` | [polarity.md](polarity.md) | **88** | Ship | +| `isSymmetric` | [is-symmetric.md](is-symmetric.md) | **85** | Ship | +| `temporalNature` | [temporal-nature.md](temporal-nature.md) | **85** | Ship | +| `marketPattern` | [market-pattern.md](market-pattern.md) | **84** | Ship (grandfathered) | +| `contradicts` | [contradicts.md](contradicts.md) | **82** | Ship | +| `isTransitive` | [is-transitive.md](is-transitive.md) | **80** | Ship | +| `specializes` | [specializes.md](specializes.md) | **80** | Ship | +| `claimType` | [claim-type.md](claim-type.md) | **64** | Soft-ship | +| `isAsymmetric` | [is-asymmetric.md](is-asymmetric.md) | **54** | **Defer** (demoted) | +| `isInverseFunctional` | [deferred-and-cut.md](deferred-and-cut.md) | 50 | Defer | +| `isFunctional` | [deferred-and-cut.md](deferred-and-cut.md) | 44 | Defer | +| `isReflexive`/`isIrreflexive` | [deferred-and-cut.md](deferred-and-cut.md) | 34 | Cut | +| `equivalentTo` | [deferred-and-cut.md](deferred-and-cut.md) | 32 | Cut | +| `verifiability` | [deferred-and-cut.md](deferred-and-cut.md) | 30 | Cut | +| property chains | [deferred-and-cut.md](deferred-and-cut.md) | 26 | Cut | +| `isHierarchical` | [deferred-and-cut.md](deferred-and-cut.md) | 18 | Cut (redundant) | + +### What changed from the decision record + +- **`isAsymmetric` demoted to Defer (54).** Under scrutiny its only consumer is a validation engine + that doesn't exist; its "don't mirror" signal is already covered by the absence of `isSymmetric`. + The shipped algebraic set tightens to **`isSymmetric` + `isTransitive`** — both with a consumer today. +- **`claimType` lands at Soft-ship (64),** not full ship — define it, populate where obvious, but no + consumer branches on it yet. + +**Final ship list (9 new/active semantic fields):** `objectKind`, `inverse`, `polarity`, +`isSymmetric`, `temporalNature`, `contradicts`, `isTransitive`, `specializes` + grandfathered +`marketPattern`; plus `claimType` as soft-ship. diff --git a/.planning/predicate-fields/claim-type.md b/.planning/predicate-fields/claim-type.md new file mode 100644 index 0000000..dcf912c --- /dev/null +++ b/.planning/predicate-fields/claim-type.md @@ -0,0 +1,67 @@ +# `claimType` + +**Type:** `'factual' | 'evaluative'` (`normative` dropped) +**Confidence: 64 / 100 — Soft-ship. Define now, populate opportunistically, don't block on it.** + +> Verdict: real and orthogonal value (fact vs opinion changes market and UI semantics), but no consumer +> *branches* on it yet. Cheap enough to define and populate where obvious; not worth a curation campaign +> until a market or badge actually reads it. + +## The problem it solves + +A claim that can be checked against the world (`manufacturer`) is a different *kind* of thing from a +perpetual opinion (`betterThan`). `claimType` records that distinction: + +- `factual` — verifiable in principle: `manufacturer`, `authoredBy`, `parentOrganization`, + `linkedAccount`, `containedInPlace`, `founded`. +- `evaluative` — subjective judgment: `betterThan`, `like`, `bullishOn`, `trust`, `recommend`, + `worseThan`. + +(`normative` — "ought" claims like `compliantWith` — was cut: too few predicates, no consumer.) + +## Examples across the value axes + +1. **[ECON] Market resolution semantics.** A `factual` claim can in principle *resolve* (an oracle or + consensus settles "is Foxconn the manufacturer?") and pay out. An `evaluative` claim is perpetually + contestable — there's no ground truth to "Apple is better than Samsung," so its market stays open as + an opinion poll. The economic layer *would* branch on this — but doesn't yet. This is the strongest + justification and also the reason the field is soft-ship: the consumer is real but future. + +2. **[RENDER] "Verifiable" vs "Opinion" badge.** The UI can badge factual claims as checkable and + evaluative ones as opinions, setting user expectations about whether a claim can ever be "settled." + This consumer *could* ship today but is low-stakes. + +3. **[QUERY] Separate the knowledge graph from the opinion graph.** "Show only factual claims about this + entity" gives a sourcing/encyclopedia view; "show opinions" gives a sentiment view. Useful slice, + not yet requested. + +## Honest case against (why not full ship) + +- **No consumer branches on it today.** Unlike `objectKind` (frontend reads it now) or `polarity` + (reputation reads it now), nothing currently changes behavior based on `claimType`. +- **Partial correlation with other fields.** `evaluative` claims usually have a `polarity` and often a + `comparative` `marketPattern`; `factual` claims usually have `polarity` absent. It's not *derivable* + (`blocked` is factual yet negative-polarity), but the correlation means it carries less *independent* + information than the top-tier fields. + +## Resolution: soft-ship + +Add the field to the type. Populate it where the answer is obvious and free (during the other backfills). +Do **not** run a dedicated curation pass or block the rollout on it. Promote to full ship the moment a +market mechanic or a shipped badge reads it. + +## Cost / complexity + +Low to define; the temptation-to-overthink (where exactly is the fact/opinion line?) is the real cost, +which soft-ship avoids by not demanding completeness. + +## Pruning check + +- *Derivable?* No, but correlated — hence the docked non-redundancy score. +- *Duplicated?* Overlaps conceptually with `marketPattern: comparative` and with `polarity`; distinct but + related. +- *Mergeable?* Considered merging into `marketPattern`; rejected — "how it trades" ≠ "what kind of claim." + +## Confidence breakdown + +Leverage 28/40 · Consumer readiness 12/25 · Simplicity 17/20 · Non-redundancy 7/15 → **64**. diff --git a/.planning/predicate-fields/contradicts.md b/.planning/predicate-fields/contradicts.md new file mode 100644 index 0000000..5f2f004 --- /dev/null +++ b/.planning/predicate-fields/contradicts.md @@ -0,0 +1,71 @@ +# `contradicts` + +**Type:** `readonly PredicateKey[]` (declared symmetrically on both predicates) +**Confidence: 82 / 100 — Ship.** + +> Verdict: the most Intuition-*native* reasoning field. A belief/trust graph is valuable precisely because +> it can represent and price disagreement — and `contradicts` is the formal hook for it. Descends from +> OWL 2 `propertyDisjointWith` and WordNet antonymy. + +## The problem it solves + +Some predicates cannot both hold for the same subject/object pair without incoherence. `contradicts` +declares those disjoint pairs so the system can *detect* conflict instead of silently storing both: + +- `trust` ⊥ `distrust` +- `bullishOn` ⊥ `bearishOn` +- `support` ⊥ `oppose` +- `agreeWith` ⊥ `disagreeWith` +- `endorse` ⊥ `reported` (softer; see "strength" below) + +## Examples across the value axes + +1. **[ECON] Disagreement markets — the differentiator.** Alice stakes `trust Bob`; Carol stakes + `distrust Bob`. Because the predicates are declared `contradicts`, the system recognizes a *priced + disagreement* and can route both into a single conflict market rather than two unrelated claims. No + classical fact-graph (DBpedia, Wikidata, Google KG) was built to model this; it is the cutting-edge + capability the whole effort is aiming at. + +2. **[RENDER] Conflict surfacing in the UI.** An entity page can render a "Contested" badge and show the + opposing camps side by side when contradictory predicates both have stake. The frontend finds the + conflict by reading `contradicts`, not by hardcoding which predicate pairs oppose. + +3. **[MACHINE] Consistency checking.** Under the Open World Assumption you can *never* infer `¬trust` from + the absence of a trust edge (foundations doc, Part IV) — so contradiction must come from an explicit + disjointness axiom. `contradicts` *is* that axiom. It's the only correct way to detect conflict in an + open-world graph. + +4. **[QUERY] Find the controversies.** "Show the most contested entities this week" = rank entities by + stake-weighted presence of `contradicts` pairs. A first-class signal for discovery/feeds. + +## Hard vs soft contradiction (a real design nuance) + +`trust`/`distrust` is a *hard* logical contradiction. `skepticalOf`/`trust` is *tension*, not strict +contradiction — one can be skeptical yet still net-trusting. The decision record flags whether we need a +separate `tension` relation. Current lean: ship `contradicts` for hard pairs now; model soft tension later +if a consumer needs it. Don't overload one field with two strengths. + +## What breaks without it + +The graph can hold `trust Bob` and `distrust Bob` with no idea they conflict. The disagreement-market +thesis — arguably Intuition's core value proposition — has no data substrate. Conflict detection devolves +to per-pair hardcoding in each consumer. + +## Cost / complexity + +Moderate — it needs **human curation** (which pairs truly oppose) and must be declared symmetrically, which +the derivation layer enforces (`A.contradicts ∋ B ⟺ B.contradicts ∋ A`). Forward-chainable / cheap to +check. The curation set is small (sentiment pairs). + +## Pruning check + +- *Derivable?* Partially correlated with `polarity` (opposite polarity is a *hint*) but not derivable — + `blocked` (negative) doesn't contradict `manufacturer` (absent); only specific semantic pairs oppose. +- *Duplicated?* No. +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 36/40 · Consumer readiness 18/25 · Simplicity 15/20 · Non-redundancy 13/15 → **82**. +(Deductions: curation cost, and the consuming market layer is emerging rather than shipped. The +strategic upside keeps it firmly in "ship.") diff --git a/.planning/predicate-fields/deferred-and-cut.md b/.planning/predicate-fields/deferred-and-cut.md new file mode 100644 index 0000000..503244a --- /dev/null +++ b/.planning/predicate-fields/deferred-and-cut.md @@ -0,0 +1,116 @@ +# Deferred & Cut Fields + +These candidates did not clear the bar. Documented here so the reasoning is preserved and so each can be +promoted later with a clear trigger. Each carries its confidence score and the specific reason it's held. + +--- + +## `isInverseFunctional` — 50 / 100 — DEFER (strategically flagged) + +**Asserts:** the object uniquely identifies the subject — `P(a,c) ∧ P(b,c) ⟹ a = b`. The object is a *key*. + +**Best case:** `linkedAccount` — whoever links a given social account is the same person. This is the +Semantic Web's entity-resolution mechanism (an IFP is a database key without the join), and for a +*permissionless* graph where the same real entity gets minted many times, dedup is strategically large. + +**Why deferred, not shipped:** +- **No consumer.** There is no dedup/identity-resolution engine reading it today. The value is entirely + prospective. +- **Dangerous without UNA.** Under no-Unique-Name semantics, a mis-asserted IFP *infers identity* and can + collapse distinct entities ("smushing"). Shipping it before there's a careful consumer is a footgun on a + permissionless network. + +**Promotion trigger:** when an identity-resolution / dedup feature is scoped. *Consider pulling it forward +to start collecting the data early* (decision-record open question #3) — but only the data, with no +auto-merge, until the consumer is built and safety-reviewed. + +--- + +## `isFunctional` — 44 / 100 — DEFER + +**Asserts:** each subject has at most one object — `P(a,b) ∧ P(a,c) ⟹ b = c`. + +**Best case:** `manufacturer`, `primaryImageOfPage`, `parentOrganization` (arguably). Enables "you asserted +two manufacturers; one is wrong" conflict detection. + +**Why deferred:** the conflict-detection consumer is the same nonexistent validation engine that demotes +`isAsymmetric`. Cardinality is also genuinely ambiguous for many predicates (does a product have exactly +one manufacturer? co-manufacturing exists), so the authoring burden comes with real edge-case debate for +limited present payoff. + +**Promotion trigger:** write-time validation engine, shipped together with the rest of the validation +fields. + +--- + +## `isReflexive` / `isIrreflexive` — 34 / 100 — CUT + +**Asserts:** `P(a,a)` always / never. + +**Why cut:** almost nothing creates self-loops, so the validation value is near-zero in practice; +`irreflexive` is largely *derivable* (`asymmetric ⟹ irreflexive`); and `reflexive` applies to a tiny set +(`sameAs`). High specification overhead for a corner case. If self-loop validation ever matters, it can be +a single special-cased rule rather than a field on all 133 specs. + +--- + +## `equivalentTo` (predicate-level alias) — 32 / 100 — CUT + +**Asserts:** two predicates are interchangeable (`createdBy ≡ authoredBy`). + +**Why cut:** we *deliberately* keep near-synonyms distinct (they carry different connotation and different +markets), so there are few true equivalences to assert. And under no-UNA, predicate aliasing risks +collapsing distinct claims and destabilizing atom-ids. The cost/risk outweighs a benefit we mostly don't +want. If genuine duplicates emerge, handle them case-by-case via deprecation (`status: 'deprecated'` + +`replacedBy`), not a blanket equivalence axiom. + +--- + +## `verifiability` (`onChain` / `offChainVerifiable` / `unverifiable`) — 30 / 100 — CUT + +**Asserts:** how a claim could be checked. + +**Why cut:** strongly correlated with `claimType` (factual things are the verifiable ones), so it's a +near-duplicate second axis. It adds three-way authoring overhead for information `claimType` already +approximates. Revisit only if an oracle/resolution system needs a *finer* verifiability signal than +factual-vs-evaluative — and even then, prefer extending `claimType` over a parallel field. + +--- + +## property chains (`P ∘ Q ⊑ R`) — 26 / 100 — CUT (for now) + +**Asserts:** composed paths imply a relation (`parentOf ∘ parentOf ⊑ grandparentOf`; +`locatedIn ∘ containedInPlace ⊑ locatedIn`). + +**Why cut:** the most expressive *and* most expensive axiom in OWL 2 (SROIQ admits it only under an +acyclicity restriction to stay decidable). No consumer, and it sits at the costly end of the +expressivity/tractability frontier we explicitly chose to stay inside (OWL 2 RL). Pure future research. + +**Promotion trigger:** a dedicated reasoning module (`@0xintuition/reasoning`) with a compute budget for +chain materialization. + +--- + +## `isHierarchical` — 18 / 100 — CUT (redundant) + +**Asserts (today):** the predicate forms a containment/subsumption tree. + +**Why cut:** fully **derivable** from `isTransitive + (isAsymmetric or a defined `inverse`)`. It encodes no +information the other fields don't already carry; it's a convenience label that invites drift (a spec could +declare `isHierarchical: true` while *not* being transitive, producing an incoherent record). Replace any +current use with the derived combination. If a tree-UI hint is genuinely wanted, compute it in +`definePredicateRecord` as a derived getter — never an authored field. + +--- + +## Summary of triggers + +| Field | Score | Promote when… | +|---|---|---| +| `isInverseFunctional` | 50 | dedup/identity-resolution feature is scoped (maybe collect data earlier) | +| `isFunctional` | 44 | write-time validation engine ships | +| `isReflexive`/`isIrreflexive` | 34 | (don't) — handle self-loops as a one-off rule if ever needed | +| `equivalentTo` | 32 | (don't) — use deprecation flow instead | +| `verifiability` | 30 | only if oracle resolution needs finer signal than `claimType` | +| property chains | 26 | a reasoning module with a compute budget exists | +| `isHierarchical` | 18 | never — derive it | diff --git a/.planning/predicate-fields/inverse.md b/.planning/predicate-fields/inverse.md new file mode 100644 index 0000000..ca2e9f7 --- /dev/null +++ b/.planning/predicate-fields/inverse.md @@ -0,0 +1,67 @@ +# `inverse` + +**Type:** `PredicateKey` (typed reference to another predicate) +**Confidence: 88 / 100 — Ship.** + +> Verdict: lets the indexer serve both directions of a relationship from a single stored edge, using the +> *correct* predicate for the reverse direction. Direct descendant of `owl:inverseOf`. A consumer (the +> indexer) reads it today. + +## The problem it solves + +Most directional relationships are interesting from both ends, but the reverse end is usually a +*different* predicate. `inverse` declares that pairing so the reverse edge never has to be minted +separately: + +- `parentOrganization` ⇄ `subOrganization` +- `follow` ⇄ `followedBy` +- `predecessorOf` ⇄ `successorOf` +- `containsPlace` ⇄ `containedInPlace` +- `reviewed` ⇄ `reviewedBy` + +## Examples across the value axes + +1. **[QUERY] One write, two views.** Kames asserts `BigCo parentOrganization Acme`. Acme's profile shows + "Parent: BigCo"; BigCo's profile lists "Subsidiaries: Acme, …" — both synthesized by the indexer from + one atom via `inverse`. We don't ask users to also mint `Acme subOrganization BigCo`, and we don't + store it. + +2. **[RENDER] The reverse section labels itself.** A profile page renders an "incoming edges" section by + looking up each predicate's `inverse` and using *its* display name. Without the field, the UI either + shows awkward passive phrasings ("is parent-organization-of by BigCo") or hardcodes reverse labels. + +3. **[MACHINE] Closes the relationship under inversion.** A reasoner that knows `inverse` can answer + "is Acme a subsidiary of BigCo?" from a `parentOrganization` assertion. This is one of the four + relational patterns (symmetry, antisymmetry, **inversion**, composition) that both OWL and the RotatE + embedding line independently identified as load-bearing — strong evidence it's not optional fluff. + +4. **[DATA] Consistency gate.** `definePredicateRecord` cross-checks that `parentOrganization.inverse === + 'subOrganization'` *and* `subOrganization.inverse === 'parentOrganization'`, and that the pair mirror + each other's algebraic properties (both transitive, etc.). A one-sided inverse fails the build. + +## Why typed key, not string + +Today the codebase has `inversePredicate: 'sub organization'` — a *display name*. Display names get +edited, translated, and drift; the link silently rots. A typed `PredicateKey` is validatable at build +time and lets the consistency gate above exist at all. This is the one genuine refactor in the field set. + +## What breaks without it + +Reverse-direction navigation either forces double-minting (more atoms, more user friction, divergent +data) or relies on hardcoded reverse-label maps in the frontend. Inversion-based inference is impossible. + +## Cost / complexity + +Low. Many predicates already imply their inverse; curating the pairs is a bounded one-time pass. The +string→key migration is mechanical. Reasoning cost is trivial (a lookup). + +## Pruning check + +- *Derivable?* No — the pairing is semantic, declared per predicate. +- *Duplicated?* Partially overlaps `isSymmetric` conceptually (a symmetric predicate is its own inverse), + which we handle by deriving `inverse = self` when `isSymmetric` is set, so authors never write both. +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 35/40 · Consumer readiness 22/25 · Simplicity 18/20 · Non-redundancy 13/15 → **88**. diff --git a/.planning/predicate-fields/is-asymmetric.md b/.planning/predicate-fields/is-asymmetric.md new file mode 100644 index 0000000..f14596c --- /dev/null +++ b/.planning/predicate-fields/is-asymmetric.md @@ -0,0 +1,51 @@ +# `isAsymmetric` + +**Type:** `boolean` (default false) +**Confidence: 54 / 100 — DEFER. Demoted from the decision record's "ship."** + +> Verdict: scrutiny didn't hold up the ship call. `isAsymmetric`'s only genuine consumer is a validation +> engine that doesn't exist, and its "don't mirror the edge" signal is already implied by the *absence* of +> `isSymmetric`. Keep it documented; add it the day write-time validation lands. + +## What it would assert + +`R(a,b) ⟹ ¬R(b,a)` — the relationship cannot hold in both directions: + +- `parentOrganization`, `betterThan`, `founder`, `predecessorOf`, `rankedAbove`. + +## The case that was made for it + +1. **[MACHINE] Validation.** Reject `A betterThan B` *and* `B betterThan A` as incoherent. +2. **[QUERY] "Don't synthesize the reverse as the same predicate."** +3. **[MACHINE] Implies irreflexivity** (`A betterThan A` is invalid). + +## Why it doesn't clear the bar + +1. **Its query/indexer value is already covered.** The indexer synthesizes a same-predicate reverse edge + *only* when `isSymmetric` is true. So "don't mirror" is the **default** — the absence of `isSymmetric` + already says it. `isAsymmetric` adds no new instruction to the one consumer that exists today (the + indexer). And where the reverse direction *is* meaningful, `inverse` already names it. + +2. **Its only unique value is write-time validation, which has no consumer.** Rejecting contradictory + asymmetric assertions requires a validation engine the system doesn't have. Per the project's own + pruning principle — *prefer fields with a consumer that exists today* — this is exactly the kind of + field to defer until that engine appears. + +3. **The implication it carries is derivable.** `asymmetric ⟹ irreflexive` is a logical entailment, not + independent information; a reasoner derives it. + +4. **Consistency with the rest of the prune.** We deferred the functional family and reflexivity for the + same reason (validation-only, no consumer). Shipping `isAsymmetric` while deferring those would be + incoherent — it sits in the same bucket. + +## When to promote + +Pull it in (likely alongside `isFunctional`/reflexivity) the moment write-time edge validation or a +consistency-checking pass becomes a real consumer. At that point all the "reject incoherent edges" fields +graduate together as a coherent validation layer. + +## Confidence breakdown + +Leverage 22/40 · Consumer readiness 8/25 · Simplicity 18/20 · Non-redundancy 6/15 → **54**. +(The deductions are consumer-readiness and non-redundancy: no live consumer, and the live signal is +already implied by `¬isSymmetric` + `inverse`.) diff --git a/.planning/predicate-fields/is-symmetric.md b/.planning/predicate-fields/is-symmetric.md new file mode 100644 index 0000000..a173f4c --- /dev/null +++ b/.planning/predicate-fields/is-symmetric.md @@ -0,0 +1,55 @@ +# `isSymmetric` + +**Type:** `boolean` (default false) +**Confidence: 85 / 100 — Ship.** + +> Verdict: store one edge, serve both directions, when the reverse is the *same* predicate. Cheap, +> indexer reads it today, one of the four canonical relational patterns. + +## The problem it solves + +Some relationships are mutual by definition — asserting one direction logically asserts the other, *with +the same predicate*. `isSymmetric` declares this so the reverse edge is synthesized, never minted twice: + +- `affiliatedWith` — if Alice is affiliated with Acme, Acme is affiliated with Alice. +- `partnerOf`, `competeWith`, `compatibleWith`, `alternativeTo`, `equivalentTo`, `sameAs`. + +(Contrast `inverse`, which handles the case where the reverse is a *different* predicate.) + +## Examples across the value axes + +1. **[QUERY] Half the writes, symmetric reads.** `Acme partnerOf BigCo` is stored once; the indexer + answers "who are BigCo's partners?" by matching the edge in either position. For a permissionless graph + paying for storage, not double-minting every mutual relationship is a real saving. + +2. **[RENDER] Both profiles populate from one assertion.** Acme's page and BigCo's page both show the + partnership without the user creating two atoms or the UI guessing direction. + +3. **[MACHINE] Canonical relational pattern.** Symmetry is one of the four patterns (symmetry, + antisymmetry, inversion, composition) that OWL formalized and the RotatE/ComplEx embedding line + independently rediscovered as essential. A graph that can't represent symmetry mis-models a large class + of relations. Cheap to assert, foundational to get right. + +4. **[DATA] Consistency.** The derivation layer sets `inverse = self` for symmetric predicates and rejects + a spec that declares both `isSymmetric` and a *different* `inverse` — a contradiction. + +## What breaks without it + +Either users mint two atoms per mutual relationship (friction + storage + drift between the two), or the +indexer can't safely answer reverse queries because it doesn't know the relation is mutual. + +## Cost / complexity + +Minimal — a boolean, obviously true/false per predicate, trivially forward-chainable (OWL 2 RL). + +## Pruning check + +- *Derivable?* No — mutuality is semantic. +- *Duplicated?* Interacts with `inverse` (symmetric ⟹ self-inverse) but is the cleaner way to express the + common "same predicate both ways" case; we derive the inverse rather than make authors write it. +- *Mergeable?* Could in theory be modeled as `inverse: `, but a dedicated boolean is far clearer to + authors and to the serializer (`owl:SymmetricProperty` is itself a dedicated type, not `inverseOf self`). + +## Confidence breakdown + +Leverage 33/40 · Consumer readiness 22/25 · Simplicity 20/20 · Non-redundancy 10/15 → **85**. diff --git a/.planning/predicate-fields/is-transitive.md b/.planning/predicate-fields/is-transitive.md new file mode 100644 index 0000000..60d24ad --- /dev/null +++ b/.planning/predicate-fields/is-transitive.md @@ -0,0 +1,67 @@ +# `isTransitive` + +**Type:** `boolean` (default false) +**Confidence: 80 / 100 — Ship.** + +> Verdict: enables reachability/closure queries (containment trees, org hierarchies, rankings) that are +> otherwise expensive or impossible to express. Indexer/query layer reads it. One of the four canonical +> relational patterns (composition). + +## The problem it solves + +For some predicates, a chain implies the endpoints relate directly: `R(a,b) ∧ R(b,c) ⟹ R(a,c)`. +`isTransitive` declares which, so the query layer can compute transitive closure: + +- `containedInPlace` / `containsPlace` — a neighborhood in a city in a country. +- `parentOrganization` / `subOrganization` — corporate hierarchies. +- `rankedAbove`, `betterThan`, `outperform` — ordering chains. +- `dependOn`, `partOfSeries`, `precededBy` / `successorOf`. + +## Examples across the value axes + +1. **[QUERY] "Everything within X" without enumerating the tree.** "All places in California" follows + `containedInPlace` transitively: county → city → district → venue. The indexer knows it may close the + relation because the predicate is marked transitive. Without the flag, this is either a hardcoded + special case per predicate or a query the system can't safely answer. + +2. **[QUERY] Org-chart rollups.** "Is Acme ultimately controlled by BigCo?" walks + `parentOrganization` through intermediate holding companies. Reputation/ownership analyses depend on + this closure. + +3. **[RENDER] Tree and breadcrumb UI.** A location breadcrumb ("Venue ▸ District ▸ City ▸ Country") or an + org tree component is generated by following a transitive predicate. The component asks "is this + predicate transitive?" to decide whether to recurse. + +4. **[MACHINE] Composition pattern.** Transitivity is the "composition" member of the four relational + patterns OWL and RotatE both treat as essential. Correctly modeling it is table stakes for a + reasoning-capable graph. + +## A caution carried from the research + +OWL DL forbids combining transitivity with cardinality restrictions on the same property (it breaks +decidability), and transitive closure is more expensive than the other RL-core inferences. We ship the +flag, but the *computation* of closure is an indexer/query-time decision (materialize vs. compute on +demand), and we should bound depth for ranking chains that could be long. This is a known, manageable cost. + +## What breaks without it + +Closure/reachability queries become per-predicate special cases or are simply unavailable; tree/breadcrumb +UI must hardcode which predicates nest. The graph can't answer "indirectly related via X" generically. + +## Cost / complexity + +Boolean, obvious to assign. The flag is cheap; the closure computation it enables is moderate cost and is +the indexer's call to make (materialized path index, bounded depth, etc.). + +## Pruning check + +- *Derivable?* No. +- *Duplicated?* No — distinct from `inverse`/`isSymmetric` (a predicate can be transitive and + asymmetric, e.g. `parentOrganization`). +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 33/40 · Consumer readiness 20/25 · Simplicity 16/20 · Non-redundancy 11/15 → **80**. +(Deduction: closure carries real compute cost, so the value is gated on the indexer choosing to +materialize it; the flag itself is unambiguous and cheap.) diff --git a/.planning/predicate-fields/market-pattern.md b/.planning/predicate-fields/market-pattern.md new file mode 100644 index 0000000..5dca857 --- /dev/null +++ b/.planning/predicate-fields/market-pattern.md @@ -0,0 +1,64 @@ +# `marketPattern` + +**Type:** `'depositional' | 'attributive' | 'comparative'` +**Confidence: 84 / 100 — Ship (grandfathered; needs a semantics clarification of its own).** + +> Verdict: the one economic field with no analog in 30 years of classical knowledge-graph research — it's +> Intuition-native and already in production. It stays. The caveat is that its three values deserve a +> dedicated semantics doc, because unlike the OWL-derived fields, there's no external literature to anchor +> their meaning. + +## The problem it solves + +Intuition predicates aren't just relations — they're *markets*. `marketPattern` classifies how a +predicate behaves economically (bonding-curve / staking dynamics), which no other field captures: + +- `depositional` — e.g. `affiliatedWith`, `alumniOf` (association-style claims). +- `attributive` — e.g. `actor`, `parentOrganization`, `containedInPlace` (attaching an attribute/role). +- `comparative` — e.g. `betterThan`, `equivalentTo`, `outperform`, `rankedAbove` (relative judgments). + +## Examples across the value axes + +1. **[ECON] Market mechanics per predicate.** The staking/curve behavior, signal interpretation, and + payout logic can differ by pattern. A `comparative` claim ("A better than B") is inherently relative and + zero-sum-ish; a `depositional` claim accrues differently. The economic layer reads this field to pick + mechanics. + +2. **[RENDER] Market UI variants.** A comparative predicate can render an A-vs-B versus widget; a + depositional one renders a simple stake-for/against control. The pattern drives which market component + shows. + +3. **[QUERY] Economic analytics.** "Total value staked across comparative markets" or "most active + attributive claims" segments the graph economically — only possible if the pattern is recorded. + +## Why the lower-than-leverage caveat + +Every other shipped field maps to a well-defined external concept (OWL characteristic, sentiment polarity, +fluent/event). `marketPattern`'s three values are *ours*, and their precise economic semantics aren't +written down anywhere in these docs. Before we lean harder on it for market mechanics, it warrants its own +clarification doc answering: what *exactly* distinguishes the three patterns, what mechanic does each +imply, and are three the right number? Until then it's correct to keep (it's in production and clearly +useful) but flagged as under-specified. + +## What breaks without it + +The economic layer loses its per-predicate behavior switch and falls back to one-size-fits-all market +mechanics — losing the expressiveness that makes different relationship types tradeable in fitting ways. + +## Cost / complexity + +Already paid (it exists on all specs). Ongoing cost is conceptual clarity, not authoring. + +## Pruning check + +- *Derivable?* No — economic behavior is an independent axis from logical/semantic structure. +- *Duplicated?* No external overlap; partially correlated with `claimType` (`comparative` ↔ `evaluative`) + but distinct (a comparative *market* is not the same statement as an evaluative *claim type*). +- *Mergeable?* Possibly worth examining against `claimType` in the clarification doc, but they answer + different questions (how it trades vs. what kind of claim it is). + +## Confidence breakdown + +Leverage 34/40 · Consumer readiness 23/25 · Simplicity 18/20 · Non-redundancy 9/15 → **84**. +(Deduction concentrated in non-redundancy/clarity: the values lack an external anchor and overlap +conceptually with `claimType`, pending the semantics doc.) diff --git a/.planning/predicate-fields/object-kind.md b/.planning/predicate-fields/object-kind.md new file mode 100644 index 0000000..a95b31e --- /dev/null +++ b/.planning/predicate-fields/object-kind.md @@ -0,0 +1,70 @@ +# `objectKind` + +**Type:** `'entity' | 'claim' | 'literal'` +**Confidence: 92 / 100 — Core. Ship first.** + +> Verdict in one line: this is the single field that turns the predicate spec into a frontend +> **rendering contract**, and it's the only place our reification model (claims-about-claims) is made +> machine-visible. Highest leverage, a consumer that exists today, near-zero cost, fully non-redundant. + +## The problem it solves + +Given an edge `⟨subject, predicate, object⟩`, software has to decide *what the object even is* before it +can render it, traverse it, or reason about it. Today that knowledge is implicit and re-derived ad hoc. +`objectKind` declares it once, on the predicate: + +- `literal` — the object is a value, not a node (`url`, `imgUrl`, `hasDescription`, `hasSource`). +- `claim` — the object is **another statement/atom**, not an entity (`disputedBy`, `confirmedBy`, + `attestedBy`, `citedBy`, `evidencedBy`, `verifiedBy`). This is Intuition's reification surface. +- `entity` — the object is a graph node (`memberOf`, `manufacturer`, `locatedIn`). The default majority. + +## Examples across the value axes + +1. **[RENDER] The app renders any edge with zero per-predicate hardcoding.** A generic `` + component switches on `objectKind`: `literal` → render `imgUrl` as an ``, `url` as an ``, + `hasDescription` as text; `entity` → render a clickable node chip; `claim` → render a nested claim + card. Without this field, the frontend needs a giant hand-maintained `switch(predicate)` covering all + 133 predicates — exactly the kind of coupling we're trying to delete. + +2. **[QUERY] The indexer knows what not to traverse.** Graph traversal, "related entities," and + PageRank-style walks must skip `literal` objects (an image URL is not a node) and must treat `claim` + objects as edges-on-edges. Marking `objectKind` lets the indexer build a clean entity-only subgraph + for traversal and a separate reification layer for claims. + +3. **[MACHINE] Reification becomes explicit.** `disputedBy`/`confirmedBy` point at *other claims*. Our + foundations doc traced 20 years of the field struggling to represent "statements about statements" + (RDF reification → singleton properties → RDF-star). `objectKind: 'claim'` is how we declare which + predicates are meta-relations, so a consumer can follow the citation/dispute graph instead of + mistaking a claim-id for an entity-id. + +4. **[ECON] Markets on claims vs markets on entities differ.** A stake on `Alice disputedBy [claim X]` + is a second-order market (betting on the credibility of another claim). The economic layer needs to + know it's pricing a meta-claim, which `objectKind: 'claim'` flags directly. + +5. **[DATA] Validation at authoring.** "You attached an entity where this predicate expects a literal" + is catchable the moment the field is known. `url`'s object should be a URL literal, not an atom. + +## What breaks without it + +Every consumer re-implements object-type detection by sniffing the atom data or hardcoding predicate +lists, and the claim-vs-entity distinction stays invisible — meaning the dispute/attestation graph can't +be walked generically. The frontend `switch` becomes the de-facto (untyped, drifting) source of truth. + +## Cost / complexity + +Trivial. A single 3-value enum, mechanically assignable across all 133 specs in an afternoon (the +literal set and claim set are small and obvious). Zero reasoning cost. + +## Pruning check + +- *Derivable?* No. Atom data may sometimes reveal a literal, but the **claim** case is not derivable from + anything we store — it's semantic intent of the predicate. +- *Duplicated?* No. Closest neighbor is classifications' object typing, but that answers *which entity + type*, not *entity vs claim vs literal* — a different, coarser axis that the rendering layer needs. +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 38/40 · Consumer readiness 24/25 · Simplicity 19/20 · Non-redundancy 11/15 → **92**. +(The only deduction: minor conceptual overlap with classifications typing, resolved by keeping this axis +deliberately coarse and rendering-focused.) diff --git a/.planning/predicate-fields/polarity.md b/.planning/predicate-fields/polarity.md new file mode 100644 index 0000000..8628e16 --- /dev/null +++ b/.planning/predicate-fields/polarity.md @@ -0,0 +1,66 @@ +# `polarity` + +**Type:** `'positive' | 'negative' | 'neutral'` (absent = no inherent sentiment) +**Confidence: 88 / 100 — Ship.** + +> Verdict: the field that lets a *trust/belief* graph aggregate signed signal and lets the frontend color +> sentiment, both from the predicate alone. Classical fact-graphs never needed it; Intuition cannot do +> without it. Grounded in signed-network theory (Heider; Leskovec et al.). + +## The problem it solves + +A large share of our predicates carry directional sentiment, and that sign is the raw material of +reputation. Encoding it once on the predicate means every consumer reads the same sign instead of each +re-hardcoding "trust is good, distrust is bad": + +- positive: `endorse`, `trust`, `vouchFor`, `support`, `bullishOn`, `like`, `recommend`, `backedBy` +- negative: `distrust`, `oppose`, `bearishOn`, `blocked`, `skepticalOf`, `disagreeWith`, `reported`, `votedAgainst` +- neutral: `neutralOn` (explicitly zero sentiment — distinct from *absent*) +- absent (no sentiment axis): `manufacturer`, `containedInPlace`, `authoredBy` + +## Examples across the value axes + +1. **[ECON] One reputation score across heterogeneous predicates.** Bob's standing = + Σ(stake on positive edges) − Σ(stake on negative edges), summed over `endorse`, `trust`, `vouchFor` + *and* `distrust`, `reported`, `blocked`. The scoring engine reads `polarity`; it does not carry its own + per-predicate sign table that drifts out of sync with the predicate set. + +2. **[RENDER] Sentiment-aware UI for free.** Edge cards render positive predicates green / negative red; + a profile splits incoming edges into "Supporters" and "Detractors"; a filter offers "show only + negative signals." All driven by the field, no per-predicate styling. + +3. **[QUERY] Signed-subgraph queries.** "Find entities with net-negative sentiment from accounts I trust" + requires the indexer to know each edge's sign. `polarity` makes that a first-class filter. + +4. **[MACHINE] Powers `contradicts` and balance reasoning.** Sign + the `contradicts` field together let + the system reason about structural balance ("entities my trusted peers distrust"). Signed-network + research (Leskovec, Huttenlocher & Kleinberg, 2010) shows these signs are predictable and meaningful — + we're encoding a quantity with 75 years of theory behind it (Heider 1946). + +## Why `neutral` is kept (and absent is different) + +`neutralOn` is a *deliberate* zero — "I have considered this and have no lean," which is information. +That's distinct from a relational predicate like `manufacturer` that has *no sentiment axis at all* +(absent). Collapsing them would lose the "explicitly neutral" signal that markets may price. (Open +question flagged in the decision record; current lean: keep the 3-value enum + absence.) + +## What breaks without it + +Every reputation/scoring/rendering surface maintains its own predicate→sign lookup. These drift, and new +predicates silently score as zero until each consumer is patched. Sentiment becomes tribal knowledge. + +## Cost / complexity + +Low. The sign of a predicate is obvious to assign; ~one afternoon across 133 specs. Trivial to consume. + +## Pruning check + +- *Derivable?* No — sentiment is semantic, not structural. +- *Duplicated?* Orthogonal to `claimType`: `trust` is positive *and* evaluative; `blocked` is negative + *and* factual; `manufacturer` is absent *and* factual. Two independent axes. +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 36/40 · Consumer readiness 22/25 · Simplicity 19/20 · Non-redundancy 11/15 → **88**. +(Deduction: the `neutral`-vs-absent subtlety is a small ongoing modeling cost.) diff --git a/.planning/predicate-fields/specializes.md b/.planning/predicate-fields/specializes.md new file mode 100644 index 0000000..bb22294 --- /dev/null +++ b/.planning/predicate-fields/specializes.md @@ -0,0 +1,61 @@ +# `specializes` + +**Type:** `PredicateKey` (this predicate is a sub-property of the referenced one) +**Confidence: 80 / 100 — Ship.** + +> Verdict: the cheapest high-leverage inference in the whole stack — roll-up. A specific edge entails the +> general one, so broad queries and reputation catch every specific sub-relation for free. Descends from +> RDFS `subPropertyOf` and KL-ONE inheritance (1985); echoed by WordNet hypernymy. + +## The problem it solves + +Specific relationships imply broader ones. `specializes` declares the hierarchy so consumers query/score +at the general level and automatically include the specifics: + +- `employedBy`, `alumniOf`, `memberOf`, `founder` → all `specializes` `affiliatedWith` +- `endorse`, `vouchFor` → `specializes` `support` (or a general positive-regard predicate) +- `subOrganization`, `branchOf` → `specializes` a general containment relation +- `authoredBy`, `createdBy`, `director`, `producer` → `specializes` `contributedTo` + +## Examples across the value axes + +1. **[QUERY] Roll-up without enumeration.** "Everyone affiliated with Acme" returns employees, alumni, and + members in one query, because each specific predicate `specializes affiliatedWith`. The query author + writes one predicate; the indexer expands the hierarchy. Without it, the query must hand-list every + sub-predicate and silently miss any added later. + +2. **[ECON] Reputation at the right granularity.** A "general affiliation" reputation signal aggregates all + sub-relations; a "contribution" score sums `authoredBy` + `createdBy` + `contributedTo`. The scoring + engine walks `specializes` instead of maintaining its own grouping table. + +3. **[RENDER] Faceted, grouped UI.** A profile groups edges under collapsible headings ("Affiliations → + employed by, alumnus of, member of") generated from the predicate hierarchy. New sub-predicates appear + under the right heading automatically. + +4. **[MACHINE] Forward-chainable entailment.** `P specializes Q` and `P(a,b)` ⟹ `Q(a,b)` is pure OWL 2 RL — + cheap, monotonic, computable at graph scale. It's the highest value-per-compute axiom we have. + +## What breaks without it + +Every "broad" query and every reputation grouping hardcodes the list of sub-predicates, and those lists +rot the moment a new predicate is added. The graph can't answer general questions without enumerating +specifics — defeating much of the point of a semantic layer. + +## Cost / complexity + +Moderate authoring — it needs **human curation** to define the hierarchy correctly (and to avoid asserting +false generalizations). Cheap to consume. The hierarchy is shallow (1–2 levels) for most predicates. + +## Pruning check + +- *Derivable?* No — the hierarchy is editorial/semantic. +- *Duplicated?* Distinct from `category` (which is a flat classification bucket for discovery, not an + entailment relation). `category` groups for *browsing*; `specializes` groups for *inference*. Worth + stating explicitly so the two don't get conflated. +- *Mergeable?* No — `category` can't carry entailment. + +## Confidence breakdown + +Leverage 35/40 · Consumer readiness 18/25 · Simplicity 15/20 · Non-redundancy 12/15 → **80**. +(Deductions: curation cost and a roll-up-query consumer that's emerging rather than fully built. The +inference is so cheap and so central that it stays a clear ship.) diff --git a/.planning/predicate-fields/temporal-nature.md b/.planning/predicate-fields/temporal-nature.md new file mode 100644 index 0000000..6f0b225 --- /dev/null +++ b/.planning/predicate-fields/temporal-nature.md @@ -0,0 +1,67 @@ +# `temporalNature` + +**Type:** `'permanent' | 'state' | 'event'` +**Confidence: 85 / 100 — Ship.** + +> Verdict: the only field that tells the system which edges *rot*. Drives freshness UX today and data +> lifecycle long-term. Grounded in 50+ years of AI knowledge representation (fluents vs events) and in +> OntoClean's rigidity meta-property. + +## The problem it solves + +Edges age very differently, and nothing in the current schema captures it. `temporalNature` classifies +the relationship's relationship-to-time: + +- `permanent` — true forever once true; never decays: `authoredBy`, `derivedFrom`, `founded`, + `precededBy`, `forkedFrom`, `byArtist`. +- `state` — a condition that holds over an interval and can silently become false: `employedBy`, + `memberOf`, `locatedIn`, `governedBy`, `availableOn`, `pinnedIn`. +- `event` — a point-in-time fact that stays true *as history*: `votedFor`, `reviewed`, `attestedBy`, + `triggered`. + +## Examples across the value axes + +1. **[RENDER] Freshness badges, automatically.** A `state` edge like `employedBy` renders an "as of {date}" + badge and a "still true?" affordance; a `permanent` edge like `authoredBy` renders neither. The UI + derives this from `temporalNature` instead of a hardcoded list of "decayable" predicates. + +2. **[DATA] Targeted re-attestation.** A data-quality job re-checks only `state` edges (those are the ones + that go stale) and never wastes work re-validating `permanent` ones. Without the field, you either + re-validate everything or nothing. + +3. **[QUERY] "Current" vs "historical" queries.** "Where does Alice work *now*?" must consider only the + most recent live `state` edge; "every company Alice has worked at" wants the full `event`-like history. + The query layer needs to know which model applies — `state` edges supersede, `event`/`permanent` edges + accumulate. + +4. **[ECON] Market resolution windows.** A market on a `state` claim (`Alice memberOf DAO`) is naturally + time-bounded and may need re-opening when the state could have changed; a market on a `permanent` + claim (`Alice founded DAO`) resolves once and stays resolved. Different economic lifecycles. + +5. **[MACHINE] Maps to a deep, validated distinction.** Situation/event calculus (McCarthy & Hayes 1969; + Kowalski & Sergot 1986) and OntoClean's rigidity (Guarino & Welty 2002) both encode exactly this + fluent/event/essential split. We're not inventing a category; we're naming a well-studied one. + +## What breaks without it + +The graph has no model of freshness. Stale `state` edges (`employedBy` from three jobs ago) look as +authoritative as permanent facts, and the UI can't distinguish "fact" from "expired condition." For a +graph meant to represent reality over time, that's a structural blind spot. + +## Cost / complexity + +Low. The three-way classification is usually obvious from the predicate's meaning. No reasoning cost — it's +a hint consumed by UI and data jobs, not an inference rule. + +## Pruning check + +- *Derivable?* No. +- *Duplicated?* No — orthogonal to every other field. (A `permanent` predicate can be positive, factual, + symmetric, etc.) +- *Mergeable?* No; the three values each drive distinct behavior. + +## Confidence breakdown + +Leverage 34/40 · Consumer readiness 21/25 · Simplicity 19/20 · Non-redundancy 11/15 → **85**. +(Deduction: full payoff for `state` edges depends on a validity-interval mechanism that doesn't exist yet; +the rendering/lifecycle value lands immediately regardless.) diff --git a/.planning/predicate-semantics-foundations.md b/.planning/predicate-semantics-foundations.md new file mode 100644 index 0000000..e35039c --- /dev/null +++ b/.planning/predicate-semantics-foundations.md @@ -0,0 +1,344 @@ +# Foundations: OWL/RDF Property Characteristics & 30 Years of Knowledge-Graph Research + +**Status:** Reference / background +**Companion to:** `predicate-semantics.md` (the concrete design) +**Date:** 2026-06-30 + +This document is the intellectual grounding for the predicate-semantics design. Part I is a deep +reference on the formal property characteristics from RDF/RDFS/OWL. Part II traces the lineage of +knowledge-graph research from ~1968 to today and extracts what each era teaches us. Part III maps +that body of work directly onto the Intuition predicate fields. Part IV records the tensions the +research warns us about — the mistakes a 30-year field has already made so we don't repeat them. + +--- + +# Part I — OWL / RDF property characteristics in depth + +## I.0 Why the predicate is the unit of meaning + +RDF models the world as **triples**: `⟨subject, predicate, object⟩`. The predicate (a.k.a. property) +is not decoration — it *is* the edge, and in RDF it is itself a first-class resource that can be +described, typed, and reasoned about. This is exactly Intuition's model: a predicate is an atom, and +the relationship's meaning is carried by that atom. So everything the Semantic Web learned about +"describing a property" applies to us directly. + +The stack has three layers of increasing commitment: + +| Layer | Year(s) | What it adds | Reasoning cost | +|---|---|---|---| +| **RDF** | 1999 / 2004 | triples, resources, literals | none (data model) | +| **RDFS** | 2004 | `subClassOf`, `subPropertyOf`, `domain`, `range` | light, tractable | +| **OWL / OWL 2** | 2004 / 2009 | property *characteristics*, disjointness, cardinality, chains | up to undecidable (OWL Full); decidable fragments (OWL DL, EL/QL/RL profiles) | + +OWL is grounded in **Description Logic (DL)** — OWL DL ≈ the DL `SHOIN(D)`; OWL 2 DL ≈ `SROIQ(D)` +(Horrocks, Kutz & Sattler, 2006). DL gives every axiom a precise model-theoretic semantics and a +known complexity, which is what makes "machine-readable" actually mean "machine-*reasonable*." + +## I.1 The property axioms, one by one + +For each: the formal rule, what it lets a reasoner *do*, and the Intuition field it maps to. + +### Object vs Datatype property → `objectKind` +OWL splits `owl:ObjectProperty` (object is a resource/node) from `owl:DatatypeProperty` (object is a +literal value). A reasoner must never traverse a datatype edge as if it were a node. This is precisely +our `objectKind: 'entity' | 'literal'` distinction (`url`, `imgUrl`, `hasDescription` are datatype-like). + +### `rdfs:subPropertyOf` → `specializes` +**Rule:** `P ⊑ Q` and `P(a,b)` ⟹ `Q(a,b)`. Every specific edge entails the general one. +**Does:** roll-up. A query over `affiliatedWith` automatically catches `employedBy`, `alumniOf`, +`memberOf`. Property hierarchies are the cheapest, highest-leverage inference in the whole stack and +they descend directly from KL-ONE's structured inheritance (1985). + +### `owl:inverseOf` → `ontology.inverse` +**Rule:** `P ≡ Q⁻` ⟹ `P(a,b) ↔ Q(b,a)`. +**Does:** store one direction, answer queries in both. `parentOrganization ⇄ subOrganization`, +`follow ⇄ followedBy`. Note OWL references the inverse *property*, not a display string — which is why +our design insists `inverse` be a typed `PredicateKey`. + +### `owl:SymmetricProperty` → `logic.symmetric` +**Rule:** `P(a,b) ⟹ P(b,a)`. Formally, `P ≡ P⁻` (a property that is its own inverse). +**Does:** synthesize the reciprocal edge. `affiliatedWith`, `partnerOf`, `competeWith`, `sameAs`. + +### `owl:AsymmetricProperty` (OWL 2) → `logic.asymmetric` +**Rule:** `P(a,b) ⟹ ¬P(b,a)`. Strictly stronger than "not symmetric." +**Does:** contradiction detection — asserting both directions is *inconsistent*. `parentOrganization`, +`betterThan`, `founder`. Asymmetry **entails irreflexivity** (see I.2). + +### `owl:TransitiveProperty` → `logic.transitive` +**Rule:** `P(a,b) ∧ P(b,c) ⟹ P(a,c)`. +**Does:** reachability and closure. `containedInPlace`, `parentOrganization`, `rankedAbove`, `dependOn`. +Caveat OWL learned the hard way: combining transitivity with cardinality restrictions breaks +decidability, so OWL DL forbids it on the same property. A lesson in not stacking powers naively. + +### `owl:ReflexiveProperty` / `owl:IrreflexiveProperty` (OWL 2) → `logic.reflexive` / `irreflexive` +**Rule:** reflexive ⟹ `P(a,a)` for all `a`; irreflexive ⟹ `¬P(a,a)` for all `a`. +**Does:** self-loop validation. `sameAs` is reflexive; the overwhelming majority of relationship +predicates are irreflexive (nothing is `betterThan` itself). + +### `owl:FunctionalProperty` → `logic.functional` +**Rule:** `P(a,b) ∧ P(a,c) ⟹ b = c`. Each subject has at most one object. +**Does:** uniqueness validation and, under no-UNA (I.4), *identity inference* — two objects asserted +for a functional property are inferred to be the same thing. `manufacturer`, `primaryImageOfPage`. + +### `owl:InverseFunctionalProperty` (IFP) → `logic.inverseFunctional` +**Rule:** `P(a,c) ∧ P(b,c) ⟹ a = b`. The object is a *key* that identifies the subject. +**Does:** entity resolution. This is how the Semantic Web does deduplication without a database join — +an IFP like `linkedAccount` says "whoever links this account is the same person." Extremely relevant +to a permissionless graph where the same entity is minted many times. + +### `owl:propertyDisjointWith` (OWL 2) → `ontology.contradicts` +**Rule:** `P` disjoint `Q` ⟹ no pair `(a,b)` may satisfy both. +**Does:** consistency. `trust ⊥ distrust`, `bullishOn ⊥ bearishOn`, `support ⊥ oppose`. This is the +formal hook for Intuition's disagreement/contradiction markets. + +### `owl:equivalentProperty` → `ontology.equivalentTo` +**Rule:** `P ≡ Q` ⟹ mutual sub-property; the two are interchangeable. Use for true aliases only. + +### `owl:propertyChainAxiom` (OWL 2 SROIQ) → P2 / future +**Rule:** `P ∘ Q ⊑ R`, e.g. `parentOf ∘ parentOf ⊑ grandparentOf`; `locatedIn ∘ containedInPlace ⊑ locatedIn`. +**Does:** compose new edges from paths. The most powerful and most expensive axiom; SROIQ admits it only +under a *regularity* (acyclicity) restriction to stay decidable. Park it until we have a reasoning layer. + +### Property restrictions / `domain` & `range` +RDFS `domain`/`range` and OWL value/cardinality restrictions type the endpoints. **We deliberately do +not duplicate these into specs** — typed targets live in `@0xintuition/classifications`. Worth knowing +the trap: in RDFS, `domain`/`range` are *axioms that entail types*, not *constraints that reject data*. +Asserting `alumniOf(x, y)` doesn't reject a non-Person `x`; it **infers** `x` is a Person. SHACL/ShEx +(2017) were later invented precisely to get *validation* (closed-world checks) back. Intuition wants +validation semantics, so this belongs in the classifications/SHACL-style layer, not OWL-style specs. + +## I.2 Characteristics are not independent — the constraint lattice + +A reasoner (and our `definePredicateRecord` derivation layer) must respect entailments *between* +characteristics: + +- `asymmetric ⟹ irreflexive` +- `symmetric ∧ irreflexive ⟹` cannot also be asymmetric +- `reflexive ⟹ ¬irreflexive` (and a property may be neither) +- `symmetric ⟹ inverse = self` +- `transitive ⟹ inverse is transitive`; `symmetric ⟹ inverse is symmetric` +- an inverse pair must mirror each other's characteristics +- a functional + transitive property over an irreflexive domain has strong implications for chains + +These are exactly the cross-checks the design assigns to the derivation/validation step. Declaring +inconsistent flags should be a build-time error, not a silent record. + +## I.3 The expressivity ↔ tractability frontier (the central lesson of OWL) + +OWL shipped **three species** (Lite, DL, Full) and OWL 2 added **three profiles** — not for fun, but +because the field discovered that *more reasoning power costs more compute, up to undecidable*: + +| Profile | Optimized for | Drops | +|---|---|---| +| **OWL 2 EL** | huge terminologies (e.g. SNOMED医) | disjunction, inverse, cardinality | +| **OWL 2 QL** | query rewriting over databases | most existentials | +| **OWL 2 RL** | rule engines / forward-chaining at scale | some equality reasoning | + +**The lesson for Intuition:** decide *how much entailment we are willing to compute at graph scale* +before we adorn predicates with axioms we can never run. A property-hierarchy + inverse + symmetry + +disjointness reasoner (essentially OWL 2 RL) is cheap, forward-chainable, and covers ~90% of our value. +Transitive closure is moderate. Property chains and full equality reasoning are where costs explode. +Our P0/P1/P2 tiers track this frontier on purpose. + +## I.4 Two assumptions that change everything: OWA & no-UNA + +- **Open World Assumption (OWA):** absence of a fact does *not* make it false. RDF/OWL are open-world. + Consequence: you **cannot infer `¬trust` from the absence of a `trust` edge.** Contradiction detection + must come from *explicit* negation or *disjointness axioms* (`contradicts`), never from missing data. + This is why `polarity` + `contradicts` are first-class fields rather than something a query infers. +- **No Unique Name Assumption (no-UNA):** two different identifiers may denote the same entity unless + said otherwise. This is what makes `sameAs`, functional, and inverse-functional properties powerful + (they *infer* identity) — and dangerous (`sameAs` "smushing" can collapse the graph if mis-asserted). + A permissionless minting model lives and dies on getting this right. + +--- + +# Part II — Thirty years (plus) of knowledge-graph research + +A compressed lineage. The throughline: the field oscillates between **expressive, hand-curated, +logically rigorous** systems and **large-scale, emergent, statistically-driven** ones — and the best +modern designs (Wikidata, Knowledge Vault) fuse both. Intuition is squarely in that fusion space. + +## II.1 Pre-history: semantic networks & frames (1968–1985) +- **Semantic networks** (Quillian, 1968; Collins & Quillian) — concepts as nodes, labeled edges; the + original "meaning as graph topology." Introduced `is-a` and property inheritance. +- **Frames** (Minsky, 1974) — structured concepts with slots and defaults; the ancestor of objects, + classes, and schema.org "types." +- **Conceptual Graphs** (Sowa, 1976/1984) — logic-based graphs unifying semantic nets with predicate + logic; an early, serious attempt at *machine-readable relations with formal meaning*. +- **WordNet** (Miller, Princeton, 1985–) — a lexical graph whose *relation types themselves* are the + contribution: hypernymy/hyponymy (`is-a`, ≈ our `specializes`), meronymy (part-of, ≈ containment), + **antonymy** (≈ our `contradicts`), troponymy. WordNet is the canonical evidence that a small, + well-chosen set of relation *properties* captures most of lexical meaning. Directly validates our + approach of enriching predicates with a compact, principled property set. + +## II.2 The logicist peak: Description Logic & Cyc (1985–2000) +- **KL-ONE** (Brachman & Schmolze, 1985) — structured inheritance networks with formal subsumption; + the direct ancestor of DL and thus of OWL. Gave us `subPropertyOf`/`subClassOf` with real semantics. +- **Description Logic** matures (1990s) — `ALC` and its family, tableau reasoners (FaCT, RACER, CLASSIC), + and the systematic mapping of *which axioms cost what* (the Description Logic Handbook, Baader et al., + 2003). This is the body of theory OWL later standardized. +- **Cyc** (Lenat, 1984–) — a decades-long bet on hand-encoding common sense. Lessons: (1) **microtheories** + — truth is *context-scoped*, the same assertion can hold in one context and fail in another (a warning + for any global truth graph); (2) hand-curation does not scale to the open world. Cyc's struggles are + why the next era went statistical and emergent. + +## II.3 The Semantic Web (1999–2010) +- **The vision** (Berners-Lee, Hendler & Lassila, *Scientific American*, 2001) — a web of machine-readable + assertions. **RDF** (1999/2004), **RDFS**, then **OWL** (2004, out of the DARPA **DAML+OIL** effort, + ~2001), **SPARQL** (2008), and **OWL 2** (2009). +- **Linked Data** (Berners-Lee, 2006) and the **Linking Open Data** project operationalized "publish + triples, link by URI." The web-scale knowledge bases arrived: **DBpedia** (Auer et al., 2007), + **YAGO** (Suchanek, Kasneci & Weikum, 2007), **Freebase** (Metaweb, 2007 → Google, 2010). +- **Lesson:** rich OWL reasoning rarely ran at web scale; most deployed value came from RDFS-level + inference (hierarchies, domains) plus links. Expressivity that can't be computed is decoration. + +## II.4 "Knowledge Graph" as a term, and the pragmatic turn (2010–2015) +- **schema.org** (Google/Bing/Yahoo, 2011) — a deliberately *lightweight, shared* vocabulary that won + adoption precisely by *not* demanding heavy OWL semantics. Intuition already speaks schema.org in its + IPFS predicate documents — a direct inheritance. +- **Google Knowledge Graph** (2012) — coined the popular term; "**things, not strings**." Built on + Freebase. Marked the shift from ontology-first to entity-first at scale. +- **Wikidata** (Vrandečić & Krötzsch, 2012; Freebase's successor) — the most important design reference + for Intuition. Permissionless, collaboratively edited, and it bakes in exactly the meta-machinery we're + reaching for: **qualifiers** (context on a statement), **ranks** (preferred/normal/deprecated values), + **references** (provenance per statement), and **property constraints** (soft validation). Wikidata is + proof that an emergent, multi-actor graph still needs disciplined per-statement metadata to be usable. + +## II.5 Statistical & learned knowledge graphs (2010–2020) +- **NELL** (Carlson & Mitchell, CMU, 2010) — never-ending extraction with *confidence scores*; knowledge + as probabilistic, not binary. +- **Knowledge Vault** (Dong et al., Google, 2014) — fuses extractors with prior graphs under a + probabilistic model; **provenance and confidence are first-class**. Validates Intuition's instinct to + treat staking/attestation (signal + provenance) as core, not metadata. +- **Knowledge-graph embeddings** — and this is the striking convergence: the embedding literature + independently rediscovered *our exact property set* as the patterns a model must capture. + - **TransE** (Bordes et al., 2013) — relation as translation `h + r ≈ t`; natively models composition + (transitivity) and inversion, struggles with symmetry and 1-to-many. + - **DistMult** (Yang et al., 2015) — bilinear; models symmetry but *cannot* model antisymmetry. + - **ComplEx** (Trouillon et al., 2016) — complex embeddings specifically to capture **antisymmetry**. + - **RotatE** (Sun et al., 2019) — relation as rotation in complex space; its headline claim is that a + good representation must capture **symmetry, antisymmetry, inversion, and composition** — i.e. + `symmetric`, `asymmetric`, `inverse`, `transitive`. When a hand-built logic (OWL) and a learned + geometry (RotatE) converge on the *same four relational properties*, that set is not arbitrary — it + is the load-bearing core. Our `logic` facet is exactly these four plus cardinality. + - **R-GCN** (Schlichtkrull et al., 2018) — relational graph neural nets for link prediction. + +## II.6 Time, provenance, and statements-about-statements (the reification thread) +This thread is where Intuition's model is most distinctive, so it matters most. +- **States vs events** is old AI: situation calculus (McCarthy & Hayes, 1969) and event calculus + (Kowalski & Sergot, 1986) distinguish **fluents** (things that hold over intervals and can change) + from **events** (instantaneous). This is precisely our `temporalNature: state | event | permanent`. +- **Temporal RDF** (Gutierrez, Hurtado & Vaisman, 2007) and temporal KG embeddings added valid-time to + triples. The lesson: a `state` edge without a validity interval silently rots. +- **PROV-O** (W3C, 2013) standardized provenance (entities, activities, agents) — the vocabulary for + "who asserted this, derived from what." +- **Reification — how to say something about a statement** — is the field's longest-running headache, + and Intuition's triples-as-atoms model *is* a reification design. The options the field tried: + 1. **RDF standard reification** (1999) — a 4-triple `rdf:Statement` blob; verbose, semantically weak. + 2. **N-ary relations** — introduce an intermediary node for the relationship. + 3. **Named graphs / quads** — attach context to whole graphs. + 4. **Singleton properties** (Nguyen, Bodenreider & Sheth, 2014) — mint a unique sub-property per + statement; strikingly close to Intuition giving each predicate/edge an atom identity. + 5. **RDF-star / SPARQL-star** (Hartig, ~2014, standardized later) — first-class edge annotations; the + modern answer and the closest analog to a property-graph with per-edge metadata. +- Our `objectKind: 'claim'` (for `disputedBy`, `confirmedBy`, `attestedBy`, `citedBy`, `evidencedBy`) is + exactly this: predicates whose object is *another statement*. Naming it makes Intuition's reification + explicit and traversable instead of implicit. + +## II.7 Signed & trust networks (the social-graph thread Intuition actually lives in) +Classical KGs modeled *facts*; Intuition models *trust and opinion*, so the relevant theory is signed +social networks, which classical OWL says nothing about: +- **Structural balance theory** (Heider, 1946; formalized by Cartwright & Harary, 1956) — "the friend of + my friend is my friend; the enemy of my enemy is my friend." The original math of **signed** (positive/ + negative) edges. This is the academic backbone of our `polarity` field in a trust graph. +- **Status theory & signed-edge prediction** (Leskovec, Huttenlocher & Kleinberg, 2010, *Signed Networks + in Social Media*) — trust/distrust edges (Epinions, Slashdot) obey balance *and* status dynamics, and + signs are predictable from local structure. Direct grounding for treating `trust`/`distrust`, + `endorse`/`oppose`, `bullishOn`/`bearishOn` as signed and mutually constraining. +- **Subjective Logic** (Jøsang, ~2001) — an algebra of opinions with explicit *uncertainty*, built for + trust propagation. Relevant if Intuition ever wants to propagate/compose trust quantitatively, and a + pointer for a future `intensity`/confidence dimension. +- **Property-graph model & Cypher** (Neo4j) — the industrial counter-tradition to RDF: edges carry + arbitrary key/value properties natively (no reification needed). Intuition's per-edge economics make it + closer to a property graph than to classic RDF, which is worth keeping in mind when we choose how much + to serialize on-chain vs. compute off-graph. + +## II.8 OntoClean — meta-properties to discipline an ontology (the closest precedent to this work) +**OntoClean** (Guarino & Welty, 2000–2002) deserves its own note because it is *the* precedent for what +we are doing: tagging relations/types with **meta-properties** to keep an ontology honest. Its tags: +- **Rigidity** — is a property essential (`+R`, true of an instance in every world: *being a person*) or + a temporary role (`~R`/`-R`: *being a student/employee*)? This is almost exactly our `temporalNature` + `permanent` vs `state` distinction, arrived at from a different direction. +- **Identity** — does the property carry a criterion for telling two instances apart? (cf. our IFP/keys.) +- **Unity** and **Dependence** — is the entity a whole, and does it depend on another? (cf. relational + vs intrinsic predicates.) +OntoClean's whole thesis — *a small set of philosophical meta-properties dramatically improves an +ontology's quality and catches modeling errors* — is the precedent that says enriching predicates with +`polarity`/`claimType`/`temporalNature`/`logic` is not gold-plating; it is the known path to a clean graph. + +--- + +# Part III — Mapping the research onto Intuition's fields + +| Field (from `predicate-semantics.md`) | Primary lineage | One-line takeaway | +|---|---|---| +| `logic.symmetric / asymmetric / transitive` | OWL property characteristics; RotatE/ComplEx | The empirically-confirmed load-bearing core — both logic and ML converge here. | +| `logic.reflexive / irreflexive` | OWL 2 | Self-loop validity; respect the constraint lattice (asymmetric ⟹ irreflexive). | +| `logic.functional / inverseFunctional` | OWL; entity resolution | IFPs are *keys* — the Semantic Web's dedup mechanism, vital for permissionless minting. | +| `ontology.inverse` | `owl:inverseOf` | Typed property ref, not a string; store one direction. | +| `ontology.specializes` | RDFS `subPropertyOf`; KL-ONE; WordNet hypernymy | Cheapest high-value inference: roll-up. | +| `ontology.contradicts` | OWL 2 `propertyDisjointWith`; WordNet antonymy; balance theory | The formal hook for disagreement markets; required because OWA forbids inferring negation from absence. | +| `ontology.equivalentTo` | `owl:equivalentProperty` | Aliases only; beware no-UNA "smushing." | +| `polarity` | Signed networks (Heider; Leskovec et al.) | Classical KGs lacked this; a *trust* graph cannot. | +| `claimType` (factual/evaluative/normative) | fact/value distinction; Cyc microtheories; subjective logic | A market over a fact ≠ a market over an opinion. | +| `verifiability` | Knowledge Vault; PROV-O; Wikidata references | Provenance/confidence as first-class, validating staking-as-signal. | +| `temporalNature` | situation/event calculus; temporal RDF; OntoClean rigidity | Distinguishes edges that rot from edges that are permanent. | +| `objectKind` (entity/claim/literal) | Object vs datatype properties; reification thread (singleton properties, RDF-star) | Makes Intuition's triples-as-atoms reification explicit and traversable. | +| `marketPattern` | Intuition-native (no classical analog) | The economic layer the prior 30 years never had. | + +--- + +# Part IV — What the research warns us about + +1. **Don't out-reason your compute (the OWL species lesson).** Pick a target profile. OWL 2 RL — + property hierarchy + inverse + symmetry + transitivity + disjointness, all forward-chainable — buys + ~90% of the value cheaply. Treat property chains and full equality reasoning as opt-in P2. + +2. **Open World means contradictions need explicit axioms.** You can never conclude `¬trust` from a + missing edge. `polarity` and `contradicts` must be declared, not inferred from absence. Bake this into + how the disagreement/market logic reads the graph. + +3. **No Unique Names makes `sameAs`/functional/IFP both powerful and dangerous.** Identity inference can + collapse distinct entities ("smushing") if mis-asserted. Gate equivalence and key-like predicates + carefully — in a permissionless system this is a primary attack surface. + +4. **Emergent graphs still need per-statement discipline (the Wikidata lesson).** Wikidata is + permissionless *and* rigorous because every statement can carry qualifiers, rank, and references. + Intuition's staking is the analog of references; we should make sure `temporalNature`/`claimType` + give authors the same per-edge expressiveness Wikidata found indispensable. + +5. **Validation ≠ entailment (the SHACL lesson).** OWL `domain`/`range` *infer* types; they don't reject + bad data. Since Intuition wants to *reject* malformed edges, typing/validation belongs in the + classifications/SHACL-style layer — which is exactly where the design leaves it. Don't accidentally + re-import OWL's inferential domain/range semantics into a validation context. + +6. **Truth can be context-scoped (the Cyc microtheory lesson).** A predicate's assertion may be true in + one context and not another. We're not adding context-scoping now, but the design should not assume + global, timeless truth so hard that adding context later requires a rewrite. + +--- + +## Selected references + +- Quillian (1968) *Semantic Memory*; Minsky (1974) *A Framework for Representing Knowledge*; Sowa (1984) *Conceptual Structures*. +- Brachman & Schmolze (1985) *KL-ONE*; Baader et al. (2003) *The Description Logic Handbook*. +- Miller (1995) *WordNet*; Lenat (1995) *Cyc*. +- Berners-Lee, Hendler & Lassila (2001) *The Semantic Web*; W3C RDF (2004), RDFS (2004), OWL (2004), OWL 2 (2009); Horrocks, Kutz & Sattler (2006) *SROIQ*. +- Guarino & Welty (2002) *OntoClean*. +- Auer et al. (2007) *DBpedia*; Suchanek et al. (2007) *YAGO*; Vrandečić & Krötzsch (2014) *Wikidata*. +- Carlson et al. (2010) *NELL*; Dong et al. (2014) *Knowledge Vault*. +- Bordes et al. (2013) *TransE*; Yang et al. (2015) *DistMult*; Trouillon et al. (2016) *ComplEx*; Sun et al. (2019) *RotatE*; Schlichtkrull et al. (2018) *R-GCN*. +- McCarthy & Hayes (1969) *Situation Calculus*; Kowalski & Sergot (1986) *Event Calculus*; Gutierrez et al. (2007) *Temporal RDF*; W3C PROV-O (2013). +- Heider (1946); Cartwright & Harary (1956) *Structural Balance*; Leskovec, Huttenlocher & Kleinberg (2010) *Signed Networks in Social Media*; Jøsang (2001) *Subjective Logic*. +- Nguyen, Bodenreider & Sheth (2014) *Singleton Property*; Hartig (2017) *RDF-star*. diff --git a/.planning/predicate-semantics.md b/.planning/predicate-semantics.md new file mode 100644 index 0000000..72effbb --- /dev/null +++ b/.planning/predicate-semantics.md @@ -0,0 +1,280 @@ +# Design: Machine-Readable Predicate Semantics + +**Status:** Draft for review +**Author:** Architecture +**Date:** 2026-06-30 +**Scope:** `packages/predicates/src/types.ts`, `record.ts`, `ipfs.ts`, and the 133 specs under `src/generated/specs/` + +--- + +## 1. Motivation + +Predicates are how edges are created in the Intuition knowledge graph. Each predicate +(`affiliatedWith`, `memberOf`, `listedIn`, `trust`, `betterThan`, …) says something precise +about the relationship between its subject and object. Today most of that meaning lives in +prose (`description`) and a handful of booleans. A machine cannot reason over prose. + +This document maps out the full set of **machine-readable fields** we should add to predicate +specs so that software can: + +1. **Entail** — infer edges that were never explicitly written. +2. **Check consistency** — detect contradictory claims. +3. **Traverse & aggregate** — compute reachability, paths, and signed-signal reputation. +4. **Validate** — reject malformed edges at authoring time. +5. **Interpret** — render, price (markets), weigh sentiment, and judge freshness. + +A field earns a place in the schema only if a machine can *do* one of those five things with +it. Anything else is documentation and stays in `description`/`examples`. + +We deliberately adopt the vocabulary of **OWL / RDF property characteristics** (symmetric, +transitive, functional, inverse, sub-property, disjoint, …). That formal system was built for +exactly this problem, so aligning with it gives us interoperability with existing reasoners +and with schema.org for free. + +--- + +## 2. Current state + +`PredicateSpec` (in `types.ts`) carries: + +| Field | Facet | Notes | +|---|---|---| +| `key`, `name`, `description` | identity / display | | +| `category`, `status` | classification / lifecycle | | +| `marketPattern` | economic | `depositional \| attributive \| comparative` | +| `conjugates` | **grammar/display** | inflection across locales — *not* a relational property | +| `thirdPerson`, `examples` | display | | +| `isSymmetric`, `isTransitive`, `isHierarchical` | algebraic | partial coverage | +| `inversePredicate` | ontological | **a display-name string, not a typed key** | + +Domain/range typing intentionally lives in the `@0xintuition/classifications` predicate matrix; +`entity-type-map.ts` is explicitly legacy. **This design does not duplicate typing into specs.** + +### Naming issue to fix first + +The user's original prototypes were *symmetric*, *conjugative*, *transitive*. Two are clean. +"Conjugative" is ambiguous and should be retired: + +- `conjugates: boolean` in code is a **grammar** concept (it sits next to `i18n`/`thirdPerson` + in the IPFS serializer) — "this verb inflects across locales." +- The *relational* idea behind "conjugative" — "this relationship has a converse expressed by a + **different** predicate" — is already `inversePredicate` (`parentOrganization ⇄ subOrganization`). + +**Decision:** drop "conjugative" as a relational term. Keep `inverse` for the converse relation. +Rename the grammar field to `inflects` and move it under a `grammar` facet, removing the collision. + +--- + +## 3. Facet model + +We organize fields into facets rather than a flat blob. Most specs will only populate a few. + +| Facet | Job(s) | Field family | +|---|---|---| +| **Algebraic** | entailment, consistency, validation | `logic { symmetric, asymmetric, transitive, reflexive, irreflexive, functional, inverseFunctional }` | +| **Ontological** | entailment, consistency | `ontology { inverse, specializes, equivalentTo, contradicts }` | +| **Polarity** | aggregation, interpretation | `polarity` | +| **Epistemic** | interpretation, consistency | `claimType`, `verifiability` | +| **Temporal** | interpretation, freshness | `temporalNature` | +| **Target** | validation, traversal | `objectKind` | +| **Economic** | interpretation | `marketPattern` (existing) | +| **Presentation** | rendering only | `grammar { inflects, thirdPerson }`, `examples`, i18n | + +--- + +## 4. Field reference + +### 4.1 Algebraic (`logic`) + +OWL property characteristics. Cheap booleans that unlock automatic edge inference. + +| Field | Meaning | Powers | Examples | +|---|---|---|---| +| `symmetric` | R(a,b) ⟹ R(b,a) | store one direction, query both; synthesize reciprocal edges | `affiliatedWith`, `partnerOf`, `competeWith`, `sameAs` | +| `asymmetric` | R(a,b) ⟹ ¬R(b,a) (implies irreflexive) | contradiction detection | `parentOrganization`, `betterThan`, `founder`, `predecessorOf` | +| `transitive` | R(a,b) ∧ R(b,c) ⟹ R(a,c) | reachability, containment trees, ranking chains | `containedInPlace`, `parentOrganization`, `rankedAbove`, `dependOn` | +| `reflexive` / `irreflexive` | R(a,a) always / never | self-loop validation | `sameAs` reflexive; nearly all else irreflexive | +| `functional` | subject → ≤1 object | uniqueness validation | `manufacturer`, `primaryImageOfPage`, `parentOrganization` | +| `inverseFunctional` | object → ≤1 subject | identity resolution (key constraint) | `linkedAccount`, `sameAs` | + +**`isHierarchical` is redundant** — derivable from `transitive + asymmetric + has-inverse`. Drop it +as a primitive; if a tree-UI hint is wanted, document it as derived, not authored. + +**Payoff:** with `symmetric` + `inverse`, reciprocal edges are never stored — the graph layer +synthesizes them, halving writes for bidirectional predicates and making queries direction-agnostic. + +### 4.2 Ontological (`ontology`) — predicates relating to predicates + +The highest-leverage *new* facet. Predicates form their own graph. + +| Field | Meaning | Powers | Example | +|---|---|---|---| +| `inverse` (typed `PredicateKey`) | converse predicate | bidirectional query | `parentOrganization` → `subOrganization` | +| `specializes` (subPropertyOf) | this edge implies a broader edge | **roll-up reasoning** | `employedBy`, `alumniOf`, `memberOf` ⊑ `affiliatedWith` | +| `equivalentTo` | predicate-level alias | dedup | curate, e.g. `createdBy` ≡ `authoredBy`? | +| `contradicts` (disjointWith) | both cannot hold for same (s,o) | **contradiction / disagreement markets** | `trust` ⊥ `distrust`; `bullishOn` ⊥ `bearishOn`; `support` ⊥ `oppose` | + +> **Typed refs, not strings.** `inversePredicate: 'sub organization'` today is a display name that can +> drift from the `subOrganization` key. All ontological references must be **typed `PredicateKey`s**, +> so they are validatable and `definePredicateRecord` can cross-check that inverse pairs mirror each +> other's algebraic properties. + +`specializes` and `contradicts` are the two to ship first (equal priority). `specializes` lets a query +for "is X affiliated with Y" automatically catch `employedBy`/`alumniOf`/`memberOf`. `contradicts` +turns the trust graph into something that can detect and price disagreement. + +### 4.3 Polarity (`polarity`) + +``` +polarity: 'positive' | 'negative' | 'neutral' | 'none' +``` + +- positive: `endorse`, `trust`, `vouchFor`, `support`, `bullishOn`, `like`, `recommend` +- negative: `oppose`, `distrust`, `bearishOn`, `blocked`, `skepticalOf`, `disagreeWith`, `reported` +- neutral: `neutralOn` +- none: `manufacturer`, `containedInPlace`, `authoredBy` (relational, no sentiment) + +Lets a reputation engine aggregate **signed signal across heterogeneous predicates** into one score, +instead of hard-coding the sign of each predicate in the scoring layer. (`intensity: weak | strong` +is a deferred follow-up.) + +### 4.4 Epistemic (`claimType`, `verifiability`) + +``` +claimType: 'factual' | 'evaluative' | 'normative' +verifiability: 'onChain' | 'offChainVerifiable' | 'unverifiable' +``` + +- `factual`/verifiable: `manufacturer`, `authoredBy`, `parentOrganization`, `linkedAccount` — staking is *truth discovery*. +- `evaluative` (subjective): `betterThan`, `like`, `bullishOn` — staking is *opinion aggregation*. +- `normative` (ought): `compliantWith`, `regulatedBy`, `governedBy`. + +A market over a fact and a market over an opinion mean different things; economics and UI should +treat them differently. The schema currently cannot tell them apart. + +### 4.5 Temporal (`temporalNature`) + +``` +temporalNature: 'permanent' | 'state' | 'event' +``` + +- `permanent` — immutable once true; never stale: `authoredBy`, `derivedFrom`, `founded`, `precededBy`. +- `state` — durative, can silently become false; **needs validity intervals / "current"**: `employedBy`, `memberOf`, `locatedIn`, `governedBy`. +- `event` — point-in-time fact, stays true as history: `votedFor`, `reviewed`, `attestedBy`. + +Tells the graph layer which edges can rot (need expiry / re-attestation) versus write-once. + +### 4.6 Target (`objectKind`) + +``` +objectKind: 'entity' | 'claim' | 'literal' +``` + +- `literal` — object is a value, do not traverse: `url`, `imgUrl`, `hasDescription`. +- `claim` (reifying / meta-relation) — points at another statement, not an entity: `disputedBy`, `confirmedBy`, `attestedBy`, `citedBy`, `evidencedBy`. +- `entity` — the default majority. + +A validation + traversal primitive, and our reification story made explicit. + +### 4.7 Domain / range — intentionally excluded + +Typed subject/object targets remain in `@0xintuition/classifications`. Do **not** add +`subjectType`/`objectType` to specs; two sources of truth would drift. An optional non-authoritative +schema.org `domain`/`range` hint for export is deferred. + +--- + +## 5. Proposed schema shape (hybrid nested) + +Decision: keep small enum semantic fields flat (they read well and serialize trivially) and group +the two field-rich families (`logic`, `ontology`) plus presentation (`grammar`) into nested objects +so the type stays scannable. Retain `as const satisfies PredicateSpec`. + +```ts +export interface PredicateSpec { + key: string; + name: string; + description: string; + category: PredicateCategory; + status: PredicateStatus; + + // economic (Intuition) + marketPattern: MarketPattern; + + // semantics (flat enums) + polarity?: 'positive' | 'negative' | 'neutral' | 'none'; + claimType?: 'factual' | 'evaluative' | 'normative'; + verifiability?: 'onChain' | 'offChainVerifiable' | 'unverifiable'; + temporalNature?: 'permanent' | 'state' | 'event'; + objectKind?: 'entity' | 'claim' | 'literal'; + + // algebraic + logic?: { + symmetric?: boolean; + asymmetric?: boolean; + transitive?: boolean; + reflexive?: boolean; + irreflexive?: boolean; + functional?: boolean; + inverseFunctional?: boolean; + }; + + // ontological (typed key refs) + ontology?: { + inverse?: PredicateKey; + specializes?: PredicateKey; + equivalentTo?: PredicateKey; + contradicts?: readonly PredicateKey[]; + }; + + // presentation (rendering only) + grammar?: { inflects?: boolean; thirdPerson?: string }; + examples?: readonly string[]; +} +``` + +`definePredicateRecord` becomes the **derivation + validation layer**: + +- normalize defaults, +- derive implied properties (`asymmetric ⟹ irreflexive`; `symmetric ⟹ inverse = self`; + `transitive ⟹ inverse is transitive`), +- cross-validate that inverse pairs mirror each other's algebraic properties and that + `contradicts` is symmetric across the pair. + +This keeps authoring terse while the materialized record stays complete and internally consistent. + +--- + +## 6. Rollout plan + +All new fields are optional ⟹ additive and non-breaking. The only real refactor is +`inversePredicate: string` → `ontology.inverse: PredicateKey`. + +**P0 — adopt now (mostly mechanical backfill across 133 specs):** +- `polarity`, `temporalNature`, `objectKind` +- `logic.{symmetric, asymmetric, transitive, irreflexive}` (migrate existing booleans in) +- convert `inverse` to a typed `PredicateKey` + +**P1 — next (human curation; highest reasoning leverage):** +- `ontology.specializes` **and** `ontology.contradicts` (curated together, equal priority) +- `claimType` +- derivation/validation engine in `definePredicateRecord` +- extend the IPFS `additionalProperty` serializer + `PredicateIpfsPropertyName` union + +**P2 — research / future:** +- `functional`/`inverseFunctional`, `equivalentTo`, `verifiability`, `intensity` +- OWL property-chain axioms (`grandparentOf` from `parentOf ∘ parentOf`) +- a standalone `@0xintuition/reasoning` module consuming these for live entailment/consistency + +--- + +## 7. Open questions + +1. **`equivalentTo` policy** — do we collapse near-synonyms (`createdBy`/`authoredBy`) at the + predicate level, or keep them distinct and only relate them? Affects atom-id stability. +2. **Inverse coverage** — do all directional predicates get a named inverse, or only where the + converse is independently useful? (Synthesizing an unnamed inverse vs. minting a predicate.) +3. **`contradicts` strength** — hard logical contradiction (`trust`/`distrust`) vs. soft tension + (`skepticalOf`/`trust`). Do we need a separate `tension` relation, or is one field enough? +4. **Serialization surface** — which of these belong in the on-chain/IPFS predicate document vs. + stay package-side metadata for the reasoning layer? diff --git a/.planning/predicate-spec-decisions.md b/.planning/predicate-spec-decisions.md new file mode 100644 index 0000000..11f7a12 --- /dev/null +++ b/.planning/predicate-spec-decisions.md @@ -0,0 +1,234 @@ +# Decisions: Pruning & Structuring the Predicate Spec + +**Status:** Decision record — supersedes the schema shape in `predicate-semantics.md` §5 +**Companion to:** `predicate-semantics.md` (the map), `predicate-semantics-foundations.md` (the research) +**Date:** 2026-06-30 + +The previous two docs asked *what could we add?* This one asks the harder question: *what should we +add, and what should we cut?* The rule is strict — **a field must name a consumer and a scenario, or it +does not ship.** "A future reasoner might want it" is not a scenario. + +--- + +## 1. The pruning principle: prefer fields with a consumer that exists today + +We have four consumers, in descending order of how real they are right now: + +| Consumer | Exists today? | Reads predicate fields to… | +|---|---|---| +| **Frontend** (pickers, edge cards, profiles) | ✅ yes | decide how to render an edge and its object | +| **Indexer / graph layer** | ✅ yes | synthesize reciprocal edges, close transitive queries | +| **Reputation / scoring** | 🟡 emerging | aggregate signed signal, roll up sub-relations | +| **General reasoning engine** | ❌ not built | entailment, consistency checking | + +A field justified by the **frontend or indexer** is load-bearing now. A field justified *only* by the +not-yet-built reasoning engine is speculative and should be **deferred until that engine is real** — +this is the "don't out-reason your compute" lesson from the OWL species history, applied to our own roadmap. +Deferring costs nothing: every field is optional and additive, so we can add it the day a consumer appears. + +--- + +## 2. The exact problems we are solving + +Six concrete problems, each with a user story and the fields it needs. If a field doesn't serve one of +these, it's cut. + +### P1 — Bidirectional relationships without double-writing +> *Kames asserts "Alice **affiliated with** Acme." On Acme's page, Alice appears under affiliations +> automatically. Kames asserts "BigCo **parent organization** Acme"; Acme's page shows BigCo as parent +> and BigCo's page lists Acme as a subsidiary — from one edge.* + +The indexer synthesizes the reverse direction instead of forcing users to mint two atoms. +**Needs:** `isSymmetric` (reverse = same predicate), `inverse` (reverse = a different predicate). +**Consumer:** indexer + frontend. **Verdict: ship.** + +### P2 — Roll-up queries and signed reputation +> *"Show everyone affiliated with Acme" returns employees, alumni, and members — without the query +> enumerating every sub-relation. Bob's trust score sums endorsements and vouches as positive signal, +> distrust and opposition as negative.* + +**Needs:** `specializes` (employedBy/alumniOf/memberOf ⊑ affiliatedWith), `polarity` (signed aggregation). +**Consumer:** indexer (query expansion) + reputation. **Verdict: ship.** + +### P3 — Surfacing contradiction (the Intuition-native problem) +> *Alice asserts "trust Bob." Carol asserts "distrust Bob." The app surfaces the tension and opens a +> market on it. The system rejects "A better than B" alongside "B better than A" as incoherent.* + +This is the problem classical fact-graphs never had and the one most worth being cutting-edge on. A +belief/trust graph is *valuable precisely because it can represent and price disagreement.* +**Needs:** `contradicts` (trust ⊥ distrust), `isAsymmetric` (one-directional relations). +**Consumer:** reputation/markets (emerging) + frontend (conflict badges). **Verdict: ship `contradicts`; ship `isAsymmetric` as cheap metadata.** + +### P4 — Automatic frontend rendering from the predicate alone (the biggest near-term win) +> *The UI receives an edge and, without any per-predicate hardcoding, knows: the object of `imgUrl` is an +> image (render inline), the object of `url` is a link, the object of `attestedBy` is **another claim** +> (render a nested claim card), the object of `memberOf` is an **entity** (render a clickable node). +> Positive predicates render green, negative red. A `state` edge like `employedBy` shows an "as of" badge; +> a `permanent` edge like `authoredBy` does not.* + +This is the field set with the most immediate, concrete payoff, and it's the one the user explicitly +called out. The predicate spec becomes a **rendering contract**, eliminating a giant `switch` in the app. +**Needs:** `objectKind` (entity | claim | literal), `polarity` (color/sentiment), `temporalNature` (freshness badge), plus existing display fields. +**Consumer:** frontend, today. **Verdict: ship — highest confidence in the whole set.** + +### P5 — Data freshness and lifecycle +> *The graph knows `employedBy` can silently become false and flags it for re-attestation or display as +> historical, while `authoredBy` is write-once and never decays.* + +**Needs:** `temporalNature`. **Consumer:** frontend + data-quality tooling. **Verdict: ship.** + +### P6 — Machine-readability / interoperability +> *A third party (or a future reasoner) pulls our predicate's IPFS document and reads standard +> schema.org/OWL-shaped property characteristics without bespoke parsing.* + +This is served *for free* by the fields above as long as we serialize them in the existing +`additionalProperty` array using OWL-aligned names. **Verdict: no new fields; constrains naming/structure (see §4).** + +--- + +## 3. Field-by-field verdicts (the cuts are the point) + +| Field | Scenario it serves | Verdict | +|---|---|---| +| `isSymmetric` | P1 reciprocal edges | **Ship** | +| `inverse` (typed key) | P1 reverse-as-different-predicate | **Ship** | +| `isTransitive` | P2 closure ("places within California", org trees) | **Ship** | +| `specializes` | P2 roll-up query + reputation | **Ship** (needs curation) | +| `polarity` | P2 reputation, P4 color | **Ship** | +| `contradicts` | P3 belief markets | **Ship** (needs curation) | +| `isAsymmetric` | P3 validation; tells indexer "don't mirror" | **Ship** (cheap, derivation-checked) | +| `objectKind` | P4 render literal/claim/entity differently | **Ship** (highest-value) | +| `temporalNature` | P4 freshness badge, P5 lifecycle | **Ship** | +| `marketPattern` | existing economic layer | **Keep** | +| `claimType` (factual/evaluative) | market-design + "opinion vs fact" badge | **Ship, simplified** — drop the `normative` value until a consumer needs it | +| `isHierarchical` | — derivable from `transitive + asymmetric + inverse` | **Cut** (redundant) | +| `isReflexive`/`isIrreflexive` | self-loop validation; rare, derivable (`asymmetric ⟹ irreflexive`) | **Defer** — no consumer creates self-loops today | +| `isFunctional` | "one manufacturer" conflict check | **Defer** — no validation engine consuming it yet | +| `isInverseFunctional` | identity resolution / dedup of minted entities | **Defer, but flagged strategic** — revisit when a dedup engine exists; powerful for permissionless minting | +| `equivalentTo` | predicate aliasing | **Cut** — we deliberately keep near-synonyms distinct; "smushing" risk (no-UNA) outweighs benefit | +| `verifiability` | onChain/offChain | **Cut** — ~correlated with `claimType`; a redundant second axis | +| property chains | grandparentOf from parentOf∘parentOf | **Cut for now** — most expensive axiom, no consumer | + +**Net change vs the original proposal:** we drop from ~7 logic booleans to **3** (`isSymmetric`, +`isAsymmetric`, `isTransitive`), cut `equivalentTo`/`verifiability`/`isHierarchical`, defer the +functional family and reflexivity, and simplify `claimType`. That is roughly a 40% reduction in surface +area — and every survivor names a consumer that exists or is emerging. + +--- + +## 4. Flat vs nested — decision: **flat** + +I previously recommended nesting (`logic{}`, `ontology{}`). **The research reverses that.** Three reasons: + +1. **The serialization target is flat.** Our IPFS document emits `additionalProperty: PropertyValue[]` — + a flat list of `{name, value}`. Nesting in TS just means flattening again at serialization. The data's + destination is flat; the source should match it. +2. **The entire tradition is flat.** OWL doesn't nest — it asserts `P rdf:type owl:SymmetricProperty`, + `P owl:inverseOf Q`. schema.org is flat key/value. RDF is flat triples. We get interoperability by + *looking like* the standards, and the standards are flat. +3. **After pruning, there's nothing to nest.** ~10 semantic fields, most absent on any given spec. Wrapping + 3 booleans in a `logic:{}` object is ceremony that adds a layer of access (`spec.logic?.symmetric`) and + complicates `as const satisfies`, grep-ability, and diffs for zero benefit. + +Nesting earns its keep when a group is large, frequently co-populated, and namespaced against collisions. +None of those hold here. **Go flat.** Use the OWL-aligned `is*` convention for the algebraic booleans so +the names self-document and map 1:1 to the serialized property names. + +(Corollary: don't rename `conjugates`. We dropped "conjugative" as a *relational* term entirely, so the +collision is gone — `conjugates` can keep meaning grammatical inflection. One less migration.) + +--- + +## 5. The resulting spec + +```ts +export interface PredicateSpec { + // identity & lifecycle + key: string; + name: string; + description: string; + category: PredicateCategory; + status: PredicateStatus; + + // economic (Intuition-native) + marketPattern: MarketPattern; + + // rendering contract (frontend reads these directly) + objectKind?: 'entity' | 'claim' | 'literal'; + polarity?: 'positive' | 'negative' | 'neutral'; + temporalNature?: 'permanent' | 'state' | 'event'; + claimType?: 'factual' | 'evaluative'; + + // algebraic (OWL-aligned, flat, is* convention) + isSymmetric?: boolean; + isAsymmetric?: boolean; + isTransitive?: boolean; + + // inter-predicate (typed key references — validated in definePredicateRecord) + inverse?: PredicateKey; + specializes?: PredicateKey; + contradicts?: readonly PredicateKey[]; + + // display + conjugates: boolean; // grammatical inflection (unchanged) + thirdPerson?: string; + examples?: readonly string[]; +} +``` + +`definePredicateRecord` stays the consistency gate: derive implications (`asymmetric ⟹ not symmetric`), +verify inverse pairs mirror each other, and verify `contradicts` is declared symmetrically on both sides. + +--- + +## 6. Pros, cons, and the value proposition + +**Pros** +- **Self-describing graph → automatic rendering.** The frontend stops hardcoding per-predicate behavior; + the spec *is* the rendering contract (P4). This is the clearest, nearest win. +- **Cheap, scalable inference.** The shipped set is exactly the forward-chainable OWL 2 RL core — symmetry, + transitivity, inverse, sub-property, disjointness. Computable at graph scale; no decidability cliffs. +- **Fewer stored edges.** Symmetric/inverse synthesis means we store one direction and serve both. +- **A differentiator, not a clone.** `contradicts` + `polarity` + `marketPattern` make this a *belief and + trust* graph that prices disagreement — something no classical fact-graph (DBpedia, Wikidata, Google KG) + was built to do. That is the cutting-edge claim, and it's grounded in signed-network theory, not vibes. +- **Interoperability for free** via OWL/schema.org-aligned flat naming. + +**Cons / risks (and mitigations)** +- *Authoring burden across 133 specs.* → Most fields are optional; `objectKind`/`polarity`/`temporalNature`/ + the booleans are mechanical and scriptable; only `specializes`/`contradicts`/`inverse` need human curation. +- *Cross-referential consistency* (inverse/specializes/contradicts form a predicate graph that can drift). + → `definePredicateRecord` validates it at build time; an inconsistent set fails the build. +- *Encoding reasoning we can't run.* → Explicitly mitigated by the cuts in §3: we ship the RL core and defer + chains, equality, and the functional family until a consumer exists. +- *Modeling opinion as fact.* → `claimType` keeps the distinction explicit so markets/UI treat them differently. + +**The one-sentence value proposition:** *Every predicate carries just enough machine-readable structure +that the graph can render itself, query across relationship hierarchies, synthesize reverse edges, and +price disagreement — using the proven flat vocabulary of 30 years of knowledge-graph research, and nothing +more than that.* + +--- + +## 7. What we are explicitly NOT doing (and why that's a feature) + +- **Not** building domain/range typing into specs — it lives in `@0xintuition/classifications` (validation, + not OWL inference). One source of truth. +- **Not** shipping property chains, equality reasoning, or the functional family — no consumer yet; they're + the expensive end of the frontier and we can add them the day a reasoner exists. +- **Not** nesting — the data's destination and the entire standards tradition are flat. +- **Not** asserting global, timeless truth so hard we can't add Cyc-style context-scoping later. + +Restraint here is the design. The graph that ships the *minimal sufficient* property set — and computes all +of it — beats the graph that declares axioms it can never run. + +--- + +## 8. Open questions for sign-off + +1. `claimType` — ship 2-value (`factual`/`evaluative`) now, or hold it entirely until a market actually + branches on it? (Leaning: ship; it's cheap and drives a frontend badge today.) +2. `polarity` — do we need `neutral` as a value, or is "absent = no sentiment" enough? (Leaning: keep + `neutral` only for predicates like `neutralOn` whose sentiment is *explicitly* zero, distinct from absent.) +3. `inverseFunctional` for identity resolution — strategically important for permissionless dedup. Do we + want to pull it forward into P1 to start collecting the data before the dedup engine exists? From 444cecc79dc97e623b78e98af66f9875828090a8 Mon Sep 17 00:00:00 2001 From: Kames Date: Wed, 1 Jul 2026 08:23:53 -0700 Subject: [PATCH 3/7] checkpoint: predicate reasoning --- .planning/00-START-HERE.md | 83 +++++ .../predicate-backend-implementation-plan.md | 212 +++++++++++++ .planning/predicate-backend-integration.md | 296 ++++++++++++++++++ .planning/predicate-semantics.md | 14 +- .planning/predicate-spec-decisions.md | 26 +- 5 files changed, 620 insertions(+), 11 deletions(-) create mode 100644 .planning/00-START-HERE.md create mode 100644 .planning/predicate-backend-implementation-plan.md create mode 100644 .planning/predicate-backend-integration.md diff --git a/.planning/00-START-HERE.md b/.planning/00-START-HERE.md new file mode 100644 index 0000000..0d5cd68 --- /dev/null +++ b/.planning/00-START-HERE.md @@ -0,0 +1,83 @@ +# Predicate Semantics — Start Here + +**What this is:** a proposal to add a small set of machine-readable fields to our predicate specs +(`packages/predicates`) so the Intuition knowledge graph can render itself, query across relationship +hierarchies, synthesize reverse edges, and price disagreement. Grounded in 30 years of knowledge-graph +research, pruned hard to stay simple. + +**Status:** Draft for team review · **Date:** 2026-06-30 · **Owner:** Architecture + +--- + +## TL;DR — what we're proposing + +Add **8 new fields** to `PredicateSpec` (flat, all optional, additive/non-breaking), plus one soft-ship +(`claimType`), plus the existing `marketPattern`: + +| Field | Type | What it unlocks | +|---|---|---| +| `objectKind` | `entity \| claim \| literal` | frontend renders any edge with no per-predicate code; makes reification visible | +| `polarity` | `positive \| negative \| neutral` | one signed reputation score across all predicates; sentiment-colored UI | +| `inverse` | `PredicateKey` | store one edge, serve both directions (different reverse predicate) | +| `isSymmetric` | `boolean` | store one edge, serve both directions (same predicate) | +| `temporalNature` | `permanent \| state \| event` | freshness model — which edges rot, which are forever | +| `contradicts` | `PredicateKey[]` | detect & price disagreement (trust ⊥ distrust) — the differentiator | +| `isTransitive` | `boolean` | closure/reachability queries (containment trees, org charts) | +| `specializes` | `PredicateKey` | roll-up queries & reputation (employedBy ⊑ affiliatedWith) | +| `claimType` *(soft-ship)* | `factual \| evaluative` | fact-vs-opinion market & UI semantics | +| `marketPattern` *(exists)* | `depositional \| attributive \| comparative` | per-predicate market mechanics | + +**Deliberately deferred or cut** (no consumer yet / redundant): `isAsymmetric`, `isFunctional`, +`isInverseFunctional`, `isReflexive`/`isIrreflexive`, `equivalentTo`, `verifiability`, property chains, +`isHierarchical`. See `predicate-fields/deferred-and-cut.md`. + +**Guiding rule:** a field ships only if it names a *consumer that exists today* (frontend, indexer) or an +*emerging* one (reputation/markets). We deliberately stay inside the cheap, forward-chainable **OWL 2 RL** +reasoning profile and don't encode inference we can't compute at scale. + +--- + +## Reading order + +1. **`00-START-HERE.md`** (this file) — the summary and reading order. +2. **`predicate-spec-decisions.md`** — ⭐ *canonical.* The pruned field set, flat-vs-nested decision, the + final `PredicateSpec`, pros/cons, and what we're explicitly not doing. +3. **`predicate-fields/`** — one document per field with multiple use cases and a confidence score + (see `predicate-fields/README.md` for the rubric and the score table). This is where each field earns + its place; the scoring overturned one earlier decision (`isAsymmetric`). +4. **`predicate-semantics.md`** — the original full brainstorm / map of everything possible. Background; + its §5 schema sketch is superseded by #2. +5. **`predicate-semantics-foundations.md`** — the research grounding: OWL/RDF property characteristics in + depth and 30 years of KG lineage. Read for the "why these and not others." +6. **`predicate-backend-integration.md`** — how the backend (`alpha/backend`) consumes these fields: + schema, indexing, query optimization, performance. **Headline:** `kg.predicates` already has dormant + slots for `is_symmetric`/`is_transitive`/`inverse_predicate_id`/`metadata`, and `kg.triples` already + has `inferred`/`confidence` for synthesized edges — so this is mostly *activation*, not construction. + Three gaps: seed real metadata, add the missing columns, build the consumers. +7. **`predicate-backend-implementation-plan.md`** — the ticket-ready build plan: 7 workstreams + (package → schema → seed → query features → API → consumers → tests), phased P0–P5 with dependencies, + acceptance criteria, and rough sizing. Start here when we're ready to execute. + +If you read only one: **`predicate-spec-decisions.md`**. + +--- + +## Open questions for the team + +1. **`claimType`** — soft-ship now (define + populate where obvious), or hold entirely until a market + branches on it? +2. **`polarity`** — keep an explicit `neutral` value (for `neutralOn`) distinct from "absent," or collapse? +3. **`isInverseFunctional`** — pull forward to *start collecting* dedup/identity data before the resolution + engine exists (data only, no auto-merge)? +4. **`marketPattern`** — its three values lack an external anchor and overlap conceptually with `claimType`; + does it need its own semantics-clarification doc before we lean on it for market mechanics? +5. **`contradicts`** — hard contradiction only (trust/distrust), or do we also model soft *tension* + (skepticalOf/trust) as a separate relation? + +--- + +## The one-sentence pitch + +*Every predicate carries just enough machine-readable structure that the graph can render itself, query +across relationship hierarchies, synthesize reverse edges, and price disagreement — using the proven flat +vocabulary of 30 years of knowledge-graph research, and nothing more than that.* diff --git a/.planning/predicate-backend-implementation-plan.md b/.planning/predicate-backend-implementation-plan.md new file mode 100644 index 0000000..8d61956 --- /dev/null +++ b/.planning/predicate-backend-implementation-plan.md @@ -0,0 +1,212 @@ +# Implementation Plan: Predicate Attributes End-to-End + +**Status:** Build plan — ticket-ready +**Companion to:** `predicate-backend-integration.md` (the mapping/why), `predicate-spec-decisions.md` (the fields) +**Date:** 2026-06-30 + +Goal: take the 8 decided fields (+ soft-ship `claimType`, + `marketPattern`) from *declared in the package* +to *stored, seeded, queried, and consumed* in the backend. The backend has dormant schema slots but **zero +implemented behavior** — so this plan covers the whole path: package → schema → seed → query features → +consumers, with sequencing, dependencies, and acceptance criteria. + +--- + +## 0. Verified ground truth (what we're building on) + +- **KG is a separate, optional Postgres** (`DATABASE_KG_URL`), written by the projection + `core_entities.rs`; it "graceful no-ops when absent." (verified: `core_entities.rs` header.) +- **The KG schema is NOT in committed migrations.** The numbered set `indexing-services/migrations/000–049` + is the **TimescaleDB** event store. `kg.predicates`/`kg.triples` exist only in the test fixture + `embeddings-job/tests/backfill_integration.rs`, described as "post-#437 shape." → **Prerequisite P0: + locate/own the KG migration mechanism.** +- **The migration runner** is psql over `migrations/*.sql`, tracked in `schema_migrations`, keyed by + `$DATABASE_URL` (`Dockerfile.migrations`). The KG DB needs its own runner/dir (different instance). +- **Predicate write path** `upsert_kg_predicate_lazy` (`core_entities.rs:485`) inserts only + `(id, slug, label)` `ON CONFLICT (id) DO NOTHING` — it can never write or update our metadata. +- **Enabler:** the package already computes a stable **atom id per predicate** (`PREDICATE_IDS` in + `predicates.ts`, via `calculateAtomId`). So a generated seed can carry **both `id` and `slug`** — the + id↔slug mapping the integration doc worried about is solved at generation time. +- **Package build** runs generator scripts (`../../scripts/*.mjs`) then `tsc`; specs live (hand-authored) + in `src/generated/specs/*.ts`; `PredicateSpec` is in `src/types.ts`; `definePredicateRecord` in + `record.ts`. +- **Counter-triple scaffolding** already exists on `kg.triples` (`is_counter_triple`, `sibling_triple_id`) + — the instance-level expression of our predicate-level `contradicts`. + +--- + +## 1. Workstreams overview + +| WS | Name | Repo | Outcome | +|---|---|---|---| +| **A** | Package = source of truth | `packages/predicates` | fields declared, validated, backfilled; seed artifact emitted | +| **B** | KG schema | `alpha/backend` (KG migrations) | columns/tables/indexes/constraints for all fields | +| **C** | Seed pipeline | both | package metadata lands in `kg.predicates` and stays in sync | +| **D** | Query features | `alpha/backend` | reverse-edge, transitive, roll-up, contradiction, validation actually work | +| **E** | API exposure | `alpha/backend/api` | predicate metadata + new query powers reach clients | +| **F** | Consumers | `recommendation-service` | polarity/transitive/specializes feed ranking | +| **G** | Test & observability | both | correctness invariants + perf guarded | + +--- + +## 2. WS-A — Package (source of truth) + +| ID | Task | Files | Size | +|---|---|---|---| +| A1 | Add fields to `PredicateSpec` (flat): `polarity`, `objectKind`, `temporalNature`, `claimType`, `isSymmetric` (exists), `isTransitive` (exists), `inverse: PredicateKey`, `specializes: PredicateKey`, `contradicts: PredicateKey[]` | `src/types.ts` | S | +| A2 | `definePredicateRecord`: defaults + **derivation/validation** — `isSymmetric ⟹ inverse=self` (reject conflicting `inverse`); inverse pairs mirror algebraic flags; `contradicts` symmetric on both sides; typed-key refs resolve to known keys (build fails otherwise) | `src/record.ts` | M | +| A3 | Migrate `inversePredicate: string` → `inverse: PredicateKey` across specs | `src/generated/specs/*.ts` | S | +| A4 | Backfill **mechanical** fields across 133 specs: `polarity`, `objectKind`, `temporalNature` (scriptable, obvious values) | specs | M | +| A5 | Backfill **curated** fields: `specializes`, `contradicts`, `inverse` pairs, `claimType` (human review) | specs | M | +| A6 | New generator `scripts/generate-predicate-seed.mjs` → emits `dist/predicate-seed.json` (and optional `.sql`): one row per predicate with `{ id: PREDICATE_IDS[key], slug: key, label, description, isSymmetric, isTransitive, inverse→id, specializes→id, contradicts→[id], polarity, objectKind, temporalNature, claimType, marketPattern }` — references resolved to atom ids | `scripts/`, `package.json` build | M | +| A7 | Tests: derivation/validation unit tests; seed snapshot test | `src/*.test.ts` | S | + +**Acceptance:** `bun run build` emits a deterministic seed artifact; invalid specs (one-sided inverse, +asymmetric `contradicts`, dangling key ref) fail the build. + +--- + +## 3. WS-B — KG schema migrations + +> Lands in the KG migration mechanism resolved in **P0**, against the `DATABASE_KG_URL` instance. + +| ID | Task | Detail | +|---|---|---| +| B1 | Confirm existing columns | `is_symmetric`, `is_transitive`, `inverse_predicate_id`, `metadata` present in live KG (not just fixture) | +| B2 | Add predicate columns | `specializes_predicate_id text`, `polarity text`, `object_kind text`, `market_pattern text`, `claim_type text` (or keep `claim_type`/`temporal_nature` in `metadata` JSONB per the column-vs-JSONB heuristic) | +| B3 | Add `temporal_nature` | JSONB-first (`metadata->>'temporalNature'`); promote later if filtered | +| B4 | Contradictions table | `CREATE TABLE kg.predicate_contradictions (predicate_id text, contradicts_id text, PRIMARY KEY (predicate_id, contradicts_id))` — store both directions (seed writes symmetric pairs) | +| B5 | CHECK constraints | `polarity IN ('positive','negative','neutral')`, `object_kind IN ('entity','claim','literal')`, `market_pattern IN ('depositional','attributive','comparative')` | +| B6 | Reconcile backend-only flags | `is_hierarchical` (we cut it — derive `is_transitive AND inverse_predicate_id IS NOT NULL` at seed, or drop), `is_market` (superseded by `market_pattern` — backfill then deprecate), `is_social` (map from `category` or leave) | +| B7 | Triple indexes | `(subject_id, predicate_id)`, `(object_id, predicate_id)`, `(predicate_id)`, partial `WHERE inferred = false` (see integration doc §5.5) | + +**Acceptance:** migrations apply cleanly on a fresh KG DB and on the post-#437 shape; fixture updated to match. + +--- + +## 4. WS-C — Seed pipeline (the missing link) + +| ID | Task | Detail | +|---|---|---| +| C1 | Seed loader | A backend step that reads `predicate-seed.json` (pinned package version) and upserts `kg.predicates` **`ON CONFLICT (id) DO UPDATE`** of the metadata columns; `metadata = kg.predicates.metadata || EXCLUDED.metadata` (merge). Implement as a small Rust seed binary in `projections` (has KG pool) or a generated SQL migration. | +| C2 | Reconcile with lazy path | Keep `upsert_kg_predicate_lazy` as-is (`DO NOTHING`, for unknown chain predicates). Seed owns metadata via `DO UPDATE`. Document the division: lazy = discovery, seed = truth. | +| C3 | Contradictions seed | Populate `kg.predicate_contradictions` from the artifact (both directions). | +| C4 | Ordering | Seed runs on deploy, idempotent, independent of ingestion order (keyed by atom id from the package). | +| C5 | CI sync | Package release regenerates the artifact; backend pins the package version; a CI check fails if the committed seed is stale vs the package. | + +**Acceptance:** after seed, every enshrined/proposed predicate row in `kg.predicates` carries correct +metadata; re-running the seed is a no-op; changing a spec + re-seed updates the row. + +--- + +## 5. WS-D — Query features (make the fields do work) + +Default to **query-time** (no data migration); materialize only measured-hot paths. + +| ID | Feature | Approach | Reads | +|---|---|---|---| +| D1 | Reverse-edge view (`isSymmetric`, `inverse`) | query-time rewrite: symmetric ⇒ match `subject_id=X OR object_id=X`; inverse ⇒ present reverse rows under the inverse predicate's label | needs B7 `object_id` index | +| D2 | Transitive closure (`isTransitive`) | bounded `WITH RECURSIVE` (depth cap ~8) helper | B7 `(subject_id,predicate_id)` | +| D3 | Roll-up (`specializes`) | load shallow hierarchy from `kg.predicates`; rewrite predicate filter to `predicate_id IN (descendants)` | B7 `(predicate_id)` | +| D4 | Contradiction detection + counter-triple wiring | the §5.4 join over `kg.predicate_contradictions`; on triple insert, if predicate `contradicts` an existing sibling `(subject,object)`, set `is_counter_triple=true`, link `sibling_triple_id` | join table + triple indexes | +| D5 | `objectKind` validation | at ingestion/API write, enforce `object_type` matches: `claim ⇒ object_type='triple'`, `literal ⇒` value, `entity ⇒ 'node'` | `object_type` column exists | +| D6 | (Phase 4) Materialize hot paths | inferred reverse edges + closure table, written `inferred=true` + provenance; **excluded from aggregates that count the origin** | partial indexes | + +**Where D1–D3 live:** the KG read layer the API uses (e.g. `api/src/clients/kg-workflows.ts` / KG read +crate). Centralize as reusable query builders so API + recommendation share them. + +**Acceptance:** golden-query tests for each (reverse view returns origin's counterpart; closure respects +depth cap; roll-up expands correctly; contradiction query finds conflicts; invalid object_kind rejected). + +--- + +## 6. WS-E — API exposure + +| ID | Task | Files | +|---|---|---| +| E1 | `GET /api/predicates`, `GET /api/predicates/:id` returning the rendering contract (`objectKind`, `polarity`, `temporalNature`, `marketPattern`, relations) | `api/src/routes/`, new `predicates/` module | +| E2 | Extend triple hydration to include predicate metadata | `api/src/triples/surreal.ts` (+ KG read path, ENG-11475) | +| E3 | Query params: filter by `polarity`, opt-in roll-up expansion, `includeInferred` toggle (default exclude) | `api/src/routes/triples.ts` | + +**Acceptance:** frontend can render an edge purely from the predicate response; inferred edges excluded by +default and labeled when included. + +--- + +## 7. WS-F — Consumers (recommendation/reputation) + +| ID | Task | Detail | +|---|---|---| +| F1 | polarity-aware scorer | new scorer reading `kg.predicates.polarity`; signed reweight; **exclude inferred** to avoid double count | +| F2 | transitive trust chains | use `isTransitive` (D2) for "trusted-by-the-trusted" social signal | +| F3 | specializes interest widening | expand interest predicates via D3 | + +(Recommendation currently reads only vaults/posts/events — these add KG reads. Scorer internals from +exploration are **unverified**; confirm before building.) + +--- + +## 8. WS-G — Testing & observability + +- G1 package unit tests (A7). +- G2 migration apply tests + update `backfill_integration.rs` fixture. +- G3 update `e2e-tests/08-dual-write-consistency.e2e.test.ts` for new predicate fields. +- G4 query-correctness tests for D1–D5. +- G5 **invariant test: no double-counting** — synthesized/rolled-up edges never summed with origins. +- G6 metrics: inferred-edge counts, closure-query latency, seed drift check. + +--- + +## 9. Sequencing + +``` +P0 Prereqs ─ resolve KG migration mechanism; pin package↔backend versions + │ +P1 Foundation (additive, no behavior change) ── WS-A, WS-B, WS-C + │ outcome: kg.predicates fully populated; dormant→live data +P2 Read features (query-time, no data migration) ── D1,D2,D3 + E1,E2 + │ outcome: reverse views, closure, roll-up, predicate API. MEASURE. +P3 Validation + contradiction ── D4,D5 (+ counter-triple alignment), E3 +P4 Optimize hot paths ── D6 materialization + index tuning (only where P2 shows pressure) +P5 Consumers ── WS-F recommendation signals +``` + +P1–P2 are low-risk and unlock most of the value (rendering contract + roll-up + reverse views). Everything +after is opt-in optimization and new product surface. + +--- + +## 10. Cross-cutting invariants & risks + +- **No double-counting** (biggest correctness trap): inferred + rolled-up edges excluded from aggregates + that already count the origin. Enforced by G5 and "exclude inferred by default." +- **OWA**: contradiction and "current state" read only explicit assertions; never infer negation from absence. +- **Replay safety**: seed and lazy paths both idempotent; seed `DO UPDATE` vs lazy `DO NOTHING` must not fight. +- **KG-optional**: all features graceful when `DATABASE_KG_URL` absent (matches current projection behavior). +- **Schema drift**: confirm live KG vs fixture (P0); resolve the predicate-FK discrepancy. +- **Version coupling**: backend seed pins a package version; CI guards drift (C5). + +--- + +## 11. P0 prerequisites to unblock (answer these first) + +1. Where do KG migrations live / how is the `DATABASE_KG_URL` schema versioned (the post-#437 mechanism)? +2. Is the fixture shape (`is_symmetric` etc.) actually deployed, or still in flight in #437? +3. Predicate FK: does `kg.triples.predicate_id` reference `kg.predicates(id)` in the live DB? +4. Has the API committed to the KG read path (ENG-11475), or do we extend SurrealDB queries meanwhile? +5. Recommendation-service scorer internals — verify before WS-F. + +--- + +## 12. Rough sizing + +| Phase | Effort (eng-weeks, rough) | Risk | +|---|---|---| +| P0 prereqs | 0.5 (mostly investigation) | unblocks everything | +| P1 foundation | 2–3 | low (additive) | +| P2 read features | 2–3 | low–med (query correctness) | +| P3 validation/contradiction | 2 | med (ingestion path, counter-triple) | +| P4 optimization | 1–2 | med (write amplification) | +| P5 consumers | 1–2 | low (additive reads) | + +These are order-of-magnitude; the curated backfill (A5) and counter-triple alignment (D4) are the most +estimate-sensitive. diff --git a/.planning/predicate-backend-integration.md b/.planning/predicate-backend-integration.md new file mode 100644 index 0000000..75894ae --- /dev/null +++ b/.planning/predicate-backend-integration.md @@ -0,0 +1,296 @@ +# Backend Integration: Activating Machine-Readable Predicate Fields + +**Status:** Draft for review +**Companion to:** `predicate-spec-decisions.md` (the field set) +**Backend mapped:** `alpha/backend` (branch `kames/simplify-feed`), 2026-06-30 +**Scope:** how the 8 shipped fields reach the backend, get stored/indexed, and optimize queries + +> ⚠️ **Verification caveat.** The KG schema below is read from the test fixture +> `indexing-services/crates/embeddings-job/tests/backfill_integration.rs` (it references PR "#437"). The +> field `is_symmetric` appears **only** in that fixture, not in committed migrations — so the canonical +> `kg.predicates` shape should be confirmed against the live `DATABASE_KG_URL` migrations before building. +> The architecture, write path, and `core_entities.rs` details are verified from source. + +--- + +## 1. Executive summary — the schema already anticipated this + +The most important finding: **the backend is already shaped for this work, but the fields are dormant.** + +`kg.predicates` already declares `is_symmetric`, `is_transitive`, `is_hierarchical`, +`inverse_predicate_id`, and a `metadata jsonb` column. `kg.triples` already has `inferred boolean` and +`confidence numeric` — purpose-built for *synthesized* edges — plus `sibling_triple_id` / +`is_counter_triple` scaffolding for counter-claims. So we are not adding net-new infrastructure. There +are exactly **three gaps**: + +1. **Seed** — nothing writes *real* metadata into `kg.predicates`. The ingestion path lazy-upserts only + `(id, slug, label)` and `ON CONFLICT DO NOTHING`, so even pre-seeded rows never receive our fields. +2. **Populate the new fields** — `polarity`, `objectKind`, `temporalNature`, `specializes`, `contradicts`, + and a real `marketPattern` enum have no home yet (columns or JSONB). +3. **Consume** — nothing reads predicate metadata. No reverse-edge synthesis, no transitive closure, no + roll-up; the recommendation service doesn't read `kg.predicates` at all. The graph's *semantics* are + currently invisible to queries and ranking. + +This doc addresses all three, with the query/performance work concentrated in gap 3. + +--- + +## 2. Current architecture (verified) + +Event-sourced, Rust ingestion → Postgres read models, with a TypeScript API. + +``` +chain events ──> TimescaleDB event_store (hypertable, append-only) + │ (rindexer-ingestion → event_data JSONB) + ▼ + projections (Rust, tokio + sqlx; Coordinator-supervised) + │ CoreEntitiesProjection dual-writes: + ├──> TimescaleDB `term` (denormalized, legacy) + ├──> SurrealDB (legacy, retiring) + └──> KG Postgres (canonical): kg.nodes, kg.predicates, kg.triples, kg.events + │ + embeddings-job ──> search.documents (pgvector) + │ + API (Hono/TS) reads SurrealDB today, KG fallback in progress (ENG-11475) + recommendation-service (Rust/Axum) reads vaults + posts + events (NOT predicates) +``` + +### Verified schema (KG Postgres) + +`kg.predicates` — `backfill_integration.rs:134`: +```sql +id text PK, slug text UNIQUE, label text, description text, +inverse_predicate_id text, +is_transitive boolean NOT NULL DEFAULT false, +is_symmetric boolean NOT NULL DEFAULT false, +is_hierarchical boolean NOT NULL DEFAULT false, +is_social boolean NOT NULL DEFAULT false, -- backend-only, not in our spec +is_market boolean NOT NULL DEFAULT false, -- backend-only, not in our spec +metadata jsonb NOT NULL DEFAULT '{}', +created_at, updated_at +``` + +`kg.triples` — `backfill_integration.rs:151`: +```sql +id text PK, status text DEFAULT 'active', visibility text DEFAULT 'public', created_by text, +subject_id text, subject_type text DEFAULT 'node', +predicate_id text REFERENCES kg.predicates(id), predicate_type text DEFAULT 'node', +object_id text, object_type text DEFAULT 'node', +edge_kind text DEFAULT 'claim', source text, source_uri text, +confidence numeric(6,5), -- ready for synthesized-edge confidence +inferred boolean NOT NULL DEFAULT false, -- ready for synthesized edges +provenance jsonb DEFAULT '{}', metadata jsonb DEFAULT '{}' +-- (+ sibling_triple_id / is_counter_triple per the counter-triple work) +``` + +### Verified write path + +`indexing-services/crates/projections/src/projection/dual/core_entities.rs:485` — +`upsert_kg_predicate_lazy()`: +```sql +INSERT INTO kg.predicates (id, slug, label) +VALUES ($1, $1, $1) +ON CONFLICT (id) DO NOTHING +``` +The chain emits `predicate_id` as a **bytes32 atom hash**; pre-seeded predicates (`pred_follows`, …) keep +their curated `slug`/`label` because `DO NOTHING` short-circuits. **Consequence:** this path can *never* +introduce or update our metadata — seeding must be a separate, slug-keyed upsert that runs `DO UPDATE` +(see §4). Note also a discrepancy to resolve: the code comment says `predicate_id` has **no FK** to +`kg.predicates`, but the fixture declares `REFERENCES kg.predicates(id)`. Confirm which is live before +relying on referential integrity. + +--- + +## 3. Field → backend home + +| Field (spec) | Backend today | Recommendation | +|---|---|---| +| `isSymmetric` | ✅ `is_symmetric` column | reuse as-is | +| `isTransitive` | ✅ `is_transitive` column | reuse as-is | +| `inverse` | ✅ `inverse_predicate_id` column | reuse; store the **predicate atom-id**, mapped from our typed `PredicateKey` at seed time | +| `specializes` | ❌ | add `specializes_predicate_id text` (mirrors `inverse_predicate_id`) | +| `contradicts` | ❌ (but `is_counter_triple`/`sibling_triple_id` scaffolding exists) | add join table `kg.predicate_contradictions(predicate_id, contradicts_id)` — many-to-many, symmetric | +| `polarity` | ❌ | **dedicated column** `polarity text` — it's a hot query/filter axis (reputation, feed) | +| `objectKind` | ❌ | **dedicated column** `object_kind text` — drives validation + render, queried often | +| `temporalNature` | ❌ | `metadata->>'temporalNature'` JSONB first; promote to column if it becomes a query filter | +| `claimType` | ❌ | `metadata->>'claimType'` JSONB (soft-ship; low traffic) | +| `marketPattern` | ⚠️ only `is_market boolean` | add `market_pattern text` (3-value); reconcile/retire the boolean | +| — | `is_hierarchical` exists | our spec **cut** this (derivable). Either derive it at seed (`is_transitive AND inverse_predicate_id IS NOT NULL`) or drop the column | +| — | `is_social` exists | backend-only flag; map from our `category` at seed time or leave to backend | + +**Column vs JSONB heuristic:** promote a field to a real column when it's a *query filter or index target* +(`polarity`, `objectKind`, `market_pattern`); keep rarely-filtered descriptive fields in `metadata` JSONB +(`temporalNature`, `claimType`) until a query needs them. This keeps the hot path indexable without +over-widening the table. + +--- + +## 4. Closing gap 1+2 — the seed path (the critical missing link) + +The package `@0xintuition/predicates` already *generates* `PREDICATE_SPECS`. Extend that generation to +emit a **seed artifact** the backend consumes, keyed by `slug` (stable) — not by the chain atom-hash `id`. + +**Recommended mechanism:** a generated, idempotent seed run as a migration/job: +```sql +INSERT INTO kg.predicates + (id, slug, label, description, is_symmetric, is_transitive, + inverse_predicate_id, specializes_predicate_id, polarity, object_kind, + market_pattern, metadata) +VALUES (...) -- one row per spec +ON CONFLICT (slug) DO UPDATE SET -- ⬅ UPDATE, unlike the lazy path's DO NOTHING + is_symmetric = EXCLUDED.is_symmetric, + ... , + metadata = kg.predicates.metadata || EXCLUDED.metadata, -- merge, don't clobber chain-added keys + updated_at = now(); +``` + +Key design points: +- **Slug-keyed**, so it's stable across redeploys and independent of the chain atom-hash id. +- **`DO UPDATE`** so spec changes propagate (the ingestion path's `DO NOTHING` deliberately won't). +- **JSONB merge** (`||`) so seeded metadata coexists with anything the pipeline writes. +- Resolve `inverse`/`specializes` typed keys → predicate ids/slugs **at generation time** in the package, + where the key graph is validated by `definePredicateRecord`. The backend stores resolved references. +- Ordering: seed must run **before or alongside** ingestion so lazy-upserted rows (id-keyed) and seeded + rows (slug-keyed) reconcile. Confirm the id↔slug mapping (the bytes32 atom hash ↔ `pred_follows`) is + available at seed time; if not, that mapping table is a prerequisite. + +This makes the package the single source of truth and the backend a projection of it — the right direction. + +--- + +## 5. Closing gap 3 — query optimization & performance (the payoff) + +### 5.1 Reverse-edge synthesis (`isSymmetric`, `inverse`) + +Two strategies; choose per access pattern: + +- **Query-time rewrite (recommended default for `isSymmetric`).** To answer "edges touching X," query both + positions: `WHERE subject_id = X OR object_id = X` for symmetric predicates, presenting the reverse view + without storing it. Zero write amplification. Needs an index on `object_id` (see 5.5). +- **Materialization (recommended for hot `inverse` pairs).** A follow-up projector reads + `is_symmetric`/`inverse_predicate_id` and writes the reverse row into `kg.triples` with + **`inferred = true`**, `source = 'synthesis'`, and provenance pointing at the origin triple. The schema's + `inferred` column exists precisely for this. Pros: read-simple, single-position queries. Cons: ~2× rows + for affected predicates, and synthesized rows must be regenerated if the origin is redacted. + +Guidance: start query-time (no migration of data), materialize only predicates proven read-heavy. Never +materialize *and* double-count in reputation — tag inferred edges and exclude them from any aggregate that +already counts the origin. + +### 5.2 Transitive closure (`isTransitive`) + +- **Bounded recursive CTE at query time (recommended first).** Postgres `WITH RECURSIVE` over `kg.triples` + filtered to the transitive predicate, with a **depth cap** (e.g. 8) to bound cost on long ranking chains: + ```sql + WITH RECURSIVE reach AS ( + SELECT object_id, 1 AS depth FROM kg.triples + WHERE subject_id = $1 AND predicate_id = $2 + UNION ALL + SELECT t.object_id, reach.depth + 1 FROM kg.triples t + JOIN reach ON t.subject_id = reach.object_id + WHERE t.predicate_id = $2 AND reach.depth < 8 + ) SELECT DISTINCT object_id FROM reach; + ``` +- **Materialized closure table** (`kg.transitive_closure(predicate_id, ancestor, descendant)`) if a + containment/org query becomes hot. Higher write cost, O(1) reads. Defer until measured. + +### 5.3 Roll-up (`specializes`) + +Cheapest of all: maintain an in-memory predicate-hierarchy map (small, from `kg.predicates`) and **rewrite +the query** to expand a general predicate to its descendants: +`affiliatedWith` → `predicate_id IN (employedBy, alumniOf, memberOf, affiliatedWith)`. One indexed `IN` +scan, no recursion at the data layer (the hierarchy is shallow). This is pure OWL 2 RL and very fast. + +### 5.4 Contradiction detection (`contradicts`) + +The disagreement query: for an entity, find `(subject, object)` pairs asserted with both a predicate and +one of its `contradicts` partners. +```sql +SELECT a.subject_id, a.object_id, a.predicate_id, b.predicate_id +FROM kg.triples a +JOIN kg.predicate_contradictions c ON c.predicate_id = a.predicate_id +JOIN kg.triples b ON b.subject_id = a.subject_id + AND b.object_id = a.object_id + AND b.predicate_id = c.contradicts_id; +``` +This is also where the existing **counter-triple** scaffolding (`is_counter_triple`, `sibling_triple_id`) +connects: a `contradicts`-declared pair *is* the predicate-level rule that the counter-triple feature +expresses at the instance level. Recommend aligning the two so a counter-triple is recognized +automatically when its predicate `contradicts` the sibling's. **OWA caveat:** detect conflict only from +*explicitly asserted* contradictory edges — never infer a negative from absence (foundations doc, Part IV). + +### 5.5 Indexing recommendations + +`kg.triples` currently has only PK + the predicate FK. The query patterns above need: +```sql +CREATE INDEX ON kg.triples (subject_id, predicate_id); -- forward edges / closure seed +CREATE INDEX ON kg.triples (object_id, predicate_id); -- reverse edges / symmetric & inverse views +CREATE INDEX ON kg.triples (predicate_id); -- roll-up IN-scans, per-predicate analytics +-- partial indexes for hot filtered axes once polarity/object_kind are columns: +CREATE INDEX ON kg.triples (subject_id) WHERE inferred = false; -- exclude synthesized in base views +``` +On `kg.predicates`, index `polarity` and `object_kind` only if predicate-level filtering (not just join) +becomes common; the table is tiny (hundreds of rows), so a seq-scan there is usually fine. + +--- + +## 6. Consumer integration + +- **recommendation-service** (`recommendation-service/src/scorers/`): a new scorer (or a `prepare()` + extension) can read `kg.predicates` to (a) reweight by `polarity` — downrank entities carrying + stake-weighted negative edges; (b) follow `isTransitive` trust chains for "trusted-by-the-trusted" + social signal; (c) use `specializes` to widen interest matching. Today it reads only vaults/posts/events, + so this is additive. *(Scorer weights cited by exploration not independently verified.)* +- **API** (`api/src/triples/surreal.ts`, `routes/triples.ts`): extend the hydrated predicate object to + include the new fields, and add a `GET /api/predicates/:id` (none exists today) so clients/the frontend + get the rendering contract (`objectKind`, `polarity`, `temporalNature`) without bundling the package. +- **Validation**: `objectKind` enables an ingestion-time (or API-time) check that `object_type` matches + (`literal` ⇏ a node id; `claim` ⇒ object is a triple id, i.e. `object_type = 'triple'`). The + `object_type` column already distinguishes `'node' | 'triple'`, so `objectKind: 'claim'` maps directly. + +--- + +## 7. Risks & performance characteristics + +- **Write amplification** from materialized synthesis/closure — mitigate by defaulting to query-time and + materializing only measured-hot predicates; always tag `inferred = true` and exclude from aggregates that + count the origin. +- **Double-counting in reputation** — the single biggest correctness trap. Synthesized reverse edges and + rolled-up sub-property edges must not be summed alongside their origins. Make "exclude inferred" the + default in scoring aggregates. +- **Seed/ingest ordering & id↔slug mapping** — the lazy path is id-keyed and `DO NOTHING`; the seed is + slug-keyed and `DO UPDATE`. They must reconcile on the same rows. Verify the atom-hash↔slug mapping + exists at seed time. +- **Fixture-vs-migration drift** — confirm the live `kg.predicates`/`kg.triples` shapes before coding; + resolve the predicate FK discrepancy (§2). +- **OWA** — contradiction and "current state" queries must read only explicit assertions. + +--- + +## 8. Phased rollout (backend) + +1. **Confirm + migrate schema.** Verify live KG shape; add `specializes_predicate_id`, `polarity`, + `object_kind`, `market_pattern` columns, the `kg.predicate_contradictions` table, and the §5.5 indexes. + Reconcile `is_hierarchical`/`is_social`/`is_market` with the spec (derive or drop). +2. **Build the seed.** Generate the slug-keyed `DO UPDATE` seed from `PREDICATE_SPECS`; resolve typed-key + references in-package. Run ahead of ingestion. +3. **Query-time reads.** Ship symmetric/inverse rewrite, `specializes` roll-up expansion, and the bounded + transitive CTE behind the API. No data migration. Measure. +4. **Materialize the hot paths** (inferred reverse edges, closure table) only where step 3 shows pressure. +5. **Consumers.** Expose predicate metadata via API; add the `polarity`/transitive/`specializes` signals to + recommendation; wire `objectKind` validation and align `contradicts` with counter-triples. + +Steps 1–3 are additive and low-risk (dormant columns, query-time reads). The schema already meeting us +halfway is the reason this is mostly *activation*, not construction. + +--- + +## 9. Things to verify before building + +1. Live `kg.predicates` / `kg.triples` migrations vs. the test fixture (esp. whether `is_symmetric` et al. + are actually deployed). +2. The predicate FK question (`kg.triples.predicate_id` → `kg.predicates.id`?). +3. Where pre-seeded predicates (`pred_follows`, …) are currently seeded, and the atom-hash ↔ slug mapping. +4. Whether the API has committed to the KG read path (ENG-11475) before we add a predicate endpoint there + vs. SurrealDB. +5. Recommendation-service internals (scorers/weights) — exploration findings here are unverified. diff --git a/.planning/predicate-semantics.md b/.planning/predicate-semantics.md index 72effbb..2f951ea 100644 --- a/.planning/predicate-semantics.md +++ b/.planning/predicate-semantics.md @@ -1,10 +1,15 @@ # Design: Machine-Readable Predicate Semantics -**Status:** Draft for review +**Status:** Background map — superseded on specifics by the decision record **Author:** Architecture **Date:** 2026-06-30 **Scope:** `packages/predicates/src/types.ts`, `record.ts`, `ipfs.ts`, and the 133 specs under `src/generated/specs/` +> **Reading note.** This is the original full brainstorm — the *map of what's possible*. For the **final, +> pruned field set and the flat schema shape**, `predicate-spec-decisions.md` is canonical and overrides +> §5 below (which still shows the earlier nested sketch and the full unpruned field list). Start with +> `00-START-HERE.md`. + --- ## 1. Motivation @@ -184,7 +189,12 @@ schema.org `domain`/`range` hint for export is deferred. --- -## 5. Proposed schema shape (hybrid nested) +## 5. Proposed schema shape (hybrid nested) — SUPERSEDED + +> ⚠️ **Superseded by `predicate-spec-decisions.md` §4–5.** The team decided **flat, not nested** +> (the serialization target and the entire OWL/RDF tradition are flat), and pruned the field set. +> The nested sketch below is kept only to show the reasoning we moved past. Use the decision record's +> flat `PredicateSpec` as the source of truth. Decision: keep small enum semantic fields flat (they read well and serialize trivially) and group the two field-rich families (`logic`, `ontology`) plus presentation (`grammar`) into nested objects diff --git a/.planning/predicate-spec-decisions.md b/.planning/predicate-spec-decisions.md index 11f7a12..e08180f 100644 --- a/.planning/predicate-spec-decisions.md +++ b/.planning/predicate-spec-decisions.md @@ -56,8 +56,9 @@ The indexer synthesizes the reverse direction instead of forcing users to mint t This is the problem classical fact-graphs never had and the one most worth being cutting-edge on. A belief/trust graph is *valuable precisely because it can represent and price disagreement.* -**Needs:** `contradicts` (trust ⊥ distrust), `isAsymmetric` (one-directional relations). -**Consumer:** reputation/markets (emerging) + frontend (conflict badges). **Verdict: ship `contradicts`; ship `isAsymmetric` as cheap metadata.** +**Needs:** `contradicts` (trust ⊥ distrust). (`isAsymmetric` would also serve here but was deferred — +its unique value is write-time validation, which has no consumer yet.) +**Consumer:** reputation/markets (emerging) + frontend (conflict badges). **Verdict: ship `contradicts`.** ### P4 — Automatic frontend rendering from the predicate alone (the biggest near-term win) > *The UI receives an edge and, without any per-predicate hardcoding, knows: the object of `imgUrl` is an @@ -96,7 +97,7 @@ This is served *for free* by the fields above as long as we serialize them in th | `specializes` | P2 roll-up query + reputation | **Ship** (needs curation) | | `polarity` | P2 reputation, P4 color | **Ship** | | `contradicts` | P3 belief markets | **Ship** (needs curation) | -| `isAsymmetric` | P3 validation; tells indexer "don't mirror" | **Ship** (cheap, derivation-checked) | +| `isAsymmetric` | P3 validation; tells indexer "don't mirror" | **Defer** — see note below; demoted in per-field review | | `objectKind` | P4 render literal/claim/entity differently | **Ship** (highest-value) | | `temporalNature` | P4 freshness badge, P5 lifecycle | **Ship** | | `marketPattern` | existing economic layer | **Keep** | @@ -109,11 +110,17 @@ This is served *for free* by the fields above as long as we serialize them in th | `verifiability` | onChain/offChain | **Cut** — ~correlated with `claimType`; a redundant second axis | | property chains | grandparentOf from parentOf∘parentOf | **Cut for now** — most expensive axiom, no consumer | -**Net change vs the original proposal:** we drop from ~7 logic booleans to **3** (`isSymmetric`, -`isAsymmetric`, `isTransitive`), cut `equivalentTo`/`verifiability`/`isHierarchical`, defer the -functional family and reflexivity, and simplify `claimType`. That is roughly a 40% reduction in surface +**Net change vs the original proposal:** we drop from ~7 logic booleans to **2** (`isSymmetric`, +`isTransitive`), cut `equivalentTo`/`verifiability`/`isHierarchical`, defer the functional family, +reflexivity, **and `isAsymmetric`**, and simplify `claimType`. That is roughly a 45% reduction in surface area — and every survivor names a consumer that exists or is emerging. +> **`isAsymmetric` was demoted to Defer** during the per-field review (`predicate-fields/is-asymmetric.md`, +> score 54). Its only unique value is write-time validation, which has no consumer yet; its "don't mirror +> the edge" signal is already implied by the *absence* of `isSymmetric` plus the presence of `inverse`. It +> graduates alongside `isFunctional` when a validation engine ships. The active algebraic set is therefore +> just `isSymmetric` + `isTransitive`, both with an indexer consumer today. + --- ## 4. Flat vs nested — decision: **flat** @@ -161,8 +168,8 @@ export interface PredicateSpec { // algebraic (OWL-aligned, flat, is* convention) isSymmetric?: boolean; - isAsymmetric?: boolean; isTransitive?: boolean; + // isAsymmetric?: boolean; // deferred — add with the validation engine (see predicate-fields/is-asymmetric.md) // inter-predicate (typed key references — validated in definePredicateRecord) inverse?: PredicateKey; @@ -176,8 +183,9 @@ export interface PredicateSpec { } ``` -`definePredicateRecord` stays the consistency gate: derive implications (`asymmetric ⟹ not symmetric`), -verify inverse pairs mirror each other, and verify `contradicts` is declared symmetrically on both sides. +`definePredicateRecord` stays the consistency gate: derive implications (`isSymmetric ⟹ inverse = self`; +a symmetric predicate cannot also declare a different `inverse`), verify inverse pairs mirror each other's +algebraic properties, and verify `contradicts` is declared symmetrically on both sides. --- From ad420c958f40ce5a1ec1f68443bdba515370c5be Mon Sep 17 00:00:00 2001 From: Kames Date: Wed, 1 Jul 2026 10:14:36 -0700 Subject: [PATCH 4/7] feat: planning docs --- .planning/00-START-HERE.md | 5 + .../predicate-query-optimization-spec.md | 318 ++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 .planning/predicate-query-optimization-spec.md diff --git a/.planning/00-START-HERE.md b/.planning/00-START-HERE.md index 0d5cd68..97abe60 100644 --- a/.planning/00-START-HERE.md +++ b/.planning/00-START-HERE.md @@ -57,6 +57,11 @@ reasoning profile and don't encode inference we can't compute at scale. 7. **`predicate-backend-implementation-plan.md`** — the ticket-ready build plan: 7 workstreams (package → schema → seed → query features → API → consumers → tests), phased P0–P5 with dependencies, acceptance criteria, and rough sizing. Start here when we're ready to execute. +8. **`predicate-query-optimization-spec.md`** — deep dive on the hardest read path: gathering metadata & + resolving identity by traversing connected triples from a root item (the Spotify→Apple Music case). + Predicate registry/parsing, the exact `kg.triples` indexes, the equivalence-class engine (query-time + vs materialized union-find), and the batched gather algorithm. **Headline:** identity/metadata is + moving from embedded JSON-LD (`data_resolved.sameAs`) to triple-native edges; this makes that fast. If you read only one: **`predicate-spec-decisions.md`**. diff --git a/.planning/predicate-query-optimization-spec.md b/.planning/predicate-query-optimization-spec.md new file mode 100644 index 0000000..b0ce95c --- /dev/null +++ b/.planning/predicate-query-optimization-spec.md @@ -0,0 +1,318 @@ +# Spec: Predicate-Aware Query Optimization for Metadata & Identity Traversal + +**Status:** Technical spec — draft for review +**Companion to:** `predicate-backend-implementation-plan.md`, `predicate-backend-integration.md` +**Backend verified:** `alpha/backend` + `@0xintuition/database-kg` (Drizzle), 2026-07-01 +**Focus:** indexing the KG DB, parsing predicates, and enabling connected-triple lookups from a root item + +--- + +## 1. The problem, concretely + +**Scenario (real — it exists today as `workers/scripts/run-kg-spotify-smoke.ts`):** a song atom is created +from a Spotify link. Its `data_resolved` is schema.org JSON-LD: + +```json +{ "@type": "MusicRecording", "name": "Man I Need", "byArtist": "Olivia Dean", + "sameAs": ["https://open.spotify.com/track/1qbmS6ep2hbBRaEZFpn7BX"] } +``` + +Later, someone connects an Apple Music link so users know it's the same song — **via a URL predicate edge** +`(song, url, appleMusicAtom)`. Now "everything we know about this song" is spread across: + +- the node's **own embedded metadata** (`data_resolved`: name, artist, Spotify url), and +- **connected triple edges** (the new Apple Music url; tags; listed-in; reviews; …), and +- possibly **co-referent nodes** declared equivalent (`sameAs`). + +### The architectural shift this forces + +| | Today | Target | +|---|---|---| +| Metadata (url, image, artist) | embedded in `data_resolved` JSONB | **also** first-class triple **edges** | +| Identity (`sameAs`) | a string array inside `data_resolved`; `canonical_url` denormalized onto `search.documents` | **edges** + a resolved **equivalence class** | +| "Find X about this item" | JSON extract (`data_resolved->>'url'`) / exact `canonical_url` match | **traverse connected triples**, predicate-aware | + +The current JSON approach is fast for the single node but **cannot answer graph questions** — "all URLs for +this song across platforms," "everything anyone has asserted about it," "what is this the same as." Moving +to triple-native traversal is what unlocks that; this spec makes it performant. A complete answer must +**union embedded metadata + edges, across the equivalence class**. + +--- + +## 2. Query taxonomy (the read patterns to optimize) + +All over `kg.triples` (subject_id, subject_type, predicate_id, object_id, object_type, status, visibility, +confidence, inferred, metadata) joined to `kg.predicates` and `kg.nodes`. + +| # | Pattern | Shape | Notes | +|---|---|---|---| +| Q1 | **Direct attributes** | `subject_id = R` | outgoing edges of root | +| Q2 | **Incoming edges** | `object_id = R` | who references root | +| Q3 | **Typed multi-value** | `subject_id = R AND predicate_id = url` | *the Spotify+Apple case — two url edges on one node* | +| Q4 | **Identity/co-reference** | closure over `sameAs` (± inverse-functional keys) | the equivalence class of R | +| Q5 | **Cross-class metadata gather** | Q1/Q3 over **all class members** | "everything about this song" | +| Q6 | **Roll-up / reverse / transitive** | expand `specializes`; add `inverse`/`isSymmetric`; `isTransitive` closure | predicate-metadata-driven | + +Q3 is the common, cheap case (multi-valued attribute on one node). Q4/Q5 are the hard, general case +(co-reference). We optimize both, but they need different machinery. + +--- + +## 3. Parsing predicates → the in-memory predicate registry + +"Properly index and parse predicates" = load `kg.predicates` (seeded from the package, per the +implementation plan) into a **typed registry keyed by predicate atom id**, built once at startup and +refreshed on seed change. `kg.predicates` is tiny (hundreds of rows), read on every traversal → cache it; +never join it per-query on the hot path. + +The registry classifies each predicate into **roles** derived from our fields: + +```ts +interface PredicateRole { + id: string; slug: string; + objectKind: 'entity' | 'claim' | 'literal'; // how to handle the object during gather + isMetadataLiteral: boolean; // objectKind === 'literal' → collect value + isEquivalence: boolean; // sameAs/equivalentTo → symmetric+transitive identity + isIdentityKey: boolean; // isInverseFunctional → shared object ⇒ same subject + isSymmetric: boolean; + isTransitive: boolean; + inverseId?: string; // reverse-direction predicate + specializesId?: string; // parent predicate (roll-up) + polarity?: 'positive'|'negative'|'neutral'; + temporalNature?: 'permanent'|'state'|'event'; +} +``` + +Fast lookup structures the query builder consults: + +- `HashMap` — the registry. +- `HashSet` per role: `EQUIVALENCE_PREDS`, `IDENTITY_KEY_PREDS`, `METADATA_LITERAL_PREDS`, + `SYMMETRIC_PREDS`. +- `HashMap>` `SPECIALIZES_CHILDREN` (parent → descendants) for roll-up expansion. +- `HashMap` `INVERSE_OF`. + +This registry is the "parsed predicate" layer. Everything downstream (gather, equivalence, rewrite) is a +function of it — so adding a predicate or changing a field is a data change, not a code change. + +> **Atom parsing note:** the atom parse/enrich workers (`kg-parse-worker`, `kg-enrichment-worker`) that +> populate `data_resolved` should *also* be able to **emit edges from JSON-LD properties** (`url`, `sameAs`, +> `byArtist`) so embedded metadata becomes queryable triples. That's the ingestion half of the shift in §1; +> this spec assumes it and focuses on read/index. + +--- + +## 4. Indexing `kg.triples` (the core of "update our indexing database") + +Today `kg.triples` has only PK + the predicate FK → every traversal is a seq scan. Required indexes, +tuned to §2 and designed for **index-only scans** (INCLUDE the columns the gather needs so Postgres skips +the heap): + +```sql +-- Q1, Q3, Q5 forward: attributes of a node/class +CREATE INDEX ix_triples_subject ON kg.triples + (subject_id, predicate_id) INCLUDE (object_id, object_type, confidence) + WHERE status = 'active' AND inferred = false; + +-- Q2 reverse + symmetric/inverse views +CREATE INDEX ix_triples_object ON kg.triples + (object_id, predicate_id) INCLUDE (subject_id, subject_type) + WHERE status = 'active' AND inferred = false; + +-- Q6 roll-up IN-scans / per-predicate analytics +CREATE INDEX ix_triples_predicate ON kg.triples (predicate_id, subject_id); + +-- Identity-key lookup ("which subjects share this url object?") — Q4 via IFP +CREATE INDEX ix_triples_object_predicate ON kg.triples + (object_id, predicate_id) INCLUDE (subject_id) + WHERE status = 'active'; -- (broader than base view; identity spans states) + +-- Equivalence edges: small, hot, stable predicate set +CREATE INDEX ix_triples_equivalence ON kg.triples (subject_id, object_id) + WHERE predicate_id = ANY () AND status = 'active'; +``` + +Design rationale: + +- **Partial `WHERE status='active' AND inferred=false`** keeps the base-view indexes small and makes + "exclude synthesized/rolled-up edges" the default (no double-counting). +- **`INCLUDE` covering columns** → index-only scans for gather; the object value fetch is a separate + batched `kg.nodes` lookup (§6), not a per-row heap hit. +- The **equivalence partial index** is pinned to the known equivalence predicate ids from the seed, so + `sameAs` traversal never scans unrelated edges. +- Reassess after load: for very hot single predicates (`url`, `hasTag`) a dedicated partial index per + predicate id can beat the shared one. + +Also index the **object value path** used for URL identity on `kg.nodes`: +```sql +CREATE INDEX ix_nodes_url ON kg.nodes ((data_resolved->>'url')); -- existing JSON path lookups +CREATE INDEX ix_nodes_search_text ON kg.nodes USING gin (to_tsvector('simple', search_text)); +``` + +--- + +## 5. Identity / co-reference resolution engine (Q4/Q5) + +The equivalence class of a node is the connected component under **equivalence predicates** (`sameAs`, +`equivalentTo` — symmetric + transitive) plus **identity keys** (any predicate flagged +`isInverseFunctional`: two subjects with the same object are the same entity). + +> In the Spotify+Apple example the two URLs *differ*, so the IFP-on-url rule does **not** auto-merge them — +> that case is either a **multi-valued attribute** on one node (Q3, cheap) or an **explicit `sameAs`** edge +> between two representation nodes (Q4). IFP auto-merge fires when two nodes genuinely share one url object. + +Two implementations: + +### 5A. Query-time (recursive CTE) — good for small classes, zero maintenance +```sql +WITH RECURSIVE class AS ( + SELECT $1::text AS node_id + UNION + SELECT CASE WHEN t.subject_id = c.node_id THEN t.object_id ELSE t.subject_id END + FROM class c + JOIN kg.triples t + ON (t.subject_id = c.node_id OR t.object_id = c.node_id) + AND t.predicate_id = ANY () + AND t.status = 'active' +) SELECT node_id FROM class; -- UNION dedups / terminates cycles +``` +Cost grows with class size and is paid on **every read**. Fine at low scale. + +### 5B. Materialized canonical id (recommended) — O(1) class lookup, incremental maintenance +New read model + projector: +```sql +CREATE TABLE kg.node_equivalence ( + node_id text PRIMARY KEY, + canonical_id text NOT NULL, + class_version bigint NOT NULL DEFAULT 0 +); +CREATE INDEX ix_node_equiv_canonical ON kg.node_equivalence (canonical_id); +``` +- A new **`EquivalenceProjection`** (indexing-services, alongside `core_entities`) runs a **union-find**: + on each `sameAs`/`equivalentTo` triple (and each IFP-key collision), `union(a, b)`; the canonical id is + deterministic (e.g. lowest atom id, or the earliest-created node) so results are stable and replay-safe. +- Class lookup = `SELECT canonical_id FROM kg.node_equivalence WHERE node_id = $1` then all members via the + `canonical_id` index. No recursion on read. +- **Merges:** joining two classes re-points the smaller class's members (path-compression); bump + `class_version` to invalidate caches (§7). +- **Splits (retraction):** redacting a `sameAs` can split a component — expensive and rare. Handle by + marking the class dirty and recomputing that component (bounded BFS), not the whole table. + +**Recommendation:** ship **5A first** (no migration, correct), add **5B** once class reads are hot or +classes grow. Same interface (`equivalenceClass(nodeId) → id[]`) so the query builder doesn't change. + +--- + +## 6. Predicate-aware metadata gather (Q5) — the algorithm + +``` +gatherMetadata(rootId, { hops = 1, includeInferred = false }): + classIds = equivalenceClass(rootId) // §5 (5A or 5B) + + edges = SELECT subject_id, predicate_id, object_id, object_type, confidence + FROM kg.triples + WHERE subject_id = ANY(classIds) + AND status='active' AND visibility='public' + AND (includeInferred OR inferred=false) // uses ix_triples_subject + + result = {} // slug -> value[] + objectIds = [] + for e in edges: + role = registry[e.predicate_id] // §3, no DB hit + switch role.objectKind: + 'literal': objectIds.push(e.object_id) // resolve value in batch below + result[role.slug] += { ref: e.object_id, kind:'literal' } + 'entity' : result[role.slug] += { ref: e.object_id, kind:'entity' } // recurse if hops>1 + 'claim' : result[role.slug] += { ref: e.object_id, kind:'claim' } + + // one batched fetch — avoids N+1 + values = SELECT id, data_resolved, classification_type + FROM kg.nodes WHERE id = ANY(objectIds) + splice resolved values into result[*].kind=='literal' + + // merge each class member's OWN embedded data_resolved (§1 completeness) + embedded = SELECT id, data_resolved FROM kg.nodes WHERE id = ANY(classIds) + merge embedded JSON-LD (url, sameAs, byArtist, …) into result, deduped + + // optional Q6: reverse/inverse edges, roll-up expansion (registry-driven) + return result +``` + +Key properties: **two batched queries** (edges by class; object values) + one embedded-data fetch — no +per-edge round trips; predicate handling comes from the in-memory registry, not joins; inferred excluded by +default so nothing is double-counted; embedded JSON-LD and edges are unioned for completeness. + +--- + +## 7. Read model, caching, guardrails + +- **Optional metadata projection** `kg.node_metadata(canonical_id PK, resolved jsonb, class_version)` — + precomputes §6 output per class for "give me the card for this item" in one read. Rebuilt by a projector + on relevant writes. Trade write cost for read latency; add only when gather is measured hot. +- **Cache** `canonical_id → resolved` in the read service, keyed by `class_version`; a class merge/edge + write bumps the version → natural invalidation. +- **Guardrails:** + - **Class-size cap** (e.g. 10k members) with logged truncation — a bad `sameAs` chain shouldn't fan out + unbounded. Never silently truncate without a signal. + - **Cycle-safe** equivalence (UNION / visited set). + - **Depth cap** on entity recursion (`hops`) and on transitive closure (§ integration doc, ~8). + - **OWA**: gather reads only explicit, active assertions. + +--- + +## 8. Pipeline & code changes + +| Area | Change | Where | +|---|---|---| +| Schema | §4 indexes; `kg.node_equivalence` (5B); optional `kg.node_metadata` | KG migrations (per impl-plan P0) | +| Predicate registry | load seed → in-memory role registry + lookup sets; refresh on change | KG read layer (`@0xintuition/database-kg`) + recommendation-service | +| Atom parse | emit edges from JSON-LD props (`url`, `sameAs`, `byArtist`) so embedded metadata becomes triples | `kg-parse-worker` / enrichment workers | +| Equivalence | `EquivalenceProjection` (union-find → `kg.node_equivalence`) | `indexing-services/crates/projections` | +| Metadata | (optional) `MetadataProjection` → `kg.node_metadata` | same | +| Query builder | `equivalenceClass()`, `gatherMetadata()`, roll-up/inverse rewrite — Drizzle | `@0xintuition/database-kg/actions` | +| API | expose gather + typed-neighbor endpoints; `includeInferred` toggle | `api/src/clients/kg-*.ts`, routes | + +All projectors idempotent + replay-safe (`ON CONFLICT`), matching the existing `core_entities` pattern, and +graceful no-op when `DATABASE_KG_URL` is absent. + +--- + +## 9. Performance expectations + +| Query | Without spec | With spec | +|---|---|---| +| Q1/Q3 attributes | seq scan O(triples) | index-only scan O(deg(node)) via `ix_triples_subject` | +| Q2 incoming | seq scan | `ix_triples_object` | +| Q4 class | recursive seq scans | 5A bounded CTE on `ix_triples_equivalence`; 5B O(1) lookup | +| Q5 gather | N+1 explosion | 2 batched index scans + registry (no per-edge joins) | +| Q6 roll-up | enumerate sub-preds by hand | registry expansion → single `predicate_id = ANY(...)` on `ix_triples_predicate` | + +Target: single-item metadata card in **one or two index-only scans + one batched node fetch**, independent +of total graph size; class resolution O(1) under 5B. + +--- + +## 10. Deliverables & acceptance criteria + +1. **Indexes (§4)** applied; `EXPLAIN` shows index-only scans for Q1–Q3, no seq scan on `kg.triples`. +2. **Predicate registry (§3)** loaded from seed; role lookups are in-memory (0 DB hits on the hot path). +3. **Equivalence (§5)** — `equivalenceClass(spotifySong)` returns the Apple Music co-referent(s); correct + under cycles; stable canonical id. +4. **Gather (§6)** — `gatherMetadata(song)` returns *both* platform URLs and unions embedded + edge + metadata in ≤ 2 batched queries; excludes inferred by default; no double-count. +5. **Guardrails (§7)** — class-size cap logs truncation; caches invalidate on `class_version` bump. +6. **Bench** — p95 for the single-item card under target on a graph ≥ 10× current size. +7. Reuse/extend the **Spotify smoke test** as the end-to-end fixture. + +--- + +## 11. Open questions + +1. Do we **migrate** embedded `data_resolved` metadata (url/sameAs) into edges, or keep both and union at + read (§6)? (Union-at-read is lower-risk; edge emission can backfill over time.) +2. Which predicates are **identity keys** (`isInverseFunctional`)? `url` per-platform is *not* globally + unique (same song, many urls) — so IFP likely applies to canonical/isrc-style ids, not raw platform urls. + This decides how much auto-merge is safe (smushing risk). +3. Materialize now (5B / `kg.node_metadata`) or start query-time (5A) and promote on measured pressure? +4. Retraction/split policy for `sameAs` — recompute-on-dirty vs periodic full rebuild. +5. Does gather cross **visibility/creator** boundaries, or only the requester's trusted set? (Affects + whether class expansion is global or scoped.) From f6b29d05eb4a5fe55f981b6165c2bd489814139e Mon Sep 17 00:00:00 2001 From: Kames Date: Thu, 2 Jul 2026 12:16:30 -0700 Subject: [PATCH 5/7] checkpoint: update predicates --- .planning/00-START-HERE.md | 22 +- .planning/forum-post-intro.md | 158 ++++++++++ .../predicate-backend-implementation-plan.md | 27 +- .planning/predicate-backend-integration.md | 23 +- .planning/predicate-fields/README.md | 17 +- .planning/predicate-fields/contradicts.md | 27 +- .planning/predicate-fields/inverse.md | 11 + .planning/predicate-fields/is-asymmetric.md | 12 +- .planning/predicate-fields/is-symmetric.md | 17 +- .planning/predicate-fields/is-transitive.md | 19 ++ .planning/predicate-fields/literal-type.md | 62 ++++ .planning/predicate-fields/object-kind.md | 13 + .planning/predicate-fields/polarity.md | 13 + .planning/predicate-fields/specializes.md | 11 +- .planning/predicate-fields/superseded-by.md | 58 ++++ .../predicate-query-optimization-spec.md | 19 +- .planning/predicate-spec-audit.md | 288 ++++++++++++++++++ .planning/predicate-spec-decisions.md | 107 ++++++- .../predicates/src/generated/specs/actor.ts | 3 + .../src/generated/specs/affiliatedWith.ts | 3 + .../src/generated/specs/agreeWith.ts | 5 + .../src/generated/specs/alternativeTo.ts | 3 + .../src/generated/specs/alumniOf.ts | 4 + .../src/generated/specs/areaServed.ts | 3 + .../src/generated/specs/attestedBy.ts | 3 + .../src/generated/specs/auditedBy.ts | 3 + .../src/generated/specs/authoredBy.ts | 3 + .../src/generated/specs/availableOn.ts | 3 + .../src/generated/specs/backedBy.ts | 4 + .../src/generated/specs/bearishOn.ts | 5 + .../src/generated/specs/betterThan.ts | 4 + .../predicates/src/generated/specs/blocked.ts | 4 + .../src/generated/specs/branchOf.ts | 4 + .../predicates/src/generated/specs/brand.ts | 3 + .../src/generated/specs/bullishOn.ts | 5 + .../src/generated/specs/byArtist.ts | 3 + .../src/generated/specs/certifiedBy.ts | 3 + .../predicates/src/generated/specs/citedBy.ts | 4 + .../src/generated/specs/compatibleWith.ts | 3 + .../src/generated/specs/competeWith.ts | 3 + .../src/generated/specs/compliantWith.ts | 3 + .../src/generated/specs/confirmedBy.ts | 3 + .../predicates/src/generated/specs/contain.ts | 4 + .../src/generated/specs/containedInPlace.ts | 5 + .../src/generated/specs/containsPlace.ts | 4 + .../src/generated/specs/contributedTo.ts | 3 + .../src/generated/specs/createdBy.ts | 3 + .../src/generated/specs/curatedBy.ts | 3 + .../src/generated/specs/delegatedTo.ts | 3 + .../src/generated/specs/dependOn.ts | 3 + .../src/generated/specs/deprecatedBy.ts | 3 + .../src/generated/specs/derivedFrom.ts | 3 + .../src/generated/specs/director.ts | 3 + .../src/generated/specs/disagreeWith.ts | 5 + .../src/generated/specs/disputedBy.ts | 3 + .../src/generated/specs/distrust.ts | 5 + .../src/generated/specs/employedBy.ts | 4 + .../src/generated/specs/enabledBy.ts | 3 + .../predicates/src/generated/specs/endorse.ts | 5 + .../src/generated/specs/equivalentTo.ts | 3 + .../src/generated/specs/evidencedBy.ts | 3 + .../src/generated/specs/expertIn.ts | 3 + .../src/generated/specs/featuredIn.ts | 3 + .../predicates/src/generated/specs/follow.ts | 3 + .../src/generated/specs/followedBy.ts | 4 + .../src/generated/specs/forkedFrom.ts | 3 + .../predicates/src/generated/specs/founded.ts | 5 + .../predicates/src/generated/specs/founder.ts | 5 + .../src/generated/specs/governedBy.ts | 3 + .../src/generated/specs/hasCategory.ts | 3 + .../src/generated/specs/hasDescription.ts | 4 + .../src/generated/specs/hasSource.ts | 2 + .../predicates/src/generated/specs/hasTag.ts | 3 + .../predicates/src/generated/specs/hasType.ts | 3 + .../src/generated/specs/hiringOrganization.ts | 3 + .../predicates/src/generated/specs/imgUrl.ts | 4 + .../src/generated/specs/implement.ts | 3 + .../predicates/src/generated/specs/inAlbum.ts | 3 + .../src/generated/specs/inPlaylist.ts | 3 + .../src/generated/specs/inspiredBy.ts | 3 + .../src/generated/specs/investedIn.ts | 3 + .../src/generated/specs/itemReviewed.ts | 3 + .../src/generated/specs/jobLocation.ts | 3 + .../src/generated/specs/learnedFrom.ts | 3 + .../predicates/src/generated/specs/like.ts | 4 + .../src/generated/specs/linkedAccount.ts | 3 + .../src/generated/specs/listedIn.ts | 4 + .../src/generated/specs/listedOn.ts | 3 + .../src/generated/specs/locatedIn.ts | 3 + .../src/generated/specs/manufacturer.ts | 3 + .../src/generated/specs/memberOf.ts | 4 + .../src/generated/specs/mentorOf.ts | 4 + .../predicates/src/generated/specs/musicBy.ts | 3 + .../src/generated/specs/musicGroupMember.ts | 3 + .../src/generated/specs/neutralOn.ts | 4 + .../predicates/src/generated/specs/oppose.ts | 5 + .../src/generated/specs/organizer.ts | 3 + .../src/generated/specs/outperform.ts | 3 + .../src/generated/specs/parentItem.ts | 3 + .../src/generated/specs/parentOrganization.ts | 4 + .../src/generated/specs/partOfSeries.ts | 3 + .../src/generated/specs/partnerOf.ts | 4 + .../src/generated/specs/peggedTo.ts | 3 + .../src/generated/specs/performer.ts | 3 + .../predicates/src/generated/specs/photo.ts | 4 + .../src/generated/specs/pinnedIn.ts | 3 + .../src/generated/specs/precededBy.ts | 4 + .../src/generated/specs/predecessorOf.ts | 4 + .../src/generated/specs/pricedIn.ts | 3 + .../src/generated/specs/primaryImageOfPage.ts | 4 + .../src/generated/specs/productionCompany.ts | 3 + .../src/generated/specs/proposed.ts | 3 + .../src/generated/specs/provider.ts | 3 + .../src/generated/specs/publishedAt.ts | 3 + .../src/generated/specs/publisher.ts | 3 + .../src/generated/specs/rankedAbove.ts | 3 + .../src/generated/specs/recommend.ts | 4 + .../src/generated/specs/reference.ts | 4 + .../src/generated/specs/regulatedBy.ts | 3 + .../src/generated/specs/replacedBy.ts | 3 + .../src/generated/specs/reported.ts | 4 + .../src/generated/specs/reviewed.ts | 4 + .../src/generated/specs/reviewedBy.ts | 4 + .../predicates/src/generated/specs/reward.ts | 4 + .../predicates/src/generated/specs/sameAs.ts | 3 + .../src/generated/specs/skepticalOf.ts | 4 + .../src/generated/specs/softwareAddOn.ts | 3 + .../predicates/src/generated/specs/speak.ts | 3 + .../src/generated/specs/sponsoredBy.ts | 3 + .../src/generated/specs/stakedIn.ts | 3 + .../src/generated/specs/studentOf.ts | 5 + .../predicates/src/generated/specs/studied.ts | 3 + .../src/generated/specs/subEvent.ts | 4 + .../src/generated/specs/subOrganization.ts | 4 + .../src/generated/specs/successorOf.ts | 4 + .../src/generated/specs/superEvent.ts | 4 + .../src/generated/specs/supersede.ts | 3 + .../predicates/src/generated/specs/support.ts | 5 + .../src/generated/specs/targetProduct.ts | 3 + .../predicates/src/generated/specs/teach.ts | 3 + .../predicates/src/generated/specs/track.ts | 3 + .../predicates/src/generated/specs/trailer.ts | 3 + .../src/generated/specs/triggered.ts | 3 + .../predicates/src/generated/specs/trust.ts | 5 + .../predicates/src/generated/specs/url.ts | 4 + .../predicates/src/generated/specs/use.ts | 3 + .../src/generated/specs/verifiedBy.ts | 3 + .../src/generated/specs/votedAgainst.ts | 5 + .../src/generated/specs/votedFor.ts | 5 + .../src/generated/specs/vouchFor.ts | 5 + .../src/generated/specs/worseThan.ts | 4 + packages/predicates/src/index.ts | 1 + packages/predicates/src/ipfs.ts | 21 ++ packages/predicates/src/record.ts | 56 ++++ packages/predicates/src/types.ts | 75 ++++- packages/predicates/src/validate.test.ts | 50 +++ packages/predicates/src/validate.ts | 170 +++++++++++ 157 files changed, 1700 insertions(+), 43 deletions(-) create mode 100644 .planning/forum-post-intro.md create mode 100644 .planning/predicate-fields/literal-type.md create mode 100644 .planning/predicate-fields/superseded-by.md create mode 100644 .planning/predicate-spec-audit.md create mode 100644 packages/predicates/src/validate.test.ts create mode 100644 packages/predicates/src/validate.ts diff --git a/.planning/00-START-HERE.md b/.planning/00-START-HERE.md index 97abe60..44ecbc3 100644 --- a/.planning/00-START-HERE.md +++ b/.planning/00-START-HERE.md @@ -11,20 +11,22 @@ research, pruned hard to stay simple. ## TL;DR — what we're proposing -Add **8 new fields** to `PredicateSpec` (flat, all optional, additive/non-breaking), plus one soft-ship -(`claimType`), plus the existing `marketPattern`: +Add **9 new fields** to `PredicateSpec` (flat, all optional, additive/non-breaking), plus two soft-ships +(`claimType`, `supersededBy`), plus the existing `marketPattern`: | Field | Type | What it unlocks | |---|---|---| | `objectKind` | `entity \| claim \| literal` | frontend renders any edge with no per-predicate code; makes reification visible | +| `literalType` *(audit)* | `url \| image \| date \| number \| text` | completes the rendering contract for literals (RDF typed-literals lesson) | | `polarity` | `positive \| negative \| neutral` | one signed reputation score across all predicates; sentiment-colored UI | -| `inverse` | `PredicateKey` | store one edge, serve both directions (different reverse predicate) | -| `isSymmetric` | `boolean` | store one edge, serve both directions (same predicate) | +| `inverse` | `PredicateKey` | store one edge, serve both directions (different reverse predicate) — with a **canonical direction** per pair | +| `isSymmetric` | `boolean` | store one edge, serve both directions (same predicate) — with **mint-time canonical ordering** | | `temporalNature` | `permanent \| state \| event` | freshness model — which edges rot, which are forever | -| `contradicts` | `PredicateKey[]` | detect & price disagreement (trust ⊥ distrust) — the differentiator | -| `isTransitive` | `boolean` | closure/reachability queries (containment trees, org charts) | -| `specializes` | `PredicateKey` | roll-up queries & reputation (employedBy ⊑ affiliatedWith) | +| `contradicts` | `PredicateKey[]` | detect & price disagreement (trust ⊥ distrust, **same subject/object pair**) — the differentiator | +| `isTransitive` | `boolean` | closure/reachability queries (containment trees, org charts) — never sentiment | +| `specializes` | `readonly PredicateKey[]` | roll-up queries & reputation (employedBy ⊑ affiliatedWith); DAG, not tree | | `claimType` *(soft-ship)* | `factual \| evaluative` | fact-vs-opinion market & UI semantics | +| `supersededBy` *(soft-ship, audit)* | `PredicateKey` | deprecation forwarding — pickers redirect, old edges stay legible | | `marketPattern` *(exists)* | `depositional \| attributive \| comparative` | per-predicate market mechanics | **Deliberately deferred or cut** (no consumer yet / redundant): `isAsymmetric`, `isFunctional`, @@ -42,6 +44,12 @@ reasoning profile and don't encode inference we can't compute at scale. 1. **`00-START-HERE.md`** (this file) — the summary and reading order. 2. **`predicate-spec-decisions.md`** — ⭐ *canonical.* The pruned field set, flat-vs-nested decision, the final `PredicateSpec`, pros/cons, and what we're explicitly not doing. + ✅ Read together with **`predicate-spec-audit.md`** — the adversarial review pass, **all findings now + folded into the docs** (2026-07-01). It confirmed the overall shape and drove: mint-time + canonicalization for symmetric/inverse triples (§5.2 — the blocking economic fix), the precise + pair-level definition of `contradicts` (P3), the `literalType` + `supersededBy` fields, `specializes` + widened to an array, the complete 14-rule validation set (§5.1), and the normative OWL/RDF + serialization table (§5.3). 3. **`predicate-fields/`** — one document per field with multiple use cases and a confidence score (see `predicate-fields/README.md` for the rubric and the score table). This is where each field earns its place; the scoring overturned one earlier decision (`isAsymmetric`). diff --git a/.planning/forum-post-intro.md b/.planning/forum-post-intro.md new file mode 100644 index 0000000..fc4a770 --- /dev/null +++ b/.planning/forum-post-intro.md @@ -0,0 +1,158 @@ +# Build the knowledge graph off-chain first: an intro to the Intuition packages (and where they're heading) + +*Draft forum post — for the Intuition community forum. Written developer-to-developer.* + +--- + +Hey everyone 👋 + +I want to introduce something we've been building that I think changes how you'll build on Intuition: a +set of focused TypeScript packages that let you **model the entire knowledge graph off-chain first**, for +free, and only settle on-chain when it actually matters. + +This post is a proper introduction — what the packages are, the concepts behind them, how to think about +them — plus a peek at where we're taking them next. It's long-ish because there's a real mental model here, +but each piece is simple on its own. Grab a coffee. + +## The shift: off-chain first + +If you've built on-chain before, you know the tax: **every piece of data costs a transaction.** Put a fact +on the graph? Pay. Connect two things? Pay again. That model quietly kills experimentation — you don't +prototype freely when every idea has a gas bill attached. + +The packages flip this. You can define atoms, wire up relationships, compute IDs, build whole creation +flows and UIs — **entirely off-chain, entirely free** — and bring things on-chain only when they're worth +settling. The official quickstart is dry-run by design: no wallet, no API key, no live submission needed. + +```bash +cd examples/hackathon-quickstart +bun run dev +``` + +Off-chain to move fast. On-chain when it counts. Everything below serves that idea. + +## The core concepts (the whole graph in three words) + +Intuition's knowledge graph is built from a tiny, composable vocabulary: + +- **Atoms** — the nouns. A person, a song, an org, a URL, a concept. Anything you can point at. +- **Predicates** — the verbs / relationships. `member of`, `authored by`, `same as`, `trusts`. +- **Triples** — a statement made of the two: **subject → predicate → object.** + `Alice → member of → Acme`. That's an edge in the graph. + +Two more ideas that matter a lot: + +- **Deterministic IDs.** Every atom, predicate, and triple has a **content-addressable ID** you can compute + *before* anything touches a chain. Same content → same ID, everywhere, forever. This is the quiet + superpower: your off-chain graph and the eventual on-chain graph speak the same identifiers, so there's no + "import/sync" cliff — you're always building the real thing. +- **`sameAs` identity.** Different atoms can point at the same real-world thing (the same song on Spotify + and Apple Music, the same person across two profiles). `sameAs` links them so the graph can treat them as + one identity without forcing everyone to agree on a single canonical atom up front. + +If you internalize atoms / predicates / triples / deterministic IDs / sameAs, you understand Intuition. + +## The packages, by layer + +We split everything into small, ESM-only packages so you install **only the layer you need** while +converging on shared vocabulary and IDs. Here's the map and how to think about each: + +### 🧩 Semantic / data modeling — *what things are and how they relate* + +- **`@0xintuition/schema-org`** — a pinned [schema.org](https://schema.org) vocabulary (types + properties, + with inherited-property lookup). This is the shared dictionary so a "MusicRecording" means the same thing + in your app and mine. +- **`@0xintuition/classifications`** — Intuition classification specs and **Creation Profiles**: the + recommended fields for a given kind of thing, plus the relationships that make sense for it. This is what + lets you render a correct "create a song" form without hardcoding it. +- **`@0xintuition/predicates`** — the canonical **predicate registry**: the blessed set of relationships, + their deterministic IDs, display helpers, and which predicates suit which entity types. When you need a + relationship, you reach here. + +### 🔑 Identity — *stable names for everything* + +- **`@0xintuition/ids`** — deterministic, content-addressable IDs for atoms, predicates, triples, + counter-triples, and OAuth atoms. The backbone that makes off-chain-first coherent. + +### 🛠️ Builder helpers — *the ergonomic layer* + +- **`@0xintuition/primitives`** — high-level builders and validators for atoms, claims, triples, and + discovery. Instead of assembling raw structures, you call `buildTripleByName(...)` and get a validated + result. This is where most app code lives. + +### ⚙️ Protocol plumbing — *when you're ready to go on-chain* + +- **`@0xintuition/protocol`** — core contract ABIs, event parsers, interaction helpers. +- **`@0xintuition/deployments`** — typed chain metadata and deployment addresses. +- **`@0xintuition/periphery`** — periphery ABIs and bridge/router helpers. +- **`@0xintuition/curves`** — bonding-curve math for the market/staking side. +- **`@0xintuition/react`** — React hooks for the core protocol interactions. + +The beauty of the layering: you can live entirely in the top three layers while prototyping, and only pull +in the protocol layer when your app owns wallet + submission flow. + +## How they work together (a tiny walkthrough) + +Say you're building a music app. Start from a Creation Profile: + +```ts +import { musicRecordingCreationProfile } from '@0xintuition/classifications/creation/music-recording' + +const fields = musicRecordingCreationProfile.fields // render your atom form +const relationships = musicRecordingCreationProfile.relationships // suggest valid triples +// e.g. MusicRecording -> inPlaylist -> MusicPlaylist +``` + +Use `fields` to render inputs, `relationships` to suggest predicates, `primitives` to build and validate +the triple, and `ids` to know its ID before you ever sign. Your UI is generated **from the package data** +instead of an app-local shadow copy that drifts. When (and if) you're ready, the protocol layer settles it +on-chain — with the exact same IDs you've been using all along. + +That's the loop: **model → validate → (optionally) settle**, with deterministic identity the whole way. + +## Where this is heading: predicates that describe themselves + +Here's the part I'm most excited to build *with you*. + +Right now a predicate's meaning mostly lives in its name and description — great for humans, opaque to +machines. We're exploring giving each predicate a little **machine-readable metadata about how the +relationship behaves**, so the graph can actually reason: + +- **Symmetric?** `Acme → partner of → BigCo` implies the reverse automatically. +- **Has an inverse?** `parent org` ⇄ `subsidiary of` — assert one, get the other. +- **Transitive?** venue in district in city ⟹ venue in city, as one query. +- **Carries sentiment?** `endorse` is positive, `distrust` negative — so reputation becomes something the + graph computes instead of every app hardcoding it. +- **What's on the other end?** entity vs. raw value vs. *another claim* — one field that lets a frontend + render any edge with zero custom code. + +Circle back to `sameAs`: imagine a song created from a Spotify link, and later someone connects the Apple +Music version **as a real edge**. Now "give me every link for this track," "what does the community know +about it," and "are these two items secretly the same thing?" all become answerable by walking connected +triples. That last one — knowing two nodes are the same reality — is exactly what machine-readable +predicates unlock, and it's everywhere: identity, products, tokens, provenance. + +This is grounded in the same ideas 30 years of knowledge-graph work (RDF, OWL, schema.org) landed on — we're +adapting the proven parts and cutting the over-engineered ones. And the vocabulary isn't settled, which is +why I'm posting now. + +## Come build with us + +An open invitation: + +- 🧪 **Prototype something.** Everything's on npm (`@alpha`) and runs off-chain out of the box. Start from + the quickstart, define some atoms and triples, break things, show us. +- 💬 **Bring your use case.** Music, identity, DeFi, social, science, provenance — every domain has + relationships that want to be first-class. Tell us yours and we'll model the predicates together. +- ❓ **Ask hard questions.** Especially "wait, how would this handle…" — those are how we find the gaps. +- 🔧 **Argue the design.** Are `symmetric` / `transitive` / `inverse` the right words or too academic? What + relationship properties are we missing? Where's the line between expressive and over-complicated? Reply and + push on it. + +The dream is a knowledge graph you can build for free, that renders itself, queries across relationships +intelligently, knows when two things are the same, and can even represent disagreement — settled on-chain +only when it matters. I think we can get there, and it's more fun to figure out with you. + +What would you build first? 👇 + +— *[your name]* diff --git a/.planning/predicate-backend-implementation-plan.md b/.planning/predicate-backend-implementation-plan.md index 8d61956..4abf06f 100644 --- a/.planning/predicate-backend-implementation-plan.md +++ b/.planning/predicate-backend-implementation-plan.md @@ -52,13 +52,15 @@ consumers, with sequencing, dependencies, and acceptance criteria. | ID | Task | Files | Size | |---|---|---|---| -| A1 | Add fields to `PredicateSpec` (flat): `polarity`, `objectKind`, `temporalNature`, `claimType`, `isSymmetric` (exists), `isTransitive` (exists), `inverse: PredicateKey`, `specializes: PredicateKey`, `contradicts: PredicateKey[]` | `src/types.ts` | S | -| A2 | `definePredicateRecord`: defaults + **derivation/validation** — `isSymmetric ⟹ inverse=self` (reject conflicting `inverse`); inverse pairs mirror algebraic flags; `contradicts` symmetric on both sides; typed-key refs resolve to known keys (build fails otherwise) | `src/record.ts` | M | +| A1 | Add fields to `PredicateSpec` (flat): `polarity`, `objectKind`, `literalType`, `temporalNature`, `claimType`, `isSymmetric` (exists), `isTransitive` (exists), `inverse: PredicateKey`, `specializes: readonly PredicateKey[]` (array — audit B1), `contradicts: PredicateKey[]`, `supersededBy: PredicateKey` | `src/types.ts` | S | +| A2 | `definePredicateRecord`: defaults + the **complete 14-rule derivation/validation set** (decision record §5.1 — incl. `specializes` acyclicity, contradiction closure down the hierarchy, no-contradiction-with-ancestor, `polarity ∧ isTransitive` error, `literalType ⟹ objectKind='literal'`, symmetric+transitive allow-list); typed-key refs resolve to known keys (build fails otherwise) | `src/record.ts` | M–L | +| A2b | **Canonical direction derivation** (audit A1): for each inverse pair, derive the canonical member (deterministic rule); export it in the record + seed so builders and the indexer agree | `src/record.ts` | S | | A3 | Migrate `inversePredicate: string` → `inverse: PredicateKey` across specs | `src/generated/specs/*.ts` | S | | A4 | Backfill **mechanical** fields across 133 specs: `polarity`, `objectKind`, `temporalNature` (scriptable, obvious values) | specs | M | | A5 | Backfill **curated** fields: `specializes`, `contradicts`, `inverse` pairs, `claimType` (human review) | specs | M | -| A6 | New generator `scripts/generate-predicate-seed.mjs` → emits `dist/predicate-seed.json` (and optional `.sql`): one row per predicate with `{ id: PREDICATE_IDS[key], slug: key, label, description, isSymmetric, isTransitive, inverse→id, specializes→id, contradicts→[id], polarity, objectKind, temporalNature, claimType, marketPattern }` — references resolved to atom ids | `scripts/`, `package.json` build | M | -| A7 | Tests: derivation/validation unit tests; seed snapshot test | `src/*.test.ts` | S | +| A6 | New generator `scripts/generate-predicate-seed.mjs` → emits `dist/predicate-seed.json` (and optional `.sql`): one row per predicate with `{ id: PREDICATE_IDS[key], slug: key, label, description, isSymmetric, isTransitive, inverse→id, isCanonicalDirection, specializes→[id], contradicts→[id] (hierarchy-closed), polarity, objectKind, literalType, temporalNature, claimType, marketPattern, supersededBy→id }` — references resolved to atom ids | `scripts/`, `package.json` build | M | +| A7 | Tests: derivation/validation unit tests (one per §5.1 rule); seed snapshot test | `src/*.test.ts` | S | +| A8 | **Mint-time canonicalization in builders** (audit A1 — blocking, spans `@0xintuition/primitives` + `ids`): symmetric predicates → canonical subject/object ordering (by atom ID) before triple-ID computation; inverse pairs → normalize to the canonical direction from A2b. Both user intents resolve to one triple, one market | `primitives`, `ids` | M | **Acceptance:** `bun run build` emits a deterministic seed artifact; invalid specs (one-sided inverse, asymmetric `contradicts`, dangling key ref) fail the build. @@ -72,9 +74,9 @@ asymmetric `contradicts`, dangling key ref) fail the build. | ID | Task | Detail | |---|---|---| | B1 | Confirm existing columns | `is_symmetric`, `is_transitive`, `inverse_predicate_id`, `metadata` present in live KG (not just fixture) | -| B2 | Add predicate columns | `specializes_predicate_id text`, `polarity text`, `object_kind text`, `market_pattern text`, `claim_type text` (or keep `claim_type`/`temporal_nature` in `metadata` JSONB per the column-vs-JSONB heuristic) | +| B2 | Add predicate columns | `polarity text`, `object_kind text`, `market_pattern text`, `superseded_by_predicate_id text` (+ `claim_type`/`temporal_nature`/`literal_type` in `metadata` JSONB per the column-vs-JSONB heuristic) | | B3 | Add `temporal_nature` | JSONB-first (`metadata->>'temporalNature'`); promote later if filtered | -| B4 | Contradictions table | `CREATE TABLE kg.predicate_contradictions (predicate_id text, contradicts_id text, PRIMARY KEY (predicate_id, contradicts_id))` — store both directions (seed writes symmetric pairs) | +| B4 | Inter-predicate join tables | `CREATE TABLE kg.predicate_contradictions (predicate_id text, contradicts_id text, PRIMARY KEY (predicate_id, contradicts_id))` — both directions, **hierarchy-closed at seed** (§5.1 rule 7). `CREATE TABLE kg.predicate_specializations (predicate_id text, specializes_id text, PRIMARY KEY (predicate_id, specializes_id))` — `specializes` is an array/DAG (audit B1), so a single column no longer fits | | B5 | CHECK constraints | `polarity IN ('positive','negative','neutral')`, `object_kind IN ('entity','claim','literal')`, `market_pattern IN ('depositional','attributive','comparative')` | | B6 | Reconcile backend-only flags | `is_hierarchical` (we cut it — derive `is_transitive AND inverse_predicate_id IS NOT NULL` at seed, or drop), `is_market` (superseded by `market_pattern` — backfill then deprecate), `is_social` (map from `category` or leave) | | B7 | Triple indexes | `(subject_id, predicate_id)`, `(object_id, predicate_id)`, `(predicate_id)`, partial `WHERE inferred = false` (see integration doc §5.5) | @@ -89,7 +91,7 @@ asymmetric `contradicts`, dangling key ref) fail the build. |---|---|---| | C1 | Seed loader | A backend step that reads `predicate-seed.json` (pinned package version) and upserts `kg.predicates` **`ON CONFLICT (id) DO UPDATE`** of the metadata columns; `metadata = kg.predicates.metadata || EXCLUDED.metadata` (merge). Implement as a small Rust seed binary in `projections` (has KG pool) or a generated SQL migration. | | C2 | Reconcile with lazy path | Keep `upsert_kg_predicate_lazy` as-is (`DO NOTHING`, for unknown chain predicates). Seed owns metadata via `DO UPDATE`. Document the division: lazy = discovery, seed = truth. | -| C3 | Contradictions seed | Populate `kg.predicate_contradictions` from the artifact (both directions). | +| C3 | Inter-predicate seeds | Populate `kg.predicate_contradictions` (both directions, hierarchy-closed — the package emits the closed set, the backend stores it verbatim) and `kg.predicate_specializations` from the artifact. | | C4 | Ordering | Seed runs on deploy, idempotent, independent of ingestion order (keyed by atom id from the package). | | C5 | CI sync | Package release regenerates the artifact; backend pins the package version; a CI check fails if the committed seed is stale vs the package. | @@ -109,7 +111,8 @@ Default to **query-time** (no data migration); materialize only measured-hot pat | D3 | Roll-up (`specializes`) | load shallow hierarchy from `kg.predicates`; rewrite predicate filter to `predicate_id IN (descendants)` | B7 `(predicate_id)` | | D4 | Contradiction detection + counter-triple wiring | the §5.4 join over `kg.predicate_contradictions`; on triple insert, if predicate `contradicts` an existing sibling `(subject,object)`, set `is_counter_triple=true`, link `sibling_triple_id` | join table + triple indexes | | D5 | `objectKind` validation | at ingestion/API write, enforce `object_type` matches: `claim ⇒ object_type='triple'`, `literal ⇒` value, `entity ⇒ 'node'` | `object_type` column exists | -| D6 | (Phase 4) Materialize hot paths | inferred reverse edges + closure table, written `inferred=true` + provenance; **excluded from aggregates that count the origin** | partial indexes | +| D6 | (Phase 4) Materialize hot paths | inferred reverse edges + closure table, written `inferred=true` + provenance; **excluded from aggregates that count the origin**; **confidence = min over the derivation chain** (audit C3 — never copied, never 1.0), and inferred edges never re-enter inference across rules without a depth bound | partial indexes | +| D7 | Duplicate-triple backstop (audit A1) | detect symmetric duplicates (`⟨A,P,B⟩`+`⟨B,P,A⟩`, P symmetric) and inverse-pair duplicates (`⟨A,P,B⟩`+`⟨B,Q,A⟩`, `Q = inverse(P)`) minted before canonicalization (A8) or via raw protocol calls; link via `sibling_triple_id`-style reference; define aggregate-stake presentation policy | triple indexes | **Where D1–D3 live:** the KG read layer the API uses (e.g. `api/src/clients/kg-workflows.ts` / KG read crate). Centralize as reusable query builders so API + recommendation share them. @@ -179,6 +182,14 @@ after is opt-in optimization and new product surface. - **No double-counting** (biggest correctness trap): inferred + rolled-up edges excluded from aggregates that already count the origin. Enforced by G5 and "exclude inferred by default." +- **No split-stake duplicates** (audit A1): one fact, one triple, one market. Mint-time canonicalization + (A8) is primary; D7 detect-and-link is the backstop. Ship A8 **with or before** any reverse-edge + synthesis feature — synthesis without canonicalization encourages fragmentation. +- **Confidence decay** (audit C3): inferred-edge confidence = min over the derivation chain; inferred + edges don't re-enter inference unbounded. +- **Conflict detection is pair-level** (audit A2): the D4 join fires on same ⟨subject, object⟩ with + disjoint predicates. Attester-level disagreement about an object is a `polarity` aggregate, not a + contradiction — don't conflate the two queries. - **OWA**: contradiction and "current state" read only explicit assertions; never infer negation from absence. - **Replay safety**: seed and lazy paths both idempotent; seed `DO UPDATE` vs lazy `DO NOTHING` must not fight. - **KG-optional**: all features graceful when `DATABASE_KG_URL` absent (matches current projection behavior). diff --git a/.planning/predicate-backend-integration.md b/.planning/predicate-backend-integration.md index 75894ae..1c5409b 100644 --- a/.planning/predicate-backend-integration.md +++ b/.planning/predicate-backend-integration.md @@ -108,12 +108,14 @@ relying on referential integrity. | `isSymmetric` | ✅ `is_symmetric` column | reuse as-is | | `isTransitive` | ✅ `is_transitive` column | reuse as-is | | `inverse` | ✅ `inverse_predicate_id` column | reuse; store the **predicate atom-id**, mapped from our typed `PredicateKey` at seed time | -| `specializes` | ❌ | add `specializes_predicate_id text` (mirrors `inverse_predicate_id`) | -| `contradicts` | ❌ (but `is_counter_triple`/`sibling_triple_id` scaffolding exists) | add join table `kg.predicate_contradictions(predicate_id, contradicts_id)` — many-to-many, symmetric | +| `specializes` (array, per audit B1) | ❌ | join table `kg.predicate_specializations(predicate_id, specializes_id)` — a DAG needs many-to-many; mirrors the contradictions table (a `text[]` column also works, but the join table keeps both inter-predicate relations uniform) | +| `contradicts` | ❌ (but `is_counter_triple`/`sibling_triple_id` scaffolding exists) | add join table `kg.predicate_contradictions(predicate_id, contradicts_id)` — many-to-many, symmetric; **seeded with the hierarchy-closed set** (audit B2 rule 7: `P ⊑ Q ∧ Q ⊥ R ⟹ P ⊥ R` expanded at seed time, so the conflict join never walks `specializes` at query time) | | `polarity` | ❌ | **dedicated column** `polarity text` — it's a hot query/filter axis (reputation, feed) | | `objectKind` | ❌ | **dedicated column** `object_kind text` — drives validation + render, queried often | +| `literalType` (audit A3) | ❌ | `metadata->>'literalType'` JSONB — rendering/validation reads it via the registry, not as a SQL filter | | `temporalNature` | ❌ | `metadata->>'temporalNature'` JSONB first; promote to column if it becomes a query filter | | `claimType` | ❌ | `metadata->>'claimType'` JSONB (soft-ship; low traffic) | +| `supersededBy` (audit D1) | ❌ | `superseded_by_predicate_id text` (mirrors `inverse_predicate_id`; nullable, rare) | | `marketPattern` | ⚠️ only `is_market boolean` | add `market_pattern text` (3-value); reconcile/retire the boolean | | — | `is_hierarchical` exists | our spec **cut** this (derivable). Either derive it at seed (`is_transitive AND inverse_predicate_id IS NOT NULL`) or drop the column | | — | `is_social` exists | backend-only flag; map from our `category` at seed time or leave to backend | @@ -162,6 +164,15 @@ This makes the package the single source of truth and the backend a projection o ### 5.1 Reverse-edge synthesis (`isSymmetric`, `inverse`) +> **Prerequisite (audit A1 — blocking):** synthesis handles the *read* side only. On the *write/mint* +> side, `⟨A, partnerOf, B⟩` and `⟨B, partnerOf, A⟩` hash to two triple IDs → **two vaults**, splitting +> stake on one fact; likewise inverse pairs (`employedBy`/`employs`). Primary fix is **mint-time +> canonicalization in the package/builders** (decision record §5.2): canonical subject/object ordering +> for symmetric predicates, canonical direction for inverse pairs. The backend's job is the **backstop**: +> detect symmetric/inverse duplicates already minted (or arriving via raw protocol calls) and link them — +> same `sibling_triple_id` pattern as counter-triples, plus an explicit policy for presenting their +> aggregate stake. Without both halves, synthesis *encourages* liquidity fragmentation. + Two strategies; choose per access pattern: - **Query-time rewrite (recommended default for `isSymmetric`).** To answer "edges touching X," query both @@ -255,6 +266,14 @@ becomes common; the table is tiny (hundreds of rows), so a seq-scan there is usu - **Write amplification** from materialized synthesis/closure — mitigate by defaulting to query-time and materializing only measured-hot predicates; always tag `inferred = true` and exclude from aggregates that count the origin. +- **Inferred-edge confidence decay (audit C3).** `kg.triples.confidence` exists for exactly this; the + policy must be explicit: an inferred edge's confidence is a **non-increasing function of its derivation + chain** — `min` of the constituent confidences (the conservative standard from the probabilistic-KG + line: PSL, Knowledge Vault) — never a copy of one constituent, never `1.0`. And inferred edges must not + feed further inference rounds across different rules without a depth bound, or materialization loops. + Skipping this paragraph is how "everything is 100% confident" graphs happen. +- **Duplicate symmetric/inverse triples** (audit A1) — see §5.1 prerequisite; mint-time canonicalization + in the package is primary, backend detect-and-link is the backstop. - **Double-counting in reputation** — the single biggest correctness trap. Synthesized reverse edges and rolled-up sub-property edges must not be summed alongside their origins. Make "exclude inferred" the default in scoring aggregates. diff --git a/.planning/predicate-fields/README.md b/.planning/predicate-fields/README.md index 8a2b884..19fa420 100644 --- a/.planning/predicate-fields/README.md +++ b/.planning/predicate-fields/README.md @@ -45,10 +45,12 @@ Every example in these docs is tagged with which axis it serves: | `polarity` | [polarity.md](polarity.md) | **88** | Ship | | `isSymmetric` | [is-symmetric.md](is-symmetric.md) | **85** | Ship | | `temporalNature` | [temporal-nature.md](temporal-nature.md) | **85** | Ship | +| `literalType` *(audit)* | [literal-type.md](literal-type.md) | **85** | Ship | | `marketPattern` | [market-pattern.md](market-pattern.md) | **84** | Ship (grandfathered) | | `contradicts` | [contradicts.md](contradicts.md) | **82** | Ship | | `isTransitive` | [is-transitive.md](is-transitive.md) | **80** | Ship | -| `specializes` | [specializes.md](specializes.md) | **80** | Ship | +| `specializes` (array) | [specializes.md](specializes.md) | **80** | Ship | +| `supersededBy` *(audit)* | [superseded-by.md](superseded-by.md) | **70** | Soft-ship | | `claimType` | [claim-type.md](claim-type.md) | **64** | Soft-ship | | `isAsymmetric` | [is-asymmetric.md](is-asymmetric.md) | **54** | **Defer** (demoted) | | `isInverseFunctional` | [deferred-and-cut.md](deferred-and-cut.md) | 50 | Defer | @@ -67,6 +69,15 @@ Every example in these docs is tagged with which axis it serves: - **`claimType` lands at Soft-ship (64),** not full ship — define it, populate where obvious, but no consumer branches on it yet. -**Final ship list (9 new/active semantic fields):** `objectKind`, `inverse`, `polarity`, +### What the audit pass added (`../predicate-spec-audit.md`, 2026-07-01) + +- **`literalType` (85, Ship)** — typed literals complete the rendering contract; `objectKind: 'literal'` + alone can't distinguish image/link/date/number/text. +- **`supersededBy` (70, Soft-ship)** — deprecation forwarding (Wikidata "replaced by"). +- **`specializes` widened to `readonly PredicateKey[]`** — hierarchies are DAGs, not trees. Score unchanged. +- Blocking process findings (mint-time canonicalization for symmetric/inverse; pair-level `contradicts` + definition) and the complete 14-rule validation set live in the decision record §5.1–5.3. + +**Final ship list (10 new/active semantic fields):** `objectKind`, `literalType`, `inverse`, `polarity`, `isSymmetric`, `temporalNature`, `contradicts`, `isTransitive`, `specializes` + grandfathered -`marketPattern`; plus `claimType` as soft-ship. +`marketPattern`; plus `claimType` and `supersededBy` as soft-ship. diff --git a/.planning/predicate-fields/contradicts.md b/.planning/predicate-fields/contradicts.md index 5f2f004..5a2e60b 100644 --- a/.planning/predicate-fields/contradicts.md +++ b/.planning/predicate-fields/contradicts.md @@ -20,11 +20,19 @@ declares those disjoint pairs so the system can *detect* conflict instead of sil ## Examples across the value axes -1. **[ECON] Disagreement markets — the differentiator.** Alice stakes `trust Bob`; Carol stakes - `distrust Bob`. Because the predicates are declared `contradicts`, the system recognizes a *priced - disagreement* and can route both into a single conflict market rather than two unrelated claims. No - classical fact-graph (DBpedia, Wikidata, Google KG) was built to model this; it is the cutting-edge - capability the whole effort is aiming at. +1. **[ECON] Disagreement markets — the differentiator.** The triples `⟨DAO-X, trust, Bob⟩` and + `⟨DAO-X, distrust, Bob⟩` — **same subject, same object** — both accrue stake. Because the predicates + are declared `contradicts`, the system recognizes a *priced contradiction* and routes both into a + single conflict market rather than two unrelated claims. No classical fact-graph (DBpedia, Wikidata, + Google KG) was built to model this; it is the cutting-edge capability the whole effort is aiming at. + + > **Precision (audit A2): pair-level only.** "Alice asserts trust Bob, Carol asserts distrust Bob" + > is **not** a contradiction — the subjects differ; that's ordinary disagreement, and the aggregate + > ("how contested is Bob?") is served by `polarity` (signed sum over edges into Bob) plus + > counter-triples. `contradicts` fires only on the same ⟨subject, object⟩ pair with disjoint + > predicates, exactly as OWL `propertyDisjointWith` defines it — and that's what the backend's + > conflict-detection join keys on. Conflating the two layers produces a conflict query that matches + > everything or nothing. 2. **[RENDER] Conflict surfacing in the UI.** An entity page can render a "Contested" badge and show the opposing camps side by side when contradictory predicates both have stake. The frontend finds the @@ -57,6 +65,15 @@ Moderate — it needs **human curation** (which pairs truly oppose) and must be the derivation layer enforces (`A.contradicts ∋ B ⟺ B.contradicts ∋ A`). Forward-chainable / cheap to check. The curation set is small (sentiment pairs). +Two additional gate rules from the audit (decision record §5.1): a predicate may not contradict its own +`specializes` ancestor/descendant (`P ⊑ Q` ∧ `P ⊥ Q` makes P unsatisfiable), and the contradiction set is +**closed down the hierarchy at build time** (`vouchFor ⊑ trust` ∧ `trust ⊥ distrust` ⟹ `vouchFor ⊥ +distrust`) — otherwise conflict detection silently misses every sub-predicate pair. + +**What it cannot express (audit D3):** converse-pair incoherence — `⟨A, betterThan, B⟩` alongside +`⟨B, betterThan, A⟩` is a *reversed pair with the same predicate*, i.e. asymmetry violation, not +disjointness. That has no shipped mechanism while `isAsymmetric` is deferred; we accept the gap knowingly. + ## Pruning check - *Derivable?* Partially correlated with `polarity` (opposite polarity is a *hint*) but not derivable — diff --git a/.planning/predicate-fields/inverse.md b/.planning/predicate-fields/inverse.md index ca2e9f7..e507b9f 100644 --- a/.planning/predicate-fields/inverse.md +++ b/.planning/predicate-fields/inverse.md @@ -39,6 +39,17 @@ separately: 'subOrganization'` *and* `subOrganization.inverse === 'parentOrganization'`, and that the pair mirror each other's algebraic properties (both transitive, etc.). A one-sided inverse fails the build. +## Canonical direction (audit A1 — ships with the field) + +Under content-addressed IDs, an inverse pair is a duplicate-fact hazard: `⟨Alice, employedBy, Acme⟩` and +`⟨Acme, employs, Alice⟩` are one fact, two mintable triples, **two vaults** — split stake. Wikidata hit the +analogous problem and needed permanent bot-sync for inverse pairs; their considered fix was canonical +directions. Ours (decision record §5.2): `definePredicateRecord` derives a **canonical direction** per +pair (deterministic rule, e.g. lexicographically smaller key); builders normalize assertions in the +non-canonical direction to the canonical triple. The non-canonical predicate stays fully usable in UI and +queries — it's a *view* over the canonical edge, not a second fact. The indexer links stray duplicates as +a backstop. + ## Why typed key, not string Today the codebase has `inversePredicate: 'sub organization'` — a *display name*. Display names get diff --git a/.planning/predicate-fields/is-asymmetric.md b/.planning/predicate-fields/is-asymmetric.md index f14596c..a7d3271 100644 --- a/.planning/predicate-fields/is-asymmetric.md +++ b/.planning/predicate-fields/is-asymmetric.md @@ -38,11 +38,21 @@ same reason (validation-only, no consumer). Shipping `isAsymmetric` while deferring those would be incoherent — it sits in the same bucket. +## The gap the deferral leaves open (audit D3) + +Be explicit about what we're living without: converse-pair incoherence — `⟨A, betterThan, B⟩` alongside +`⟨B, betterThan, A⟩` — has **no shipped mechanism**. `contradicts` cannot express it (it's a reversed +pair with the *same* predicate, not a same-pair with disjoint predicates). The decision record's P3 now +states this gap rather than implying `contradicts` covers it. Comparative-predicate coherence is the +concrete scenario that graduates this field. + ## When to promote Pull it in (likely alongside `isFunctional`/reflexivity) the moment write-time edge validation or a consistency-checking pass becomes a real consumer. At that point all the "reject incoherent edges" fields -graduate together as a coherent validation layer. +graduate together as a coherent validation layer. Note that the mint-time canonicalization machinery for +symmetric/inverse predicates (audit A1, decision record §5.2) builds direction-aware handling into the +builders — once that exists, activating `isAsymmetric` becomes materially cheaper. ## Confidence breakdown diff --git a/.planning/predicate-fields/is-symmetric.md b/.planning/predicate-fields/is-symmetric.md index a173f4c..d6dc2d3 100644 --- a/.planning/predicate-fields/is-symmetric.md +++ b/.planning/predicate-fields/is-symmetric.md @@ -33,10 +33,23 @@ the same predicate*. `isSymmetric` declares this so the reverse edge is synthesi 4. **[DATA] Consistency.** The derivation layer sets `inverse = self` for symmetric predicates and rejects a spec that declares both `isSymmetric` and a *different* `inverse` — a contradiction. +## The Intuition-specific requirement: mint-time canonicalization (audit A1 — blocking) + +OWL's world has no cost to a duplicate edge. Ours does: every triple has a **deterministic ID and its own +vault**, so `⟨Acme, partnerOf, BigCo⟩` and `⟨BigCo, partnerOf, Acme⟩` — the *same fact* — hash to two +triple IDs and **two markets**, splitting stake. The flag alone doesn't prevent this; nothing stops two +users independently minting both directions. + +Therefore `isSymmetric` ships **with** canonicalization (decision record §5.2): builders in `primitives` +canonically order subject/object (by atom ID) before computing the triple ID, so both user intents resolve +to one triple and one market. The indexer detects-and-links pre-existing duplicates as a backstop +(`sibling_triple_id` pattern). Without this, symmetric synthesis *encourages* liquidity fragmentation. + ## What breaks without it -Either users mint two atoms per mutual relationship (friction + storage + drift between the two), or the -indexer can't safely answer reverse queries because it doesn't know the relation is mutual. +Either users mint two atoms per mutual relationship (friction + storage + drift between the two — and +under content-addressed IDs, **split stake across two markets for one fact**), or the indexer can't safely +answer reverse queries because it doesn't know the relation is mutual. ## Cost / complexity diff --git a/.planning/predicate-fields/is-transitive.md b/.planning/predicate-fields/is-transitive.md index 60d24ad..7c9d429 100644 --- a/.planning/predicate-fields/is-transitive.md +++ b/.planning/predicate-fields/is-transitive.md @@ -43,6 +43,25 @@ decidability), and transitive closure is more expensive than the other RL-core i flag, but the *computation* of closure is an indexer/query-time decision (materialize vs. compute on demand), and we should bound depth for ranking chains that could be long. This is a known, manageable cost. +## ⚠️ Sentiment is never transitive (audit C1 — enforced as gate rule 10) + +The single most tempting authoring mistake is "if Alice trusts Bob and Bob trusts Carol, Alice trusts +Carol." The canonical result — Guha et al. (2004), *Propagation of Trust and Distrust* — debunks it: +trust decays sharply with path length, and **distrust does not propagate transitively at all** (one-step +only; two distrust hops arguably flip sign, per structural balance). Marking a sentiment predicate +transitive lets the indexer synthesize edges the theory says are false, and closure over a social graph is +also a combinatorial explosion. So `definePredicateRecord` **errors on `polarity` present ∧ +`isTransitive`**. Transitivity belongs to structural containment (`containedInPlace`, `partOfSeries`, +`parentOrganization`, `dependOn`) and `sameAs` — never to `trust`, `endorse`, `distrust`, or kin. + +Related caution on comparatives: per-attester chains of `betterThan`/`rankedAbove` are transitive, but the +**community aggregate is not** — pairwise majority preferences famously cycle (Condorcet). Closure over +comparative predicates should operate within one attester's assertions, not across the aggregate. + +Also note (gate rule 11): transitivity is **not inherited** via `specializes` — a sub-property of a +transitive property is not itself transitive (RDFS/OWL semantics). The indexer must never expand closure +over the sub-property hierarchy. + ## What breaks without it Closure/reachability queries become per-predicate special cases or are simply unavailable; tree/breadcrumb diff --git a/.planning/predicate-fields/literal-type.md b/.planning/predicate-fields/literal-type.md new file mode 100644 index 0000000..b933d46 --- /dev/null +++ b/.planning/predicate-fields/literal-type.md @@ -0,0 +1,62 @@ +# `literalType` + +**Type:** `'url' | 'image' | 'date' | 'number' | 'text'` — only meaningful when `objectKind === 'literal'` +**Confidence: 85 / 100 — Ship.** *(Added by the audit pass, `predicate-spec-audit.md` A3.)* + +> Verdict: the missing half of the rendering contract. `objectKind: 'literal'` says the object is a value, +> but not *what kind* — and the literal subset is exactly where rendering varies most. RDF solved this with +> typed literals (`xsd:anyURI`, `xsd:date`, …); this is our pragmatic equivalent. + +## The problem it solves + +`objectKind`'s flagship promise is "the frontend renders any edge with zero per-predicate code." But its +own example betrays a gap: *"render `imgUrl` as an ``, `url` as an ``"* — that switch is back on +the **predicate key**. Knowing "it's a literal" doesn't distinguish image / link / date / number / prose. +Without a datatype, the per-predicate `switch` the field set promises to delete survives for literals: + +- `imgUrl` → `image` → `` +- `url`, `hasSource` → `url` → `` +- `foundedOn`, `releaseDate` → `date` → formatted date, locale-aware +- `employeeCount`, `price` → `number` → formatted numeral +- `hasDescription`, `bio` → `text` → prose block + +## Examples across the value axes + +1. **[RENDER] Completes the generic `` component.** The `literal` branch switches on + `literalType` instead of predicate key. New literal predicates render correctly with zero frontend + changes — the actual promise of the rendering contract, now kept for the hardest subset. + +2. **[DATA] Value validation at write time.** `literalType: 'url'` ⟹ validate URL shape; + `'date'` ⟹ parseable date; `'number'` ⟹ numeric. Catchable at authoring/API time the moment the field + exists — no engine needed, it's a format check, not inference. + +3. **[MACHINE] Serializes to standard XSD datatypes.** `url → xsd:anyURI`, `date → xsd:date`, + `number → xsd:decimal` — third parties get typed literals exactly as RDF defines them. + +4. **[QUERY] Typed filtering/sorting.** "Sort by release date," "filter numeric ranges" require the indexer + to know which literal edges are dates/numbers before it can index them as such. + +## Why not lean on `classifications` / schema.org property types instead? + +That re-introduces a cross-package lookup in the hot rendering path — the coupling the rendering-contract +argument was built to avoid. The literal predicate set is small; the annotation is mechanical. + +## Cost / complexity + +Trivial. Only populated on literal predicates (a small set), values are obvious, validated by rule 12 +(`literalType` present ⟹ `objectKind === 'literal'`) in `definePredicateRecord`. + +## Pruning check + +- *Derivable?* Sometimes sniffable from stored values, but that's runtime guessing — the point is a + declared contract. Not derivable from other spec fields. +- *Duplicated?* No. `objectKind` is the coarse axis; this refines exactly one of its values. +- *Mergeable?* Could be folded into `objectKind` as a 7-value enum (`entity | claim | url | image | …`), + but that muddles the coarse entity/claim/literal distinction consumers branch on first. Two fields, one + subordinate, is cleaner. + +## Confidence breakdown + +Leverage 34/40 · Consumer readiness 23/25 · Simplicity 19/20 · Non-redundancy 9/15 → **85**. +(Deduction: partial overlap with what value-sniffing or schema.org types could approximate; the declared +contract still wins for the hot path.) diff --git a/.planning/predicate-fields/object-kind.md b/.planning/predicate-fields/object-kind.md index a95b31e..93d458b 100644 --- a/.planning/predicate-fields/object-kind.md +++ b/.planning/predicate-fields/object-kind.md @@ -44,6 +44,19 @@ can render it, traverse it, or reason about it. Today that knowledge is implicit 5. **[DATA] Validation at authoring.** "You attached an entity where this predicate expects a literal" is catchable the moment the field is known. `url`'s object should be a URL literal, not an atom. +## Two refinements from the audit + +- **`literalType` companion (audit A3).** "Render `imgUrl` as ``, `url` as ``" — that switch is + on the predicate key, not on `objectKind`; knowing "it's a literal" doesn't distinguish image / link / + date / number / prose. The companion field `literalType: 'url' | 'image' | 'date' | 'number' | 'text'` + (see `literal-type.md`, ships at ~85) completes the rendering contract for the literal branch — RDF's + typed-literals lesson applied here. +- **Mixed-kind policy (audit D5).** schema.org's `rangeIncludes` is famously multi-valued, and a few + predicates will have objects that are legitimately entity-or-literal (`hasSource`: URL or an entity). + The single-value enum is still right — the rendering contract needs one answer — with this tie-break: + assign the **dominant** kind; if genuinely mixed, leave the field **absent** (absent = "sniff the atom," + today's behavior) rather than guessing. + ## What breaks without it Every consumer re-implements object-type detection by sniffing the atom data or hardcoding predicate diff --git a/.planning/predicate-fields/polarity.md b/.planning/predicate-fields/polarity.md index 8628e16..31030b8 100644 --- a/.planning/predicate-fields/polarity.md +++ b/.planning/predicate-fields/polarity.md @@ -37,6 +37,19 @@ re-hardcoding "trust is good, distrust is bad": research (Leskovec, Huttenlocher & Kleinberg, 2010) shows these signs are predictable and meaningful — we're encoding a quantity with 75 years of theory behind it (Heider 1946). +## Direction of aggregation — the precise definition (audit D4) + +One sentence prevents a class of reputation bugs: **polarity is the sign of the edge from subject toward +object; signed aggregation accrues to the *object*** (the Heider/Leskovec signed-network convention). So +`reported` is negative *about the thing reported*, not about the reporter. Two corollaries the gate and +indexer enforce: + +- **Inverse pairs share polarity** (gate rule 8, lint): `trust`/`trustedBy` are both positive. But for the + inverse direction the aggregation target flips to the triple's *subject* — the indexer handles this by + normalizing through the canonical direction (decision record §5.2) before aggregating, so every signed + edge is summed exactly once, onto the right node. +- Never mark a sentiment predicate transitive (gate rule 10; see `is-transitive.md`). + ## Why `neutral` is kept (and absent is different) `neutralOn` is a *deliberate* zero — "I have considered this and have no lean," which is information. diff --git a/.planning/predicate-fields/specializes.md b/.planning/predicate-fields/specializes.md index bb22294..e05a29e 100644 --- a/.planning/predicate-fields/specializes.md +++ b/.planning/predicate-fields/specializes.md @@ -1,8 +1,17 @@ # `specializes` -**Type:** `PredicateKey` (this predicate is a sub-property of the referenced one) +**Type:** `readonly PredicateKey[]` (this predicate is a sub-property of each referenced one) **Confidence: 80 / 100 — Ship.** +> **Type widened by the audit (`predicate-spec-audit.md` B1):** originally a single `PredicateKey`. +> Property hierarchies are **DAGs, not trees** — RDFS `subPropertyOf`, WordNet hypernymy, and schema.org +> all permit multiple broader terms, and real cases hit it immediately (`founder` specializes +> `affiliatedWith` *and* a creation relation; `authoredBy` sits under both `contributedTo` and +> provenance). Widening later is a migration; widening now is free, and it matches `contradicts`' shape. +> Curation guidance — usually one parent, never more than two — becomes a lint warning, not a type +> constraint. The gate also enforces acyclicity (rule 5) and closes `contradicts` down the hierarchy +> (rule 7). + > Verdict: the cheapest high-leverage inference in the whole stack — roll-up. A specific edge entails the > general one, so broad queries and reputation catch every specific sub-relation for free. Descends from > RDFS `subPropertyOf` and KL-ONE inheritance (1985); echoed by WordNet hypernymy. diff --git a/.planning/predicate-fields/superseded-by.md b/.planning/predicate-fields/superseded-by.md new file mode 100644 index 0000000..7eb37ee --- /dev/null +++ b/.planning/predicate-fields/superseded-by.md @@ -0,0 +1,58 @@ +# `supersededBy` + +**Type:** `PredicateKey` — only valid when `status === 'deprecated'` +**Confidence: 70 / 100 — Soft-ship.** *(Added by the audit pass, `predicate-spec-audit.md` D1.)* + +> Verdict: deprecation without forwarding is a dead end. Wikidata's "replaced by" is the precedent; with +> 133 community-evolving specs, predicate succession will be routine, and the pointer is one optional key. + +## The problem it solves + +`status: 'deprecated'` currently tells consumers only "don't use this." Nothing tells: + +- a **picker** what to offer instead (it can only hide the predicate), +- a **renderer** of existing triples how to present an old edge ("this used `partneredWith`, superseded by + `partnerOf`"), +- a **migration script** where old edges should map. + +`supersededBy` is the forwarding address. + +## Examples across the value axes + +1. **[RENDER] Pickers hide-and-redirect.** Selecting or searching a deprecated predicate surfaces "use + `partnerOf` instead" — driven by the field, not a hardcoded redirect table. + +2. **[DATA] Old triples stay legible.** Edges minted under a deprecated predicate render with a + "superseded" affordance linking to the successor, instead of silently rotting or disappearing from + grouped views. + +3. **[QUERY] Optional read-time widening.** A query on the successor can opt in to including + edges asserted under its deprecated predecessors — the same shape as `specializes` roll-up, reusing + that machinery. + +4. **[MACHINE] Serializes to `schema:supersededBy`** — an existing schema.org term, so the succession + chain is standards-legible. + +## Why soft-ship, not ship + +The consumer (pickers/migration tooling) is real but the *population* is nearly empty today — few +predicates are deprecated yet. Define the field and validation now; populate as deprecations happen. +That's exactly the soft-ship band. + +## Cost / complexity + +Trivial. One optional key; validated by rule 14 in `definePredicateRecord` (must point at a +non-deprecated spec; only valid on `status: 'deprecated'`; chains permitted but must be acyclic). + +## Pruning check + +- *Derivable?* No — succession is editorial. +- *Duplicated?* No. `status` says *that* it's retired; this says *what replaced it*. Distinct from + `specializes` (entailment between live predicates) and `inverse` (direction pairing). +- *Mergeable?* No. + +## Confidence breakdown + +Leverage 24/40 · Consumer readiness 18/25 · Simplicity 19/20 · Non-redundancy 13/15 → **70**. +(Deductions: leverage is modest — lifecycle hygiene, not new query power — and today's deprecated set is +small. The cost is so low that the field still clears its band comfortably.) diff --git a/.planning/predicate-query-optimization-spec.md b/.planning/predicate-query-optimization-spec.md index b0ce95c..d688e3c 100644 --- a/.planning/predicate-query-optimization-spec.md +++ b/.planning/predicate-query-optimization-spec.md @@ -72,12 +72,14 @@ interface PredicateRole { id: string; slug: string; objectKind: 'entity' | 'claim' | 'literal'; // how to handle the object during gather isMetadataLiteral: boolean; // objectKind === 'literal' → collect value - isEquivalence: boolean; // sameAs/equivalentTo → symmetric+transitive identity + literalType?: 'url'|'image'|'date'|'number'|'text'; // audit A3 — how to resolve/render the value + isEquivalence: boolean; // EXPLICIT allow-list (sameAs) — see note below isIdentityKey: boolean; // isInverseFunctional → shared object ⇒ same subject isSymmetric: boolean; isTransitive: boolean; inverseId?: string; // reverse-direction predicate - specializesId?: string; // parent predicate (roll-up) + isCanonicalDirection: boolean; // audit A1 — inverse pairs normalize to one direction + specializesIds: string[]; // parent predicates (roll-up) — array/DAG per audit B1 polarity?: 'positive'|'negative'|'neutral'; temporalNature?: 'permanent'|'state'|'event'; } @@ -94,6 +96,15 @@ Fast lookup structures the query builder consults: This registry is the "parsed predicate" layer. Everything downstream (gather, equivalence, rewrite) is a function of it — so adding a predicate or changing a field is a data change, not a code change. +> **Guardrail (audit C2): `isEquivalence` is an explicit allow-list, never inferred from algebra.** +> `isSymmetric + isTransitive` mathematically characterizes an equivalence-like relation, and it's +> tempting to let any such predicate trigger identity behavior. Don't: identity merge is the +> highest-blast-radius inference in the system (no-UNA "smushing" — one bad edge merges two entities' +> metadata, stake context, and reputation), and inferring the behavior from two booleans means a future +> author can create it *by accident*. Equivalence-class membership is opt-in by predicate key — today +> **`sameAs` only** (`equivalentTo` was cut from the spec). The symmetric+transitive combination on any +> other predicate is a build-time lint (decision record §5.1 rule 9). + > **Atom parsing note:** the atom parse/enrich workers (`kg-parse-worker`, `kg-enrichment-worker`) that > populate `data_resolved` should *also* be able to **emit edges from JSON-LD properties** (`url`, `sameAs`, > `byArtist`) so embedded metadata becomes queryable triples. That's the ingestion half of the shift in §1; @@ -152,8 +163,8 @@ CREATE INDEX ix_nodes_search_text ON kg.nodes USING gin (to_tsvector('simple', s ## 5. Identity / co-reference resolution engine (Q4/Q5) -The equivalence class of a node is the connected component under **equivalence predicates** (`sameAs`, -`equivalentTo` — symmetric + transitive) plus **identity keys** (any predicate flagged +The equivalence class of a node is the connected component under **equivalence predicates** (the explicit +allow-list from §3 — today `sameAs` only) plus **identity keys** (any predicate flagged `isInverseFunctional`: two subjects with the same object are the same entity). > In the Spotify+Apple example the two URLs *differ*, so the IFP-on-url rule does **not** auto-merge them — diff --git a/.planning/predicate-spec-audit.md b/.planning/predicate-spec-audit.md new file mode 100644 index 0000000..21c1d46 --- /dev/null +++ b/.planning/predicate-spec-audit.md @@ -0,0 +1,288 @@ +# Audit: Predicate Spec Review Against 30 Years of KG Research + +**Status:** ✅ Applied — all findings (A1–D5) folded into the planning docs on 2026-07-01; this file +remains as the record of *why* each change was made +**Audits:** `predicate-spec-decisions.md` (canonical), `predicate-fields/*` (scores), backend docs +**Date:** 2026-07-01 + +This is the adversarial pass: take the proposed field set and structure, and stress-test it against the +research record (OWL 2 semantics, RDFS entailment rules, Wikidata's operational lessons, signed-network +theory, the identity/smushing literature) *and* against Intuition's own mechanics (content-addressed IDs, +staking, counter-triples). Findings are ordered by severity. §E lists what survives unchanged — which is +most of it. + +--- + +## A. Must resolve before shipping (semantic or economic correctness) + +### A1 — Symmetric/inverse predicates + content-addressed IDs = duplicate facts with split stake + +**The finding.** The entire OWL tradition assumes a triple store where synthesizing a reverse edge is a +pure inference: cheap, idempotent, no side effects. Intuition triples are different — each triple has a +**deterministic ID and its own vault/market**. That creates a failure mode OWL never had: + +- `⟨Acme, partnerOf, BigCo⟩` and `⟨BigCo, partnerOf, Acme⟩` are the *same fact* but hash to **two + different triple IDs → two different markets**. Stake splits across them; neither reflects true conviction. +- Same for inverse pairs: `⟨Alice, employedBy, Acme⟩` vs `⟨Acme, employs, Alice⟩` — one fact, two + mintable triples, two vaults. + +The current docs handle the *read* side (indexer synthesizes reverse edges, marks them `inferred`) but +not the *write/mint* side. Nothing stops two users from independently minting both directions. + +**The research precedent.** Wikidata hit exactly this operationally: inverse property pairs (e.g. +child/parent-style pairs) required permanent bot armies to keep both directions in sync, and the community's +considered response was to **avoid inverse properties and designate one canonical direction** wherever +possible. We should learn this before we ship, not after. + +**Recommendation (two layers):** +1. **Mint-time canonicalization in `primitives`.** For `isSymmetric` predicates, canonically order + subject/object (e.g., by atom ID) before computing the triple ID — both user intents resolve to one + triple, one market. For `inverse` pairs, designate a **canonical direction** per pair (a derived flag in + `definePredicateRecord`, e.g., the non-inverse member or lexicographically smaller key) and have + builders normalize to it: a user asserting the non-canonical direction gets the canonical triple. +2. **Indexer-side merge as backstop** for triples minted before the rule (or via raw protocol calls): + detect symmetric/inverse duplicates and link them (the `sibling_triple_id` pattern already exists for + counter-triples; this is a second, analogous linkage), with an explicit policy for how their stake is + presented in aggregate. + +This is the one place the proposal is not just incomplete but **economically unsound** if skipped: edge +synthesis without mint canonicalization actively encourages liquidity fragmentation. +**Owner surface:** `primitives` + `ids` (canonicalization), backend (merge backstop), decision record §5. + +### A2 — `contradicts` conflates two different kinds of conflict + +**The finding.** The docs use two incompatible notions of contradiction interchangeably: + +1. **Pair-level logical disjointness** (OWL `propertyDisjointWith`): `⟨s, trust, o⟩` and `⟨s, distrust, o⟩` + with the **same subject and object** cannot both hold. This is what `contradicts` formally is, and what + `contradicts.md` defines ("cannot both hold for the same subject/object pair"). +2. **Attester-level disagreement**: *"Alice asserts trust Bob; Carol asserts distrust Bob"* (decision + record P3, `contradicts.md` example 1). If the triples are `⟨Alice, trust, Bob⟩` and + `⟨Carol, distrust, Bob⟩`, the **subjects differ** — there is no logical contradiction at all. Two people + disagreeing about Bob is not incoherence; it's the graph working as intended. + +These need different machinery. Pair-level disjointness is a consistency/validation signal (and the correct +trigger for a *single* conflict market: same s/o pair, disjoint predicates). Attester-level disagreement +about an object is a **sentiment-aggregation** question already served by `polarity` (signed sum over +edges into Bob) plus the existing counter-triple mechanism. + +**Recommendation:** Define `contradicts` strictly as pair-level disjointness (matching OWL and +`contradicts.md`'s own formal definition). Rewrite the P3 user story so the example shares a subject — +or explicitly shows both layers: pair-level conflict routes to one market; object-level disagreement is a +polarity aggregate. This is a documentation fix, but it determines what the indexer's conflict-detection +query actually joins on, so it must be settled before WS-D. **Owner surface:** decision record P3, +`contradicts.md`, backend conflict-detection spec. + +### A3 — `objectKind: 'literal'` can't fulfill the rendering contract without a datatype + +**The finding.** `objectKind` is our highest-scored field (92) on the promise that the frontend renders any +edge with zero per-predicate code. But its own flagship example betrays a gap: *"`literal` → render +`imgUrl` as an ``, `url` as an ``"* — that switch is back on the **predicate key**, not on +`objectKind`. Knowing "it's a literal" doesn't tell you image vs link vs date vs number vs prose. RDF +solved exactly this with **typed literals** (`xsd:anyURI`, `xsd:date`, …); schema.org with `rangeIncludes` +datatypes. Without a datatype, the "giant switch" the field promises to delete survives for the literal +subset — which is precisely the subset with the most rendering variance. + +**Recommendation:** Add a companion field, only meaningful when `objectKind === 'literal'`: + +```ts +literalType?: 'url' | 'image' | 'date' | 'number' | 'text'; +``` + +Validated in `definePredicateRecord` (`literalType` present ⟹ `objectKind === 'literal'`). It scores like +`objectKind` itself: frontend consumer today, mechanical to author (the literal predicate set is small), +non-redundant, serializes cleanly to XSD datatypes. Estimated score under the rubric: **~85 (Ship)**. +Alternative if we want to stay minimal: document that literal rendering falls back to schema.org property +types via `classifications` — but that re-introduces a cross-package lookup in the hot rendering path, +which the rendering-contract argument was built to avoid. **Owner surface:** types, decision record §5, +new `predicate-fields/literal-type.md`. + +--- + +## B. Structural decisions — cheap now, painful later + +### B1 — `specializes` should be an array (DAG), not a single key (tree) + +RDFS `subPropertyOf`, WordNet hypernymy, and schema.org all permit **multiple broader terms** — property +hierarchies are DAGs, not trees. Real cases hit this immediately: `founder` specializes `affiliatedWith` +*and* plausibly a creation/contribution relation; `authoredBy` sits under both `contributedTo` and a +provenance relation. A single `PredicateKey` forces false choices, and widening the type later is a +migration; widening it now is free. It also makes the shape consistent with `contradicts` +(`readonly PredicateKey[]`). + +**Recommendation:** `specializes?: readonly PredicateKey[]`. Keep the curation guidance "usually one +parent, never more than two" as a lint warning, not a type constraint. + +### B2 — Enumerate the full validation/derivation rule set for `definePredicateRecord` + +The decision record names three rules (symmetric ⟹ self-inverse; inverse mirroring; contradicts declared +symmetrically). The DL literature gives us the **complete** list for our field set — and half of it is +missing. The gate should enforce: + +| # | Rule | Kind | Source | +|---|---|---|---| +| 1 | `isSymmetric` ⟹ no distinct `inverse` (inverse = self) | error | OWL (already specified) | +| 2 | `P.inverse = Q ⟺ Q.inverse = P` | error | OWL (already specified) | +| 3 | `contradicts` symmetric: `B ∈ A.contradicts ⟺ A ∈ B.contradicts` | error | OWL (already specified) | +| 4 | `contradicts` irreflexive: `P ∉ P.contradicts` | error | coherence | +| 5 | `specializes` acyclic (no `P ⊑ … ⊑ P`) | error | RDFS coherence | +| 6 | P may not contradict its own `specializes` ancestor/descendant (asserting P entails the ancestor — contradiction with it is incoherent) | error | DL: `P ⊑ Q` ∧ `P ⊥ Q` ⟹ P unsatisfiable | +| 7 | **Derive** contradiction closure down the hierarchy: `P ⊑ Q` ∧ `Q ⊥ R` ⟹ `P ⊥ R` — expand at build time so consumers get the full disjointness set without walking `specializes` at query time | derivation | OWL 2 RL | +| 8 | Inverse pairs mirror algebra: `P.isTransitive ⟺ inverse(P).isTransitive` (theorem: the inverse of a transitive relation is transitive); same `temporalNature`; same `claimType`; polarity equal (lint, not error — near-universal but conceivable exceptions) | error/lint | DL theorems | +| 9 | `isSymmetric && isTransitive` together ⟹ the predicate behaves as an **equivalence relation** — allowed only for an explicit allow-list (`sameAs`); warn otherwise | lint | see C2 | +| 10 | `polarity` present (sentiment predicate) ∧ `isTransitive` ⟹ **error** | error | see C1 | +| 11 | `isTransitive` is **not** inherited via `specializes` (a sub-property of a transitive property is not transitive — RDFS/OWL) — document so the indexer never expands closure over sub-properties | doc/derivation guard | OWL semantics | +| 12 | `literalType` present ⟹ `objectKind === 'literal'` (if A3 accepted) | error | — | +| 13 | `objectKind === 'claim'` ∧ `inverse` present ⟹ warn (the inverse's *subject* would be a claim — expressible but almost always a modeling smell) | lint | — | + +Rules 6, 7, and 11 are the ones most likely to cause silent bugs if unenforced: 6 and 7 because +`contradicts` × `specializes` interaction is invisible until a conflict query misses (`vouchFor ⊑ trust`, +`trust ⊥ distrust` — a `vouchFor`/`distrust` pair on the same s/o *is* a conflict and won't be detected +without closure); 11 because "transitive-ish inheritance" is an extremely natural indexer bug to write. + +### B3 — Ship the explicit serialization mapping table (P6 is asserted, not specified) + +The decision record claims interoperability "for free" via OWL-aligned flat naming, but no doc pins the +actual field↔IRI mapping. Make it normative: + +| Spec field | Serialized as | Standard | +|---|---|---| +| `isSymmetric: true` | `rdf:type owl:SymmetricProperty` | OWL 2 | +| `isTransitive: true` | `rdf:type owl:TransitiveProperty` | OWL 2 | +| `inverse` | `owl:inverseOf` | OWL 2 | +| `specializes` | `rdfs:subPropertyOf` | RDFS | +| `contradicts` | `owl:propertyDisjointWith` | OWL 2 | +| `literalType` (if accepted) | XSD datatype on range (`xsd:anyURI`, `xsd:date`, …) | RDF/XSD | +| `objectKind` / `polarity` / `temporalNature` / `claimType` / `marketPattern` | Intuition-namespaced `PropertyValue` entries (no standard equivalent — that's fine; they're our pragmatic extensions, like Wikidata's own property metadata) | — | + +Third parties get standards-compliant characteristics without bespoke parsing only if this table is +implemented exactly; otherwise P6 is aspiration. + +--- + +## C. Guardrails the literature insists on (encode as lint rules + doc warnings) + +### C1 — Sentiment is never transitive + +Guha et al. (2004), *Propagation of Trust and Distrust* — the canonical result: trust propagation decays +sharply with path length, and **distrust does not propagate transitively at all** (one-step only; two +"distrust" hops do not imply distrust — arguably the opposite, per structural balance). Marking any +polarity-bearing predicate `isTransitive` would let the indexer synthesize edges the theory says are +false, and at graph scale, closure over a social predicate is also a combinatorial explosion. +`isTransitive` belongs to **structural containment** (`locatedIn`, `partOf`, `subOrganizationOf`) and +`sameAs` — nothing social. This is validation rule B2-#10, and it deserves a visible warning in +`is-transitive.md` because it's the single most tempting authoring mistake ("if Alice trusts Bob and Bob +trusts Carol…" is exactly the intuition the research debunks). + +### C2 — The equivalence engine keys off an explicit `sameAs` allow-list, not inferred algebra + +`isSymmetric + isTransitive` mathematically characterizes an equivalence-like relation, and it's tempting +to have the identity engine (query-optimization spec §5) treat *any* such predicate as identity-bearing. +Don't. The no-UNA "smushing" literature (foundations doc, Part IV) is unambiguous: **identity merge is the +highest-blast-radius inference in the system** — one bad `sameAs` edge already merges two entities' +metadata, stake context, and reputation; letting the behavior attach to an algebraic pattern rather than a +named predicate means a future author can create it *by accident* with two booleans. Equivalence-class +membership must be opt-in by predicate key (today: `sameAs` only), with the symmetric+transitive +combination elsewhere triggering lint (B2-#9). + +### C3 — Inferred edges need a confidence-decay policy + +`kg.triples.confidence` exists, and transitive closure will write inferred edges derived from chains. The +probabilistic-KG literature (PSL, Markov logic, Google Knowledge Vault) is consistent: chained inference +compounds uncertainty — an inferred edge's confidence must be a **non-increasing function of the chain** +(min or product of constituent confidences; min is the conservative standard choice), never a copy of one +constituent and never 1.0. Also: inferred edges must **not feed further rounds of inference across +different rules** without depth bounds (materialization loops). One paragraph in the backend +implementation plan (WS-D) settles this; its absence is how "everything is 100% confident" graphs happen. + +--- + +## D. Worth considering (small, non-blocking) + +### D1 — `supersededBy?: PredicateKey` on deprecated predicates + +`status: 'deprecated'` is a dead end today — nothing tells a picker, a renderer of old triples, or a +migration script *what replaced it*. Wikidata's "replaced by" is the precedent, and with 133 community- +evolving specs, deprecation-with-forwarding will be routine. Cheap (one optional key, validated to point +at a non-deprecated spec), real consumer (pickers hide-and-redirect; old edges render with a "superseded" +affordance). Estimated score: **~70 (Soft-ship)**. Natural companion to the lifecycle fields we already have. + +### D2 — Roadmap note: the predicate metadata should eventually live *in the graph* + +Wikidata models property metadata as ordinary statements **on the property entities themselves** — which +is why its community can extend and contest property semantics without a software release. Our metadata +lives in a TS package + IPFS docs — right for now (deterministic, reviewable, ships with the code). But +the end-state for a permissionless knowledge graph is **self-description**: `⟨trust, contradicts, distrust⟩` +as an actual triple that can itself be staked on. The current design is compatible with that future +(predicates are atoms; the meta-relations are just more predicates) — state it explicitly in the decision +record §7 so nobody designs it out, and so "the community disagrees with our `contradicts` curation" has +an eventual on-protocol answer rather than a GitHub issue. + +### D3 — Acknowledge the P3 asymmetry hole explicitly + +With `isAsymmetric` deferred, the P3 story's second example — rejecting *"A better than B"* alongside +*"B better than A"* — has **no shipped mechanism**: that incoherence is converse-pair (`⟨s,P,o⟩` vs +`⟨o,P,s⟩`), which `contradicts` (same-pair, different-predicate) cannot express. The deferral is still +right (score 54 stands; no validation engine exists), but the decision record should stop implying P3 +covers it. Amend the story, and note in `is-asymmetric.md` that comparative-predicate coherence is what +graduates it. (If A1's mint-time canonicalization lands, the machinery for direction-aware handling will +exist and make `isAsymmetric` cheaper to activate later — the two findings are related.) + +### D4 — Define `polarity`'s direction of aggregation precisely + +One sentence prevents a class of reputation bugs: **polarity is the sign of the edge from subject toward +object; signed aggregation accrues to the *object*.** (Heider/Leskovec signed-network convention.) So +`reported` is negative *about the thing reported*, not about the reporter. Without the sentence, someone +will eventually aggregate signal onto subjects, and the two implementations will disagree. Also specify +whether inverse pairs share polarity (per B2-#8: yes, lint-enforced) — for `trustedBy`, the aggregation +target flips to the *subject*, which the indexer must handle by normalizing through the canonical direction +(ties back to A1). + +### D5 — Policy for mixed-kind objects on `objectKind` + +schema.org's `rangeIncludes` is famously multi-valued, and a few of our predicates will have objects that +are legitimately entity-or-literal (`hasSource`: URL or an entity). Single-value enum is still right +(the rendering contract needs one answer), but state the tie-break policy: assign the **dominant** kind; +if genuinely mixed, leave the field absent (absent = "sniff the atom," today's behavior) rather than +guessing. One paragraph in `object-kind.md`. + +--- + +## E. What survives the audit unchanged (confirmations) + +- **Flat structure** — reconfirmed. Every argument in decisions §4 holds; nothing found favors nesting. +- **The pruning bar and all cuts/deferrals** — reconfirmed field by field. `isHierarchical` (redundant), + `equivalentTo` (smushing — reinforced by C2), `verifiability`, property chains (still the most expensive + axiom with no consumer), reflexivity, the functional family: all correct calls. `isAsymmetric` at Defer + is consistent (with the D3 documentation caveat). No cut field needs resurrection. +- **OWL 2 RL scoping** — the shipped algebra (symmetric, transitive, inverse, subPropertyOf, disjointness) + is exactly the forward-chainable core; still the right ceiling. +- **`claimType` soft-ship, `polarity` with explicit `neutral`, `temporalNature`'s 3 values** — all hold. + (`temporalNature: 'state'` remains the correct future hook for since/until qualifiers via reification — + no change needed now.) +- **The scoring rubric and bands** — no score changed by more than noise except where new fields are + proposed (A3's `literalType` ~85, D1's `supersededBy` ~70). +- **`definePredicateRecord` as the single consistency gate** — right pattern; B2 just completes its rule set. +- **The differentiator thesis** — `contradicts` + `polarity` + `marketPattern` as a *priced-disagreement* + graph survives scrutiny; A2 sharpens it rather than weakening it. + +--- + +## Summary of proposed changes + +| # | Change | Severity | Touches | +|---|---|---|---| +| A1 | Mint-time canonicalization for symmetric/inverse triples + merge backstop | **Blocking** (economic) | primitives, ids, backend, decisions §5 | +| A2 | Define `contradicts` as pair-level; fix P3 story; split attester-disagreement into polarity aggregation | **Blocking** (semantic) | decisions P3, contradicts.md, backend WS-D | +| A3 | Add `literalType` companion field (~85, Ship) | **High** | types, decisions §5, new field doc | +| B1 | `specializes` → `readonly PredicateKey[]` | High (cheap now) | types, decisions §5, specializes.md | +| B2 | Complete validation rule set (13 rules) in `definePredicateRecord` | High | record.ts spec, decisions §5 | +| B3 | Normative field↔IRI serialization table | Medium | decisions §2/P6 | +| C1–C3 | Lint rules + doc warnings (no transitive sentiment; sameAs allow-list; confidence decay) | Medium | field docs, backend plan | +| D1 | `supersededBy` (~70, Soft-ship) | Low | types, decisions | +| D2–D5 | Doc clarifications (self-description roadmap, P3 hole, polarity direction, mixed objectKind) | Low | various docs | + +Net effect on the field set: **+1 ship** (`literalType`), **+1 soft-ship** (`supersededBy`), **1 type +widening** (`specializes` array), **0 removals**. The shape and philosophy hold; the fixes are about +Intuition-specific mechanics (A1, A2) and completing the consistency layer (B2) — exactly the places +where our design departs from the classical literature and therefore couldn't lean on it. diff --git a/.planning/predicate-spec-decisions.md b/.planning/predicate-spec-decisions.md index e08180f..4d5014d 100644 --- a/.planning/predicate-spec-decisions.md +++ b/.planning/predicate-spec-decisions.md @@ -51,13 +51,27 @@ The indexer synthesizes the reverse direction instead of forcing users to mint t **Consumer:** indexer (query expansion) + reputation. **Verdict: ship.** ### P3 — Surfacing contradiction (the Intuition-native problem) -> *Alice asserts "trust Bob." Carol asserts "distrust Bob." The app surfaces the tension and opens a -> market on it. The system rejects "A better than B" alongside "B better than A" as incoherent.* +> *The triples `⟨DAO-X, trust, Bob⟩` and `⟨DAO-X, distrust, Bob⟩` — **same subject, same object**, +> disjoint predicates — both exist with stake. The system recognizes a priced contradiction, routes both +> sides into a single conflict market, and badges the pair as contested.* This is the problem classical fact-graphs never had and the one most worth being cutting-edge on. A belief/trust graph is *valuable precisely because it can represent and price disagreement.* -**Needs:** `contradicts` (trust ⊥ distrust). (`isAsymmetric` would also serve here but was deferred — -its unique value is write-time validation, which has no consumer yet.) + +**Precision (audit A2):** `contradicts` is strictly **pair-level disjointness** (OWL +`propertyDisjointWith`): `⟨s, P, o⟩` and `⟨s, Q, o⟩` cannot both hold for the *same* subject/object pair. +It is **not** the mechanism for attester-level disagreement — "Alice asserts trust Bob, Carol asserts +distrust Bob" has *different subjects* and is not a logical contradiction; it's the graph working as +intended. That aggregate ("how contested is Bob?") is served by **`polarity`** (signed sum over edges +into Bob) plus the existing counter-triple mechanism. The two layers need different queries, and the +backend's conflict-detection join (integration doc §5.4) is built on the pair-level definition. + +**Known gap (audit D3):** converse-pair incoherence — `⟨A, betterThan, B⟩` alongside `⟨B, betterThan, A⟩` +— is *not* expressible by `contradicts` (same pair, different predicate ≠ reversed pair, same predicate). +That is asymmetry violation, and it has **no shipped mechanism** while `isAsymmetric` is deferred. We +accept the gap knowingly; comparative-predicate coherence is what graduates `isAsymmetric`. + +**Needs:** `contradicts` (trust ⊥ distrust). **Consumer:** reputation/markets (emerging) + frontend (conflict badges). **Verdict: ship `contradicts`.** ### P4 — Automatic frontend rendering from the predicate alone (the biggest near-term win) @@ -69,7 +83,9 @@ its unique value is write-time validation, which has no consumer yet.) This is the field set with the most immediate, concrete payoff, and it's the one the user explicitly called out. The predicate spec becomes a **rendering contract**, eliminating a giant `switch` in the app. -**Needs:** `objectKind` (entity | claim | literal), `polarity` (color/sentiment), `temporalNature` (freshness badge), plus existing display fields. +**Needs:** `objectKind` (entity | claim | literal), `literalType` (audit A3 — image vs link vs date; +without it the literal branch still switches on predicate key), `polarity` (color/sentiment), +`temporalNature` (freshness badge), plus existing display fields. **Consumer:** frontend, today. **Verdict: ship — highest confidence in the whole set.** ### P5 — Data freshness and lifecycle @@ -99,7 +115,9 @@ This is served *for free* by the fields above as long as we serialize them in th | `contradicts` | P3 belief markets | **Ship** (needs curation) | | `isAsymmetric` | P3 validation; tells indexer "don't mirror" | **Defer** — see note below; demoted in per-field review | | `objectKind` | P4 render literal/claim/entity differently | **Ship** (highest-value) | +| `literalType` *(audit A3)* | P4 — a literal alone doesn't say image vs link vs date; typed literals complete the rendering contract | **Ship** (~85) | | `temporalNature` | P4 freshness badge, P5 lifecycle | **Ship** | +| `supersededBy` *(audit D1)* | deprecation forwarding — pickers redirect, old edges render "superseded" (Wikidata "replaced by") | **Soft-ship** (~70) | | `marketPattern` | existing economic layer | **Keep** | | `claimType` (factual/evaluative) | market-design + "opinion vs fact" badge | **Ship, simplified** — drop the `normative` value until a consumer needs it | | `isHierarchical` | — derivable from `transitive + asymmetric + inverse` | **Cut** (redundant) | @@ -115,6 +133,10 @@ This is served *for free* by the fields above as long as we serialize them in th reflexivity, **and `isAsymmetric`**, and simplify `claimType`. That is roughly a 45% reduction in surface area — and every survivor names a consumer that exists or is emerging. +**Audit pass (2026-07-01, `predicate-spec-audit.md`) added:** `literalType` (ship, ~85 — completes the +rendering contract for literals), `supersededBy` (soft-ship, ~70 — deprecation forwarding), widened +`specializes` to an array (DAG, not tree), and left every cut/deferral standing. Zero removals. + > **`isAsymmetric` was demoted to Defer** during the per-field review (`predicate-fields/is-asymmetric.md`, > score 54). Its only unique value is write-time validation, which has no consumer yet; its "don't mirror > the edge" signal is already implied by the *absence* of `isSymmetric` plus the presence of `inverse`. It @@ -156,12 +178,14 @@ export interface PredicateSpec { description: string; category: PredicateCategory; status: PredicateStatus; + supersededBy?: PredicateKey; // audit D1 — required-when status==='deprecated' has a successor // economic (Intuition-native) marketPattern: MarketPattern; // rendering contract (frontend reads these directly) objectKind?: 'entity' | 'claim' | 'literal'; + literalType?: 'url' | 'image' | 'date' | 'number' | 'text'; // audit A3 — only when objectKind==='literal' polarity?: 'positive' | 'negative' | 'neutral'; temporalNature?: 'permanent' | 'state' | 'event'; claimType?: 'factual' | 'evaluative'; @@ -173,7 +197,7 @@ export interface PredicateSpec { // inter-predicate (typed key references — validated in definePredicateRecord) inverse?: PredicateKey; - specializes?: PredicateKey; + specializes?: readonly PredicateKey[]; // audit B1 — array: property hierarchies are DAGs, not trees contradicts?: readonly PredicateKey[]; // display @@ -183,9 +207,67 @@ export interface PredicateSpec { } ``` -`definePredicateRecord` stays the consistency gate: derive implications (`isSymmetric ⟹ inverse = self`; -a symmetric predicate cannot also declare a different `inverse`), verify inverse pairs mirror each other's -algebraic properties, and verify `contradicts` is declared symmetrically on both sides. +### 5.1 The complete validation/derivation rule set (audit B2) + +`definePredicateRecord` stays the single consistency gate. The DL literature gives the **complete** rule +set for our fields — enforce all of it, not just the obvious three: + +| # | Rule | Kind | +|---|---|---| +| 1 | `isSymmetric` ⟹ no distinct `inverse` (inverse = self) | error | +| 2 | Inverse mirroring: `P.inverse = Q ⟺ Q.inverse = P` | error | +| 3 | `contradicts` symmetric: `B ∈ A.contradicts ⟺ A ∈ B.contradicts` | error | +| 4 | `contradicts` irreflexive: `P ∉ P.contradicts` | error | +| 5 | `specializes` acyclic (no `P ⊑ … ⊑ P`) | error | +| 6 | P may not contradict its own `specializes` ancestor/descendant (`P ⊑ Q` ∧ `P ⊥ Q` ⟹ P unsatisfiable) | error | +| 7 | **Derive contradiction closure** down the hierarchy: `P ⊑ Q` ∧ `Q ⊥ R` ⟹ `P ⊥ R`, expanded at build time (else `vouchFor ⊑ trust`, `trust ⊥ distrust` leaves a `vouchFor`/`distrust` conflict undetected) | derivation | +| 8 | Inverse pairs mirror algebra: `isTransitive` equal (theorem), `temporalNature`/`claimType` equal; `polarity` equal | error / polarity=lint | +| 9 | `isSymmetric && isTransitive` ⟹ equivalence-relation behavior — allowed only for the explicit identity allow-list (`sameAs`); warn otherwise | lint | +| 10 | `polarity` present (sentiment predicate) ∧ `isTransitive` ⟹ error — sentiment is never transitive (Guha 2004; see `is-transitive.md`) | error | +| 11 | `isTransitive` is **not** inherited via `specializes` — documented invariant so the indexer never expands closure over sub-properties | doc guard | +| 12 | `literalType` present ⟹ `objectKind === 'literal'` | error | +| 13 | `objectKind === 'claim'` ∧ `inverse` present ⟹ warn (the inverse's subject would be a claim — near-always a modeling smell) | lint | +| 14 | `supersededBy` points at a non-deprecated spec; only valid when `status === 'deprecated'` | error | + +Rules 6, 7, and 11 are the silent-bug ones — contradiction × hierarchy interaction is invisible until a +conflict query misses, and "transitive-ish inheritance" is the most natural indexer bug to write. + +### 5.2 Mint-time canonicalization for symmetric & inverse predicates (audit A1 — blocking) + +OWL assumes reverse-edge synthesis is a free inference. **Intuition triples have deterministic IDs and +their own vaults** — so `⟨Acme, partnerOf, BigCo⟩` and `⟨BigCo, partnerOf, Acme⟩` are the *same fact* +hashing to **two triple IDs → two markets**, splitting stake. Same for inverse pairs +(`⟨Alice, employedBy, Acme⟩` vs `⟨Acme, employs, Alice⟩`). Wikidata's operational lesson is exactly this: +inverse pairs demanded permanent bot-sync, and the community's answer was canonical directions. We fix it +at mint, not with bots: + +1. **Symmetric predicates:** builders (`primitives`) canonically order subject/object (by atom ID) before + computing the triple ID — both user intents resolve to one triple, one market. +2. **Inverse pairs:** `definePredicateRecord` derives a **canonical direction** per pair (deterministic + rule, e.g. lexicographically smaller key is canonical); builders normalize an assertion in the + non-canonical direction to the canonical triple. The non-canonical predicate remains fully usable in + UI/queries — it's a *view*, not a second fact. +3. **Indexer backstop:** duplicates minted before the rule (or via raw protocol calls) are detected and + linked (the `sibling_triple_id` pattern, as for counter-triples), with an explicit aggregate-stake + presentation policy. + +Without this, edge synthesis *encourages* liquidity fragmentation — it's the one place the proposal was +economically unsound, and it's why canonicalization ships with the fields, not after them. + +### 5.3 Normative serialization mapping (audit B3 — makes P6 real) + +Interoperability is only "for free" if the field↔IRI mapping is pinned and implemented exactly: + +| Spec field | Serialized as | Standard | +|---|---|---| +| `isSymmetric: true` | `rdf:type owl:SymmetricProperty` | OWL 2 | +| `isTransitive: true` | `rdf:type owl:TransitiveProperty` | OWL 2 | +| `inverse` | `owl:inverseOf` | OWL 2 | +| `specializes` | `rdfs:subPropertyOf` (one entry per parent) | RDFS | +| `contradicts` | `owl:propertyDisjointWith` | OWL 2 | +| `supersededBy` | `schema:supersededBy` | schema.org | +| `literalType` | XSD datatype on the range (`xsd:anyURI`, `xsd:date`, `xsd:decimal`, …) | RDF/XSD | +| `objectKind` / `polarity` / `temporalNature` / `claimType` / `marketPattern` | Intuition-namespaced `PropertyValue` entries — our pragmatic extensions, no standard equivalent (that's fine) | — | --- @@ -226,6 +308,13 @@ more than that.* the expensive end of the frontier and we can add them the day a reasoner exists. - **Not** nesting — the data's destination and the entire standards tradition are flat. - **Not** asserting global, timeless truth so hard we can't add Cyc-style context-scoping later. +- **Not (yet) putting the metadata in the graph itself — but never designing that out.** Wikidata models + property metadata as ordinary statements *on the property entities*, which is why its community can + extend and contest property semantics without a software release. Our metadata lives in the TS package + + IPFS docs for now (deterministic, reviewable, ships with code) — but the end-state for a permissionless + graph is **self-description**: `⟨trust, contradicts, distrust⟩` as an actual, stakeable triple. The + design is compatible (predicates are atoms; the meta-relations are just more predicates); "the community + disagrees with our curation" eventually gets an on-protocol answer, not a GitHub issue. (Audit D2.) Restraint here is the design. The graph that ships the *minimal sufficient* property set — and computes all of it — beats the graph that declares axioms it can never run. diff --git a/packages/predicates/src/generated/specs/actor.ts b/packages/predicates/src/generated/specs/actor.ts index bb71964..b586341 100644 --- a/packages/predicates/src/generated/specs/actor.ts +++ b/packages/predicates/src/generated/specs/actor.ts @@ -8,4 +8,7 @@ export const actor = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/affiliatedWith.ts b/packages/predicates/src/generated/specs/affiliatedWith.ts index cb66f41..e411d4e 100644 --- a/packages/predicates/src/generated/specs/affiliatedWith.ts +++ b/packages/predicates/src/generated/specs/affiliatedWith.ts @@ -10,4 +10,7 @@ export const affiliatedWith = { category: 'Affiliation/Membership', status: 'proposed', isSymmetric: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/agreeWith.ts b/packages/predicates/src/generated/specs/agreeWith.ts index 565aa89..3650237 100644 --- a/packages/predicates/src/generated/specs/agreeWith.ts +++ b/packages/predicates/src/generated/specs/agreeWith.ts @@ -9,4 +9,9 @@ export const agreeWith = { thirdPerson: 'agrees with', category: 'Sentiment/Opinion', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['disagreeWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/alternativeTo.ts b/packages/predicates/src/generated/specs/alternativeTo.ts index 5fd39ab..1331d96 100644 --- a/packages/predicates/src/generated/specs/alternativeTo.ts +++ b/packages/predicates/src/generated/specs/alternativeTo.ts @@ -9,4 +9,7 @@ export const alternativeTo = { category: 'Curation/Containment', status: 'enshrined', isSymmetric: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/alumniOf.ts b/packages/predicates/src/generated/specs/alumniOf.ts index e16744e..8857a1a 100644 --- a/packages/predicates/src/generated/specs/alumniOf.ts +++ b/packages/predicates/src/generated/specs/alumniOf.ts @@ -8,4 +8,8 @@ export const alumniOf = { conjugates: false, category: 'Affiliation/Membership', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/areaServed.ts b/packages/predicates/src/generated/specs/areaServed.ts index 06519a3..a67238f 100644 --- a/packages/predicates/src/generated/specs/areaServed.ts +++ b/packages/predicates/src/generated/specs/areaServed.ts @@ -8,4 +8,7 @@ export const areaServed = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/attestedBy.ts b/packages/predicates/src/generated/specs/attestedBy.ts index fdf1dfd..3ce7d4e 100644 --- a/packages/predicates/src/generated/specs/attestedBy.ts +++ b/packages/predicates/src/generated/specs/attestedBy.ts @@ -8,4 +8,7 @@ export const attestedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'claim', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/auditedBy.ts b/packages/predicates/src/generated/specs/auditedBy.ts index 197f6ba..c4a5ba7 100644 --- a/packages/predicates/src/generated/specs/auditedBy.ts +++ b/packages/predicates/src/generated/specs/auditedBy.ts @@ -9,4 +9,7 @@ export const auditedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/authoredBy.ts b/packages/predicates/src/generated/specs/authoredBy.ts index a34bc57..8eab545 100644 --- a/packages/predicates/src/generated/specs/authoredBy.ts +++ b/packages/predicates/src/generated/specs/authoredBy.ts @@ -8,4 +8,7 @@ export const authoredBy = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/availableOn.ts b/packages/predicates/src/generated/specs/availableOn.ts index 24ecc1d..fcb6965 100644 --- a/packages/predicates/src/generated/specs/availableOn.ts +++ b/packages/predicates/src/generated/specs/availableOn.ts @@ -9,4 +9,7 @@ export const availableOn = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/backedBy.ts b/packages/predicates/src/generated/specs/backedBy.ts index bd1f911..63d50ab 100644 --- a/packages/predicates/src/generated/specs/backedBy.ts +++ b/packages/predicates/src/generated/specs/backedBy.ts @@ -9,4 +9,8 @@ export const backedBy = { conjugates: false, category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/bearishOn.ts b/packages/predicates/src/generated/specs/bearishOn.ts index ca65f06..1e4df26 100644 --- a/packages/predicates/src/generated/specs/bearishOn.ts +++ b/packages/predicates/src/generated/specs/bearishOn.ts @@ -9,4 +9,9 @@ export const bearishOn = { conjugates: false, category: 'Sentiment/Opinion', status: 'enshrined', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['bullishOn'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/betterThan.ts b/packages/predicates/src/generated/specs/betterThan.ts index c74df91..b71949e 100644 --- a/packages/predicates/src/generated/specs/betterThan.ts +++ b/packages/predicates/src/generated/specs/betterThan.ts @@ -9,4 +9,8 @@ export const betterThan = { conjugates: false, category: 'Comparison/Ranking', status: 'enshrined', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'evaluative', + contradicts: ['worseThan'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/blocked.ts b/packages/predicates/src/generated/specs/blocked.ts index ee376f3..2781359 100644 --- a/packages/predicates/src/generated/specs/blocked.ts +++ b/packages/predicates/src/generated/specs/blocked.ts @@ -8,4 +8,8 @@ export const blocked = { conjugates: false, category: 'Social/Reputation', status: 'proposed', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/branchOf.ts b/packages/predicates/src/generated/specs/branchOf.ts index 6ac9477..6541ab8 100644 --- a/packages/predicates/src/generated/specs/branchOf.ts +++ b/packages/predicates/src/generated/specs/branchOf.ts @@ -9,4 +9,8 @@ export const branchOf = { category: 'Affiliation/Membership', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/brand.ts b/packages/predicates/src/generated/specs/brand.ts index 0dd44d2..f1a8930 100644 --- a/packages/predicates/src/generated/specs/brand.ts +++ b/packages/predicates/src/generated/specs/brand.ts @@ -9,4 +9,7 @@ export const brand = { category: 'Identity/Classification', status: 'proposed', examples: ['(iPhone, brand, Apple)', '(AWS, brand, Amazon Web Services)'], + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/bullishOn.ts b/packages/predicates/src/generated/specs/bullishOn.ts index aa025db..a17b714 100644 --- a/packages/predicates/src/generated/specs/bullishOn.ts +++ b/packages/predicates/src/generated/specs/bullishOn.ts @@ -9,4 +9,9 @@ export const bullishOn = { conjugates: false, category: 'Sentiment/Opinion', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['bearishOn'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/byArtist.ts b/packages/predicates/src/generated/specs/byArtist.ts index af76a23..5b3b712 100644 --- a/packages/predicates/src/generated/specs/byArtist.ts +++ b/packages/predicates/src/generated/specs/byArtist.ts @@ -8,4 +8,7 @@ export const byArtist = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/certifiedBy.ts b/packages/predicates/src/generated/specs/certifiedBy.ts index e0fec35..91ef16d 100644 --- a/packages/predicates/src/generated/specs/certifiedBy.ts +++ b/packages/predicates/src/generated/specs/certifiedBy.ts @@ -9,4 +9,7 @@ export const certifiedBy = { conjugates: false, category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/citedBy.ts b/packages/predicates/src/generated/specs/citedBy.ts index 9ffbebd..15bd745 100644 --- a/packages/predicates/src/generated/specs/citedBy.ts +++ b/packages/predicates/src/generated/specs/citedBy.ts @@ -10,4 +10,8 @@ export const citedBy = { category: 'Provenance/Evidence', status: 'proposed', inversePredicate: 'reference', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'reference', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/compatibleWith.ts b/packages/predicates/src/generated/specs/compatibleWith.ts index 68a9cc8..c1cd987 100644 --- a/packages/predicates/src/generated/specs/compatibleWith.ts +++ b/packages/predicates/src/generated/specs/compatibleWith.ts @@ -10,4 +10,7 @@ export const compatibleWith = { category: 'Domain-Specific', status: 'proposed', isSymmetric: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/competeWith.ts b/packages/predicates/src/generated/specs/competeWith.ts index 6ef7cf7..9232682 100644 --- a/packages/predicates/src/generated/specs/competeWith.ts +++ b/packages/predicates/src/generated/specs/competeWith.ts @@ -10,4 +10,7 @@ export const competeWith = { category: 'Comparison/Ranking', status: 'proposed', isSymmetric: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/compliantWith.ts b/packages/predicates/src/generated/specs/compliantWith.ts index c3c38a8..8b63c05 100644 --- a/packages/predicates/src/generated/specs/compliantWith.ts +++ b/packages/predicates/src/generated/specs/compliantWith.ts @@ -9,4 +9,7 @@ export const compliantWith = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/confirmedBy.ts b/packages/predicates/src/generated/specs/confirmedBy.ts index d39dad6..20fb0bd 100644 --- a/packages/predicates/src/generated/specs/confirmedBy.ts +++ b/packages/predicates/src/generated/specs/confirmedBy.ts @@ -8,4 +8,7 @@ export const confirmedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'claim', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/contain.ts b/packages/predicates/src/generated/specs/contain.ts index 918ad26..c677802 100644 --- a/packages/predicates/src/generated/specs/contain.ts +++ b/packages/predicates/src/generated/specs/contain.ts @@ -11,4 +11,8 @@ export const contain = { status: 'enshrined', isHierarchical: true, inversePredicate: 'listed in', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'listedIn', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/containedInPlace.ts b/packages/predicates/src/generated/specs/containedInPlace.ts index 16426d4..9534ce7 100644 --- a/packages/predicates/src/generated/specs/containedInPlace.ts +++ b/packages/predicates/src/generated/specs/containedInPlace.ts @@ -11,4 +11,9 @@ export const containedInPlace = { isTransitive: true, isHierarchical: true, inversePredicate: 'contains place', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'containsPlace', + specializes: ['locatedIn'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/containsPlace.ts b/packages/predicates/src/generated/specs/containsPlace.ts index 3267403..4593a91 100644 --- a/packages/predicates/src/generated/specs/containsPlace.ts +++ b/packages/predicates/src/generated/specs/containsPlace.ts @@ -11,4 +11,8 @@ export const containsPlace = { isTransitive: true, isHierarchical: true, inversePredicate: 'contained in place', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'containedInPlace', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/contributedTo.ts b/packages/predicates/src/generated/specs/contributedTo.ts index 493306e..f9473d4 100644 --- a/packages/predicates/src/generated/specs/contributedTo.ts +++ b/packages/predicates/src/generated/specs/contributedTo.ts @@ -9,4 +9,7 @@ export const contributedTo = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/createdBy.ts b/packages/predicates/src/generated/specs/createdBy.ts index 16adc00..32ac5de 100644 --- a/packages/predicates/src/generated/specs/createdBy.ts +++ b/packages/predicates/src/generated/specs/createdBy.ts @@ -8,4 +8,7 @@ export const createdBy = { conjugates: false, category: 'Authorship/Contribution', status: 'enshrined', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/curatedBy.ts b/packages/predicates/src/generated/specs/curatedBy.ts index dc20a4e..bb5edea 100644 --- a/packages/predicates/src/generated/specs/curatedBy.ts +++ b/packages/predicates/src/generated/specs/curatedBy.ts @@ -9,4 +9,7 @@ export const curatedBy = { conjugates: false, category: 'Curation/Containment', status: 'enshrined', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/delegatedTo.ts b/packages/predicates/src/generated/specs/delegatedTo.ts index 65a101c..52f3694 100644 --- a/packages/predicates/src/generated/specs/delegatedTo.ts +++ b/packages/predicates/src/generated/specs/delegatedTo.ts @@ -9,4 +9,7 @@ export const delegatedTo = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/dependOn.ts b/packages/predicates/src/generated/specs/dependOn.ts index 590fa8d..d9402fd 100644 --- a/packages/predicates/src/generated/specs/dependOn.ts +++ b/packages/predicates/src/generated/specs/dependOn.ts @@ -10,4 +10,7 @@ export const dependOn = { category: 'Curation/Containment', status: 'proposed', isTransitive: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/deprecatedBy.ts b/packages/predicates/src/generated/specs/deprecatedBy.ts index ec98496..eee13cd 100644 --- a/packages/predicates/src/generated/specs/deprecatedBy.ts +++ b/packages/predicates/src/generated/specs/deprecatedBy.ts @@ -9,4 +9,7 @@ export const deprecatedBy = { conjugates: false, category: 'Temporal/Lifecycle', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/derivedFrom.ts b/packages/predicates/src/generated/specs/derivedFrom.ts index 1afde20..48fe89a 100644 --- a/packages/predicates/src/generated/specs/derivedFrom.ts +++ b/packages/predicates/src/generated/specs/derivedFrom.ts @@ -8,4 +8,7 @@ export const derivedFrom = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/director.ts b/packages/predicates/src/generated/specs/director.ts index 7c3cfd5..58d0d23 100644 --- a/packages/predicates/src/generated/specs/director.ts +++ b/packages/predicates/src/generated/specs/director.ts @@ -8,4 +8,7 @@ export const director = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/disagreeWith.ts b/packages/predicates/src/generated/specs/disagreeWith.ts index 686df63..0287ff5 100644 --- a/packages/predicates/src/generated/specs/disagreeWith.ts +++ b/packages/predicates/src/generated/specs/disagreeWith.ts @@ -9,4 +9,9 @@ export const disagreeWith = { thirdPerson: 'disagrees with', category: 'Sentiment/Opinion', status: 'enshrined', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['agreeWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/disputedBy.ts b/packages/predicates/src/generated/specs/disputedBy.ts index 6880d4f..3326db1 100644 --- a/packages/predicates/src/generated/specs/disputedBy.ts +++ b/packages/predicates/src/generated/specs/disputedBy.ts @@ -9,4 +9,7 @@ export const disputedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'claim', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/distrust.ts b/packages/predicates/src/generated/specs/distrust.ts index 9943f54..64281f5 100644 --- a/packages/predicates/src/generated/specs/distrust.ts +++ b/packages/predicates/src/generated/specs/distrust.ts @@ -10,4 +10,9 @@ export const distrust = { thirdPerson: 'distrusts', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['trust'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/employedBy.ts b/packages/predicates/src/generated/specs/employedBy.ts index 3d8a8b4..dfa6f50 100644 --- a/packages/predicates/src/generated/specs/employedBy.ts +++ b/packages/predicates/src/generated/specs/employedBy.ts @@ -8,4 +8,8 @@ export const employedBy = { conjugates: false, category: 'Affiliation/Membership', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/enabledBy.ts b/packages/predicates/src/generated/specs/enabledBy.ts index 698eeb3..bdcaa5f 100644 --- a/packages/predicates/src/generated/specs/enabledBy.ts +++ b/packages/predicates/src/generated/specs/enabledBy.ts @@ -9,4 +9,7 @@ export const enabledBy = { conjugates: false, category: 'Temporal/Lifecycle', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/endorse.ts b/packages/predicates/src/generated/specs/endorse.ts index be13f92..4000509 100644 --- a/packages/predicates/src/generated/specs/endorse.ts +++ b/packages/predicates/src/generated/specs/endorse.ts @@ -10,4 +10,9 @@ export const endorse = { thirdPerson: 'endorses', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + specializes: ['support'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/equivalentTo.ts b/packages/predicates/src/generated/specs/equivalentTo.ts index a89f0e5..4862a31 100644 --- a/packages/predicates/src/generated/specs/equivalentTo.ts +++ b/packages/predicates/src/generated/specs/equivalentTo.ts @@ -11,4 +11,7 @@ export const equivalentTo = { status: 'proposed', isTransitive: true, isSymmetric: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/evidencedBy.ts b/packages/predicates/src/generated/specs/evidencedBy.ts index 19c0cf5..36be97d 100644 --- a/packages/predicates/src/generated/specs/evidencedBy.ts +++ b/packages/predicates/src/generated/specs/evidencedBy.ts @@ -8,4 +8,7 @@ export const evidencedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'claim', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/expertIn.ts b/packages/predicates/src/generated/specs/expertIn.ts index b02ab84..c9e5365 100644 --- a/packages/predicates/src/generated/specs/expertIn.ts +++ b/packages/predicates/src/generated/specs/expertIn.ts @@ -9,4 +9,7 @@ export const expertIn = { conjugates: false, category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/featuredIn.ts b/packages/predicates/src/generated/specs/featuredIn.ts index 5c8d898..e07cbfe 100644 --- a/packages/predicates/src/generated/specs/featuredIn.ts +++ b/packages/predicates/src/generated/specs/featuredIn.ts @@ -9,4 +9,7 @@ export const featuredIn = { category: 'Curation/Containment', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/follow.ts b/packages/predicates/src/generated/specs/follow.ts index 2b45f8a..6fd00da 100644 --- a/packages/predicates/src/generated/specs/follow.ts +++ b/packages/predicates/src/generated/specs/follow.ts @@ -10,4 +10,7 @@ export const follow = { thirdPerson: 'follows', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/followedBy.ts b/packages/predicates/src/generated/specs/followedBy.ts index 724a25c..53b928c 100644 --- a/packages/predicates/src/generated/specs/followedBy.ts +++ b/packages/predicates/src/generated/specs/followedBy.ts @@ -11,4 +11,8 @@ export const followedBy = { status: 'proposed', isTransitive: true, inversePredicate: 'preceded by', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'precededBy', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/forkedFrom.ts b/packages/predicates/src/generated/specs/forkedFrom.ts index 57275c4..445f095 100644 --- a/packages/predicates/src/generated/specs/forkedFrom.ts +++ b/packages/predicates/src/generated/specs/forkedFrom.ts @@ -8,4 +8,7 @@ export const forkedFrom = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/founded.ts b/packages/predicates/src/generated/specs/founded.ts index a33d9f3..a83b316 100644 --- a/packages/predicates/src/generated/specs/founded.ts +++ b/packages/predicates/src/generated/specs/founded.ts @@ -9,4 +9,9 @@ export const founded = { category: 'Affiliation/Membership', status: 'proposed', inversePredicate: 'founder', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'founder', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/founder.ts b/packages/predicates/src/generated/specs/founder.ts index a11b33c..9d65a3b 100644 --- a/packages/predicates/src/generated/specs/founder.ts +++ b/packages/predicates/src/generated/specs/founder.ts @@ -10,4 +10,9 @@ export const founder = { category: 'Affiliation/Membership', status: 'proposed', inversePredicate: 'founded', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'founded', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/governedBy.ts b/packages/predicates/src/generated/specs/governedBy.ts index 73cb566..f3d3e59 100644 --- a/packages/predicates/src/generated/specs/governedBy.ts +++ b/packages/predicates/src/generated/specs/governedBy.ts @@ -9,4 +9,7 @@ export const governedBy = { conjugates: false, category: 'Domain-Specific', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hasCategory.ts b/packages/predicates/src/generated/specs/hasCategory.ts index 177444c..cfbc5eb 100644 --- a/packages/predicates/src/generated/specs/hasCategory.ts +++ b/packages/predicates/src/generated/specs/hasCategory.ts @@ -10,4 +10,7 @@ export const hasCategory = { category: 'Identity/Classification', status: 'enshrined', examples: ['(Uniswap, has category, DeFi)', '(Aave, has category, Lending)'], + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hasDescription.ts b/packages/predicates/src/generated/specs/hasDescription.ts index 86d286f..720c8a0 100644 --- a/packages/predicates/src/generated/specs/hasDescription.ts +++ b/packages/predicates/src/generated/specs/hasDescription.ts @@ -8,4 +8,8 @@ export const hasDescription = { conjugates: false, category: 'Metadata/Linking', status: 'enshrined', + objectKind: 'literal', + literalType: 'text', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hasSource.ts b/packages/predicates/src/generated/specs/hasSource.ts index 4c58272..49ab75c 100644 --- a/packages/predicates/src/generated/specs/hasSource.ts +++ b/packages/predicates/src/generated/specs/hasSource.ts @@ -8,4 +8,6 @@ export const hasSource = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hasTag.ts b/packages/predicates/src/generated/specs/hasTag.ts index 06db794..a67e5f7 100644 --- a/packages/predicates/src/generated/specs/hasTag.ts +++ b/packages/predicates/src/generated/specs/hasTag.ts @@ -10,4 +10,7 @@ export const hasTag = { category: 'Identity/Classification', status: 'enshrined', examples: ['(ETHGlobal, has tag, hackathon)', '(Rust, has tag, systems-programming)'], + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hasType.ts b/packages/predicates/src/generated/specs/hasType.ts index a6f080d..3a788e1 100644 --- a/packages/predicates/src/generated/specs/hasType.ts +++ b/packages/predicates/src/generated/specs/hasType.ts @@ -10,4 +10,7 @@ export const hasType = { category: 'Identity/Classification', status: 'enshrined', examples: ['(Uniswap, has type, Decentralized Exchange)', '(Ethereum, has type, Blockchain)'], + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/hiringOrganization.ts b/packages/predicates/src/generated/specs/hiringOrganization.ts index 3438b32..2b02731 100644 --- a/packages/predicates/src/generated/specs/hiringOrganization.ts +++ b/packages/predicates/src/generated/specs/hiringOrganization.ts @@ -8,4 +8,7 @@ export const hiringOrganization = { conjugates: false, category: 'Affiliation/Membership', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/imgUrl.ts b/packages/predicates/src/generated/specs/imgUrl.ts index 24899a6..994aa9b 100644 --- a/packages/predicates/src/generated/specs/imgUrl.ts +++ b/packages/predicates/src/generated/specs/imgUrl.ts @@ -9,4 +9,8 @@ export const imgUrl = { conjugates: false, category: 'Metadata/Linking', status: 'enshrined', + objectKind: 'literal', + literalType: 'image', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/implement.ts b/packages/predicates/src/generated/specs/implement.ts index df84422..98c1167 100644 --- a/packages/predicates/src/generated/specs/implement.ts +++ b/packages/predicates/src/generated/specs/implement.ts @@ -10,4 +10,7 @@ export const implement = { thirdPerson: 'implements', category: 'Domain-Specific', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/inAlbum.ts b/packages/predicates/src/generated/specs/inAlbum.ts index 668d3c8..d0f6592 100644 --- a/packages/predicates/src/generated/specs/inAlbum.ts +++ b/packages/predicates/src/generated/specs/inAlbum.ts @@ -8,4 +8,7 @@ export const inAlbum = { conjugates: false, category: 'Curation/Containment', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/inPlaylist.ts b/packages/predicates/src/generated/specs/inPlaylist.ts index 662ca23..eee3e52 100644 --- a/packages/predicates/src/generated/specs/inPlaylist.ts +++ b/packages/predicates/src/generated/specs/inPlaylist.ts @@ -8,4 +8,7 @@ export const inPlaylist = { conjugates: false, category: 'Curation/Containment', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/inspiredBy.ts b/packages/predicates/src/generated/specs/inspiredBy.ts index a1da1d5..c85fd53 100644 --- a/packages/predicates/src/generated/specs/inspiredBy.ts +++ b/packages/predicates/src/generated/specs/inspiredBy.ts @@ -9,4 +9,7 @@ export const inspiredBy = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/investedIn.ts b/packages/predicates/src/generated/specs/investedIn.ts index eebf562..6ab0cb9 100644 --- a/packages/predicates/src/generated/specs/investedIn.ts +++ b/packages/predicates/src/generated/specs/investedIn.ts @@ -8,4 +8,7 @@ export const investedIn = { conjugates: false, category: 'Affiliation/Membership', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/itemReviewed.ts b/packages/predicates/src/generated/specs/itemReviewed.ts index e315ec1..898210e 100644 --- a/packages/predicates/src/generated/specs/itemReviewed.ts +++ b/packages/predicates/src/generated/specs/itemReviewed.ts @@ -8,4 +8,7 @@ export const itemReviewed = { conjugates: false, category: 'Social/Reputation', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/jobLocation.ts b/packages/predicates/src/generated/specs/jobLocation.ts index 0eb115d..7d8216e 100644 --- a/packages/predicates/src/generated/specs/jobLocation.ts +++ b/packages/predicates/src/generated/specs/jobLocation.ts @@ -8,4 +8,7 @@ export const jobLocation = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/learnedFrom.ts b/packages/predicates/src/generated/specs/learnedFrom.ts index 872a162..51c262c 100644 --- a/packages/predicates/src/generated/specs/learnedFrom.ts +++ b/packages/predicates/src/generated/specs/learnedFrom.ts @@ -9,4 +9,7 @@ export const learnedFrom = { conjugates: false, category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/like.ts b/packages/predicates/src/generated/specs/like.ts index f66ea4a..5668430 100644 --- a/packages/predicates/src/generated/specs/like.ts +++ b/packages/predicates/src/generated/specs/like.ts @@ -9,4 +9,8 @@ export const like = { thirdPerson: 'likes', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/linkedAccount.ts b/packages/predicates/src/generated/specs/linkedAccount.ts index 9a62d86..7e93d54 100644 --- a/packages/predicates/src/generated/specs/linkedAccount.ts +++ b/packages/predicates/src/generated/specs/linkedAccount.ts @@ -8,4 +8,7 @@ export const linkedAccount = { conjugates: false, category: 'Metadata/Linking', status: 'enshrined', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/listedIn.ts b/packages/predicates/src/generated/specs/listedIn.ts index 10c4be3..f452153 100644 --- a/packages/predicates/src/generated/specs/listedIn.ts +++ b/packages/predicates/src/generated/specs/listedIn.ts @@ -11,4 +11,8 @@ export const listedIn = { status: 'enshrined', isHierarchical: true, inversePredicate: 'contain', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'contain', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/listedOn.ts b/packages/predicates/src/generated/specs/listedOn.ts index b26e041..1a67815 100644 --- a/packages/predicates/src/generated/specs/listedOn.ts +++ b/packages/predicates/src/generated/specs/listedOn.ts @@ -9,4 +9,7 @@ export const listedOn = { conjugates: false, category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/locatedIn.ts b/packages/predicates/src/generated/specs/locatedIn.ts index 1df097d..27f83a5 100644 --- a/packages/predicates/src/generated/specs/locatedIn.ts +++ b/packages/predicates/src/generated/specs/locatedIn.ts @@ -11,4 +11,7 @@ export const locatedIn = { status: 'proposed', isTransitive: true, isHierarchical: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/manufacturer.ts b/packages/predicates/src/generated/specs/manufacturer.ts index c9e715c..cc21253 100644 --- a/packages/predicates/src/generated/specs/manufacturer.ts +++ b/packages/predicates/src/generated/specs/manufacturer.ts @@ -8,4 +8,7 @@ export const manufacturer = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/memberOf.ts b/packages/predicates/src/generated/specs/memberOf.ts index 726a6fe..2767518 100644 --- a/packages/predicates/src/generated/specs/memberOf.ts +++ b/packages/predicates/src/generated/specs/memberOf.ts @@ -9,4 +9,8 @@ export const memberOf = { category: 'Affiliation/Membership', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/mentorOf.ts b/packages/predicates/src/generated/specs/mentorOf.ts index 0130f6d..45eef5d 100644 --- a/packages/predicates/src/generated/specs/mentorOf.ts +++ b/packages/predicates/src/generated/specs/mentorOf.ts @@ -9,4 +9,8 @@ export const mentorOf = { category: 'Knowledge/Expertise', status: 'proposed', inversePredicate: 'student of', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'studentOf', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/musicBy.ts b/packages/predicates/src/generated/specs/musicBy.ts index d60f235..366777c 100644 --- a/packages/predicates/src/generated/specs/musicBy.ts +++ b/packages/predicates/src/generated/specs/musicBy.ts @@ -8,4 +8,7 @@ export const musicBy = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/musicGroupMember.ts b/packages/predicates/src/generated/specs/musicGroupMember.ts index 9bd8739..000bc37 100644 --- a/packages/predicates/src/generated/specs/musicGroupMember.ts +++ b/packages/predicates/src/generated/specs/musicGroupMember.ts @@ -9,4 +9,7 @@ export const musicGroupMember = { category: 'Affiliation/Membership', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/neutralOn.ts b/packages/predicates/src/generated/specs/neutralOn.ts index 8ab0c2b..8eff021 100644 --- a/packages/predicates/src/generated/specs/neutralOn.ts +++ b/packages/predicates/src/generated/specs/neutralOn.ts @@ -9,4 +9,8 @@ export const neutralOn = { conjugates: false, category: 'Sentiment/Opinion', status: 'proposed', + objectKind: 'entity', + polarity: 'neutral', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/oppose.ts b/packages/predicates/src/generated/specs/oppose.ts index 040c5f4..e5e5dc4 100644 --- a/packages/predicates/src/generated/specs/oppose.ts +++ b/packages/predicates/src/generated/specs/oppose.ts @@ -10,4 +10,9 @@ export const oppose = { thirdPerson: 'opposes', category: 'Sentiment/Opinion', status: 'proposed', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['support'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/organizer.ts b/packages/predicates/src/generated/specs/organizer.ts index a69def8..37d5bb9 100644 --- a/packages/predicates/src/generated/specs/organizer.ts +++ b/packages/predicates/src/generated/specs/organizer.ts @@ -8,4 +8,7 @@ export const organizer = { conjugates: false, category: 'Affiliation/Membership', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/outperform.ts b/packages/predicates/src/generated/specs/outperform.ts index 1c551eb..e03953a 100644 --- a/packages/predicates/src/generated/specs/outperform.ts +++ b/packages/predicates/src/generated/specs/outperform.ts @@ -10,4 +10,7 @@ export const outperform = { thirdPerson: 'outperforms', category: 'Comparison/Ranking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/parentItem.ts b/packages/predicates/src/generated/specs/parentItem.ts index 3e7f2db..2144759 100644 --- a/packages/predicates/src/generated/specs/parentItem.ts +++ b/packages/predicates/src/generated/specs/parentItem.ts @@ -9,4 +9,7 @@ export const parentItem = { category: 'Curation/Containment', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/parentOrganization.ts b/packages/predicates/src/generated/specs/parentOrganization.ts index d8661e5..b117984 100644 --- a/packages/predicates/src/generated/specs/parentOrganization.ts +++ b/packages/predicates/src/generated/specs/parentOrganization.ts @@ -11,4 +11,8 @@ export const parentOrganization = { isTransitive: true, isHierarchical: true, inversePredicate: 'sub organization', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'subOrganization', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/partOfSeries.ts b/packages/predicates/src/generated/specs/partOfSeries.ts index d8ab892..2505a9b 100644 --- a/packages/predicates/src/generated/specs/partOfSeries.ts +++ b/packages/predicates/src/generated/specs/partOfSeries.ts @@ -8,4 +8,7 @@ export const partOfSeries = { conjugates: false, category: 'Curation/Containment', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/partnerOf.ts b/packages/predicates/src/generated/specs/partnerOf.ts index ee80772..9d94844 100644 --- a/packages/predicates/src/generated/specs/partnerOf.ts +++ b/packages/predicates/src/generated/specs/partnerOf.ts @@ -9,4 +9,8 @@ export const partnerOf = { category: 'Affiliation/Membership', status: 'proposed', isSymmetric: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + specializes: ['affiliatedWith'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/peggedTo.ts b/packages/predicates/src/generated/specs/peggedTo.ts index 50c4ea5..5c1b426 100644 --- a/packages/predicates/src/generated/specs/peggedTo.ts +++ b/packages/predicates/src/generated/specs/peggedTo.ts @@ -9,4 +9,7 @@ export const peggedTo = { conjugates: false, category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/performer.ts b/packages/predicates/src/generated/specs/performer.ts index 34e48d3..f6fa898 100644 --- a/packages/predicates/src/generated/specs/performer.ts +++ b/packages/predicates/src/generated/specs/performer.ts @@ -8,4 +8,7 @@ export const performer = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/photo.ts b/packages/predicates/src/generated/specs/photo.ts index 91b080f..3fb182d 100644 --- a/packages/predicates/src/generated/specs/photo.ts +++ b/packages/predicates/src/generated/specs/photo.ts @@ -8,4 +8,8 @@ export const photo = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'literal', + literalType: 'image', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/pinnedIn.ts b/packages/predicates/src/generated/specs/pinnedIn.ts index e2a2734..7a46633 100644 --- a/packages/predicates/src/generated/specs/pinnedIn.ts +++ b/packages/predicates/src/generated/specs/pinnedIn.ts @@ -10,4 +10,7 @@ export const pinnedIn = { category: 'Curation/Containment', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/precededBy.ts b/packages/predicates/src/generated/specs/precededBy.ts index 871c8f4..0eee53f 100644 --- a/packages/predicates/src/generated/specs/precededBy.ts +++ b/packages/predicates/src/generated/specs/precededBy.ts @@ -11,4 +11,8 @@ export const precededBy = { status: 'proposed', isTransitive: true, inversePredicate: 'followed by', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'followedBy', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/predecessorOf.ts b/packages/predicates/src/generated/specs/predecessorOf.ts index fc2dc69..9ee982c 100644 --- a/packages/predicates/src/generated/specs/predecessorOf.ts +++ b/packages/predicates/src/generated/specs/predecessorOf.ts @@ -9,4 +9,8 @@ export const predecessorOf = { category: 'Comparison/Ranking', status: 'proposed', inversePredicate: 'successor of', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'successorOf', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/pricedIn.ts b/packages/predicates/src/generated/specs/pricedIn.ts index 5fc3f31..fe7374e 100644 --- a/packages/predicates/src/generated/specs/pricedIn.ts +++ b/packages/predicates/src/generated/specs/pricedIn.ts @@ -9,4 +9,7 @@ export const pricedIn = { conjugates: false, category: 'Domain-Specific', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/primaryImageOfPage.ts b/packages/predicates/src/generated/specs/primaryImageOfPage.ts index ca9ef2f..90dfed1 100644 --- a/packages/predicates/src/generated/specs/primaryImageOfPage.ts +++ b/packages/predicates/src/generated/specs/primaryImageOfPage.ts @@ -8,4 +8,8 @@ export const primaryImageOfPage = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'literal', + literalType: 'image', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/productionCompany.ts b/packages/predicates/src/generated/specs/productionCompany.ts index 6204f3b..dfadd30 100644 --- a/packages/predicates/src/generated/specs/productionCompany.ts +++ b/packages/predicates/src/generated/specs/productionCompany.ts @@ -8,4 +8,7 @@ export const productionCompany = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/proposed.ts b/packages/predicates/src/generated/specs/proposed.ts index edf4998..74f3389 100644 --- a/packages/predicates/src/generated/specs/proposed.ts +++ b/packages/predicates/src/generated/specs/proposed.ts @@ -9,4 +9,7 @@ export const proposed = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + temporalNature: 'event', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/provider.ts b/packages/predicates/src/generated/specs/provider.ts index 29d9679..5e786b8 100644 --- a/packages/predicates/src/generated/specs/provider.ts +++ b/packages/predicates/src/generated/specs/provider.ts @@ -9,4 +9,7 @@ export const provider = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/publishedAt.ts b/packages/predicates/src/generated/specs/publishedAt.ts index 242ba2c..93fe2e6 100644 --- a/packages/predicates/src/generated/specs/publishedAt.ts +++ b/packages/predicates/src/generated/specs/publishedAt.ts @@ -9,4 +9,7 @@ export const publishedAt = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/publisher.ts b/packages/predicates/src/generated/specs/publisher.ts index 4e45509..35afe2d 100644 --- a/packages/predicates/src/generated/specs/publisher.ts +++ b/packages/predicates/src/generated/specs/publisher.ts @@ -9,4 +9,7 @@ export const publisher = { conjugates: false, category: 'Authorship/Contribution', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/rankedAbove.ts b/packages/predicates/src/generated/specs/rankedAbove.ts index 97206a8..fba8584 100644 --- a/packages/predicates/src/generated/specs/rankedAbove.ts +++ b/packages/predicates/src/generated/specs/rankedAbove.ts @@ -9,4 +9,7 @@ export const rankedAbove = { conjugates: false, category: 'Curation/Containment', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/recommend.ts b/packages/predicates/src/generated/specs/recommend.ts index 55db37b..94246d9 100644 --- a/packages/predicates/src/generated/specs/recommend.ts +++ b/packages/predicates/src/generated/specs/recommend.ts @@ -10,4 +10,8 @@ export const recommend = { thirdPerson: 'recommends', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/reference.ts b/packages/predicates/src/generated/specs/reference.ts index a503b0a..f68554d 100644 --- a/packages/predicates/src/generated/specs/reference.ts +++ b/packages/predicates/src/generated/specs/reference.ts @@ -11,4 +11,8 @@ export const reference = { category: 'Provenance/Evidence', status: 'proposed', inversePredicate: 'cited by', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'citedBy', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/regulatedBy.ts b/packages/predicates/src/generated/specs/regulatedBy.ts index bf7d632..83630ac 100644 --- a/packages/predicates/src/generated/specs/regulatedBy.ts +++ b/packages/predicates/src/generated/specs/regulatedBy.ts @@ -9,4 +9,7 @@ export const regulatedBy = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/replacedBy.ts b/packages/predicates/src/generated/specs/replacedBy.ts index 593bd28..917fcad 100644 --- a/packages/predicates/src/generated/specs/replacedBy.ts +++ b/packages/predicates/src/generated/specs/replacedBy.ts @@ -9,4 +9,7 @@ export const replacedBy = { conjugates: false, category: 'Temporal/Lifecycle', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/reported.ts b/packages/predicates/src/generated/specs/reported.ts index 8ccd65e..0c123a4 100644 --- a/packages/predicates/src/generated/specs/reported.ts +++ b/packages/predicates/src/generated/specs/reported.ts @@ -8,4 +8,8 @@ export const reported = { conjugates: false, category: 'Social/Reputation', status: 'proposed', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'event', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/reviewed.ts b/packages/predicates/src/generated/specs/reviewed.ts index 3158f17..d0c7594 100644 --- a/packages/predicates/src/generated/specs/reviewed.ts +++ b/packages/predicates/src/generated/specs/reviewed.ts @@ -8,4 +8,8 @@ export const reviewed = { conjugates: false, category: 'Social/Reputation', status: 'proposed', + objectKind: 'entity', + temporalNature: 'event', + claimType: 'evaluative', + inverse: 'reviewedBy', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/reviewedBy.ts b/packages/predicates/src/generated/specs/reviewedBy.ts index 225180c..f148e71 100644 --- a/packages/predicates/src/generated/specs/reviewedBy.ts +++ b/packages/predicates/src/generated/specs/reviewedBy.ts @@ -8,4 +8,8 @@ export const reviewedBy = { conjugates: false, category: 'Social/Reputation', status: 'proposed', + objectKind: 'entity', + temporalNature: 'event', + claimType: 'evaluative', + inverse: 'reviewed', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/reward.ts b/packages/predicates/src/generated/specs/reward.ts index 60516fd..6ea10de 100644 --- a/packages/predicates/src/generated/specs/reward.ts +++ b/packages/predicates/src/generated/specs/reward.ts @@ -10,4 +10,8 @@ export const reward = { thirdPerson: 'rewards', category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/sameAs.ts b/packages/predicates/src/generated/specs/sameAs.ts index b219e7e..75a1fe9 100644 --- a/packages/predicates/src/generated/specs/sameAs.ts +++ b/packages/predicates/src/generated/specs/sameAs.ts @@ -13,4 +13,7 @@ export const sameAs = { isTransitive: true, isSymmetric: true, inversePredicate: 'same as', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/skepticalOf.ts b/packages/predicates/src/generated/specs/skepticalOf.ts index 5033fff..9270645 100644 --- a/packages/predicates/src/generated/specs/skepticalOf.ts +++ b/packages/predicates/src/generated/specs/skepticalOf.ts @@ -9,4 +9,8 @@ export const skepticalOf = { conjugates: false, category: 'Sentiment/Opinion', status: 'proposed', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'state', + claimType: 'evaluative', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/softwareAddOn.ts b/packages/predicates/src/generated/specs/softwareAddOn.ts index 3c9e5d8..7612290 100644 --- a/packages/predicates/src/generated/specs/softwareAddOn.ts +++ b/packages/predicates/src/generated/specs/softwareAddOn.ts @@ -9,4 +9,7 @@ export const softwareAddOn = { conjugates: false, category: 'Domain-Specific', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/speak.ts b/packages/predicates/src/generated/specs/speak.ts index 5fae480..25d264c 100644 --- a/packages/predicates/src/generated/specs/speak.ts +++ b/packages/predicates/src/generated/specs/speak.ts @@ -10,4 +10,7 @@ export const speak = { thirdPerson: 'speaks', category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/sponsoredBy.ts b/packages/predicates/src/generated/specs/sponsoredBy.ts index 7df8dd0..dc05f7b 100644 --- a/packages/predicates/src/generated/specs/sponsoredBy.ts +++ b/packages/predicates/src/generated/specs/sponsoredBy.ts @@ -9,4 +9,7 @@ export const sponsoredBy = { conjugates: false, category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/stakedIn.ts b/packages/predicates/src/generated/specs/stakedIn.ts index 346d450..a389ab4 100644 --- a/packages/predicates/src/generated/specs/stakedIn.ts +++ b/packages/predicates/src/generated/specs/stakedIn.ts @@ -9,4 +9,7 @@ export const stakedIn = { conjugates: false, category: 'Economic/Market', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/studentOf.ts b/packages/predicates/src/generated/specs/studentOf.ts index 7fa3e5f..8f9f2df 100644 --- a/packages/predicates/src/generated/specs/studentOf.ts +++ b/packages/predicates/src/generated/specs/studentOf.ts @@ -10,4 +10,9 @@ export const studentOf = { category: 'Knowledge/Expertise', status: 'proposed', inversePredicate: 'mentor of', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'mentorOf', + specializes: ['learnedFrom'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/studied.ts b/packages/predicates/src/generated/specs/studied.ts index e3f236a..1903201 100644 --- a/packages/predicates/src/generated/specs/studied.ts +++ b/packages/predicates/src/generated/specs/studied.ts @@ -9,4 +9,7 @@ export const studied = { conjugates: false, category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/subEvent.ts b/packages/predicates/src/generated/specs/subEvent.ts index e4852e6..4ed541a 100644 --- a/packages/predicates/src/generated/specs/subEvent.ts +++ b/packages/predicates/src/generated/specs/subEvent.ts @@ -11,4 +11,8 @@ export const subEvent = { isTransitive: true, isHierarchical: true, inversePredicate: 'super event', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'superEvent', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/subOrganization.ts b/packages/predicates/src/generated/specs/subOrganization.ts index dddd838..9b2c10e 100644 --- a/packages/predicates/src/generated/specs/subOrganization.ts +++ b/packages/predicates/src/generated/specs/subOrganization.ts @@ -11,4 +11,8 @@ export const subOrganization = { isTransitive: true, isHierarchical: true, inversePredicate: 'parent organization', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', + inverse: 'parentOrganization', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/successorOf.ts b/packages/predicates/src/generated/specs/successorOf.ts index 9d2cb4e..83c4382 100644 --- a/packages/predicates/src/generated/specs/successorOf.ts +++ b/packages/predicates/src/generated/specs/successorOf.ts @@ -9,4 +9,8 @@ export const successorOf = { category: 'Comparison/Ranking', status: 'proposed', inversePredicate: 'predecessor of', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'predecessorOf', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/superEvent.ts b/packages/predicates/src/generated/specs/superEvent.ts index 12c1db6..f066743 100644 --- a/packages/predicates/src/generated/specs/superEvent.ts +++ b/packages/predicates/src/generated/specs/superEvent.ts @@ -11,4 +11,8 @@ export const superEvent = { isTransitive: true, isHierarchical: true, inversePredicate: 'sub event', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', + inverse: 'subEvent', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/supersede.ts b/packages/predicates/src/generated/specs/supersede.ts index 364daf0..842e3ac 100644 --- a/packages/predicates/src/generated/specs/supersede.ts +++ b/packages/predicates/src/generated/specs/supersede.ts @@ -9,4 +9,7 @@ export const supersede = { conjugates: false, category: 'Comparison/Ranking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/support.ts b/packages/predicates/src/generated/specs/support.ts index 55f2a04..4d5ae99 100644 --- a/packages/predicates/src/generated/specs/support.ts +++ b/packages/predicates/src/generated/specs/support.ts @@ -10,4 +10,9 @@ export const support = { thirdPerson: 'supports', category: 'Sentiment/Opinion', status: 'proposed', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['oppose'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/targetProduct.ts b/packages/predicates/src/generated/specs/targetProduct.ts index fb0b2c0..ea1a0dc 100644 --- a/packages/predicates/src/generated/specs/targetProduct.ts +++ b/packages/predicates/src/generated/specs/targetProduct.ts @@ -9,4 +9,7 @@ export const targetProduct = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/teach.ts b/packages/predicates/src/generated/specs/teach.ts index 4e9182f..47cd1ea 100644 --- a/packages/predicates/src/generated/specs/teach.ts +++ b/packages/predicates/src/generated/specs/teach.ts @@ -9,4 +9,7 @@ export const teach = { thirdPerson: 'teaches', category: 'Knowledge/Expertise', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/track.ts b/packages/predicates/src/generated/specs/track.ts index c53afb5..201bdcc 100644 --- a/packages/predicates/src/generated/specs/track.ts +++ b/packages/predicates/src/generated/specs/track.ts @@ -10,4 +10,7 @@ export const track = { category: 'Curation/Containment', status: 'proposed', isHierarchical: true, + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/trailer.ts b/packages/predicates/src/generated/specs/trailer.ts index e6bcdc4..b66b054 100644 --- a/packages/predicates/src/generated/specs/trailer.ts +++ b/packages/predicates/src/generated/specs/trailer.ts @@ -8,4 +8,7 @@ export const trailer = { conjugates: false, category: 'Metadata/Linking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/triggered.ts b/packages/predicates/src/generated/specs/triggered.ts index a1eac03..7867fa7 100644 --- a/packages/predicates/src/generated/specs/triggered.ts +++ b/packages/predicates/src/generated/specs/triggered.ts @@ -9,4 +9,7 @@ export const triggered = { conjugates: false, category: 'Temporal/Lifecycle', status: 'proposed', + objectKind: 'entity', + temporalNature: 'event', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/trust.ts b/packages/predicates/src/generated/specs/trust.ts index cfb4413..595d65b 100644 --- a/packages/predicates/src/generated/specs/trust.ts +++ b/packages/predicates/src/generated/specs/trust.ts @@ -10,4 +10,9 @@ export const trust = { thirdPerson: 'trusts', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + contradicts: ['distrust'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/url.ts b/packages/predicates/src/generated/specs/url.ts index f322e9c..40fec9a 100644 --- a/packages/predicates/src/generated/specs/url.ts +++ b/packages/predicates/src/generated/specs/url.ts @@ -8,4 +8,8 @@ export const url = { conjugates: false, category: 'Metadata/Linking', status: 'enshrined', + objectKind: 'literal', + literalType: 'url', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/use.ts b/packages/predicates/src/generated/specs/use.ts index bb6b8df..d69a75d 100644 --- a/packages/predicates/src/generated/specs/use.ts +++ b/packages/predicates/src/generated/specs/use.ts @@ -10,4 +10,7 @@ export const use = { thirdPerson: 'uses', category: 'Domain-Specific', status: 'proposed', + objectKind: 'entity', + temporalNature: 'state', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/verifiedBy.ts b/packages/predicates/src/generated/specs/verifiedBy.ts index 6bcfe69..302ecaf 100644 --- a/packages/predicates/src/generated/specs/verifiedBy.ts +++ b/packages/predicates/src/generated/specs/verifiedBy.ts @@ -9,4 +9,7 @@ export const verifiedBy = { conjugates: false, category: 'Provenance/Evidence', status: 'proposed', + objectKind: 'claim', + temporalNature: 'permanent', + claimType: 'factual', } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/votedAgainst.ts b/packages/predicates/src/generated/specs/votedAgainst.ts index 85f8998..fbcbca9 100644 --- a/packages/predicates/src/generated/specs/votedAgainst.ts +++ b/packages/predicates/src/generated/specs/votedAgainst.ts @@ -8,4 +8,9 @@ export const votedAgainst = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + polarity: 'negative', + temporalNature: 'event', + claimType: 'factual', + contradicts: ['votedFor'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/votedFor.ts b/packages/predicates/src/generated/specs/votedFor.ts index e25e979..48d77e8 100644 --- a/packages/predicates/src/generated/specs/votedFor.ts +++ b/packages/predicates/src/generated/specs/votedFor.ts @@ -8,4 +8,9 @@ export const votedFor = { conjugates: false, category: 'Governance/Policy', status: 'proposed', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'event', + claimType: 'factual', + contradicts: ['votedAgainst'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/vouchFor.ts b/packages/predicates/src/generated/specs/vouchFor.ts index 328b11a..6b0143d 100644 --- a/packages/predicates/src/generated/specs/vouchFor.ts +++ b/packages/predicates/src/generated/specs/vouchFor.ts @@ -10,4 +10,9 @@ export const vouchFor = { thirdPerson: 'vouches for', category: 'Social/Reputation', status: 'enshrined', + objectKind: 'entity', + polarity: 'positive', + temporalNature: 'state', + claimType: 'evaluative', + specializes: ['support'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/generated/specs/worseThan.ts b/packages/predicates/src/generated/specs/worseThan.ts index 1b367a1..7e7b470 100644 --- a/packages/predicates/src/generated/specs/worseThan.ts +++ b/packages/predicates/src/generated/specs/worseThan.ts @@ -9,4 +9,8 @@ export const worseThan = { conjugates: false, category: 'Comparison/Ranking', status: 'proposed', + objectKind: 'entity', + temporalNature: 'permanent', + claimType: 'evaluative', + contradicts: ['betterThan'], } as const satisfies PredicateSpec; diff --git a/packages/predicates/src/index.ts b/packages/predicates/src/index.ts index 1facd8b..2f9941e 100644 --- a/packages/predicates/src/index.ts +++ b/packages/predicates/src/index.ts @@ -7,3 +7,4 @@ export * from './ipfs'; export * from './launch'; export * from './predicates'; export * from './types'; +export * from './validate'; diff --git a/packages/predicates/src/ipfs.ts b/packages/predicates/src/ipfs.ts index a3daa20..70372b9 100644 --- a/packages/predicates/src/ipfs.ts +++ b/packages/predicates/src/ipfs.ts @@ -68,6 +68,27 @@ export function buildPredicateIpfsDocument( }); } + // Machine-readable semantics (serialized per predicate-spec-decisions.md §5.3). Only emitted when set. + const optionalProperties: Array< + [PredicateIpfsDocument['additionalProperty'][number]['name'], unknown] + > = [ + ['objectKind', options.objectKind], + ['literalType', options.literalType], + ['polarity', options.polarity], + ['temporalNature', options.temporalNature], + ['claimType', options.claimType], + ['inverse', options.inverse], + ['specializes', options.specializes], + ['contradicts', options.contradicts], + ['supersededBy', options.supersededBy], + ]; + + for (const [name, value] of optionalProperties) { + if (value === undefined) continue; + if (Array.isArray(value) && value.length === 0) continue; + additionalProperty.push({ '@type': 'PropertyValue', name, value }); + } + const document: PredicateIpfsDocument = { '@context': 'https://schema.org/', '@type': 'DefinedTerm', diff --git a/packages/predicates/src/record.ts b/packages/predicates/src/record.ts index 7eb7db2..7c476f1 100644 --- a/packages/predicates/src/record.ts +++ b/packages/predicates/src/record.ts @@ -1,8 +1,55 @@ import type { PredicateRecord, PredicateSpec } from './types.js'; +/** + * Per-spec consistency gate. Checks the rules that need only a single spec (see + * `.planning/predicate-spec-decisions.md` §5.1). Cross-referential rules that need the whole set + * (inverse mirroring, contradicts symmetry, specializes acyclicity, key existence) live in + * `validate.ts` and run over `PREDICATE_SPECS`. + */ +function assertSpecConsistency(spec: PredicateSpec): void { + const where = `predicate "${spec.key}"`; + + // Rule 1 — isSymmetric ⟹ no distinct `inverse` (a symmetric predicate is its own inverse). + if (spec.isSymmetric && spec.inverse && spec.inverse !== spec.key) { + throw new Error( + `${where}: isSymmetric predicates are their own inverse; remove inverse: "${spec.inverse}".` + ); + } + + // Rule 4 — contradicts is irreflexive. + if (spec.contradicts?.includes(spec.key)) { + throw new Error(`${where}: contradicts must not include the predicate itself.`); + } + + // Rule 10 — sentiment is never transitive (Guha 2004). polarity ∧ isTransitive is incoherent. + if (spec.polarity !== undefined && spec.isTransitive) { + throw new Error( + `${where}: a predicate with polarity ("${spec.polarity}") cannot be isTransitive — sentiment does not propagate transitively.` + ); + } + + // Rule 12 — literalType only means anything for a literal object. + if (spec.literalType !== undefined && spec.objectKind !== 'literal') { + throw new Error( + `${where}: literalType ("${spec.literalType}") requires objectKind: 'literal' (got ${ + spec.objectKind ?? 'undefined' + }).` + ); + } + + // Rule 14 (partial) — supersededBy only makes sense on a deprecated predicate. + if (spec.supersededBy !== undefined && spec.status !== 'deprecated') { + throw new Error( + `${where}: supersededBy is only valid when status === 'deprecated' (got '${spec.status}').` + ); + } +} + export function definePredicateRecord( spec: TSpec ): PredicateRecord { + assertSpecConsistency(spec); + return { key: spec.key, name: spec.name, @@ -17,5 +64,14 @@ export function definePredicateRecord( ...(spec.thirdPerson ? { thirdPerson: spec.thirdPerson } : {}), ...(spec.examples ? { examples: spec.examples } : {}), ...(spec.inversePredicate ? { inversePredicate: spec.inversePredicate } : {}), + ...(spec.objectKind ? { objectKind: spec.objectKind } : {}), + ...(spec.literalType ? { literalType: spec.literalType } : {}), + ...(spec.polarity ? { polarity: spec.polarity } : {}), + ...(spec.temporalNature ? { temporalNature: spec.temporalNature } : {}), + ...(spec.claimType ? { claimType: spec.claimType } : {}), + ...(spec.inverse ? { inverse: spec.inverse } : {}), + ...(spec.specializes ? { specializes: spec.specializes } : {}), + ...(spec.contradicts ? { contradicts: spec.contradicts } : {}), + ...(spec.supersededBy ? { supersededBy: spec.supersededBy } : {}), }; } diff --git a/packages/predicates/src/types.ts b/packages/predicates/src/types.ts index 609eeb8..296c26d 100644 --- a/packages/predicates/src/types.ts +++ b/packages/predicates/src/types.ts @@ -3,6 +3,29 @@ export type PredicateStorageStrategy = 'inline' | 'ipfs'; export type PredicateForm = 'base' | 'thirdPerson' | 'pastParticiple' | 'displayName'; export type TextDirection = 'ltr' | 'rtl'; +/** + * Machine-readable relationship semantics added per `.planning/predicate-spec-decisions.md`. + * All optional and additive — a predicate omitting them behaves exactly as before. + */ + +/** What the object of an edge is, so a consumer can render/traverse it without per-predicate code. */ +export type PredicateObjectKind = 'entity' | 'claim' | 'literal'; +/** Refines `objectKind: 'literal'` into a concrete datatype (RDF/XSD-aligned). Only meaningful when objectKind === 'literal'. */ +export type PredicateLiteralType = 'url' | 'image' | 'date' | 'number' | 'text'; +/** Sign of the edge from subject toward object; signed aggregation accrues to the object. */ +export type PredicatePolarity = 'positive' | 'negative' | 'neutral'; +/** Freshness/lifecycle model — which edges silently decay, which are write-once. */ +export type PredicateTemporalNature = 'permanent' | 'state' | 'event'; +/** Fact vs. opinion, for market design and UI semantics. */ +export type PredicateClaimType = 'factual' | 'evaluative'; +/** + * A reference to another predicate by its `key`. Existence and cross-referential consistency + * (inverse mirroring, contradicts symmetry, specializes acyclicity) are validated over the full + * set in `validate.ts`; the raw type is `string` to avoid a circular dependency on the derived + * `PredicateKey` union (which is computed from the spec array). + */ +export type PredicateKeyRef = string; + export type PredicateCategory = | 'Identity/Classification' | 'Social/Reputation' @@ -31,6 +54,16 @@ export interface PredicateDefinition { isSymmetric: boolean; isHierarchical: boolean; inversePredicate?: string; + // machine-readable semantics (see PredicateSpec) + objectKind?: PredicateObjectKind; + literalType?: PredicateLiteralType; + polarity?: PredicatePolarity; + temporalNature?: PredicateTemporalNature; + claimType?: PredicateClaimType; + inverse?: PredicateKeyRef; + specializes?: readonly PredicateKeyRef[]; + contradicts?: readonly PredicateKeyRef[]; + supersededBy?: PredicateKeyRef; } export type PredicateStatus = 'enshrined' | 'proposed' | 'deprecated'; @@ -45,10 +78,32 @@ export interface PredicateSpec { category: PredicateCategory; status: PredicateStatus; examples?: readonly string[]; + + // algebraic (OWL-aligned, flat, is* convention) isTransitive?: boolean; isSymmetric?: boolean; + /** @deprecated Legacy display-name of the inverse. Prefer the typed-key `inverse`; kept for back-compat. */ isHierarchical?: boolean; + /** @deprecated Display-name of the inverse predicate. Prefer the typed-key `inverse`; kept for back-compat. */ inversePredicate?: string; + + // rendering contract (frontend reads these directly) + objectKind?: PredicateObjectKind; + /** Only meaningful when `objectKind === 'literal'` (validated). */ + literalType?: PredicateLiteralType; + polarity?: PredicatePolarity; + temporalNature?: PredicateTemporalNature; + claimType?: PredicateClaimType; + + // inter-predicate (typed key references — validated over the full set in validate.ts) + /** The reverse-direction predicate's `key` (different predicate). Omit for symmetric predicates (inverse = self). */ + inverse?: PredicateKeyRef; + /** Parent predicate `key`(s) this one is a sub-property of. A DAG: usually one, occasionally two. */ + specializes?: readonly PredicateKeyRef[]; + /** Predicate `key`(s) that are pair-level disjoint with this one (declared symmetrically). */ + contradicts?: readonly PredicateKeyRef[]; + /** Successor predicate `key`; only valid when `status === 'deprecated'`. */ + supersededBy?: PredicateKeyRef; } export type PredicateRecord = PredicateDefinition & { @@ -99,6 +154,15 @@ export interface PredicateIpfsOptions { isSymmetric?: boolean; isHierarchical?: boolean; inversePredicate?: string; + objectKind?: PredicateObjectKind; + literalType?: PredicateLiteralType; + polarity?: PredicatePolarity; + temporalNature?: PredicateTemporalNature; + claimType?: PredicateClaimType; + inverse?: PredicateKeyRef; + specializes?: readonly PredicateKeyRef[]; + contradicts?: readonly PredicateKeyRef[]; + supersededBy?: PredicateKeyRef; } export type PredicateIpfsPropertyName = @@ -108,7 +172,16 @@ export type PredicateIpfsPropertyName = | 'isTransitive' | 'isSymmetric' | 'isHierarchical' - | 'inversePredicate'; + | 'inversePredicate' + | 'objectKind' + | 'literalType' + | 'polarity' + | 'temporalNature' + | 'claimType' + | 'inverse' + | 'specializes' + | 'contradicts' + | 'supersededBy'; export interface PredicateIpfsDocument extends PredicateAtomDocument { inDefinedTermSet: string; diff --git a/packages/predicates/src/validate.test.ts b/packages/predicates/src/validate.test.ts new file mode 100644 index 0000000..ded31eb --- /dev/null +++ b/packages/predicates/src/validate.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { PREDICATE_SPECS } from './generated/index'; +import { validatePredicateSet } from './validate'; + +describe('predicate set consistency gate', () => { + const result = validatePredicateSet(PREDICATE_SPECS); + + it('has zero hard errors across the full registry', () => { + expect(result.errors).toEqual([]); + }); + + it('declares inverse pairs symmetrically with mirrored transitivity', () => { + const byKey = new Map(PREDICATE_SPECS.map((s) => [s.key, s])); + for (const spec of PREDICATE_SPECS) { + if (!spec.inverse) continue; + const other = byKey.get(spec.inverse); + expect(other, `${spec.key}.inverse -> ${spec.inverse}`).toBeDefined(); + expect(other?.inverse).toBe(spec.key); + expect(Boolean(other?.isTransitive)).toBe(Boolean(spec.isTransitive)); + } + }); + + it('declares contradicts symmetrically and never on itself', () => { + const byKey = new Map(PREDICATE_SPECS.map((s) => [s.key, s])); + for (const spec of PREDICATE_SPECS) { + for (const ref of spec.contradicts ?? []) { + expect(ref).not.toBe(spec.key); + expect(byKey.get(ref)?.contradicts ?? []).toContain(spec.key); + } + } + }); + + it('never marks a polarity-bearing predicate transitive (Guha 2004)', () => { + for (const spec of PREDICATE_SPECS) { + if (spec.polarity !== undefined) expect(spec.isTransitive ?? false).toBe(false); + } + }); + + it('only attaches literalType to literal objects', () => { + for (const spec of PREDICATE_SPECS) { + if (spec.literalType !== undefined) expect(spec.objectKind).toBe('literal'); + } + }); + + it('closes contradictions down the specializes hierarchy (rule 7)', () => { + // endorse ⊑ support, support ⊥ oppose ⟹ endorse ⊥ oppose (derived, not authored). + expect(result.contradictionClosure.endorse).toContain('oppose'); + expect(result.contradictionClosure.vouchFor).toContain('oppose'); + }); +}); diff --git a/packages/predicates/src/validate.ts b/packages/predicates/src/validate.ts new file mode 100644 index 0000000..4836a6a --- /dev/null +++ b/packages/predicates/src/validate.ts @@ -0,0 +1,170 @@ +import type { PredicateSpec } from './types.js'; + +/** + * Cross-referential consistency gate for the predicate set (see + * `.planning/predicate-spec-decisions.md` §5.1). These rules need the whole set — key existence, + * inverse mirroring, contradicts symmetry, specializes acyclicity, contradiction-vs-hierarchy + * coherence. Per-spec rules (symmetry/inverse conflict, polarity∧transitive, literalType, + * supersededBy status) are enforced in `definePredicateRecord`. + * + * `errors` are hard violations (should fail the build/tests). `warnings` are the lint-level rules + * (near-universal but with conceivable exceptions) that are surfaced, not enforced. + */ +export interface PredicateSetValidation { + errors: string[]; + warnings: string[]; + /** Contradiction set closed down the specializes hierarchy (rule 7): key → disjoint keys. */ + contradictionClosure: Record; +} + +export function validatePredicateSet(specs: readonly PredicateSpec[]): PredicateSetValidation { + const errors: string[] = []; + const warnings: string[] = []; + const byKey = new Map(); + + for (const spec of specs) { + if (byKey.has(spec.key)) { + errors.push(`duplicate predicate key "${spec.key}".`); + } + byKey.set(spec.key, spec); + } + + const known = (key: string) => byKey.has(key); + const at = (spec: PredicateSpec) => `predicate "${spec.key}"`; + + for (const spec of specs) { + // Referenced keys must exist. + for (const ref of spec.specializes ?? []) { + if (!known(ref)) errors.push(`${at(spec)}: specializes unknown predicate "${ref}".`); + } + for (const ref of spec.contradicts ?? []) { + if (!known(ref)) errors.push(`${at(spec)}: contradicts unknown predicate "${ref}".`); + } + if (spec.inverse && !known(spec.inverse)) { + errors.push(`${at(spec)}: inverse references unknown predicate "${spec.inverse}".`); + } + if (spec.supersededBy) { + if (!known(spec.supersededBy)) { + errors.push( + `${at(spec)}: supersededBy references unknown predicate "${spec.supersededBy}".` + ); + } else if (byKey.get(spec.supersededBy)?.status === 'deprecated') { + errors.push(`${at(spec)}: supersededBy must point at a non-deprecated predicate.`); + } + } + + // Rule 2 — inverse mirroring + transitivity mirroring (rule 8, algebraic part = error). + if (spec.inverse && known(spec.inverse)) { + const other = byKey.get(spec.inverse) as PredicateSpec; + if (other.inverse !== spec.key) { + errors.push( + `${at(spec)}: inverse "${spec.inverse}" must declare inverse "${spec.key}" back (got "${ + other.inverse ?? 'undefined' + }").` + ); + } + if (Boolean(spec.isTransitive) !== Boolean(other.isTransitive)) { + errors.push( + `${at(spec)}: inverse pair must agree on isTransitive (theorem) — "${spec.key}"=${Boolean( + spec.isTransitive + )}, "${spec.inverse}"=${Boolean(other.isTransitive)}.` + ); + } + // Rule 8 (editorial part) — lint, not error. + if ((spec.temporalNature ?? null) !== (other.temporalNature ?? null)) { + warnings.push( + `${at(spec)}: inverse pair temporalNature differs from "${spec.inverse}" (lint).` + ); + } + if ((spec.claimType ?? null) !== (other.claimType ?? null)) { + warnings.push(`${at(spec)}: inverse pair claimType differs from "${spec.inverse}" (lint).`); + } + // Rule 13 — objectKind 'claim' with an inverse is a modeling smell (lint). + if (spec.objectKind === 'claim') { + warnings.push( + `${at(spec)}: objectKind 'claim' with an inverse ("${spec.inverse}") — the inverse's subject would be a claim (lint).` + ); + } + } + + // Rule 3 — contradicts symmetry. + for (const ref of spec.contradicts ?? []) { + if (known(ref) && !(byKey.get(ref)?.contradicts ?? []).includes(spec.key)) { + errors.push( + `${at(spec)}: contradicts "${ref}" is not declared symmetrically ("${ref}" must contradict "${spec.key}").` + ); + } + } + + // Rule 9 — symmetric + transitive = equivalence relation; allowed only for the identity allow-list. + const EQUIVALENCE_ALLOW_LIST = new Set(['sameAs']); + if (spec.isSymmetric && spec.isTransitive && !EQUIVALENCE_ALLOW_LIST.has(spec.key)) { + warnings.push( + `${at(spec)}: isSymmetric+isTransitive behaves as an equivalence relation; identity behavior is opt-in by key (allow-list: sameAs) (lint).` + ); + } + } + + // Rule 5 — specializes acyclic. DFS over the specializes DAG. + const visitState = new Map(); + const stack: string[] = []; + const detectCycle = (key: string): void => { + visitState.set(key, 1); + stack.push(key); + for (const parent of byKey.get(key)?.specializes ?? []) { + if (!known(parent)) continue; + const state = visitState.get(parent) ?? 0; + if (state === 1) { + errors.push(`specializes cycle: ${[...stack, parent].join(' ⊑ ')}.`); + } else if (state === 0) { + detectCycle(parent); + } + } + stack.pop(); + visitState.set(key, 2); + }; + for (const spec of specs) { + if ((visitState.get(spec.key) ?? 0) === 0) detectCycle(spec.key); + } + + // Ancestors under specializes (transitive closure of the parent relation), for rules 6 & 7. + const ancestorsOf = (key: string): Set => { + const out = new Set(); + const walk = (k: string) => { + for (const parent of byKey.get(k)?.specializes ?? []) { + if (known(parent) && !out.has(parent)) { + out.add(parent); + walk(parent); + } + } + }; + walk(key); + return out; + }; + + // Rule 6 — a predicate may not contradict its own specializes ancestor/descendant. + for (const spec of specs) { + const ancestors = ancestorsOf(spec.key); + for (const ref of spec.contradicts ?? []) { + if (ancestors.has(ref)) { + errors.push( + `${at(spec)}: contradicts "${ref}" which is a specializes-ancestor — asserting "${spec.key}" would entail and contradict it.` + ); + } + } + } + + // Rule 7 — derive the contradiction closure down the hierarchy: P ⊑ Q ∧ Q ⊥ R ⟹ P ⊥ R. + const contradictionClosure: Record = {}; + for (const spec of specs) { + const disjoint = new Set(spec.contradicts ?? []); + for (const ancestor of ancestorsOf(spec.key)) { + for (const ref of byKey.get(ancestor)?.contradicts ?? []) { + if (ref !== spec.key) disjoint.add(ref); + } + } + if (disjoint.size > 0) contradictionClosure[spec.key] = [...disjoint].sort(); + } + + return { errors, warnings, contradictionClosure }; +} From 3cdb3a744c935d47eff7aba7a1f0c5cb0231299f Mon Sep 17 00:00:00 2001 From: Kames Date: Wed, 5 Aug 2026 13:11:59 -0700 Subject: [PATCH 6/7] checkpoint: classifications update --- .planning/forum-post-intro.md | 18 +- .../src/generated/specs/aggregate-rating.ts | 3 + .../src/generated/specs/article.ts | 8 + .../src/generated/specs/book.ts | 11 ++ .../src/generated/specs/brand.ts | 7 + .../src/generated/specs/comment.ts | 4 + .../src/generated/specs/company.ts | 8 + .../src/generated/specs/dataset.ts | 8 + .../src/generated/specs/defined-term.ts | 25 +++ .../src/generated/specs/ethereum-account.ts | 6 + .../src/generated/specs/ethereum-erc20.ts | 6 + .../specs/ethereum-smart-contract.ts | 6 + .../src/generated/specs/event.ts | 16 ++ .../src/generated/specs/image.ts | 16 ++ .../src/generated/specs/job-posting.ts | 16 ++ .../src/generated/specs/local-business.ts | 33 ++++ .../src/generated/specs/location.ts | 26 +++ .../src/generated/specs/mobile-application.ts | 23 +++ .../src/generated/specs/movie.ts | 18 ++ .../src/generated/specs/music-album.ts | 9 + .../src/generated/specs/music-group.ts | 9 + .../src/generated/specs/music-recording.ts | 18 ++ .../src/generated/specs/news-article.ts | 16 ++ .../src/generated/specs/person.ts | 17 ++ .../src/generated/specs/podcast-episode.ts | 34 ++++ .../src/generated/specs/podcast-series.ts | 27 +++ .../src/generated/specs/product.ts | 8 + .../src/generated/specs/review.ts | 7 + .../src/generated/specs/service.ts | 8 + .../generated/specs/social-media-account.ts | 16 ++ .../generated/specs/social-media-posting.ts | 7 + .../generated/specs/software-application.ts | 7 + .../src/generated/specs/software.ts | 17 ++ .../src/generated/specs/thing.ts | 8 + .../src/generated/specs/tv-series.ts | 18 ++ .../src/generated/specs/video-object.ts | 17 ++ .../src/generated/specs/web-page.ts | 4 + .../src/generated/specs/web-site.ts | 4 + packages/classifications/src/identity.test.ts | 164 ++++++++++++++++++ packages/classifications/src/identity.ts | 104 +++++++++++ packages/classifications/src/index.ts | 13 ++ packages/classifications/src/types.ts | 104 +++++++++++ 42 files changed, 889 insertions(+), 5 deletions(-) create mode 100644 packages/classifications/src/identity.test.ts create mode 100644 packages/classifications/src/identity.ts diff --git a/.planning/forum-post-intro.md b/.planning/forum-post-intro.md index fc4a770..5ea61de 100644 --- a/.planning/forum-post-intro.md +++ b/.planning/forum-post-intro.md @@ -14,6 +14,11 @@ This post is a proper introduction — what the packages are, the concepts behin them — plus a peek at where we're taking them next. It's long-ish because there's a real mental model here, but each piece is simple on its own. Grab a coffee. +One thing up front so nobody's surprised: what's out today is the *packages* — the client-side TypeScript +layer you build against. The backend that indexes and serves the graph isn't open source yet. It's coming, +and soon, but I didn't want to bury it: everything I describe below runs without it, which is kind of the +whole point. + ## The shift: off-chain first If you've built on-chain before, you know the tax: **every piece of data costs a transaction.** Put a fact @@ -43,9 +48,9 @@ Intuition's knowledge graph is built from a tiny, composable vocabulary: Two more ideas that matter a lot: - **Deterministic IDs.** Every atom, predicate, and triple has a **content-addressable ID** you can compute - *before* anything touches a chain. Same content → same ID, everywhere, forever. This is the quiet - superpower: your off-chain graph and the eventual on-chain graph speak the same identifiers, so there's no - "import/sync" cliff — you're always building the real thing. + *before* anything touches a chain. Same content → same ID, everywhere, forever. This is the part that + quietly makes everything else work: your off-chain graph and the eventual on-chain graph speak the same + identifiers, so there's no "import/sync" cliff later. You're always building the real thing. - **`sameAs` identity.** Different atoms can point at the same real-world thing (the same song on Spotify and Apple Music, the same person across two profiles). `sameAs` links them so the graph can treat them as one identity without forcing everyone to agree on a single canonical atom up front. @@ -88,8 +93,11 @@ converging on shared vocabulary and IDs. Here's the map and how to think about e - **`@0xintuition/curves`** — bonding-curve math for the market/staking side. - **`@0xintuition/react`** — React hooks for the core protocol interactions. -The beauty of the layering: you can live entirely in the top three layers while prototyping, and only pull -in the protocol layer when your app owns wallet + submission flow. +The nice thing about splitting it this way: you can live entirely in the top three layers while prototyping, +and only reach for the protocol layer once your app actually owns a wallet and a submission flow. (Reading +the *global* graph back — everyone's atoms and triples, not just the ones you built locally — is the job of +the indexing backend, which, as mentioned, is still on its way. For now you're modeling and settling; the +querying side lands when that opens up.) ## How they work together (a tiny walkthrough) diff --git a/packages/classifications/src/generated/specs/aggregate-rating.ts b/packages/classifications/src/generated/specs/aggregate-rating.ts index f92c324..baa561f 100644 --- a/packages/classifications/src/generated/specs/aggregate-rating.ts +++ b/packages/classifications/src/generated/specs/aggregate-rating.ts @@ -47,4 +47,7 @@ export const aggregateRating: ClassificationSpec = { }, ], defaults: { pluginId: 'aggregate-rating', provider: 'opengraph' }, + // D23: withdrawn from IID scope — remodeled as a triple + // (subject —hasAggregateRating→ provider atom). + identity: null, }; diff --git a/packages/classifications/src/generated/specs/article.ts b/packages/classifications/src/generated/specs/article.ts index 6ce757e..489b9a4 100644 --- a/packages/classifications/src/generated/specs/article.ts +++ b/packages/classifications/src/generated/specs/article.ts @@ -56,4 +56,12 @@ export const article: ClassificationSpec = { }, ], defaults: { pluginId: 'article', provider: 'dictionary' }, + identity: { + identifies: 'an article', + ladder: [ + { kind: 'scheme', scheme: 'doi', source: { kind: 'field', key: 'url' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'headline', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/book.ts b/packages/classifications/src/generated/specs/book.ts index cc8d8f2..44a1513 100644 --- a/packages/classifications/src/generated/specs/book.ts +++ b/packages/classifications/src/generated/specs/book.ts @@ -54,4 +54,15 @@ export const book: ClassificationSpec = { }, ], defaults: { pluginId: 'book' }, + identity: { + identifies: 'the work; isbn identifies editions, bridged via equivalence', + ladder: [ + { kind: 'scheme', scheme: 'isbn', source: { kind: 'field', key: 'isbn' } }, + // D27: T2 community open data + { kind: 'scheme', scheme: 'olid', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + // D21: author omitted — authorship is an authoredBy triple + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/brand.ts b/packages/classifications/src/generated/specs/brand.ts index 847e57d..92dc69b 100644 --- a/packages/classifications/src/generated/specs/brand.ts +++ b/packages/classifications/src/generated/specs/brand.ts @@ -38,4 +38,11 @@ export const brand: ClassificationSpec = { }, ], defaults: { pluginId: 'brand', provider: 'company-profile' }, + identity: { + identifies: 'a brand/concept (instance businesses are local-business atoms)', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'gen1', tag: 2, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/comment.ts b/packages/classifications/src/generated/specs/comment.ts index 2f22bf0..ddd687f 100644 --- a/packages/classifications/src/generated/specs/comment.ts +++ b/packages/classifications/src/generated/specs/comment.ts @@ -47,4 +47,8 @@ export const comment: ClassificationSpec = { }, ], defaults: { pluginId: 'comment', provider: 'social-media-posting' }, + identity: { + identifies: 'one comment on one subject (relational entity)', + ladder: [{ kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }], + }, }; diff --git a/packages/classifications/src/generated/specs/company.ts b/packages/classifications/src/generated/specs/company.ts index 4481397..000232a 100644 --- a/packages/classifications/src/generated/specs/company.ts +++ b/packages/classifications/src/generated/specs/company.ts @@ -49,4 +49,12 @@ export const company: ClassificationSpec = { }, ], defaults: { pluginId: 'company' }, + identity: { + identifies: 'a legal entity', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/dataset.ts b/packages/classifications/src/generated/specs/dataset.ts index a78f3da..6d2d5b9 100644 --- a/packages/classifications/src/generated/specs/dataset.ts +++ b/packages/classifications/src/generated/specs/dataset.ts @@ -38,4 +38,12 @@ export const dataset: ClassificationSpec = { }, ], defaults: { pluginId: 'dataset', provider: 'opengraph' }, + identity: { + identifies: 'a dataset (version lineage out of ID scope)', + ladder: [ + { kind: 'scheme', scheme: 'doi', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/defined-term.ts b/packages/classifications/src/generated/specs/defined-term.ts index 5e5e4e0..eb3b238 100644 --- a/packages/classifications/src/generated/specs/defined-term.ts +++ b/packages/classifications/src/generated/specs/defined-term.ts @@ -27,6 +27,24 @@ export const definedTerm: ClassificationSpec = { required: false, placeholder: 'Structured, semantic network that organizes data.', }, + { + key: 'inDefinedTermSet', + schemaProperty: 'inDefinedTermSet', + label: 'Term Set', + description: 'The glossary or vocabulary the term belongs to.', + fieldType: 'string', + required: false, + placeholder: 'https://schema.org', + }, + { + key: 'termCode', + schemaProperty: 'termCode', + label: 'Term Code', + description: 'The code identifying the term within its set.', + fieldType: 'string', + required: false, + placeholder: 'merkle-tree', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -38,4 +56,11 @@ export const definedTerm: ClassificationSpec = { }, ], defaults: { pluginId: 'defined-term', provider: 'dictionary' }, + identity: { + identifies: 'a term within a term set', + ladder: [ + { kind: 'scheme', scheme: 'termset', source: { kind: 'derivation', name: 'termset-term' } }, + { kind: 'gen1', tag: 2, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/ethereum-account.ts b/packages/classifications/src/generated/specs/ethereum-account.ts index 96fa4b0..5c95831 100644 --- a/packages/classifications/src/generated/specs/ethereum-account.ts +++ b/packages/classifications/src/generated/specs/ethereum-account.ts @@ -22,4 +22,10 @@ export const ethereumAccount: ClassificationSpec = { }, ], defaults: { pluginId: 'ethereum-account', provider: 'etherscan' }, + identity: { + identifies: 'an EOA/keypair (D24: fixed eip155:1)', + ladder: [ + { kind: 'scheme', scheme: 'caip10', source: { kind: 'derivation', name: 'caip10-eoa' } }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/ethereum-erc20.ts b/packages/classifications/src/generated/specs/ethereum-erc20.ts index eed5045..d76b8ad 100644 --- a/packages/classifications/src/generated/specs/ethereum-erc20.ts +++ b/packages/classifications/src/generated/specs/ethereum-erc20.ts @@ -54,4 +54,10 @@ export const ethereumErc20: ClassificationSpec = { }, ], defaults: { pluginId: 'ethereum-smart-contract', provider: 'etherscan' }, + identity: { + identifies: 'a token asset on a chain', + ladder: [ + { kind: 'scheme', scheme: 'caip19', source: { kind: 'derivation', name: 'caip19-erc20' } }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/ethereum-smart-contract.ts b/packages/classifications/src/generated/specs/ethereum-smart-contract.ts index d869c0e..72618a3 100644 --- a/packages/classifications/src/generated/specs/ethereum-smart-contract.ts +++ b/packages/classifications/src/generated/specs/ethereum-smart-contract.ts @@ -30,4 +30,10 @@ export const ethereumSmartContract: ClassificationSpec = { }, ], defaults: { pluginId: 'ethereum-smart-contract', provider: 'etherscan' }, + identity: { + identifies: 'a deployed contract', + ladder: [ + { kind: 'scheme', scheme: 'caip10', source: { kind: 'derivation', name: 'caip10-contract' } }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/event.ts b/packages/classifications/src/generated/specs/event.ts index 0977b4f..328788f 100644 --- a/packages/classifications/src/generated/specs/event.ts +++ b/packages/classifications/src/generated/specs/event.ts @@ -55,4 +55,20 @@ export const event: ClassificationSpec = { }, ], defaults: { pluginId: 'event' }, + identity: { + identifies: 'an event occurrence', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 3, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'startDate', from: 'field' }, + ], + }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/image.ts b/packages/classifications/src/generated/specs/image.ts index 03afe41..c53dcd1 100644 --- a/packages/classifications/src/generated/specs/image.ts +++ b/packages/classifications/src/generated/specs/image.ts @@ -53,6 +53,14 @@ export const image: ClassificationSpec = { required: false, placeholder: 'knowledge-graph', }, + { + key: 'contentHash', + label: 'Content Hash', + description: 'Hash of the image bytes as alg:hex, e.g. sha256:.', + fieldType: 'string', + required: false, + placeholder: 'sha256:9f86d081884c7d65…', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -64,4 +72,12 @@ export const image: ClassificationSpec = { }, ], defaults: { pluginId: 'image', provider: 'github' }, + identity: { + identifies: 'an image — byte-identity first; url is a mutable manifestation fallback (F4)', + ladder: [ + { kind: 'scheme', scheme: 'hash', source: { kind: 'field', key: 'contentHash' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/job-posting.ts b/packages/classifications/src/generated/specs/job-posting.ts index fc345f9..ffb1720 100644 --- a/packages/classifications/src/generated/specs/job-posting.ts +++ b/packages/classifications/src/generated/specs/job-posting.ts @@ -65,4 +65,20 @@ export const jobPosting: ClassificationSpec = { }, ], defaults: { pluginId: 'job-posting', provider: 'opengraph' }, + identity: { + identifies: 'one posting', + ladder: [ + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + // D21: hiringOrganization omitted — employer linkage is a triple + { + kind: 'gen1', + tag: 2, + recipe: [ + { key: 'title', from: 'field' }, + { key: 'datePosted', from: 'field' }, + ], + }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'title', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/local-business.ts b/packages/classifications/src/generated/specs/local-business.ts index 7db0328..b264574 100644 --- a/packages/classifications/src/generated/specs/local-business.ts +++ b/packages/classifications/src/generated/specs/local-business.ts @@ -52,6 +52,24 @@ export const localBusiness: ClassificationSpec = { required: false, placeholder: 'https://bluebottlecoffee.com', }, + { + key: 'latitude', + schemaProperty: 'latitude', + label: 'Latitude', + description: 'The latitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '37.7823', + }, + { + key: 'longitude', + schemaProperty: 'longitude', + label: 'Longitude', + description: 'The longitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '-122.4076', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -63,4 +81,19 @@ export const localBusiness: ClassificationSpec = { }, ], defaults: { pluginId: 'local-business', provider: 'places' }, + identity: { + identifies: 'one business at one place (instance level)', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 2, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'geo7', from: 'geohash', precision: 7 }, + ], + }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/location.ts b/packages/classifications/src/generated/specs/location.ts index caf71da..87222d8 100644 --- a/packages/classifications/src/generated/specs/location.ts +++ b/packages/classifications/src/generated/specs/location.ts @@ -34,6 +34,24 @@ export const location: ClassificationSpec = { required: false, placeholder: 'San Francisco, CA 94129', }, + { + key: 'latitude', + schemaProperty: 'latitude', + label: 'Latitude', + description: 'The latitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '48.8584', + }, + { + key: 'longitude', + schemaProperty: 'longitude', + label: 'Longitude', + description: 'The longitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '2.2945', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -45,4 +63,12 @@ export const location: ClassificationSpec = { }, ], defaults: { pluginId: 'location' }, + identity: { + identifies: 'a named place or coordinate point', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'geo', source: { kind: 'geohash', precision: 8 } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/mobile-application.ts b/packages/classifications/src/generated/specs/mobile-application.ts index 3f31c02..7c0a921 100644 --- a/packages/classifications/src/generated/specs/mobile-application.ts +++ b/packages/classifications/src/generated/specs/mobile-application.ts @@ -45,6 +45,14 @@ export const mobileApplication: ClassificationSpec = { required: false, placeholder: 'https://apps.apple.com/app/spotify/id324684580', }, + { + key: 'bundleId', + label: 'Bundle / Package ID', + description: 'The store bundle identifier or Android package name.', + fieldType: 'string', + required: false, + placeholder: 'com.spotify.music', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -56,4 +64,19 @@ export const mobileApplication: ClassificationSpec = { }, ], defaults: { pluginId: 'mobile-application', provider: 'opengraph' }, + identity: { + identifies: 'a mobile app per store ecosystem', + ladder: [ + { kind: 'scheme', scheme: 'appid', source: { kind: 'derivation', name: 'appid-bundle' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'downloadUrl' } }, + { + kind: 'gen1', + tag: 3, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'operatingSystem', from: 'field' }, + ], + }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/movie.ts b/packages/classifications/src/generated/specs/movie.ts index ef99413..96d99a1 100644 --- a/packages/classifications/src/generated/specs/movie.ts +++ b/packages/classifications/src/generated/specs/movie.ts @@ -46,4 +46,22 @@ export const movie: ClassificationSpec = { }, ], defaults: { pluginId: 'movie' }, + identity: { + identifies: 'a film (work level)', + ladder: [ + // D27: open registries first — wd (CC0), tmdb (daily ID exports), then imdb (closed) + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'tmdb', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'imdb', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 4, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'yearPublished', from: 'year', of: 'datePublished' }, + ], + }, + { kind: 'gen1', tag: 5, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/music-album.ts b/packages/classifications/src/generated/specs/music-album.ts index 4e836b7..6873ca1 100644 --- a/packages/classifications/src/generated/specs/music-album.ts +++ b/packages/classifications/src/generated/specs/music-album.ts @@ -38,4 +38,13 @@ export const musicAlbum: ClassificationSpec = { }, ], defaults: { pluginId: 'song', provider: 'musicbrainz' }, + identity: { + identifies: 'the release-group (the album as a work)', + ladder: [ + { kind: 'scheme', scheme: 'mbid', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + // D21: byArtist omitted — artist linkage is a triple + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/music-group.ts b/packages/classifications/src/generated/specs/music-group.ts index 36d188e..8151fbe 100644 --- a/packages/classifications/src/generated/specs/music-group.ts +++ b/packages/classifications/src/generated/specs/music-group.ts @@ -29,4 +29,13 @@ export const musicGroup: ClassificationSpec = { }, ], defaults: { pluginId: 'song', provider: 'musicbrainz' }, + identity: { + identifies: 'an artist/band public identity', + ladder: [ + { kind: 'scheme', scheme: 'isni', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'mbid', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/music-recording.ts b/packages/classifications/src/generated/specs/music-recording.ts index 0acafc8..dce7a29 100644 --- a/packages/classifications/src/generated/specs/music-recording.ts +++ b/packages/classifications/src/generated/specs/music-recording.ts @@ -36,6 +36,15 @@ export const musicRecording: ClassificationSpec = { required: false, placeholder: 'Discovery', }, + { + key: 'isrc', + schemaProperty: 'isrc', + label: 'ISRC', + description: 'The International Standard Recording Code when known.', + fieldType: 'string', + required: false, + placeholder: 'USSM10007459', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -47,4 +56,13 @@ export const musicRecording: ClassificationSpec = { }, ], defaults: { pluginId: 'song', provider: 'musicbrainz' }, + identity: { + identifies: 'the recording (isrc level; compositions are iswc territory)', + ladder: [ + { kind: 'scheme', scheme: 'isrc', source: { kind: 'field', key: 'isrc' } }, + { kind: 'scheme', scheme: 'mbid', source: { kind: 'same-as' } }, + // D21: byArtist omitted (feat.-credit formatting forks); inAlbum was never identity + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/news-article.ts b/packages/classifications/src/generated/specs/news-article.ts index 1adb4d2..a10079f 100644 --- a/packages/classifications/src/generated/specs/news-article.ts +++ b/packages/classifications/src/generated/specs/news-article.ts @@ -55,4 +55,20 @@ export const newsArticle: ClassificationSpec = { }, ], defaults: { pluginId: 'news-article', provider: 'opengraph' }, + identity: { + identifies: 'a news story as published (URL-anchored)', + ladder: [ + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 2, + recipe: [ + { key: 'headline', from: 'field' }, + { key: 'datePublished', from: 'field' }, + ], + }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'headline', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/person.ts b/packages/classifications/src/generated/specs/person.ts index bb81ab2..f3845a3 100644 --- a/packages/classifications/src/generated/specs/person.ts +++ b/packages/classifications/src/generated/specs/person.ts @@ -45,4 +45,21 @@ export const person: ClassificationSpec = { }, ], defaults: { pluginId: 'person', provider: 'wikidata' }, + identity: { + identifies: 'an individual human', + ladder: [ + { kind: 'scheme', scheme: 'isni', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 4, + recipe: [ + { key: 'givenName', from: 'field' }, + { key: 'familyName', from: 'field' }, + ], + }, + // D25: mononyms + { kind: 'gen1', tag: 5, recipe: [{ key: 'givenName', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/podcast-episode.ts b/packages/classifications/src/generated/specs/podcast-episode.ts index f1454a5..697aeec 100644 --- a/packages/classifications/src/generated/specs/podcast-episode.ts +++ b/packages/classifications/src/generated/specs/podcast-episode.ts @@ -45,6 +45,22 @@ export const podcastEpisode: ClassificationSpec = { required: false, placeholder: '2026-02-26', }, + { + key: 'feedGuid', + label: 'Feed GUID', + description: 'The Podcasting 2.0 GUID of the feed this item belongs to.', + fieldType: 'string', + required: false, + placeholder: '917393e3-1b1e-5cef-ace4-edaa54e1f810', + }, + { + key: 'itemGuid', + label: 'Item GUID', + description: 'The RSS value of this episode item within its feed.', + fieldType: 'string', + required: false, + placeholder: 'urn:example:ep42', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -56,4 +72,22 @@ export const podcastEpisode: ClassificationSpec = { }, ], defaults: { pluginId: 'podcast-episode', provider: 'opengraph' }, + identity: { + identifies: + 'one FEED ITEM (manifestation) — the episode-work is the equivalence cluster over feed items (D26)', + ladder: [ + { kind: 'scheme', scheme: 'rssitem', source: { kind: 'derivation', name: 'rss-item' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + // D21: partOfSeries omitted — series linkage is a triple + { + kind: 'gen1', + tag: 3, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'datePublished', from: 'field' }, + ], + }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/podcast-series.ts b/packages/classifications/src/generated/specs/podcast-series.ts index a4a115c..f7ddee0 100644 --- a/packages/classifications/src/generated/specs/podcast-series.ts +++ b/packages/classifications/src/generated/specs/podcast-series.ts @@ -27,6 +27,22 @@ export const podcastSeries: ClassificationSpec = { required: false, placeholder: 'https://www.bankless.com/podcast', }, + { + key: 'feedUrl', + label: 'RSS Feed URL', + description: 'The RSS feed URL; used to derive the Podcasting 2.0 GUID.', + fieldType: 'url', + required: false, + placeholder: 'https://feeds.example.com/show.xml', + }, + { + key: 'podcastGuid', + label: 'Podcast GUID', + description: 'The declared Podcasting 2.0 value; survives feed migrations.', + fieldType: 'string', + required: false, + placeholder: '917393e3-1b1e-5cef-ace4-edaa54e1f810', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -38,4 +54,15 @@ export const podcastSeries: ClassificationSpec = { }, ], defaults: { pluginId: 'podcast-series', provider: 'opengraph' }, + identity: { + identifies: 'the show (feed level)', + ladder: [ + // D26: the declared survives feed migrations + { kind: 'scheme', scheme: 'podcastguid', source: { kind: 'field', key: 'podcastGuid' } }, + // derived from the feed URL — forks on migration, fallback only + { kind: 'scheme', scheme: 'podcastguid', source: { kind: 'podcast-guid', key: 'feedUrl' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/product.ts b/packages/classifications/src/generated/specs/product.ts index 9debeb8..5580ffa 100644 --- a/packages/classifications/src/generated/specs/product.ts +++ b/packages/classifications/src/generated/specs/product.ts @@ -56,4 +56,12 @@ export const product: ClassificationSpec = { }, ], defaults: { pluginId: 'product' }, + identity: { + identifies: 'the product model (not individual units)', + ladder: [ + { kind: 'scheme', scheme: 'gtin', source: { kind: 'field', key: 'gtin' } }, + // D21: brand and SKU omitted — brand linkage is a triple + { kind: 'gen1', tag: 2, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/review.ts b/packages/classifications/src/generated/specs/review.ts index 4bf6a3b..04f79c5 100644 --- a/packages/classifications/src/generated/specs/review.ts +++ b/packages/classifications/src/generated/specs/review.ts @@ -47,4 +47,11 @@ export const review: ClassificationSpec = { }, ], defaults: { pluginId: 'review', provider: 'opengraph' }, + identity: { + identifies: 'one review of one subject (relational entity)', + ladder: [ + // gen1 recipe needs the subject's IID; awaits IID-typed reference fields + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/service.ts b/packages/classifications/src/generated/specs/service.ts index a10a2bd..7b99183 100644 --- a/packages/classifications/src/generated/specs/service.ts +++ b/packages/classifications/src/generated/specs/service.ts @@ -47,4 +47,12 @@ export const service: ClassificationSpec = { }, ], defaults: { pluginId: 'service' }, + identity: { + identifies: 'a service offering', + ladder: [ + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + // D21: provider omitted — provider linkage is a triple + { kind: 'gen1', tag: 2, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/social-media-account.ts b/packages/classifications/src/generated/specs/social-media-account.ts index 485e619..8354f73 100644 --- a/packages/classifications/src/generated/specs/social-media-account.ts +++ b/packages/classifications/src/generated/specs/social-media-account.ts @@ -36,6 +36,22 @@ export const socialMediaAccount: ClassificationSpec = { required: false, placeholder: 'https://x.com/karpathy', }, + { + key: 'platformUserId', + label: 'Platform User ID', + description: 'The immutable platform-assigned user ID (survives handle renames).', + fieldType: 'string', + required: false, + placeholder: '295218901', + }, ], defaults: { pluginId: 'social-media-account' }, + identity: { + identifies: 'an account on a platform (the person behind it is a separate atom)', + ladder: [ + { kind: 'scheme', scheme: 'acct', source: { kind: 'derivation', name: 'acct-strong' } }, + // handles rename and get recycled; ranks below the immutable user id + { kind: 'scheme', scheme: 'acct', source: { kind: 'derivation', name: 'acct-weak' } }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/social-media-posting.ts b/packages/classifications/src/generated/specs/social-media-posting.ts index 550545d..40f502e 100644 --- a/packages/classifications/src/generated/specs/social-media-posting.ts +++ b/packages/classifications/src/generated/specs/social-media-posting.ts @@ -38,4 +38,11 @@ export const socialMediaPosting: ClassificationSpec = { }, ], defaults: { pluginId: 'social-media-posting' }, + identity: { + identifies: 'one post', + ladder: [ + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 2, recipe: [{ key: 'textHash', from: 'text-hash', of: 'text' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/software-application.ts b/packages/classifications/src/generated/specs/software-application.ts index e19d4a9..3920b33 100644 --- a/packages/classifications/src/generated/specs/software-application.ts +++ b/packages/classifications/src/generated/specs/software-application.ts @@ -63,4 +63,11 @@ export const softwareApplication: ClassificationSpec = { }, ], defaults: { pluginId: 'software-application', provider: 'opengraph' }, + identity: { + identifies: 'an end-user application', + ladder: [ + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }, + { kind: 'gen1', tag: 4, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/software.ts b/packages/classifications/src/generated/specs/software.ts index 1e7cb79..c32e9e0 100644 --- a/packages/classifications/src/generated/specs/software.ts +++ b/packages/classifications/src/generated/specs/software.ts @@ -27,6 +27,14 @@ export const software: ClassificationSpec = { required: true, placeholder: 'https://github.com/0xintuition/intuition-data-structure', }, + { + key: 'packageUrl', + label: 'Package URL', + description: 'The purl package coordinate, version-free.', + fieldType: 'string', + required: false, + placeholder: 'pkg:npm/react', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -38,4 +46,13 @@ export const software: ClassificationSpec = { }, ], defaults: { pluginId: 'software', provider: 'github' }, + identity: { + identifies: + 'the package/project, version-free — purl (open spec) first; repo URLs migrate (F5)', + ladder: [ + { kind: 'scheme', scheme: 'purl', source: { kind: 'field', key: 'packageUrl' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'codeRepository' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/thing.ts b/packages/classifications/src/generated/specs/thing.ts index d257523..42d7bf2 100644 --- a/packages/classifications/src/generated/specs/thing.ts +++ b/packages/classifications/src/generated/specs/thing.ts @@ -38,4 +38,12 @@ export const thing: ClassificationSpec = { }, ], defaults: { pluginId: 'thing', provider: 'wikidata' }, + identity: { + identifies: 'anything not covered by a specific classification (weakest IDs by design)', + ladder: [ + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/tv-series.ts b/packages/classifications/src/generated/specs/tv-series.ts index afa6bf6..045d4d9 100644 --- a/packages/classifications/src/generated/specs/tv-series.ts +++ b/packages/classifications/src/generated/specs/tv-series.ts @@ -47,4 +47,22 @@ export const tvSeries: ClassificationSpec = { }, ], defaults: { pluginId: 'tv-series' }, + identity: { + identifies: 'the series (seasons/episodes are unmodeled levels)', + ladder: [ + // D27: open registries first + { kind: 'scheme', scheme: 'wd', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'tmdb', source: { kind: 'same-as' } }, + { kind: 'scheme', scheme: 'imdb', source: { kind: 'same-as' } }, + { + kind: 'gen1', + tag: 4, + recipe: [ + { key: 'name', from: 'field' }, + { key: 'startYear', from: 'year', of: 'startDate' }, + ], + }, + { kind: 'gen1', tag: 5, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/video-object.ts b/packages/classifications/src/generated/specs/video-object.ts index 2a30d90..14fa61b 100644 --- a/packages/classifications/src/generated/specs/video-object.ts +++ b/packages/classifications/src/generated/specs/video-object.ts @@ -45,6 +45,14 @@ export const videoObject: ClassificationSpec = { required: false, placeholder: 'https://example.com/videos/how-intuition-works', }, + { + key: 'contentHash', + label: 'Content Hash', + description: 'Hash of the video bytes as alg:hex, e.g. sha256:.', + fieldType: 'string', + required: false, + placeholder: 'sha256:9f86d081884c7d65…', + }, { key: 'sameAs', schemaProperty: 'sameAs', @@ -56,4 +64,13 @@ export const videoObject: ClassificationSpec = { }, ], defaults: { pluginId: 'video-object' }, + identity: { + identifies: 'a video — byte-identity first; platform URLs are manifestation fallbacks (F4)', + ladder: [ + { kind: 'scheme', scheme: 'hash', source: { kind: 'field', key: 'contentHash' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'contentUrl' } }, + { kind: 'scheme', scheme: 'url', source: { kind: 'same-as' } }, + { kind: 'gen1', tag: 3, recipe: [{ key: 'name', from: 'field' }] }, + ], + }, }; diff --git a/packages/classifications/src/generated/specs/web-page.ts b/packages/classifications/src/generated/specs/web-page.ts index ce56053..ac0b758 100644 --- a/packages/classifications/src/generated/specs/web-page.ts +++ b/packages/classifications/src/generated/specs/web-page.ts @@ -47,4 +47,8 @@ export const webPage: ClassificationSpec = { }, ], defaults: { pluginId: 'web-page', provider: 'opengraph' }, + identity: { + identifies: 'a page at a canonical URL', + ladder: [{ kind: 'scheme', scheme: 'url', source: { kind: 'field', key: 'url' } }], + }, }; diff --git a/packages/classifications/src/generated/specs/web-site.ts b/packages/classifications/src/generated/specs/web-site.ts index bba3b3c..b6eafb7 100644 --- a/packages/classifications/src/generated/specs/web-site.ts +++ b/packages/classifications/src/generated/specs/web-site.ts @@ -38,4 +38,8 @@ export const webSite: ClassificationSpec = { }, ], defaults: { pluginId: 'web-site', provider: 'opengraph' }, + identity: { + identifies: 'a site (origin level)', + ladder: [{ kind: 'scheme', scheme: 'url', source: { kind: 'url-origin', key: 'url' } }], + }, }; diff --git a/packages/classifications/src/identity.test.ts b/packages/classifications/src/identity.test.ts new file mode 100644 index 0000000..6a77529 --- /dev/null +++ b/packages/classifications/src/identity.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, it } from 'vitest'; +import { CLASSIFICATION_SPECS } from './classifications.js'; +import { + IDENTITY_SCHEME_CLASS, + IDENTITY_SCHEME_TYPING, + rungClass, + topRungAnchorEligible, +} from './identity.js'; +import type { ClassificationSpec, IdentityRung, IdentityValueSource } from './types.js'; + +const CLASS_ORDER = { A: 0, B: 1, C: 2 } as const; + +function specFieldKeys(spec: ClassificationSpec): Set { + return new Set(spec.fields.map((field) => field.key)); +} + +/** Every spec field a declarative source reads, or null when engine-owned. */ +function sourceFieldKeys(source: IdentityValueSource): readonly string[] | null { + switch (source.kind) { + case 'field': + case 'url-origin': + case 'podcast-guid': + return [source.key]; + case 'same-as': + return ['sameAs']; + case 'geohash': + return ['latitude', 'longitude']; + case 'derivation': + // Derivation input contracts are engine-owned; covered below. + return DERIVATION_INPUTS[source.name] ?? null; + } +} + +/** The spec fields each named derivation reads (scheme-registry.md). */ +const DERIVATION_INPUTS: Record = { + 'acct-strong': ['platform', 'platformUserId'], + 'acct-weak': ['platform', 'username'], + 'appid-bundle': ['operatingSystem', 'bundleId'], + 'rss-item': ['feedGuid', 'itemGuid'], + 'termset-term': ['inDefinedTermSet', 'termCode'], + 'caip10-eoa': ['address'], + 'caip10-contract': ['chainId', 'address'], + 'caip19-erc20': ['chainId', 'address'], +}; + +describe('identity ladders', () => { + it('every classification declares identity (or explicit null)', () => { + for (const spec of CLASSIFICATION_SPECS) { + expect(spec.identity !== undefined, `${spec.slug} must declare identity`).toBe(true); + } + }); + + it('only aggregate-rating is outside IID scope (D23)', () => { + const outOfScope = CLASSIFICATION_SPECS.filter((spec) => spec.identity === null).map( + (spec) => spec.slug + ); + expect(outOfScope).toEqual(['aggregate-rating']); + }); + + it('ladders are non-empty and ordered A ≥ B ≥ C', () => { + for (const spec of CLASSIFICATION_SPECS) { + if (!spec.identity) continue; + expect(spec.identity.ladder.length, `${spec.slug} ladder is empty`).toBeGreaterThan(0); + const order = spec.identity.ladder.map((rung) => CLASS_ORDER[rungClass(rung)]); + for (let index = 1; index < order.length; index++) { + expect( + order[index] >= order[index - 1], + `${spec.slug}: rung ${index} (${order[index]}) outranks rung ${index - 1}` + ).toBe(true); + } + } + }); + + it('every source reads only fields the classification declares', () => { + for (const spec of CLASSIFICATION_SPECS) { + if (!spec.identity) continue; + const declared = specFieldKeys(spec); + for (const rung of spec.identity.ladder) { + if (rung.kind !== 'scheme') continue; + const keys = sourceFieldKeys(rung.source); + expect( + keys, + `${spec.slug}: unknown derivation in ${JSON.stringify(rung.source)}` + ).not.toBeNull(); + for (const key of keys ?? []) { + expect(declared.has(key), `${spec.slug}: source reads undeclared field "${key}"`).toBe( + true + ); + } + } + } + }); + + it('gen1 recipes reference declared fields only (D21 surface)', () => { + for (const spec of CLASSIFICATION_SPECS) { + if (!spec.identity) continue; + const declared = specFieldKeys(spec); + for (const rung of spec.identity.ladder) { + if (rung.kind !== 'gen1') continue; + expect(rung.recipe.length, `${spec.slug} gen1 r${rung.tag}: empty recipe`).toBeGreaterThan( + 0 + ); + for (const recipeField of rung.recipe) { + const reads = + recipeField.from === 'field' + ? recipeField.key + : recipeField.from === 'geohash' + ? null // reads latitude/longitude, checked via declared coords below + : recipeField.of; + if (reads !== null) { + expect( + declared.has(reads), + `${spec.slug} gen1 r${rung.tag}: recipe reads undeclared field "${reads}"` + ).toBe(true); + } else { + expect( + declared.has('latitude') && declared.has('longitude'), + `${spec.slug}: geohash recipe needs coords` + ).toBe(true); + } + } + } + } + }); + + it('gen1 rung tags are unique per ladder (D22)', () => { + for (const spec of CLASSIFICATION_SPECS) { + if (!spec.identity) continue; + const tags = spec.identity.ladder + .filter((rung): rung is Extract => rung.kind === 'gen1') + .map((rung) => rung.tag); + expect(new Set(tags).size, `${spec.slug}: duplicate gen1 tags`).toBe(tags.length); + } + }); + + it('scheme registry maps cover every scheme exactly once', () => { + expect(Object.keys(IDENTITY_SCHEME_CLASS).sort()).toEqual( + Object.keys(IDENTITY_SCHEME_TYPING).sort() + ); + for (const spec of CLASSIFICATION_SPECS) { + if (!spec.identity) continue; + for (const rung of spec.identity.ladder) { + if (rung.kind === 'scheme') { + expect( + IDENTITY_SCHEME_CLASS[rung.scheme], + `${spec.slug}: unregistered scheme` + ).toBeDefined(); + } + } + } + }); + + it('anchor eligibility matches D30 expectations for the flagship lanes', () => { + const bySlug = new Map(CLASSIFICATION_SPECS.map((spec) => [spec.slug, spec])); + // Unambiguous Class A top rungs → P0 anchors. + for (const slug of ['music-recording', 'book', 'product', 'ethereum-erc20']) { + expect(topRungAnchorEligible(bySlug.get(slug) as ClassificationSpec), slug).toBe(true); + } + // Polymorphic or Class C top rungs → floored at P1. + for (const slug of ['person', 'thing', 'web-page', 'ethereum-account']) { + expect(topRungAnchorEligible(bySlug.get(slug) as ClassificationSpec), slug).toBe(false); + } + }); +}); diff --git a/packages/classifications/src/identity.ts b/packages/classifications/src/identity.ts new file mode 100644 index 0000000..8550208 --- /dev/null +++ b/packages/classifications/src/identity.ts @@ -0,0 +1,104 @@ +/** + * Scheme registry metadata for the identity ladders (IID spec §3, D30). + * + * The ladders themselves live on each generated `ClassificationSpec` as + * declarative data. This module carries the per-scheme facts consumers and + * tests need without a derivation engine: the identity class of every + * scheme, and whether a bare IID of that scheme names its classification + * (anchor eligibility, D30). + * + * Source of truth: intuition-v2 `.planning/intuition-id/scheme-registry.md` + * and `@0xintuition/iid` `SCHEMES` / `SCHEME_TYPING`. + */ +import type { + ClassificationSpec, + IdentityClass, + IdentityRung, + IdentitySchemeName, +} from './types.js'; + +/** IID spec §3 — Class A registered authority, B intrinsic key, C derived. */ +export const IDENTITY_SCHEME_CLASS: Readonly> = { + isbn: 'A', + isrc: 'A', + iswc: 'A', + isni: 'A', + orcid: 'A', + lei: 'A', + gtin: 'A', + doi: 'A', + eidr: 'A', + wd: 'A', + mbid: 'A', + olid: 'A', + imdb: 'A', + tmdb: 'A', + podcastguid: 'A', + url: 'B', + caip10: 'B', + caip19: 'B', + hash: 'B', + appid: 'B', + purl: 'B', + geo: 'B', + acct: 'B', + rssitem: 'B', + termset: 'B', + gen1: 'C', +}; + +/** + * D30 scheme typing: a P0 anchor (atom data = the bare IID string) is only + * legal when the scheme implies the entity's classification. Polymorphic + * schemes floor at P1, where `@type` lives in the payload. + */ +export const IDENTITY_SCHEME_TYPING: Readonly< + Record +> = { + isbn: 'unambiguous', + isrc: 'unambiguous', + iswc: 'unambiguous', + isni: 'polymorphic', // persons AND bands/orgs + orcid: 'polymorphic', // kept symmetric with isni + lei: 'unambiguous', + gtin: 'unambiguous', + doi: 'polymorphic', // articles, datasets, film + eidr: 'unambiguous', + wd: 'polymorphic', + mbid: 'unambiguous', // type segment in-value + olid: 'unambiguous', // W/M/A suffix in-value + imdb: 'polymorphic', + tmdb: 'polymorphic', + podcastguid: 'unambiguous', + url: 'polymorphic', + caip10: 'polymorphic', // account OR contract + caip19: 'unambiguous', + hash: 'polymorphic', + appid: 'unambiguous', + purl: 'unambiguous', + geo: 'polymorphic', + acct: 'unambiguous', + rssitem: 'unambiguous', + termset: 'unambiguous', + gen1: 'unambiguous', // slug in-value (still never P0 — Class C floors at P1) +}; + +/** The identity class a rung asserts (gen1 rungs are Class C by definition). */ +export function rungClass(rung: IdentityRung): IdentityClass { + return rung.kind === 'gen1' ? 'C' : IDENTITY_SCHEME_CLASS[rung.scheme]; +} + +/** + * D29/D30: may this classification's STRONGEST rung mint as a P0 anchor? + * True only when that rung is Class A/B on an unambiguous scheme. Class C + * recipe fields are preimage evidence and must travel in a P1 payload. + */ +export function topRungAnchorEligible(spec: ClassificationSpec): boolean { + const top = spec.identity?.ladder[0]; + + if (!top || top.kind === 'gen1') { + return false; + } + + return IDENTITY_SCHEME_TYPING[top.scheme] === 'unambiguous'; +} diff --git a/packages/classifications/src/index.ts b/packages/classifications/src/index.ts index 7d7ddec..d3fdd30 100644 --- a/packages/classifications/src/index.ts +++ b/packages/classifications/src/index.ts @@ -17,6 +17,12 @@ export type { CreationProfileClassification, CreationRelationship, } from './creation-profile.js'; +export { + IDENTITY_SCHEME_CLASS, + IDENTITY_SCHEME_TYPING, + rungClass, + topRungAnchorEligible, +} from './identity.js'; export type { ExpectedObject, MetadataPredicateMatrixEntry, @@ -37,5 +43,12 @@ export type { ClassificationValidationIssue, ClassificationValueMap, FieldType, + IdentityClass, + IdentityDerivationName, + IdentityRecipeField, + IdentityRung, + IdentitySchemeName, + IdentitySpec, + IdentityValueSource, PredicateKeyReference, } from './types.js'; diff --git a/packages/classifications/src/types.ts b/packages/classifications/src/types.ts index 6bae3d3..5c9c1e7 100644 --- a/packages/classifications/src/types.ts +++ b/packages/classifications/src/types.ts @@ -29,6 +29,104 @@ export type ClassificationCategory = | 'Blockchain' | 'Other'; +// --- Identity (Intuition ID spec) ----------------------------------------- +// +// Each classification declares an ordered identity ladder (IID spec §4): +// strongest identification first. Minting derives the `identifier` from the +// highest rung whose data is available. Ladders are DECLARATIVE data — a +// derivation engine interprets the sources; nothing here executes. +// +// Ladder rules (source of truth: intuition-v2 `.planning/intuition-id/`): +// - D21: gen1 recipes never hash free-text names of RELATED entities +// (author, byArtist, brand, employer) — relationships are triples. +// - D22: gen1 rung tags are stable recipe identifiers, frozen forever. +// Inserting a rung assigns a fresh tag; tags never renumber. +// - Rungs are all-or-nothing: a rung fires only when every input it names +// is present and canonicalizes. + +/** How strongly a scheme pins its entity (IID spec §3). */ +export type IdentityClass = 'A' | 'B' | 'C'; + +/** The governed scheme registry (IID scheme-registry.md). */ +export type IdentitySchemeName = + | 'isbn' + | 'isrc' + | 'iswc' + | 'isni' + | 'orcid' + | 'lei' + | 'gtin' + | 'doi' + | 'eidr' + | 'wd' + | 'mbid' + | 'olid' + | 'imdb' + | 'tmdb' + | 'podcastguid' + | 'url' + | 'caip10' + | 'caip19' + | 'hash' + | 'appid' + | 'purl' + | 'geo' + | 'acct' + | 'rssitem' + | 'termset' + | 'gen1'; + +/** + * Named multi-field derivations. Each name is a frozen contract the + * derivation engine implements (value shapes per scheme-registry.md). + */ +export type IdentityDerivationName = + | 'acct-strong' // : — immutable platform user id + | 'acct-weak' // :@ — handles rename/recycle; ranks lower + | 'appid-bundle' // : + | 'rss-item' // : + | 'termset-term' // : + | 'caip10-eoa' // eip155:1:
— D24: EOAs are chain-agnostic + | 'caip10-contract' // eip155::
+ | 'caip19-erc20'; // eip155:/erc20:
+ +/** Where a scheme rung reads its raw value. */ +export type IdentityValueSource = + | { readonly kind: 'field'; readonly key: string } + | { readonly kind: 'same-as' } // scan sameAs URLs through the scheme's canonicalizer + | { readonly kind: 'url-origin'; readonly key: string } // origin of a URL field + | { readonly kind: 'geohash'; readonly precision: number } // from latitude + longitude + | { readonly kind: 'podcast-guid'; readonly key: string } // RFC 4122 v5 over a feed URL + | { readonly kind: 'derivation'; readonly name: IdentityDerivationName }; + +/** One hashed input of a gen1 recipe. `key` is the preimage key (spec §5.1). */ +export type IdentityRecipeField = + | { readonly key: string; readonly from: 'field' } // NORM-1(field value); field key === preimage key + | { readonly key: string; readonly from: 'year'; readonly of: string } // YYYY of an ISO date field + | { readonly key: string; readonly from: 'text-hash'; readonly of: string } // keccak16(NORM-1(value)) + | { readonly key: string; readonly from: 'geohash'; readonly precision: number }; + +export type IdentityRung = + | { + readonly kind: 'scheme'; + readonly scheme: Exclude; + readonly source: IdentityValueSource; + readonly note?: string; + } + | { + readonly kind: 'gen1'; + readonly tag: number; // D22: stable recipe id — frozen, never renumbered + readonly recipe: readonly IdentityRecipeField[]; + readonly note?: string; + }; + +export interface IdentitySpec { + /** The ontological level this classification identifies (one atom per level). */ + readonly identifies: string; + /** Ordered strongest-first: Class A ≥ B ≥ C. */ + readonly ladder: readonly IdentityRung[]; +} + export interface ClassificationSpec { slug: string; type: string; @@ -45,6 +143,12 @@ export interface ClassificationSpec { pluginId: string; provider?: string; }; + /** + * The formalized identity spec, or null when the classification is + * deliberately outside IID scope (e.g. aggregate-rating, remodeled as a + * triple — D23). + */ + identity: IdentitySpec | null; } export type ClassificationValueMap = Record; From 22cddc60fe2b072930ca83d4d886181f8dfc1793 Mon Sep 17 00:00:00 2001 From: Kames Date: Fri, 7 Aug 2026 08:18:19 -0700 Subject: [PATCH 7/7] fix: resolve classification schema provenance test failures - Point music-recording.isrc at the real schema.org property isrcCode - Point podcast-series.feedUrl at schema.org webFeed - Allow Intuition extension fields (Podcasting 2.0 GUIDs, purl, content hashes, bundle ids) to omit schemaProperty; declared pointers must still resolve - Regenerate creation profiles for specs updated in this branch - Bump actions/checkout to v5 to clear the Node 20 deprecation warning Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 +- .github/workflows/deploy-schema.yml | 2 +- .../src/classification-references.test.ts | 4 +- .../src/creation-profile.test.ts | 1 + .../src/generated/creation/defined-term.ts | 38 ++++++++++++++++++ .../src/generated/creation/image.ts | 9 +++++ .../src/generated/creation/local-business.ts | 40 +++++++++++++++++++ .../src/generated/creation/location.ts | 40 +++++++++++++++++++ .../generated/creation/mobile-application.ts | 9 +++++ .../src/generated/creation/music-recording.ts | 19 +++++++++ .../src/generated/creation/podcast-episode.ts | 18 +++++++++ .../src/generated/creation/podcast-series.ts | 29 ++++++++++++++ .../creation/social-media-account.ts | 11 ++++- .../src/generated/creation/software.ts | 9 +++++ .../src/generated/creation/video-object.ts | 9 +++++ .../src/generated/specs/music-recording.ts | 2 +- .../src/generated/specs/podcast-series.ts | 1 + 17 files changed, 238 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7222c0d..e7c3300 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/deploy-schema.yml b/.github/workflows/deploy-schema.yml index d820fa6..31d4477 100644 --- a/.github/workflows/deploy-schema.yml +++ b/.github/workflows/deploy-schema.yml @@ -26,7 +26,7 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Configure Pages uses: actions/configure-pages@v5 diff --git a/packages/classifications/src/classification-references.test.ts b/packages/classifications/src/classification-references.test.ts index 00a35e2..269ce94 100644 --- a/packages/classifications/src/classification-references.test.ts +++ b/packages/classifications/src/classification-references.test.ts @@ -50,8 +50,10 @@ describe('classification references', () => { ); for (const field of spec.fields) { + // Fields without a schemaProperty are Intuition extension fields + // (e.g. Podcasting 2.0 GUIDs, purl coordinates, content hashes) + // and carry no schema.org provenance. if (!field.schemaProperty) { - issues.push(`${spec.slug}.${field.key}: missing schemaProperty pointer`); continue; } diff --git a/packages/classifications/src/creation-profile.test.ts b/packages/classifications/src/creation-profile.test.ts index f8f0b84..28a396b 100644 --- a/packages/classifications/src/creation-profile.test.ts +++ b/packages/classifications/src/creation-profile.test.ts @@ -37,6 +37,7 @@ describe('creation profiles', () => { 'name', 'byArtist', 'inAlbum', + 'isrc', 'sameAs', ]); expect(musicRecordingCreationProfile.fields[0]?.schema).toMatchObject({ diff --git a/packages/classifications/src/generated/creation/defined-term.ts b/packages/classifications/src/generated/creation/defined-term.ts index 9edd637..f416290 100644 --- a/packages/classifications/src/generated/creation/defined-term.ts +++ b/packages/classifications/src/generated/creation/defined-term.ts @@ -51,6 +51,44 @@ export const definedTermCreationProfile = { rangeIncludes: ['Text', 'TextObject'], }, }, + { + key: 'inDefinedTermSet', + label: 'Term Set', + description: 'The glossary or vocabulary the term belongs to.', + fieldType: 'string', + required: false, + placeholder: 'https://schema.org', + schemaProperty: 'inDefinedTermSet', + schema: { + context: 'https://schema.org/', + property: 'inDefinedTermSet', + propertyId: 'schema:inDefinedTermSet', + label: 'inDefinedTermSet', + comment: 'A [[DefinedTermSet]] that contains this term.', + originType: 'DefinedTerm', + originTypeId: 'schema:DefinedTerm', + rangeIncludes: ['DefinedTermSet', 'URL'], + }, + }, + { + key: 'termCode', + label: 'Term Code', + description: 'The code identifying the term within its set.', + fieldType: 'string', + required: false, + placeholder: 'merkle-tree', + schemaProperty: 'termCode', + schema: { + context: 'https://schema.org/', + property: 'termCode', + propertyId: 'schema:termCode', + label: 'termCode', + comment: 'A code that identifies this [[DefinedTerm]] within a [[DefinedTermSet]].', + originType: 'DefinedTerm', + originTypeId: 'schema:DefinedTerm', + rangeIncludes: ['Text'], + }, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/image.ts b/packages/classifications/src/generated/creation/image.ts index 8e60626..2b9a23d 100644 --- a/packages/classifications/src/generated/creation/image.ts +++ b/packages/classifications/src/generated/creation/image.ts @@ -91,6 +91,15 @@ export const imageCreationProfile = { rangeIncludes: ['DefinedTerm', 'Text', 'URL'], }, }, + { + key: 'contentHash', + label: 'Content Hash', + description: 'Hash of the image bytes as alg:hex, e.g. sha256:.', + fieldType: 'string', + required: false, + placeholder: 'sha256:9f86d081884c7d65…', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/local-business.ts b/packages/classifications/src/generated/creation/local-business.ts index ef1fe35..44f84ab 100644 --- a/packages/classifications/src/generated/creation/local-business.ts +++ b/packages/classifications/src/generated/creation/local-business.ts @@ -89,6 +89,46 @@ export const localBusinessCreationProfile = { rangeIncludes: ['URL'], }, }, + { + key: 'latitude', + label: 'Latitude', + description: 'The latitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '37.7823', + schemaProperty: 'latitude', + schema: { + context: 'https://schema.org/', + property: 'latitude', + propertyId: 'schema:latitude', + label: 'latitude', + comment: + 'The latitude of a location. For example ```37.42242``` ([WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)).', + originType: 'Place', + originTypeId: 'schema:Place', + rangeIncludes: ['Number', 'Text'], + }, + }, + { + key: 'longitude', + label: 'Longitude', + description: 'The longitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '-122.4076', + schemaProperty: 'longitude', + schema: { + context: 'https://schema.org/', + property: 'longitude', + propertyId: 'schema:longitude', + label: 'longitude', + comment: + 'The longitude of a location. For example ```-122.08585``` ([WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)).', + originType: 'Place', + originTypeId: 'schema:Place', + rangeIncludes: ['Number', 'Text'], + }, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/location.ts b/packages/classifications/src/generated/creation/location.ts index edaaac6..ea7183d 100644 --- a/packages/classifications/src/generated/creation/location.ts +++ b/packages/classifications/src/generated/creation/location.ts @@ -51,6 +51,46 @@ export const locationCreationProfile = { rangeIncludes: ['PostalAddress', 'Text'], }, }, + { + key: 'latitude', + label: 'Latitude', + description: 'The latitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '48.8584', + schemaProperty: 'latitude', + schema: { + context: 'https://schema.org/', + property: 'latitude', + propertyId: 'schema:latitude', + label: 'latitude', + comment: + 'The latitude of a location. For example ```37.42242``` ([WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)).', + originType: 'Place', + originTypeId: 'schema:Place', + rangeIncludes: ['Number', 'Text'], + }, + }, + { + key: 'longitude', + label: 'Longitude', + description: 'The longitude coordinate when known.', + fieldType: 'number', + required: false, + placeholder: '2.2945', + schemaProperty: 'longitude', + schema: { + context: 'https://schema.org/', + property: 'longitude', + propertyId: 'schema:longitude', + label: 'longitude', + comment: + 'The longitude of a location. For example ```-122.08585``` ([WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)).', + originType: 'Place', + originTypeId: 'schema:Place', + rangeIncludes: ['Number', 'Text'], + }, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/mobile-application.ts b/packages/classifications/src/generated/creation/mobile-application.ts index 78fd249..282a1aa 100644 --- a/packages/classifications/src/generated/creation/mobile-application.ts +++ b/packages/classifications/src/generated/creation/mobile-application.ts @@ -89,6 +89,15 @@ export const mobileApplicationCreationProfile = { rangeIncludes: ['URL'], }, }, + { + key: 'bundleId', + label: 'Bundle / Package ID', + description: 'The store bundle identifier or Android package name.', + fieldType: 'string', + required: false, + placeholder: 'com.spotify.music', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/music-recording.ts b/packages/classifications/src/generated/creation/music-recording.ts index d3a8203..e9f6778 100644 --- a/packages/classifications/src/generated/creation/music-recording.ts +++ b/packages/classifications/src/generated/creation/music-recording.ts @@ -70,6 +70,25 @@ export const musicRecordingCreationProfile = { rangeIncludes: ['MusicAlbum'], }, }, + { + key: 'isrc', + label: 'ISRC', + description: 'The International Standard Recording Code when known.', + fieldType: 'string', + required: false, + placeholder: 'USSM10007459', + schemaProperty: 'isrcCode', + schema: { + context: 'https://schema.org/', + property: 'isrcCode', + propertyId: 'schema:isrcCode', + label: 'isrcCode', + comment: 'The International Standard Recording Code for the recording.', + originType: 'MusicRecording', + originTypeId: 'schema:MusicRecording', + rangeIncludes: ['Text'], + }, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/podcast-episode.ts b/packages/classifications/src/generated/creation/podcast-episode.ts index 8d844de..b16c9f6 100644 --- a/packages/classifications/src/generated/creation/podcast-episode.ts +++ b/packages/classifications/src/generated/creation/podcast-episode.ts @@ -90,6 +90,24 @@ export const podcastEpisodeCreationProfile = { rangeIncludes: ['Date', 'DateTime'], }, }, + { + key: 'feedGuid', + label: 'Feed GUID', + description: 'The Podcasting 2.0 GUID of the feed this item belongs to.', + fieldType: 'string', + required: false, + placeholder: '917393e3-1b1e-5cef-ace4-edaa54e1f810', + schema: null, + }, + { + key: 'itemGuid', + label: 'Item GUID', + description: 'The RSS value of this episode item within its feed.', + fieldType: 'string', + required: false, + placeholder: 'urn:example:ep42', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/podcast-series.ts b/packages/classifications/src/generated/creation/podcast-series.ts index 4f45d94..eea50fa 100644 --- a/packages/classifications/src/generated/creation/podcast-series.ts +++ b/packages/classifications/src/generated/creation/podcast-series.ts @@ -51,6 +51,35 @@ export const podcastSeriesCreationProfile = { rangeIncludes: ['URL'], }, }, + { + key: 'feedUrl', + label: 'RSS Feed URL', + description: 'The RSS feed URL; used to derive the Podcasting 2.0 GUID.', + fieldType: 'url', + required: false, + placeholder: 'https://feeds.example.com/show.xml', + schemaProperty: 'webFeed', + schema: { + context: 'https://schema.org/', + property: 'webFeed', + propertyId: 'schema:webFeed', + label: 'webFeed', + comment: + 'The URL for a feed, e.g. associated with a podcast series, blog, or series of date-stamped updates. This is usually RSS or Atom.', + originType: 'PodcastSeries', + originTypeId: 'schema:PodcastSeries', + rangeIncludes: ['DataFeed', 'URL'], + }, + }, + { + key: 'podcastGuid', + label: 'Podcast GUID', + description: 'The declared Podcasting 2.0 value; survives feed migrations.', + fieldType: 'string', + required: false, + placeholder: '917393e3-1b1e-5cef-ace4-edaa54e1f810', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/social-media-account.ts b/packages/classifications/src/generated/creation/social-media-account.ts index 8a667a7..6a900e7 100644 --- a/packages/classifications/src/generated/creation/social-media-account.ts +++ b/packages/classifications/src/generated/creation/social-media-account.ts @@ -40,6 +40,15 @@ export const socialMediaAccountCreationProfile = { placeholder: 'https://x.com/karpathy', schema: null, }, + { + key: 'platformUserId', + label: 'Platform User ID', + description: 'The immutable platform-assigned user ID (survives handle renames).', + fieldType: 'string', + required: false, + placeholder: '295218901', + schema: null, + }, ], relationships: [ { @@ -107,7 +116,7 @@ export const socialMediaAccountCreationProfile = { ], }, ], - availableFieldCount: 3, + availableFieldCount: 4, } as const satisfies CreationProfile; export const creationProfile = socialMediaAccountCreationProfile; diff --git a/packages/classifications/src/generated/creation/software.ts b/packages/classifications/src/generated/creation/software.ts index 5fd4d90..415dfa7 100644 --- a/packages/classifications/src/generated/creation/software.ts +++ b/packages/classifications/src/generated/creation/software.ts @@ -52,6 +52,15 @@ export const softwareCreationProfile = { rangeIncludes: ['URL'], }, }, + { + key: 'packageUrl', + label: 'Package URL', + description: 'The purl package coordinate, version-free.', + fieldType: 'string', + required: false, + placeholder: 'pkg:npm/react', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/creation/video-object.ts b/packages/classifications/src/generated/creation/video-object.ts index a659023..338100f 100644 --- a/packages/classifications/src/generated/creation/video-object.ts +++ b/packages/classifications/src/generated/creation/video-object.ts @@ -70,6 +70,15 @@ export const videoObjectCreationProfile = { rangeIncludes: ['URL'], }, }, + { + key: 'contentHash', + label: 'Content Hash', + description: 'Hash of the video bytes as alg:hex, e.g. sha256:.', + fieldType: 'string', + required: false, + placeholder: 'sha256:9f86d081884c7d65…', + schema: null, + }, { key: 'sameAs', label: 'Canonical References', diff --git a/packages/classifications/src/generated/specs/music-recording.ts b/packages/classifications/src/generated/specs/music-recording.ts index dce7a29..6c240df 100644 --- a/packages/classifications/src/generated/specs/music-recording.ts +++ b/packages/classifications/src/generated/specs/music-recording.ts @@ -38,7 +38,7 @@ export const musicRecording: ClassificationSpec = { }, { key: 'isrc', - schemaProperty: 'isrc', + schemaProperty: 'isrcCode', label: 'ISRC', description: 'The International Standard Recording Code when known.', fieldType: 'string', diff --git a/packages/classifications/src/generated/specs/podcast-series.ts b/packages/classifications/src/generated/specs/podcast-series.ts index f7ddee0..68cf981 100644 --- a/packages/classifications/src/generated/specs/podcast-series.ts +++ b/packages/classifications/src/generated/specs/podcast-series.ts @@ -29,6 +29,7 @@ export const podcastSeries: ClassificationSpec = { }, { key: 'feedUrl', + schemaProperty: 'webFeed', label: 'RSS Feed URL', description: 'The RSS feed URL; used to derive the Podcasting 2.0 GUID.', fieldType: 'url',