{
  "schema": "graph-rest-contract/1",
  "build_contract": "https://miscsubjects.com/build.json",
  "origin": "https://miscsubjects.com",
  "canonical_schema": "https://miscsubjects.com/schemas/jarticle.json",
  "identity": "One stored object per slug. Pages, embedded cards and query results resolve that slug. Account/profile URLs and source posts have their own objects.",
  "minimum_required": [
    "slug",
    "title"
  ],
  "typed_minimum": [
    "slug",
    "title",
    "kind"
  ],
  "publication": "status defaults to draft; use published to show on the site. A self entity block draws the existing native object widget.",
  "headers": {
    "Content-Type": "application/json",
    "X-Terminal-Key": "<YOUR_EXISTING_TERMINAL_KEY>",
    "If-Match": "<hash from GET; required for edit/delete>"
  },
  "sheet_columns": [
    "method",
    "path",
    "body_json",
    "if_match",
    "http_status",
    "response_json"
  ],
  "operations": {
    "add": {
      "method": "POST",
      "path": "/api/jarticles",
      "body": {
        "slug": "new-person",
        "title": "New person",
        "kind": "person",
        "status": "published",
        "body": [
          {
            "type": "entity",
            "ref": "#"
          }
        ]
      },
      "success": 201,
      "duplicate": 409
    },
    "read": {
      "method": "GET",
      "path": "/api/jarticles/{slug}"
    },
    "edit": {
      "method": "PATCH",
      "path": "/api/jarticles/{slug}",
      "headers": {
        "If-Match": "<hash>"
      },
      "body": {
        "ops": [
          {
            "op": "set",
            "path": "facts.street",
            "value": "Vichy Avenue"
          }
        ]
      },
      "stale": 412
    },
    "query": {
      "method": "POST",
      "path": "/api/jarticles/query",
      "body": {
        "kind": "person",
        "related": {
          "predicate": "resides_at",
          "to": {
            "kind": "house",
            "where": {
              "facts.street": "Vichy Avenue"
            }
          },
          "current": true
        },
        "scope": "all"
      }
    },
    "public_query": {
      "method": "GET",
      "path": "/graph/query.json?q=<URL-encoded query JSON>"
    },
    "manifest": {
      "method": "GET",
      "path": "/graph/manifest.json?limit=200",
      "continuation": "Follow next verbatim. A changed snapshot returns 409; restart. After the complete manifest, remove absent IDs from your own mirror. Fetch changed objects by their JSON link and compare hashes."
    }
  },
  "starter_person": {
    "slug": "new-person",
    "title": "New person",
    "kind": "person"
  },
  "starter_house": {
    "slug": "new-house",
    "title": "New house",
    "kind": "house"
  },
  "reference": {
    "type": "entity",
    "ref": "337-melrose-avenue"
  },
  "query": {
    "scope": [
      "articles",
      "atlas",
      "all"
    ],
    "where": "Field path → exact value, or {eq|contains|ends_with|in|exists: value}. String matching is case-insensitive.",
    "related": "{predicate, to: id or selector}, or {predicate, from: id or selector}. Array means all; any/all combine selectors. current:true excludes historical or ended relationships.",
    "pagination": "limit 1–200; follow next as the next POST body. Every page reports total and provider coverage.",
    "sort": "Field path; direction asc or desc. Articles sort within their provider; Atlas is ordered by ID. Social-post feeds use published_at.",
    "sync": "updated_since filters creation/edit recency, not source publication time. Use a full manifest to detect unpublishing/deletion. No automatic crawler or recurring trigger is installed."
  },
  "examples": {
    "people_on_vichy_avenue": {
      "kind": "person",
      "related": {
        "predicate": "resides_at",
        "to": {
          "kind": "house",
          "where": {
            "facts.street": "Vichy Avenue"
          }
        },
        "current": true
      },
      "scope": "all"
    },
    "gmail_addresses": {
      "kind": "email",
      "where": {
        "facts.domain": "gmail.com"
      },
      "scope": "all"
    },
    "linkedin_profiles": {
      "kind": "social_profile",
      "where": {
        "facts.platform": "linkedin"
      },
      "scope": "all"
    },
    "posts_by_marin_people": {
      "kind": "social_post",
      "related": {
        "predicate": "posted_by",
        "to": {
          "kind": "social_profile",
          "related": {
            "predicate": "account_of",
            "to": {
              "kind": "person",
              "related": {
                "predicate": "associated_with_place",
                "to": "marin-county"
              }
            }
          }
        }
      },
      "sort": "facts.published_at",
      "direction": "desc",
      "limit": 9
    },
    "posts_by_marin_people_and_their_organizations": {
      "kind": "social_post",
      "related": {
        "predicate": "posted_by",
        "to": {
          "kind": "social_profile",
          "related": {
            "predicate": "account_of",
            "to": {
              "any": [
                {
                  "kind": "person",
                  "related": {
                    "predicate": "associated_with_place",
                    "to": "marin-county"
                  }
                },
                {
                  "related": {
                    "predicate": "owned_by",
                    "to": {
                      "kind": "person",
                      "related": {
                        "predicate": "associated_with_place",
                        "to": "marin-county"
                      }
                    }
                  }
                }
              ]
            }
          }
        }
      },
      "sort": "facts.published_at",
      "direction": "desc",
      "limit": 9
    },
    "posts_tagging_marin_ij": {
      "kind": "social_post",
      "related": {
        "predicate": "tags_account",
        "to": {
          "kind": "social_profile",
          "related": {
            "predicate": "account_of",
            "to": "marin-independent-journal"
          }
        }
      },
      "sort": "facts.published_at",
      "direction": "desc"
    },
    "posts_with_marincounty_hashtag": {
      "kind": "social_post",
      "related": {
        "predicate": "has_hashtag",
        "to": "hashtag-marincounty"
      },
      "sort": "facts.published_at",
      "direction": "desc"
    },
    "facebook_marincounty_posts": {
      "kind": "social_post",
      "related": {
        "predicate": "has_hashtag",
        "to": "hashtag-marincounty"
      },
      "sort": "facts.published_at",
      "direction": "desc",
      "where": {
        "facts.platform": "facebook"
      }
    },
    "dsco_people": {
      "kind": "person",
      "related": {
        "predicate": "works_at",
        "to": "dsco",
        "current": true
      }
    },
    "facebook_profiles": {
      "kind": "social_profile",
      "where": {
        "facts.platform": "facebook"
      },
      "scope": "all"
    },
    "instagram_profiles": {
      "kind": "social_profile",
      "where": {
        "facts.platform": "instagram"
      },
      "scope": "all"
    },
    "email_addresses": {
      "kind": "email",
      "scope": "all"
    }
  },
  "rules": [
    "A shared handle or same display name is evidence, not automatic same-person identity.",
    "Prior residence is previously_resided_at; current residence is resides_at. Workplace addresses do not imply residence.",
    "A post tagging an account, a post containing a hashtag and a post authored by someone associated with a place are separate paths.",
    "Owner statements, source observations, source publication time and observation time remain distinct.",
    "A Gmail-domain query matches gmail.com. A custom domain is not gmail.com even if Gmail hosts it.",
    "Query scope is the public canonical-object store and Atlas. Raw discovery observations are provenance, not independently merged profiles."
  ],
  "apps_script": "https://miscsubjects.com/graph/sheets.gs"
}