mirror of https://github.com/garrytan/gstack.git
feat(brain): gstack-core@1.0.0 schema pack (T1 / Phase 0)
Defines 8 typed page kinds for the brain entity model: gstack/user-profile, gstack/product, gstack/goal, gstack/developer-persona, gstack/brand, gstack/competitive-intel, gstack/skill-run, gstack/take Each declares frontmatter shape (typed fields with required/optional flags), retention policy (immutable / archive-after-90d / never-archive), and emits_links graph for mcp__gbrain__schema_graph rendering. getSchemaPackMutationPayload() returns JSON in the shape accepted by mcp__gbrain__schema_apply_mutations. Idempotent registration: gbrain skips when pack+version already installed. test/gstack-schema-pack.test.ts: 16 invariants on pack shape, retention policies, link verb consistency, JSON serializability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fc293584df
commit
446a4dce80
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* gstack-core@1.0.0 schema pack (T1 / Phase 0).
|
||||
*
|
||||
* Defines the 7 typed page kinds gstack writes into a personal gbrain:
|
||||
* gstack/user-profile, gstack/product, gstack/goal, gstack/developer-persona,
|
||||
* gstack/brand, gstack/competitive-intel, gstack/skill-run
|
||||
*
|
||||
* Plus the typed take kind gstack writes for Phase 2 calibration:
|
||||
* gstack/take (kind=bet, holder=<user>, with expected_resolution_date)
|
||||
*
|
||||
* Exports JSON consumed by `mcp__gbrain__schema_apply_mutations` at first
|
||||
* /setup-gbrain or /sync-gbrain after this lands. Registration is idempotent
|
||||
* (gbrain's mutation handler skips re-registration when pack version matches).
|
||||
*
|
||||
* Each type carries frontmatter shape + link types. Link inference enables
|
||||
* `mcp__gbrain__schema_graph` to render the gstack subgraph correctly.
|
||||
*/
|
||||
|
||||
import {
|
||||
GSTACK_SCHEMA_PACK_NAME,
|
||||
GSTACK_SCHEMA_PACK_VERSION,
|
||||
} from './brain-cache-spec';
|
||||
|
||||
export interface SchemaFieldShape {
|
||||
name: string;
|
||||
type: 'string' | 'date' | 'number' | 'enum' | 'wikilink-array' | 'string-array';
|
||||
required: boolean;
|
||||
/** For enum types. */
|
||||
values?: ReadonlyArray<string>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SchemaTypeDefinition {
|
||||
/** Page type slug, e.g. `gstack/product`. */
|
||||
type: string;
|
||||
/** Human-readable purpose. Surfaces in `mcp__gbrain__schema_explain_type`. */
|
||||
description: string;
|
||||
/** Per-page-type retention semantics; 'immutable' means never auto-archive. */
|
||||
retention: 'immutable' | 'archive-after-90d' | 'never-archive';
|
||||
/** Frontmatter fields the page MUST or MAY carry. */
|
||||
fields: ReadonlyArray<SchemaFieldShape>;
|
||||
/**
|
||||
* Link types this page emits via `[[wikilink]]` references in body or
|
||||
* frontmatter. Used by gbrain's link inference + schema_graph rendering.
|
||||
*/
|
||||
emits_links?: ReadonlyArray<{ verb: string; target_type: string }>;
|
||||
}
|
||||
|
||||
export interface SchemaPackJSON {
|
||||
name: string;
|
||||
version: string;
|
||||
page_types: ReadonlyArray<SchemaTypeDefinition>;
|
||||
link_verbs: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────── */
|
||||
/* Page type definitions */
|
||||
/* ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const USER_PROFILE: SchemaTypeDefinition = {
|
||||
type: 'gstack/user-profile',
|
||||
description:
|
||||
'Cross-project profile of the gstack user: tone/conviction patterns, ' +
|
||||
'decision tendencies, calibration profile reference. One per user identity. ' +
|
||||
'Read by all planning skills for tone-aware + bias-aware recommendations.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/user-profile' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/user-profile/<user-slug>' },
|
||||
{ name: 'user_slug', type: 'string', required: true, description: 'Resolved per USER_SLUG_RESOLUTION_ORDER' },
|
||||
{ name: 'last_updated_by', type: 'string', required: false, description: 'Last skill that touched this page' },
|
||||
{ name: 'last_updated_at', type: 'date', required: false, description: 'ISO-8601 datetime' },
|
||||
{ name: 'pattern_statements', type: 'string-array', required: false, description: 'Bias tags from calibration (e.g., "under-expands on infra plans")' },
|
||||
{ name: 'taste_signals', type: 'string-array', required: false, description: 'Recurring design/eng preferences observed across reviews' },
|
||||
],
|
||||
emits_links: [
|
||||
{ verb: 'has_calibration', target_type: 'gstack/take' },
|
||||
],
|
||||
};
|
||||
|
||||
const PRODUCT: SchemaTypeDefinition = {
|
||||
type: 'gstack/product',
|
||||
description:
|
||||
'Per-project product model: what the product IS today (value prop, target user, ' +
|
||||
'stage, team), with active goals + recent decisions. Single source of truth ' +
|
||||
'every planning skill consults before asking the user about their product.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/product' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/product/<project-slug>' },
|
||||
{ name: 'title', type: 'string', required: true, description: 'Project / product name' },
|
||||
{ name: 'last_updated_by', type: 'string', required: false, description: '/office-hours or /plan-ceo-review' },
|
||||
{ name: 'last_updated_at', type: 'date', required: false, description: 'ISO-8601' },
|
||||
{ name: 'status', type: 'enum', required: true, values: ['active', 'paused', 'archived'], description: 'Project status' },
|
||||
],
|
||||
emits_links: [
|
||||
{ verb: 'targets', target_type: 'gstack/goal' },
|
||||
{ verb: 'observed_by', target_type: 'gstack/developer-persona' },
|
||||
{ verb: 'has_brand', target_type: 'gstack/brand' },
|
||||
{ verb: 'competes_with', target_type: 'gstack/competitive-intel' },
|
||||
{ verb: 'history', target_type: 'gstack/skill-run' },
|
||||
],
|
||||
};
|
||||
|
||||
const GOAL: SchemaTypeDefinition = {
|
||||
type: 'gstack/goal',
|
||||
description:
|
||||
'A time-bounded outcome the user has committed to (ship X by Y, hit metric Z). ' +
|
||||
'Multiple active goals per project. Auto-flips to status=expired when ' +
|
||||
'expected_resolution date passes; preflight surfaces expired goals for review.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/goal' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/goal/<project-slug>/<goal-id>' },
|
||||
{ name: 'title', type: 'string', required: true, description: 'One-line goal statement' },
|
||||
{ name: 'project', type: 'string', required: true, description: 'project slug' },
|
||||
{ name: 'committed_at', type: 'date', required: true, description: 'When the user committed' },
|
||||
{ name: 'expected_resolution', type: 'date', required: false, description: 'ISO-8601; flips to expired after' },
|
||||
{ name: 'status', type: 'enum', required: true, values: ['active', 'resolved', 'expired', 'archived'], description: 'Lifecycle state' },
|
||||
{ name: 'resolution_note', type: 'string', required: false, description: 'Filled when resolved' },
|
||||
],
|
||||
emits_links: [
|
||||
{ verb: 'belongs_to', target_type: 'gstack/product' },
|
||||
],
|
||||
};
|
||||
|
||||
const DEVELOPER_PERSONA: SchemaTypeDefinition = {
|
||||
type: 'gstack/developer-persona',
|
||||
description:
|
||||
'Per-project model of the target developer using this product (when product ' +
|
||||
'is developer-facing). Captures persona, friction patterns, prior TTHW ' +
|
||||
'measurements. Read by devex + design skills for calibrated recommendations.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/developer-persona' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/developer-persona/<project-slug>' },
|
||||
{ name: 'persona', type: 'string', required: true, description: 'One-line target developer description' },
|
||||
{ name: 'tthw_measurements', type: 'string-array', required: false, description: 'Historical TTHW times with dates' },
|
||||
{ name: 'friction_patterns', type: 'string-array', required: false, description: 'Where developers get stuck' },
|
||||
],
|
||||
};
|
||||
|
||||
const BRAND: SchemaTypeDefinition = {
|
||||
type: 'gstack/brand',
|
||||
description:
|
||||
"Per-project brand voice: visual direction, design language, tone-of-voice. " +
|
||||
'Read by design skills + devex skills (for consistency checks across CLI/docs/UI).',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/brand' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/brand/<project-slug>' },
|
||||
{ name: 'aesthetic', type: 'string', required: false, description: 'e.g., "minimal/typographic"' },
|
||||
{ name: 'typography', type: 'string', required: false, description: 'Font system summary' },
|
||||
{ name: 'color_system', type: 'string', required: false, description: 'Palette summary' },
|
||||
{ name: 'voice', type: 'string', required: false, description: 'Tone of writing' },
|
||||
],
|
||||
};
|
||||
|
||||
const COMPETITIVE_INTEL: SchemaTypeDefinition = {
|
||||
type: 'gstack/competitive-intel',
|
||||
description:
|
||||
'Per-project competitive landscape: incumbents, indirect substitutes, measured ' +
|
||||
'competitor benchmarks (TTHW, pricing, feature parity). Read by CEO + devex.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/competitive-intel' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/competitive-intel/<project-slug>' },
|
||||
{ name: 'competitors', type: 'string-array', required: false, description: 'Named competitors with positioning notes' },
|
||||
{ name: 'benchmarks', type: 'string-array', required: false, description: 'Measured comparison points (TTHW etc.)' },
|
||||
],
|
||||
};
|
||||
|
||||
const SKILL_RUN: SchemaTypeDefinition = {
|
||||
type: 'gstack/skill-run',
|
||||
description:
|
||||
'Every gstack skill invocation that produces output writes one of these on completion. ' +
|
||||
'Time-series log of decisions, modes, mode-selected, outcomes. Powers /retro ' +
|
||||
'and (deferred) /gstack-reflect. Auto-archives to summary-only after 90 days.',
|
||||
retention: 'archive-after-90d',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/skill-run' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/skill-run/<project>/<skill>/<timestamp>' },
|
||||
{ name: 'skill', type: 'string', required: true, description: 'Skill name (e.g., plan-ceo-review)' },
|
||||
{ name: 'project', type: 'string', required: true, description: 'Project slug' },
|
||||
{ name: 'branch', type: 'string', required: false, description: 'Git branch' },
|
||||
{ name: 'commit', type: 'string', required: false, description: 'Short SHA' },
|
||||
{ name: 'duration_s', type: 'number', required: false, description: 'Skill duration in seconds' },
|
||||
{ name: 'outcome', type: 'enum', required: true, values: ['success', 'error', 'aborted'], description: 'Completion state' },
|
||||
{ name: 'mode', type: 'string', required: false, description: 'Mode chosen (for skills with mode)' },
|
||||
{ name: 'decisions', type: 'number', required: false, description: 'Count of AUQ decisions' },
|
||||
{ name: 'takes_written', type: 'number', required: false, description: 'Calibration bets written (E5)' },
|
||||
],
|
||||
emits_links: [
|
||||
{ verb: 'related_to', target_type: 'gstack/product' },
|
||||
{ verb: 'related_to', target_type: 'gstack/goal' },
|
||||
{ verb: 'writes_bet', target_type: 'gstack/take' },
|
||||
],
|
||||
};
|
||||
|
||||
const TAKE: SchemaTypeDefinition = {
|
||||
type: 'gstack/take',
|
||||
description:
|
||||
'Typed predictions (kind=bet) written by planning skills (Phase 2 / E5). ' +
|
||||
'Resolved bets feed the user-profile calibration. Never auto-archived.',
|
||||
retention: 'never-archive',
|
||||
fields: [
|
||||
{ name: 'type', type: 'string', required: true, description: 'gstack/take' },
|
||||
{ name: 'slug', type: 'string', required: true, description: 'gstack/take/<project>/<date>/<id>' },
|
||||
{ name: 'kind', type: 'enum', required: true, values: ['bet', 'hunch', 'fact', 'event'], description: 'Take kind' },
|
||||
{ name: 'holder', type: 'string', required: true, description: 'User identity (whoami / user-slug)' },
|
||||
{ name: 'claim', type: 'string', required: true, description: 'The prediction text' },
|
||||
{ name: 'weight', type: 'number', required: false, description: '0-1 confidence (per-skill from SKILL_CALIBRATION_WEIGHTS)' },
|
||||
{ name: 'since_date', type: 'date', required: false, description: 'When the take was written' },
|
||||
{ name: 'expected_resolution', type: 'date', required: false, description: 'Target resolution date' },
|
||||
{ name: 'resolved_at', type: 'date', required: false, description: 'When marked resolved' },
|
||||
{ name: 'resolved_quality', type: 'enum', required: false, values: ['correct', 'incorrect', 'partial'], description: 'Calibration outcome' },
|
||||
{ name: 'source_skill', type: 'string', required: false, description: 'Which skill wrote this bet' },
|
||||
],
|
||||
emits_links: [
|
||||
{ verb: 'belongs_to', target_type: 'gstack/user-profile' },
|
||||
{ verb: 'origin', target_type: 'gstack/skill-run' },
|
||||
],
|
||||
};
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────── */
|
||||
/* Schema pack assembly */
|
||||
/* ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
export const GSTACK_CORE_SCHEMA_PACK: SchemaPackJSON = {
|
||||
name: GSTACK_SCHEMA_PACK_NAME,
|
||||
version: GSTACK_SCHEMA_PACK_VERSION,
|
||||
page_types: [
|
||||
USER_PROFILE,
|
||||
PRODUCT,
|
||||
GOAL,
|
||||
DEVELOPER_PERSONA,
|
||||
BRAND,
|
||||
COMPETITIVE_INTEL,
|
||||
SKILL_RUN,
|
||||
TAKE,
|
||||
],
|
||||
// Link verbs surface in mcp__gbrain__schema_graph as edge labels.
|
||||
link_verbs: [
|
||||
'has_calibration',
|
||||
'targets',
|
||||
'observed_by',
|
||||
'has_brand',
|
||||
'competes_with',
|
||||
'history',
|
||||
'belongs_to',
|
||||
'related_to',
|
||||
'writes_bet',
|
||||
'origin',
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the JSON shape gbrain's `schema_apply_mutations` MCP op expects.
|
||||
* Idempotent on the brain side: gbrain skips re-registration when pack+version match.
|
||||
*/
|
||||
export function getSchemaPackMutationPayload(): {
|
||||
schema_pack: SchemaPackJSON;
|
||||
schema_version: number;
|
||||
} {
|
||||
return {
|
||||
schema_pack: GSTACK_CORE_SCHEMA_PACK,
|
||||
schema_version: 1, // gbrain mutation API version, not pack version
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns just the page type names. Used by tests + audit subcommand. */
|
||||
export function getSchemaPackTypeNames(): ReadonlyArray<string> {
|
||||
return GSTACK_CORE_SCHEMA_PACK.page_types.map((t) => t.type);
|
||||
}
|
||||
|
||||
/** Returns the retention policy for a given page type. Throws on unknown. */
|
||||
export function getRetentionPolicy(pageType: string): SchemaTypeDefinition['retention'] {
|
||||
const def = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === pageType);
|
||||
if (!def) throw new Error(`Unknown page type: ${pageType}`);
|
||||
return def.retention;
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* gstack-core@1.0.0 schema pack validation (T1).
|
||||
*
|
||||
* Asserts the schema pack is well-formed and matches the v1.48 plan:
|
||||
* - Exactly 8 page types (7 entities + 1 take)
|
||||
* - Frontmatter shape is internally consistent
|
||||
* - Retention policies match SKILL_RUN_RETENTION_DAYS spec
|
||||
* - Link verbs only reference declared verbs
|
||||
* - JSON payload shape is acceptable to mcp__gbrain__schema_apply_mutations
|
||||
*
|
||||
* Gate-tier, free, pure import + assertion.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import {
|
||||
GSTACK_CORE_SCHEMA_PACK,
|
||||
getSchemaPackMutationPayload,
|
||||
getSchemaPackTypeNames,
|
||||
getRetentionPolicy,
|
||||
} from '../scripts/gstack-schema-pack';
|
||||
import {
|
||||
GSTACK_SCHEMA_PACK_NAME,
|
||||
GSTACK_SCHEMA_PACK_VERSION,
|
||||
} from '../scripts/brain-cache-spec';
|
||||
|
||||
describe('gstack-core schema pack', () => {
|
||||
test('identity matches brain-cache-spec constants', () => {
|
||||
expect(GSTACK_CORE_SCHEMA_PACK.name).toBe(GSTACK_SCHEMA_PACK_NAME);
|
||||
expect(GSTACK_CORE_SCHEMA_PACK.version).toBe(GSTACK_SCHEMA_PACK_VERSION);
|
||||
});
|
||||
|
||||
test('declares exactly 8 page types (7 entities + gstack/take)', () => {
|
||||
expect(GSTACK_CORE_SCHEMA_PACK.page_types.length).toBe(8);
|
||||
});
|
||||
|
||||
test('all 7 brain-cache entities have a matching schema page type', () => {
|
||||
const types = getSchemaPackTypeNames();
|
||||
const required = [
|
||||
'gstack/user-profile',
|
||||
'gstack/product',
|
||||
'gstack/goal',
|
||||
'gstack/developer-persona',
|
||||
'gstack/brand',
|
||||
'gstack/competitive-intel',
|
||||
'gstack/skill-run',
|
||||
];
|
||||
for (const name of required) {
|
||||
expect(types).toContain(name);
|
||||
}
|
||||
});
|
||||
|
||||
test('gstack/take exists with kind=bet supported (Phase 2 / E5)', () => {
|
||||
const take = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/take');
|
||||
expect(take).toBeDefined();
|
||||
const kind = take!.fields.find((f) => f.name === 'kind');
|
||||
expect(kind?.values).toContain('bet');
|
||||
expect(kind?.values).toContain('fact');
|
||||
});
|
||||
|
||||
test('every page type has a required type + slug field', () => {
|
||||
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
||||
const typeField = def.fields.find((f) => f.name === 'type');
|
||||
const slugField = def.fields.find((f) => f.name === 'slug');
|
||||
expect(typeField?.required).toBe(true);
|
||||
expect(slugField?.required).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('enum fields declare their values', () => {
|
||||
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
||||
for (const field of def.fields) {
|
||||
if (field.type === 'enum') {
|
||||
expect(field.values).toBeDefined();
|
||||
expect(field.values!.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('skill-run is the only archive-after-90d type', () => {
|
||||
const archived = GSTACK_CORE_SCHEMA_PACK.page_types
|
||||
.filter((t) => t.retention === 'archive-after-90d')
|
||||
.map((t) => t.type);
|
||||
expect(archived).toEqual(['gstack/skill-run']);
|
||||
});
|
||||
|
||||
test('gstack/take is never-archive (calibration scorecard preservation)', () => {
|
||||
expect(getRetentionPolicy('gstack/take')).toBe('never-archive');
|
||||
});
|
||||
|
||||
test('getRetentionPolicy throws on unknown type (defensive)', () => {
|
||||
expect(() => getRetentionPolicy('gstack/nonexistent')).toThrow();
|
||||
});
|
||||
|
||||
test('link verbs declared on emits_links are also in pack.link_verbs', () => {
|
||||
const declared = new Set(GSTACK_CORE_SCHEMA_PACK.link_verbs);
|
||||
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
||||
for (const link of def.emits_links ?? []) {
|
||||
expect(declared.has(link.verb)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('link verbs only target declared gstack/ page types', () => {
|
||||
const declared = new Set(getSchemaPackTypeNames());
|
||||
for (const def of GSTACK_CORE_SCHEMA_PACK.page_types) {
|
||||
for (const link of def.emits_links ?? []) {
|
||||
expect(declared.has(link.target_type)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('mutation payload is well-formed JSON', () => {
|
||||
const payload = getSchemaPackMutationPayload();
|
||||
expect(payload.schema_version).toBe(1);
|
||||
expect(payload.schema_pack).toBeDefined();
|
||||
expect(typeof payload.schema_pack.name).toBe('string');
|
||||
expect(Array.isArray(payload.schema_pack.page_types)).toBe(true);
|
||||
// round-trip through JSON to catch unserializable values (functions, undefined, etc.)
|
||||
const json = JSON.stringify(payload);
|
||||
const reparsed = JSON.parse(json);
|
||||
expect(reparsed.schema_pack.name).toBe(payload.schema_pack.name);
|
||||
});
|
||||
|
||||
test('gstack/product has expected emits_links graph (product → goal/persona/brand/etc.)', () => {
|
||||
const product = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/product')!;
|
||||
const verbs = (product.emits_links ?? []).map((l) => `${l.verb}:${l.target_type}`);
|
||||
expect(verbs).toContain('targets:gstack/goal');
|
||||
expect(verbs).toContain('observed_by:gstack/developer-persona');
|
||||
expect(verbs).toContain('has_brand:gstack/brand');
|
||||
expect(verbs).toContain('competes_with:gstack/competitive-intel');
|
||||
});
|
||||
|
||||
test('gstack/goal has lifecycle status enum (active/resolved/expired/archived)', () => {
|
||||
const goal = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/goal')!;
|
||||
const status = goal.fields.find((f) => f.name === 'status');
|
||||
expect(status?.values).toEqual(['active', 'resolved', 'expired', 'archived']);
|
||||
});
|
||||
|
||||
test('gstack/skill-run records the bet count for calibration coverage', () => {
|
||||
const sr = GSTACK_CORE_SCHEMA_PACK.page_types.find((t) => t.type === 'gstack/skill-run')!;
|
||||
const takesField = sr.fields.find((f) => f.name === 'takes_written');
|
||||
expect(takesField).toBeDefined();
|
||||
expect(takesField?.type).toBe('number');
|
||||
});
|
||||
|
||||
test('gstack/user-profile is never-archive (cross-project, long-lived)', () => {
|
||||
expect(getRetentionPolicy('gstack/user-profile')).toBe('never-archive');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue