diff --git a/browse/css/preview-yaml.css b/browse/css/preview-yaml.css index 082edb1..281dfcb 100644 --- a/browse/css/preview-yaml.css +++ b/browse/css/preview-yaml.css @@ -29,6 +29,16 @@ border-color: var(--primary); color: var(--primary); } +/* The ".zddc schema" badge is clickable — it opens the full JSON Schema. */ +.yaml-shell__schema--link { + cursor: pointer; +} +.yaml-shell__schema--link:hover, +.yaml-shell__schema--link:focus-visible { + background: var(--primary); + color: var(--bg); + outline: none; +} /* CodeMirror has to fill the grid cell. The vendored CSS sets `height: 300px` by default — we override to 100% so it grows with diff --git a/browse/js/preview-yaml.js b/browse/js/preview-yaml.js index 47aad2e..1e8ac17 100644 --- a/browse/js/preview-yaml.js +++ b/browse/js/preview-yaml.js @@ -114,7 +114,18 @@ views: 'viewmap', convert: 'convert', created_by: 'string', - inherit: 'bool' + inherit: 'bool', + // Keys the Go decoder (zddc/internal/zddc/file.go) accepts that the + // lint was missing — flagged valid configs as "unknown key". + party_source: 'string', + history: 'bool', + history_globs: 'string[]', + records: 'object', + auto_own_roles: 'string[]', + received_path: 'string', + planned_response_date: 'string', + planned_review_date: 'string', + field_codes: 'object' }; var ACL_KEYS = { inherit: 'bool', permissions: 'stringmap', @@ -279,6 +290,12 @@ if (t !== 'object') { addTypeErr(path, kind, t, issues); return; } walkObject(val, CONVERT_KEYS, path, issues); return; + case 'object': + // Free-form map (records, field_codes) — the server accepts any + // nested shape, so we only check it's a mapping, not its keys. + if (t === 'null') return; + if (t !== 'object') { addTypeErr(path, kind, t, issues); return; } + return; } } @@ -433,9 +450,21 @@ var schemaTag = document.createElement('span'); schemaTag.className = 'md-shell__source yaml-shell__schema'; if (isZddcFile(node.name)) { - schemaTag.textContent = '.zddc schema'; + schemaTag.textContent = '.zddc schema ↗'; schemaTag.title = 'Linted against the .zddc cascade schema ' - + '(unknown keys, bad enums, and wrong types are flagged).'; + + '(unknown keys, bad enums, and wrong types are flagged). ' + + 'Click to view the full JSON Schema.'; + // Clickable → opens the canonical machine grammar the lint mirrors. + schemaTag.classList.add('yaml-shell__schema--link'); + schemaTag.setAttribute('role', 'link'); + schemaTag.setAttribute('tabindex', '0'); + var openSchema = function () { + window.open('/.api/zddc-schema', '_blank', 'noopener'); + }; + schemaTag.addEventListener('click', openSchema); + schemaTag.addEventListener('keydown', function (ev) { + if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openSchema(); } + }); } else { schemaTag.textContent = 'YAML'; } diff --git a/zddc/internal/handler/zddcfile.go b/zddc/internal/handler/zddcfile.go index 07a4f82..998c887 100644 --- a/zddc/internal/handler/zddcfile.go +++ b/zddc/internal/handler/zddcfile.go @@ -143,9 +143,10 @@ func ServeZddcFile(cfg config.Config, w http.ResponseWriter, r *http.Request) { // renderVirtualZddc produces a YAML body for a directory that has no // .zddc on disk. The body is the cascade's leaf-level ZddcFile — -// i.e. what internal/zddc/defaults/'s paths: tree declares for this exact -// directory, plus any contributions the walker threaded through. The -// goal is to expose the embedded defaults' source of truth: a new +// i.e. what the built-in defaults bundle (exportable/overridable as a +// root .zddc.zip via `zddc-server show-defaults`) declares for this +// exact directory, plus any contributions the walker threaded through. +// The goal is to expose the baseline's source of truth: a new // user opening the virtual .zddc here sees, in the same yaml shape // they would write themselves, what behavior is currently declared // at this level. A header comment names the source and points at @@ -163,13 +164,16 @@ func renderVirtualZddc(chain zddc.PolicyChain) (string, error) { var b strings.Builder b.WriteString("# Virtual .zddc — no file on disk at this directory.\n") - b.WriteString("# The content below is what the embedded defaults\n") - b.WriteString("# (internal/zddc/defaults/'s paths: tree) declare for this\n") - b.WriteString("# exact path. Edit and save through the YAML editor in\n") - b.WriteString("# browse to materialise a real .zddc here carrying your\n") - b.WriteString("# changes; the bytes you save become the override\n") - b.WriteString("# verbatim (no merge, no synthesis — .zddc files drive\n") - b.WriteString("# policy and are the single source of truth).\n") + b.WriteString("# The content below is what the policy baseline declares\n") + b.WriteString("# for this exact path: the built-in defaults bundle — the\n") + b.WriteString("# same one you can export, and override, as a root\n") + b.WriteString("# .zddc.zip (`zddc-server show-defaults`) — with any\n") + b.WriteString("# on-disk ancestor .zddc overrides already threaded in.\n") + b.WriteString("# Edit and save through the YAML editor in browse to\n") + b.WriteString("# materialise a real .zddc here carrying your changes;\n") + b.WriteString("# the bytes you save become the override verbatim (no\n") + b.WriteString("# merge, no synthesis — .zddc files drive policy and are\n") + b.WriteString("# the single source of truth).\n") b.WriteString("#\n") b.WriteString("# For the COMPOSED effective config across the whole\n") b.WriteString("# cascade (all ancestors merged), query:\n")