Documentation
¶
Overview ¶
Package projectshape introduces named, shared understandings of the implementation pattern a workspace follows — the "architectural archetype" the project lives under (gm-4uso).
A Shape primes personas (PM, coaches, managers) with the right vocabulary and the right default recommendations. A coach helping with a 2-tier-with-SPA shape says different things than one helping a mobile-with-backend shape.
The persona dispatcher injects the active workspace's Shape via the prompt envelope's project-values layer (gm-3d6). Shapes are orthogonal to the gm-9rv persona axes (Personality / Perspective / Purview); this package owns the project-values layer injection only, not Phase or Purview interactions.
This bead (gm-4uso) ships the seed registry of five shapes plus the `.gemba/project_shape.toml` loader. CLI affordances (`gemba shape list`, `gemba shape update`) and operator-authored shape files under `.gemba/shapes/` are deliberately deferred to follow-up beads — the foundation here is the structured Shape type and the dispatcher injection.
Index ¶
Constants ¶
const ConfigFileName = "project_shape.toml"
ConfigFileName is the relative path (under the workspace's `.gemba/` directory) where operators pin the active shape.
Variables ¶
var ErrUnknownShape = errors.New("projectshape: unknown shape")
ErrUnknownShape is returned by [Lookup] when the requested name has no registered shape. Wrapped errors carry the offending name in their text so the operator sees what they typed.
Functions ¶
This section is empty.
Types ¶
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the in-memory lookup table for named shapes. Built once at startup (typically via DefaultRegistry) and treated as read-only thereafter. Methods are safe for concurrent reads; Register is not concurrent-safe, mirroring the persona registry pattern.
func DefaultRegistry ¶
func DefaultRegistry() *Registry
DefaultRegistry returns a Registry pre-populated with the 5 seed shapes from SeedShapes. The returned registry is independent of any other registry the caller already holds — mutations do not leak across calls.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns an empty Registry. Most callers want DefaultRegistry instead — it ships pre-populated with the 5 seed shapes.
func (*Registry) Lookup ¶
Lookup returns the shape registered under name. The bool is false when no shape is registered under that name. Callers that want a typed error (e.g. config validation) should call Registry.Resolve instead.
func (*Registry) LookupOrGeneric ¶
LookupOrGeneric returns the shape registered under name, or a zero-value Shape when name is empty or unknown. The dispatcher uses this on the no-shape-configured fallback path: a zero-value Shape's Render() returns the empty string, so the project-values layer simply contributes nothing — exactly what gm-4uso requires for a workspace that hasn't pinned a shape yet.
func (*Registry) Names ¶
Names returns the registered shape names in ascending order. The `gemba shape list` CLI surface (deferred follow-up bead) consumes this to render its menu.
type Shape ¶
type Shape struct {
// Name is the canonical, stable identifier for this shape.
// Personas reason about a stable identifier (e.g. "two-tier-spa")
// across consults — DO NOT translate the name for display.
Name string
// Description is a one-paragraph blurb used at the top of the
// rendered block. Speaks in the second person to the persona
// ("Your project is …") so the framing reads naturally inline
// with the rest of the system prompt.
Description string
// Tiers names the architectural tiers the project has, in the
// order a request flows through them (e.g. "spa frontend",
// "http api backend", "postgres"). Personas read this to scope
// advice; "your project has no edge tier" follows from this list.
Tiers []string
// RequestPath is the canonical path a typical user request
// follows through the tiers. Phrased as a chain
// ("browser → https → api → postgres"). Concrete; one example
// is more useful than abstract topology fields.
RequestPath string
// Tooling is the set of build/test/deploy tools the operator
// reaches for daily (e.g. "vite", "go test", "goreleaser",
// "systemd"). Keeps coach advice in the operator's vocabulary.
Tooling []string
// DeploymentSurface describes where the project runs in
// production (e.g. "single VPS via systemd", "vercel + edge
// functions", "iOS/Android stores + backend cluster"). Drives
// recommendations about release cadence, rollback strategy, and
// observability defaults.
DeploymentSurface string
}
Shape is one architectural archetype. It captures the tiers a project actually has, the runtime path a typical request follows, the tooling and deployment surface, and a short prose blurb the persona reads as framing.
The fields are deliberately small. A persona doesn't need to know every detail of the build — it needs to know enough to say "you have no edge tier; don't recommend Cloudflare Workers patterns" or "the backend is in Go; suggest go test idioms, not jest".
A zero-value Shape is valid: Render returns the empty string so a dispatcher with no shape configured emits no project-values contribution from this layer.
func LoadConfig ¶
LoadConfig reads `.gemba/project_shape.toml` at path and resolves the named shape via the supplied registry. Behaviour:
- File missing → returns a zero-value Shape and a nil error. The dispatcher's Render() then contributes nothing to the project- values layer, which is the gm-4uso "no shape configured" fallback.
- File present but [project_shape] block absent or `active` empty → returns zero-value Shape, nil error (treat as "no shape pinned").
- File present, `active` set, and the registry has no such shape → returns a wrapped ErrUnknownShape so the operator sees a clean error instead of silent fallback to generic.
- Malformed TOML → wrapped error naming the path.
path MUST be the full path to the file (typically filepath.Join(workspaceDir, ".gemba", ConfigFileName)). The caller owns workspace-dir resolution; this loader is path-pure for ease of testing.
func SeedShapes ¶
func SeedShapes() []Shape
SeedShapes returns the 5 architectural-archetype seed shapes gm-4uso ships. The returned slice is fresh on every call so a caller can mutate it without polluting the registry.
The five seeds (in this order) are:
- gemba — this workspace's shape: two-tier Go server + Vite SPA on a single VPS (matches the bead's first seed verbatim).
- two-tier-spa — generalisation of (1): SPA + backend API.
- two-tier-ssr — server-rendered (Next/Remix/Rails/Django) plus progressive-enhancement JS.
- three-tier-with-edge — SPA → edge functions → origin db/services.
- mobile-with-backend — native/RN/Flutter mobile + backend API + push channel; mobile has its own release cycle.
Names match the bead's seed enumeration verbatim so personas can reason about a stable identifier across consults.
func (Shape) IsZero ¶
IsZero reports whether s carries no shape information. The dispatcher uses this to skip the project-values injection when no shape is configured.
func (Shape) Render ¶
Render returns the human-readable, prompt-ready block the dispatcher injects into the project-values layer. Format is deliberately compact — bulleted facts rather than prose — so the model anchors on the structured signals without inflating the prompt budget.
A zero-value Shape returns the empty string. The dispatcher interprets that as "no shape configured" and omits the project- values contribution entirely (gm-4uso's no-shape fallback).