@3fn/core 14.0.0 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.kiro/steering/Civitas-System-Overview.md +3 -12
  2. package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
  3. package/.kiro/steering/Task-Completion-Protocol.md +7 -7
  4. package/.kiro/steering/start-up-tasks.md +4 -4
  5. package/dist/ComponentTokens.android.kt +12 -12
  6. package/dist/ComponentTokens.ios.swift +12 -12
  7. package/dist/ComponentTokens.web.css +3 -3
  8. package/dist/DesignTokens.android.kt +1 -1
  9. package/dist/DesignTokens.dtcg.json +2 -2
  10. package/dist/DesignTokens.ios.swift +1 -1
  11. package/dist/DesignTokens.web.css +1 -1
  12. package/dist/android/DesignTokens.android.kt +1 -1
  13. package/dist/browser/designerpunk.esm.js +14 -9
  14. package/dist/browser/designerpunk.esm.min.js +7 -7
  15. package/dist/browser/designerpunk.umd.js +14 -9
  16. package/dist/browser/designerpunk.umd.min.js +12 -12
  17. package/dist/browser/tokens.css +3 -3
  18. package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
  19. package/dist/build/tokens/defineComponentTokens.js +26 -0
  20. package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
  21. package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
  22. package/dist/components/core/Avatar-Base/index.d.ts +1 -1
  23. package/dist/components/core/Avatar-Base/index.js +2 -2
  24. package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
  25. package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
  26. package/dist/generators/TokenFileGenerator.js +7 -2
  27. package/dist/ios/DesignTokens.ios.swift +1 -1
  28. package/dist/tokens/component/progress.d.ts +65 -5
  29. package/dist/tokens/component/progress.js +79 -18
  30. package/dist/types/generated/TokenTypes.d.ts +1 -1
  31. package/dist/types/generated/TokenTypes.js +1 -1
  32. package/dist/web/DesignTokens.web.css +1 -1
  33. package/governance/BUILD-SYSTEM-SETUP.md +1 -2
  34. package/governance/Component-Development-Guide.md +1 -1
  35. package/governance/Component-Development-Standards.md +19 -18
  36. package/governance/Component-Family-Avatar.md +6 -6
  37. package/governance/Component-Family-Badge.md +19 -19
  38. package/governance/Component-Family-Button.md +26 -26
  39. package/governance/Component-Family-Chip.md +14 -14
  40. package/governance/Component-Family-Container.md +12 -12
  41. package/governance/Component-Family-Form-Inputs.md +61 -61
  42. package/governance/Component-Family-Icon.md +8 -8
  43. package/governance/Component-Family-Navigation.md +1 -1
  44. package/governance/Component-Inheritance-Structures.md +29 -28
  45. package/governance/Component-Readiness-Status.md +6 -7
  46. package/governance/Component-Templates.md +26 -26
  47. package/governance/Contract-System-Reference.md +1 -1
  48. package/governance/Process-Development-Workflow.md +4 -4
  49. package/governance/Process-File-Organization.md +1 -4
  50. package/governance/Process-Hook-Operations.md +6 -7
  51. package/governance/Process-Spec-Planning.md +8 -14
  52. package/governance/Rosetta-System-Architecture.md +7 -5
  53. package/governance/Token-Quick-Reference.md +35 -22
  54. package/governance/Web-Authoring-Standards.md +1 -1
  55. package/governance/classification-map.md +109 -3
  56. package/governance/completion-documentation-guide.md +14 -31
  57. package/governance/platform-implementation-guidelines.md +1 -2
  58. package/governance/release-management-system.md +28 -63
  59. package/package.json +2 -6
  60. package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
  61. package/src/build/tokens/defineComponentTokens.ts +43 -1
  62. package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
  63. package/src/components/core/Avatar-Base/index.ts +1 -1
  64. package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
  65. package/src/generators/TokenFileGenerator.ts +7 -2
  66. package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
  67. package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
  68. package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
  69. package/src/tokens/component/progress.ts +83 -21
  70. package/src/types/generated/TokenTypes.ts +1 -1
  71. package/token-index/components.yaml +8 -8
  72. package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
  73. package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
  74. package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
  75. package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
  76. package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
  77. package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
  78. package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
  79. package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
  80. package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
  81. package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
  82. package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
  83. package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
  84. package/src/tools/release/cli/ReleasePipeline.ts +0 -165
  85. package/src/tools/release/cli/release-tool.ts +0 -107
  86. package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
  87. package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
  88. package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
  89. package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
  90. package/src/tools/release/pipeline/TagResolver.ts +0 -40
  91. package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
  92. package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
  93. package/src/tools/release/publishers/NpmPublisher.ts +0 -196
  94. package/src/tools/release/release-config.json +0 -5
  95. package/src/tools/release/types/index.ts +0 -282
  96. package/src/tools/release/validators/SemanticVersionValidator.ts +0 -67
@@ -1,14 +1,19 @@
1
1
  ---
2
2
  id: release-management-system
3
3
  inclusion: manual
4
+ name: Release Management System
5
+ description: The release recipe (derive-classify-ratify, hand-authored notes) and how agents discover what changed and why — replaces the retired automated release tool
6
+ aliases: release recipe, release process, release notes, what changed, version bump, changelog, release delta
4
7
  ---
5
8
 
6
9
  # Release Management System
7
10
 
11
+ > **Audience framing**: this documents **DesignerPunk's own** release process. Consumers of the package read it as a worked example of a recipe-over-tooling release model — the paths, scripts, and named roles below are DesignerPunk's, not yours. (Whether DesignerPunk's release notes themselves ship to consumers is an open Spec 123 question, deferred by the Q6 ballot.)
12
+
8
13
  **Date**: 2026-02-28
9
- **Last Reviewed**: 2026-07-05
10
- **Last Updated**: 2026-02-28
11
- **Purpose**: Mental model of the release management system for AI agents
14
+ **Last Reviewed**: 2026-08-12
15
+ **Last Updated**: 2026-08-12
16
+ **Purpose**: Mental model of the release process for AI agents — the recipe, and how to discover what changed and why
12
17
  **Organization**: process-standard
13
18
  **Scope**: cross-project
14
19
  **Layer**: 2
@@ -18,72 +23,32 @@ inclusion: manual
18
23
 
19
24
  ## Overview
20
25
 
21
- The release tool is an on-demand CLI at `src/tools/release/`. It replaces the previous 203-file system with a focused pipeline: discover summary docs since last git tag → extract structured changes → classify by priority → recommend version bump → generate markdown release notes → optionally create GitHub release.
22
-
23
- **Key principles:**
24
- - Runs on-demand only. No timers, no hooks, no passive file generation.
25
- - Git tags are the only persistent state. No state files, no caches, no history accumulation.
26
- - Human-reviewed before publishing. The tool recommends; Peter decides.
26
+ Releases are executed by a **documented recipe, not a standing tool**. The automated release manager (an on-demand CLI that scanned spec summary docs to recommend versions and generate notes) was **RETIRED on 2026-08-12** — Q6 ballot: `.kiro/docs/ballots/2026-08-12-q6-release-manager-retirement.md`. It was retired because the PR gate made it redundant-and-worse: every merge to `main` is one squash commit whose title is a disciplined change description, so the release delta is derivable with one `git log` — while the tool, reading only spec summaries, was structurally blind to issue-driven work and mis-recommended the v14.0.0 release outright (patch/"no consumer-facing changes" against a breaking component wave).
27
27
 
28
- ---
28
+ **Key principles (unchanged by the retirement):**
29
+ - Human-reviewed, human-decided: the recipe derives and drafts; **the repo's release owner (in DesignerPunk: Peter) ratifies the version bump and merges the release PR**.
30
+ - Git tags are the only persistent release state.
31
+ - Verification stays mechanized; judgment stays human. DesignerPunk's publish guard scripts (`check:drift`, `verify:token-index-clean`, the `prepublishOnly` chain — this repo's package scripts, not shipped to consumers) block a broken publish mechanically and are NOT part of the retired tool.
29
32
 
30
- ## Architecture
33
+ ## The Release Recipe
31
34
 
32
- ```
33
- CLI Entry Point (src/tools/release/cli/release-tool.ts)
34
- ├── analyze → ReleasePipeline.analyze()
35
- ├── notes → ReleasePipeline.generateNotes()
36
- └── release → ReleasePipeline.release()
35
+ The operational sequence lives in `.kiro/hooks/RELEASE-FLOW.md` (the PR-gated release flow). The judgment half, summarized:
37
36
 
38
- ReleasePipeline (src/tools/release/cli/ReleasePipeline.ts)
39
- ├── TagResolver — git describe --tags --abbrev=0
40
- ├── SummaryScanner — git log + glob docs/specs/*/task-*-summary.md
41
- ├── ChangeExtractor — parse summary doc markdown sections
42
- ├── ChangeClassifier — map to 🔴 breaking / 🟡 prominent / 🔵 context
43
- ├── NotesRenderer — markdown generation (public + internal)
44
- └── GitHubPublisher — git tag + GitHub release creation
45
- ```
46
-
47
- ---
37
+ 1. **Derive the delta**: `git log $(git describe --tags --abbrev=0)..main --oneline` — every line is a squash-merged PR title (the changelog spine). Scope a second pass to the shipped surface to separate consumer-facing from internal — **the authoritative shipped-surface list is `package.json` `files[]`** (fifteen-plus roots beyond `src/`, including `governance/` and the other served-content roots; scoping to `src/` alone would have dropped v14.0.0's docs-corpus entry).
38
+ 2. **Classify**: for each change, read its task summary (`docs/specs/…`) or PR body for substance; classify 🔴 breaking / 🟡 minor / 🔵 patch-or-internal. Issue-driven work has no summary doc — its PR title and body are the record; do not assume spec-shaped work is the whole delta (the retired tool's fatal assumption).
39
+ 3. **Recommend the bump; the release owner ratifies.** Removals or behavior breaks → major. New behavior → minor. Fixes/internal → patch.
40
+ 4. **Hand-author the notes** at `docs/releases/release-X.Y.Z.md` (v14.0.0 is the format precedent). Notes ride the release PR with the version bump and any token-index regeneration.
41
+ 5. **Publish per RELEASE-FLOW.md** (repo-internal; and the dual-registry playbook it references): release PR → the release owner merges → publish from merged `main` → then tag and GitHub release: `git tag -a vX.Y.Z && git push origin vX.Y.Z && gh release create vX.Y.Z --notes-file docs/releases/release-X.Y.Z.md`.
48
42
 
49
- ## CLI Commands
50
-
51
- | Command | What It Does | When to Use |
52
- |---------|-------------|-------------|
53
- | `npm run release:analyze` | Scan changes since last tag, display recommendation | Check what's accumulated |
54
- | `npm run release:notes` | Generate markdown release notes to `docs/releases/` | Preview release content |
55
- | `npm run release:run` | Full release: notes + tag + GitHub publish | Actual release |
56
- | `npm run release:run -- --dry-run` | Preview release without tagging or publishing | Pre-release check |
57
-
58
- Shell wrapper: `./.kiro/hooks/release-manager.sh analyze|notes|release`
59
-
60
- ---
43
+ ## Discovering What Changed and Why
61
44
 
62
- ## AI Agent Decision Points
45
+ Agents answering "what changed, and why?" — for any purpose, not just releases — follow the record chain. *(Consumer note: in a consumer repo only the served governance corpus is reachable; the chain's other paths are DesignerPunk-internal — the PATTERN transfers, the paths don't.)*
63
46
 
64
- ### 1. Summary Document Quality
65
- Release notes are generated from summary docs. Better summaries → better release notes.
66
- - **What Was Done** → becomes the change description
67
- - **Key Changes** → becomes the bullet points
68
- - **Deliverables** → drives priority classification (🔴/🟡/🔵)
47
+ 1. **What, at a glance**: squash-commit titles on `main` (`git log vX..vY --oneline`, or between any two points). Every commit is a PR with a disciplined title.
48
+ 2. **What, curated per release**: `docs/releases/release-X.Y.Z.md` — hand-authored, consumer-facing framing, breaking changes called out with migration guidance.
49
+ 3. **Why, per task**: `docs/specs/[spec]/task-N-summary.md` (public summary) and `.kiro/specs/[spec]/completion/` (detailed record) — for spec-shaped work. For issue-driven work: the PR body and any `.kiro/issues/` record.
50
+ 4. **Why, decision-grade**: `.kiro/docs/ballots/` (ratified decisions with their evidence and counter-arguments) and `governance/classification-map.md` (per-rule classifications with dated history). When a change traces to a ruling, the ballot is the authoritative why.
69
51
 
70
- ### 2. Deliverables Section
71
- When present, the `## Deliverables *(optional)*` section drives classification directly:
72
- - `🔴` → breaking/consumer-facing → major bump
73
- - `🟡` → ecosystem → minor bump
74
- - `🔵` → internal/context → patch bump
75
-
76
- When absent, keyword heuristics apply (less accurate, human-reviewed anyway).
77
-
78
- ### 3. Summary Document Location
79
- Must be `docs/specs/[spec-name]/task-N-summary.md` — the scanner looks here via git log.
80
-
81
- ---
82
-
83
- ## Post-Commit Analysis
84
-
85
- `release:analyze` runs post-merge on `main` (non-blocking, informational) — merged history is the analysis's correct input. Run `npm run release:analyze` locally for on-demand detail.
86
-
87
- ---
52
+ ## Historical Note
88
53
 
89
- *For operational task completion workflow, see Process-Development-Workflow.md.*
54
+ The retired tool's own history is instructive: it replaced a 203-file predecessor (Spec 065's rebuild), and its retirement continues that simplification — 203 files → 24 files → a recipe — completed once the PR gate supplied, as a by-product of merge discipline, the structured change record the tooling existed to reconstruct. Records: Spec 065 (the rebuild), the Q6 ballot (the retirement), `docs/releases/release-14.0.0.md` (the live trial that settled it).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3fn/core",
3
- "version": "14.0.0",
3
+ "version": "14.1.0",
4
4
  "description": "True Native cross-platform design system with mathematical foundations",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Peter Michaels Allen",
@@ -115,11 +115,6 @@
115
115
  "test:performance:isolated": "jest --config jest.performance.config.js --runInBand",
116
116
  "test:watch": "jest --watch --config jest.functional.config.js",
117
117
  "test:coverage": "jest --coverage --config jest.functional.config.js",
118
- "release:analyze": "npx tsx src/tools/release/cli/release-tool.ts analyze",
119
- "release:notes": "npx tsx src/tools/release/cli/release-tool.ts notes",
120
- "release:run": "npx tsx src/tools/release/cli/release-tool.ts release",
121
- "validate:release-setup": "node scripts/validate-release-setup.js",
122
- "diagnose:release-issues": "node scripts/diagnose-release-issues.js",
123
118
  "audit:tokens": "node scripts/audit-component-tokens.js",
124
119
  "audit:tokens:detailed": "node scripts/audit-component-tokens.js -- --detailed",
125
120
  "audit:coverage-map": "npx tsx tools/agent-generator/coverage-map.ts",
@@ -133,6 +128,7 @@
133
128
  "figma:generate-component": "npx tsx scripts/figma-component-generator.ts",
134
129
  "check:drift": "node scripts/check-package-name-drift.js",
135
130
  "check:id-uniqueness": "tsx scripts/check-id-uniqueness.ts",
131
+ "check:section-citations": "tsx scripts/check-section-citations.ts",
136
132
  "prepack": "npm run build",
137
133
  "verify:token-index-clean": "git diff --quiet token-index/ || (echo '✖ token-index/ is stale relative to the committed state. Regenerate and commit it on the RELEASE BRANCH (it traverses the PR gate with the version bump) before publishing. See .kiro/hooks/RELEASE-FLOW.md.' && exit 1)",
138
134
  "prepublishOnly": "npm run build && npm run check:drift && npm run verify:token-index-clean",
@@ -392,6 +392,119 @@ describe('defineComponentTokens', () => {
392
392
  });
393
393
  });
394
394
 
395
+ describe('Family-mismatch guard (reference path)', () => {
396
+ // A reference literal that carries `category`, as every real primitive in
397
+ // src/tokens/** does. The guard keys off this field.
398
+ const createMockPrimitiveWithCategory = (
399
+ name: string,
400
+ baseValue: number,
401
+ category: string
402
+ ): PrimitiveTokenReference => ({ name, baseValue, category });
403
+
404
+ test('throws when a reference primitive belongs to a different family than declared', () => {
405
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
406
+
407
+ expect(() =>
408
+ defineComponentTokens({
409
+ component: 'ButtonIcon',
410
+ family: 'spacing',
411
+ tokens: {
412
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
413
+ },
414
+ })
415
+ ).toThrow(/Token family mismatch/);
416
+ });
417
+
418
+ test('error names the component, token, declared family, primitive and its real family', () => {
419
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
420
+
421
+ let message = '';
422
+ try {
423
+ defineComponentTokens({
424
+ component: 'ButtonIcon',
425
+ family: 'spacing',
426
+ tokens: {
427
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
428
+ },
429
+ });
430
+ } catch (error) {
431
+ message = (error as Error).message;
432
+ }
433
+
434
+ expect(message).toContain("component 'ButtonIcon'");
435
+ expect(message).toContain("token 'size.large'");
436
+ expect(message).toContain("'spacing' family call");
437
+ expect(message).toContain("'size600'");
438
+ expect(message).toContain("'sizing' family");
439
+ });
440
+
441
+ test('regression: the real Button-Icon defect (PR #126) now fails at authoring time', () => {
442
+ // Before PR #126, buttonIcon.tokens.ts declared family 'spacing' while referencing
443
+ // sizing primitives. That produced `SpacingTokens.size600` — a member no generated
444
+ // platform file defines — and was caught only by reading generated Swift/Kotlin.
445
+ expect(() =>
446
+ defineComponentTokens({
447
+ component: 'ButtonIcon',
448
+ family: 'spacing',
449
+ tokens: {
450
+ 'inset.large': {
451
+ reference: createMockPrimitiveWithCategory('space150', 12, 'spacing'),
452
+ reasoning: 'Correctly family-matched spacing token',
453
+ },
454
+ 'size.large': {
455
+ reference: createMockPrimitiveWithCategory('size600', 48, 'sizing'),
456
+ reasoning: 'Mis-stamped sizing token — the defect',
457
+ },
458
+ },
459
+ })
460
+ ).toThrow(/references primitive 'size600' from the 'sizing' family/);
461
+ });
462
+
463
+ test('accepts a reference whose category matches the declared family', () => {
464
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
465
+
466
+ const result = defineComponentTokens({
467
+ component: 'ButtonIcon',
468
+ family: 'sizing',
469
+ tokens: {
470
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
471
+ },
472
+ });
473
+
474
+ expect(result['size.large']).toBe(48);
475
+ expect((getTokenContract(result) ?? [])[0].family).toBe('sizing');
476
+ });
477
+
478
+ test('does not fire for reference literals without a category (back-compat)', () => {
479
+ const bareRef = createMockPrimitiveToken('space100', 8);
480
+
481
+ expect(() =>
482
+ defineComponentTokens({
483
+ component: 'ButtonIcon',
484
+ family: 'sizing',
485
+ tokens: {
486
+ 'inset.small': { reference: bareRef, reasoning: 'Bare reference literal' },
487
+ },
488
+ })
489
+ ).not.toThrow();
490
+ });
491
+
492
+ test('does not fire for value-path tokens (the Avatar icon-size case is NOT covered)', () => {
493
+ // Documents a real limitation: the Avatar `icon.size.*` gap fillers were mis-stamped
494
+ // `family: 'spacing'` on the VALUE path. No reference exists to cross-check, so this
495
+ // guard cannot catch that class of mislabel.
496
+ expect(() =>
497
+ defineComponentTokens({
498
+ component: 'Avatar',
499
+ family: 'spacing',
500
+ tokens: {
501
+ 'icon.size.xs': { value: 12, reasoning: 'Dimensional value in a spacing call' },
502
+ },
503
+ })
504
+ ).not.toThrow();
505
+ });
506
+ });
507
+
395
508
  describe('Idempotent re-branding (Spec 124, caveat c)', () => {
396
509
  test('re-applying the brand to the same return does not throw', () => {
397
510
  const result = defineComponentTokens({
@@ -65,6 +65,16 @@ export interface PrimitiveTokenReference {
65
65
  name: string;
66
66
  /** Unitless base value from the primitive token */
67
67
  baseValue: number;
68
+ /**
69
+ * Primitive token family (the `category` field of a `PrimitiveToken`, e.g. 'spacing',
70
+ * 'sizing', 'radius'). OPTIONAL for backward compatibility: minimal `{ name, baseValue }`
71
+ * reference literals remain valid.
72
+ *
73
+ * When present, it is cross-checked against the call's declared `family` by the
74
+ * family-mismatch guard in {@link defineComponentTokens}. Real primitives from
75
+ * src/tokens/** always carry it, so the guard is active for all production authoring.
76
+ */
77
+ category?: string;
68
78
  }
69
79
 
70
80
  /**
@@ -216,8 +226,40 @@ export function defineComponentTokens<T extends Record<string, TokenDefinition>>
216
226
  if (isTokenWithReference(definition)) {
217
227
  // Token with primitive reference
218
228
  const primitiveToken = definition.reference;
229
+
230
+ // FAMILY-MISMATCH GUARD.
231
+ // The call's `family` is stamped onto every token it registers, and the generator
232
+ // derives platform output from it — notably the primitive class name used for
233
+ // reference-path tokens (getFamilyClassName) and the Android `.dp` suffix. A call
234
+ // that declares one family but references another family's primitive therefore emits
235
+ // a member that does not exist on the target platform. That is exactly the Button-Icon
236
+ // defect: a `family: 'spacing'` call referencing `sizingTokens.size600` generated
237
+ // `SpacingTokens.size600`, a non-existent member (PR #126).
238
+ //
239
+ // Only enforced when the reference carries a `category` — minimal `{ name, baseValue }`
240
+ // literals (used widely in tests) are intentionally exempt rather than rejected.
241
+ //
242
+ // Escape hatch: a token that legitimately needs another family's VALUE should use the
243
+ // value path (`value:`), which asserts no cross-family token-chain claim.
244
+ if (
245
+ typeof primitiveToken.category === 'string' &&
246
+ primitiveToken.category !== family
247
+ ) {
248
+ throw new Error(
249
+ `Token family mismatch in defineComponentTokens() for component '${component}': ` +
250
+ `token '${key}' is declared in a '${family}' family call but references primitive ` +
251
+ `'${primitiveToken.name}' from the '${primitiveToken.category}' family. ` +
252
+ `Every token in a call is stamped with that call's family, which drives platform ` +
253
+ `output (e.g. the generated '${family.charAt(0).toUpperCase() + family.slice(1)}Tokens' ` +
254
+ `class reference), so this would emit a non-existent platform member. ` +
255
+ `Fix: move '${key}' into a separate defineComponentTokens() call with ` +
256
+ `family: '${primitiveToken.category}', or use the value path if no token-chain ` +
257
+ `relationship is intended.`
258
+ );
259
+ }
260
+
219
261
  const value = primitiveToken.baseValue;
220
-
262
+
221
263
  values[key] = value;
222
264
  registeredTokens.push({
223
265
  name: tokenName,
@@ -33,7 +33,16 @@
33
33
  * Icon Size Derivations (in Avatar.web.ts):
34
34
  * - xs: calc(icon.size050 × 0.75) = 12px
35
35
  * - xxl: calc(icon.size050 × 4) = 64px
36
- *
36
+ *
37
+ * TOKEN FAMILY: all dimensional Avatar tokens (container `size.*` AND icon
38
+ * `icon.size.*` gap fillers) are registered by the single sizing-family call
39
+ * `AvatarSizingTokens` below. The `icon.size.*` pair previously lived in a
40
+ * separate `AvatarTokens` call stamped `family: 'spacing'` — a mislabel, since
41
+ * both are dimensional sizing values. `AvatarTokens` is removed; consumers use
42
+ * `AvatarSizingTokens` or the `getAvatarIconSize()` / `getAvatarSize()` accessors.
43
+ * Generated platform output is unaffected: both calls declare `component: 'Avatar'`,
44
+ * so all tokens still land in one `AvatarTokens` Swift enum / Kotlin object.
45
+ *
37
46
  * COLOR TOKENS (Spec 058):
38
47
  * Avatar color tokens are defined in this file following the Rosetta System architecture
39
48
  * which mandates component tokens live at src/components/[ComponentName]/tokens.ts.
@@ -56,13 +65,24 @@ import { defineComponentTokens } from '../../../build/tokens';
56
65
  import { SIZING_BASE_VALUE, sizingTokens } from '../../../tokens/SizingTokens';
57
66
 
58
67
  /**
59
- * Avatar sizing tokens — container dimensions for each size variant.
68
+ * Avatar sizing tokens — container dimensions and icon dimensions for each size variant.
60
69
  *
61
70
  * Previously in a separate file (avatar-sizing.tokens.ts). Inlined to prevent
62
71
  * architectural anomaly (no other component splits tokens across files) and
63
72
  * simplify the package surface for sync.
64
73
  *
74
+ * ONE CALL PER FAMILY. Every token registered by a single defineComponentTokens()
75
+ * call is stamped with that call's `family`, and the family drives platform output
76
+ * (Swift type, Kotlin `.dp` suffix, primitive class name). The `icon.size.*` gap
77
+ * fillers below are dimensional sizing values, so they live in this sizing-family
78
+ * call — they were previously mis-stamped `family: 'spacing'` in a separate
79
+ * `AvatarTokens` call, the same latent mislabel class fixed for Button-Icon.
80
+ * Do NOT spread/merge this result with another call's result — the rich metadata
81
+ * rides on a non-enumerable brand (see src/build/tokens/defineComponentTokens.ts,
82
+ * TOKEN_CONTRACT_BRAND) that a spread would silently drop.
83
+ *
65
84
  * @see .kiro/specs/092-sizing-token-family/design.md
85
+ * @see src/components/core/Button-Icon/buttonIcon.tokens.ts for the two-call pattern
66
86
  */
67
87
  export const AvatarSizingTokens = defineComponentTokens({
68
88
  component: 'Avatar',
@@ -92,38 +112,15 @@ export const AvatarSizingTokens = defineComponentTokens({
92
112
  reference: sizingTokens.size1600,
93
113
  reasoning: 'Extra extra large avatar (128px). Full profile view, onboarding.',
94
114
  },
95
- },
96
- });
97
115
 
98
- /**
99
- * Avatar component tokens defined using the hybrid authoring API.
100
- *
101
- * Each token either references a primitive spacing token or uses a family-conformant
102
- * derivation, and includes reasoning explaining why the token exists.
103
- *
104
- * NOTE: Web platform uses CSS calc() with icon tokens for sizing (see Avatar.styles.css).
105
- * These component tokens are primarily used for iOS/Android platforms and documentation.
106
- *
107
- * Size token values:
108
- * - size.xs: 24px (3 × base, references size300)
109
- * - size.sm: 32px (4 × base, references size400)
110
- * - size.md: 40px (5 × base, references size500)
111
- * - size.lg: 48px (6 × base, references size600)
112
- * - size.xl: 80px (10 × base, derivation)
113
- * - size.xxl: 128px (16 × base, derivation)
114
- *
115
- * Icon size token values (gap fillers - web uses calc() instead):
116
- * - icon.size.xs: 12px (1.5 × base, derivation) - web uses calc(icon.size050 × 0.75)
117
- * - icon.size.xxl: 64px (8 × base, derivation) - web uses calc(icon.size050 × 4)
118
- *
119
- * @see Requirements 2.1-2.6, 3.1, 3.6 in .kiro/specs/042-avatar-component/requirements.md
120
- */
121
- export const AvatarTokens = defineComponentTokens({
122
- component: 'Avatar',
123
- family: 'spacing',
124
- tokens: {
125
- // Icon size tokens (gap fillers for sizes without existing icon tokens)
126
- // These fill gaps where no standard icon token exists at the required 50% ratio
116
+ // Icon size tokens (gap fillers for sizes without an existing icon token).
117
+ // Kept on the VALUE path deliberately: sizing primitives exist at both values
118
+ // (size150 = 12, size800 = 64), but the reference path currently emits a
119
+ // fabricated `SizingTokens.<name>` class on iOS/Android that no generated or
120
+ // hand-written platform file defines. Switching these to `reference:` would
121
+ // trade compiling output (`12.dp`) for non-compiling output and break
122
+ // Avatar.android.kt's `val iconSizeXs: Dp = GeneratedAvatarTokens.iconSizeXs`.
123
+ // Revisit once TokenFileGenerator.getFamilyClassName emits a real platform type.
127
124
  'icon.size.xs': {
128
125
  value: SIZING_BASE_VALUE * 1.5,
129
126
  reasoning: 'Icon size for xs avatar (12px = 1.5× base) maintains 50% ratio (12/24). No existing icon token at this size, so component token fills the gap.',
@@ -273,7 +270,7 @@ export function getAvatarSize(variant: AvatarSizeVariant): number {
273
270
  * @see Requirements 3.1, 3.6 in .kiro/specs/042-avatar-component/requirements.md
274
271
  */
275
272
  export function getAvatarIconSize(variant: AvatarIconSizeVariant): number {
276
- return AvatarTokens[`icon.size.${variant}`];
273
+ return AvatarSizingTokens[`icon.size.${variant}`];
277
274
  }
278
275
 
279
276
  /**
@@ -11,7 +11,7 @@ export { AVATAR_DEFAULTS } from './types';
11
11
 
12
12
  // Token exports
13
13
  export {
14
- AvatarTokens,
14
+ AvatarSizingTokens,
15
15
  getAvatarSize,
16
16
  getAvatarIconSize,
17
17
  getAvatarSizeTokenReference,
@@ -1,26 +1,35 @@
1
1
  /**
2
2
  * Button-Icon Component Token Definitions
3
- *
3
+ *
4
4
  * Stemma System naming: [Family]-[Type] = Button-Icon
5
5
  * Type: Primitive (foundational component)
6
- *
6
+ *
7
7
  * Platform-agnostic token definitions for the Button-Icon component.
8
8
  * Uses the defineComponentTokens() API to register tokens with the global
9
9
  * ComponentTokenRegistry for pipeline integration.
10
- *
10
+ *
11
+ * Two separate defineComponentTokens() calls are used — one per token family
12
+ * (spacing, sizing) — following the Avatar-Base pattern (avatar.tokens.ts). Every
13
+ * token registered by a single call is stamped with that call's `family`, so inset
14
+ * (spacing) and size (sizing) tokens must NOT share a call even though they live in
15
+ * the same file. The two branded results are exported separately and must NOT be
16
+ * spread/merged into one object — the rich metadata rides on a non-enumerable brand
17
+ * (see src/build/tokens/defineComponentTokens.ts, TOKEN_CONTRACT_BRAND) that a spread
18
+ * would silently drop.
19
+ *
11
20
  * The build system generates platform-specific values from these definitions:
12
21
  * - Web: CSS custom properties (var(--buttonicon-inset-large))
13
22
  * - iOS: Swift constants (ButtonIconTokens.insetLarge)
14
23
  * - Android: Kotlin constants (ButtonIconTokens.insetLarge)
15
- *
24
+ *
16
25
  * Token Relationships:
17
- * - buttonIcon.inset.large (12px) references space150
18
- * - buttonIcon.inset.medium (10px) references space125 (strategic flexibility token)
19
- * - buttonIcon.inset.small (8px) references space100
20
- * - buttonIcon.size.large (48px) references size600
21
- * - buttonIcon.size.medium (40px) references size500
22
- * - buttonIcon.size.small (32px) references size400
23
- *
26
+ * - buttonIcon.inset.large (12px) references space150 — family: spacing
27
+ * - buttonIcon.inset.medium (10px) references space125 (strategic flexibility token) — family: spacing
28
+ * - buttonIcon.inset.small (8px) references space100 — family: spacing
29
+ * - buttonIcon.size.large (48px) references size600 — family: sizing
30
+ * - buttonIcon.size.medium (40px) references size500 — family: sizing
31
+ * - buttonIcon.size.small (32px) references size400 — family: sizing
32
+ *
24
33
  * @see .kiro/specs/035-button-icon-component/design.md for token consumption strategy
25
34
  * @see .kiro/specs/034-component-architecture-system for Stemma System details
26
35
  * @see .kiro/specs/037-component-token-generation-pipeline for pipeline integration
@@ -32,28 +41,21 @@ import { spacingTokens } from '../../../tokens/SpacingTokens';
32
41
  import { sizingTokens } from '../../../tokens/SizingTokens';
33
42
 
34
43
  /**
35
- * Button-Icon component tokens defined using the hybrid authoring API.
36
- *
44
+ * Button-Icon inset (padding) tokens — reference spacing primitives.
45
+ *
37
46
  * Each token references a primitive token and includes reasoning
38
47
  * explaining why the token exists and its purpose in the component.
39
- *
40
- * Inset (padding) token values — reference spacing primitives:
48
+ *
41
49
  * - inset.large: 12px (1.5 × base, references space150)
42
50
  * - inset.medium: 10px (1.25 × base, references space125 strategic flexibility token)
43
51
  * - inset.small: 8px (1 × base, references space100)
44
- *
45
- * Size (width/height) token values:
46
- * - size.large: 48px (6 × base, references size600)
47
- * - size.medium: 40px (5 × base, references size500)
48
- * - size.small: 32px (4 × base, references size400)
49
- *
52
+ *
50
53
  * @see Requirements 6.1, 6.2, 6.3 in .kiro/specs/040-component-alignment/requirements.md
51
54
  */
52
- export const ButtonIconTokens = defineComponentTokens({
55
+ export const ButtonIconInsetTokens = defineComponentTokens({
53
56
  component: 'ButtonIcon',
54
57
  family: 'spacing',
55
58
  tokens: {
56
- // Inset (padding) tokens
57
59
  'inset.large': {
58
60
  reference: spacingTokens.space150,
59
61
  reasoning: 'Large button variant requires 12px padding (1.5× base) for comfortable touch target and visual balance with larger icon sizes',
@@ -66,8 +68,22 @@ export const ButtonIconTokens = defineComponentTokens({
66
68
  reference: spacingTokens.space100,
67
69
  reasoning: 'Small button variant uses 8px padding (1× base) for minimal footprint in dense UI layouts while meeting minimum touch target requirements',
68
70
  },
69
- // Size (width/height) tokens — reference sizing primitives, not spacing
70
- // @see Requirements 6.1, 6.2, 6.3
71
+ },
72
+ });
73
+
74
+ /**
75
+ * Button-Icon size (width/height) tokens — reference sizing primitives, not spacing.
76
+ *
77
+ * - size.large: 48px (6 × base, references size600)
78
+ * - size.medium: 40px (5 × base, references size500)
79
+ * - size.small: 32px (4 × base, references size400)
80
+ *
81
+ * @see Requirements 6.1, 6.2, 6.3 in .kiro/specs/040-component-alignment/requirements.md
82
+ */
83
+ export const ButtonIconSizeTokens = defineComponentTokens({
84
+ component: 'ButtonIcon',
85
+ family: 'sizing',
86
+ tokens: {
71
87
  'size.large': {
72
88
  reference: sizingTokens.size600,
73
89
  reasoning: 'Large button size (48px = 6× base) provides generous touch target exceeding tapAreaRecommended, calculated as icon (24px) + padding (12px × 2)',
@@ -107,7 +123,7 @@ export type ButtonIconSizeVariant = 'large' | 'medium' | 'small';
107
123
  * ```
108
124
  */
109
125
  export function getButtonIconInset(variant: ButtonIconInsetVariant): number {
110
- return ButtonIconTokens[`inset.${variant}`];
126
+ return ButtonIconInsetTokens[`inset.${variant}`];
111
127
  }
112
128
 
113
129
  /**
@@ -126,7 +142,7 @@ export function getButtonIconInset(variant: ButtonIconInsetVariant): number {
126
142
  * @see Requirements 6.1, 6.2, 6.3 in .kiro/specs/040-component-alignment/requirements.md
127
143
  */
128
144
  export function getButtonIconSize(variant: ButtonIconSizeVariant): number {
129
- return ButtonIconTokens[`size.${variant}`];
145
+ return ButtonIconSizeTokens[`size.${variant}`];
130
146
  }
131
147
 
132
148
  /**
@@ -565,6 +565,7 @@ export class TokenFileGenerator {
565
565
  private getiOSComponentTokenType(token: RegisteredComponentToken): string {
566
566
  switch (token.family) {
567
567
  case 'spacing':
568
+ case 'sizing':
568
569
  case 'radius':
569
570
  case 'fontSize':
570
571
  case 'tapArea':
@@ -619,8 +620,12 @@ export class TokenFileGenerator {
619
620
  const familyClass = this.getFamilyClassName(token.family);
620
621
  return `${familyClass}.${token.primitiveReference}`;
621
622
  }
622
- // Dimensional families need .dp suffix to match primitive token output (Task 1.4)
623
- const dimensionalFamilies = ['spacing', 'radius', 'tapArea', 'fontSize', 'borderWidth'];
623
+ // Dimensional families need .dp suffix to match primitive token output (Task 1.4).
624
+ // 'sizing' is dimensional (component width/height) and belongs here: without it, a
625
+ // value-path sizing token emits a bare Int, which fails to compile against consumers
626
+ // that type the member as Dp (e.g. Avatar.android.kt's
627
+ // `val iconSizeXs: Dp = GeneratedAvatarTokens.iconSizeXs`).
628
+ const dimensionalFamilies = ['spacing', 'sizing', 'radius', 'tapArea', 'fontSize', 'borderWidth'];
624
629
  if (dimensionalFamilies.includes(token.family)) {
625
630
  return `${token.value}.dp`;
626
631
  }
@@ -22,18 +22,20 @@ import {
22
22
  PROGRESS_COLOR_TOKEN_COUNT,
23
23
  } from '../semantic/ColorTokens';
24
24
  import {
25
- ProgressTokens,
25
+ getProgressRegisteredTokens,
26
26
  PROGRESS_COMPONENT_TOKEN_COUNT,
27
27
  progressComponentTokenNames,
28
28
  } from '../component/progress';
29
- import { getTokenContract } from '../../build/tokens';
30
29
  import type { RegisteredComponentToken } from '../../registries/ComponentTokenRegistry';
31
30
 
32
31
  // Spec 124: defineComponentTokens no longer self-registers into ComponentTokenRegistry;
33
32
  // the rich tokens ride back on the branded return and are recovered via getTokenContract.
34
33
  // (Was: reads of ComponentTokenRegistry.getByComponent('Progress') populated by the import
35
34
  // side effect — now empty/false-red.)
36
- const progressRegisteredTokens: RegisteredComponentToken[] = getTokenContract(ProgressTokens) ?? [];
35
+ //
36
+ // Progress registers through THREE family calls (sizing / spacing / borderWidth); the
37
+ // helper concatenates their branded contracts in declaration order.
38
+ const progressRegisteredTokens: RegisteredComponentToken[] = getProgressRegisteredTokens();
37
39
 
38
40
  describe('Progress Token Compliance', () => {
39
41
  // ==========================================================================