Merge master; OpenFleet keeps its name, PR 177's rental descriptor becomes OpenRental

PR 177 shipped a second OpenFleet under the same slug: a published listing of OpenAgent profiles and ipfile swarms with CoinPay rental offers. Anthony ruled that OpenFleet means the human-controlled fleet and the record of who spawned whom, so the rental contract is renamed OpenRental (slug openrental, catalogs family, group noun listing): docs/openrental.md, logicsrc-openrental.schema.json with type logicsrc.openrental and scope kind listing, fixtures/openrental, createOpenRental and OpenRental* types in the SDK, the openrental validator kind. Minor bumps because an export moved: @logicsrc/schemas 0.2.0, @logicsrc/validators 0.2.0, @logicsrc/sdk 0.2.0. The discovery contract test now covers openfleet, openrental and openwall, one per family, and accepts a landing-page link in llms.txt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01RZV4zJ2pDZLNN3kE5jFCmV
This commit is contained in:
Anthony Ettinger 2026-09-13 08:45:33 +00:00
commit 68f3151699
35 changed files with 2068 additions and 14 deletions

View file

@ -0,0 +1,31 @@
import { describe, expect, it, vi } from "vitest";
import { familyOfSpec } from "../src/lib/specs";
import { listDocs, readDoc } from "../src/lib/docs";
import { GET as llms } from "../src/app/llms.txt/route";
import { GET as llmsFull } from "../src/app/llms-full.txt/route";
import sitemap from "../src/app/sitemap";
// Spec discovery must work even when the optional blog database is offline.
vi.mock("../src/lib/supabase", () => ({ publicClient: () => { throw new Error("offline"); } }));
describe.each([
{ slug: "openfleet", name: "OpenFleet", family: "process" },
{ slug: "openrental", name: "OpenRental", family: "catalogs" },
{ slug: "openwall", name: "OpenWall", family: "people" }
])("$name public discovery", ({ slug, name, family }) => {
it("serves the specification through its family and docs index", () => {
expect(familyOfSpec(slug)?.slug).toBe(family);
expect(listDocs()).toContainEqual(expect.objectContaining({ slug, title: name }));
expect(readDoc(slug)).toMatch(/Status: \*{0,2}0\.1/);
});
it("includes a reachable docs URL and the full contract in the LLM feeds", async () => {
expect(await llms().text()).toMatch(new RegExp(`\\[${name}\\]\\(https://[^)]+/docs/${slug}\\)`));
expect(await llmsFull().text()).toContain(readDoc(slug)!.trim());
});
it("includes the docs route in the sitemap without a blog connection", async () => {
const entries = await sitemap();
expect(entries.some((entry) => new URL(entry.url).pathname === `/docs/${slug}`)).toBe(true);
});
});

View file

@ -134,7 +134,21 @@ do_install() {
ok "downloaded${short_sha:+ ($short_sha)}"
info "installing dependencies (this can take a minute)…"
( cd "$STAGE" && npm install --no-audit --no-fund --ignore-scripts ) >"$BUILD_LOG" 2>&1 \
# Git dependency preparation can break under a newer system npm even with
# --ignore-scripts. Use the repo's tested npm where its Node engine permits
# it, while retaining the documented older-Node CLI installation path.
( cd "$STAGE" &&
npm_spec="" &&
# Older tags predate the selector; preserve their install behavior too.
if [ -f scripts/install-npm.cjs ]; then
npm_spec="$(node scripts/install-npm.cjs)" || exit 1
fi
if [ -n "$npm_spec" ]; then
npm exec --yes --package="$npm_spec" -- npm install --no-audit --no-fund --ignore-scripts
else
npm install --no-audit --no-fund --ignore-scripts
fi
) >"$BUILD_LOG" 2>&1 \
|| step_fail "npm install failed"
info "building the CLI…"
( cd "$STAGE" && npm run build:cli ) >>"$BUILD_LOG" 2>&1 \

View file

@ -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("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 }),
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"),
@ -91,6 +92,7 @@ export const FAMILIES: Family[] = [
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"),
s("openrental", "OpenRental", "One file an operator serves about the agents and file swarms it rents out: members, metadata and rates through CoinPay", { landing: undefined, status: "draft" }),
s("opensite", "OpenSite", "One record about a page or a site: the card a reader would draw, declared by the site or read from it, kept by an index")
]
},