Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run check

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
26 changes: 26 additions & 0 deletions lib/boundary.mjs
Original file line number Diff line number Diff line change
@@ -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
};
}
71 changes: 71 additions & 0 deletions lib/envelope.mjs
Original file line number Diff line number Diff line change
@@ -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 };
}
4 changes: 4 additions & 0 deletions lib/index.mjs
Original file line number Diff line number Diff line change
@@ -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";
37 changes: 37 additions & 0 deletions lib/meta.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions lib/paths.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

export const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..");
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
36 changes: 34 additions & 2 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -11,16 +16,43 @@ 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;
console.log(`missing ${file}`);
}
}

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");

81 changes: 81 additions & 0 deletions scripts/validate-consistency.mjs
Original file line number Diff line number Diff line change
@@ -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");
3 changes: 3 additions & 0 deletions spec/non_adoption_boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading