Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The schema follows a few rules, informed by how agent-maintained wikis actually
- **Trust is structural, not remembered.** Immutable `raw/`, append-only `log.md`, a git commit per operation, a pre-commit hook that lints the staged snapshot (exactly the bytes that will land) and makes lint errors uncommittable, and commit-pinned verification fields. Agent-maintained judgment metadata decays, so `confidence:` is reduced to the two states a lint can actually check (`low` = uncited, `contested` = sources disagree and the body must explain), inferred claims are marked inline with `(inferred)`, and tags are validated against a `taxonomy.md`, whose `## Page types` section also describes every allowed page type with a one-line meaning, so each bundle self-describes its type vocabulary to OKF consumers.
- **Contested is a state to exit.** The documented failure mode of agent wikis is contradictions accumulating faster than they resolve. Lint flags contested pages older than 30 days; the reconcile workflow rewrites in place, moving losing claims to a dated "Superseded claims" section instead of deleting them.
- **Autonomous but reversible.** The maintenance workflow runs unattended on a `maintenance` branch with an exhaustively-listed set of safe actions (mechanical fixes, index rebuild, unambiguous cross-links); everything else becomes a proposal. The human reviews the branch diff and merges. Nothing automated ever lands on main directly.
- **Native OKF conformance.** Every wiki is an [OKF v0.2](https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md) bundle: markdown files with YAML frontmatter, ordinary markdown links as the edge form (bundle-absolute `[title](/dir/page.md)`, the form v0.2 §6.1 recommends), reserved `index.md` (stamped with `okf_version` frontmatter by `rebuild-index`, the mechanism §12 specifies) and `log.md` (date-grouped headings, bold-action-word entries). `check_okf` enforces the spec's three conformance rules: parseable frontmatter on every non-reserved `.md`, a non-empty `type`, and reserved-file structure. Provenance uses the spec's `sources` shape (§5.1): mapping entries with a required `resource`, enforced by `check_sources`, with pre-0.2 string entries downgraded to warnings. Scriptorium's schema is a strict superset of OKF's (the spec's `generated.at` is our `updated`; its `resource` is our `source_path`; its `title` is optional, with the index prettifying filenames when absent). One deliberate deviation: `raw/` is excluded from conformance because prime directive 1 makes those sources immutable, and OKF has no concept of a non-concept directory.
- **Native OKF conformance.** Every wiki is an [OKF v0.2](https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/main/SPEC.md) bundle: markdown files with YAML frontmatter, ordinary markdown links as the edge form (bundle-absolute `[title](/dir/page.md)`, the form v0.2 §6.1 recommends), reserved `index.md` (stamped with `okf_version` frontmatter by `rebuild-index`, the mechanism §12 specifies) and `log.md` (date-grouped headings, bold-action-word entries). `check_okf` enforces the spec's three conformance rules: parseable frontmatter on every non-reserved `.md`, a non-empty `type`, and reserved-file structure. Provenance uses the spec's `sources` shape (§5.1): mapping entries with a required `resource`, enforced by `check_sources`, with pre-0.2 string entries downgraded to warnings. Pages carry OKF's own page-level keys directly: a required `title`, `generated: { by, at }` with an offset datetime (§5.2; the spec's last-meaningful-change record), and the §5.4 `status` values (`draft | stable | deprecated`), with `check_okf_fields` validating their shape and a page named in another page's `supersedes` required to be `deprecated`. Everything else (`created`, `confidence`, `supersedes`, and the spec's `resource` as our `source_path`) is a producer extension key, so the schema stays a strict superset of OKF's. One deliberate deviation: `raw/` is excluded from conformance because prime directive 1 makes those sources immutable, and OKF has no concept of a non-concept directory.
- **Opt-in extension points for non-wiki trees.** The engine can also lint markdown trees that aren't wikis (a findings folder, a labs journal): `okf_conformance: False` turns off the OKF rules for trees that aren't bundles, `non_page_allowed` accepts glob patterns, `index_file`/`index_body_fn` relocate and reshape the generated index (`index_file: None` disables it), `extra_secret_patterns`/`secret_allow_res` extend the secrets scan, `extra_checks` runs custom callables, and `extra_commands` registers extra `lint.py <verb>` subcommands (`{verb: (callable(root) -> exit code, help)}`) which dispatch ahead of the wiki-root guard and appear in `lint.py help` alongside the engine's own verbs, so a wiki never has to intercept `argv` and end up with a second, partial usage string. Every knob's default lives once in `wikilint/settings.py` (`DEFAULTS`) and a wiki's `lint.py` lists a key only to override it. `DEFAULTS` covers *every* key the engine reads, in two documented tiers: `EXTENSION_DEFAULTS` (the extension points above) preserves the original wiki behavior, so a wiki written before an extension existed keeps behaving as it did; `CORE_DEFAULTS` (the schema knobs — page dirs, required fields, staleness, ADRs, mermaid, coverage, ...) defaults to the neutral/disabled value, so a default can only ever silence a check, never invent one. A minimal config is therefore a handful of lines rather than a full key list. Bad values (a malformed regex, an out-of-tree `index_file`, a non-callable check) are rejected at startup with a clear message rather than a mid-run traceback.

## Unattended maintenance
Expand Down
32 changes: 18 additions & 14 deletions tests/extension_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@


def index_entry_extra(fields):
return f"(updated {fields.get('updated', '?')})"
generated = fields.get("generated")
at = generated.get("at", "") if isinstance(generated, dict) else ""
return f"(updated {at[:10] or '?'})"


# knobs: infra
Expand Down Expand Up @@ -41,11 +43,12 @@ def index_entry_extra(fields):
'contested_max_days': 30,
'reverse_fields': ['depends_on'],
'membership': {'member_type': 'component',
'status_field': 'component_status',
'active_statuses': ['active'],
'container_type': 'topology',
'container_field': 'includes'},
'required_fields': ['type', 'created', 'updated', 'description', 'tags'],
'type_required': {'component': ['component_kind', 'status', 'last_verified'],
'required_fields': ['type', 'created', 'generated.at', 'description', 'tags'],
'type_required': {'component': ['component_kind', 'component_status', 'last_verified'],
'topology': ['scope', 'includes'],
'concept': [],
'runbook': [],
Expand All @@ -62,7 +65,7 @@ def index_entry_extra(fields):
'subnet',
'peer',
'volume'],
'status': ['active',
'component_status': ['active',
'planned',
'deprecated',
'retired']},
Expand Down Expand Up @@ -137,13 +140,14 @@ def index_entry_extra(fields):
'producers',
'consumers'],
'membership': {'member_type': 'module',
'status_field': 'module_status',
'active_statuses': ['active'],
'container_type': 'architecture',
'container_field': 'includes'},
'required_fields': ['type', 'created', 'updated', 'description', 'tags'],
'required_fields': ['type', 'created', 'generated.at', 'description', 'tags'],
'type_required': {'module': ['source_path',
'language',
'status',
'module_status',
'last_verified_commit',
'last_verified'],
'service': ['source_path',
Expand All @@ -159,15 +163,15 @@ def index_entry_extra(fields):
'storage',
'last_verified_commit'],
'architecture': ['scope', 'includes'],
'adr': ['adr_number', 'status', 'date'],
'adr': ['adr_number', 'adr_status', 'date'],
'concept': [],
'runbook': [],
'postmortem': [],
'synthesis': [],
'source': [],
'query': []},
'enum_fields': {'confidence': ['low', 'contested']},
'type_enum_fields': {'module': {'status': ['active',
'type_enum_fields': {'module': {'module_status': ['active',
'experimental',
'deprecated',
'removed']},
Expand All @@ -190,7 +194,7 @@ def index_entry_extra(fields):
'data-flow',
'deployment',
'request-flow']},
'adr': {'status': ['proposed',
'adr': {'adr_status': ['proposed',
'accepted',
'superseded',
'rejected']}},
Expand Down Expand Up @@ -270,15 +274,15 @@ def index_entry_extra(fields):
'membership': None,
'required_fields': ['type',
'created',
'updated',
'generated.at',
'description',
'tags',
'subsystem'],
'type_required': {'subsystem': ['owners', 'mission', 'last_owner_review'],
'owners': [],
'module': ['source_path',
'language',
'status',
'module_status',
'criticality',
'last_verified_commit',
'last_verified',
Expand All @@ -296,15 +300,15 @@ def index_entry_extra(fields):
'storage',
'last_verified_commit'],
'architecture': ['scope', 'includes'],
'adr': ['adr_number', 'status', 'date'],
'adr': ['adr_number', 'adr_status', 'date'],
'concept': [],
'runbook': [],
'postmortem': [],
'synthesis': [],
'source': [],
'query': []},
'enum_fields': {'confidence': ['low', 'contested']},
'type_enum_fields': {'module': {'status': ['active',
'type_enum_fields': {'module': {'module_status': ['active',
'experimental',
'deprecated',
'removed'],
Expand Down Expand Up @@ -335,7 +339,7 @@ def index_entry_extra(fields):
'data-flow',
'deployment',
'request-flow']},
'adr': {'status': ['proposed',
'adr': {'adr_status': ['proposed',
'accepted',
'superseded',
'rejected']}},
Expand Down
5 changes: 4 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ def use_variant(variant):

def page(ptype, description, extra_fm="", body="Body.\n", tags="[alpha]",
created=TODAY, updated=None):
"""A valid page in the template's OKF shape. `updated` is the date of
the last edit, written as `generated.at` (an offset datetime)."""
updated = updated or created
return (
f"---\ntype: {ptype}\ncreated: {created}\nupdated: {updated}\n"
f"---\ntype: {ptype}\ntitle: Test Page\ncreated: {created}\n"
f"generated: {{ by: claude-code/test, at: {updated}T00:00:00Z }}\n"
f"description: {description}\ntags: {tags}\nsources: []\n{extra_fm}---\n\n{body}"
)

Expand Down
Loading
Loading