Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/features/teams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export {
type Team, type Position, type Relationship, type RelationshipArchetype,
type CommunicationForm, type CommEdge, type PositionKind,
} from "./lib/team";
// The effective-team precedence resolver (#3152, epic #3151) — the planner's `teamFleet.ts` composes
// this with its own `bsc plan team get` binding + `blueprint.team` once #3152's plan.db/CLI half
// lands (blocked on this stream's write scope; see `bsc plan request 1`).
export { effectiveTeam, type TeamGraph } from "./lib/effectiveTeam";
32 changes: 32 additions & 0 deletions src/features/teams/lib/effectiveTeam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest";
import { effectiveTeam, type TeamGraph } from "./effectiveTeam";

const graph = (n: string): TeamGraph => ({
positions: [{ nodeId: n, kind: "agent", personaId: `persona-${n}` }],
relationships: [],
});

describe("effectiveTeam (#3152)", () => {
it("falls back to the blueprint's team when no per-project binding has ever been set", () => {
const blueprintTeam = graph("architect");
expect(effectiveTeam(null, blueprintTeam)).toBe(blueprintTeam);
expect(effectiveTeam(undefined, blueprintTeam)).toBe(blueprintTeam);
});

it("the per-project binding wins over the blueprint's team once set", () => {
const binding = graph("curator");
const blueprintTeam = graph("architect");
expect(effectiveTeam(binding, blueprintTeam)).toBe(binding);
});

it("an explicitly-set EMPTY binding still wins — a deliberate unpin is a recorded decision, not an absence", () => {
const empty: TeamGraph = { positions: [], relationships: [] };
const blueprintTeam = graph("architect");
expect(effectiveTeam(empty, blueprintTeam)).toBe(empty);
});

it("no binding and no blueprint team ⇒ no team", () => {
expect(effectiveTeam(null, undefined)).toBeUndefined();
expect(effectiveTeam(undefined, undefined)).toBeUndefined();
});
});
32 changes: 32 additions & 0 deletions src/features/teams/lib/effectiveTeam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Effective team resolution (#3152, part of the Teams-in-Planner epic #3151) — a project's team can
// come from two places: its OWN per-project binding (planner-owned storage, `bsc plan team get/set`,
// decoupled from the blueprint per option (b)) or the BLUEPRINT it was seeded from (`blueprint.team`,
// #2450, a reusable template). The binding wins once a project has pinned its own team; an unset
// binding falls back to the blueprint's team so a project that never re-pins keeps working exactly as
// before (#3101/#3102 seeding stays byte-identical). Pure (no React/Tauri/bsc) so both this feature
// and the planner's fleet-seeding path (`teamFleet.ts`) can unit-test the precedence directly.
import type { Position, Relationship } from "./team";

/** The minimal team shape this resolver needs — structurally the same as this feature's {@link Team}
* minus its library-identity fields (id/name/blurb/builtin), and the same shape the planner's
* `BlueprintTeam` and a `bsc plan team get` blob carry. Kept local (not imported from the planner) so
* this feature never depends on it — the planner depends on teams, never the reverse. */
export interface TeamGraph {
positions: Position[];
relationships: Relationship[];
}

/**
* Resolve a project's EFFECTIVE team: its per-project `binding` (from `bsc plan team get`) if one has
* ever been set, else the `blueprintTeam` it was seeded from. `binding` is `null`/`undefined` exactly
* when no per-project team has been set yet (the planner's blob store returns nothing until the first
* `bsc plan team set`) — that, and only that, is what falls back; an explicitly-set EMPTY binding
* (`{ positions: [], relationships: [] }`, a project deliberately unpinning its team) still wins, since
* it is a real, its own recorded decision.
*/
export function effectiveTeam(
binding: TeamGraph | null | undefined,
blueprintTeam: TeamGraph | undefined,
): TeamGraph | undefined {
return binding ?? blueprintTeam;
}