Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .moon/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ projects:
# rather than the schema.
stamp-gate: 'tools/stamp-gate'
# The design-ledger gate: validates the Compass design-decision ledger
# (docs/designs/product/DECISIONS.md), the per-record `Status:` headers, and
# (docs/designs/DECISIONS.md), the per-record `Status:` headers, and
# dangling supersession/Record pointers (SEA-1187). Migrated from the sealed
# repo with the corpus it governs (SEA-1794).
design-ledger-gate: 'tools/design-ledger-gate'
Expand Down
430 changes: 215 additions & 215 deletions docs/designs/product/DECISIONS.md → docs/designs/DECISIONS.md

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions docs/designs/product/compass-architecture-lineage/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Status: Active

The early Compass milestone records (v0.3 through v0.8) are retired. Every
load-bearing decision they made is now a row in
[`DECISIONS.md`](../DECISIONS.md) — the canonical index of current truth — so the
[`DECISIONS.md`](../../DECISIONS.md) — the canonical index of current truth — so the
milestone narratives themselves carried nothing live except a thin layer of
still-true *rationale*: the "why not the alternative" context a one-line ledger
row deliberately drops. This record preserves that rationale in one place and
Expand All @@ -18,18 +18,18 @@ did, when that reasoning is not obvious from the one-line statement.
## The current shape, in one paragraph

Compass is a three-tier system — Client → Server → Runner, with the
communication layer as the spine ([DL-007](../DECISIONS.md)). Postgres is the
communication layer as the spine ([DL-007](../../DECISIONS.md)). Postgres is the
store of record and is not swappable; an in-memory event bus is a cache/fan-out
ring over it, not a second store ([DL-020](../DECISIONS.md),
[DL-021](../DECISIONS.md)). Transport is gRPC everywhere, authenticated by
per-Runner provisioned tokens ([DL-013](../DECISIONS.md)). The in-container
ring over it, not a second store ([DL-020](../../DECISIONS.md),
[DL-021](../../DECISIONS.md)). Transport is gRPC everywhere, authenticated by
per-Runner provisioned tokens ([DL-013](../../DECISIONS.md)). The in-container
agent is a first-party program on the Oh My Pi SDK, emitting `compass.v1`
natively ([DL-023](../DECISIONS.md)), one per per-agent container on the Runner
for blast-radius isolation ([DL-024](../DECISIONS.md)). The UI shell is
natively ([DL-023](../../DECISIONS.md)), one per per-agent container on the Runner
for blast-radius isolation ([DL-024](../../DECISIONS.md)). The UI shell is
board-primary, with channel chat folded into the workspace
([DL-031](../DECISIONS.md)), rendering a first-party typed session trace over a
typed gRPC stream ([DL-039](../DECISIONS.md)). The agent tree is the organizing
primitive ([DL-095](../DECISIONS.md)).
([DL-031](../../DECISIONS.md)), rendering a first-party typed session trace over a
typed gRPC stream ([DL-039](../../DECISIONS.md)). The agent tree is the organizing
primitive ([DL-095](../../DECISIONS.md)).

## Carried-over rationale

Expand All @@ -46,30 +46,30 @@ the linked row is the authority.
- **The comms layer is the *structure* of the product, not a pipeline.** Audit
and search are substrate properties — every message is a durable, queryable
row — not a feature bolted onto a message bus. This is why comms is built
first-party ([DL-021](../DECISIONS.md)) rather than adopted as a dependency:
first-party ([DL-021](../../DECISIONS.md)) rather than adopted as a dependency:
the primacy of the comms substrate is the reason to own it, and the one-line
row names the mechanism (Postgres write-through fan-out), not the primacy.
- **Postgres store + in-memory bus is a deliberate duality.** The store of
record and the fan-out ring are two things on purpose
([DL-020](../DECISIONS.md), [DL-021](../DECISIONS.md)): the store is the
([DL-020](../../DECISIONS.md), [DL-021](../../DECISIONS.md)): the store is the
durable truth, the ring is an ephemeral cache/fan-out for live delivery. The
ring is never a second store; losing it loses no committed state.

### Runner, containers, config

- **A per-agent container is a structural sandbox, not credential avoidance.**
Isolation, clone-per-workstream, scoped credentials, and default-deny egress
are the reason for the container boundary ([DL-024](../DECISIONS.md)) — the
are the reason for the container boundary ([DL-024](../../DECISIONS.md)) — the
blast radius is structural, so a compromised agent is contained by the
boundary itself rather than by trusting it to hold a narrow credential.
- **Containers are throwaway; durable state lives in the Server.** An agent
container can be torn down and relocated without context loss because nothing
durable lives in it — the session log and all state are the Server's
([DL-020](../DECISIONS.md), and the session-persistence chain
([DL-020](../../DECISIONS.md), and the session-persistence chain
DL-088/DL-089). This is what makes restart-and-resume a first-class operation
rather than a recovery hack.
- **Config distribution is a Runner-mediated pull to a local read-only mount to
avoid a cross-host network filesystem** ([DL-022](../DECISIONS.md)). The
avoid a cross-host network filesystem** ([DL-022](../../DECISIONS.md)). The
rejected alternative was mounting config over the network into every
container; materializing it locally per Runner keeps the container's config
read path host-local and offline-survivable. The row names the pull; the
Expand All @@ -83,16 +83,16 @@ the linked row is the authority.

- **One ALPN/h2 port serves both gRPC-Web and native gRPC** — the authenticated
listener multiplexes on the protocol, so operators expose a single port rather
than a Web gateway beside a native one ([DL-012](../DECISIONS.md)).
than a Web gateway beside a native one ([DL-012](../../DECISIONS.md)).
- **Operator-brings-PEM certs; ACME is deferred.** The cert model is
operator-provisioned PEM material ([DL-012](../DECISIONS.md)); automated
operator-provisioned PEM material ([DL-012](../../DECISIONS.md)); automated
issuance is a later additive, not a v1 dependency, so the door has no
build-time coupling to an ACME provider.
- **The provisioned token is written to a `0600` file, never stdout or a log.**
A minted Runner token is a bearer credential; it lands in a mode-`0600` file
and is never emitted to a stream a log aggregator could capture.
- **The token is per-Runner, not per-job.** A Runner enrolls once and holds one
token for its lifetime ([DL-013](../DECISIONS.md)); a per-job token was
token for its lifetime ([DL-013](../../DECISIONS.md)); a per-job token was
rejected as churn with no isolation gain, since the Runner is already the
trust boundary.
- **Async token-resolve cannot run in a sync `tonic` interceptor** — resolving a
Expand All @@ -103,12 +103,12 @@ the linked row is the authority.
nothing.** A subscriber attaches to the live fan-out before reading the
backlog snapshot, taking the snapshot under a lock ordering that guarantees no
message falls between the snapshot and the live stream — the no-drop mechanism
behind the cache/fan-out ring ([DL-021](../DECISIONS.md)).
behind the cache/fan-out ring ([DL-021](../../DECISIONS.md)).

### Storage & ownership

- **Transcript bodies live in object storage behind a blob seam**, not in
Postgres rows ([DL-019](../DECISIONS.md)) — the store of record indexes them;
Postgres rows ([DL-019](../../DECISIONS.md)) — the store of record indexes them;
the bodies ride an S3-compatible seam so large transcripts never bloat the
relational store.
- **Owner-membership is transitive.** An agent inherits its owner's channel
Expand All @@ -117,13 +117,13 @@ the linked row is the authority.
- **Secrets cross a boundary contract**: encryption-at-rest, per-principal
authorization, and audit redaction are the three properties any secret-bearing
path must hold — the contract later realized by the Server ownership layer
([DL-052](../DECISIONS.md)).
([DL-052](../../DECISIONS.md)).

### UI shell & session rendering

- **The board-primary reshape folds comms *into* the workspace.** The shell
moved back from a channel-first prototype to a board-primary workspace with
channel chat as a surface within it ([DL-031](../DECISIONS.md)); the rationale
channel chat as a surface within it ([DL-031](../../DECISIONS.md)); the rationale
is that supervision is board-first — an operator watches the fleet board and
drops into a channel from it, not the reverse.
- **Agent identity composes at read time.** An `Account` and its
Expand All @@ -135,7 +135,7 @@ the linked row is the authority.
closable one, so the operator's spatial model of the workspace is stable.
- **The session trace is a typed contract, not opaque bytes and not ACP.**
Session events cross a typed gRPC stream and Compass renders them first-party
([DL-039](../DECISIONS.md)); the block-level CSS taxonomy
([DL-039](../../DECISIONS.md)); the block-level CSS taxonomy
(`.block-thinking` / `.block-tool` / `.block-plan` / `.block-diff`) is the
live UI contract that typing buys — a renderer that understands block kinds
rather than replaying an opaque byte stream.
Expand Down
2 changes: 2 additions & 0 deletions docs/designs/repo/compass-eng-docs/design.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Compass engineering docs site

Status: Active

Design record for making Compass build-in-the-open: relocate the Compass design
records + product specs into this PUBLIC repo (`RigelBuild/compass`) and
stand up a public Astro Starlight **engineering docs** site with production
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/product/compass.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Living source-of-truth for how **Compass** currently behaves and is built. The
point-in-time design rationale — the ADE vision (the Dispatcher, the Bridge,
per-agent containers, the three-tier Client→Server→Runner architecture) — lives
in the design corpus, indexed by the decision ledger
([`../../designs/product/DECISIONS.md`](../../designs/product/DECISIONS.md)) with
([`../../designs/DECISIONS.md`](../../designs/DECISIONS.md)) with
the surviving milestone rationale in the
[architecture lineage](../../designs/product/compass-architecture-lineage/design.md)
record; this spec describes only what the code exposes today.
Expand Down
123 changes: 95 additions & 28 deletions tools/design-ledger-gate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
//
// Conventions (mirroring tools/spec-impact-gate/gate.test.ts and
// tools/no-bash-gate/index.test.ts):
// - Literal paths (`docs/designs/product/...`), NOT values derived from the
// module constants (PRODUCT_DIR / DECISIONS_PATH / HISTORICAL_CHAIN /
// LARGE_RECORD_BYTES): those constants ARE the thing under test, so deriving
// inputs from them would let a drifted constant pass silently.
// - Literal paths (`docs/designs/<bucket>/...`), NOT values derived from the
// module constants (DESIGNS_ROOT / GOVERNED_ROOTS / DECISIONS_PATH /
// HISTORICAL_CHAIN / LARGE_RECORD_BYTES): those constants ARE the thing under
// test, so deriving inputs from them would let a drifted constant pass silently.
// - `row()` / `header()` yield valid baselines so each test perturbs one axis.
// - `.message` is human prose, asserted only by its identifying substring.

Expand All @@ -33,7 +33,7 @@ import {
touchesRecord,
} from "./index.ts";

const LEDGER = "docs/designs/product/DECISIONS.md";
const LEDGER = "docs/designs/DECISIONS.md";
const smallRecord = (): RecordContent => ({ headings: [], sizeBytes: 100 });
const noChange: Changed = { files: [], body: null, headBranch: "" };

Expand Down Expand Up @@ -116,33 +116,55 @@ describe("touchesRecord", () => {
true,
);
});
test("a record under a second governed root (agent) is a record", () => {
expect(touchesRecord("docs/designs/agent/compass-x/design.md")).toBe(true);
});
test("a flat <name>.md at a second governed root is a record", () => {
expect(touchesRecord("docs/designs/repo/compass-drop-proto.md")).toBe(true);
});
test("the ledger DECISIONS.md is NOT a record", () => {
expect(touchesRecord("docs/designs/product/DECISIONS.md")).toBe(false);
expect(touchesRecord("docs/designs/DECISIONS.md")).toBe(false);
});
test("a nested non-design.md file is not a record", () => {
expect(touchesRecord("docs/designs/product/foo/bar.md")).toBe(false);
});
test("a file outside the product dir is not a record", () => {
test("a flat .md inside a subgroup is NOT a record (governed at root only)", () => {
expect(touchesRecord("docs/designs/infra/ci/foo.md")).toBe(false);
});
test("a file under an ungoverned bucket is not a record", () => {
expect(touchesRecord("docs/designs/platform/x.md")).toBe(false);
});
test("a file under a non-bucket path is not a record", () => {
expect(touchesRecord("docs/designs/notabucket/x.md")).toBe(false);
});
test("a non-markdown product file is not a record", () => {
expect(touchesRecord("docs/designs/product/notes.txt")).toBe(false);
});
});

describe("resolveRecordRelative", () => {
test("a nested record's `../sibling` pointer → product-relative sibling", () => {
test("a nested record's `../sibling` pointer → designs-root-relative sibling", () => {
expect(
resolveRecordRelative(
"compass-0.6/design.md",
"product/compass-0.6/design.md",
"../compass-0.8/design.md",
),
).toBe("compass-0.8/design.md");
).toBe("product/compass-0.8/design.md");
});
test("a top-level record's bare pointer → that product-relative path", () => {
test("a cross-bucket pointer resolves inside DESIGNS_ROOT", () => {
// A ui/ record superseded by an agent/ record: `../../agent/...` from
// `ui/<name>/design.md` climbs to the designs root then into agent/.
expect(
resolveRecordRelative(
"ui/compass-tauri-shell/design.md",
"../../agent/compass-native-app/design.md",
),
).toBe("agent/compass-native-app/design.md");
});
test("a top-level record's bare pointer → that designs-root-relative path", () => {
expect(resolveRecordRelative("a.md", "b.md")).toBe("b.md");
});
test("a pointer that climbs out of PRODUCT_DIR → null", () => {
test("a pointer that climbs out of DESIGNS_ROOT → null", () => {
expect(resolveRecordRelative("a.md", "../../escape.md")).toBeNull();
});
});
Expand Down Expand Up @@ -707,6 +729,30 @@ describe("evaluate — record Status: header presence & grammar", () => {
expect(vs[0]?.message).toContain("malformed");
expect(vs[0]?.line).toBe(3);
});
test("newly-governed non-product bucket record, statusLine null → 'missing'", () => {
// The cutover made non-product buckets governed (repo/, infra/, …),
// which is why compass-eng-docs/design.md had to gain `Status: Active`.
// This locks that a record under a NON-product governed bucket is
// header-enforced identically — a regression that special-cased the
// product bucket for header presence would pass the product tests above
// yet silently un-enforce every repo/infra/ui/… record.
const vs = evaluate(
[row()],
[
header({
path: "docs/designs/repo/compass-eng-docs/design.md",
statusLine: null,
line: 3,
}),
],
noChange,
smallRecord,
);
expect(vs.length).toBe(1);
expect(vs[0]?.message).toContain("missing");
expect(vs[0]?.file).toBe("docs/designs/repo/compass-eng-docs/design.md");
expect(vs[0]?.line).toBe(3);
});
});

describe("evaluate — Historical-set membership", () => {
Expand Down Expand Up @@ -755,9 +801,10 @@ describe("evaluate — Historical-set membership", () => {

describe("evaluate — record-level Superseded pointer", () => {
// The Status pointer is RECORD-relative; readRecord receives a
// product-relative path. This path-aware resolver (unlike smallRecord, which
// ignores its arg) returns a record only for the exact product-relative path
// that exists, so it locks down the resolution base.
// designs-root-relative (bucket-qualified) path. This path-aware resolver
// (unlike smallRecord, which ignores its arg) returns a record only for the
// exact designs-root-relative path that exists, so it locks down the
// resolution base.
const onlyExists =
(existing: string) =>
(p: string): RecordContent | null =>
Expand All @@ -780,9 +827,9 @@ describe("evaluate — record-level Superseded pointer", () => {
expect(vs[0]?.line).toBe(3);
});
test("record-relative pointer resolves to a sibling under a nested record", () => {
// A nested non-chain record + `../sibling/design.md` → product-relative
// `sibling/design.md`. The resolver only knows that product-relative path,
// so a correct base is the only way this passes.
// A nested non-chain record + `../sibling/design.md` → designs-root-relative
// `product/sibling/design.md`. The resolver only knows that path, so a
// correct base is the only way this passes.
expect(
evaluate(
[],
Expand All @@ -794,14 +841,34 @@ describe("evaluate — record-level Superseded pointer", () => {
}),
],
noChange,
onlyExists("compass-dock-in-sidebar/design.md"),
onlyExists("product/compass-dock-in-sidebar/design.md"),
),
).toEqual([]);
});
test("a cross-bucket record-relative pointer resolves inside DESIGNS_ROOT", () => {
// A ui/ record superseded by an agent/ record: `../../agent/...` climbs to
// the designs root, then into agent/. Resolves as long as it stays inside
// DESIGNS_ROOT.
expect(
evaluate(
[],
[
header({
path: "docs/designs/ui/compass-tauri-shell/design.md",
statusLine:
"Status: Superseded by ../../agent/compass-native-app/design.md",
}),
],
noChange,
onlyExists("agent/compass-native-app/design.md"),
),
).toEqual([]);
});
test("a product-relative form does NOT resolve from a nested record (base is locked)", () => {
// Writing the pointer product-relative (`sibling/design.md`) from inside a
// nested record is wrong: it resolves record-relative to
// `compass-ade-shell/sibling/design.md`, which the resolver rejects.
test("a designs-root-relative form does NOT resolve from a nested record (base is locked)", () => {
// Writing the pointer bucket-qualified (`compass-dock-in-sidebar/design.md`)
// from inside a nested record is wrong: it resolves record-relative to
// `product/compass-ade-shell/compass-dock-in-sidebar/design.md`, which the
// resolver rejects.
const vs = evaluate(
[],
[
Expand All @@ -811,7 +878,7 @@ describe("evaluate — record-level Superseded pointer", () => {
}),
],
noChange,
onlyExists("compass-dock-in-sidebar/design.md"),
onlyExists("product/compass-dock-in-sidebar/design.md"),
);
expect(vs.length).toBe(1);
expect(vs[0]?.message).toContain("does not resolve");
Expand All @@ -827,7 +894,7 @@ describe("evaluate — record-level Superseded pointer", () => {
}),
],
noChange,
onlyExists("other-record.md"),
onlyExists("product/other-record.md"),
),
).toEqual([]);
});
Expand All @@ -853,7 +920,7 @@ describe("evaluate — touch-coupling (DL-Q1)", () => {
evaluate(
[],
[],
changed([rec, "docs/designs/product/DECISIONS.md"], null),
changed([rec, "docs/designs/DECISIONS.md"], null),
smallRecord,
),
).toEqual([]);
Expand Down Expand Up @@ -922,7 +989,7 @@ describe("runOnce", () => {
const d: Deps = {
root: "/fake",
readText: async (_root, rel) =>
rel === "docs/designs/product/DECISIONS.md"
rel === "docs/designs/DECISIONS.md"
? validLedger
: "# Title\n\nStatus: Active\n",
listRecordFiles: async () => [oneRecord],
Expand Down Expand Up @@ -953,7 +1020,7 @@ describe("runOnce", () => {
test("a violation present → exit 1 and prints it", async () => {
const { d, errs } = deps({
readText: async (_root, rel) =>
rel === "docs/designs/product/DECISIONS.md"
rel === "docs/designs/DECISIONS.md"
? validLedger
: "# Title\n\nStatus: bogus value\n",
});
Expand Down
Loading
Loading