From 05cb792d90957e1ea155f442485f54e5cdf5158e Mon Sep 17 00:00:00 2001 From: cem Date: Fri, 26 Jun 2026 13:24:22 +0000 Subject: [PATCH 1/2] refactor: extract shared utilities from duplicated patterns - lib/paths.mjs: shared root directory resolution - lib/meta.mjs: single source of truth from ontology/graphite.yaml - lib/envelope.mjs: envelope creation and validation from schema - lib/boundary.mjs: non-adoption boundary checks from meta - lib/index.mjs: barrel export - Refactored scripts/check.mjs to use shared lib (meta, boundary, envelope) - Added scripts/validate-consistency.mjs to detect cross-file drift - Fixed content drift: README and boundary spec now aligned with YAML - Added yaml dependency for ontology parsing - Added .gitignore for node_modules Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 1 + README.md | 2 +- lib/boundary.mjs | 26 ++++++++++ lib/envelope.mjs | 71 ++++++++++++++++++++++++++++ lib/index.mjs | 4 ++ lib/meta.mjs | 37 +++++++++++++++ lib/paths.mjs | 4 ++ package-lock.json | 33 +++++++++++++ package.json | 6 ++- scripts/check.mjs | 36 +++++++++++++- scripts/validate-consistency.mjs | 81 ++++++++++++++++++++++++++++++++ spec/non_adoption_boundary.md | 3 ++ 12 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 lib/boundary.mjs create mode 100644 lib/envelope.mjs create mode 100644 lib/index.mjs create mode 100644 lib/meta.mjs create mode 100644 lib/paths.mjs create mode 100644 package-lock.json create mode 100644 scripts/validate-consistency.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index 6c489b8..6507f8d 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Until `1.0`: - do not claim the foundation runs on Graphite, - do not restructure the foundation around Graphite, - do not treat Graphite as stable API, -- do not use Graphite informally in future sessions as if adopted. +- do not invoke Graphite informally in future sessions as if adopted. Current role: diff --git a/lib/boundary.mjs b/lib/boundary.mjs new file mode 100644 index 0000000..3f011ff --- /dev/null +++ b/lib/boundary.mjs @@ -0,0 +1,26 @@ +import { getNonAdoptionBoundary, getAdoption, getStatus } from "./meta.mjs"; + +export async function isAdopted() { + const adoption = await getAdoption(); + return adoption.foundation_wide === true; +} + +export async function getBoundaryRules() { + return getNonAdoptionBoundary(); +} + +export async function checkBoundary() { + const adopted = await isAdopted(); + if (adopted) { + return { enforced: false, reason: "foundation-wide adoption is active" }; + } + + const rules = await getBoundaryRules(); + const status = await getStatus(); + + return { + enforced: true, + status, + rules + }; +} diff --git a/lib/envelope.mjs b/lib/envelope.mjs new file mode 100644 index 0000000..65d7edd --- /dev/null +++ b/lib/envelope.mjs @@ -0,0 +1,71 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rootDir } from "./paths.mjs"; + +let _schema = null; + +async function loadSchema() { + if (_schema) return _schema; + const text = await readFile(join(rootDir, "protocol", "envelope.schema.json"), "utf8"); + _schema = JSON.parse(text); + return _schema; +} + +export async function getRequiredFields() { + const schema = await loadSchema(); + return schema.required; +} + +export async function createEnvelope({ kind, payload, gate }) { + const schema = await loadSchema(); + const protocolPattern = schema.properties.protocol.pattern; + const match = protocolPattern.match(/\^(.+)\$/); + const prefix = match ? match[1].split("/")[0] : "graphite"; + + const envelope = { + protocol: `${prefix}/0.0`, + kind, + created_at: new Date().toISOString(), + payload: payload || {} + }; + + if (gate) { + envelope.gate = gate; + } + + return envelope; +} + +export async function validateEnvelope(envelope) { + const schema = await loadSchema(); + const errors = []; + + if (typeof envelope !== "object" || envelope === null) { + return { valid: false, errors: ["envelope must be a non-null object"] }; + } + + for (const field of schema.required) { + if (!(field in envelope)) { + errors.push(`missing required field: ${field}`); + } + } + + for (const [key, rule] of Object.entries(schema.properties)) { + if (!(key in envelope)) continue; + const value = envelope[key]; + + if (rule.type === "string" && typeof value !== "string") { + errors.push(`${key} must be a string`); + } else if (rule.type === "object" && (typeof value !== "object" || value === null)) { + errors.push(`${key} must be an object`); + } + + if (rule.pattern && typeof value === "string") { + if (!new RegExp(rule.pattern).test(value)) { + errors.push(`${key} does not match pattern ${rule.pattern}`); + } + } + } + + return { valid: errors.length === 0, errors }; +} diff --git a/lib/index.mjs b/lib/index.mjs new file mode 100644 index 0000000..cfd804b --- /dev/null +++ b/lib/index.mjs @@ -0,0 +1,4 @@ +export { rootDir } from "./paths.mjs"; +export { loadMeta, getStatus, getAdoption, getNonAdoptionBoundary, getProperties, clearCache } from "./meta.mjs"; +export { createEnvelope, validateEnvelope, getRequiredFields } from "./envelope.mjs"; +export { isAdopted, getBoundaryRules, checkBoundary } from "./boundary.mjs"; diff --git a/lib/meta.mjs b/lib/meta.mjs new file mode 100644 index 0000000..ff03acb --- /dev/null +++ b/lib/meta.mjs @@ -0,0 +1,37 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { parse as parseYaml } from "yaml"; +import { rootDir } from "./paths.mjs"; + +let _cache = null; + +export async function loadMeta() { + if (_cache) return _cache; + const text = await readFile(join(rootDir, "ontology", "graphite.yaml"), "utf8"); + _cache = parseYaml(text); + return _cache; +} + +export async function getStatus() { + const meta = await loadMeta(); + return meta.status; +} + +export async function getAdoption() { + const meta = await loadMeta(); + return meta.adoption; +} + +export async function getNonAdoptionBoundary() { + const meta = await loadMeta(); + return meta.non_adoption_boundary; +} + +export async function getProperties() { + const meta = await loadMeta(); + return meta.properties; +} + +export function clearCache() { + _cache = null; +} diff --git a/lib/paths.mjs b/lib/paths.mjs new file mode 100644 index 0000000..ea09c67 --- /dev/null +++ b/lib/paths.mjs @@ -0,0 +1,4 @@ +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +export const rootDir = join(dirname(fileURLToPath(import.meta.url)), ".."); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..21efc2e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "@gsm-foundation/graphite", + "version": "0.0.0-incubating", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gsm-foundation/graphite", + "version": "0.0.0-incubating", + "dependencies": { + "yaml": "^2.8.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json index e96febb..68c8b01 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "description": "ontology-based server communications bootstrap kit", "type": "module", "scripts": { - "check": "node scripts/check.mjs" + "check": "node scripts/check.mjs", + "validate": "node scripts/validate-consistency.mjs" }, "engines": { "node": ">=18" + }, + "dependencies": { + "yaml": "^2.8.4" } } diff --git a/scripts/check.mjs b/scripts/check.mjs index a60fcc3..7fd8300 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -1,4 +1,9 @@ import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rootDir } from "../lib/paths.mjs"; +import { loadMeta } from "../lib/meta.mjs"; +import { validateEnvelope, createEnvelope } from "../lib/envelope.mjs"; +import { checkBoundary } from "../lib/boundary.mjs"; const required = [ "README.md", @@ -11,9 +16,10 @@ const required = [ ]; let failed = 0; + for (const file of required) { try { - await readFile(new URL(`../${file}`, import.meta.url), "utf8"); + await readFile(join(rootDir, file), "utf8"); console.log(`ok ${file}`); } catch { failed += 1; @@ -21,6 +27,32 @@ for (const file of required) { } } +const meta = await loadMeta(); +if (meta.name && meta.status) { + console.log(`ok meta: ${meta.name} [${meta.status}]`); +} else { + failed += 1; + console.log("fail meta: missing name or status in ontology"); +} + +const boundary = await checkBoundary(); +if (boundary.enforced && boundary.rules && boundary.rules.length > 0) { + console.log(`ok boundary: ${boundary.rules.length} rules enforced`); +} else if (!boundary.enforced) { + console.log(`ok boundary: adoption active`); +} else { + failed += 1; + console.log("fail boundary: no rules defined"); +} + +const sample = await createEnvelope({ kind: "check.probe", payload: { probe: true } }); +const result = await validateEnvelope(sample); +if (result.valid) { + console.log("ok envelope: sample validates"); +} else { + failed += 1; + console.log(`fail envelope: ${result.errors.join(", ")}`); +} + if (failed) process.exit(1); console.log("graphite incubator check passed"); - diff --git a/scripts/validate-consistency.mjs b/scripts/validate-consistency.mjs new file mode 100644 index 0000000..4a558a5 --- /dev/null +++ b/scripts/validate-consistency.mjs @@ -0,0 +1,81 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rootDir } from "../lib/paths.mjs"; +import { loadMeta } from "../lib/meta.mjs"; + +const meta = await loadMeta(); +let failed = 0; + +function fail(msg) { + failed += 1; + console.log(`DRIFT ${msg}`); +} + +function ok(msg) { + console.log(`ok ${msg}`); +} + +// 1. package.json status must reflect ontology status +const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8")); +const pkgHasStatus = pkg.version.includes("incubating"); +const ontologyIncubating = meta.status.includes("incubating"); + +if (pkgHasStatus === ontologyIncubating) { + ok("package.json version aligns with ontology status"); +} else { + fail("package.json version does not reflect ontology status"); +} + +// 2. README boundary rules must cover ontology boundary rules +const readme = await readFile(join(rootDir, "README.md"), "utf8"); +const readmeLower = readme.toLowerCase(); + +for (const rule of meta.non_adoption_boundary) { + const keywords = rule.replace(/before 1\.0/g, "").trim().split(/\s+/).filter(w => w.length > 3); + const found = keywords.every(kw => readmeLower.includes(kw.toLowerCase())); + if (found) { + ok(`README covers boundary rule: "${rule}"`); + } else { + fail(`README may be missing boundary rule: "${rule}"`); + } +} + +// 3. spec/non_adoption_boundary.md must cover ontology boundary rules +const boundarySpec = await readFile(join(rootDir, "spec", "non_adoption_boundary.md"), "utf8"); +const boundaryLower = boundarySpec.toLowerCase(); + +for (const rule of meta.non_adoption_boundary) { + const keywords = rule.replace(/before 1\.0/g, "").trim().split(/\s+/).filter(w => w.length > 3); + const found = keywords.every(kw => boundaryLower.includes(kw.toLowerCase())); + if (found) { + ok(`boundary spec covers rule: "${rule}"`); + } else { + fail(`boundary spec may be missing rule: "${rule}"`); + } +} + +// 4. adoption gate spec must reference the signed decision from ontology +const gateSpec = await readFile(join(rootDir, "spec", "adoption_gate.md"), "utf8"); + +if (gateSpec.includes(meta.adoption.requires_signed_decision)) { + ok("adoption gate references correct signed decision"); +} else { + fail(`adoption gate missing signed decision: ${meta.adoption.requires_signed_decision}`); +} + +// 5. protocol spec must reference the same protocol prefix as the schema +const schema = JSON.parse(await readFile(join(rootDir, "protocol", "envelope.schema.json"), "utf8")); +const protocolSpec = await readFile(join(rootDir, "spec", "graphite.protocol.md"), "utf8"); +const schemaPrefix = schema.properties.protocol.pattern.match(/\^([^/]+)/)?.[1] || ""; + +if (protocolSpec.includes(`${schemaPrefix}/`)) { + ok("protocol spec uses same prefix as envelope schema"); +} else { + fail(`protocol spec does not reference prefix "${schemaPrefix}" from schema`); +} + +if (failed) { + console.log(`\n${failed} consistency issue(s) found`); + process.exit(1); +} +console.log("\nall consistency checks passed"); diff --git a/spec/non_adoption_boundary.md b/spec/non_adoption_boundary.md index 2e410a4..9724a7f 100644 --- a/spec/non_adoption_boundary.md +++ b/spec/non_adoption_boundary.md @@ -15,6 +15,9 @@ Graphite pre-1.0 module Do not use: ```text +do not use as public foundation language before 1.0 +do not use as stable api before 1.0 +do not invoke informally in future sessions as if adopted foundation runs on Graphite Graphite is adopted Graphite protocol is stable From fcc827f3818f4df9db82fdcaf680162cddc2bab1 Mon Sep 17 00:00:00 2001 From: cem Date: Fri, 26 Jun 2026 13:26:03 +0000 Subject: [PATCH 2/2] ci: add npm ci step to install dependencies before check Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index aa14d78..9eddc27 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,5 +12,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 + - run: npm ci - run: npm run check