diff --git a/packages/agents/content/skills/_data/work-types.json b/packages/agents/content/skills/_data/work-types.json index 340dc40f..e08f9670 100644 --- a/packages/agents/content/skills/_data/work-types.json +++ b/packages/agents/content/skills/_data/work-types.json @@ -1,127 +1,133 @@ { - "version": "1.0.0", + "markers": { + "breaking": { + "emoji": "๐Ÿšจ", + "label": "Breaking" + } + }, "tiers": ["public", "internal", "process"], "types": [ { - "key": "feat", "aliases": ["feature"], - "tier": "public", + "breakingPolicy": "optional", "emoji": "๐ŸŽ‰", + "key": "feat", "label": "Features", - "breakingPolicy": "optional" + "tier": "public" }, { - "key": "drop", "aliases": [], - "tier": "public", - "emoji": "๐Ÿ—‘๏ธ", - "label": "Removals", - "breakingPolicy": "required" + "breakingPolicy": "required", + "emoji": "๐Ÿชฆ", + "key": "drop", + "label": "Removed", + "tier": "public" }, { - "key": "deprecate", "aliases": [], - "tier": "public", - "emoji": "โš ๏ธ", - "label": "Deprecations", - "breakingPolicy": "forbidden" + "breakingPolicy": "forbidden", + "emoji": "๐Ÿ—‘๏ธ", + "key": "deprecate", + "label": "Deprecated", + "tier": "public" }, { - "key": "fix", "aliases": ["bugfix"], - "tier": "public", + "breakingPolicy": "forbidden", "emoji": "๐Ÿ›", - "label": "Fixes", - "breakingPolicy": "forbidden" + "key": "fix", + "label": "Bug fixes", + "tier": "public" }, { - "key": "sec", "aliases": ["security"], - "tier": "public", + "breakingPolicy": "optional", "emoji": "๐Ÿ”’", + "key": "sec", "label": "Security", - "breakingPolicy": "optional" + "tier": "public" }, { - "key": "perf", "aliases": ["performance"], - "tier": "public", + "breakingPolicy": "forbidden", "emoji": "โšก", + "key": "perf", "label": "Performance", - "breakingPolicy": "forbidden" + "tier": "public" }, { - "key": "internal", "aliases": ["utility"], - "tier": "internal", - "emoji": "๐Ÿ”ง", - "label": "Internal functionality", - "breakingPolicy": "forbidden" + "breakingPolicy": "forbidden", + "emoji": "๐Ÿ—๏ธ", + "key": "internal", + "label": "Internal features", + "tier": "internal" }, { - "key": "refactor", "aliases": [], - "tier": "internal", + "breakingPolicy": "forbidden", "emoji": "โ™ป๏ธ", + "key": "refactor", "label": "Refactoring", - "breakingPolicy": "forbidden" + "tier": "internal" }, { - "key": "tests", "aliases": ["test"], - "tier": "internal", + "breakingPolicy": "forbidden", "emoji": "๐Ÿงช", + "key": "tests", "label": "Tests", - "breakingPolicy": "forbidden" + "tier": "internal" }, { - "key": "tooling", "aliases": [], - "tier": "process", - "emoji": "๐Ÿ› ๏ธ", + "breakingPolicy": "forbidden", + "emoji": "โš™๏ธ", + "key": "tooling", "label": "Tooling", - "breakingPolicy": "forbidden" + "tier": "process" }, { - "key": "ci", "aliases": [], - "tier": "process", - "emoji": "๐Ÿค–", + "breakingPolicy": "forbidden", + "emoji": "๐Ÿ‘ท", + "key": "ci", "label": "CI", - "breakingPolicy": "forbidden" + "tier": "process" }, { - "key": "deps", "aliases": ["dep"], - "tier": "process", + "breakingPolicy": "forbidden", "emoji": "๐Ÿ“ฆ", + "key": "deps", "label": "Dependencies", - "breakingPolicy": "forbidden" + "tier": "process" }, { - "key": "ai", "aliases": [], - "tier": "process", - "emoji": "๐Ÿง ", - "label": "AI", - "breakingPolicy": "forbidden" + "breakingPolicy": "forbidden", + "emoji": "๐Ÿค–", + "key": "ai", + "label": "Agentic support", + "tier": "process" }, { - "key": "docs", "aliases": ["doc"], - "tier": "process", + "breakingPolicy": "forbidden", "emoji": "๐Ÿ“š", + "key": "docs", "label": "Documentation", - "breakingPolicy": "forbidden" + "tier": "process" }, { - "key": "fmt", "aliases": [], - "tier": "process", - "emoji": "๐Ÿ’…", - "label": "Formatting", "breakingPolicy": "forbidden", - "excludedFromChangelog": true + "emoji": "๐ŸŽจ", + "excludedFromChangelog": true, + "key": "fmt", + "label": "Formatting", + "tier": "process" } - ] + ], + "version": "1.0.0" } diff --git a/packages/agents/content/skills/summarize-change/SKILL.md b/packages/agents/content/skills/summarize-change/SKILL.md index 2f5db85e..e5a73639 100644 --- a/packages/agents/content/skills/summarize-change/SKILL.md +++ b/packages/agents/content/skills/summarize-change/SKILL.md @@ -61,23 +61,23 @@ Good: "Heavy-upload sessions were intermittently failing as users hit the upstre ## Details -### Features +### ๐ŸŽ‰ Features {Only if applicable} -### Fixes +### ๐Ÿ› Bug fixes {Only if applicable} -### Refactoring +### โ™ป๏ธ Refactoring {Only if applicable} -### Tests +### ๐Ÿงช Tests {Only if applicable} -### Dependencies +### ๐Ÿ“ฆ Dependencies {Only if applicable} ``` @@ -88,7 +88,9 @@ Good: "Heavy-upload sessions were intermittently failing as users hit the upstre - The change summary follows **newspaper style** โ€” progressive disclosure from most to least essential: `## What` is the headline (outcome in plain language), `## Why` is the context (motivation and background), `## Details` is the full story (implementation mechanics) - Ignore auto-formatter and lint-fix changes - Omit inapplicable Details subsections -- Order Details subsections per `work-types.json` tier order: public โ†’ internal โ†’ process +- Subsection headings use `{emoji} {label}` from the matching [work-types.json](../_data/work-types.json) `types[]` entry. For any subsection not enumerated in the example template above, look up the entry by work-type key and use its `emoji` and `label`. +- Order Details subsections per `work-types.json` tier order: public โ†’ internal โ†’ process. +- Prefix any individual `## Details` entry that describes a breaking change with `๐Ÿšจ **Breaking:** ` (drawn from `markers.breaking` in [work-types.json](../_data/work-types.json), rendered as `{emoji} **{label}:** `). Trigger conditions: a commit with the `!` breaking marker (e.g., `feat!`) or a `BREAKING CHANGE:` footer. The entry stays under its work-type subsection โ€” the prefix tags it inline rather than relocating it to a separate section. - `## What` and `## Why` are required; Details subsections are optional - Never list automated checks (formatting, linting, typechecking, unit tests) in a test plan. They run automatically in CI. diff --git a/packages/agents/schemas/work-types.schema.json b/packages/agents/schemas/work-types.schema.json index 81e291df..9b1a2d31 100644 --- a/packages/agents/schemas/work-types.schema.json +++ b/packages/agents/schemas/work-types.schema.json @@ -1,77 +1,103 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/williamthorsen/codeassembly/raw/agents-v0.1.0/packages/agents/schemas/work-types.schema.json", - "title": "Work types", - "description": "Canonical work-types taxonomy consumed by skill content and downstream changelog/release-notes tooling. Defines the work-type vocabulary, tier grouping, breaking-change policy, and changelog-eligibility flags.", - "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "required": ["version", "tiers", "types"], + "description": "Canonical work-types taxonomy consumed by skill content and downstream changelog/release-notes tooling. Defines the work-type vocabulary, tier grouping, breaking-change policy, changelog-eligibility flags, and cross-cutting section markers (e.g., the breaking-changes indicator).", "properties": { - "version": { - "type": "string", - "description": "Semantic version of the data shape. Bump when the structure changes in a way that breaks downstream consumers.", - "pattern": "^\\d+\\.\\d+\\.\\d+$" + "markers": { + "additionalProperties": false, + "description": "Cross-cutting section markers used in PR descriptions and release-notes rendering. Orthogonal to `types[]` โ€” these are not work types but visual indicators applied inline to individual entries within their work-type subsection. Add new markers as sibling keys under `markers`; each follows the same `{emoji, label}` shape.", + "properties": { + "breaking": { + "additionalProperties": false, + "description": "Marker for breaking-change entries. Consumed by `summarize-change` as an inline prefix (`{emoji} **{label}:**`, e.g., `๐Ÿšจ **Breaking:**`) on individual `## Details` entries that describe a breaking change; the entry stays under its work-type subsection.", + "properties": { + "emoji": { + "description": "Glyph rendered at the start of the inline breaking-change prefix.", + "minLength": 1, + "type": "string" + }, + "label": { + "description": "Bare tag word rendered inside the inline prefix's bold construction (e.g., `**Breaking:**`). Singular โ€” punctuation belongs to the rendering template.", + "minLength": 1, + "type": "string" + } + }, + "required": ["emoji", "label"], + "type": "object" + } + }, + "required": ["breaking"], + "type": "object" }, "tiers": { - "type": "array", "description": "Canonical tier vocabulary in precedence order (higher tier wins ties). Mirrored at `types[].tier`. Each position is pinned via `prefixItems` so that order is enforced structurally.", + "items": false, + "maxItems": 3, + "minItems": 3, "prefixItems": [ - { "type": "string", "const": "public" }, - { "type": "string", "const": "internal" }, - { "type": "string", "const": "process" } + { "const": "public", "type": "string" }, + { "const": "internal", "type": "string" }, + { "const": "process", "type": "string" } ], - "minItems": 3, - "maxItems": 3, - "items": false + "type": "array" }, "types": { - "type": "array", "description": "The work-type records, in canonical render order. Order is meaningful: when ranking applicable types within a tier, earlier entries win.", "items": { - "type": "object", "additionalProperties": false, - "required": ["key", "aliases", "tier", "emoji", "label", "breakingPolicy"], "properties": { - "key": { - "type": "string", - "description": "Canonical short identifier used in commit messages (e.g., `feat`, `fix`).", - "pattern": "^[a-z][a-z0-9-]*$" - }, "aliases": { - "type": "array", "description": "Alternative identifiers that resolve to the same `key`. Globally unique across all types (asserted in test).", "items": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" }, + "type": "array", "uniqueItems": true }, - "tier": { - "type": "string", - "description": "Which tier this type belongs to. Drives precedence and grouping in skill content.", - "enum": ["public", "internal", "process"] + "breakingPolicy": { + "description": "Whether a breaking-change marker (`!`) is forbidden, optional, or required for this type.", + "enum": ["forbidden", "optional", "required"], + "type": "string" }, "emoji": { - "type": "string", "description": "Single-character (or short) emoji used in section headers and changelog rendering.", - "minLength": 1 + "minLength": 1, + "type": "string" + }, + "excludedFromChangelog": { + "description": "When true, commits of this type are omitted from the generated changelog (e.g., `fmt`).", + "type": "boolean" + }, + "key": { + "description": "Canonical short identifier used in commit messages (e.g., `feat`, `fix`).", + "pattern": "^[a-z][a-z0-9-]*$", + "type": "string" }, "label": { - "type": "string", "description": "Human-readable category label used in section headings (e.g., `Features`, `Fixes`).", - "minLength": 1 - }, - "breakingPolicy": { - "type": "string", - "description": "Whether a breaking-change marker (`!`) is forbidden, optional, or required for this type.", - "enum": ["forbidden", "optional", "required"] + "minLength": 1, + "type": "string" }, - "excludedFromChangelog": { - "type": "boolean", - "description": "When true, commits of this type are omitted from the generated changelog (e.g., `fmt`)." + "tier": { + "description": "Which tier this type belongs to. Drives precedence and grouping in skill content.", + "enum": ["public", "internal", "process"], + "type": "string" } - } - } + }, + "required": ["aliases", "breakingPolicy", "emoji", "key", "label", "tier"], + "type": "object" + }, + "type": "array" + }, + "version": { + "description": "Semantic version of the data shape. Bump when the structure changes in a way that breaks downstream consumers.", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "type": "string" } - } + }, + "required": ["markers", "tiers", "types", "version"], + "title": "Work types", + "type": "object" } diff --git a/packages/agents/src/lib/__tests__/work-types-schema.test.ts b/packages/agents/src/lib/__tests__/work-types-schema.test.ts index c57363e4..8899ac98 100644 --- a/packages/agents/src/lib/__tests__/work-types-schema.test.ts +++ b/packages/agents/src/lib/__tests__/work-types-schema.test.ts @@ -15,20 +15,32 @@ type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } /** Shape of a single record under `types[]` โ€” used to type the live JSON for cross-element checks. */ interface WorkTypeRecord { - key: string; aliases: string[]; + breakingPolicy: string; + emoji: string; + excludedFromChangelog?: boolean; + key: string; + label: string; tier: string; +} + +/** Shape of a single marker record (cross-cutting section indicator). */ +interface MarkerRecord { emoji: string; label: string; - breakingPolicy: string; - excludedFromChangelog?: boolean; +} + +/** Shape of the top-level `markers` block. */ +interface MarkersBlock { + breaking: MarkerRecord; } /** Shape of the live `work-types.json` document โ€” used to type the live JSON for cross-element checks. */ interface WorkTypesDocument { - version: string; + markers: MarkersBlock; tiers: string[]; types: WorkTypeRecord[]; + version: string; } /** @@ -95,16 +107,16 @@ describe('work-types.schema.json', () => { }, { description: 'rejects a type record missing a required field', - // Guards `types[].required: ["key", "aliases", "tier", "emoji", "label", "breakingPolicy"]`. + // Guards `types[].required: ["aliases", "breakingPolicy", "emoji", "key", "label", "tier"]`. // Builds a record without `breakingPolicy`. input: buildMinimalDoc({ types: [ { - key: 'feat', aliases: ['feature'], - tier: 'public', emoji: '๐ŸŽ‰', + key: 'feat', label: 'Features', + tier: 'public', }, ], }), @@ -142,6 +154,48 @@ describe('work-types.schema.json', () => { // catch; if the constraint were widened, this rejection test would fail loudly. input: buildMinimalDoc({ version: 'v1.0.0' }), }, + { + description: 'rejects a document missing the `markers` top-level field', + // Guards top-level `required: ["markers", "tiers", "types", "version"]`. Constructed inline + // because `buildMinimalDoc` always supplies `markers`. + input: { + tiers: ['public', 'internal', 'process'], + types: [], + version: '1.0.0', + }, + }, + { + description: 'rejects a `markers` block missing the `breaking` field', + // Guards `markers.required: ["breaking"]`. + input: buildMinimalDoc({ markers: {} }), + }, + { + description: 'rejects a `markers.breaking` record missing the `emoji` field', + // Guards `markers.breaking.required: ["emoji", "label"]`. + input: buildMinimalDoc({ markers: { breaking: { label: 'Breaking' } } }), + }, + { + description: 'rejects a `markers.breaking` record missing the `label` field', + // Guards `markers.breaking.required: ["emoji", "label"]`. + input: buildMinimalDoc({ markers: { breaking: { emoji: '๐Ÿšจ' } } }), + }, + { + description: 'rejects a `markers.breaking.emoji` that is an empty string', + // Guards `markers.breaking.properties.emoji.minLength: 1`. + input: buildMinimalDoc({ markers: { breaking: { emoji: '', label: 'Breaking' } } }), + }, + { + description: 'rejects a `markers.breaking.label` that is an empty string', + // Guards `markers.breaking.properties.label.minLength: 1`. + input: buildMinimalDoc({ markers: { breaking: { emoji: '๐Ÿšจ', label: '' } } }), + }, + { + description: 'rejects an unknown field on `markers.breaking`', + // Guards `markers.breaking.additionalProperties: false`. + input: buildMinimalDoc({ + markers: { breaking: { emoji: '๐Ÿšจ', extra: 'nope', label: 'Breaking' } }, + }), + }, ])('$description', async ({ input }) => { const output = await validate(schemaId, input, FLAG); expect(output).toMatchObject({ valid: false }); @@ -198,6 +252,15 @@ describe('work-types.schema.json', () => { // schema is ever weakened, this assertion still catches a misordered live file. expect(liveData.tiers).toEqual(['public', 'internal', 'process']); }); + + it('exposes `markers.breaking` with the canonical glyph and label', () => { + // The `summarize-change` skill template prefixes breaking-change entries with + // `{emoji} **{label}:**` (e.g., `๐Ÿšจ **Breaking:**`) using these values directly. + // A silent rename of either field would still pass schema validation but break the + // downstream consumer; assert the values explicitly. + expect(liveData.markers.breaking.emoji).toBe('๐Ÿšจ'); + expect(liveData.markers.breaking.label).toBe('Breaking'); + }); }); // region | Helpers @@ -209,9 +272,15 @@ describe('work-types.schema.json', () => { */ function buildMinimalDoc(overrides: Record = {}): JsonValue { return { - version: '1.0.0', + markers: { + breaking: { + emoji: '๐Ÿšจ', + label: 'Breaking', + }, + }, tiers: ['public', 'internal', 'process'], types: [], + version: '1.0.0', ...overrides, }; } @@ -222,12 +291,12 @@ function buildMinimalDoc(overrides: Record = {}): JsonValue { */ function buildTypeRecord(overrides: Record = {}): JsonValue { return { - key: 'feat', aliases: [], - tier: 'public', + breakingPolicy: 'optional', emoji: '๐ŸŽ‰', + key: 'feat', label: 'Features', - breakingPolicy: 'optional', + tier: 'public', ...overrides, }; }