Skip to content
Merged
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"type": "module",
"scripts": {
"dev": "npm run refresh:data && eleventy --serve",
"refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:msdmd && npm run refresh:sitrep && npm run refresh:textbook && npm run refresh:works",
"refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:skills && npm run refresh:msdmd && npm run refresh:sitrep && npm run refresh:textbook && npm run refresh:works",
"refresh:canon": "node scripts/fetch-canon.mjs && node scripts/parse-canon.mjs",
"refresh:github": "node scripts/fetch-github-org.mjs",
"refresh:skills": "node scripts/fetch-skill-registry.mjs",
"refresh:msdmd": "node scripts/fetch-org-msdmd.mjs",
"refresh:sitrep": "node scripts/fetch-sitrep.mjs",
"refresh:textbook": "node scripts/fetch-textbook.mjs",
Expand All @@ -21,7 +22,7 @@
"validate": "node scripts/validate-content.mjs && node scripts/verify-generated-routes.mjs && node scripts/verify-article-canon.mjs",
"build": "npm run validate && eleventy && pagefind --site _site && node scripts/write-build-info.mjs",
"pretest": "node scripts/prepare-tests.mjs",
"test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/org-msdmd.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sitrep.test.mjs",
"test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/org-msdmd.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sitrep.test.mjs tests/webmcp.test.mjs",
"test:generated": "node --test tests/generated-site.test.mjs tests/human-ui-generated.test.mjs tests/textbook-generated.test.mjs tests/math-generated.test.mjs && node tests/links.test.mjs",
"test:browser": "playwright test",
"test:e2e": "playwright test tests/site.spec.mjs",
Expand Down
155 changes: 155 additions & 0 deletions scripts/fetch-skill-registry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';

// === MODULE_BUILD ===
// id: skill_registry_collector
// purpose: Publish a commit-pinned projection of skill-lib/skills.json for the website-owned WebMCP surface without transferring skill authority to the website.
// entrypoint: npm run refresh:skills
// tests: tests/webmcp.test.mjs
// === END MODULE_BUILD ===
// === BOUNDARIES ===
// id: skill_registry_source_boundary
// network: reads only commit-pinned raw.githubusercontent.com/The-Interdependency/skill-lib/<commit>/skills.json
// storage: writes generated, public asset, and last-known-good JSON snapshots only
// authority: The-Interdependency/skill-lib remains canonical; this website publishes a derived registry projection for browser tools
// failure: a network or parse failure falls back only to a previously verified snapshot; absence of both fails closed
// === END BOUNDARIES ===
// === CONTRACTS ===
// id: skill_registry_exact_input_identity
// given: skill-lib skills.json is projected into the website
// then: repository, exact head commit, source path, and SHA-256 remain attached to the projection
// class: evidence
//
// id: skill_registry_dependency_integrity
// given: a skill declares depends_on entries
// then: every dependency names another skill in the same registry
// class: correctness
// === END CONTRACTS ===
// Usage: run `npm run refresh:skills` after `npm run refresh:github`; WebMCP consumes `/assets/data/skill-registry.json`. Use `OFFLINE=1` only after at least one verified online refresh has created the last-known-good snapshot.

const ORGANIZATION = 'The-Interdependency';
const REPOSITORY = 'skill-lib';
const SOURCE_PATH = 'skills.json';
const RAW_GITHUB_ORIGIN = 'https://raw.githubusercontent.com';
const GENERATED_REPOS = 'src/_data/generated/repos.json';
const GENERATED_OUT = 'src/_data/generated/skillRegistry.json';
const PUBLIC_OUT = 'src/assets/data/skill-registry.json';
const SNAPSHOT_OUT = 'src/_data/snapshots/skill-registry.last-known-good.json';

function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}

function stableJson(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}

function rawGithubUrl(commit) {
return new URL(`/${ORGANIZATION}/${REPOSITORY}/${commit}/${SOURCE_PATH}`, RAW_GITHUB_ORIGIN);
}

function getText(target) {
const url = target instanceof URL ? target : new URL(target);
if (url.protocol !== 'https:' || url.origin !== RAW_GITHUB_ORIGIN) {
throw new Error(`refusing non-raw-GitHub target: ${url.origin}`);
}
return execFileSync(
'curl',
['-fsSL', '--retry', '2', '--max-time', '20', url.href],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
);
}

export function normalizeRegistry(sourceText, commit) {
const parsed = JSON.parse(String(sourceText));
if (parsed?.repo !== `${ORGANIZATION}/${REPOSITORY}`) {
throw new Error(`unexpected registry repo: ${parsed?.repo || 'missing'}`);
}
if (!Array.isArray(parsed.skills) || parsed.skills.length === 0) {
throw new Error('skill registry has no skills');
}

const skills = parsed.skills.map(skill => ({
name: String(skill.name || '').trim(),
path: String(skill.path || '').trim(),
kind: String(skill.kind || '').trim(),
depends_on: Array.isArray(skill.depends_on) ? skill.depends_on.map(String) : [],
description: String(skill.description || '').trim()
}));

const names = new Set(skills.map(skill => skill.name));
if (names.size !== skills.length || skills.some(skill => !skill.name || !skill.path || !skill.description)) {
throw new Error('skill registry contains duplicate or incomplete skill records');
}
for (const skill of skills) {
for (const dependency of skill.depends_on) {
if (!names.has(dependency)) throw new Error(`unresolved skill dependency: ${skill.name} -> ${dependency}`);
}
}

return {
version: parsed.version,
source: {
repository: `${ORGANIZATION}/${REPOSITORY}`,
commit,
path: SOURCE_PATH,
sha256: sha256(sourceText)
},
install_path: parsed.install_path,
superseded_skills: Array.isArray(parsed.superseded_skills) ? parsed.superseded_skills : [],
skills
};
}

async function readFallback() {
const text = await readFile(SNAPSHOT_OUT, 'utf8');
const parsed = JSON.parse(text);
if (!parsed?.source?.commit || !Array.isArray(parsed.skills) || parsed.skills.length === 0) {
throw new Error('last-known-good skill registry snapshot is invalid');
}
return parsed;
}

async function writeProjection(registry, fallback, hmmm = []) {
await Promise.all([
mkdir('src/_data/generated', { recursive: true }),
mkdir('src/assets/data', { recursive: true })
]);
const serialized = stableJson({ ...registry, fallback, hmmm });
await Promise.all([
writeFile(GENERATED_OUT, serialized),
writeFile(PUBLIC_OUT, serialized)
]);
}

async function main() {
if (process.env.OFFLINE === '1') {
const snapshot = await readFallback();
await writeProjection(snapshot, true, ['offline build: using last-known-good skill registry snapshot']);
return;
}

try {
const repoIndex = JSON.parse(await readFile(GENERATED_REPOS, 'utf8'));
const skillLib = (repoIndex.repositories || []).find(repo => repo.name === REPOSITORY);
if (!skillLib?.head_sha) throw new Error('skill-lib head_sha missing from generated repository index');

const sourceText = getText(rawGithubUrl(skillLib.head_sha));
const registry = normalizeRegistry(sourceText, skillLib.head_sha);
await mkdir('src/_data/snapshots', { recursive: true });
await writeFile(SNAPSHOT_OUT, stableJson(registry));
await writeProjection(registry, false);
} catch (error) {
try {
const snapshot = await readFallback();
Comment thread
erinepshovel-code marked this conversation as resolved.
await writeProjection(snapshot, true, [`registry refresh failed: ${error.message}`]);
} catch (fallbackError) {
throw new Error(`skill registry refresh failed and no valid snapshot exists: ${error.message}; fallback: ${fallbackError.message}`);
}
}
}

if (import.meta.url === `file://${process.argv[1]}`) {
await main();
}
3 changes: 2 additions & 1 deletion src/_includes/layouts/base.njk
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<!-- Usage: every ordinary site page inherits a human-facing navigation. Machine-readable context remains discoverable through head metadata and dedicated routes without occupying the visible reading path. -->
<!-- Usage: every ordinary site page inherits a human-facing navigation. Machine-readable context remains discoverable through head metadata and dedicated routes without occupying the visible reading path. Routes with `webmcp: true` additionally load the browser-native WebMCP provider module. -->
<html lang="en">
<head>
<meta charset="utf-8">
Expand All @@ -19,6 +19,7 @@
<script src="/assets/js/site.js" defer></script>
<script src="/assets/js/org-map.js" type="module"></script>
<script src="/assets/js/sitrep.js" type="module"></script>
{% if webmcp %}<script src="/assets/js/webmcp.js" type="module"></script>{% endif %}
</head>
<body>
<a class="skip-link" href="#content">Skip to content</a>
Expand Down
120 changes: 120 additions & 0 deletions src/assets/js/webmcp-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// === MODULE_BUILD ===
// id: webmcp_skill_registry_adapter
// purpose: Provide deterministic, read-only operations over the website's commit-pinned skill-lib registry projection.
// entrypoint: imported by /assets/js/webmcp.js and tests/webmcp.test.mjs
// tests: tests/webmcp.test.mjs
// === END MODULE_BUILD ===
// === BOUNDARIES ===
// id: webmcp_skill_registry_read_boundary
// network: none
// storage: none
// user_data: none
// operational_effects: none; all exported operations are read-only transformations over supplied registry data
// === END BOUNDARIES ===
// === CONTRACTS ===
// id: webmcp_registry_smallest_dependency_closure
// given: a registered skill name
// then: resolveSkillClosure returns that skill plus every transitive depends_on prerequisite exactly once in dependency-first order
// class: correctness
// === END CONTRACTS ===
// Usage: `const registry = createSkillRegistry(data); registry.findSkills({ query: 'repository audit' })`. Consumers must pass the generated `/assets/data/skill-registry.json` projection, never invent skill records locally.

function normalizeText(value) {
return String(value || '').trim().toLowerCase();
}

function publicSkill(skill, source) {
return {
name: skill.name,
kind: skill.kind,
depends_on: [...skill.depends_on],
description: skill.description,
canonical_path: skill.path,
canonical_url: `https://github.com/${source.repository}/blob/${source.commit}/${skill.path}`
};
}

export function createSkillRegistry(registryData) {
if (!registryData?.source?.repository || !registryData?.source?.commit || !Array.isArray(registryData.skills)) {
throw new Error('invalid skill registry projection');
}

const source = registryData.source;
const byName = new Map(registryData.skills.map(skill => [skill.name, skill]));

function requireSkill(name) {
const skill = byName.get(String(name || '').trim());
if (!skill) throw new Error(`unknown skill: ${name}`);
return skill;
}

function listSkills({ kind = '' } = {}) {
const normalizedKind = normalizeText(kind);
return registryData.skills
.filter(skill => !normalizedKind || normalizeText(skill.kind) === normalizedKind)
.map(skill => publicSkill(skill, source));
}

function findSkills({ query, kind = '', limit = 8 } = {}) {
const terms = normalizeText(query).split(/\s+/).filter(Boolean);
const normalizedKind = normalizeText(kind);
const boundedLimit = Math.max(1, Math.min(Number(limit) || 8, 20));
if (terms.length === 0) return listSkills({ kind }).slice(0, boundedLimit);

return registryData.skills
.filter(skill => !normalizedKind || normalizeText(skill.kind) === normalizedKind)
.map(skill => {
const name = normalizeText(skill.name);
const description = normalizeText(skill.description);
const path = normalizeText(skill.path);
let score = 0;
for (const term of terms) {
if (name === term) score += 8;
if (name.includes(term)) score += 5;
if (path.includes(term)) score += 3;
if (description.includes(term)) score += 2;
}
return { skill, score };
})
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
.slice(0, boundedLimit)
.map(item => publicSkill(item.skill, source));
}

function inspectSkill({ name } = {}) {
return publicSkill(requireSkill(name), source);
}

function resolveSkillClosure({ name } = {}) {
const ordered = [];
const visiting = new Set();
const visited = new Set();

function visit(skillName) {
if (visited.has(skillName)) return;
if (visiting.has(skillName)) throw new Error(`skill dependency cycle at ${skillName}`);
visiting.add(skillName);
const skill = requireSkill(skillName);
for (const dependency of skill.depends_on) visit(dependency);
visiting.delete(skillName);
visited.add(skillName);
ordered.push(publicSkill(skill, source));
}

visit(String(name || '').trim());
return ordered;
}

function getRegistryStatus() {
return {
registry_version: registryData.version,
skill_count: registryData.skills.length,
source: { ...source },
fallback: Boolean(registryData.fallback),
hmmm: Array.isArray(registryData.hmmm) ? [...registryData.hmmm] : []
};
}

return { listSkills, findSkills, inspectSkill, resolveSkillClosure, getRegistryStatus };
}
Loading
Loading