mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 12:54:03 +00:00
Add OpenSkill descriptions for human capabilities and OpenProfile discovery
This commit is contained in:
parent
ba69f58243
commit
25e18f0766
31 changed files with 878 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
||||
|
|
|
|||
|
|
@ -259,6 +259,10 @@ export default function OpenProfilePage(): ReactNode {
|
|||
<h2>Where everything lives</h2>
|
||||
</div>
|
||||
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
|
||||
<li>
|
||||
<Link href="/openprofile/skills">Skills and knowledge</Link>: portable human capabilities,
|
||||
occupations and taxonomy links through OpenSkill
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/docs/openprofile">Specification</Link>, with the eight rules, discovery,
|
||||
verification and the version history
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { readSkill, SKILL_SLUGS } from "@/lib/skills";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export const dynamicParams = false;
|
||||
export function generateStaticParams() {
|
||||
return SKILL_SLUGS.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const source = readSkill(slug);
|
||||
if (source === null) return new Response("Not found", { status: 404 });
|
||||
return new Response(source, {
|
||||
headers: {
|
||||
"content-type": "text/markdown; charset=utf-8",
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "public, max-age=3600",
|
||||
"x-content-type-options": "nosniff"
|
||||
}
|
||||
});
|
||||
}
|
||||
38
apps/logicsrc-web/src/app/openskill/[slug]/page.tsx
Normal file
38
apps/logicsrc-web/src/app/openskill/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { marked } from "marked";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { sanitizeRenderedHtml } from "@/lib/html";
|
||||
import { readSkill, SKILL_SLUGS, summarizeSkill } from "@/lib/skills";
|
||||
|
||||
export const dynamicParams = false;
|
||||
export function generateStaticParams() {
|
||||
return SKILL_SLUGS.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const source = readSkill(slug);
|
||||
if (!source) return { title: "Not found · OpenSkill" };
|
||||
const concept = summarizeSkill(source, slug);
|
||||
return {
|
||||
title: `${concept.name} · OpenSkill · LogicSRC`,
|
||||
description: concept.description,
|
||||
alternates: { canonical: `/openskill/${slug}`, types: { "text/markdown": `/openskill/${slug}/openskill.md` } }
|
||||
};
|
||||
}
|
||||
|
||||
export default async function SkillPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const source = readSkill(slug);
|
||||
if (!source) notFound();
|
||||
const concept = summarizeSkill(source, slug);
|
||||
const html = sanitizeRenderedHtml(await marked.parse(source));
|
||||
return <SiteShell crumbTitle={concept.name}>
|
||||
<article className="band" style={{ maxWidth: "48rem" }}>
|
||||
<p><Link href="/openskill">← All concepts</Link>{" · "}<a href={`/openskill/${slug}/openskill.md`}>Markdown source</a></p>
|
||||
<div className="blog-content" style={{ lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</article>
|
||||
</SiteShell>;
|
||||
}
|
||||
23
apps/logicsrc-web/src/app/openskill/catalog.md/route.ts
Normal file
23
apps/logicsrc-web/src/app/openskill/catalog.md/route.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { listSkills } from "@/lib/skills";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, "");
|
||||
|
||||
export function GET() {
|
||||
const records = listSkills().map((concept) =>
|
||||
`- [${concept.name}](${SITE_URL}/openskill/${concept.slug}/openskill.md) — ${concept.kind ?? "kind unstated"}: ${concept.description}`
|
||||
);
|
||||
return new Response([
|
||||
"# OpenSkill catalog", "",
|
||||
"Portable descriptions of human skills, knowledge and occupations. These are concept definitions; personal claims and evidence belong in OpenProfile.", "",
|
||||
`Specification: ${SITE_URL}/docs/openskill`, "",
|
||||
...records, ""
|
||||
].join("\n"), {
|
||||
headers: {
|
||||
"content-type": "text/markdown; charset=utf-8",
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "public, max-age=3600",
|
||||
"x-content-type-options": "nosniff"
|
||||
}
|
||||
});
|
||||
}
|
||||
52
apps/logicsrc-web/src/app/openskill/page.tsx
Normal file
52
apps/logicsrc-web/src/app/openskill/page.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteShell } from "@/components/site-shell";
|
||||
import { listSkills } from "@/lib/skills";
|
||||
import { pre, table, td, th } from "../openontology/ui";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenSkill · Human skills and knowledge · LogicSRC",
|
||||
description: "Portable descriptions of human skills, knowledge and occupations, linked to ESCO and O*NET and carried in OpenProfile.",
|
||||
alternates: { canonical: "/openskill" }
|
||||
};
|
||||
|
||||
export default function OpenSkillPage() {
|
||||
const concepts = listSkills();
|
||||
return (
|
||||
<SiteShell active="OpenSkill">
|
||||
<section className="band">
|
||||
<div className="section-head">
|
||||
<p className="eyebrow">People and agents · Draft 0.1</p>
|
||||
<h1>OpenSkill</h1>
|
||||
<p>What you know. What you can do.</p>
|
||||
</div>
|
||||
<p>Branding, accounting, software engineering, hardware architecture. Describe a human capability in a small Markdown file, connect it to an established taxonomy, and carry it between profiles, portfolios and job boards.</p>
|
||||
<p><Link className="button-primary" href="/docs/openskill">Read the specification</Link>{" · "}<Link href="/openprofile">Use it in OpenProfile</Link>{" · "}<a href="/openskill/catalog.md">Markdown catalog</a></p>
|
||||
</section>
|
||||
<section className="band">
|
||||
<div className="section-head"><h2>A starting vocabulary</h2><p>Skills describe activity; knowledge describes a subject; occupations describe roles. Each entry has its own editable Markdown source.</p></div>
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table style={table}>
|
||||
<thead><tr><th style={th}>Concept</th><th style={th}>Kind</th><th style={th}>Description</th></tr></thead>
|
||||
<tbody>{concepts.map((concept) => <tr key={concept.slug}>
|
||||
<td style={td}><Link href={`/openskill/${concept.slug}`}>{concept.name}</Link></td>
|
||||
<td style={td}>{concept.kind ?? "Unstated"}</td>
|
||||
<td style={td}>{concept.description}</td>
|
||||
</tr>)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>These initial examples cover design, finance and computing. The format accepts other domains and local concepts, including concepts with no taxonomy match.</p>
|
||||
</section>
|
||||
<section className="band">
|
||||
<div className="section-head"><h2>Add it to your profile</h2><p>Plain words work. Links add a shared meaning.</p></div>
|
||||
<pre style={pre}>{"## Skills\n\n- [Branding](https://logicsrc.com/openskill/branding)\n- [Logo design](https://logicsrc.com/openskill/logo-design)\n- Accounting\n- [Software engineer](https://logicsrc.com/openskill/software-engineer)"}</pre>
|
||||
<p>A profile entry is a claim. Experience, self-assessment and evidence stay with the person making it. An occupation label does not establish a credential or a license.</p>
|
||||
</section>
|
||||
<section className="band">
|
||||
<div className="section-head"><h2>Connected to existing knowledge</h2></div>
|
||||
<p><a href="https://esco.ec.europa.eu/en/use-esco/use-esco-services-api/esco-web-service-api">ESCO</a> provides linked skills, knowledge and occupations. <a href="https://www.onetcenter.org/content.html">O*NET</a> describes occupations and their requirements. OpenSkill records explicit mappings, while keeping the description small enough to read and carry on its own.</p>
|
||||
<p>These are descriptive capability records. Executable agent procedures retain their separate <code>SKILL.md</code> format. Reading a capability never installs a tool or grants it permission to act.</p>
|
||||
</section>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import type { MetadataRoute } from "next";
|
|||
import { publicClient } from "@/lib/supabase";
|
||||
import { DOC_SLUGS } from "@/lib/docs";
|
||||
import { FAMILIES, allSpecs } from "@/lib/specs";
|
||||
import { SKILL_SLUGS } from "@/lib/skills";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -76,5 +77,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||
postEntries = [];
|
||||
}
|
||||
|
||||
return [...staticEntries, ...docEntries, ...postEntries];
|
||||
const skills: MetadataRoute.Sitemap = SKILL_SLUGS.map((slug) => ({
|
||||
url: `${base}/openskill/${slug}`, changeFrequency: "monthly", priority: 0.6
|
||||
}));
|
||||
return [...staticEntries, ...docEntries, ...skills, ...postEntries];
|
||||
}
|
||||
|
|
|
|||
59
apps/logicsrc-web/src/lib/skills.ts
Normal file
59
apps/logicsrc-web/src/lib/skills.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { marked } from "marked";
|
||||
|
||||
// Public concepts only. Read at build time; request paths never select files.
|
||||
export const SKILL_SLUGS = [
|
||||
"branding", "logo-design", "graphic-design", "investing", "investor",
|
||||
"financial-advisor", "accounting", "accountant", "software-development",
|
||||
"developer", "software-engineer", "computer-science", "hardware-architectures",
|
||||
"hardware-architect"
|
||||
] as const;
|
||||
|
||||
const SKILLS_DIR = resolve(process.cwd(), "../../docs/openskill");
|
||||
|
||||
export type SkillSummary = {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
kind?: string;
|
||||
domain?: string;
|
||||
aliases?: string;
|
||||
};
|
||||
|
||||
export function readSkill(slug: string): string | null {
|
||||
if (!(SKILL_SLUGS as readonly string[]).includes(slug)) return null;
|
||||
return readFileSync(resolve(SKILLS_DIR, `${slug}.md`), "utf8");
|
||||
}
|
||||
|
||||
// A catalog view of the introduction, not a lossy replacement for the record.
|
||||
// Markdown tokenization keeps headings or bullets inside code fences inert.
|
||||
export function summarizeSkill(markdown: string, slug: string): SkillSummary {
|
||||
const result: SkillSummary = { slug, name: slug, description: "" };
|
||||
const tokens = marked.lexer(markdown);
|
||||
const start = tokens.findIndex((token) => token.type === "heading" && token.depth === 1);
|
||||
if (start < 0) return result;
|
||||
const title = tokens[start];
|
||||
if (title.type === "heading") result.name = title.text;
|
||||
let identity = true;
|
||||
for (const token of tokens.slice(start + 1)) {
|
||||
if (token.type === "heading") break;
|
||||
if (token.type === "space") continue;
|
||||
if (token.type === "list" && identity) {
|
||||
for (const item of token.items) {
|
||||
const match = /^(?:\*\*)?(Kind|Domain|Aliases)(?:\*\*)?:\s*(.+)$/i.exec(item.text);
|
||||
if (!match) continue;
|
||||
const key = match[1].toLowerCase() as "kind" | "domain" | "aliases";
|
||||
result[key] ??= match[2].trim();
|
||||
}
|
||||
} else {
|
||||
identity = false;
|
||||
if (token.type === "paragraph" && !result.description) result.description = token.text;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function listSkills(): SkillSummary[] {
|
||||
return SKILL_SLUGS.map((slug) => summarizeSkill(readSkill(slug)!, slug));
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ export const FAMILIES: Family[] = [
|
|||
"One Markdown file for a person or an agent, served from their own domain and linked from every platform that has a page for them. The profile carries the identity, the accounts and the topics; the sections carry what a platform needs to match on, so a job board, a booking site or a dating app reads the file instead of asking forty questions again.",
|
||||
specs: [
|
||||
s("openprofile", "OpenProfile.md", "One Markdown file for who you are and where you are, people and agents alike", { status: "0.2" }),
|
||||
s("openskill", "OpenSkill", "Portable descriptions of human skills, knowledge and occupations, linked from OpenProfile", { status: "draft" }),
|
||||
s("openagent", "OpenAgent", "A portable agent profile: identity, owner, skills and requested permissions", { status: "0.1" }),
|
||||
s("openwall", "OpenWall", "Consent-based broadcasts and direct messages across contact networks, with an AT Protocol mapping", { landing: undefined, status: "draft" }),
|
||||
s("openresume", "OpenResume.md", "What you have done, in the same spirit, linked from the profile", { landing: undefined }),
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ pre {
|
|||
|
||||
@media (max-width: 720px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.rail {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue