feat(zddc): .zddc JSON Schema (machine grammar) with structure/option tiers

Authoritative machine form of the GRAMMAR.md grammar: zddc.schema.json
(draft 2020-12) describes every .zddc key with type, enum, description, and
x-zddc-tier — "structure" (the project shape an end user shouldn't change:
paths, worm, *_tool, views, available_tools, auto_own*, party_source, history*,
records, acl, created_by) vs "option" (the blanks an operator fills: roles
members, field_codes, convert, display, admins, title, planned dates). This is
the contract a future .zddc form view uses to render option fields editable and
structure fields read-only.

Embedded (ZddcSchemaBytes) and served at GET /.api/zddc-schema for the client.
Test locks the tier classification.

Scope note: the schema uses $ref (recursive paths:) + patternProperties, which
the in-tree internal/jsonschema validator doesn't support — so it drives the
form/client now; wiring it as the SERVER validator (replacing validate.go's
hand-rolled checks) needs a $ref-capable validator and is a separate decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-06-05 14:54:07 -05:00
parent 7b59d82cdb
commit 9b9d823a67
5 changed files with 317 additions and 0 deletions

View file

@ -796,6 +796,13 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
return return
} }
// The .zddc JSON Schema (machine grammar) — drives the .zddc form view +
// client validation. Static, read-only, no auth.
if urlPath == handler.ZddcSchemaPath {
handler.ServeZddcSchema(w, r)
return
}
// Auth check endpoints — machine-only forward_auth targets used by // Auth check endpoints — machine-only forward_auth targets used by
// upstream proxies (e.g. the dev-shell pod's Caddy in front of // upstream proxies (e.g. the dev-shell pod's Caddy in front of
// code-server) to gate routes on root-admin status. Handled before // code-server) to gate routes on root-admin status. Handled before

View file

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// ZddcSchemaPath is the JSON endpoint serving the .zddc JSON Schema (the machine
// grammar). The browse client + the .zddc form view fetch it to drive editing
// (per-property x-zddc-tier marks structure vs option) and validation.
const ZddcSchemaPath = "/.api/zddc-schema"
// ServeZddcSchema returns the embedded .zddc JSON Schema. Read-only, no auth —
// it documents the policy grammar and leaks nothing. GET/HEAD only.
func ServeZddcSchema(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/schema+json; charset=utf-8")
w.Header().Set("Cache-Control", "max-age=300")
if r.Method == http.MethodHead {
return
}
_, _ = w.Write(zddc.ZddcSchemaBytes())
}

View file

@ -0,0 +1,20 @@
package zddc
import _ "embed"
// schemaBytes is the embedded .zddc JSON Schema (draft 2020-12). The
// authoritative machine grammar: it drives the form view (per-property
// x-zddc-tier: structure|option decides editable vs read-only) and client
// validation. Server-side structural validation still lives in validate.go —
// this schema uses $ref + patternProperties, which the in-tree
// internal/jsonschema validator does not yet support.
//
//go:embed zddc.schema.json
var schemaBytes []byte
// ZddcSchemaBytes returns a copy of the embedded .zddc JSON Schema.
func ZddcSchemaBytes() []byte {
out := make([]byte, len(schemaBytes))
copy(out, schemaBytes)
return out
}

View file

@ -0,0 +1,59 @@
package zddc
import (
"encoding/json"
"testing"
)
// The embedded .zddc schema must be valid JSON, cover the grammar's keys, and
// tag every property structure|option — the contract the form view relies on.
func TestZddcSchemaTiers(t *testing.T) {
var doc struct {
Properties map[string]struct {
Tier string `json:"x-zddc-tier"`
} `json:"properties"`
}
if err := json.Unmarshal(ZddcSchemaBytes(), &doc); err != nil {
t.Fatalf("schema is not valid JSON: %v", err)
}
if len(doc.Properties) == 0 {
t.Fatal("schema has no properties")
}
// Every property carries a valid tier.
for name, p := range doc.Properties {
if p.Tier != "structure" && p.Tier != "option" {
t.Errorf("property %q has x-zddc-tier=%q, want structure|option", name, p.Tier)
}
}
// Spot-check the classification the form depends on.
wantTier := map[string]string{
"roles": "option", // who's in a group — the headline customisation
"field_codes": "option",
"convert": "option",
"display": "option",
"admins": "option",
"title": "option",
"paths": "structure", // the project shape
"worm": "structure",
"party_source": "structure",
"default_tool": "structure",
"available_tools": "structure",
"auto_own": "structure",
"acl": "structure",
"records": "structure",
"views": "structure",
"created_by": "structure",
}
for name, want := range wantTier {
p, ok := doc.Properties[name]
if !ok {
t.Errorf("schema missing expected key %q", name)
continue
}
if p.Tier != want {
t.Errorf("key %q tier=%q, want %q", name, p.Tier, want)
}
}
}

View file

@ -0,0 +1,203 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://zddc.varasys.io/schema/zddc.schema.json",
"title": ".zddc policy document",
"description": "Machine schema for the .zddc grammar (see GRAMMAR.md). Each property carries x-zddc-tier: 'structure' (the project shape an end user should not change — paths, WORM, tools, behaviors) or 'option' (the blanks an operator fills — role members, field-code vocabularies, names, labels). A form view renders option fields editable and structure fields read-only. NOTE: not all keys are valid at every level; the cascade + the per-location form decide relevance. Server-side validation still lives in validate.go (this draft-2020-12 schema uses $ref + patternProperties, which the in-tree validator does not yet support); the schema drives the form + client today.",
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"description": "Human title for this directory.",
"x-zddc-tier": "option"
},
"created_by": {
"type": "string",
"description": "Email of the user who created this folder. Set by the server; audit only.",
"x-zddc-tier": "structure"
},
"admins": {
"type": "array",
"items": { "type": "string" },
"description": "Principals (emails, globs, or role names) who administer this subtree. Root admins are super-admins; deeper entries are subtree admins. Elevation-gated full bypass over scope.",
"x-zddc-tier": "option"
},
"roles": {
"type": "object",
"description": "Named principal groups referenced by acl/worm/admins. Membership UNIONS across the cascade. The operator fills the members.",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"members": {
"type": "array",
"items": { "type": "string" },
"description": "Email patterns (alice@x, *@acme.com, *) in this role."
},
"reset": {
"type": "boolean",
"description": "Stop the membership union here: ancestor definitions above this level are excluded."
}
}
},
"x-zddc-tier": "option"
},
"acl": {
"type": "object",
"description": "Access control for this level. permissions maps a principal (email/glob/role) to a verb string from r w c d a (empty string = explicit deny). inherit:false clamps the ACL level-walk so ancestor levels' grants do not apply.",
"additionalProperties": false,
"properties": {
"inherit": { "type": "boolean", "description": "false = this level's ACL does not inherit ancestor levels." },
"permissions": {
"type": "object",
"patternProperties": {
"^.+$": { "type": "string", "pattern": "^[rwcda]*$", "description": "Verb subset of r w c d a; empty = explicit deny." }
},
"additionalProperties": false
}
},
"x-zddc-tier": "structure"
},
"worm": {
"type": "array",
"items": { "type": "string" },
"description": "WORM zone: write/delete/admin stripped for all; create survives only for the listed principals; admins bypass. Unions across the cascade.",
"x-zddc-tier": "structure"
},
"inherit": {
"type": "boolean",
"description": "false = stop the cascade here; everything below (ancestors + embedded defaults) is ignored. Makes a subtree a self-contained island.",
"x-zddc-tier": "structure"
},
"default_tool": {
"type": "string",
"enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"],
"description": "Tool served at <dir> (no trailing slash). Sugar for views.dir.tool.",
"x-zddc-tier": "structure"
},
"dir_tool": {
"type": "string",
"enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"],
"description": "Tool served at <dir>/ (trailing slash). Sugar for views.dir_slash.tool; defaults to browse.",
"x-zddc-tier": "structure"
},
"views": {
"type": "object",
"description": "Per-URL-shape tool + supporting-config mapping.",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"tool": { "type": "string", "enum": ["archive", "transmittal", "classifier", "browse", "tables", "landing", "form"] },
"config": { "type": "string", "description": "Supporting-file name resolved under .zddc.d/ (no slashes)." }
}
},
"x-zddc-tier": "structure"
},
"available_tools": {
"type": "array",
"items": { "type": "string" },
"description": "Tools the apps subsystem may auto-serve here and below. Concat-dedupe union across the cascade.",
"x-zddc-tier": "structure"
},
"auto_own": {
"type": "boolean",
"description": "mkdir here writes a creator-owned .zddc (creator: rwcda).",
"x-zddc-tier": "structure"
},
"auto_own_fenced": {
"type": "boolean",
"description": "The auto-own .zddc is written with acl.inherit:false (private to its creator).",
"x-zddc-tier": "structure"
},
"auto_own_roles": {
"type": "array",
"items": { "type": "string" },
"description": "Roles also granted rwcda in the auto-own .zddc, alongside the creator.",
"x-zddc-tier": "structure"
},
"virtual": {
"type": "boolean",
"description": "Never materialise on disk; treat requests as virtual routes.",
"x-zddc-tier": "structure"
},
"drop_target": {
"type": "boolean",
"description": "This directory accepts drag-drop uploads (browse drop-zone). Leaf-only.",
"x-zddc-tier": "structure"
},
"party_source": {
"type": "string",
"description": "A new <party>/ here requires registration in <source>/<party>.yaml (e.g. 'ssr'). Leaf-only.",
"x-zddc-tier": "structure"
},
"history": {
"type": "boolean",
"description": "Snapshot text (markdown) edits to .history/ in this subtree with a server-stamped audit line.",
"x-zddc-tier": "structure"
},
"history_globs": {
"type": "array",
"items": { "type": "string" },
"description": "Which basenames get edit history (default [\"*.md\"]).",
"x-zddc-tier": "structure"
},
"convert": {
"type": "object",
"description": "Template variables for MD→{docx,html,pdf} conversion. Cascades leaf→root, per-key latest wins.",
"additionalProperties": false,
"properties": {
"client": { "type": "string" },
"project": { "type": "string" },
"contractor": { "type": "string" },
"project_number": { "type": "string" }
},
"x-zddc-tier": "option"
},
"field_codes": {
"type": "object",
"description": "Vocabularies for tracking-number / record field components. Map-merged per code across the cascade.",
"additionalProperties": { "type": "object" },
"x-zddc-tier": "option"
},
"records": {
"type": "object",
"description": "Per-record-type rules keyed by filename pattern (filename_format, field_defaults, locked, row_field, row_scope_fields, folder_fields).",
"additionalProperties": { "type": "object" },
"x-zddc-tier": "structure"
},
"display": {
"type": "object",
"description": "Human labels for child entries (on-disk name → label). Leaf-only.",
"additionalProperties": { "type": "string" },
"x-zddc-tier": "option"
},
"tables": {
"type": "object",
"description": "Legacy directory-of-YAML table views (stem → spec path).",
"additionalProperties": { "type": "string" },
"x-zddc-tier": "structure"
},
"received_path": {
"type": "string",
"description": "Links a workflow folder back to its canonical submittal in received/. Set by Plan Review.",
"x-zddc-tier": "structure"
},
"planned_review_date": {
"type": "string",
"description": "Doc-controller's committed review-completion date (YYYY-MM-DD), on the canonical submittal.",
"x-zddc-tier": "option"
},
"planned_response_date": {
"type": "string",
"description": "Doc-controller's committed response-issuance date (YYYY-MM-DD), on the canonical submittal.",
"x-zddc-tier": "option"
},
"paths": {
"type": "object",
"description": "Virtual sub-directory rules. Each key is a single path segment (literal or '*'); the value is a nested .zddc applied at the matching child directory. Recursive.",
"additionalProperties": { "$ref": "#" },
"x-zddc-tier": "structure"
}
}
}