From e6654b8fc72fcffd33487bec00a477e83753fb11 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:12:57 -0700 Subject: [PATCH 1/8] feat(webmcp): add canonical skill registry collector --- scripts/fetch-skill-registry.mjs | 148 +++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 scripts/fetch-skill-registry.mjs diff --git a/scripts/fetch-skill-registry.mjs b/scripts/fetch-skill-registry.mjs new file mode 100644 index 0000000..b0ae7e3 --- /dev/null +++ b/scripts/fetch-skill-registry.mjs @@ -0,0 +1,148 @@ +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//skills.json +// storage: writes generated 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 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 writeGenerated(registry, fallback, hmmm = []) { + await mkdir('src/_data/generated', { recursive: true }); + const output = { ...registry, fallback, hmmm }; + await writeFile(GENERATED_OUT, stableJson(output)); +} + +async function main() { + if (process.env.OFFLINE === '1') { + const snapshot = await readFallback(); + await writeGenerated(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 writeGenerated(registry, false); + } catch (error) { + try { + const snapshot = await readFallback(); + await writeGenerated(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(); +} From fa719a5a76e9b061fe78147d407a6d3dedeaf576 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:13:18 -0700 Subject: [PATCH 2/8] feat(webmcp): add read-only skill registry adapter --- src/assets/js/webmcp-registry.js | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/assets/js/webmcp-registry.js diff --git a/src/assets/js/webmcp-registry.js b/src/assets/js/webmcp-registry.js new file mode 100644 index 0000000..6f9b1e4 --- /dev/null +++ b/src/assets/js/webmcp-registry.js @@ -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 }; +} From bd53c4776731a27fe8ab89135fbb4408b37d74de Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:13:47 -0700 Subject: [PATCH 3/8] feat(webmcp): register browser-native skill registry tools --- src/assets/js/webmcp.js | 149 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/assets/js/webmcp.js diff --git a/src/assets/js/webmcp.js b/src/assets/js/webmcp.js new file mode 100644 index 0000000..4d32c66 --- /dev/null +++ b/src/assets/js/webmcp.js @@ -0,0 +1,149 @@ +import { createSkillRegistry } from './webmcp-registry.js'; + +// === MODULE_BUILD === +// id: interdependency_webmcp_surface +// purpose: Register the website-owned, read-only WebMCP tool surface over the commit-pinned skill-lib registry projection. +// entrypoint: /webmcp/ +// tests: tests/webmcp.test.mjs +// === END MODULE_BUILD === +// === BOUNDARIES === +// id: interdependency_webmcp_surface_boundary +// network: same-origin GET of /assets/data/skill-registry.json only +// storage: none +// user_data: none +// operational_effects: none; v0 exposes registry discovery and dependency resolution only +// authority: the website owns tool registration; The-Interdependency/skill-lib remains authority for skill definitions +// === END BOUNDARIES === +// === CONTRACTS === +// id: webmcp_tools_are_read_only_registry_operations +// given: an agent invokes any v0 tool +// then: execution reads the generated registry projection and returns structured results without mutating the site, GitHub, or skill-lib +// class: safety +// === END CONTRACTS === +// Usage: open `/webmcp/` in a WebMCP-capable browser or in-app browser. The page registers five read-only tools. Unsupported browsers still show the human-readable capability/status page and do not polyfill or fake WebMCP support. + +const REGISTRY_URL = '/assets/data/skill-registry.json'; +const statusElement = () => document.querySelector('[data-webmcp-status]'); +const sourceElement = () => document.querySelector('[data-webmcp-source]'); + +function setStatus(message, state = 'hmmm') { + const target = statusElement(); + if (!target) return; + target.textContent = message; + target.dataset.state = state; +} + +function jsonResult(value) { + return JSON.stringify(value, null, 2); +} + +async function loadRegistry() { + const response = await fetch(REGISTRY_URL, { headers: { accept: 'application/json' } }); + if (!response.ok) throw new Error(`registry HTTP ${response.status}`); + return response.json(); +} + +function updateSource(status) { + const target = sourceElement(); + if (!target) return; + const suffix = status.fallback ? ' (last-known-good fallback)' : ''; + target.textContent = `${status.source.repository}@${status.source.commit.slice(0, 12)}:${status.source.path}${suffix}`; +} + +async function registerTool(tool) { + await document.modelContext.registerTool(tool); +} + +export async function registerInterdependencyWebMCP() { + if (!document.modelContext?.registerTool) { + setStatus('WebMCP API unavailable in this browser. The registry projection is live; tool registration requires a WebMCP-capable browser.', 'hmmm'); + return { registered: false, reason: 'webmcp-unavailable' }; + } + if (globalThis.__interdependencyWebMcpRegistered) { + return { registered: true, reused: true }; + } + + const data = await loadRegistry(); + const registry = createSkillRegistry(data); + const status = registry.getRegistryStatus(); + updateSource(status); + + await registerTool({ + name: 'tiw_registry_status', + title: 'The Interdependency registry status', + description: 'Return provenance, version, skill count, and fallback state for the website\'s commit-pinned projection of The-Interdependency/skill-lib.', + inputSchema: { type: 'object', properties: {}, additionalProperties: false }, + annotations: { readOnlyHint: true, untrustedContentHint: false }, + execute: async () => jsonResult(registry.getRegistryStatus()) + }); + + await registerTool({ + name: 'tiw_list_skills', + title: 'List Interdependency skills', + description: 'List skills in The-Interdependency/skill-lib registry. Optionally filter by exact skill kind.', + inputSchema: { + type: 'object', + properties: { + kind: { type: 'string', description: 'Optional exact kind such as procedural or metadata-block.' } + }, + additionalProperties: false + }, + annotations: { readOnlyHint: true, untrustedContentHint: false }, + execute: async input => jsonResult(registry.listSkills(input)) + }); + + await registerTool({ + name: 'tiw_find_skill', + title: 'Find an Interdependency skill', + description: 'Search the canonical skill registry by task words, skill name, path, and description. Returns the highest-scoring matches without loading the whole library into agent context.', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Task or capability to search for.' }, + kind: { type: 'string', description: 'Optional exact kind filter.' }, + limit: { type: 'integer', minimum: 1, maximum: 20, default: 8 } + }, + required: ['query'], + additionalProperties: false + }, + annotations: { readOnlyHint: true, untrustedContentHint: false }, + execute: async input => jsonResult(registry.findSkills(input)) + }); + + await registerTool({ + name: 'tiw_inspect_skill', + title: 'Inspect an Interdependency skill', + description: 'Return one registered skill\'s kind, description, declared dependencies, canonical path, and commit-pinned GitHub source URL.', + inputSchema: { + type: 'object', + properties: { name: { type: 'string', description: 'Exact registered skill name.' } }, + required: ['name'], + additionalProperties: false + }, + annotations: { readOnlyHint: true, untrustedContentHint: false }, + execute: async input => jsonResult(registry.inspectSkill(input)) + }); + + await registerTool({ + name: 'tiw_resolve_skill_closure', + title: 'Resolve Interdependency skill closure', + description: 'Resolve the smallest dependency-first transitive skill closure required by one exact registered skill.', + inputSchema: { + type: 'object', + properties: { name: { type: 'string', description: 'Exact registered skill name.' } }, + required: ['name'], + additionalProperties: false + }, + annotations: { readOnlyHint: true, untrustedContentHint: false }, + execute: async input => jsonResult(registry.resolveSkillClosure(input)) + }); + + globalThis.__interdependencyWebMcpRegistered = true; + setStatus(`WebMCP live: 5 read-only tools registered over ${status.skill_count} skills.`, status.fallback ? 'hmmm' : 'implemented'); + return { registered: true, tools: 5, registry: status }; +} + +registerInterdependencyWebMCP().catch(error => { + console.error('Interdependency WebMCP registration failed', error); + setStatus(`WebMCP registration failed: ${error.message}`, 'hmmm'); +}); From 8ad1fa94fac055220a99902f30c9e33096f51a82 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:14:07 -0700 Subject: [PATCH 4/8] feat(webmcp): add public provider and status page --- src/webmcp/index.njk | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/webmcp/index.njk diff --git a/src/webmcp/index.njk b/src/webmcp/index.njk new file mode 100644 index 0000000..cdadcba --- /dev/null +++ b/src/webmcp/index.njk @@ -0,0 +1,53 @@ +--- +layout: layouts/base.njk +title: The Interdependency WebMCP +description: Browser-native, read-only WebMCP access to the commit-pinned The Interdependency skill registry. +permalink: /webmcp/ +webmcp: true +--- +
+

WebMCP Challenge · implemented public surface

+

The Interdependency WebMCP

+

This page is both the human-readable boundary and the browser-native WebMCP provider for The Interdependency skill registry. The website owns the registered tools; The-Interdependency/skill-lib remains the canonical source for skill definitions.

+
+ +
+

Runtime status

+

Loading the commit-pinned skill registry and checking this browser for WebMCP support…

+

Registry source: resolving…

+ +
+ +
+

Registered v0 tools

+
+
tiw_registry_status
+
Provenance, registry version, skill count, and fallback state.
+
tiw_list_skills
+
List registered skills, optionally filtered by exact skill kind.
+
tiw_find_skill
+
Search the registry by task words, names, paths, and descriptions while returning only a bounded relevant set.
+
tiw_inspect_skill
+
Inspect one skill and receive its declared dependencies plus a commit-pinned canonical source URL.
+
tiw_resolve_skill_closure
+
Resolve the smallest dependency-first transitive closure required by one registered skill.
+
+
+ +
+

Authority boundary

+
skill-lib/SKILL.md + skills.json
+        ↓ commit-pinned build projection
+interdependentway.org skill registry
+        ↓ browser-native WebMCP registration
+read-only agent tools
+

The browser tools do not mutate GitHub, install skills, change canon, or create a competing skill definition. Their job is discovery, inspection, and dependency resolution.

+
+ +
+

Usage guidance

+

Open this route in a WebMCP-capable browser or in-app browser, then ask the browser agent to find or inspect an Interdependency skill. For example: “Find the skill for auditing and repairing a repository, then resolve its dependency closure.”

+

In an ordinary browser without WebMCP support, this page remains a truthful status and documentation surface; it does not fake tool availability.

+
+ +
hmmmWrite-capable operations are intentionally absent from v0. Any future install, propagation, or repository-changing tool requires a separate authenticated boundary rather than quietly upgrading this read-only surface.
From 96f548bf88068c2f0a1f10317610d6529c99a2db Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:14:22 -0700 Subject: [PATCH 5/8] feat(webmcp): load provider only on declared routes --- src/_includes/layouts/base.njk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index 633d11d..b14202b 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -1,5 +1,5 @@ - + @@ -19,6 +19,7 @@ + {% if webmcp %}{% endif %} From 7b80ffe35cdd6a6483dcbee8bc8b57e1c2cb7a07 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:14:51 -0700 Subject: [PATCH 6/8] fix(webmcp): publish registry through existing asset passthrough --- scripts/fetch-skill-registry.mjs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/fetch-skill-registry.mjs b/scripts/fetch-skill-registry.mjs index b0ae7e3..db0cea3 100644 --- a/scripts/fetch-skill-registry.mjs +++ b/scripts/fetch-skill-registry.mjs @@ -11,7 +11,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; // === BOUNDARIES === // id: skill_registry_source_boundary // network: reads only commit-pinned raw.githubusercontent.com/The-Interdependency/skill-lib//skills.json -// storage: writes generated and last-known-good JSON snapshots only +// 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 === @@ -34,6 +34,7 @@ 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) { @@ -110,16 +111,22 @@ async function readFallback() { return parsed; } -async function writeGenerated(registry, fallback, hmmm = []) { - await mkdir('src/_data/generated', { recursive: true }); - const output = { ...registry, fallback, hmmm }; - await writeFile(GENERATED_OUT, stableJson(output)); +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 writeGenerated(snapshot, true, ['offline build: using last-known-good skill registry snapshot']); + await writeProjection(snapshot, true, ['offline build: using last-known-good skill registry snapshot']); return; } @@ -132,11 +139,11 @@ async function main() { const registry = normalizeRegistry(sourceText, skillLib.head_sha); await mkdir('src/_data/snapshots', { recursive: true }); await writeFile(SNAPSHOT_OUT, stableJson(registry)); - await writeGenerated(registry, false); + await writeProjection(registry, false); } catch (error) { try { const snapshot = await readFallback(); - await writeGenerated(snapshot, true, [`registry refresh failed: ${error.message}`]); + 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}`); } From 50c1d69d47f7f38bf39bac4d0cb486b106989629 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:15:08 -0700 Subject: [PATCH 7/8] build(webmcp): refresh registry and include contract tests --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6b55f90..6f46995 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", From ab5616688d4fc07489ff72c10e9424a1035636a9 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Mon, 31 Aug 2026 12:15:29 -0700 Subject: [PATCH 8/8] test(webmcp): verify registry and provider contracts --- tests/webmcp.test.mjs | 84 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/webmcp.test.mjs diff --git a/tests/webmcp.test.mjs b/tests/webmcp.test.mjs new file mode 100644 index 0000000..9872c4c --- /dev/null +++ b/tests/webmcp.test.mjs @@ -0,0 +1,84 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { normalizeRegistry } from '../scripts/fetch-skill-registry.mjs'; +import { createSkillRegistry } from '../src/assets/js/webmcp-registry.js'; + +// Usage: run with `npm test`; these checks verify registry provenance, dependency closure, WebMCP tool names/read-only annotations, and the dedicated provider route without requiring a WebMCP-capable test browser. + +const sourceRegistry = JSON.stringify({ + version: 1, + repo: 'The-Interdependency/skill-lib', + install_path: '.agents/skills//', + superseded_skills: [], + skills: [ + { name: 'msdmd', path: 'msdmd/SKILL.md', kind: 'metadata-block', description: 'foundational metadata convention' }, + { name: 'cap-build', path: 'cap-build/SKILL.md', kind: 'metadata-block', depends_on: ['msdmd'], description: 'capability inventory' }, + { name: 'repo-audit-repair', path: 'repo-audit-repair/SKILL.md', kind: 'procedural', description: 'audit and repair a repository' } + ] +}); + +function sampleProjection() { + return { + ...normalizeRegistry(sourceRegistry, '0123456789abcdef0123456789abcdef01234567'), + fallback: false, + hmmm: [] + }; +} + +test('skill registry projection preserves exact source identity and rejects unresolved dependencies', () => { + const projection = sampleProjection(); + assert.equal(projection.source.repository, 'The-Interdependency/skill-lib'); + assert.equal(projection.source.commit, '0123456789abcdef0123456789abcdef01234567'); + assert.equal(projection.source.path, 'skills.json'); + assert.match(projection.source.sha256, /^[a-f0-9]{64}$/); + + const broken = JSON.stringify({ + version: 1, + repo: 'The-Interdependency/skill-lib', + skills: [{ name: 'a', path: 'a/SKILL.md', kind: 'procedural', depends_on: ['missing'], description: 'broken' }] + }); + assert.throws(() => normalizeRegistry(broken, 'abc'), /unresolved skill dependency/); +}); + +test('registry adapter finds skills and resolves the smallest dependency-first closure', () => { + const registry = createSkillRegistry(sampleProjection()); + const matches = registry.findSkills({ query: 'audit repository' }); + assert.equal(matches[0].name, 'repo-audit-repair'); + + const closure = registry.resolveSkillClosure({ name: 'cap-build' }); + assert.deepEqual(closure.map(skill => skill.name), ['msdmd', 'cap-build']); + assert.match(closure[1].canonical_url, /The-Interdependency\/skill-lib\/blob\/0123456789abcdef/); +}); + +test('WebMCP provider registers only the five declared read-only registry tools', async () => { + const source = await readFile('src/assets/js/webmcp.js', 'utf8'); + for (const name of [ + 'tiw_registry_status', + 'tiw_list_skills', + 'tiw_find_skill', + 'tiw_inspect_skill', + 'tiw_resolve_skill_closure' + ]) { + assert.match(source, new RegExp(`name: '${name}'`)); + } + assert.equal((source.match(/annotations: \{ readOnlyHint: true/g) || []).length, 5); + assert.match(source, /document\.modelContext\.registerTool/); + assert.doesNotMatch(source, /unregisterTool|install_skill|propagate_skill|update_file|create_file/); +}); + +test('dedicated WebMCP route loads the provider explicitly and keeps unsupported browsers truthful', async () => { + const [page, layout, packageJson] = await Promise.all([ + readFile('src/webmcp/index.njk', 'utf8'), + readFile('src/_includes/layouts/base.njk', 'utf8'), + readFile('package.json', 'utf8') + ]); + assert.match(page, /permalink: \/webmcp\//); + assert.match(page, /webmcp: true/); + assert.match(page, /The Interdependency WebMCP/); + assert.match(page, /data-webmcp-status/); + assert.match(page, /does not fake tool availability/); + assert.match(layout, /\{% if webmcp %\}