@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.
- package/.kiro/steering/Civitas-System-Overview.md +3 -12
- package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
- package/.kiro/steering/Task-Completion-Protocol.md +7 -7
- package/.kiro/steering/start-up-tasks.md +4 -4
- package/dist/ComponentTokens.android.kt +12 -12
- package/dist/ComponentTokens.ios.swift +12 -12
- package/dist/ComponentTokens.web.css +3 -3
- package/dist/DesignTokens.android.kt +1 -1
- package/dist/DesignTokens.dtcg.json +2 -2
- package/dist/DesignTokens.ios.swift +1 -1
- package/dist/DesignTokens.web.css +1 -1
- package/dist/android/DesignTokens.android.kt +1 -1
- package/dist/browser/designerpunk.esm.js +14 -9
- package/dist/browser/designerpunk.esm.min.js +7 -7
- package/dist/browser/designerpunk.umd.js +14 -9
- package/dist/browser/designerpunk.umd.min.js +12 -12
- package/dist/browser/tokens.css +3 -3
- package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
- package/dist/build/tokens/defineComponentTokens.js +26 -0
- package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
- package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
- package/dist/components/core/Avatar-Base/index.d.ts +1 -1
- package/dist/components/core/Avatar-Base/index.js +2 -2
- package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
- package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
- package/dist/generators/TokenFileGenerator.js +7 -2
- package/dist/ios/DesignTokens.ios.swift +1 -1
- package/dist/tokens/component/progress.d.ts +65 -5
- package/dist/tokens/component/progress.js +79 -18
- package/dist/types/generated/TokenTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.js +1 -1
- package/dist/web/DesignTokens.web.css +1 -1
- package/governance/BUILD-SYSTEM-SETUP.md +1 -2
- package/governance/Component-Development-Guide.md +1 -1
- package/governance/Component-Development-Standards.md +19 -18
- package/governance/Component-Family-Avatar.md +6 -6
- package/governance/Component-Family-Badge.md +19 -19
- package/governance/Component-Family-Button.md +26 -26
- package/governance/Component-Family-Chip.md +14 -14
- package/governance/Component-Family-Container.md +12 -12
- package/governance/Component-Family-Form-Inputs.md +61 -61
- package/governance/Component-Family-Icon.md +8 -8
- package/governance/Component-Family-Navigation.md +1 -1
- package/governance/Component-Inheritance-Structures.md +29 -28
- package/governance/Component-Readiness-Status.md +6 -7
- package/governance/Component-Templates.md +26 -26
- package/governance/Contract-System-Reference.md +1 -1
- package/governance/Process-Development-Workflow.md +4 -4
- package/governance/Process-File-Organization.md +1 -4
- package/governance/Process-Hook-Operations.md +6 -7
- package/governance/Process-Spec-Planning.md +8 -14
- package/governance/Rosetta-System-Architecture.md +7 -5
- package/governance/Token-Quick-Reference.md +35 -22
- package/governance/Web-Authoring-Standards.md +1 -1
- package/governance/classification-map.md +109 -3
- package/governance/completion-documentation-guide.md +14 -31
- package/governance/platform-implementation-guidelines.md +1 -2
- package/governance/release-management-system.md +28 -63
- package/package.json +2 -6
- package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
- package/src/build/tokens/defineComponentTokens.ts +43 -1
- package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
- package/src/components/core/Avatar-Base/index.ts +1 -1
- package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
- package/src/generators/TokenFileGenerator.ts +7 -2
- package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
- package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
- package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
- package/src/tokens/component/progress.ts +83 -21
- package/src/types/generated/TokenTypes.ts +1 -1
- package/token-index/components.yaml +8 -8
- package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
- package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
- package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
- package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
- package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
- package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
- package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
- package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
- package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
- package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
- package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
- package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
- package/src/tools/release/cli/ReleasePipeline.ts +0 -165
- package/src/tools/release/cli/release-tool.ts +0 -107
- package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
- package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
- package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
- package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
- package/src/tools/release/pipeline/TagResolver.ts +0 -40
- package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
- package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
- package/src/tools/release/publishers/NpmPublisher.ts +0 -196
- package/src/tools/release/release-config.json +0 -5
- package/src/tools/release/types/index.ts +0 -282
- 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-
|
|
10
|
-
**Last Updated**: 2026-
|
|
11
|
-
**Purpose**: Mental model of the release
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
273
|
+
return AvatarSizingTokens[`icon.size.${variant}`];
|
|
277
274
|
}
|
|
278
275
|
|
|
279
276
|
/**
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ==========================================================================
|