gstack-schema-pack.ts
281 lines · scripts/gstack-schema-pack.ts at 3f79d81
1/**
2 * gstack-core@1.0.0 schema pack (T1 / Phase 0).
3 *
4 * Defines the 7 typed page kinds gstack writes into a personal gbrain:
5 * gstack/user-profile, gstack/product, gstack/goal, gstack/developer-persona,
6 * gstack/brand, gstack/competitive-intel, gstack/skill-run
7 *
8 * Plus the typed take kind gstack writes for Phase 2 calibration:
9 * gstack/take (kind=bet, holder=<user>, with expected_resolution_date)
10 *
11 * Exports JSON consumed by `mcp__gbrain__schema_apply_mutations` at first
12 * /setup-gbrain or /sync-gbrain after this lands. Registration is idempotent
13 * (gbrain's mutation handler skips re-registration when pack version matches).
14 *
15 * Each type carries frontmatter shape + link types. Link inference enables
16 * `mcp__gbrain__schema_graph` to render the gstack subgraph correctly.
17 */
19import {
20 GSTACK_SCHEMA_PACK_NAME,
21 GSTACK_SCHEMA_PACK_VERSION,
22} from './brain-cache-spec';
24export interface SchemaFieldShape {
25 name: string;
26 type: 'string' | 'date' | 'number' | 'enum' | 'wikilink-array' | 'string-array';
27 required: boolean;
28 /** For enum types. */
29 values?: ReadonlyArray<string>;
30 description: string;
31}
33export interface SchemaTypeDefinition {
34 /** Page type slug, e.g. `gstack/product`. */
35 type: string;
36 /** Human-readable purpose. Surfaces in `mcp__gbrain__schema_explain_type`. */
37 description: string;
38 /** Per-page-type retention semantics; 'immutable' means never auto-archive. */
39 retention: 'immutable' | 'archive-after-90d' | 'never-archive';
40 /** Frontmatter fields the page MUST or MAY carry. */
41 fields: ReadonlyArray<SchemaFieldShape>;
42 /**
43 * Link types this page emits via `[[wikilink]]` references in body or
44 * frontmatter. Used by gbrain's link inference + schema_graph rendering.
45 */
46 emits_links?: ReadonlyArray<{ verb: string; target_type: string }>;
47}
49export interface SchemaPackJSON {
50 name: string;
51 version: string;
52 page_types: ReadonlyArray<SchemaTypeDefinition>;
53 link_verbs: ReadonlyArray<string>;
54}
56/* ────────────────────────────────────────────────────────────────── */
57/* Page type definitions */
58/* ────────────────────────────────────────────────────────────────── */
60const USER_PROFILE: SchemaTypeDefinition = {
61 type: 'gstack/user-profile',
62 description:
63 'Cross-project profile of the gstack user: tone/conviction patterns, ' +
64 'decision tendencies, calibration profile reference. One per user identity. ' +
65 'Read by all planning skills for tone-aware + bias-aware recommendations.',
66 retention: 'never-archive',
67 fields: [
68 { name: 'type', type: 'string', required: true, description: 'gstack/user-profile' },
69 { name: 'slug', type: 'string', required: true, description: 'gstack/user-profile/<user-slug>' },
70 { name: 'user_slug', type: 'string', required: true, description: 'Resolved per USER_SLUG_RESOLUTION_ORDER' },
71 { name: 'last_updated_by', type: 'string', required: false, description: 'Last skill that touched this page' },
72 { name: 'last_updated_at', type: 'date', required: false, description: 'ISO-8601 datetime' },
73 { name: 'pattern_statements', type: 'string-array', required: false, description: 'Bias tags from calibration (e.g., "under-expands on infra plans")' },
74 { name: 'taste_signals', type: 'string-array', required: false, description: 'Recurring design/eng preferences observed across reviews' },
75 ],
76 emits_links: [
77 { verb: 'has_calibration', target_type: 'gstack/take' },
78 ],
79};
81const PRODUCT: SchemaTypeDefinition = {
82 type: 'gstack/product',
83 description:
84 'Per-project product model: what the product IS today (value prop, target user, ' +
85 'stage, team), with active goals + recent decisions. Single source of truth ' +
86 'every planning skill consults before asking the user about their product.',
87 retention: 'never-archive',
88 fields: [
89 { name: 'type', type: 'string', required: true, description: 'gstack/product' },
90 { name: 'slug', type: 'string', required: true, description: 'gstack/product/<project-slug>' },
91 { name: 'title', type: 'string', required: true, description: 'Project / product name' },
92 { name: 'last_updated_by', type: 'string', required: false, description: '/office-hours or /plan-ceo-review' },
93 { name: 'last_updated_at', type: 'date', required: false, description: 'ISO-8601' },
94 { name: 'status', type: 'enum', required: true, values: ['active', 'paused', 'archived'], description: 'Project status' },
95 ],
96 emits_links: [
97 { verb: 'targets', target_type: 'gstack/goal' },
98 { verb: 'observed_by', target_type: 'gstack/developer-persona' },
99 { verb: 'has_brand', target_type: 'gstack/brand' },
100 { verb: 'competes_with', target_type: 'gstack/competitive-intel' },
101 { verb: 'history', target_type: 'gstack/skill-run' },
102 ],
103};
105const GOAL: SchemaTypeDefinition = {
106 type: 'gstack/goal',
107 description:
108 'A time-bounded outcome the user has committed to (ship X by Y, hit metric Z). ' +
109 'Multiple active goals per project. Auto-flips to status=expired when ' +
110 'expected_resolution date passes; preflight surfaces expired goals for review.',
111 retention: 'never-archive',
112 fields: [
113 { name: 'type', type: 'string', required: true, description: 'gstack/goal' },
114 { name: 'slug', type: 'string', required: true, description: 'gstack/goal/<project-slug>/<goal-id>' },
115 { name: 'title', type: 'string', required: true, description: 'One-line goal statement' },
116 { name: 'project', type: 'string', required: true, description: 'project slug' },
117 { name: 'committed_at', type: 'date', required: true, description: 'When the user committed' },
118 { name: 'expected_resolution', type: 'date', required: false, description: 'ISO-8601; flips to expired after' },
119 { name: 'status', type: 'enum', required: true, values: ['active', 'resolved', 'expired', 'archived'], description: 'Lifecycle state' },
120 { name: 'resolution_note', type: 'string', required: false, description: 'Filled when resolved' },
121 ],
122 emits_links: [
123 { verb: 'belongs_to', target_type: 'gstack/product' },
124 ],
125};
127const DEVELOPER_PERSONA: SchemaTypeDefinition = {
128 type: 'gstack/developer-persona',
129 description:
130 'Per-project model of the target developer using this product (when product ' +
131 'is developer-facing). Captures persona, friction patterns, prior TTHW ' +
132 'measurements. Read by devex + design skills for calibrated recommendations.',
133 retention: 'never-archive',
134 fields: [
135 { name: 'type', type: 'string', required: true, description: 'gstack/developer-persona' },
136 { name: 'slug', type: 'string', required: true, description: 'gstack/developer-persona/<project-slug>' },
137 { name: 'persona', type: 'string', required: true, description: 'One-line target developer description' },
138 { name: 'tthw_measurements', type: 'string-array', required: false, description: 'Historical TTHW times with dates' },
139 { name: 'friction_patterns', type: 'string-array', required: false, description: 'Where developers get stuck' },
140 ],
141};
143const BRAND: SchemaTypeDefinition = {
144 type: 'gstack/brand',
145 description:
146 "Per-project brand voice: visual direction, design language, tone-of-voice. " +
147 'Read by design skills + devex skills (for consistency checks across CLI/docs/UI).',
148 retention: 'never-archive',
149 fields: [
150 { name: 'type', type: 'string', required: true, description: 'gstack/brand' },
151 { name: 'slug', type: 'string', required: true, description: 'gstack/brand/<project-slug>' },
152 { name: 'aesthetic', type: 'string', required: false, description: 'e.g., "minimal/typographic"' },
153 { name: 'typography', type: 'string', required: false, description: 'Font system summary' },
154 { name: 'color_system', type: 'string', required: false, description: 'Palette summary' },
155 { name: 'voice', type: 'string', required: false, description: 'Tone of writing' },
156 ],
157};
159const COMPETITIVE_INTEL: SchemaTypeDefinition = {
160 type: 'gstack/competitive-intel',
161 description:
162 'Per-project competitive landscape: incumbents, indirect substitutes, measured ' +
163 'competitor benchmarks (TTHW, pricing, feature parity). Read by CEO + devex.',
164 retention: 'never-archive',
165 fields: [
166 { name: 'type', type: 'string', required: true, description: 'gstack/competitive-intel' },
167 { name: 'slug', type: 'string', required: true, description: 'gstack/competitive-intel/<project-slug>' },
168 { name: 'competitors', type: 'string-array', required: false, description: 'Named competitors with positioning notes' },
169 { name: 'benchmarks', type: 'string-array', required: false, description: 'Measured comparison points (TTHW etc.)' },
170 ],
171};
173const SKILL_RUN: SchemaTypeDefinition = {
174 type: 'gstack/skill-run',
175 description:
176 'Every gstack skill invocation that produces output writes one of these on completion. ' +
177 'Time-series log of decisions, modes, mode-selected, outcomes. Powers /retro ' +
178 'and (deferred) /gstack-reflect. Auto-archives to summary-only after 90 days.',
179 retention: 'archive-after-90d',
180 fields: [
181 { name: 'type', type: 'string', required: true, description: 'gstack/skill-run' },
182 { name: 'slug', type: 'string', required: true, description: 'gstack/skill-run/<project>/<skill>/<timestamp>' },
183 { name: 'skill', type: 'string', required: true, description: 'Skill name (e.g., plan-ceo-review)' },
184 { name: 'project', type: 'string', required: true, description: 'Project slug' },
185 { name: 'branch', type: 'string', required: false, description: 'Git branch' },
186 { name: 'commit', type: 'string', required: false, description: 'Short SHA' },
187 { name: 'duration_s', type: 'number', required: false, description: 'Skill duration in seconds' },
188 { name: 'outcome', type: 'enum', required: true, values: ['success', 'error', 'aborted'], description: 'Completion state' },
189 { name: 'mode', type: 'string', required: false, description: 'Mode chosen (for skills with mode)' },
190 { name: 'decisions', type: 'number', required: false, description: 'Count of AUQ decisions' },
191 { name: 'takes_written', type: 'number', required: false, description: 'Calibration bets written (E5)' },
192 ],
193 emits_links: [
194 { verb: 'related_to', target_type: 'gstack/product' },
195 { verb: 'related_to', target_type: 'gstack/goal' },
196 { verb: 'writes_bet', target_type: 'gstack/take' },
197 ],
198};
200const TAKE: SchemaTypeDefinition = {
201 type: 'gstack/take',
202 description:
203 'Typed predictions (kind=bet) written by planning skills (Phase 2 / E5). ' +
204 'Resolved bets feed the user-profile calibration. Never auto-archived.',
205 retention: 'never-archive',
206 fields: [
207 { name: 'type', type: 'string', required: true, description: 'gstack/take' },
208 { name: 'slug', type: 'string', required: true, description: 'gstack/take/<project>/<date>/<id>' },
209 { name: 'kind', type: 'enum', required: true, values: ['bet', 'hunch', 'fact', 'event'], description: 'Take kind' },
210 { name: 'holder', type: 'string', required: true, description: 'User identity (whoami / user-slug)' },
211 { name: 'claim', type: 'string', required: true, description: 'The prediction text' },
212 { name: 'weight', type: 'number', required: false, description: '0-1 confidence (per-skill from SKILL_CALIBRATION_WEIGHTS)' },
213 { name: 'since_date', type: 'date', required: false, description: 'When the take was written' },
214 { name: 'expected_resolution', type: 'date', required: false, description: 'Target resolution date' },
215 { name: 'resolved_at', type: 'date', required: false, description: 'When marked resolved' },
216 { name: 'resolved_quality', type: 'enum', required: false, values: ['correct', 'incorrect', 'partial'], description: 'Calibration outcome' },
217 { name: 'source_skill', type: 'string', required: false, description: 'Which skill wrote this bet' },
218 ],
219 emits_links: [
220 { verb: 'belongs_to', target_type: 'gstack/user-profile' },
221 { verb: 'origin', target_type: 'gstack/skill-run' },
222 ],
223};
225/* ────────────────────────────────────────────────────────────────── */
226/* Schema pack assembly */
227/* ────────────────────────────────────────────────────────────────── */
229export const GSTACK_CORE_SCHEMA_PACK: SchemaPackJSON = {
230 name: GSTACK_SCHEMA_PACK_NAME,
231 version: GSTACK_SCHEMA_PACK_VERSION,
232 page_types: [
233 USER_PROFILE,
234 PRODUCT,
235 GOAL,
236 DEVELOPER_PERSONA,
237 BRAND,
238 COMPETITIVE_INTEL,
239 SKILL_RUN,
240 TAKE,
241 ],
242 // Link verbs surface in mcp__gbrain__schema_graph as edge labels.
243 link_verbs: [
244 'has_calibration',
245 'targets',
246 'observed_by',
247 'has_brand',
248 'competes_with',
249 'history',
250 'belongs_to',
251 'related_to',
252 'writes_bet',
253 'origin',
254 ],
255};
257/**
258 * Returns the JSON shape gbrain's `schema_apply_mutations` MCP op expects.
259 * Idempotent on the brain side: gbrain skips re-registration when pack+version match.
260 */
261export function getSchemaPackMutationPayload(): {
262 schema_pack: SchemaPackJSON;
263 schema_version: number;
264} {
265 return {
266 schema_pack: GSTACK_CORE_SCHEMA_PACK,
267 schema_version: 1, // gbrain mutation API version, not pack version
268 };
269}
271/** Returns just the page type names. Used by tests + audit subcommand. */
272export function getSchemaPackTypeNames(): ReadonlyArray<string> {
273 return GSTACK_CORE_SCHEMA_PACK.page_types.map((t) => t.type);
274}
276/** Returns the retention policy for a given page type. Throws on unknown. */
277export function getRetentionPolicy(pageType: string): SchemaTypeDefinition['retention'] {
278 const def = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === pageType);
279 if (!def) throw new Error(`Unknown page type: ${pageType}`);
280 return def.retention;
281}