From 25e18f0766f23e2785aff99665cb7c207ce353bb Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 13 Sep 2026 15:47:58 +0000 Subject: [PATCH] Add OpenSkill descriptions for human capabilities and OpenProfile discovery --- .../contract/openskill.contract.test.ts | 56 +++++++ .../contract/spec-discovery.contract.test.ts | 1 + apps/logicsrc-web/e2e/openskill.spec.ts | 22 +++ apps/logicsrc-web/next.config.ts | 5 +- .../app/.well-known/openprofile.md/route.ts | 1 + apps/logicsrc-web/src/app/llms.txt/route.ts | 2 + .../logicsrc-web/src/app/openprofile/page.tsx | 4 + .../openskill/[slug]/openskill.md/route.ts | 21 +++ .../src/app/openskill/[slug]/page.tsx | 38 +++++ .../src/app/openskill/catalog.md/route.ts | 23 +++ apps/logicsrc-web/src/app/openskill/page.tsx | 52 +++++++ apps/logicsrc-web/src/app/sitemap.ts | 6 +- apps/logicsrc-web/src/lib/skills.ts | 59 +++++++ apps/logicsrc-web/src/lib/specs.ts | 1 + apps/logicsrc-web/src/styles.css | 2 +- docs/openprofile.md | 18 +++ docs/openskill.md | 146 ++++++++++++++++++ docs/openskill/accountant.md | 30 ++++ docs/openskill/accounting.md | 29 ++++ docs/openskill/branding.md | 30 ++++ docs/openskill/computer-science.md | 30 ++++ docs/openskill/developer.md | 30 ++++ docs/openskill/financial-advisor.md | 30 ++++ docs/openskill/graphic-design.md | 30 ++++ docs/openskill/hardware-architect.md | 30 ++++ docs/openskill/hardware-architectures.md | 30 ++++ docs/openskill/investing.md | 30 ++++ docs/openskill/investor.md | 28 ++++ docs/openskill/logo-design.md | 35 +++++ docs/openskill/software-development.md | 31 ++++ docs/openskill/software-engineer.md | 31 ++++ 31 files changed, 878 insertions(+), 3 deletions(-) create mode 100644 apps/logicsrc-web/contract/openskill.contract.test.ts create mode 100644 apps/logicsrc-web/e2e/openskill.spec.ts create mode 100644 apps/logicsrc-web/src/app/openskill/[slug]/openskill.md/route.ts create mode 100644 apps/logicsrc-web/src/app/openskill/[slug]/page.tsx create mode 100644 apps/logicsrc-web/src/app/openskill/catalog.md/route.ts create mode 100644 apps/logicsrc-web/src/app/openskill/page.tsx create mode 100644 apps/logicsrc-web/src/lib/skills.ts create mode 100644 docs/openskill.md create mode 100644 docs/openskill/accountant.md create mode 100644 docs/openskill/accounting.md create mode 100644 docs/openskill/branding.md create mode 100644 docs/openskill/computer-science.md create mode 100644 docs/openskill/developer.md create mode 100644 docs/openskill/financial-advisor.md create mode 100644 docs/openskill/graphic-design.md create mode 100644 docs/openskill/hardware-architect.md create mode 100644 docs/openskill/hardware-architectures.md create mode 100644 docs/openskill/investing.md create mode 100644 docs/openskill/investor.md create mode 100644 docs/openskill/logo-design.md create mode 100644 docs/openskill/software-development.md create mode 100644 docs/openskill/software-engineer.md diff --git a/apps/logicsrc-web/contract/openskill.contract.test.ts b/apps/logicsrc-web/contract/openskill.contract.test.ts new file mode 100644 index 0000000..6423b20 --- /dev/null +++ b/apps/logicsrc-web/contract/openskill.contract.test.ts @@ -0,0 +1,56 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { readSkill, SKILL_SLUGS, listSkills, summarizeSkill } from "../src/lib/skills"; +import { GET } from "../src/app/openskill/[slug]/openskill.md/route"; +import { GET as catalogMarkdown } from "../src/app/openskill/catalog.md/route"; +import { buildRedirects } from "../next.config"; + +describe("OpenSkill capability records", () => { + it("provides a portable index whose links resolve to the published record sources", async () => { + const response = catalogMarkdown(); + expect(response.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); + const body = await response.text(); + const paths = [...body.matchAll(/\]\((https?:\/\/[^)]+\/openskill\/([^/]+)\/openskill\.md)\)/g)]; + expect(paths).toHaveLength(SKILL_SLUGS.length); + for (const [, , slug] of paths) expect(readSkill(slug)).not.toBeNull(); + }); + + it("serves every published record losslessly as cross-origin Markdown", async () => { + for (const slug of SKILL_SLUGS) { + const source = readFileSync(resolve(process.cwd(), `../../docs/openskill/${slug}.md`), "utf8"); + const response = await GET(new Request(`https://example.com/openskill/${slug}/openskill.md`), { params: Promise.resolve({ slug }) }); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); + expect(response.headers.get("access-control-allow-origin")).toBe("*"); + expect(response.headers.get("content-disposition")).toBeNull(); + expect(await response.text()).toBe(source); + } + }); + + it("does not expose unlisted files or resolve request paths as filesystem paths", async () => { + for (const slug of ["unknown", "../openprofile", "../../package.json", "__proto__"]) { + expect(readSkill(slug)).toBeNull(); + const response = await GET(new Request("https://example.com"), { params: Promise.resolve({ slug }) }); + expect(response.status).toBe(404); + } + }); + + it("keeps knowledge, capabilities and occupations distinct in the catalog", () => { + const catalog = listSkills(); + expect(catalog.find((c) => c.slug === "logo-design")?.kind).toBe("skill"); + expect(catalog.find((c) => c.slug === "accounting")?.kind).toBe("knowledge"); + expect(catalog.find((c) => c.slug === "accountant")?.kind).toBe("occupation"); + expect(catalog.every((c) => c.name && c.description)).toBe(true); + }); + + it("does not infer a kind or consume metadata from examples or later sections", () => { + const source = "# Weaving\n\nMake interlaced textiles.\n\n```markdown\n- **Kind**: occupation\n```\n\n## Notes\n\n- **Kind**: knowledge\n"; + expect(summarizeSkill(source, "weaving")).toEqual({ slug: "weaving", name: "Weaving", description: "Make interlaced textiles." }); + expect(summarizeSkill("# Unusual craft\n\n- **Kind**: local-category\n\nA local practice.", "craft").kind).toBe("local-category"); + }); + + it("lets the OpenProfile skills path discover the same concept catalog", () => { + expect(buildRedirects()).toContainEqual({ source: "/openprofile/skills", destination: "/openskill", permanent: true }); + }); +}); diff --git a/apps/logicsrc-web/contract/spec-discovery.contract.test.ts b/apps/logicsrc-web/contract/spec-discovery.contract.test.ts index a771bd1..5324102 100644 --- a/apps/logicsrc-web/contract/spec-discovery.contract.test.ts +++ b/apps/logicsrc-web/contract/spec-discovery.contract.test.ts @@ -10,6 +10,7 @@ vi.mock("../src/lib/supabase", () => ({ publicClient: () => { throw new Error("o describe.each([ { slug: "openagent", name: "OpenAgent", family: "people" }, + { slug: "openskill", name: "OpenSkill", family: "people" }, { slug: "openabtest", name: "OpenABTest", family: "process" }, { slug: "openfleet", name: "OpenFleet", family: "process" }, { slug: "openrental", name: "OpenRental", family: "catalogs" }, diff --git a/apps/logicsrc-web/e2e/openskill.spec.ts b/apps/logicsrc-web/e2e/openskill.spec.ts new file mode 100644 index 0000000..4aa98ea --- /dev/null +++ b/apps/logicsrc-web/e2e/openskill.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from "@playwright/test"; + +test("human capabilities are discoverable and their Markdown can travel independently", async ({ page, request }) => { + await page.goto("/openprofile/skills"); + await expect(page).toHaveURL(/\/openskill$/); + await expect(page.getByRole("heading", { name: "OpenSkill", exact: true })).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); + + await page.getByRole("link", { name: "Logo design", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Logo design", exact: true })).toBeVisible(); + const sourceLink = page.getByRole("link", { name: "Markdown source", exact: true }); + const source = await request.get((await sourceLink.getAttribute("href"))!); + expect(source.status()).toBe(200); + expect(source.headers()["content-type"]).toBe("text/markdown; charset=utf-8"); + expect(await source.text()).toContain("# Logo design"); + expect(await source.text()).toContain("**Kind**: skill"); + + const index = await request.get("/openskill/catalog.md"); + expect(index.status()).toBe(200); + expect(index.headers()["content-type"]).toBe("text/markdown; charset=utf-8"); + expect(await index.text()).toContain("/openskill/accountant/openskill.md"); +}); diff --git a/apps/logicsrc-web/next.config.ts b/apps/logicsrc-web/next.config.ts index 4d9d5af..72ca626 100644 --- a/apps/logicsrc-web/next.config.ts +++ b/apps/logicsrc-web/next.config.ts @@ -79,7 +79,10 @@ export function buildRewrites( * page. Permanent, so the old URL in the sitemap and in links keeps working. */ export function buildRedirects() { - return [{ source: "/agent-swarm", destination: "/openfleet", permanent: true }]; + return [ + { source: "/agent-swarm", destination: "/openfleet", permanent: true }, + { source: "/openprofile/skills", destination: "/openskill", permanent: true } + ]; } const nextConfig: NextConfig = { diff --git a/apps/logicsrc-web/src/app/.well-known/openprofile.md/route.ts b/apps/logicsrc-web/src/app/.well-known/openprofile.md/route.ts index ed4541e..828a347 100644 --- a/apps/logicsrc-web/src/app/.well-known/openprofile.md/route.ts +++ b/apps/logicsrc-web/src/app/.well-known/openprofile.md/route.ts @@ -31,6 +31,7 @@ Open coordination standards for humans, AI agents, plugins, payment systems and - [OpenMCP](${SITE_URL}/openmcp): an open catalog of MCP relays, live at https://openmcp.logicsrc.com - [OpenProfile.md](${SITE_URL}/openprofile): one Markdown file for who and where, people and agents alike +- [OpenSkill](${SITE_URL}/openskill): portable descriptions of human skills, knowledge and occupations - [ASDLC](${SITE_URL}/asdlc): the agentic software development lifecycle - [OpenCreds](${SITE_URL}/opencreds): end-to-end encrypted credential sharing - [Docs](${SITE_URL}/docs): every specification LogicSRC publishes diff --git a/apps/logicsrc-web/src/app/llms.txt/route.ts b/apps/logicsrc-web/src/app/llms.txt/route.ts index 1416578..d4e43b7 100644 --- a/apps/logicsrc-web/src/app/llms.txt/route.ts +++ b/apps/logicsrc-web/src/app/llms.txt/route.ts @@ -33,6 +33,8 @@ export function GET(): Response { - [Blog](${SITE_URL}/blog): Project notes and release announcements. - [Blog RSS](${SITE_URL}/blog/rss.xml): Machine-readable feed of posts. - [This site's OpenProfile.md](${SITE_URL}/.well-known/openprofile.md) +- [OpenSkill concept catalog](${SITE_URL}/openskill): Human skills, knowledge and occupations. Each concept page links its portable openskill.md source; these are capability descriptions, not executable agent instructions. +- [OpenSkill Markdown index](${SITE_URL}/openskill/catalog.md): Names, kinds, descriptions and direct links to every published concept record. ${families.join("\n\n")} diff --git a/apps/logicsrc-web/src/app/openprofile/page.tsx b/apps/logicsrc-web/src/app/openprofile/page.tsx index 1026c4d..c21159b 100644 --- a/apps/logicsrc-web/src/app/openprofile/page.tsx +++ b/apps/logicsrc-web/src/app/openprofile/page.tsx @@ -259,6 +259,10 @@ export default function OpenProfilePage(): ReactNode {

Where everything lives