Skip to content
Open
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
209 changes: 209 additions & 0 deletions scripts/canonical-field-contract.mjs
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)
}
Comment on lines +42 to +45

Copy link
Copy Markdown

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, existsSync passes and readdirSync throws ENOTDIR. 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
-if (!existsSync(ROOT)) {
+if (!existsSync(ROOT) || !statSync(ROOT).isDirectory()) {
   console.error(`usage: canonical-field-contract.mjs [canonical-dir] [--json]\n  no such directory: ${ROOT}`)
   process.exit(64)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!existsSync(ROOT)) {
console.error(`usage: canonical-field-contract.mjs [canonical-dir] [--json]\n no such directory: ${ROOT}`)
process.exit(64)
}
if (!existsSync(ROOT) || !statSync(ROOT).isDirectory()) {
console.error(`usage: canonical-field-contract.mjs [canonical-dir] [--json]\n no such directory: ${ROOT}`)
process.exit(64)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/canonical-field-contract.mjs` around lines 42 - 45, Update the ROOT
validation in canonical-field-contract.mjs to require an existing directory,
using a directory-specific filesystem check rather than existsSync alone.
Preserve the existing usage message and exit 64 behavior for both missing paths
and file paths, preventing readdirSync from receiving a non-directory path.


// ---------------------------------------------------------------------------
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 -80

Repository: 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 -160

Repository: 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.mjs

Repository: chittyos/chittymarket

Length of output: 2430


Make parseFields handle documented YAML values

The parser treats the three current runtimes: [] fields as strings, so line 171 skips runtime validation. Any non-empty runtimes: [...] value would be skipped in the same way. The parser also cannot represent documented classification: [...] values. If quoted scalars are valid, quotes cause false KIND_UNKNOWN and STATUS_UNKNOWN findings. Parse flow sequences and surrounding quotes, or report unsupported shapes as UNPROVEN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/canonical-field-contract.mjs` around lines 92 - 111, Update
parseFields to parse documented YAML flow sequences such as runtimes: [] and
classification: [...] into arrays, including non-empty values, and strip
surrounding quotes from scalar values before validation. Ensure unsupported
value shapes are represented as UNPROVEN rather than silently skipped, while
preserving existing block-list parsing behavior.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

canonical/README.md makes the sweep permanently exit 2, which blocks any trigger wiring.

Every .md file under the root is treated as an artifact. canonical/README.md is documentation, not an artifact, so it always lands in unproven and forces exit 2. The PR states the checker is not yet wired to a trigger; with this behavior it can never reach exit 0. scripts/lint-plugins.sh (lines 259-280) already skips README.md for the same reason.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/canonical-field-contract.mjs` around lines 133 - 138, Update the
file-sweep logic around the frontmatter check to explicitly exclude the root
documentation file canonical/README.md from artifact validation before it can be
added to unproven. Keep the exclusion visible and narrowly scoped, while
continuing to validate all other Markdown files normally.


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)
Loading