diff --git a/packages/amico-run/src/fleet_projection_verb.ts b/packages/amico-run/src/fleet_projection_verb.ts new file mode 100644 index 000000000..66b76dd7f --- /dev/null +++ b/packages/amico-run/src/fleet_projection_verb.ts @@ -0,0 +1,260 @@ +// `amico fleet status --projection` (#1068, fleet rearchitect P3b-1) — the +// fleet-AUTHORITY status: the entitlement-gated read of amicissimo's +// published, provenance-stamped projection (spec spec-20260913-114814 §3 D1, +// countermeasure row 1). The session-registry `status --session ` keeps +// its pinned contract; `--projection` routes here. +// +// The three properties this module exists to enforce: +// 1. ONE PARSER PATH. The projection is validated + rendered by +// @amicode/schema's fleet_projection reader (contract v1) — this verb +// never parses topology/health/locks itself. A stale/future/absent +// contract version surfaces the reader's LOUD rejection verbatim +// (invariant 5: one path, versioned — the hub rejects stale contract +// versions loudly, and so does the client read). +// 2. THE INVOCATION SEAM. The publisher is a SUBPROCESS BOUNDARY: +// `python3 -m fleet_authority publish --out ` with cwd = the +// resolved amicissimo checkout (amicissimo#414, the companion entry +// point — possibly unmerged at the time this lands, so the seam is a +// typed, injectable interface; the default impl spawns exactly the +// pinned command line and the tests mock it with fixture projections). +// 3. THE BOOTSTRAP EXCEPTION. Absent entitlement or absent checkout is +// base-standalone HONESTLY STATED with a pointer to the grant path, +// exiting FLEET_BOOTSTRAP_EXIT (75) — distinct from success (a silent +// no), from usage (64, the user's mistake), and from a stack trace +// (never). The mode field is untouched: stating base-standalone is a +// floor report, never a mode-machine write (spec invariant 7). +// +// Entitlement + checkout resolution follow `amico premium`'s machinery +// precedent (src/premium.ts — PREMIUM_CODE "amicissimo", the AMICISSIMO_ROOT +// ladder): the same codes file, the same ladder, the same funnel invariant — +// a not-granted machine loses nothing, it is told what it is. +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import { mkdtempSync, rmSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import * as path from "node:path"; +import { + FleetContractVersionError, + freshnessAdvisory, + freshnessBetween, + readProjection, + renderFleetStatus, + type FleetProjection, +} from "@amicode/schema"; +import { PREMIUM_CODE, readCodes } from "./premium.js"; +import type { VerbResult } from "./verbs.js"; + +/** The bootstrap exception's exit code. 75: deliberately distinct from 0 + * (success — a silent no would lie), 64 (usage — this is not the user's + * mistake), and 1 (an unexpected failure — this is an expected, honest + * state). The P3b-2 consumers (installer, guard) branch on it. */ +export const FLEET_BOOTSTRAP_EXIT = 75; + +/** The subprocess boundary's typed record — what the default impl spawns and + * what the tests mock. `program` + `args` is the full command line; `cwd` is + * the resolved amicissimo checkout (so `-m fleet_authority` resolves against + * the checkout's package); `outPath` is where the projection must land. */ +export interface PublisherInvocation { + program: string; + args: string[]; + cwd: string; + outPath: string; +} + +export interface PublisherResult { + code: number; + stdout: string; + stderr: string; +} + +export interface FleetProjectionDeps { + checkDir?: (p: string) => boolean; + readFile?: (p: string) => string | null; + /** THE invocation seam (injectable): the publisher subprocess call. */ + runPublisher?: (inv: PublisherInvocation) => PublisherResult; +} + +function flagValue(argv: string[], name: string): string | undefined { + const i = argv.indexOf(name); + return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; +} + +/** Parse the projection-status flags: `--projection` (the routing marker, + * tolerated wherever it appears), `--checkout `, `--config `, + * `--previous `. Anything else is a usage error naming the + * offender — never silently ignored. */ +function parseFlags(argv: string[]): { ok: true; flags: { checkout?: string; config?: string; previous?: string } } | { ok: false; errors: string[] } { + const flags: { checkout?: string; config?: string; previous?: string } = {}; + const takesValue = new Set(["--checkout", "--config", "--previous"]); + for (let i = 0; i < argv.length; i++) { + const tok = argv[i]; + if (tok === "--projection") continue; + if (tok.startsWith("--")) { + if (!takesValue.has(tok)) return { ok: false, errors: [`unknown flag ${tok} — the projection status accepts --checkout, --config, --previous (and the routing marker --projection)`] }; + const v = argv[i + 1]; + if (v === undefined) return { ok: false, errors: [`${tok} requires a value`] }; + flags[tok.slice(2) as "checkout" | "config" | "previous"] = v; + i++; + continue; + } + return { ok: false, errors: [`unexpected positional argument ${tok}`] }; + } + return { ok: true, flags }; +} + +/** The default publisher subprocess — THE invocation seam's production impl. + * Exactly the pinned command line, nothing else: `python3 -m fleet_authority + * publish --out ` in the checkout's cwd (the #414 entry point). */ +function defaultRunPublisher(inv: PublisherInvocation): PublisherResult { + const r = spawnSync(inv.program, inv.args, { cwd: inv.cwd, encoding: "utf8" }); + return { code: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" }; +} + +function fail(errors: string[], extra: Record = {}): VerbResult { + return { json: { verb: "fleet", subcommand: "status", projection: true, ok: false, errors, ...extra }, code: 64 }; +} + +/** The bootstrap exception — base-standalone honestly stated, never a mode + * write, never a crash, exit FLEET_BOOTSTRAP_EXIT. */ +function bootstrap(reason: "entitlement" | "checkout", rendered: string, extra: Record = {}): VerbResult { + return { + json: { + verb: "fleet", + subcommand: "status", + projection: true, + ok: false, + bootstrap: true, + reason, + mode: "standalone", + rendered, + note: "bootstrap exception — base-standalone stated, not written: the mode field is untouched (spec invariant 7); grant the entitlement + provide the checkout to light the fleet surfaces", + ...extra, + }, + code: FLEET_BOOTSTRAP_EXIT, + }; +} + +/** A section's carried value, with the base default applied when the section + * is absent (mode absent = standalone, posture absent = ok — the projection + * contract's additive-optional discipline; the base default is applied, not + * invented: the reader's render states it in provenance). */ +function scalarOrBase(proj: FleetProjection, section: string, base: string): unknown { + const s = proj.sections?.[section]; + return s?.value === undefined ? base : s.value; +} + +/** `amico fleet status --projection` — resolve the checkout (the premium + * ladder), gate on the entitlement, invoke the publisher at the subprocess + * seam, read the result through the ONE fleet projection reader, and print + * the provenance-rendered status summary. Backs the CLI (amico.ts) and the + * MCP facade through the same fleetVerb router as the registry verbs. */ +export function fleetProjectionStatus(argv: string[], deps: FleetProjectionDeps = {}): VerbResult { + const parsed = parseFlags(argv); + if (!parsed.ok) return fail(parsed.errors); + + const configFile = + parsed.flags.config ?? path.join(homedir(), ".amico", "amicode", "entitlements.toml"); + const checkout = + parsed.flags.checkout ?? process.env.AMICISSIMO_ROOT ?? path.join(homedir(), "harmoniqs", "amicissimo"); + + // ── the entitlement gate (the premium machinery precedent) ── + const codes = readCodes(configFile, { readFile: deps.readFile }); + if (!codes.includes(PREMIUM_CODE)) { + const rendered = [ + `fleet status: base-standalone (bootstrap exception) — this install does not hold the \`${PREMIUM_CODE}\` entitlement code,`, + "so the fleet-authority surfaces are not staged for it (the base product works fully standalone).", + "", + "Grant path: repo access to harmoniqs/amicissimo + the code in:", + ` ${configFile}`, + "", + "(bootstrap exception, exit 75 — distinct from success and from usage; see `amico premium`)", + ].join("\n"); + return bootstrap("entitlement", rendered, { config: configFile }); + } + + // ── the checkout ladder (AMICISSIMO_ROOT → org-home default) ── + const existsDir = + deps.checkDir ?? ((p: string) => fs.existsSync(p) && fs.statSync(p).isDirectory()); + if (!existsDir(checkout)) { + const rendered = [ + `fleet status: base-standalone (bootstrap exception) — the \`${PREMIUM_CODE}\` entitlement is granted, but no amicissimo checkout is present at:`, + ` ${checkout}`, + "", + "Clone harmoniqs/amicissimo there, or set AMICISSIMO_ROOT, or pass --checkout .", + "", + "(bootstrap exception, exit 75 — distinct from success and from usage)", + ].join("\n"); + return bootstrap("checkout", rendered, { checkout }); + } + + // ── the invocation seam: publish, then read ── + const outDir = mkdtempSync(path.join(tmpdir(), "fleet-projection-")); + try { + const inv: PublisherInvocation = { + program: "python3", + args: ["-m", "fleet_authority", "publish", "--out", path.join(outDir, "projection.json")], + cwd: checkout, + outPath: path.join(outDir, "projection.json"), + }; + const result = deps.runPublisher ? deps.runPublisher(inv) : defaultRunPublisher(inv); + if (result.code !== 0) { + return fail( + [ + `the fleet-authority publisher failed (exit ${result.code}): ${result.stderr.trim() || "(no stderr)"}`, + `invoked \`${inv.program} ${inv.args.join(" ")}\` in ${checkout} — the amicissimo#414 entry point (python3 -m fleet_authority) may be absent from this checkout; nothing was read or rendered`, + ], + { checkout, out_path: inv.outPath }, + ); + } + + let previous: FleetProjection | null = null; + if (parsed.flags.previous !== undefined) { + try { + previous = readProjection(parsed.flags.previous); + } catch (e) { + return fail([`--previous ${parsed.flags.previous}: ${(e as Error).message}`], { checkout }); + } + } + + let proj: FleetProjection; + try { + proj = readProjection(inv.outPath); + } catch (e) { + // The reader's LOUD rejection surfaces verbatim — a versioned contract + // refuses both directions, naming both versions (invariant 5). + const message = + e instanceof FleetContractVersionError + ? `${e.message} (projection published to ${inv.outPath} speaks a contract this CLI does not)` + : `the published projection at ${inv.outPath} failed the contract read: ${(e as Error).message}`; + return fail([message], { checkout, out_path: inv.outPath }); + } + + const verdict = previous === null ? null : freshnessBetween(previous, proj); + const advisory = verdict === null ? "" : freshnessAdvisory(verdict); + const fresh = proj.freshness ?? {}; + return { + json: { + verb: "fleet", + subcommand: "status", + projection: true, + ok: true, + checkout, + mode: scalarOrBase(proj, "mode", "standalone"), + posture: scalarOrBase(proj, "posture", "ok"), + publisher: proj.publisher ?? {}, + sections: proj.sections ?? {}, + freshness: { + counter: fresh.counter, + hub_epoch: fresh.hub_epoch, + ...(verdict === null ? {} : { verdict, advisory: advisory === "" ? undefined : advisory }), + }, + summary: renderFleetStatus(proj, previous), + note: "read through the ONE fleet projection reader (@amicode/schema fleet_projection, contract v" + + String(proj.contract_version) + ") — amicissimo parses and publishes, amicode consumes (spec §3 D1); provenance renders beside the data, never merged", + }, + code: 0, + }; + } finally { + rmSync(outDir, { recursive: true, force: true }); + } +} diff --git a/packages/amico-run/src/fleet_verb.ts b/packages/amico-run/src/fleet_verb.ts index 3067f2864..ff44cb88b 100644 --- a/packages/amico-run/src/fleet_verb.ts +++ b/packages/amico-run/src/fleet_verb.ts @@ -48,6 +48,11 @@ // construction (amico.ts prints `VerbResult.json`), exactly like the other spine verbs. import { FRONTIER_MODELS, ladderRungs } from "./ledger_dispatch.js"; import { fleetDigest } from "./fleet_digest.js"; +// The fleet-authority projection status (#1068, rearchitect P3b-1): the same +// `amico fleet status` verb, one more read path — `--projection` routes to the +// entitlement-gated publisher invocation + the shared reader, while the +// pinned `--session ` registry contract stays byte-identical. +import { fleetProjectionStatus, type FleetProjectionDeps } from "./fleet_projection_verb.js"; import { applyEvent, enqueueSignal, @@ -580,19 +585,31 @@ export function fleetSweep(argv: string[]): VerbResult { // ── subcommand router ──────────────────────────────────────────────────────────── const USAGE = "amico fleet list [--state ] [--root D] | amico fleet status --session | " + + "amico fleet status --projection [--checkout D] [--config F] [--previous ] | " + 'amico fleet steer --session --message "" | amico fleet stop --session [--reason ""] | ' + "amico fleet re-tier --session --model [--variant ] | amico fleet sweep [--dry-run] | " + "amico fleet launch --session --pid | " + 'amico fleet finish --session --outcome settled|crashed --pid [--step ""] | ' + "amico fleet digest [--post ] [--machines a,b] [--jobs-line \"\"] [--dry-run] [--root D]"; +/** Optional injection surface for the fleet verb's sub-verbs — the projection + * status's hermetic seam (publisher subprocess, entitlement file, checkout + * probe). Existing callers (amico.ts, mcp_serve.ts) pass nothing and get the + * production defaults. */ +export interface FleetVerbDeps { + projection?: FleetProjectionDeps; +} + /** The `fleet` verb body: route on the subcommand. Backs BOTH the CLI (amico.ts) and the * MCP facade (mcp_serve.ts) — one impl, two transports. */ -export function fleetVerb(argv: string[]): VerbResult { +export function fleetVerb(argv: string[], deps: FleetVerbDeps = {}): VerbResult { const sub = argv[0]; const rest = argv.slice(1); if (sub === "list") return fleetList(rest); - if (sub === "status") return fleetStatus(rest); + if (sub === "status") { + if (rest.includes("--projection")) return fleetProjectionStatus(rest, deps.projection); + return fleetStatus(rest); + } if (sub === "steer") return fleetSteer(rest); if (sub === "stop") return fleetStop(rest); if (sub === "re-tier") return fleetRetier(rest); diff --git a/packages/amico-run/src/premium.ts b/packages/amico-run/src/premium.ts index d6cef1bac..3895853dd 100644 --- a/packages/amico-run/src/premium.ts +++ b/packages/amico-run/src/premium.ts @@ -43,7 +43,9 @@ export const PREMIUM_CODE = "amicissimo"; // replaces both). Minimal TOML: the v1 file is `codes = [...]` (+ `expired`). // An absent file = public-only, silently; a malformed file = no codes, silently // (an entitlement failure never dead-ends anything — the funnel invariant). -function readCodes(file: string, deps: PremiumDeps): string[] { +// EXPORTED for the fleet projection status verb (#1068): the entitlement gate +// is the same machinery — one codes reader, never a per-verb reimplementation. +export function readCodes(file: string, deps: PremiumDeps): string[] { const read = deps.readFile ?? ((p: string) => (fs.existsSync(p) ? fs.readFileSync(p, "utf8") : null)); const raw = read(file); if (raw === null) return []; diff --git a/packages/amico-run/src/verbs.ts b/packages/amico-run/src/verbs.ts index 3b7a55586..58fd476c6 100644 --- a/packages/amico-run/src/verbs.ts +++ b/packages/amico-run/src/verbs.ts @@ -141,6 +141,11 @@ const profile: Verb = { // `digest` is the fourth RENDERING (unified-fleet spec slice 1): it reads the registry, // probes configured machines, and posts the distilled block through the amico-slack // contract — a projection, never a second state machine. +// `status --projection` is the fleet-AUTHORITY read (#1068, rearchitect P3b-1): +// entitlement-gated, it invokes amicissimo's publisher (`python3 -m fleet_authority`) +// and renders the projection through the ONE shared reader — the registry verbs and +// the authority read are deliberately one verb surface, two read paths, no second +// topology parser anywhere. // // Deliberate contrast with `ledger` above: the ledger is an append-only immutable JSONL // event log; this registry is mutable per-session TOML state. They share record I/O @@ -148,7 +153,7 @@ const profile: Verb = { const fleet: Verb = { name: "fleet", summary: - "fleet registry: list/status read verbs, steer/stop/re-tier as signal enqueuers (never a record write), sweep with a pid-liveness guard, digest as the Slack projection", + "fleet registry: list/status read verbs, steer/stop/re-tier as signal enqueuers (never a record write), sweep with a pid-liveness guard, digest as the Slack projection; status --projection reads the fleet-authority projection (entitlement-gated, #1068)", generalizes: "the fleet view + in-chat /fleet + Amico's conversational fleet questions + the Slack digest, over ~/.amico/ops/fleet", slice: "fleet substrate (§9 step 2)", run: fleetVerb, diff --git a/packages/amico-run/test/fixtures/fleet_authority/projection.json b/packages/amico-run/test/fixtures/fleet_authority/projection.json new file mode 100644 index 000000000..bfc99fe50 --- /dev/null +++ b/packages/amico-run/test/fixtures/fleet_authority/projection.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "contract_version": 1, + "publisher": { "identity": "test-publisher", "published_at": "2026-09-13T12:00:00Z" }, + "freshness": { "counter": 4, "hub_epoch": "33333333-3333-4333-8333-333333333333" }, + "sections": { + "mode": { + "value": "fleet", + "provenance": { "source": "fleet.json", "parsed_from": "role='client' (vocabulary mapping)" } + }, + "posture": { + "value": "ok", + "provenance": { "source": "fleet-status.json", "parsed_from": "base default (posture absent = ok)" } + }, + "topology": { + "value": { + "role": "client", + "canonical": { "host": "hq-hub-01.example.internal", "port": 4096, "sshAlias": "hq-hub-01" }, + "previousBinary": "/home/example/.amico/server/bin/opencode", + "previousPort": 4096 + }, + "provenance": { + "source": "fleet.json", + "parsed_from": "topology schema v1 fields: role, canonical, previousBinary, previousPort" + } + }, + "health": { + "value": { + "collected_at": "2026-08-30T22:10:04Z", + "devices": [ + { "name": "atom-01", "reachable": true, "detail": "this machine" }, + { "name": "ion-02", "reachable": false, "detail": "ssh failed" }, + { "name": "hub-03", "reachable": true, "detail": "ssh ok" } + ], + "chat_db": { "sessions": null, "last_session": "null", "server_http": "200" }, + "server_guard": { "ok": false, "pid": "", "db_file": "none", "served_sessions": 100, "notes": "server not running;" }, + "vault_sync": { "age_minutes": 0, "clean": true }, + "repos": [ + { "name": "lab-notebook", "branch": "main", "dirty": 12, "ahead": 0, "behind": 3, "wip_branches": "wip/alpha" } + ] + }, + "provenance": { + "source": "fleet-status.json", + "parsed_from": "fields: chat_db, collected_at, devices[3], repos[1], server_guard, vault_sync" + } + }, + "locks": { + "value": { "rows": [{ "session": "ses_0183f2", "holder": "atom-01", "leased_until": "2026-08-30T23:10:04Z" }] }, + "provenance": { "source": "hub lock state", "parsed_from": "rendered from hub lock state: 1 row(s) — a rendering, never the enforcement" } + } + } +} diff --git a/packages/amico-run/test/fleet_projection_verb.test.ts b/packages/amico-run/test/fleet_projection_verb.test.ts new file mode 100644 index 000000000..ee4153ff4 --- /dev/null +++ b/packages/amico-run/test/fleet_projection_verb.test.ts @@ -0,0 +1,227 @@ +// fleet_projection_verb.test.ts — `amico fleet status --projection` (#1068, +// fleet rearchitect P3b-1): the fleet-authority half of the `amico fleet` +// surface. The session-registry `status --session ` keeps its pinned +// behavior; `--projection` routes to the entitlement-gated projection status. +// +// The three properties this suite exists to defend: +// 1. THE INVOCATION SEAM IS EXPLICIT AND INJECTABLE. The publisher is a +// subprocess boundary — `python3 -m fleet_authority publish --out

` +// with cwd = the amicissimo checkout (amicissimo#414, the companion +// entry point, possibly unmerged). The seam is a typed interface the +// tests mock; the default impl spawns exactly the pinned command line. +// 2. THE BOOTSTRAP EXCEPTION IS HONEST AND DISTINCT. Absent entitlement or +// absent checkout states base-standalone with a pointer to the grant +// path and exits with FLEET_BOOTSTRAP_EXIT (75) — never a stack trace, +// never exit 0 (a silent no), never 64 (a usage error the user made). +// 3. THE OUTPUT COMES FROM THE READER, NEVER A SECOND PARSER. The +// projection is validated + rendered by @amicode/schema's +// fleet_projection reader; a stale contract version from the publisher +// surfaces the reader's LOUD rejection verbatim (both versions named). +// +// Run: pnpm --filter @amicode/amico-run test fleet_projection_verb +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fleetVerb } from "../src/fleet_verb.js"; +import { fleetProjectionStatus, FLEET_BOOTSTRAP_EXIT, type FleetProjectionDeps } from "../src/fleet_projection_verb.js"; + +// The committed fixture projection — a full document shaped on amicissimo's +// Python publisher fixtures (client role → fleet mode, health + locks present). +const FIXTURE = new URL("./fixtures/fleet_authority/projection.json", import.meta.url); + +let tmp: string; +beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), "fleet-proj-verb-")); +}); +afterEach(() => rmSync(tmp, { recursive: true, force: true })); + +/** A hermetic world: an entitlements file carrying the `amicissimo` code, a + * checkout dir, and a runPublisher that copies the fixture projection to the + * outPath the verb handed it — the real publisher's #414 contract, faked. */ +function grantedWorld(over: Partial = {}, fixture: string = FIXTURE.pathname) { + const entitlements = join(tmp, "entitlements.toml"); + writeFileSync(entitlements, 'codes = ["amicissimo"]\n'); + const checkout = join(tmp, "amicissimo"); + const calls: Array<{ program: string; args: string[]; cwd: string; outPath: string }> = []; + const deps: FleetProjectionDeps = { + readFile: (p) => (p === entitlements ? 'codes = ["amicissimo"]' : fixtureFileSafe(p, fixture)), + checkDir: (p) => p === checkout, + runPublisher: (inv) => { + calls.push(inv); + writeFileSync(inv.outPath, readFileSync(fixture, "utf8")); + return { code: 0, stdout: "", stderr: "" }; + }, + ...over, + }; + return { entitlements, checkout, calls, deps }; +} + +/** Only the fixture + the entitlements file exist on disk in these tests. */ +function fixtureFileSafe(p: string, _fixture: string): string | null { + return null; +} + +function run(argv: string[], deps: FleetProjectionDeps): { json: Record; code: number } { + return fleetProjectionStatus(["--projection", ...argv], deps) as unknown as { json: Record; code: number }; +} + +// ── the invocation seam: a subprocess boundary, pinned ───────────────────────── + +describe("the publisher invocation seam (python3 -m fleet_authority)", () => { + it("invokes exactly `python3 -m fleet_authority publish --out

` with cwd = the resolved checkout", () => { + const w = grantedWorld(); + const r = run(["--checkout", w.checkout, "--config", w.entitlements], w.deps); + expect(r.code).toBe(0); + expect(w.calls).toHaveLength(1); + expect(w.calls[0].program).toBe("python3"); + expect(w.calls[0].args.slice(0, 3)).toEqual(["-m", "fleet_authority", "publish"]); + expect(w.calls[0].args[3]).toBe("--out"); + expect(w.calls[0].outPath.endsWith(".json")).toBe(true); + expect(w.calls[0].cwd).toBe(w.checkout); + }); + + it("resolves the checkout through the AMICISSIMO_ROOT ladder (flag → env → org-home default)", () => { + const w = grantedWorld(); + const seen: string[] = []; + const deps: FleetProjectionDeps = { ...w.deps, checkDir: (p) => (seen.push(p), true) }; + const prev = process.env.AMICISSIMO_ROOT; + process.env.AMICISSIMO_ROOT = "/env/checkout"; + try { + run(["--config", w.entitlements], deps); // no --checkout → env ladder + expect(seen).toContain("/env/checkout"); + } finally { + if (prev === undefined) delete process.env.AMICISSIMO_ROOT; + else process.env.AMICISSIMO_ROOT = prev; + } + }); +}); + +// ── the status read: from the reader, one parser path ───────────────────────── + +describe("amico fleet status --projection (granted world)", () => { + it("publishes, reads through the shared reader, and prints the summary with per-section provenance", () => { + const w = grantedWorld(); + const r = run(["--checkout", w.checkout, "--config", w.entitlements], w.deps); + expect(r.code).toBe(0); + expect(r.json).toMatchObject({ verb: "fleet", subcommand: "status", ok: true, mode: "fleet", posture: "ok" }); + expect(r.json.checkout).toBe(w.checkout); + expect((r.json.freshness as Record).counter).toBe(4); + const summary = r.json.summary as string; + expect(summary).toContain("mode: fleet"); + expect(summary).toMatch(/source: fleet\.json/); // provenance renders + expect(summary).toContain("counter 4"); // carried freshness fields render + }); + + it("a stale contract_version from the publisher surfaces the reader's LOUD rejection, both versions named", () => { + const stale = join(tmp, "stale-projection.json"); + writeFileSync(stale, JSON.stringify({ schema_version: 1, contract_version: 0, sections: {} })); + const w = grantedWorld({}, stale); + const r = run(["--checkout", w.checkout, "--config", w.entitlements], w.deps); + expect(r.code).toBe(64); + expect((r.json.errors as string[]).join(" ")).toContain("v0"); + expect((r.json.errors as string[]).join(" ")).toContain("v1"); + }); + + it("a failed publisher invocation reports honestly (entry point may be absent — amicissimo#414), never a stack trace", () => { + const w = grantedWorld({ runPublisher: () => ({ code: 1, stdout: "", stderr: "No module named fleet_authority" }) }); + const r = run(["--checkout", w.checkout, "--config", w.entitlements], w.deps); + expect(r.code).toBe(64); + const errors = (r.json.errors as string[]).join(" "); + expect(errors).toContain("fleet_authority"); + expect(errors).toContain("No module named fleet_authority"); + expect(errors).toMatch(/amicissimo#414|entry point/i); + }); + + it("a --previous projection yields the D1 freshness verdict, cross-epoch surfaced as unknown + force-refetch", () => { + const w = grantedWorld(); + const previous = join(tmp, "previous.json"); + writeFileSync(previous, JSON.stringify({ + schema_version: 1, + contract_version: 1, + freshness: { counter: 99, hub_epoch: "99999999-9999-4999-8999-999999999999" }, + sections: {}, + })); + const r = run(["--checkout", w.checkout, "--config", w.entitlements, "--previous", previous], w.deps); + expect(r.code).toBe(0); + expect((r.json.freshness as Record).verdict).toBe("unknown"); + expect(r.json.summary as string).toMatch(/force refetch/i); + }); +}); + +// ── the bootstrap exception: honest, distinct, never a crash ─────────────────── + +describe("the bootstrap exception (no entitlement / no checkout)", () => { + it("absent entitlement → base-standalone stated with the grant path, exit FLEET_BOOTSTRAP_EXIT, publisher never invoked", () => { + let invoked = 0; + const deps: FleetProjectionDeps = { + readFile: () => null, // no entitlements file at all + checkDir: () => true, + runPublisher: () => { + invoked += 1; + return { code: 0, stdout: "", stderr: "" }; + }, + }; + const r = run(["--config", join(tmp, "entitlements.toml")], deps); + expect(r.code).toBe(FLEET_BOOTSTRAP_EXIT); + expect(r.code).not.toBe(0); + expect(r.code).not.toBe(64); + expect(invoked).toBe(0); // not granted → the publisher is never spawned + expect(r.json).toMatchObject({ ok: false, bootstrap: true, reason: "entitlement", mode: "standalone" }); + const rendered = [r.json.rendered, r.json.note].filter(Boolean).join("\n") as string; + expect(rendered).toMatch(/base-standalone/); + expect(rendered).toContain("entitlements.toml"); // the honest pointer + expect(rendered).toMatch(/amicissimo/); + }); + + it("entitlement without the amicissimo code → the same honest bootstrap", () => { + const entitlements = join(tmp, "entitlements.toml"); + writeFileSync(entitlements, 'codes = ["issimo"]\n'); + const r = run(["--config", entitlements], { readFile: (p) => (p === entitlements ? 'codes = ["issimo"]' : null), checkDir: () => true }); + expect(r.code).toBe(FLEET_BOOTSTRAP_EXIT); + expect(r.json).toMatchObject({ bootstrap: true, reason: "entitlement" }); + }); + + it("absent checkout → base-standalone stated with the clone pointer, exit FLEET_BOOTSTRAP_EXIT", () => { + const entitlements = join(tmp, "entitlements.toml"); + writeFileSync(entitlements, 'codes = ["amicissimo"]\n'); + const r = run(["--config", entitlements, "--checkout", join(tmp, "no-such-checkout")], { + readFile: (p) => (p === entitlements ? 'codes = ["amicissimo"]' : null), + checkDir: () => false, + }); + expect(r.code).toBe(FLEET_BOOTSTRAP_EXIT); + expect(r.json).toMatchObject({ ok: false, bootstrap: true, reason: "checkout", mode: "standalone" }); + const rendered = [r.json.rendered, r.json.note].filter(Boolean).join("\n") as string; + expect(rendered).toMatch(/base-standalone/); + expect(rendered).toMatch(/AMICISSIMO_ROOT/); // the escape hatch, named + }); + + it("an unknown flag is a usage error (64), distinct from the bootstrap exception", () => { + const w = grantedWorld(); + const r = run(["--checkout", w.checkout, "--config", w.entitlements, "--bogus"], w.deps); + expect(r.code).toBe(64); + expect((r.json.errors as string[]).join(" ")).toContain("--bogus"); + }); +}); + +// ── the router: --projection routes within `amico fleet status` ──────────────── + +describe("the fleet verb router", () => { + it("`status --projection` routes to the projection status; plain `status` keeps its pinned --session contract", () => { + const w = grantedWorld(); + const routed = fleetVerb( + ["status", "--projection", "--checkout", w.checkout, "--config", w.entitlements], + { projection: w.deps }, + ) as unknown as { json: Record; code: number }; + expect(routed.code).toBe(0); + expect(routed.json.ok).toBe(true); + expect(routed.json.mode).toBe("fleet"); + expect(w.calls).toHaveLength(1); // the publisher was invoked through the seam + + // the pinned session-registry contract is untouched (fleet_verb.test.ts's + // `--session is required` case) — asserted here from the same router. + const plain = fleetVerb(["status"]) as unknown as { json: Record; code: number }; + expect(plain.code).toBe(64); + expect((plain.json.errors as string[]).join(" ")).toMatch(/--session is required/); + }); +}); diff --git a/packages/schema/src/fleet_projection.ts b/packages/schema/src/fleet_projection.ts new file mode 100644 index 000000000..036cd1b34 --- /dev/null +++ b/packages/schema/src/fleet_projection.ts @@ -0,0 +1,288 @@ +// The fleet projection reader (amicode#1068, fleet rearchitect P3b-1; spec +// spec-20260913-114814 §3 D1, invariant 5) — the amicode-side consumer of +// amicissimo's fleet-authority projection document (contract v1). +// +// The projection FORMAT is owned by amicissimo's fleet_authority package +// (Python, amicissimo#412/#413): its `contract.py` is the authority and this +// module MIRRORS it — the TS reader consumes, never redefines. A field that +// is unclear here is unclear because the Python was not read first; the +// companion entry point (`python3 -m fleet_authority`, amicissimo#414) +// publishes; this package reads + renders. +// +// What v1 covers here (the Python contract module's own surface, transposed): +// - **The read entry point.** `readProjection` is the ONE gate a consumer +// calls on a fetched projection (path or in-memory object). It validates +// the envelope — contract_version first, then schema_version — and rejects +// anything it cannot speak LOUDLY: a `FleetContractVersionError` naming BOTH +// the rejected version and this consumer's version. A missing version is +// never assumed current; a future version is never silently coerced. +// - **The epoch semantics (spec D1).** A projection's freshness is +// PUBLISHER-COMPUTED: a monotonic counter bound to a hub-instance epoch (a +// UUID minted at store creation; a re-image = a new store = a new epoch). +// `freshnessBetween` is the ONLY freshness comparison a consumer makes: +// same epoch + higher counter → "fresh"; equal counter → "stale"; +// cross-epoch (or a counter that went backwards) → "unknown" — force +// refetch + surface, never a false-fresh badge. Consumers NEVER compute +// age from a local wall clock (D1); the publisher's `published_at` stamp is +// provenance, not a freshness input. This module holds no clock at all. +// - **Provenance rendering.** Every section stamps its `source` (where it +// came from) and `parsed_from` (what it was parsed from). Provenance is +// METADATA beside the value — `renderFleetStatus` prints it on its own +// line, and it never merges into the data. Absent sections render the +// base defaults (mode absent = standalone, posture absent = ok — the +// post-amendment ADR-0005 vocabulary), never invented values. +// +// Placement: this module lives in @amicode/schema because the repo's +// convention puts cross-package shared contract code here — the extension's +// P3b-2 consumer and amico-run's verb both import from the package root +// (the documented seam `validate`/`mode_registry`/`skill_revision` already +// established). Fleet-class IMPLEMENTATION stays amicissimo overlay content +// (spec R1); a reader is a consumer. +import { readFileSync } from "node:fs"; + +export const FLEET_CONTRACT_VERSION = 1; + +export const SUPPORTED_PROJECTION_SCHEMA_VERSIONS: readonly number[] = [1]; + +export const MODE_VOCABULARY = ["standalone", "fleet"] as const; +export const POSTURE_VOCABULARY = ["ok", "degraded", "hub-down"] as const; + +/** The D1 freshness verdict. "unknown" means: cross-epoch or rewind — the + * comparison says nothing trustworthy, so the consumer force-refetches and + * surfaces; it must never render a false-fresh badge. */ +export type FleetFreshness = "fresh" | "stale" | "unknown"; + +/** A versioned-contract rejection. The message ALWAYS names both versions — + * the seen one and this consumer's — the rejection is loud, never a silent + * coercion or a silent swallow. Mirrors the Python `ContractVersionError`. */ +export class FleetContractVersionError extends Error { + /** The contract_version the document carried (undefined when absent). */ + readonly seen: unknown; + + constructor(seen: unknown) { + const described = seen === undefined ? "absent (no contract_version field)" : `v${String(seen)}`; + super( + `projection carries contract ${described}; this consumer speaks ` + + `v${FLEET_CONTRACT_VERSION} — refusing loudly (never silently coerced)`, + ); + this.name = "FleetContractVersionError"; + this.seen = seen; + } +} + +// ── the envelope types (permissive on purpose: the reader validates the +// version gates loudly and renders everything else honestly — it does not +// invent a second, stricter format the authority does not own) ──────────── + +export interface FleetProvenance { + /** Where the section came from (the file/surface it was parsed from). */ + source?: unknown; + /** What it was parsed from — the actual fields present. */ + parsed_from?: unknown; +} + +export interface FleetSection { + /** The carried value — EXACTLY what the publisher emitted, provenance never + * merged in. */ + value?: unknown; + /** Metadata BESIDE the value, never inside it. */ + provenance?: FleetProvenance; +} + +export interface FleetFreshnessStamp { + counter?: unknown; + hub_epoch?: unknown; +} + +export interface FleetProjection { + schema_version?: unknown; + contract_version?: unknown; + publisher?: { identity?: unknown; published_at?: unknown }; + freshness?: FleetFreshnessStamp; + sections?: Record; + [key: string]: unknown; +} + +/** Read one projection document (a file path or an in-memory object) under + * the contract. Returns the document unchanged when it is lawful; throws + * `FleetContractVersionError` for any contract-version mismatch (missing, + * stale, or future — each naming both versions) and an `Error` for a schema + * version this contract does not speak or a document that is not a JSON + * object. Mirrors the Python `read_projection` gate order exactly: + * contract_version first, then schema_version. */ +export function readProjection(source: string | Record): FleetProjection { + let doc: unknown; + if (typeof source === "string") { + doc = JSON.parse(readFileSync(source, "utf8")); + } else { + doc = source; + } + if (doc === null || typeof doc !== "object" || Array.isArray(doc)) { + throw new Error(`projection document is not a JSON object: ${typeName(doc)}`); + } + const carrier = doc as FleetProjection; + const seen = carrier.contract_version; + if (seen !== FLEET_CONTRACT_VERSION) { + throw new FleetContractVersionError(seen); + } + const schema = carrier.schema_version; + if (!(SUPPORTED_PROJECTION_SCHEMA_VERSIONS as readonly unknown[]).includes(schema)) { + throw new Error( + `projection carries unsupported schema_version ${describeValue(schema)}; ` + + `this contract speaks ${SUPPORTED_PROJECTION_SCHEMA_VERSIONS.join(", ")}`, + ); + } + return carrier; +} + +/** The one freshness comparison a consumer makes (spec §3 D1). Cross-epoch is + * ALWAYS "unknown" — the re-image case: the counter reset, so a naive + * comparison would mislabel either side. A counter that went backwards + * within one epoch is likewise "unknown" (monotonic is the contract; + * backwards means something this contract cannot read, not something it + * should trust). Absent freshness fields compare unknown — never a crash, + * never silently fresh. */ +export function freshnessBetween(previous: FleetProjection | null, fetched: FleetProjection): FleetFreshness { + if (previous === null) return "fresh"; + const fresh = (fetched.freshness ?? {}) as FleetFreshnessStamp; + const prev = (previous.freshness ?? {}) as FleetFreshnessStamp; + if (fresh.hub_epoch !== prev.hub_epoch) return "unknown"; + if (!isCounter(fresh.counter) || !isCounter(prev.counter)) return "unknown"; + if (fresh.counter > prev.counter) return "fresh"; + if (fresh.counter === prev.counter) return "stale"; + return "unknown"; +} + +/** The surfaced text for a verdict: unknown MUST be surfaced with its + * force-refetch instruction, stale says what it is, fresh stays quiet + * (no badge noise). */ +export function freshnessAdvisory(verdict: FleetFreshness): string { + if (verdict === "unknown") { + return "unknown freshness (cross-epoch or rewind) — force refetch and surface; never a false-fresh badge (spec §3 D1)"; + } + if (verdict === "stale") return "stale — same counter as the previous fetch; nothing new was published"; + return ""; +} + +/** Render a status summary from a lawful projection: every section's value + * with its provenance on its own line (source + parsed-from, never merged + * into the data), the carried freshness fields verbatim (counter + epoch, + * published_at as the publisher's stamp), and — when a previous projection + * is given — the D1 verdict with its advisory surfaced. Absent sections + * render the base defaults (mode = standalone, posture = ok), absent + * envelope fields render as absent: honest renderings, never inventions, + * never a wall-clock age (this module holds no clock at all). */ +export function renderFleetStatus(proj: FleetProjection, previous: FleetProjection | null = null): string { + const sections = proj.sections ?? {}; + const lines: string[] = []; + + const publisher = proj.publisher ?? {}; + const identity = publisher.identity === undefined ? "unknown" : String(publisher.identity); + const stamp = publisher.published_at === undefined ? "unstamped" : String(publisher.published_at); + lines.push( + `fleet status — contract v${String(proj.contract_version)}, schema v${String(proj.schema_version)}, publisher: ${identity} (${stamp})`, + ); + + // mode + posture always render — base-defaultable scalars (post-amendment + // ADR-0005 vocabulary; mode absent = standalone, posture absent = ok). + lines.push(...renderScalarSection("mode", sections.mode, MODE_VOCABULARY, "standalone", "mode absent = standalone")); + lines.push(...renderScalarSection("posture", sections.posture, POSTURE_VOCABULARY, "ok", "posture absent = ok")); + + // topology / health / locks render ONLY when their section is present — + // a field the publisher did not carry stays absent (additive-optional; + // the consumer falls back to base defaults, never invents). + if (sections.topology !== undefined) { + lines.push(`topology: ${compactJson(sections.topology.value)}`); + lines.push(...provenanceLines(sections.topology)); + } + if (sections.health !== undefined) { + lines.push(`health: ${compactJson(sections.health.value)}`); + lines.push(...provenanceLines(sections.health)); + } + if (sections.locks !== undefined) { + const rows = rowsOf(sections.locks.value); + lines.push(`locks: ${rows} row(s) — a rendering, never the enforcement`); + lines.push(...provenanceLines(sections.locks)); + } + + const fresh = proj.freshness ?? {}; + if (fresh.counter === undefined || fresh.hub_epoch === undefined) { + lines.push("freshness: absent (no carried freshness fields to render — refusing to guess)"); + } else { + lines.push(`freshness: counter ${String(fresh.counter)} @ epoch ${String(fresh.hub_epoch)}`); + } + if (previous !== null) { + const verdict = freshnessBetween(previous, proj); + lines.push(`freshness verdict: ${verdict}`); + const advisory = freshnessAdvisory(verdict); + if (advisory !== "") lines.push(` ${advisory}`); + } + return lines.join("\n"); +} + +// ── internals ─────────────────────────────────────────────────────────────── + +/** Mirrors the Python gate's `isinstance(counter, int)`: an integer counter, + * and only an integer counter, orders freshness. Anything else — a float, a + * string, absent — is unknown freshness, never a crash, never silently + * coerced. */ +function isCounter(v: unknown): v is number { + return typeof v === "number" && Number.isInteger(v); +} + +/** A base-defaultable scalar section (mode / posture): the carried value, a + * loud surface when it sits outside the closed vocabulary (surfaced, never + * silently remapped), and its provenance — or the base default with + * base-default provenance when the section is absent. */ +function renderScalarSection( + name: string, + section: FleetSection | undefined, + vocabulary: readonly string[], + baseDefault: string, + baseNote: string, +): string[] { + const lines: string[] = []; + if (section === undefined) { + lines.push(`${name}: ${baseDefault}`); + lines.push(` provenance — source: unknown; parsed from: base default (${baseNote})`); + return lines; + } + const value = section.value === undefined ? baseDefault : String(section.value); + lines.push(`${name}: ${value}`); + if (!(vocabulary as readonly unknown[]).includes(section.value)) { + lines.push(` warning: value ${describeValue(section.value)} is outside the ${name} vocabulary ${vocabulary.join(", ")} — surfaced, never silently remapped`); + } + lines.push(...provenanceLines(section)); + return lines; +} + +/** The provenance line(s) for a section: source + parsed-from as METADATA + * beside the value, never merged into it. */ +function provenanceLines(section: FleetSection): string[] { + const p = section.provenance ?? {}; + const source = p.source === undefined || p.source === null ? "unknown" : String(p.source); + const parsedFrom = p.parsed_from === undefined || p.parsed_from === null ? "unknown" : String(p.parsed_from); + return [` provenance — source: ${source}; parsed from: ${parsedFrom}`]; +} + +/** The lock row count — the section's value carries `{rows: [...]}`; anything + * else renders as 0 rows named honestly, never a crash. */ +function rowsOf(value: unknown): number { + if (value === null || typeof value !== "object") return 0; + const rows = (value as { rows?: unknown }).rows; + return Array.isArray(rows) ? rows.length : 0; +} + +function compactJson(v: unknown): string { + return JSON.stringify(v) ?? "absent"; +} + +function describeValue(v: unknown): string { + return JSON.stringify(v) ?? String(v); +} + +function typeName(v: unknown): string { + if (Array.isArray(v)) return "array"; + if (v === null) return "null"; + return typeof v; +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index fcf20d7e2..404778af3 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -163,6 +163,32 @@ export { type FetchSurface, } from "./watched_repos.js"; +// The fleet projection reader (amicode#1068, fleet rearchitect P3b-1): the +// TS consumer of amicissimo's fleet-authority projection (contract v1, schema +// v1 — the FORMAT is owned by amicissimo's fleet_authority package; this +// module mirrors its contract module's read gate + epoch-bound freshness +// semantics and renders per-section provenance BESIDE the data, never merged). +// Same documented root seam: amico-run's `amico fleet status --projection` +// verb and the extension's P3b-2 consumer both import from here, so the +// rearchitect's "one path, versioned" invariant holds across languages — +// publish, don't codegen (spec §3 D1). +export { + FLEET_CONTRACT_VERSION, + SUPPORTED_PROJECTION_SCHEMA_VERSIONS, + MODE_VOCABULARY, + POSTURE_VOCABULARY, + FleetContractVersionError, + readProjection, + freshnessBetween, + freshnessAdvisory, + renderFleetStatus, + type FleetFreshness, + type FleetProjection, + type FleetProvenance, + type FleetSection, + type FleetFreshnessStamp, +} from "./fleet_projection.js"; + // ajv-formats ships a CJS default export; under NodeNext the default import can // bind the module namespace rather than the callable, so normalize defensively. const addFormats = (typeof addFormatsDefault === "function" diff --git a/packages/schema/test/fixtures/fleet_projection.json b/packages/schema/test/fixtures/fleet_projection.json new file mode 100644 index 000000000..570b78593 --- /dev/null +++ b/packages/schema/test/fixtures/fleet_projection.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "contract_version": 1, + "publisher": { "identity": "fixture-publisher", "published_at": "2026-09-13T09:30:00Z" }, + "freshness": { "counter": 12, "hub_epoch": "22222222-2222-4222-8222-222222222222" }, + "sections": { + "mode": { + "value": "standalone", + "provenance": { "source": "fleet.json", "parsed_from": "base default (mode absent = standalone)" } + }, + "posture": { + "value": "degraded", + "provenance": { "source": "fleet-status.json", "parsed_from": "posture field: 'degraded'" } + }, + "health": { + "value": { + "collected_at": "2026-08-30T22:10:04Z", + "devices": [ + { "name": "atom-01", "reachable": true, "detail": "this machine" }, + { "name": "ion-02", "reachable": false, "detail": "ssh failed" }, + { "name": "hub-03", "reachable": true, "detail": "ssh ok" } + ], + "chat_db": { "sessions": null, "last_session": "null", "server_http": "200" }, + "server_guard": { "ok": false, "pid": "", "db_file": "none", "served_sessions": 100, "notes": "server not running;" }, + "vault_sync": { "age_minutes": 0, "clean": true }, + "repos": [ + { "name": "lab-notebook", "branch": "main", "dirty": 12, "ahead": 0, "behind": 3, "wip_branches": "wip/alpha" } + ] + }, + "provenance": { + "source": "fleet-status.json", + "parsed_from": "fields: chat_db, collected_at, devices[3], repos[1], server_guard, vault_sync" + } + } + } +} diff --git a/packages/schema/test/fleet_projection.test.ts b/packages/schema/test/fleet_projection.test.ts new file mode 100644 index 000000000..9780a9809 --- /dev/null +++ b/packages/schema/test/fleet_projection.test.ts @@ -0,0 +1,272 @@ +// fleet_projection.test.ts — the TS projection reader (amicode#1068, fleet +// rearchitect P3b-1): the amicode-side consumer of amicissimo's fleet-authority +// projection (contract v1, schema v1). The FORMAT is owned by amicissimo's +// fleet_authority package (Python, amicissimo#412/#413, READ-ONLY reference at +// the sibling worktree) — this suite pins the TS reader against the SAME +// rejection semantics the Python contract module pins (`read_projection`, +// `freshness_between`): a versioned, loud, never-silently-coerced read. +// +// The three properties this suite exists to defend: +// 1. THE CONTRACT IS VERSIONED AND THE REJECTION IS LOUD. A projection +// carrying a stale, future, or absent contract_version is refused with a +// message naming BOTH the seen version and this consumer's v1 — never +// assumed current, never coerced (spec invariant 5). +// 2. FRESHNESS IS PUBLISHER-COMPUTED AND EPOCH-BOUND (spec §3 D1). Same +// epoch + higher counter = fresh; equal = stale; cross-epoch or rewind = +// unknown — force refetch, surfaced, never a false-fresh badge. The reader +// NEVER computes age from a wall clock: it renders the carried fields +// only (pinned by a source guard — the module has no Date at all). +// 3. PROVENANCE RENDERS BESIDE THE DATA, NEVER MERGED INTO IT. Every +// section's source + parsed_from render as metadata; a section's value +// is exactly what the publisher carried, never with provenance folded in. +// +// Fixture shapes are modeled on the Python tests' fixtures +// (tests/fleet_authority/ in amicissimo): the same epoch UUIDs, the same +// base-default discipline (mode absent = standalone, posture absent = ok). +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { + FLEET_CONTRACT_VERSION, + SUPPORTED_PROJECTION_SCHEMA_VERSIONS, + MODE_VOCABULARY, + POSTURE_VOCABULARY, + FleetContractVersionError, + readProjection, + freshnessBetween, + freshnessAdvisory, + renderFleetStatus, + type FleetProjection, +} from "../src/fleet_projection.js"; + +// The Python contract tests' epochs (test_contract.py) — the same UUIDs, so a +// cross-repo reader can be diffed against the authority's own fixtures. +const E1 = "44444444-4444-4444-8444-444444444444"; +const E2 = "55555555-5555-4555-8555-555555555555"; + +function proj(epoch: string, counter: number): FleetProjection { + return { + schema_version: 1, + contract_version: 1, + freshness: { hub_epoch: epoch, counter }, + sections: {}, + } as FleetProjection; +} + +// A full projection, shaped on the Python publisher tests' output given the +// fleet_topology.json + fleet_health.json fixtures (client role → fleet mode). +const FULL = { + schema_version: 1, + contract_version: 1, + publisher: { identity: "test-publisher", published_at: "2026-09-13T12:00:00Z" }, + freshness: { counter: 4, hub_epoch: "33333333-3333-4333-8333-333333333333" }, + sections: { + mode: { + value: "fleet", + provenance: { source: "fleet.json", parsed_from: "role='client' (vocabulary mapping)" }, + }, + posture: { + value: "ok", + provenance: { source: "fleet-status.json", parsed_from: "base default (posture absent = ok)" }, + }, + topology: { + value: { + role: "client", + canonical: { host: "hq-hub-01.example.internal", port: 4096, sshAlias: "hq-hub-01" }, + previousBinary: "/home/example/.amico/server/bin/opencode", + previousPort: 4096, + }, + provenance: { + source: "fleet.json", + parsed_from: "topology schema v1 fields: role, canonical, previousBinary, previousPort", + }, + }, + health: { + value: { + collected_at: "2026-08-30T22:10:04Z", + devices: [ + { name: "atom-01", reachable: true, detail: "this machine" }, + { name: "ion-02", reachable: false, detail: "ssh failed" }, + ], + }, + provenance: { source: "fleet-status.json", parsed_from: "fields: collected_at, devices[2]" }, + }, + locks: { + value: { rows: [{ session: "ses_0183f2", holder: "atom-01", leased_until: "2026-08-30T23:10:04Z" }] }, + provenance: { source: "hub lock state", parsed_from: "rendered from hub lock state: 1 row(s) — a rendering, never the enforcement" }, + }, + }, +}; + +// ── the contract is versioned, and says so ─────────────────────────────────── + +describe("the fleet projection contract (v1)", () => { + it("is at v1 with schema v1, mirroring the Python contract module's constants", () => { + expect(FLEET_CONTRACT_VERSION).toBe(1); + expect(SUPPORTED_PROJECTION_SCHEMA_VERSIONS).toEqual([1]); + }); + + it("carries the post-amendment mode + posture vocabularies", () => { + expect(MODE_VOCABULARY).toEqual(["standalone", "fleet"]); + expect(POSTURE_VOCABULARY).toEqual(["ok", "degraded", "hub-down"]); + }); +}); + +// ── the read entry point: loud, version-checking, never coerced ───────────── + +describe("readProjection", () => { + it("accepts a current projection unchanged (in-memory dict)", () => { + const r = readProjection(FULL); + expect(r.contract_version).toBe(1); + expect(((r.sections ?? {}).mode as { value: unknown }).value).toBe("fleet"); + }); + + it("reads a projection from a file path", () => { + const r = readProjection(fileURLToPath(new URL("./fixtures/fleet_projection.json", import.meta.url))); + expect(r.schema_version).toBe(1); + expect(((r.sections ?? {}).posture as { value: unknown }).value).toBe("degraded"); + }); + + it("a STALE contract version is rejected loudly, naming BOTH versions", () => { + expect(() => readProjection({ schema_version: 1, contract_version: 0, sections: {} })).toThrowError(FleetContractVersionError); + try { + readProjection({ schema_version: 1, contract_version: 0, sections: {} }); + expect.unreachable("stale contract_version must throw"); + } catch (e) { + expect(e).toBeInstanceOf(FleetContractVersionError); + const msg = (e as Error).message; + expect(msg).toContain("v0"); // the seen version, named + expect(msg).toContain(`v${FLEET_CONTRACT_VERSION}`); // this consumer's version, named + } + }); + + it("a FUTURE contract version is rejected loudly too, never silently coerced", () => { + try { + readProjection({ schema_version: 1, contract_version: 2, sections: {} }); + expect.unreachable("future contract_version must throw"); + } catch (e) { + expect(e).toBeInstanceOf(FleetContractVersionError); + expect((e as Error).message).toContain("v2"); + expect((e as Error).message).toContain(`v${FLEET_CONTRACT_VERSION}`); + } + }); + + it("an ABSENT contract version is rejected loudly, never assumed current", () => { + try { + readProjection({ schema_version: 1, sections: {} }); + expect.unreachable("absent contract_version must throw"); + } catch (e) { + expect(e).toBeInstanceOf(FleetContractVersionError); + const msg = (e as Error).message; + expect(msg).toContain("absent"); + expect(msg).toContain(`v${FLEET_CONTRACT_VERSION}`); + } + }); + + it("an unknown schema_version is rejected loudly (before section data is trusted)", () => { + expect(() => readProjection({ schema_version: 99, contract_version: 1, sections: {} })).toThrowError(/schema_version/); + }); + + it("a non-object projection document is rejected, never coerced into one", () => { + expect(() => readProjection([1, 2, 3] as unknown as Record)).toThrowError(/not a JSON object/); + }); +}); + +// ── freshness semantics: publisher-computed, epoch-bound, never wall-clock ─── + +describe("freshnessBetween (spec §3 D1)", () => { + it("same epoch + higher counter = fresh", () => { + expect(freshnessBetween(proj(E1, 3), proj(E1, 4))).toBe("fresh"); + }); + + it("same epoch + equal counter = stale", () => { + expect(freshnessBetween(proj(E1, 4), proj(E1, 4))).toBe("stale"); + }); + + it("cross-epoch is ALWAYS unknown freshness, never silently fresh (the re-image case, both directions)", () => { + expect(freshnessBetween(proj(E1, 5), proj(E2, 1))).toBe("unknown"); + expect(freshnessBetween(proj(E1, 1), proj(E2, 5))).toBe("unknown"); + }); + + it("a counter that went BACKWARDS within one epoch is unknown, not fresh", () => { + expect(freshnessBetween(proj(E1, 5), proj(E1, 2))).toBe("unknown"); + }); + + it("a first fetch has no previous, so it is fresh", () => { + expect(freshnessBetween(null, proj(E1, 1))).toBe("fresh"); + }); + + it("absent freshness envelope fields compare UNKNOWN, never a crash", () => { + const bare = { schema_version: 1, contract_version: 1, sections: {} } as unknown as FleetProjection; + expect(freshnessBetween(proj(E1, 1), bare)).toBe("unknown"); + expect(freshnessBetween(bare, proj(E1, 1))).toBe("unknown"); + }); + + it("unknown freshness carries the force-refetch advisory; stale says so; fresh stays quiet", () => { + expect(freshnessAdvisory("unknown")).toMatch(/force refetch/i); + expect(freshnessAdvisory("stale")).toMatch(/stale/i); + expect(freshnessAdvisory("fresh")).toBe(""); + }); +}); + +// ── provenance renders beside the data, never merged into it ───────────────── + +describe("renderFleetStatus (provenance rendering)", () => { + it("renders every section's value with its source + parsed_from as separate metadata", () => { + const out = renderFleetStatus(readProjection(FULL)); + expect(out).toContain("mode: fleet"); + expect(out).toContain("posture: ok"); + expect(out).toContain("source: fleet.json"); // the mode section's source + expect(out).toContain("role='client' (vocabulary mapping)"); // its parsed_from + expect(out).toContain("hub lock state"); // the locks section's source + expect(out).toContain("a rendering, never the enforcement"); // its parsed_from + expect(out).toContain("test-publisher"); // the publisher identity renders + }); + + it("never merges provenance into the value — the value carries exactly what the publisher carried", () => { + const r = readProjection(FULL); + const sections = r.sections ?? {}; + // the section object holds value + provenance as SEPARATE keys, only + expect(Object.keys(sections.mode as Record).sort()).toEqual(["provenance", "value"]); + const topologyValue = (sections.topology as { value: Record }).value; + expect(topologyValue).not.toHaveProperty("provenance"); // metadata stays BESIDE + expect(topologyValue).not.toHaveProperty("source"); + expect(topologyValue).not.toHaveProperty("parsed_from"); + }); + + it("renders the carried freshness fields verbatim — counter + epoch, published_at as provenance, no computed age", () => { + const out = renderFleetStatus(readProjection(FULL)); + expect(out).toContain("counter 4"); + expect(out).toContain("33333333-3333-4333-8333-333333333333"); + expect(out).toContain("2026-09-13T12:00:00Z"); // the carried published_at stamp + expect(out).not.toMatch(/age|old|ago|minute|hour|second/i); // never wall-clock age + }); + + it("surfaces unknown freshness with the force-refetch advisory when a previous is given", () => { + const out = renderFleetStatus(readProjection(FULL), proj("11111111-1111-4111-8111-111111111111", 99)); + expect(out).toMatch(/unknown freshness/i); + expect(out).toMatch(/force refetch/i); + }); + + it("absent sections render the base defaults (mode = standalone, posture = ok), never invented values", () => { + const out = renderFleetStatus(readProjection({ schema_version: 1, contract_version: 1, sections: {} })); + expect(out).toMatch(/mode: standalone/); + expect(out).toMatch(/base default/); + expect(out).toMatch(/posture: ok/); + }); + + it("absent envelope fields render honestly — no crash, no invention", () => { + const out = renderFleetStatus(readProjection({ schema_version: 1, contract_version: 1, sections: {} })); + expect(out).toMatch(/freshness: absent/); + expect(out).toMatch(/\(unstamped\)/); + }); +}); + +// ── D1's never-wall-clock rule, pinned at the source ───────────────────────── + +describe("the reader never computes age from a wall clock (D1 source guard)", () => { + it("the reader module has no Date/now reference at all — freshness renders carried fields only", () => { + const src = readFileSync(fileURLToPath(new URL("../src/fleet_projection.ts", import.meta.url)), "utf8"); + expect(src).not.toMatch(/\bDate\b|\bnow\b|performance\.now|process\.hrtime/); + }); +});