-
Notifications
You must be signed in to change notification settings - Fork 0
feat(contract): validate canonical frontmatter FIELDS, not just artifact shape #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| #!/usr/bin/env node | ||
| // canonical-field-contract — validate the frontmatter FIELD CONTRACT of every | ||
| // artifact under canonical/. | ||
| // | ||
| // Why this exists. Patterning the SHAPE of an artifact without TYPING its field | ||
| // VALUES only relocates the drift. Measured before this file existed: | ||
| // | ||
| // status: 4 distinct values across 14 declarers; THREE were malformed — | ||
| // two were the enum DEFINITION pasted into the value slot | ||
| // ("DRAFT|PENDING|CERTIFIED|...", "active | experimental | deprecated") | ||
| // and one was foreign vocabulary ("backlog"). | ||
| // canon_uri: 9 undeclared top-level namespaces, inconsistent depth | ||
| // (gov vs gov/governance vs gov/authority/*), trailing-slash drift | ||
| // (rel vs rel/), and TWO competing addressing schemes — | ||
| // service-based core/services/* alongside class-based skills/*. | ||
| // | ||
| // Both escaped because nothing validated them. classification: and runtimes: | ||
| // did NOT escape — they are already clean controlled vocabularies. The | ||
| // difference is enforcement, not intent. | ||
| // | ||
| // SCOPE BOUNDARY: this is a work engine, not an authority. Data contracts are | ||
| // chartered to ChittySchema. This file encodes the CURRENT OBSERVED contract so | ||
| // it can be attacked and corrected; it does not own the contract. Where the | ||
| // observed corpus and this file disagree, that disagreement is the finding. | ||
| // | ||
| // Exit codes — a caller must be able to tell these apart: | ||
| // 0 every artifact parsed AND satisfied the contract | ||
| // 1 contract violations found | ||
| // 2 sweep INCOMPLETE — something intended to be checked could not be | ||
| // (unreadable file, unparseable frontmatter). NOT the same as "clean". | ||
| // 3 the runner itself crashed | ||
| // 64 usage error | ||
| // | ||
| // Usage: node scripts/canonical-field-contract.mjs [canonical-dir] [--json] | ||
|
|
||
| import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs' | ||
| import { join, relative } from 'node:path' | ||
|
|
||
| const ROOT = process.argv.find((a, i) => i >= 2 && !a.startsWith('--')) ?? 'canonical' | ||
| const AS_JSON = process.argv.includes('--json') | ||
|
|
||
| if (!existsSync(ROOT)) { | ||
| console.error(`usage: canonical-field-contract.mjs [canonical-dir] [--json]\n no such directory: ${ROOT}`) | ||
| process.exit(64) | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // THE CONTRACT (v0 — observed, not decreed) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| // Present on 100% of the 60 canonical artifacts measured 2026-08-12. | ||
| const REQUIRED = ['name', 'description', 'kind', 'classification', 'plugin', 'runtimes'] | ||
|
|
||
| // `kind` is the sub-kind discriminator. mcp-server/mcp are the SAME sub-kind | ||
| // under two names — recorded as drift rather than silently normalised, because | ||
| // which one is canonical is ChittySchema's call, not this file's. | ||
| const KIND = new Set(['skill', 'agent', 'tool', 'mcp', 'mcp-server', 'command']) | ||
| const KIND_ALIASES = new Map([['mcp-server', 'mcp']]) | ||
|
|
||
| // Observed clean vocabulary — these two never drifted. | ||
| const RUNTIMES = new Set([ | ||
| 'claude-code', 'codex', 'openclaw', 'claude-skills', 'chatgpt-apps', 'orchestrator-kv', | ||
| ]) | ||
|
|
||
| // status DID drift. A value that contains a separator is the enum definition | ||
| // pasted into the value slot — the single highest-signal defect in the corpus. | ||
| const STATUS = new Set(['draft', 'active', 'experimental', 'deprecated', 'pending', 'certified', 'canonical', 'archived']) | ||
|
|
||
| // canon_uri: two addressing schemes coexist. Both are recorded; the conflict is | ||
| // reported rather than resolved here. | ||
| const CANON_NS = new Set([ | ||
| 'core', 'gov', 'docs', 'rel', 'legal', 'skills', 'commands', 'capability', 'integration', | ||
| ]) | ||
| const CANON_RE = /^chittycanon:\/\/([a-z0-9_-]+)(\/[a-z0-9/_#-]*)?$/ | ||
|
|
||
| const findings = [] | ||
| const unproven = [] | ||
| const seenNames = new Map() | ||
| let checked = 0 | ||
|
|
||
| const add = (file, code, msg) => findings.push({ file, code, msg }) | ||
|
|
||
| function frontmatter(text) { | ||
| if (!text.startsWith('---')) return null | ||
| const end = text.indexOf('\n---', 3) | ||
| if (end === -1) return null | ||
| return text.slice(4, end) | ||
| } | ||
|
|
||
| // Minimal, deliberately strict: scalars and `- ` list items only. Anything it | ||
| // cannot represent is reported as UNPROVEN rather than guessed at. | ||
| function parseFields(block) { | ||
| const out = {} | ||
| let key = null | ||
| for (const raw of block.split('\n')) { | ||
| if (!raw.trim() || raw.trimStart().startsWith('#')) continue | ||
| const top = raw.match(/^([a-z0-9_]+):\s*(.*)$/i) | ||
| if (top) { | ||
| key = top[1] | ||
| const v = top[2].trim() | ||
| out[key] = v === '' || v === '|' || v === '>' ? [] : v | ||
| continue | ||
| } | ||
| const item = raw.match(/^\s+-\s+(.*)$/) | ||
| if (item && key) { | ||
| if (!Array.isArray(out[key])) out[key] = [] | ||
| out[key].push(item[1].trim()) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
|
Comment on lines
+92
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect frontmatter value styles for the validated fields.
set -euo pipefail
fd -e md . canonical | while IFS= read -r f; do
awk 'NR==1 && $0!="---" {exit} NR>1 && $0=="---" {exit} {print FILENAME": "$0}' "$f"
done | rg -n '(runtimes|classification|kind|status|canon_uri|canonical_uri):' | head -80Repository: chittyos/chittymarket Length of output: 5706 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- target script ---'
sed -n '80,185p' scripts/canonical-field-contract.mjs
printf '%s\n' '--- representative frontmatter ---'
python3 - <<'PY'
from pathlib import Path
fields = {"runtimes", "classification", "kind", "status"}
shown = 0
for path in Path("canonical").rglob("*.md"):
text = path.read_text()
if not text.startswith("---\n"):
continue
end = text.find("\n---", 4)
if end < 0:
continue
lines = text[4:end].splitlines()
selected = [line for line in lines if line.split(":", 1)[0].strip() in fields]
if selected:
print(path)
print("\n".join(selected))
shown += 1
if shown >= 12:
break
PY
printf '%s\n' '--- documented parser assumptions ---'
rg -n -C 3 'runtimes:|classification:|UNPROVEN|parseFields|Array\.isArray' \
docs scripts/canonical-field-contract.mjs | head -160Repository: chittyos/chittymarket Length of output: 11236 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
from collections import Counter, defaultdict
def frontmatter(text):
if not text.startswith("---"):
return None
end = text.find("\n---", 3)
return None if end == -1 else text[4:end]
def parse_fields(block):
out = {}
key = None
for raw in block.split("\n"):
if not raw.strip() or raw.lstrip().startswith("#"):
continue
top = __import__("re").match(r"^([a-z0-9_]+):\s*(.*)$", raw, __import__("re").I)
if top:
key = top.group(1)
v = top.group(2).strip()
out[key] = [] if v in ("", "|", ">") else v
continue
item = __import__("re").match(r"^\s+-\s+(.*)$", raw)
if item and key:
if not isinstance(out[key], list):
out[key] = []
out[key].append(item.group(1).strip())
return out
raw_shapes = Counter()
parsed_shapes = Counter()
examples = defaultdict(list)
quoted = []
for path in Path("canonical").rglob("*.md"):
block = frontmatter(path.read_text())
if block is None:
continue
for field in ("runtimes", "classification", "kind", "status"):
lines = block.splitlines()
for i, line in enumerate(lines):
if line.lower().startswith(field + ":"):
value = line.split(":", 1)[1].strip()
if value in ("", "|", ">"):
shape = "empty/block-style"
elif value.startswith("[") and value.endswith("]"):
shape = "inline-flow-sequence"
elif value[:1] in ("'", '"'):
shape = "quoted-scalar"
else:
shape = "plain-scalar"
raw_shapes[field, shape] += 1
examples[field, shape].append((str(path), line))
break
parsed = parse_fields(block)
for field in ("runtimes", "classification", "kind", "status"):
if field in parsed:
parsed_shapes[field, "array" if isinstance(parsed[field], list) else "string"] += 1
print("RAW SHAPES")
for k, n in sorted(raw_shapes.items()):
print(f"{k[0]:15} {k[1]:24} {n}")
print("\nPARSED SHAPES")
for k, n in sorted(parsed_shapes.items()):
print(f"{k[0]:15} {k[1]:10} {n}")
print("\nEXAMPLES")
for (field, shape), rows in sorted(examples.items()):
print(f"{field} / {shape}:")
for row in rows[:3]:
print(" ", row[0], "=>", row[1])
PY
printf '%s\n' '--- reporting tail ---'
sed -n '185,212p' scripts/canonical-field-contract.mjsRepository: chittyos/chittymarket Length of output: 2430 Make The parser treats the three current 🤖 Prompt for AI Agents |
||
|
|
||
| function walk(dir) { | ||
| for (const entry of readdirSync(dir)) { | ||
| const p = join(dir, entry) | ||
| const st = statSync(p) | ||
| if (st.isDirectory()) { walk(p); continue } | ||
| if (!entry.endsWith('.md')) continue | ||
| check(p) | ||
| } | ||
| } | ||
|
|
||
| function check(path) { | ||
| const rel = relative('.', path) | ||
| let text | ||
| try { | ||
| text = readFileSync(path, 'utf8') | ||
| } catch (e) { | ||
| unproven.push({ file: rel, code: 'UNREADABLE', msg: String(e.message) }) | ||
| return | ||
| } | ||
|
|
||
| const block = frontmatter(text) | ||
| if (block === null) { | ||
| // No frontmatter at all: cannot be checked, and MUST NOT count as clean. | ||
| unproven.push({ file: rel, code: 'NO_FRONTMATTER', msg: 'no frontmatter block — contract unprovable' }) | ||
| return | ||
| } | ||
|
Comment on lines
+133
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Every Add an explicit non-artifact exclusion so exclusions stay visible rather than implicit. 🐛 Proposed exclusion+// Documentation files under canonical/ are not artifacts. Excluded explicitly
+// so the exclusion is auditable rather than implicit.
+const NON_ARTIFACTS = new Set(['README.md'])
+
function walk(dir) {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry)
const st = statSync(p)
if (st.isDirectory()) { walk(p); continue }
if (!entry.endsWith('.md')) continue
+ if (NON_ARTIFACTS.has(entry)) continue
check(p)
}
}🤖 Prompt for AI Agents |
||
|
|
||
| checked++ | ||
| const f = parseFields(block) | ||
|
|
||
| for (const r of REQUIRED) { | ||
| if (!(r in f)) add(rel, 'MISSING_REQUIRED', `required field '${r}' absent`) | ||
| } | ||
|
|
||
| // name uniqueness across the whole corpus — a duplicate canonical name is an | ||
| // identity collision, the defect class chittyanima exists to resolve. | ||
| if (typeof f.name === 'string' && f.name) { | ||
| const prev = seenNames.get(f.name) | ||
| if (prev) add(rel, 'DUPLICATE_NAME', `name '${f.name}' already declared by ${prev}`) | ||
| else seenNames.set(f.name, rel) | ||
| } | ||
|
|
||
| if (typeof f.kind === 'string' && f.kind) { | ||
| if (!KIND.has(f.kind)) add(rel, 'KIND_UNKNOWN', `kind '${f.kind}' not in the observed set`) | ||
| else if (KIND_ALIASES.has(f.kind)) { | ||
| add(rel, 'KIND_ALIAS_DRIFT', `kind '${f.kind}' is an alias of '${KIND_ALIASES.get(f.kind)}' — one sub-kind, two names`) | ||
| } | ||
| } | ||
|
|
||
| if (typeof f.status === 'string' && f.status) { | ||
| const v = f.status.trim() | ||
| if (/[|,/]/.test(v)) { | ||
| add(rel, 'ENUM_DEFINITION_AS_VALUE', `status '${v}' is an enum DEFINITION in the value slot, not an instance`) | ||
| } else if (!STATUS.has(v.toLowerCase())) { | ||
| add(rel, 'STATUS_UNKNOWN', `status '${v}' outside the observed vocabulary`) | ||
| } | ||
| } | ||
|
|
||
| for (const rt of Array.isArray(f.runtimes) ? f.runtimes : []) { | ||
| if (!RUNTIMES.has(rt)) add(rel, 'RUNTIME_UNKNOWN', `runtime '${rt}' not a known substrate`) | ||
| } | ||
|
|
||
| const uri = typeof f.canon_uri === 'string' ? f.canon_uri | ||
| : typeof f.canonical_uri === 'string' ? f.canonical_uri : null | ||
| if (uri) { | ||
| if ('canon_uri' in f && 'canonical_uri' in f) { | ||
| add(rel, 'URI_KEY_DRIFT', 'both canon_uri and canonical_uri declared') | ||
| } | ||
| const m = uri.match(CANON_RE) | ||
| if (!m) add(rel, 'URI_MALFORMED', `canon uri '${uri}' does not match chittycanon://<ns>/<path>`) | ||
| else { | ||
| if (!CANON_NS.has(m[1])) add(rel, 'URI_NAMESPACE_UNKNOWN', `namespace '${m[1]}' undeclared`) | ||
| if (uri.endsWith('/')) add(rel, 'URI_TRAILING_SLASH', `canon uri '${uri}' has a trailing slash`) | ||
| if (!m[2] || m[2] === '/') add(rel, 'URI_DEPTH', `canon uri '${uri}' is namespace-only — no addressable path`) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| walk(ROOT) | ||
| } catch (e) { | ||
| console.error(`crashed: ${e.stack}`) | ||
| process.exit(3) | ||
| } | ||
|
|
||
| if (AS_JSON) { | ||
| console.log(JSON.stringify({ checked, findings, unproven }, null, 2)) | ||
| } else { | ||
| console.log(`canonical field contract — ${checked} artifact(s) checked under ${ROOT}`) | ||
| for (const f of findings) console.log(` FAIL ${f.file} [${f.code}] ${f.msg}`) | ||
| for (const u of unproven) console.log(` UNPROVEN ${u.file} [${u.code}] ${u.msg}`) | ||
| console.log(`\n ${findings.length} violation(s), ${unproven.length} UNPROVEN`) | ||
| if (unproven.length) console.log(' sweep INCOMPLETE — exit 2. Nothing here may be read as "contract clean".') | ||
| } | ||
|
|
||
| if (unproven.length) process.exit(2) | ||
| process.exit(findings.length ? 1 : 0) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate that the argument is a directory, not only that the path exists.
If a caller passes a file path,
existsSyncpasses andreaddirSyncthrowsENOTDIR. The runner then reports exit 3 (runner crash) instead of exit 64 (usage error). Callers that distinguish exit codes get the wrong signal.🐛 Proposed usage check
📝 Committable suggestion
🤖 Prompt for AI Agents