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,72 @@
import type { ErrorObject } from "ajv";
type MemberReference = { kind: "openagent" | "openswarm"; id: string };
// Called only after the JSON Schema has checked the shape. Keep the checks
// which need sibling values here; uniqueItems cannot enforce identity when
// two entries have different URLs or metadata.
type FleetReferences = {
members: MemberReference[];
rentals?: Array<{
id: string;
scope: { kind: "listing" } | { kind: "members"; members: MemberReference[] };
minimum_units?: number;
maximum_units?: number;
valid_from?: string;
valid_until?: string;
}>;
};
function memberKey(member: MemberReference): string {
return JSON.stringify([member.kind, member.id]);
}
export function validateOpenRentalReferences(data: unknown): ErrorObject[] {
const listing = data as FleetReferences;
const errors: ErrorObject[] = [];
function report(keyword: string, instancePath: string, message: string) {
errors.push({ keyword, instancePath, schemaPath: "#/openrental-semantics", params: {}, message });
}
const members = new Set<string>();
listing.members.forEach((member, i) => {
const key = memberKey(member);
if (members.has(key)) {
report("uniqueMember", `/members/${i}`, "duplicates an existing member kind and id");
}
members.add(key);
});
const rentals = new Set<string>();
listing.rentals?.forEach((rental, i) => {
const path = `/rentals/${i}`;
if (rentals.has(rental.id)) {
report("uniqueRental", `${path}/id`, "duplicates an existing rental id");
}
rentals.add(rental.id);
if (rental.scope.kind === "members") {
const scopedMembers = new Set<string>();
rental.scope.members.forEach((member, j) => {
const key = memberKey(member);
const memberPath = `${path}/scope/members/${j}`;
if (!members.has(key)) {
report("memberReference", memberPath, "must reference a member of this listing by kind and id");
}
if (scopedMembers.has(key)) {
report("uniqueMember", memberPath, "duplicates a member in this rental scope");
}
scopedMembers.add(key);
});
}
if (rental.maximum_units !== undefined && rental.maximum_units < (rental.minimum_units ?? 1)) {
report("rentalUnits", `${path}/maximum_units`, "must be at least minimum_units");
}
if (rental.valid_from && rental.valid_until && Date.parse(rental.valid_from) >= Date.parse(rental.valid_until)) {
report("rentalPeriod", `${path}/valid_until`, "must be later than valid_from");
}
});
return errors;
}