{
  "$id": "https://fmind.github.io/fkf/fkf.schema.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "The committed definition of one fkf base. It holds no secret.",
  "properties": {
    "bin": {
      "description": "Absolute or ~-relative machine-local directories outside the base, prepended to PATH for every declared command. Put base-controlled executables in \u003cbase\u003e/sources so trust hashes them.",
      "items": {
        "type": "string"
      },
      "type": "array"
    },
    "clients": {
      "additionalProperties": {
        "additionalProperties": false,
        "properties": {
          "script": {
            "maxLength": 255,
            "pattern": "^[a-z0-9][a-z0-9_-]*\\.py$",
            "type": "string"
          },
          "url": {
            "pattern": "^https://[^\\s?#@]+$",
            "type": "string"
          }
        },
        "required": [
          "url",
          "script"
        ],
        "type": "object"
      },
      "description": "Online apps with one uv Python script each under clients/; called through explicit uv argv.",
      "propertyNames": {
        "maxLength": 63,
        "pattern": "^[a-z0-9][a-z0-9-]*$"
      },
      "type": "object"
    },
    "fkf": {
      "const": 1,
      "description": "Configuration contract marker. v1 accepts exactly fkf: 1.",
      "type": "integer"
    },
    "identities": {
      "additionalProperties": {
        "additionalProperties": false,
        "properties": {
          "aliases": {
            "description": "Exact entity URIs, emails, or provider logins that resolve to canonical.",
            "items": {
              "maxLength": 320,
              "minLength": 1,
              "pattern": "^(?:[a-z][a-z0-9+.-]*:[^\\s]+|[A-Za-z0-9][A-Za-z0-9._+@-]*)$",
              "type": "string"
            },
            "minItems": 1,
            "type": "array",
            "uniqueItems": true
          },
          "canonical": {
            "description": "Canonical entity URI using an open non-reserved scheme.",
            "pattern": "^[a-z][a-z0-9+.-]*:[^\\s]+$",
            "type": "string"
          },
          "kind": {
            "description": "Optional classification used by graph and people views.",
            "enum": [
              "person",
              "organization",
              "repository"
            ],
            "type": "string"
          },
          "owner": {
            "default": false,
            "description": "Marks the one owning person, omitted from ambient people discovery and expansion.",
            "type": "boolean"
          }
        },
        "required": [
          "canonical",
          "aliases"
        ],
        "type": "object"
      },
      "description": "Declared exact aliases for canonical people, organizations, and repositories.",
      "propertyNames": {
        "maxLength": 63,
        "pattern": "^[a-z0-9][a-z0-9-]*$"
      },
      "type": "object"
    },
    "layers": {
      "additionalProperties": false,
      "description": "Explicit activation. A disabled layer is not created, listed, served, or scanned.",
      "properties": {
        "events": {
          "description": "Dated collected documents (JSON).",
          "type": "boolean"
        },
        "index": {
          "description": "Point-in-time collected documents (JSON).",
          "type": "boolean"
        },
        "projects": {
          "description": "Intent and decisions over weeks (Markdown, status-bearing).",
          "type": "boolean"
        },
        "tasks": {
          "description": "Execution evidence (Markdown).",
          "type": "boolean"
        },
        "wiki": {
          "description": "Durable approved knowledge (Markdown, OKF v0.2).",
          "type": "boolean"
        }
      },
      "type": "object"
    },
    "name": {
      "description": "MCP server name and resource URI authority; informational elsewhere.",
      "maxLength": 63,
      "pattern": "^[a-z0-9][a-z0-9-]*$",
      "type": "string"
    },
    "schema": {
      "additionalProperties": {
        "additionalProperties": false,
        "properties": {
          "cardinality": {
            "description": "Allowed scalar count per record or explicit Markdown relation list.",
            "enum": [
              "one",
              "optional",
              "many"
            ],
            "type": "string"
          },
          "description": {
            "description": "Shared human and machine meaning of this field across sources and authored relations.",
            "maxLength": 512,
            "minLength": 1,
            "type": "string"
          },
          "examples": {
            "description": "Bounded examples that clarify the semantic value or URI shape.",
            "items": {
              "maxLength": 512,
              "type": "string"
            },
            "maxItems": 8,
            "type": "array"
          },
          "relation": {
            "default": false,
            "description": "Values are canonical fkf URIs transcribed as graph edges of this field name.",
            "type": "boolean"
          },
          "weight": {
            "description": "Optional lexical-ranking multiplier. Defaults to 10 for id, 5 for title, and 1 for every other field.",
            "maximum": 100,
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "description",
          "cardinality"
        ],
        "type": "object"
      },
      "description": "Open semantic dictionary shared by every source and authored relation. id must have cardinality one; fkf enforces cross-field rules while loading.",
      "maxProperties": 64,
      "minProperties": 1,
      "propertyNames": {
        "maxLength": 64,
        "pattern": "^[a-z][a-z0-9_-]*$"
      },
      "required": [
        "id"
      ],
      "type": "object"
    },
    "sources": {
      "additionalProperties": {
        "additionalProperties": false,
        "allOf": [
          {
            "required": [
              "fields"
            ]
          },
          {
            "if": {
              "anyOf": [
                {
                  "not": {
                    "required": [
                      "layer"
                    ]
                  }
                },
                {
                  "properties": {
                    "layer": {
                      "const": "events"
                    }
                  }
                }
              ]
            },
            "then": {
              "properties": {
                "fields": {
                  "required": [
                    "id",
                    "time",
                    "title"
                  ]
                }
              }
            }
          },
          {
            "if": {
              "required": [
                "max_age_hours"
              ]
            },
            "then": {
              "properties": {
                "layer": {
                  "const": "index"
                }
              },
              "required": [
                "layer"
              ]
            }
          }
        ],
        "properties": {
          "auth": {
            "description": "Optional direct argv that checks provider login readiness before collection. It accepts no placeholders; stdout and stderr are discarded and never logged.",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "prefixItems": [
              {
                "description": "Literal executable: a bare name resolved on PATH or an absolute machine-local path outside the base.",
                "minLength": 1,
                "not": {
                  "pattern": "\\{\\{"
                },
                "type": "string"
              }
            ],
            "type": "array"
          },
          "bodies": {
            "default": "none",
            "description": "Rebuildable body-cache policy: none never stores, cache stores after explicit read --body, and sync also prefetches missing bodies during collection.",
            "enum": [
              "none",
              "cache",
              "sync"
            ],
            "type": "string"
          },
          "body": {
            "description": "Argv (never a shell string) fetching one record's body on demand. Must name {{id}} and may name any declared field plus {{base}} or {{home}}.",
            "items": {
              "type": "string"
            },
            "minItems": 2,
            "prefixItems": [
              {
                "description": "Literal executable: a bare name resolved on PATH or an absolute machine-local path outside the base. Placeholders and base-relative paths are refused.",
                "minLength": 1,
                "not": {
                  "pattern": "\\{\\{"
                },
                "type": "string"
              }
            ],
            "type": "array"
          },
          "enabled": {
            "description": "Whether sync runs this source. Disabled entries are still validated.",
            "type": "boolean"
          },
          "fields": {
            "additionalProperties": {
              "oneOf": [
                {
                  "description": "A declared semantic projection indexed for lexical context; every path contributes scalar values. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                  "pattern": "^\\.",
                  "type": "string"
                },
                {
                  "items": {
                    "description": "A declared semantic projection indexed for lexical context; every path contributes scalar values. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  "maxItems": 32,
                  "minItems": 1,
                  "type": "array"
                }
              ]
            },
            "description": "Associates root schema names with provider paths. id and title are required, plus time for events; every declared value is indexed lexically and relation fields are transcribed into the graph.",
            "maxProperties": 64,
            "minProperties": 2,
            "properties": {
              "category": {
                "oneOf": [
                  {
                    "description": "Optional authorship role: created, received, or saved; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Optional authorship role: created, received, or saved; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              },
              "id": {
                "oneOf": [
                  {
                    "description": "Required. Exactly one scalar is the record identity that its URI fragment names. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Required. Exactly one scalar is the record identity that its URI fragment names. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              },
              "time": {
                "oneOf": [
                  {
                    "description": "Required for an events source. Exactly one scalar is the record timestamp. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Required for an events source. Exactly one scalar is the record timestamp. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              },
              "title": {
                "oneOf": [
                  {
                    "description": "Required meaningful human-readable subject line; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Required meaningful human-readable subject line; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              },
              "url": {
                "oneOf": [
                  {
                    "description": "Suggested provider URL; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Suggested provider URL; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              },
              "visibility": {
                "oneOf": [
                  {
                    "description": "Optional audience role: private, shared, or public; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                    "pattern": "^\\.",
                    "type": "string"
                  },
                  {
                    "items": {
                      "description": "Optional audience role: private, shared, or public; at most one scalar. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
                      "pattern": "^\\.",
                      "type": "string"
                    },
                    "maxItems": 32,
                    "minItems": 1,
                    "type": "array"
                  }
                ]
              }
            },
            "propertyNames": {
              "maxLength": 64,
              "pattern": "^[a-z][a-z0-9_-]*$"
            },
            "required": [
              "id",
              "title"
            ],
            "type": "object"
          },
          "format": {
            "default": "json",
            "description": "json expects one document; ndjson expects one JSON value per line.",
            "enum": [
              "json",
              "ndjson"
            ],
            "type": "string"
          },
          "install": {
            "description": "Printed by `fkf status` when the binary is missing. Never executed.",
            "type": "string"
          },
          "layer": {
            "default": "events",
            "description": "events files one JSON document per day; index files one point-in-time JSON document.",
            "enum": [
              "events",
              "index"
            ],
            "type": "string"
          },
          "max_age_hours": {
            "description": "Refresh this index source after this many hours; overrides sync.index_max_age_hours.",
            "maximum": 87600,
            "minimum": 1,
            "type": "integer"
          },
          "min_interval": {
            "description": "Least time between two invocations of THIS source across the whole sync. Retry spaces the attempts of one failing call; this spaces every call, which is what a provider's rate limit actually counts.",
            "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+)$",
            "type": "string"
          },
          "recency": {
            "additionalProperties": false,
            "properties": {
              "half_life_days": {
                "description": "Source-local exponential recency half-life; undated records receive no bonus.",
                "maximum": 3650,
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "half_life_days"
            ],
            "type": "object"
          },
          "records": {
            "description": "Path to the records inside each decoded document or page. A jq subset: .key, .a.b, [n], [], .\"odd key\".",
            "pattern": "^\\.",
            "type": "string"
          },
          "requires": {
            "description": "Executable names fkf status checks on the ordinary collection/body PATH, including helpers and non-standard interpreters. FKF reports test[0] readiness separately on the test-only tests/ PATH and never infers dependencies from argv or helper contents.",
            "items": {
              "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]*$",
              "type": "string"
            },
            "type": "array",
            "uniqueItems": true
          },
          "retry": {
            "additionalProperties": false,
            "dependentRequired": {
              "backoff": [
                "attempts"
              ],
              "on": [
                "attempts"
              ]
            },
            "description": "How fkf re-invokes this command, never what it is — the same relationship `timeout:` has to `run:`. `fkf trust` prints it beside the line it modifies.",
            "properties": {
              "attempts": {
                "description": "Total runs allowed, including the first.",
                "maximum": 5,
                "minimum": 1,
                "type": "integer"
              },
              "backoff": {
                "description": "Wait before the next attempt, growing linearly with the attempt number.",
                "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+)$",
                "type": "string"
              },
              "on": {
                "description": "Which failures may be retried: `exit:\u003cn\u003e`, or a substring matched against the command's stderr. Required whenever attempts exceeds one — retrying every failure is how a source failing for a real reason hammers a provider quietly. The matched text is never logged or stored.",
                "items": {
                  "minLength": 1,
                  "type": "string"
                },
                "minItems": 1,
                "type": "array"
              }
            },
            "type": "object"
          },
          "run": {
            "description": "Direct argv producing JSON; no shell parses it. A helper shebang selects its interpreter. Argument placeholders: {{date}}, {{next_date}}, {{start}}, {{end}}, {{base}}, {{home}}. No collected data is ever substituted.",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "prefixItems": [
              {
                "description": "Literal executable: a bare name resolved on PATH or an absolute machine-local path outside the base.",
                "minLength": 1,
                "not": {
                  "pattern": "\\{\\{"
                },
                "type": "string"
              }
            ],
            "type": "array"
          },
          "test": {
            "description": "Optional direct argv run by `fkf test` to verify this source. The trusted base tests/ tree is prepended only for this command; it receives no record or collection window. Argument placeholders: {{base}}, {{home}}.",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "prefixItems": [
              {
                "description": "Literal executable: a bare name resolved on the test-only PATH (tests/ first), or an absolute machine-local path outside the base.",
                "minLength": 1,
                "not": {
                  "pattern": "\\{\\{"
                },
                "type": "string"
              }
            ],
            "type": "array"
          },
          "timeout": {
            "description": "Per-command timeout; overrides sync.timeout.",
            "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+)$",
            "type": "string"
          },
          "window": {
            "default": false,
            "description": "Render run: ONCE for the whole requested range — {{start}}/{{end}} span every day being collected, not one. Events bucket records by fields.time; index sources reject it.",
            "type": "boolean"
          }
        },
        "required": [
          "run"
        ],
        "type": "object"
      },
      "description": "Declared collection commands, keyed by \u003cprovider\u003e-\u003cresource\u003e.",
      "propertyNames": {
        "maxLength": 250,
        "pattern": "^[a-z0-9][a-z0-9-]*$"
      },
      "type": "object"
    },
    "sync": {
      "additionalProperties": false,
      "properties": {
        "concurrency": {
          "default": 4,
          "maximum": 4,
          "minimum": 1,
          "type": "integer"
        },
        "days": {
          "default": 30,
          "description": "Completed local days to collect when no --date is given.",
          "maximum": 366,
          "minimum": 1,
          "type": "integer"
        },
        "index_max_age_hours": {
          "default": 168,
          "description": "Refresh an index document only when it is older than this; 1..87600.",
          "maximum": 87600,
          "minimum": 1,
          "type": "integer"
        },
        "timeout": {
          "description": "Per-command timeout, 1s..1h.",
          "pattern": "^(0|([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+)$",
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "required": [
    "fkf",
    "name",
    "schema",
    "layers"
  ],
  "title": "fkf base configuration",
  "type": "object"
}
