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:
parent
7b59d82cdb
commit
9b9d823a67
5 changed files with 317 additions and 0 deletions
|
|
@ -796,6 +796,13 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
|||
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
|
||||
// upstream proxies (e.g. the dev-shell pod's Caddy in front of
|
||||
// code-server) to gate routes on root-admin status. Handled before
|
||||
|
|
|
|||
28
zddc/internal/handler/schemahandler.go
Normal file
28
zddc/internal/handler/schemahandler.go
Normal 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())
|
||||
}
|
||||
20
zddc/internal/zddc/schema.go
Normal file
20
zddc/internal/zddc/schema.go
Normal 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
|
||||
}
|
||||
59
zddc/internal/zddc/schema_test.go
Normal file
59
zddc/internal/zddc/schema_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
zddc/internal/zddc/zddc.schema.json
Normal file
203
zddc/internal/zddc/zddc.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue