diff --git a/.changeset/product-management.md b/.changeset/product-management.md new file mode 100644 index 0000000..605ab8a --- /dev/null +++ b/.changeset/product-management.md @@ -0,0 +1,20 @@ +--- +"@rolexjs/core": minor +"@rolexjs/prototype": minor +"rolexjs": minor +"@rolexjs/mcp-server": minor +--- + +feat: add product management system + +Add product management as a new entity type in RoleX, enabling vision, strategy, +behavior contracts (BDD specs), releases, channels, and ownership tracking. + +New commands: +- `!product.create` — create a product with vision +- `!product.strategy` — define product strategy +- `!product.spec` — add behavior contract (BDD specification) +- `!product.release` — publish a version release +- `!product.channel` — add distribution channel +- `!product.own` / `!product.disown` — manage product ownership +- `!product.deprecate` — deprecate a product diff --git a/apps/mcp-server/src/index.ts b/apps/mcp-server/src/index.ts index 371b2ff..8098d96 100644 --- a/apps/mcp-server/src/index.ts +++ b/apps/mcp-server/src/index.ts @@ -7,7 +7,15 @@ import { localPlatform } from "@rolexjs/local-platform"; import { FastMCP } from "fastmcp"; -import { createRoleX, detail, type ProjectAction, renderProjectResult, type State } from "rolexjs"; +import { + createRoleX, + detail, + type ProductAction, + type ProjectAction, + renderProductResult, + renderProjectResult, + type State, +} from "rolexjs"; import { z } from "zod"; import { instructions } from "./instructions.js"; @@ -263,6 +271,12 @@ server.addTool({ const opResult = result as { state: State }; return renderProjectResult(action, opResult.state); } + // Render product results as readable text + if (locator.startsWith("!product.")) { + const action = locator.slice("!product.".length) as ProductAction; + const opResult = result as { state: State }; + return renderProductResult(action, opResult.state); + } return JSON.stringify(result, null, 2); }, }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 51d4826..6d55938 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,8 +3,8 @@ * * Domain-specific structures and processes built on @rolexjs/system. * - * Structures — the concept tree (23 concepts, 3 relations) - * Processes — how the world changes (32 processes, 5 layers) + * Structures — the concept tree (28 concepts, 4 relations) + * Processes — how the world changes (40 processes, 6 layers) * * Layer 1: Execution — want, plan, todo, finish, complete, abandon * Layer 2: Cognition — reflect, realize, master @@ -45,6 +45,8 @@ export type { ContextData, Platform, PrototypeRegistry, RoleXRepository } from " export { background, + // Product + channel, // Organization charter, // Project @@ -69,14 +71,22 @@ export { position, principle, procedure, + // Product + product, // Project project, + // Product + release, // Organization — Position requirement, // Project scope, // Level 0 society, + // Product + spec, + // Product + strategy, task, tone, // Project @@ -106,17 +116,30 @@ export { wikiProject, } from "./project.js"; +// ===== Processes — Layer 3c: Product ===== + +export { + channelProduct, + disownProduct, + ownProduct, + releaseProduct, + specProduct, + strategyProduct, +} from "./product.js"; + // ===== Processes — Layer 4: Lifecycle ===== export { abolish, archive, born, + deprecate, die, dissolve, establish, found, launch, + publish, rehire, retire, } from "./lifecycle.js"; diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts index 2f66aba..70e7317 100644 --- a/packages/core/src/lifecycle.ts +++ b/packages/core/src/lifecycle.ts @@ -10,7 +10,15 @@ * No real deletion — everything transforms to the "past" branch. */ import { create, process, transform } from "@rolexjs/system"; -import { individual, organization, past, position, project, society } from "./structures.js"; +import { + individual, + organization, + past, + position, + product, + project, + society, +} from "./structures.js"; // Creation export const born = process( @@ -22,6 +30,7 @@ export const born = process( export const found = process("found", "Found an organization", society, create(organization)); export const establish = process("establish", "Establish a position", society, create(position)); export const launch = process("launch", "Launch a project", society, create(project)); +export const publish = process("publish", "Publish a product", society, create(product)); // Retirement & death export const retire = process( @@ -35,6 +44,14 @@ export const die = process("die", "An individual dies", individual, transform(in // Archive project export const archive = process("archive", "Archive a project", project, transform(project, past)); +// Deprecate product +export const deprecate = process( + "deprecate", + "Deprecate a product", + product, + transform(product, past) +); + // Dissolution export const dissolve = process( "dissolve", diff --git a/packages/core/src/product.ts b/packages/core/src/product.ts new file mode 100644 index 0000000..174607f --- /dev/null +++ b/packages/core/src/product.ts @@ -0,0 +1,51 @@ +/** + * Product management — strategy, specs, releases, channels, ownership. + * + * own / disown — ownership (who is responsible) + * strategy — define product strategy + * spec — add behavior contract (BDD specification) + * release — add version release + * channel — add distribution channel + */ +import { create, link, process, unlink } from "@rolexjs/system"; +import { channel, product, release, spec, strategy } from "./structures.js"; + +// Ownership +export const ownProduct = process( + "own", + "Assign an owner to the product", + product, + link(product, "ownership") +); +export const disownProduct = process( + "disown", + "Remove an owner from the product", + product, + unlink(product, "ownership") +); + +// Structure +export const strategyProduct = process( + "strategy", + "Define the strategy for a product", + product, + create(strategy) +); +export const specProduct = process( + "spec", + "Add a behavior contract to a product", + product, + create(spec) +); +export const releaseProduct = process( + "release", + "Add a version release to a product", + product, + create(release) +); +export const channelProduct = process( + "channel", + "Add a distribution channel to a product", + product, + create(channel) +); diff --git a/packages/core/src/structures.ts b/packages/core/src/structures.ts index 0300d8b..97fc48d 100644 --- a/packages/core/src/structures.ts +++ b/packages/core/src/structures.ts @@ -32,6 +32,12 @@ * │ │ ├── milestone "Key checkpoint" │ * │ │ ├── deliverable "Project output" │ * │ │ └── wiki "Project knowledge base" │ + * │ ├── product "A product with contracts" │ + * │ │ │ ∿ ownership → individual │ + * │ │ ├── strategy "Product strategy" │ + * │ │ ├── spec "Product behavior contract" │ + * │ │ ├── release "Product version release" │ + * │ │ └── channel "Product distribution channel" │ * │ └── past "Things no longer active" │ * └─────────────────────────────────────────────────────────┘ */ @@ -120,3 +126,18 @@ export const milestone = structure( ); export const deliverable = structure("deliverable", "Project output and delivery", project); export const wiki = structure("wiki", "Project-level knowledge base entry", project); + +// ================================================================ +// Product — product management entity +// ================================================================ + +export const product = structure( + "product", + "A product with vision, contracts, and releases", + society, + [relation("ownership", "Who owns this product", individual)] +); +export const strategy = structure("strategy", "Product strategy — how to win", product); +export const spec = structure("spec", "Product behavior contract — BDD specification", product); +export const release = structure("release", "Product version release", product); +export const channel = structure("channel", "Product distribution channel", product); diff --git a/packages/prototype/src/instructions.ts b/packages/prototype/src/instructions.ts index 928d8a8..3f8f01a 100644 --- a/packages/prototype/src/instructions.ts +++ b/packages/prototype/src/instructions.ts @@ -589,6 +589,126 @@ const projectArchive = def( ["project"] ); +// ================================================================ +// Product — product management +// ================================================================ + +const productCreate = def( + "product", + "create", + { + content: { + type: "gherkin", + required: false, + description: "Gherkin Feature source for the product (vision)", + }, + id: { type: "string", required: false, description: "User-facing identifier (kebab-case)" }, + alias: { type: "string[]", required: false, description: "Alternative names" }, + }, + ["content", "id", "alias"] +); + +const productStrategy = def( + "product", + "strategy", + { + product: { type: "string", required: true, description: "Product id" }, + content: { + type: "gherkin", + required: true, + description: "Gherkin Feature source for the strategy", + }, + id: { type: "string", required: false, description: "Strategy id" }, + }, + ["product", "content", "id"] +); + +const productSpec = def( + "product", + "spec", + { + product: { type: "string", required: true, description: "Product id" }, + content: { + type: "gherkin", + required: true, + description: "Gherkin Feature source for the behavior contract (BDD specification)", + }, + id: { + type: "string", + required: false, + description: "Spec id (keywords joined by hyphens)", + }, + }, + ["product", "content", "id"] +); + +const productRelease = def( + "product", + "release", + { + product: { type: "string", required: true, description: "Product id" }, + content: { + type: "gherkin", + required: true, + description: "Gherkin Feature source for the release", + }, + id: { + type: "string", + required: false, + description: "Release id (e.g. v1.0.0)", + }, + }, + ["product", "content", "id"] +); + +const productChannel = def( + "product", + "channel", + { + product: { type: "string", required: true, description: "Product id" }, + content: { + type: "gherkin", + required: true, + description: "Gherkin Feature source for the distribution channel", + }, + id: { + type: "string", + required: false, + description: "Channel id (e.g. npm, cloud-platform)", + }, + }, + ["product", "content", "id"] +); + +const productOwn = def( + "product", + "own", + { + product: { type: "string", required: true, description: "Product id" }, + individual: { type: "string", required: true, description: "Individual id (owner)" }, + }, + ["product", "individual"] +); + +const productDisown = def( + "product", + "disown", + { + product: { type: "string", required: true, description: "Product id" }, + individual: { type: "string", required: true, description: "Individual id (owner to remove)" }, + }, + ["product", "individual"] +); + +const productDeprecate = def( + "product", + "deprecate", + { + product: { type: "string", required: true, description: "Product id" }, + }, + ["product"] +); + // ================================================================ // Census — society-level queries // ================================================================ @@ -600,7 +720,7 @@ const censusList = def( type: { type: "string", required: false, - description: "Filter by type (individual, organization, position, project, past)", + description: "Filter by type (individual, organization, position, project, product, past)", }, }, ["type"] @@ -881,6 +1001,16 @@ export const instructions: Record = { "project.wiki": projectWiki, "project.archive": projectArchive, + // product + "product.create": productCreate, + "product.strategy": productStrategy, + "product.spec": productSpec, + "product.release": productRelease, + "product.channel": productChannel, + "product.own": productOwn, + "product.disown": productDisown, + "product.deprecate": productDeprecate, + // census "census.list": censusList, diff --git a/packages/prototype/src/ops.ts b/packages/prototype/src/ops.ts index be58256..9e35bd4 100644 --- a/packages/prototype/src/ops.ts +++ b/packages/prototype/src/ops.ts @@ -377,6 +377,58 @@ export function createOps(ctx: OpsContext): Ops { return archive(await resolve(project), "archive"); }, + // ---- Product ---- + + async "product.create"( + content?: string, + id?: string, + alias?: readonly string[] + ): Promise { + validateGherkin(content); + const node = await rt.create(society, C.product, content, id, alias); + return ok(node, "create"); + }, + + async "product.strategy"(product: string, strategy: string, id?: string): Promise { + validateGherkin(strategy); + const node = await rt.create(await resolve(product), C.strategy, strategy, id); + return ok(node, "strategy"); + }, + + async "product.spec"(product: string, spec: string, id?: string): Promise { + validateGherkin(spec); + const node = await rt.create(await resolve(product), C.spec, spec, id); + return ok(node, "spec"); + }, + + async "product.release"(product: string, release: string, id?: string): Promise { + validateGherkin(release); + const node = await rt.create(await resolve(product), C.release, release, id); + return ok(node, "release"); + }, + + async "product.channel"(product: string, channel: string, id?: string): Promise { + validateGherkin(channel); + const node = await rt.create(await resolve(product), C.channel, channel, id); + return ok(node, "channel"); + }, + + async "product.own"(product: string, individual: string): Promise { + const prodNode = await resolve(product); + await rt.link(prodNode, await resolve(individual), "ownership", "own"); + return ok(prodNode, "own"); + }, + + async "product.disown"(product: string, individual: string): Promise { + const prodNode = await resolve(product); + await rt.unlink(prodNode, await resolve(individual), "ownership", "own"); + return ok(prodNode, "disown"); + }, + + async "product.deprecate"(product: string): Promise { + return archive(await resolve(product), "deprecate"); + }, + // ---- Org ---- async "org.found"(content?: string, id?: string, alias?: readonly string[]): Promise { diff --git a/packages/rolexjs/src/index.ts b/packages/rolexjs/src/index.ts index 9b97efd..c97d965 100644 --- a/packages/rolexjs/src/index.ts +++ b/packages/rolexjs/src/index.ts @@ -28,6 +28,9 @@ export { renderIssueList, renderIssueResult, } from "./issue-render.js"; +// Product Render +export type { ProductAction } from "./product-render.js"; +export { renderProduct, renderProductResult } from "./product-render.js"; // Project Render export type { ProjectAction } from "./project-render.js"; export { renderProject, renderProjectResult } from "./project-render.js"; diff --git a/packages/rolexjs/src/product-render.ts b/packages/rolexjs/src/product-render.ts new file mode 100644 index 0000000..12d3396 --- /dev/null +++ b/packages/rolexjs/src/product-render.ts @@ -0,0 +1,141 @@ +/** + * Product Render — format product state as readable text. + * + * Renders product operations into human-readable summaries. + * Ownership links show owner names only (not full individual trees). + * Specs show behavior contract titles for quick overview. + */ +import type { State } from "@rolexjs/system"; +import { describe, hint } from "./render.js"; + +// ================================================================ +// Types +// ================================================================ + +export type ProductAction = + | "create" + | "strategy" + | "spec" + | "release" + | "channel" + | "own" + | "disown" + | "deprecate"; + +// ================================================================ +// Product Overview +// ================================================================ + +export function renderProduct(state: State): string { + const lines: string[] = []; + const id = state.id ?? "(no id)"; + const tag = state.tag ? ` #${state.tag}` : ""; + + // Title + lines.push(`# ${id}${tag}`); + + // Feature body (vision) + if (state.information) { + lines.push(""); + lines.push(state.information); + } + + // Owners (ownership links — compact) + const owners = state.links?.filter((l) => l.relation === "ownership") ?? []; + if (owners.length > 0) { + lines.push(""); + lines.push("## Owner"); + for (const o of owners) { + const alias = o.target.alias?.length ? ` (${o.target.alias.join(", ")})` : ""; + lines.push(`- ${o.target.id ?? "(no id)"}${alias}`); + } + } + + // Children by type + const children = state.children ?? []; + + const strategies = children.filter((c) => c.name === "strategy"); + const specs = children.filter((c) => c.name === "spec"); + const releases = children.filter((c) => c.name === "release"); + const channels = children.filter((c) => c.name === "channel"); + + if (strategies.length > 0) { + lines.push(""); + lines.push("## Strategy"); + for (const s of strategies) { + if (s.information) lines.push(s.information); + } + } + + if (specs.length > 0) { + lines.push(""); + lines.push("## Specs"); + for (const s of specs) { + const title = s.id ?? extractFeatureTitle(s.information); + lines.push(`- ${title}`); + } + } + + if (releases.length > 0) { + lines.push(""); + lines.push("## Releases"); + for (const r of releases) { + const tag = r.tag ? ` #${r.tag}` : ""; + const title = r.id ?? extractFeatureTitle(r.information); + lines.push(`- ${title}${tag}`); + } + } + + if (channels.length > 0) { + lines.push(""); + lines.push("## Channels"); + for (const c of channels) { + const title = c.id ?? extractFeatureTitle(c.information); + lines.push(`- ${title}`); + } + } + + return lines.join("\n"); +} + +// ================================================================ +// Compose — main entry point +// ================================================================ + +/** + * Render a product operation result as readable text. + * Returns status + hint + product overview. + */ +export function renderProductResult(action: ProductAction, state: State): string { + const name = state.id ?? state.name; + const lines: string[] = []; + + // Layer 1: Status + lines.push(describe(action, name, state)); + + // Layer 2: Hint + lines.push(hint(action)); + + // Layer 3: Product overview + const productState = isProductNode(state) ? state : null; + if (productState) { + lines.push(""); + lines.push(renderProduct(productState)); + } + + return lines.join("\n"); +} + +// ================================================================ +// Helpers +// ================================================================ + +function isProductNode(state: State): boolean { + return state.name === "product"; +} + +function extractFeatureTitle(information?: string | null): string { + if (!information) return "(untitled)"; + const match = information.match(/^Feature:\s*(.+)$/m); + return match?.[1]?.trim() ?? information.split("\n")[0].trim(); +} diff --git a/packages/rolexjs/src/render.ts b/packages/rolexjs/src/render.ts index 5262da7..fb7bd27 100644 --- a/packages/rolexjs/src/render.ts +++ b/packages/rolexjs/src/render.ts @@ -43,6 +43,16 @@ const descriptions: Record string> = { wiki: (n) => `Wiki entry "${n}" added.`, archive: (n) => `Project "${n}" archived.`, + // Product + create: (n) => `Product "${n}" created.`, + strategy: (n) => `Strategy defined for "${n}".`, + spec: (n) => `Spec "${n}" added.`, + release: (n) => `Release "${n}" published.`, + channel: (n) => `Channel "${n}" added.`, + own: (n) => `Owner assigned to "${n}".`, + disown: (n) => `Owner removed from "${n}".`, + deprecate: (n) => `Product "${n}" deprecated.`, + // Role activate: (n) => `Role "${n}" activated.`, focus: (n) => `Focused on goal "${n}".`, @@ -103,6 +113,16 @@ const hints: Record = { wiki: "knowledge entry recorded.", archive: "the project is archived.", + // Product + create: "define strategy, add specs, or assign an owner.", + strategy: "add behavior specs (BDD contracts) for the product.", + spec: "add more specs, or publish a release.", + release: "add distribution channels, or continue adding specs.", + channel: "distribution channel recorded.", + own: "the individual is now the product owner.", + disown: "the individual is no longer the product owner.", + deprecate: "the product is deprecated.", + // Role activate: "want a goal, or check the current state.", focus: "plan how to work toward it, or add tasks.", @@ -258,6 +278,11 @@ const CONCEPT_ORDER: readonly string[] = [ "milestone", "deliverable", "wiki", + // Product + "strategy", + "spec", + "release", + "channel", ]; /** Summarize plan/task completion for a goal heading. */ diff --git a/packages/rolexjs/src/role.ts b/packages/rolexjs/src/role.ts index f0e8e41..9a4084f 100644 --- a/packages/rolexjs/src/role.ts +++ b/packages/rolexjs/src/role.ts @@ -15,6 +15,7 @@ import type { OpResult, Ops } from "@rolexjs/prototype"; import type { RoleContext } from "./context.js"; import { type IssueAction, type LabelResolver, renderIssueResult } from "./issue-render.js"; +import { type ProductAction, renderProductResult } from "./product-render.js"; import { type ProjectAction, renderProjectResult } from "./project-render.js"; import { render } from "./render.js"; @@ -235,6 +236,12 @@ export class Role { const opResult = result as { state: import("@rolexjs/system").State }; return renderProjectResult(action, opResult.state) as T; } + // Render product results as readable text + if (locator.startsWith("!product.")) { + const action = locator.slice("!product.".length) as ProductAction; + const opResult = result as { state: import("@rolexjs/system").State }; + return renderProductResult(action, opResult.state) as T; + } return result; } }