Site IA: broad sidebar, four spec families, one registry (#169)

Anthony: "that site needs better information architecture, it's impossible
to find anything", "start broad in sidebar and drill down with dedicated
pages, not all one page", and "I see none of our specs" on the home page.

One registry, lib/specs.ts, now lists every specification in four
families (people and agents; access and credentials; catalogs a site
serves about itself; agents and process), with a landing path, a
specification path and, for OpenServer's blocks, a parent. Everything
that lists specs reads it: the sidebar (lib/nav.ts, four groups: Start,
Specs, Tools, Company, rendered by SiteShell and by the home page string
from the same array), /specs and /specs/<family>, the home page's
Standards Surface grid (families with their specs, replacing the five
abstract primitives), /docs (grouped by family, then guides), the sitemap
and llms.txt. DOC_SLUGS is derived from the registry. Adding a spec is
one entry plus its files; the four hand-kept lists are gone.


Claude-Session: https://claude.ai/code/session_014cmNRtR2vL1p89dbVQ7FZJ

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-12 20:38:28 -07:00 • committed by GitHub
parent a005d7c716
commit eee9ce09c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 553 additions and 219 deletions

View file

@ -1,54 +1,20 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { docSlugs } from "./specs";
// Repo-root docs/ (read at build time during static generation, so there is
// no runtime filesystem dependency in the deployed image).
const DOCS_DIR = resolve(process.cwd(), "../../docs");
// Curated, public-facing reference docs. Internal notes (roadmap, positioning,
// arcade) are intentionally excluded.
export const DOC_SLUGS = [
"asdlc",
"openswarm",
"opencreds",
"openprd",
"openontology",
"openontology-governance",
"openontology-interoperability",
"openjob",
"openresume",
"openprofile",
"openbroadcast",
"openguest",
"openmcp",
"openaccess",
"openserver",
"openthreat",
"openfile",
"opendisk",
"opencoupon",
"openrecipe",
"openaffiliate",
"openstream",
"opencpu",
"openmemory",
"opengpu",
"openbandwidth",
"openspec-comparison",
"data-model",
"cli",
"tui",
"config",
"permissions",
"plugins",
"credential-sharing",
"agent-screening",
] as const;
// The public docs: every spec in lib/specs.ts that has a specification text,
// then the guides listed there. Internal notes (roadmap, positioning, arcade)
// are not in the registry and so are not served.
export const DOC_SLUGS: readonly string[] = docSlugs();
export type DocSlug = (typeof DOC_SLUGS)[number];
export type DocSlug = string;
export function isDocSlug(slug: string): slug is DocSlug {
return (DOC_SLUGS as readonly string[]).includes(slug);
return DOC_SLUGS.includes(slug);
}
export function readDoc(slug: string): string | null {

View file

@ -0,0 +1,61 @@
import { FAMILIES } from "./specs";
/**
* The sidebar, broad first. Four groups: where to start, the spec families
* (each a page that drills down to its specs), the tools, and the company.
* Rendered by the React SiteShell and by the server-string home page from the
* same array, so the two cannot drift.
*/
export type NavItem = { href: string; label: string; external?: boolean };
export type NavGroup = { label: string; items: NavItem[] };
export const NAV_GROUPS: NavGroup[] = [
{
label: "Start",
items: [
{ href: "/", label: "Overview" },
{ href: "/specs", label: "Specs" },
{ href: "/docs", label: "Docs" },
{ href: "/blog", label: "Blog" }
]
},
{
label: "Specs",
items: FAMILIES.map((f) => ({ href: `/specs/${f.slug}`, label: f.name }))
},
{
label: "Tools",
items: [
{ href: "/docs/cli", label: "CLI" },
{ href: "/openspec", label: "OpenSpec.dev mode" },
{ href: "/credential-sharing", label: "Credentials" }
]
},
{
label: "Company",
items: [
{ href: "/pricing", label: "Pricing" },
{ href: "/hire-us", label: "Hire Us" },
{ href: "/about", label: "About" },
{ href: "https://github.com/profullstack/logicsrc", label: "GitHub ↗", external: true },
{ href: "/terms", label: "Terms" },
{ href: "/privacy", label: "Privacy" }
]
}
];
/** The sidebar as a server-rendered string, for the home page template. */
export function renderNavHtml(activeHref = "/"): string {
return NAV_GROUPS.map(
(g) =>
`<span class="nav-group">${g.label}</span>` +
g.items
.map(
(i) =>
`<a href="${i.href}"${i.href === activeHref ? ' class="active" aria-current="page"' : ""}${
i.external ? ' target="_blank" rel="noreferrer"' : ""
}>${i.label}</a>`
)
.join("")
).join("");
}

View file

@ -4,14 +4,8 @@
// browser. Interactivity (hire-us form, CoinPay button, section scroll) lives in
// the `home-interactivity` client component.
import { renderInstallCommand } from "./install-command";
const primitives = [
{ name: "Identity", detail: "DIDs, OAuth accounts, profiles, and organization membership." },
{ name: "Coordination", detail: "Boards, posts, threads, comments, tasks, bids, and submissions." },
{ name: "Agents", detail: "Agent profiles, capabilities, runs, logs, permissions, and audit trails." },
{ name: "Value", detail: "Payments, escrow, wallets, reputation events, and settlement hooks." },
{ name: "Events", detail: "Event streams, webhooks, schema versions, and integration audit logs." }
];
import { renderNavHtml } from "./nav";
import { FAMILIES, familyTree } from "./specs";
const schemas = [
{ name: "logicsrc-task", path: "packages/schemas/schemas/logicsrc-task.schema.json" },
@ -128,26 +122,7 @@ export function renderPageMarkup(): string {
</div>
</div>
${renderInstallCommand("rail")}
<nav aria-label="LogicSRC sections">
<a class="active" href="#overview">Overview</a>
<a href="#schemas">Schemas</a>
<a href="/agent-swarm">Soon</a>
<a href="/agentbyte">AgentByte</a>
<a href="/credential-sharing">Credentials</a>
<a href="/openontology">OpenOntology</a>
<a href="/openprd">OpenPRD</a>
<a href="#cli">CLI</a>
<a href="/docs">Docs</a>
<a href="/blog">Blog</a>
<a href="/openspec">OpenSpec</a>
<a href="/pricing">Pricing</a>
<a href="/hire-us">Hire Us</a>
<a href="/about">About</a>
<a href="https://github.com/profullstack/logicsrc" target="_blank" rel="noreferrer">GitHub ↗</a>
<a href="/terms">Terms</a>
<a href="/privacy">Privacy</a>
<a href="#reference">Reference</a>
</nav>
<nav aria-label="LogicSRC sections">${renderNavHtml("/")}</nav>
</aside>
<section class="workspace">
<header id="overview" class="hero">
@ -170,16 +145,18 @@ export function renderPageMarkup(): string {
<section class="band">
<div class="section-head">
<h2>Standards Surface</h2>
<p>LogicSRC defines the shared language; products can implement it without owning the standard.</p>
<p>Four families of specifications. LogicSRC defines the shared language; products implement it without owning the standard.</p>
</div>
<div class="primitive-grid">
${primitives.map((item) => `
${FAMILIES.map((family) => `
<article class="tile">
<h3>${item.name}</h3>
<p>${item.detail}</p>
<h3><a href="/specs/${family.slug}">${family.name}</a></h3>
<p>${family.line}.</p>
<p class="tile-specs">${familyTree(family).map(({ spec }) => `<a href="${spec.landing ?? spec.doc ?? "/specs"}">${spec.name}</a>`).join(" · ")}</p>
</article>
`).join("")}
</div>
<p class="section-foot"><a href="/specs">Every spec, by family →</a></p>
</section>
<section id="agent-swarm" class="band coming-soon">

View file

@ -0,0 +1,150 @@
/**
* The one registry of LogicSRC specifications.
*
* Everything that lists specs reads this file: the sidebar, /specs and its
* family pages, the home page's family grid, /docs, the sitemap and llms.txt.
* Adding a spec is one entry here plus its docs/<slug>.md and, if it has one,
* its app/<slug>/page.tsx. There is no second list to keep in step.
*
* The site starts broad in the sidebar (four families) and drills down:
* /specs/<family> lists the family's specs, /<slug> is a spec's landing page,
* /docs/<slug> is the specification text.
*/
export type Spec = {
slug: string;
name: string;
/** One sentence, no trailing period; shown on lists. */
line: string;
/** Landing page path when one exists (usually /<slug>). */
landing?: string;
/** Specification text path when one exists (usually /docs/<slug>). */
doc?: string;
/** A block of a larger spec, listed under it. */
parent?: string;
status?: "0.1" | "0.2" | "draft" | "soon";
};
export type Family = {
slug: string;
name: string;
/** One line under the family name. */
line: string;
/** A short paragraph on the family page. */
blurb: string;
specs: Spec[];
};
const s = (
slug: string,
name: string,
line: string,
extra: Partial<Spec> = {}
): Spec => ({ slug, name, line, landing: `/${slug}`, doc: `/docs/${slug}`, ...extra });
export const FAMILIES: Family[] = [
{
slug: "people",
name: "People and agents",
line: "Who someone is, what they have done, and what they offer, in files they own",
blurb:
"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("openresume", "OpenResume.md", "What you have done, in the same spirit, linked from the profile", { landing: undefined }),
s("openjob", "OpenJob", "What the work is, so a candidate's agent and a job board agree", { landing: undefined }),
s("openbroadcast", "OpenBroadcast", "The Broadcast section: the show a person hosts and who they are seeking"),
s("openguest", "OpenGuest", "The Guest section: that a person will appear, their expertise, availability and terms"),
s("agentbyte", "AgentByte", "Agent screening sessions, policy events and APIs", { doc: "/docs/agent-screening", status: "draft" })
]
},
{
slug: "access",
name: "Access and credentials",
line: "Grants you can carry, and the vault the tokens live in",
blurb:
"OAuth 2.1 with a grant you can carry between apps, a portable vault format for the credentials behind an agent's accounts, and the sync architecture that moves team secrets between the places they are kept.",
specs: [
s("openaccess", "OpenAccess", "OAuth 2.1 with a grant you can carry: one hub account, apps keep their own users, entitlements travel"),
s("opencreds", "OpenCreds", "A portable vault for the credentials behind an agent's accounts"),
s("credential-sharing", "Credential Sharing", "End-to-end-encrypted team vaults with source and target diffs, approval, sync, rollback and audit")
]
},
{
slug: "catalogs",
name: "Catalogs a site serves about itself",
line: "One file at a fixed URL, read by directories instead of scraped",
blurb:
"A provider, a merchant, a scanner or a relay already keeps a table of what it sells or found. Each of these is that table, exported at /.well-known/<slug>.json in a shape every reader agrees on, verified by the origin it came from. Directories such as nichedb.dev read the file; the publisher stays the author.",
specs: [
s("openserver", "OpenServer", "One file a hosting provider serves about what it sells: every offer, its specs, price, location and stock"),
s("opencpu", "OpenCPU", "The compute block: threads against cores, allocation, and a range for what a buyer can dial", { parent: "openserver" }),
s("openmemory", "OpenMemory", "The memory block: mebibytes, DDR type, ECC as three states, allocation", { parent: "openserver" }),
s("opendisk", "OpenDisk", "The disk a machine will rent: free GiB, price per GiB-month, location, policy", { parent: "openserver" }),
s("opengpu", "OpenGPU", "The gpu block: the card by vendor name, count, VRAM, interconnect, access", { parent: "openserver" }),
s("openbandwidth", "OpenBandwidth", "The network block: port, meter, overage, IPv4 and IPv6 as a priced resource", { parent: "openserver" }),
s("openfile", "OpenFile", "One file a publisher serves about the files it has published: hash, swarm and HTTP routes", { parent: "openserver" }),
s("openmcp", "OpenMCP", "An open catalog of MCP relays: a relay serves /.well-known/openmcp.json and a catalog probes it"),
s("opencoupon", "OpenCoupon", "One file a merchant serves about what is on offer right now, expired codes kept so directories learn they died"),
s("openaffiliate", "OpenAffiliate", "One file a merchant serves about the commission it pays"),
s("openrecipe", "OpenRecipe.md", "One Markdown file that is a recipe, with schema.org derived from it and never the reverse"),
s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located")
]
},
{
slug: "process",
name: "Agents and process",
line: "How agents coordinate, settle, stream, and how the software that serves them gets built",
blurb:
"The lifecycle for building software when agents work in parallel and CI is the only gate, the requirement document an agent can execute, the settlement and proof layer under a swarm, a lossless byte-stream envelope, and the five nouns a shared ontology needs.",
specs: [
s("asdlc", "ASDLC", "The Agentic Software Development Lifecycle: nine phases, four conformance levels and the ratchet rule"),
s("openprd", "OpenPRD", "A product requirement document an agent can execute and a person can read"),
s("openswarm", "OpenSwarm", "Settlement and proof of work done under a peer-to-peer swarm"),
s("openstream", "OpenStream", "A lossless byte-stream relay envelope, with benchmark reports per release", { landing: undefined }),
s("openontology", "OpenOntology", "Five nouns for a shared ontology, with governance and interoperability notes"),
s("agent-swarm", "AgentSwarm", "Provider-neutral agent orchestration, model routing and cost controls", { doc: undefined, status: "soon" }),
s("openspec", "OpenSpec.dev comparison", "How LogicSRC compares with OpenSpec.dev, and the compatibility mode", { doc: "/docs/openspec-comparison" })
]
}
];
/** Guides that are documentation rather than a specification. Listed on /docs under their own heading. */
export const GUIDES: Array<{ slug: string; name: string }> = [
{ slug: "data-model", name: "Data model" },
{ slug: "cli", name: "CLI" },
{ slug: "tui", name: "TUI" },
{ slug: "config", name: "Config" },
{ slug: "permissions", name: "Permissions" },
{ slug: "plugins", name: "Plugins" },
{ slug: "openontology-governance", name: "OpenOntology governance" },
{ slug: "openontology-interoperability", name: "OpenOntology interoperability" }
];
export function allSpecs(): Spec[] {
return FAMILIES.flatMap((f) => f.specs);
}
export function familyBySlug(slug: string): Family | undefined {
return FAMILIES.find((f) => f.slug === slug);
}
export function familyOfSpec(slug: string): Family | undefined {
return FAMILIES.find((f) => f.specs.some((x) => x.slug === slug));
}
/** The docs/<slug>.md files served at /docs/<slug>: every spec that has one, then the guides. */
export function docSlugs(): string[] {
const fromSpecs = allSpecs()
.map((x) => x.doc)
.filter((d): d is string => Boolean(d))
.map((d) => d.replace(/^\/docs\//, ""));
return Array.from(new Set([...fromSpecs, ...GUIDES.map((g) => g.slug)]));
}
/** Top-level specs of a family, each with the blocks that nest under it. */
export function familyTree(family: Family): Array<{ spec: Spec; children: Spec[] }> {
return family.specs
.filter((x) => !x.parent)
.map((spec) => ({ spec, children: family.specs.filter((c) => c.parent === spec.slug) }));
}