@3fn/core 13.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 (284) hide show
  1. package/.kiro/agents/ada-prompt.md +80 -132
  2. package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
  3. package/.kiro/agents/ada.json +44 -59
  4. package/.kiro/agents/ada.json.attribution.json +13 -0
  5. package/.kiro/agents/data-prompt.md +83 -74
  6. package/.kiro/agents/data-prompt.md.attribution.json +53 -0
  7. package/.kiro/agents/data.json +31 -38
  8. package/.kiro/agents/data.json.attribution.json +13 -0
  9. package/.kiro/agents/kenya-prompt.md +83 -72
  10. package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
  11. package/.kiro/agents/kenya.json +27 -35
  12. package/.kiro/agents/kenya.json.attribution.json +13 -0
  13. package/.kiro/agents/leonardo-prompt.md +176 -234
  14. package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
  15. package/.kiro/agents/leonardo.json +28 -36
  16. package/.kiro/agents/leonardo.json.attribution.json +13 -0
  17. package/.kiro/agents/lina-prompt.md +110 -151
  18. package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
  19. package/.kiro/agents/lina.json +46 -59
  20. package/.kiro/agents/lina.json.attribution.json +13 -0
  21. package/.kiro/agents/sparky-prompt.md +89 -72
  22. package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
  23. package/.kiro/agents/sparky.json +37 -37
  24. package/.kiro/agents/sparky.json.attribution.json +13 -0
  25. package/.kiro/agents/stacy-prompt.md +74 -48
  26. package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
  27. package/.kiro/agents/stacy.json +28 -30
  28. package/.kiro/agents/stacy.json.attribution.json +13 -0
  29. package/.kiro/agents/thurgood-prompt.md +94 -138
  30. package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
  31. package/.kiro/agents/thurgood.json +31 -35
  32. package/.kiro/agents/thurgood.json.attribution.json +13 -0
  33. package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
  34. package/.kiro/steering/Civitas-System-Overview.md +7 -16
  35. package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
  36. package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
  37. package/.kiro/steering/Task-Completion-Protocol.md +98 -17
  38. package/.kiro/steering/core-goals.md +3 -3
  39. package/.kiro/steering/personal-note.md +1 -1
  40. package/.kiro/steering/start-up-tasks.md +17 -6
  41. package/application-mcp-server/src/index.ts +26 -0
  42. package/dist/ComponentTokens.android.kt +12 -12
  43. package/dist/ComponentTokens.ios.swift +12 -12
  44. package/dist/ComponentTokens.web.css +3 -3
  45. package/dist/DesignTokens.android.kt +1 -1
  46. package/dist/DesignTokens.dtcg.json +8 -5
  47. package/dist/DesignTokens.figma.json +2 -2
  48. package/dist/DesignTokens.ios.swift +1 -1
  49. package/dist/DesignTokens.web.css +1 -1
  50. package/dist/android/DesignTokens.android.kt +1 -1
  51. package/dist/blend/OklchBlendCalculator.js +1 -0
  52. package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
  53. package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
  54. package/dist/browser/designerpunk.esm.js +36 -90
  55. package/dist/browser/designerpunk.esm.min.js +33 -36
  56. package/dist/browser/designerpunk.umd.js +36 -90
  57. package/dist/browser/designerpunk.umd.min.js +47 -50
  58. package/dist/browser/tokens.css +3 -3
  59. package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
  60. package/dist/build/tokens/defineComponentTokens.js +26 -0
  61. package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
  62. package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
  63. package/dist/components/core/Avatar-Base/index.d.ts +1 -1
  64. package/dist/components/core/Avatar-Base/index.js +2 -2
  65. package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
  66. package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
  67. package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
  68. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
  69. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
  70. package/dist/components/core/Button-CTA/types.d.ts +0 -24
  71. package/dist/components/core/Button-CTA/types.js +6 -0
  72. package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
  73. package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
  74. package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
  75. package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
  76. package/dist/generators/DTCGFormatGenerator.js +8 -0
  77. package/dist/generators/TokenFileGenerator.js +7 -2
  78. package/dist/integration/BuildErrorHandler.js +2 -2
  79. package/dist/ios/DesignTokens.ios.swift +1 -1
  80. package/dist/mcp/application-mcp.js +24 -0
  81. package/dist/mcp/docs-mcp.js +130 -15
  82. package/dist/mcp/product-mcp.js +25 -0
  83. package/dist/tokens/OpacityTokens.js +1 -1
  84. package/dist/tokens/component/progress.d.ts +65 -5
  85. package/dist/tokens/component/progress.js +79 -18
  86. package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
  87. package/dist/tokens/semantic/BlendTokens.js +17 -5
  88. package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
  89. package/dist/tokens/semantic/OpacityTokens.js +4 -4
  90. package/dist/types/ComponentTypes.d.ts +1 -1
  91. package/dist/types/generated/TokenTypes.d.ts +1 -1
  92. package/dist/types/generated/TokenTypes.js +1 -1
  93. package/dist/validators/StemmaTokenUsageValidator.js +3 -2
  94. package/dist/web/DesignTokens.web.css +1 -1
  95. package/governance/BUILD-SYSTEM-SETUP.md +1 -2
  96. package/governance/Component-Development-Guide.md +23 -13
  97. package/governance/Component-Development-Standards.md +21 -20
  98. package/governance/Component-Family-Avatar.md +6 -7
  99. package/governance/Component-Family-Badge.md +19 -20
  100. package/governance/Component-Family-Button.md +30 -43
  101. package/governance/Component-Family-Chip.md +14 -15
  102. package/governance/Component-Family-Container.md +12 -13
  103. package/governance/Component-Family-Data-Display.md +1 -2
  104. package/governance/Component-Family-Divider.md +1 -2
  105. package/governance/Component-Family-Form-Inputs.md +65 -65
  106. package/governance/Component-Family-Icon.md +9 -10
  107. package/governance/Component-Family-Loading.md +1 -2
  108. package/governance/Component-Family-Modal.md +1 -2
  109. package/governance/Component-Family-Navigation.md +1 -2
  110. package/governance/Component-Family-Progress.md +0 -1
  111. package/governance/Component-Inheritance-Structures.md +219 -99
  112. package/governance/Component-MCP-Document-Template.md +6 -5
  113. package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
  114. package/governance/Component-Quick-Reference.md +33 -33
  115. package/governance/Component-Readiness-Status.md +59 -44
  116. package/governance/Component-Templates.md +57 -61
  117. package/governance/Contract-System-Reference.md +7 -7
  118. package/governance/MCP-Integration-Guide.md +1 -1
  119. package/governance/Process-Cross-Reference-Standards.md +31 -13
  120. package/governance/Process-Development-Workflow.md +49 -59
  121. package/governance/Process-File-Organization.md +25 -28
  122. package/governance/Process-Hook-Operations.md +22 -11
  123. package/governance/Process-Orchestration-Model-Selection.md +92 -0
  124. package/governance/Process-Spec-Planning.md +98 -52
  125. package/governance/Process-Task-Type-Definitions.md +80 -4
  126. package/governance/Product-Handoff-Protocol.md +2 -0
  127. package/governance/Rosetta-System-Architecture.md +13 -11
  128. package/governance/Test-Behavioral-Contract-Validation.md +38 -31
  129. package/governance/Test-Failure-Audit-Methodology.md +1 -1
  130. package/governance/Token-Family-Accessibility.md +1 -2
  131. package/governance/Token-Family-Blend.md +18 -16
  132. package/governance/Token-Family-Blur.md +0 -1
  133. package/governance/Token-Family-Border.md +1 -2
  134. package/governance/Token-Family-Color.md +0 -1
  135. package/governance/Token-Family-Glow.md +1 -2
  136. package/governance/Token-Family-Layering.md +0 -1
  137. package/governance/Token-Family-Motion.md +1 -2
  138. package/governance/Token-Family-Opacity.md +0 -1
  139. package/governance/Token-Family-Radius.md +1 -2
  140. package/governance/Token-Family-Responsive.md +1 -2
  141. package/governance/Token-Family-Shadow.md +1 -2
  142. package/governance/Token-Family-Sizing.md +0 -1
  143. package/governance/Token-Family-Spacing.md +1 -2
  144. package/governance/Token-Family-Typography.md +1 -2
  145. package/governance/Token-Governance.md +8 -8
  146. package/governance/Token-Quick-Reference.md +47 -34
  147. package/governance/Token-Resolution-Patterns.md +1 -1
  148. package/governance/Token-Semantic-Structure.md +1 -1
  149. package/governance/Web-Authoring-Standards.md +5 -5
  150. package/governance/browser-distribution-guide.md +1 -4
  151. package/governance/classification-map.md +474 -0
  152. package/governance/completion-documentation-guide.md +23 -37
  153. package/governance/component-meta-authoring-guide.md +1 -1
  154. package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
  155. package/governance/platform-implementation-guidelines.md +2 -3
  156. package/governance/release-management-system.md +28 -63
  157. package/governance/rosetta-system-principles.md +8 -6
  158. package/governance/stemma-system-principles.md +18 -17
  159. package/mcp-server/src/index.ts +24 -6
  160. package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
  161. package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
  162. package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
  163. package/mcp-server/src/indexer/index-health.ts +27 -2
  164. package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
  165. package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
  166. package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
  167. package/mcp-server/src/tools/list-cross-references.ts +2 -2
  168. package/package.json +24 -26
  169. package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
  170. package/src/__tests__/console-allowlist.json +14 -0
  171. package/src/__tests__/console-fail-setup.ts +169 -0
  172. package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
  173. package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
  174. package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
  175. package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
  176. package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
  177. package/src/blend/OklchBlendCalculator.ts +3 -0
  178. package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
  179. package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
  180. package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
  181. package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
  182. package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
  183. package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
  184. package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
  185. package/src/build/tokens/defineComponentTokens.ts +43 -1
  186. package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
  187. package/src/cli/__tests__/init.test.ts +45 -11
  188. package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
  189. package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
  190. package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
  191. package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
  192. package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
  193. package/src/components/core/Avatar-Base/contracts.yaml +11 -1
  194. package/src/components/core/Avatar-Base/index.ts +1 -1
  195. package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
  196. package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
  197. package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
  198. package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
  199. package/src/components/core/Button-CTA/README.md +3 -6
  200. package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
  201. package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
  202. package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
  203. package/src/components/core/Button-CTA/contracts.yaml +6 -29
  204. package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
  205. package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
  206. package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
  207. package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
  208. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
  209. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
  210. package/src/components/core/Button-CTA/types.ts +10 -28
  211. package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
  212. package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
  213. package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
  214. package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
  215. package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
  216. package/src/components/core/Input-Text-Base/README.md +25 -2
  217. package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
  218. package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
  219. package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
  220. package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
  221. package/src/components/core/Input-Text-Base/types.ts +13 -1
  222. package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
  223. package/src/components/core/Input-Text-Email/README.md +8 -7
  224. package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
  225. package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
  226. package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
  227. package/src/components/core/Input-Text-Password/README.md +9 -8
  228. package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
  229. package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
  230. package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
  231. package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
  232. package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
  233. package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
  234. package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
  235. package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
  236. package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
  237. package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
  238. package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
  239. package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
  240. package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
  241. package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
  242. package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
  243. package/src/generators/DTCGFormatGenerator.ts +6 -0
  244. package/src/generators/TokenFileGenerator.ts +7 -2
  245. package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
  246. package/src/integration/BuildErrorHandler.ts +2 -2
  247. package/src/tokens/OpacityTokens.ts +1 -1
  248. package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
  249. package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
  250. package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
  251. package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
  252. package/src/tokens/component/progress.ts +83 -21
  253. package/src/tokens/semantic/BlendTokens.ts +26 -5
  254. package/src/tokens/semantic/OpacityTokens.ts +4 -4
  255. package/src/types/ComponentTypes.ts +1 -1
  256. package/src/types/generated/TokenTypes.ts +1 -1
  257. package/src/validators/StemmaTokenUsageValidator.ts +3 -2
  258. package/token-index/components.yaml +8 -8
  259. package/token-index/semantics.yaml +1 -2
  260. package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
  261. package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
  262. package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
  263. package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
  264. package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
  265. package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
  266. package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
  267. package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
  268. package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
  269. package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
  270. package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
  271. package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
  272. package/src/tools/release/cli/ReleasePipeline.ts +0 -165
  273. package/src/tools/release/cli/release-tool.ts +0 -107
  274. package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
  275. package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
  276. package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
  277. package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
  278. package/src/tools/release/pipeline/TagResolver.ts +0 -40
  279. package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
  280. package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
  281. package/src/tools/release/publishers/NpmPublisher.ts +0 -196
  282. package/src/tools/release/release-config.json +0 -5
  283. package/src/tools/release/types/index.ts +0 -282
  284. package/src/tools/release/validators/SemanticVersionValidator.ts +0 -67
@@ -19,9 +19,12 @@ import {
19
19
  assertMustFixCouplings,
20
20
  assertScope,
21
21
  runRelocationIntegrityGate,
22
+ scanPromptReferences,
22
23
  LOCKED_IDENTITY_IDS,
23
24
  DEFAULT_PROJECT_ROOT,
25
+ DEFAULT_GOVERNANCE_DIR,
24
26
  } from '../relocation-integrity-gate';
27
+ import { DocumentIndexer } from '../../indexer/DocumentIndexer';
25
28
 
26
29
  describe('classifyReference — identity vs served vs template (Req 8 AC5)', () => {
27
30
  it('classifies a placeholder path-shape as template', () => {
@@ -198,20 +201,84 @@ describe('runRelocationIntegrityGate — full gate (the 119-A exit check, Req 8
198
201
  expect(result.unresolved).toEqual([]);
199
202
  });
200
203
 
201
- it('resolves every served prompt ref via the legacy-fallback (Req 8 AC1–AC3)', async () => {
204
+ it('resolves a served prompt ref via legacy-fallback — fixture prompt (Req 8 AC1–AC3)', async () => {
205
+ // Anti-vacuity moved OFF live data (2026-07-11, Spec 122 U6): the served floor
206
+ // (≥1 served `.kiro/steering/*.md` ref in the REAL agent prompts) died by progress —
207
+ // the 122 cutovers regenerate agent prompts into runtime-neutral MCP-cue form, removing
208
+ // `.kiro/steering/*.md` file references. With Leonardo (U6) cut over, ZERO live prompts
209
+ // carry served refs (same class as the template floor's death at U3). The served-
210
+ // resolution behavior is now exercised NON-VACUOUSLY against a fixture prompt carrying a
211
+ // real relocated-doc ref, resolved through a real indexer (frozen legacy manifest seeded);
212
+ // the live leg below keeps the every-served-ref-resolves invariant without an inventory floor.
213
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-served-'));
214
+ const indexer = new DocumentIndexer();
215
+ await indexer.indexDirectory(DEFAULT_GOVERNANCE_DIR); // seeds the frozen legacy manifest at its tail
216
+ try {
217
+ fs.mkdirSync(path.join(root, '.kiro/agents'), { recursive: true });
218
+ fs.writeFileSync(
219
+ path.join(root, '.kiro/agents', 'fixture-prompt.md'),
220
+ '# Fixture\n\nSee `get_section({ path: ".kiro/steering/Token-Governance.md", heading: "Token Usage Governance" })`.\n',
221
+ );
222
+ const scan = scanPromptReferences(root, indexer);
223
+ const served = scan.references.filter((r) => r.role === 'served');
224
+ expect(served.length).toBeGreaterThanOrEqual(1);
225
+ expect(served.every((r) => r.resolved)).toBe(true);
226
+ expect(served.every((r) => r.strategy === 'legacy-fallback')).toBe(true);
227
+ } finally {
228
+ fs.rmSync(root, { recursive: true, force: true });
229
+ }
230
+ });
231
+
232
+ it('live prompts: every served ref (if any) resolves via legacy-fallback (Req 8 AC1–AC3)', async () => {
202
233
  const result = await runRelocationIntegrityGate();
203
234
  const served = result.references.filter((r) => r.role === 'served');
204
- expect(served.length).toBeGreaterThan(0);
235
+ // NO count floor: the live corpus legitimately holds zero served refs since U6 (the
236
+ // fixture leg above covers non-vacuous resolution). If any served refs exist, each must
237
+ // resolve via legacy-fallback; the resolutionMechanism description holds regardless.
205
238
  expect(served.every((r) => r.resolved)).toBe(true);
206
239
  expect(served.every((r) => r.strategy === 'legacy-fallback')).toBe(true);
207
240
  expect(result.resolutionMechanism).toMatch(/legacy-fallback/);
208
241
  });
209
242
 
210
- it('excludes template placeholders from pass/fail (Req 8 AC1 — not real refs)', async () => {
243
+ it('excludes template placeholders from pass/fail — fixture prompt (Req 8 AC1 — not real refs)', () => {
244
+ // Anti-vacuity moved OFF live data (2026-07-11, Spec 122 U3): the live floor
245
+ // (≥1 template in the real prompts) died by progress — Ada's U2 regeneration dropped
246
+ // `Token-Family-{Name}.md` (3 → 2) and Lina's U3 regeneration removed the corpus's
247
+ // LAST template placeholders (`Component-Family-{Name}.md` / `{FamilyName}`, 2 → 0),
248
+ // exactly as this test's old comment predicted. The exclusion behavior is now
249
+ // exercised non-vacuously via the extracted reference-axis seam on a fixture prompt;
250
+ // the live leg below keeps the invariant without an inventory floor.
251
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-tpl-'));
252
+ try {
253
+ fs.mkdirSync(path.join(root, '.kiro/agents'), { recursive: true });
254
+ fs.writeFileSync(
255
+ path.join(root, '.kiro/agents', 'fixture-prompt.md'),
256
+ '# Fixture\n\nSee `get_document_summary({ path: ".kiro/steering/Component-Family-{Name}.md" })`.\n',
257
+ );
258
+ // A template-only scan must never touch the resolver — throwing stub proves it.
259
+ const neverCalled = {
260
+ resolveRef: (): never => {
261
+ throw new Error('resolveRef must not be called for a template placeholder');
262
+ },
263
+ };
264
+ const scan = scanPromptReferences(root, neverCalled);
265
+ const templates = scan.references.filter((r) => r.role === 'template');
266
+ expect(templates.length).toBe(1);
267
+ expect(templates[0].ref).toBe('.kiro/steering/Component-Family-{Name}.md');
268
+ expect(templates[0].resolved).toBe(false);
269
+ // a template ref must never appear in unresolved — the exclusion under test
270
+ expect(scan.unresolved).toEqual([]);
271
+ } finally {
272
+ fs.rmSync(root, { recursive: true, force: true });
273
+ }
274
+ });
275
+
276
+ it('live prompts: any template placeholder stays excluded from unresolved (Req 8 AC1)', async () => {
211
277
  const result = await runRelocationIntegrityGate();
212
278
  const templates = result.references.filter((r) => r.role === 'template');
213
- expect(templates.length).toBe(3);
214
- // a template ref must never appear in unresolved
279
+ // NO count floor: the live corpus legitimately holds zero placeholders since U3
280
+ // (see the fixture leg above for the non-vacuous exclusion assertion; the classifier
281
+ // routing itself is directly covered by the classifyReference tests).
215
282
  for (const t of templates) {
216
283
  expect(result.unresolved.some((u) => u.includes(t.ref))).toBe(false);
217
284
  }
@@ -269,14 +269,44 @@ export function assertMustFixCouplings(projectRoot: string): CouplingCheck[] {
269
269
  // non-identity .kiro/steering/ doc remains referenced in any agent JSON.
270
270
  {
271
271
  const dir = path.join(projectRoot, '.kiro/agents');
272
- const jsons = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
272
+ const jsons = fs
273
+ .readdirSync(dir)
274
+ .filter((f) => f.endsWith('.json') && !f.endsWith('.attribution.json'))
275
+ .sort();
276
+
277
+ // Spec 122 cutover-ledger awareness (added at Ada's cutover, U2/PR #55): a GENERATED
278
+ // config carries only its designed ambient members — Ada's trim took the fleet total
279
+ // from 122 to 100 governance/ entries and broke the old flat `govEntries >= 120`
280
+ // floor, and every later cutover would break any fixed total again. The floor's real
281
+ // job is anti-vacuity over the HAND-maintained configs (proof the scan actually saw
282
+ // the remediated entries), so it is now PER-CONFIG (≥5 governance/ entries each; the
283
+ // thinnest hand config today carries 9) over NON-ledger configs only. Generated
284
+ // configs are exempt from the floor — their ref integrity is the generator gate's job
285
+ // (122-sweep-1-refs + 122-canonical-vs-truth) — but they stay IN the stray-relocating
286
+ // scan below: no config, hand or generated, may point a relocating doc at
287
+ // .kiro/steering/. When the ledger covers every seat, the floor leg is vacuously true
288
+ // and this gate's remediation duty is fully handed to the generation-era checks.
289
+ let ledgerAgents = new Set<string>();
290
+ try {
291
+ const ledgerText = fs.readFileSync(path.join(projectRoot, 'canonical/cutover-ledger.yaml'), 'utf-8');
292
+ ledgerAgents = new Set([...ledgerText.matchAll(/^\s*-\s*([a-z0-9-]+)\s*$/gm)].map((m) => m[1]));
293
+ } catch {
294
+ ledgerAgents = new Set(); // pre-122 tree: floor applies to all configs, as before
295
+ }
296
+
273
297
  let govEntries = 0;
298
+ const thinHandConfigs: string[] = [];
274
299
  const strayRelocating = new Set<string>();
275
300
  let strayIdentityLeak = false; // an .kiro/steering/ entry that is NOT an identity doc
276
301
  for (const j of jsons) {
277
302
  const content = fs.readFileSync(path.join(dir, j), 'utf-8');
303
+ const agentName = j.replace(/\.json$/, '');
278
304
  const govRe = /(?:file|skill):\/\/[^"]*governance\/[^"]*\.md/g;
279
- govEntries += (content.match(govRe) || []).length;
305
+ const configGovEntries = (content.match(govRe) || []).length;
306
+ govEntries += configGovEntries;
307
+ if (!ledgerAgents.has(agentName) && configGovEntries < 5) {
308
+ thinHandConfigs.push(`${j} (${configGovEntries})`);
309
+ }
280
310
  const steerRe = /(?:file|skill):\/\/[^"]*\.kiro\/steering\/([^"/]+\.md)/g;
281
311
  let mm: RegExpExecArray | null;
282
312
  while ((mm = steerRe.exec(content)) !== null) {
@@ -287,13 +317,13 @@ export function assertMustFixCouplings(projectRoot: string): CouplingCheck[] {
287
317
  }
288
318
  }
289
319
  }
290
- const ok = govEntries >= 120 && !strayIdentityLeak;
320
+ const ok = thinHandConfigs.length === 0 && !strayIdentityLeak;
291
321
  checks.push({
292
322
  surface: 'agent-definition resources[] (file:// + skill://)',
293
323
  remediated: ok,
294
324
  detail: ok
295
- ? `governance/ entries=${govEntries} (≥120); zero relocating docs left at .kiro/steering/`
296
- : `governance/ entries=${govEntries}; UNREMEDIATED relocating docs still at .kiro/steering/: ${[...strayRelocating].join(', ')}`,
325
+ ? `governance/ entries=${govEntries} (per-hand-config floor ≥5 held; ${ledgerAgents.size} generator-governed config(s) exempt); zero relocating docs left at .kiro/steering/`
326
+ : `governance/ entries=${govEntries}; hand configs under the ≥5 floor: ${thinHandConfigs.join(', ') || 'none'}; UNREMEDIATED relocating docs still at .kiro/steering/: ${[...strayRelocating].join(', ')}`,
297
327
  });
298
328
  }
299
329
 
@@ -511,27 +541,28 @@ export interface GateOptions {
511
541
  indexer?: DocumentIndexer;
512
542
  }
513
543
 
544
+ /** The reference axis's scan output — see {@link scanPromptReferences}. */
545
+ export interface PromptReferenceScan {
546
+ references: ReferenceCheck[];
547
+ unresolved: string[];
548
+ referencedIdentityIds: Set<string>;
549
+ }
550
+
514
551
  /**
515
- * Run the relocation-integrity gate (the 119-A exit check).
516
- *
517
- * Builds a real `DocumentIndexer` over `governance/` (the frozen legacy manifest is
518
- * seeded automatically at the tail of `indexDirectory`), then exercises the live
519
- * `resolveRef` per prompt reference. Identity refs bypass MCP (static presence);
520
- * must-fix couplings + family-guidance + scope are asserted statically.
552
+ * The reference axis (Req 8 AC1–AC5), extracted from the orchestrator as its own
553
+ * exported seam: scan every prompt under `projectRoot`, classify each steering ref
554
+ * (template / identity / served), and resolve served refs through the live indexer.
555
+ * Template placeholders are excluded from pass/fail here — they are recorded but
556
+ * NEVER pushed to `unresolved` — which is the behavior the fixture-based unit test
557
+ * exercises directly (the live corpus carries zero placeholders since the Spec 122
558
+ * U3 regeneration, so full-gate runs can no longer test this leg non-vacuously).
559
+ * Only served refs touch the indexer; a template-only or identity-only scan never
560
+ * calls `resolveRef`.
521
561
  */
522
- export async function runRelocationIntegrityGate(opts: GateOptions = {}): Promise<GateResult> {
523
- const projectRoot = opts.projectRoot ?? DEFAULT_PROJECT_ROOT;
524
- const governanceDir = opts.governanceDir ?? DEFAULT_GOVERNANCE_DIR;
525
-
526
- let indexer = opts.indexer;
527
- if (!indexer) {
528
- indexer = new DocumentIndexer();
529
- // indexDirectory seeds the frozen legacy manifest at its tail (re-seed
530
- // obligation) — so the legacy-fallback resolution mechanism is live here.
531
- await indexer.indexDirectory(governanceDir);
532
- }
533
-
534
- // --- Reference axis (Req 8 AC1–AC5) ---
562
+ export function scanPromptReferences(
563
+ projectRoot: string,
564
+ indexer: Pick<DocumentIndexer, 'resolveRef'>
565
+ ): PromptReferenceScan {
535
566
  const references: ReferenceCheck[] = [];
536
567
  const unresolved: string[] = [];
537
568
  const referencedIdentityIds = new Set<string>();
@@ -583,6 +614,32 @@ export async function runRelocationIntegrityGate(opts: GateOptions = {}): Promis
583
614
  }
584
615
  }
585
616
 
617
+ return { references, unresolved, referencedIdentityIds };
618
+ }
619
+
620
+ /**
621
+ * Run the relocation-integrity gate (the 119-A exit check).
622
+ *
623
+ * Builds a real `DocumentIndexer` over `governance/` (the frozen legacy manifest is
624
+ * seeded automatically at the tail of `indexDirectory`), then exercises the live
625
+ * `resolveRef` per prompt reference. Identity refs bypass MCP (static presence);
626
+ * must-fix couplings + family-guidance + scope are asserted statically.
627
+ */
628
+ export async function runRelocationIntegrityGate(opts: GateOptions = {}): Promise<GateResult> {
629
+ const projectRoot = opts.projectRoot ?? DEFAULT_PROJECT_ROOT;
630
+ const governanceDir = opts.governanceDir ?? DEFAULT_GOVERNANCE_DIR;
631
+
632
+ let indexer = opts.indexer;
633
+ if (!indexer) {
634
+ indexer = new DocumentIndexer();
635
+ // indexDirectory seeds the frozen legacy manifest at its tail (re-seed
636
+ // obligation) — so the legacy-fallback resolution mechanism is live here.
637
+ await indexer.indexDirectory(governanceDir);
638
+ }
639
+
640
+ // --- Reference axis (Req 8 AC1–AC5) ---
641
+ const { references, unresolved, referencedIdentityIds } = scanPromptReferences(projectRoot, indexer);
642
+
586
643
  // --- Identity axis (Req 8 AC5) ---
587
644
  const identity = assertIdentityPresence(projectRoot, referencedIdentityIds);
588
645
  for (const c of identity) {
@@ -16,13 +16,13 @@ import { ErrorHandler, MCPError } from '../utils/error-handler';
16
16
  */
17
17
  export const listCrossReferencesTool = {
18
18
  name: 'list_cross_references',
19
- description: 'List cross-references in a document. Returns all referenced documents with context, source section, and line number.',
19
+ description: 'List cross-references in a document. Returns all referenced documents with context, source section, and line number. Enumerates both `.md`-path links (target = the linked path, verbatim) and bare-id links validated against the doc-id index (target = the doc id); unresolvable bare-id targets are excluded (surfaced via index-health). Addressing contract (D5): the `path` parameter resolves through the same strategy chain as the other document tools — doc id, then indexed relative path, then legacy `.kiro/steering/…` path.',
20
20
  inputSchema: {
21
21
  type: 'object' as const,
22
22
  properties: {
23
23
  path: {
24
24
  type: 'string',
25
- description: 'Document path (e.g., ".kiro/steering/Component Development Guide.md")'
25
+ description: 'Document ref: doc id (e.g., "token-governance"), indexed relative path, or legacy `.kiro/steering/…` path — same resolver chain as get_document_summary'
26
26
  }
27
27
  },
28
28
  required: ['path'] as string[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3fn/core",
3
- "version": "13.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",
@@ -42,42 +42,42 @@
42
42
  ],
43
43
  "exports": {
44
44
  ".": {
45
- "import": "./dist/browser/designerpunk.esm.js",
46
- "types": "./dist/browser-entry.d.ts"
45
+ "types": "./dist/browser-entry.d.ts",
46
+ "import": "./dist/browser/designerpunk.esm.js"
47
47
  },
48
48
  "./components": {
49
- "import": "./dist/browser/designerpunk.esm.js",
50
- "types": "./dist/browser-entry.d.ts"
49
+ "types": "./dist/browser-entry.d.ts",
50
+ "import": "./dist/browser/designerpunk.esm.js"
51
51
  },
52
52
  "./tokens.css": "./dist/DesignTokens.web.css",
53
53
  "./component-tokens.css": "./dist/ComponentTokens.web.css",
54
54
  "./config": {
55
+ "types": "./dist/config/index.d.ts",
55
56
  "import": "./dist/config/index.js",
56
- "require": "./dist/config/index.js",
57
- "types": "./dist/config/index.d.ts"
57
+ "require": "./dist/config/index.js"
58
58
  },
59
59
  "./types": {
60
+ "types": "./dist/types/index.d.ts",
60
61
  "import": "./dist/types/index.js",
61
- "require": "./dist/types/index.js",
62
- "types": "./dist/types/index.d.ts"
62
+ "require": "./dist/types/index.js"
63
63
  },
64
64
  "./build": {
65
+ "types": "./dist/build/tokens/index.d.ts",
65
66
  "import": "./dist/build/tokens/index.js",
66
- "require": "./dist/build/tokens/index.js",
67
- "types": "./dist/build/tokens/index.d.ts"
67
+ "require": "./dist/build/tokens/index.js"
68
68
  },
69
69
  "./blend": {
70
+ "types": "./dist/blend/index.d.ts",
70
71
  "import": "./dist/blend/index.js",
71
- "require": "./dist/blend/index.js",
72
- "types": "./dist/blend/index.d.ts"
72
+ "require": "./dist/blend/index.js"
73
73
  },
74
74
  "./jest-preset": {
75
75
  "require": "./dist/testing/jest-preset.js"
76
76
  },
77
77
  "./testing": {
78
+ "types": "./dist/testing/index.d.ts",
78
79
  "import": "./dist/testing/index.js",
79
- "require": "./dist/testing/index.js",
80
- "types": "./dist/testing/index.d.ts"
80
+ "require": "./dist/testing/index.js"
81
81
  },
82
82
  "./grid.css": "./src/styles/responsive-grid.css",
83
83
  "./fonts/inter.css": "./src/assets/fonts/inter/inter.css",
@@ -106,22 +106,18 @@
106
106
  "build:verify": "node verify-build.js",
107
107
  "build:validate": "npx tsx src/validators/buildValidation.ts",
108
108
  "lint": "eslint 'src/components/**/*.ts' 'src/components/**/*.tsx' --no-error-on-unmatched-pattern",
109
- "test": "jest --testPathIgnorePatterns='performance/__tests__|__tests__/performance|PerformanceValidation'",
109
+ "test": "jest --config jest.functional.config.js",
110
110
  "test:all": "jest",
111
+ "test:agent-generator": "jest --config tools/agent-generator/jest.config.js",
111
112
  "test:consumer": "jest --roots='<rootDir>/tests' --testMatch='**/consumer-integration.test.ts' --runInBand --testTimeout=120000 --forceExit",
112
113
  "test:resolution-matrix": "node src/config/__resolution-matrix__/run-matrix.js",
113
114
  "test:performance": "jest --testPathPatterns='performance/__tests__|__tests__/performance'",
114
115
  "test:performance:isolated": "jest --config jest.performance.config.js --runInBand",
115
- "test:quick": "jest --testPathIgnorePatterns='(performance|integration)'",
116
- "test:watch": "jest --watch --testPathIgnorePatterns='performance/__tests__|__tests__/performance'",
117
- "test:coverage": "jest --coverage --testPathIgnorePatterns='performance/__tests__|__tests__/performance'",
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",
116
+ "test:watch": "jest --watch --config jest.functional.config.js",
117
+ "test:coverage": "jest --coverage --config jest.functional.config.js",
123
118
  "audit:tokens": "node scripts/audit-component-tokens.js",
124
119
  "audit:tokens:detailed": "node scripts/audit-component-tokens.js -- --detailed",
120
+ "audit:coverage-map": "npx tsx tools/agent-generator/coverage-map.ts",
125
121
  "audit:mode-parity": "npx tsx src/validators/ModeParity.ts",
126
122
  "audit:theme-drift": "npx tsx src/tools/ThemeFileGenerator.ts > /tmp/theme-skeleton-check.ts && diff -u src/tokens/themes/dark/SemanticOverrides.ts /tmp/theme-skeleton-check.ts || echo '⚠️ Theme file drift detected — regenerate with: npm run generate:theme-skeleton'",
127
123
  "generate:theme-skeleton": "npx tsx src/tools/ThemeFileGenerator.ts",
@@ -132,9 +128,11 @@
132
128
  "figma:generate-component": "npx tsx scripts/figma-component-generator.ts",
133
129
  "check:drift": "node scripts/check-package-name-drift.js",
134
130
  "check:id-uniqueness": "tsx scripts/check-id-uniqueness.ts",
131
+ "check:section-citations": "tsx scripts/check-section-citations.ts",
135
132
  "prepack": "npm run build",
136
- "prepublishOnly": "npm run build && npm run check:drift",
137
- "postpublish": "git diff --quiet token-index/ || (git add token-index/ && git commit -m 'Regenerate token-index (post-publish)' && git push origin main)"
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)",
134
+ "prepublishOnly": "npm run build && npm run check:drift && npm run verify:token-index-clean",
135
+ "postpublish": "git diff --quiet token-index/ || echo '⚠️ token-index/ changed during publish — commit it via a PR (main is branch-protected; never push it directly). See .kiro/hooks/RELEASE-FLOW.md.'"
138
136
  },
139
137
  "devDependencies": {
140
138
  "@types/glob": "^8.1.0",
@@ -183,11 +183,13 @@ describe('CSS Bundling for Browser Distribution', () => {
183
183
  expect(bundleContent).toMatch(/:hover/);
184
184
  });
185
185
 
186
- it('should contain disabled state styles', () => {
186
+ it('should not contain disabled state styles (no-disabled-states philosophy)', () => {
187
187
  const bundleContent = fs.readFileSync(ESM_BUNDLE_PATH, 'utf-8');
188
-
189
- // Components should have disabled styles
190
- expect(bundleContent).toMatch(/:disabled|--disabled/);
188
+
189
+ // DesignerPunk does not support disabled states: if an action is
190
+ // unavailable, the component should not be rendered. No component CSS
191
+ // should ship disabled selectors or modifier classes.
192
+ expect(bundleContent).not.toMatch(/:disabled|--disabled/);
191
193
  });
192
194
  });
193
195
 
@@ -0,0 +1,14 @@
1
+ [
2
+ { "suite": "src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts", "pattern": "^AvatarBaseElement: \"alt\" is required when \"src\" is provided\\.", "reason": "Spec 126 (O2, ratified 2026-07-09) resolved the original Req 5.4 / Req 9.2 tension this entry was seeded against: the warn condition is now `type === 'human' && src && alt == null && !decorative`, so a genuinely decorative or empty-alt (via setAttribute) fixture no longer warns at all. This entry is retained as a safety net, not a live suppression path — every fixture in this suite that legitimately triggers the warn (human + src + absent alt, incl. the property-form alt='' collapse) wraps it in a local jest.spyOn(console,'warn').mockImplementation(() => {}), which per console-fail-setup.ts's documented shadow behavior fully swallows the output before this hook ever sees it. Pattern updated to match the new Spec 126 message string (was: '\"alt\" prop is required when \"src\" is provided')." },
3
+ { "suite": "src/components/core/Chip-Base/__tests__/ChipBase.test.ts", "pattern": "^ChipBase: Could not calculate blend colors", "reason": "jsdom stylesheet limitation: jsdom never applies real CSS, so the blend-color token lookup cannot succeed and the component's designed CSS-fallback warn fires every run (fired async via rAF). PR #39 adjudicated as environmental noise, not a defect. Component-prefixed pattern (Lina review, PR #39) — a bare 'Could not calculate blend colors' substring is shared by Button components too; prefix-matching prevents accidentally swallowing a cross-component warn. Discharges the pending jsdom-stylesheet-limitation doc-addition (125-B-backlog.md item 5; Thurgood chip, 2026-07-09)." },
4
+ { "suite": "src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts", "pattern": "^ChipFilter: Could not calculate blend colors", "reason": "jsdom stylesheet limitation: jsdom never applies real CSS, so the blend-color token lookup cannot succeed and the component's designed CSS-fallback warn fires every run (fired async via rAF). PR #39 adjudicated as environmental noise, not a defect. Component-prefixed pattern (Lina review, PR #39) — a bare 'Could not calculate blend colors' substring is shared by Button components too; prefix-matching prevents accidentally swallowing a cross-component warn. Discharges the pending jsdom-stylesheet-limitation doc-addition (125-B-backlog.md item 5; Thurgood chip, 2026-07-09)." },
5
+ { "suite": "src/components/core/Chip-Input/__tests__/ChipInput.test.ts", "pattern": "^ChipInput: Could not calculate blend colors", "reason": "jsdom stylesheet limitation: jsdom never applies real CSS, so the blend-color token lookup cannot succeed and the component's designed CSS-fallback warn fires every run (fired async via rAF). PR #39 adjudicated as environmental noise, not a defect. Component-prefixed pattern (Lina review, PR #39) — a bare 'Could not calculate blend colors' substring is shared by Button components too; prefix-matching prevents accidentally swallowing a cross-component warn. Discharges the pending jsdom-stylesheet-limitation doc-addition (125-B-backlog.md item 5; Thurgood chip, 2026-07-09)." },
6
+ { "suite": "src/build/errors/__tests__/ErrorHandler.test.ts", "pattern": "^\\[(ERROR|WARNING|INFO)\\]|^Suggestions:|^ {2}\\u2022 |^ {2}\\.\\.\\. and \\d+ more", "reason": "Deliberate error-path logging: this suite constructs BuildErrors with fake TEST_ERROR/CONFIG_ERROR/BUILD_ERROR codes specifically to assert ErrorHandler's own console-emitting default logger (ErrorHandler.ts defaultLogger/formatErrorMessage) — the console output IS the behavior under test. PR #39 adjudicated as expected noise (96 error blocks silenced fleet-wide, this suite's share among them)." },
7
+ { "suite": "src/build/errors/__tests__/ErrorHandler.integration.test.ts", "pattern": "^\\[(ERROR|WARNING|INFO)\\]|^Suggestions:|^ {2}\\u2022 |^ {2}\\.\\.\\. and \\d+ more", "reason": "Deliberate error-path logging: this suite exercises ErrorHandler/ErrorReporter integration with fake BUILD_ERROR/CONFIG_ERROR codes — the console output IS the behavior under test. PR #39 adjudicated as expected noise." },
8
+ { "suite": "product-mcp-server/src/__tests__/GapDetector.test.ts", "pattern": "^\\[mcp-product-server\\] Component directory not found: .* \\u2014 gap detection disabled$", "reason": "Deliberate error-path logging: the 'with missing component directory' tests intentionally point GapDetector at a nonexistent path to assert its disabled-gracefully behavior — the console.error IS the assertion target for the first test and expected repeat noise for the others in the same describe block. PR #39 adjudicated as expected." },
9
+ { "suite": "product-mcp-server/src/__tests__/ProductIndexerWalk.test.ts", "pattern": "^\\[mcp-product-server\\] Indexed: \\d+ screens, \\d+ domain objects, \\d+ templates, \\d+ one-off components$", "reason": "ProductIndexer.index() logs an informational indexing summary via console.error (not a failure signal) on every index() call. PR #39 adjudicated as expected per-run noise, silenced via a beforeAll spy in this suite." },
10
+ { "suite": "product-mcp-server/src/__tests__/Spec108-ProductTokens.test.ts", "pattern": "^\\[mcp-product-server\\] Indexed: \\d+ screens, \\d+ domain objects, \\d+ templates, \\d+ one-off components$", "reason": "ProductIndexer.index() logs an informational indexing summary via console.error (not a failure signal) on every index() call. PR #39 adjudicated as expected per-run noise, silenced via a beforeAll spy in this suite." },
11
+ { "suite": "src/__tests__/integration/Spec107-DesignLanguageContext.test.ts", "pattern": "^\\[mcp-product-server\\] Indexed: \\d+ screens, \\d+ domain objects, \\d+ templates, \\d+ one-off components$", "reason": "ProductIndexer.index() logs an informational indexing summary via console.error (not a failure signal); this suite's ProductIndexer instances are constructed inside nested beforeAll hooks, so PR #39 installed the spy in an outer beforeAll (not beforeEach) to be active before them. Adjudicated as expected noise." },
12
+ { "suite": "src/cli/__tests__/figma-extract.test.ts", "pattern": "^\\u274c |^Usage: npm run figma:extract|^\\s+npm run figma:extract|^\\s+Run the DTCG generator first|^$", "reason": "Genuinely novel class, adjudicated in this task (125-B Task 4.4, not part of PR #39): figma-extract is a CLI tool whose console.error output IS its product surface — every message this suite asserts on is the deliberate, deterministic error/usage text figma-extract.ts emits (arg validation, DTCG-load failures, extraction failures, usage banners, blank-line separators). This suite installs its own module-top-level console.error spy (mockConsoleError, restored per-file, not per-test) and reads its .mock.calls directly; this hook's beforeEach captures the CURRENT console.error before spying and calls through to it, so the suite's own assertions keep working unaffected." },
13
+ { "suite": "src/cli/__tests__/figma-push.test.ts", "pattern": "^\\u274c |^Resolution:|^\\s+\\d+\\.\\s|^\\s+-\\s|^\\s+Run the DTCG generator first|^$", "reason": "Genuinely novel class, adjudicated in this task (125-B Task 4.4, not part of PR #39): figma-push is a CLI tool whose console.error output IS its product surface — every message this suite asserts on is the deliberate, deterministic error/resolution text figma-push.ts emits (arg validation, DTCG-load failures, drift/sync-failure reporting with its numbered resolution steps and ' - <detail>' lists, blank-line separators). Same module-top-level console.error spy pattern as figma-extract.test.ts; this hook chains through to it rather than shadowing it." }
14
+ ]
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Console-fail hook (125-B, Requirement 11 / Design C8).
3
+ *
4
+ * Captures console.error/console.warn per test and FAILS the test when
5
+ * unallowlisted output is produced. Checked against the checked-in
6
+ * allowlist (`src/__tests__/console-allowlist.json`, a (suite × message-pattern)
7
+ * grain — see that file for entries + reasons, and
8
+ * governance/classification-map.md § "console-fail-root-lanes" for the rule
9
+ * this hook enforces).
10
+ *
11
+ * SCOPE — root functional lanes ONLY (wired via the root jest.config.js's
12
+ * `setupFilesAfterEnv`). Sub-package suites (mcp-server, application-mcp-server
13
+ * — own jest 29 configs outside this repo's root `roots`) are NOT covered by
14
+ * this hook; see governance/classification-map.md
15
+ * § "console-fail-subpackage-deferred" for the recorded deferral.
16
+ * `product-mcp-server/src/**` IS covered — it lives inside root `roots`.
17
+ *
18
+ * VERSION-AGNOSTIC BY DESIGN (125-B C8 constraint, recorded for any future
19
+ * sub-package replication): plain `jest.spyOn` / method-swap capture only —
20
+ * no jest-30-only API. Stable on jest 29 and jest 30.
21
+ *
22
+ * Known limitation (recorded, not solved here): suites that install their
23
+ * OWN console.error/warn spy on top of this hook's (26 files predate this
24
+ * hook — "house style", nested `describe`-level `beforeEach`, which runs
25
+ * AFTER this hook's root-level `beforeEach`) shadow this hook for the
26
+ * duration of their own mock. Output fully swallowed by a local mock's
27
+ * `mockImplementation(() => {})` never reaches this hook (nothing to check
28
+ * — correct no-op). Output a local mock passes through (e.g. Chip's
29
+ * "anything else passes through" pattern) DOES reach this hook via the
30
+ * bound reference the local suite captured. Because this hook is a plain
31
+ * property swap rather than a `jest.spyOn` mock (see below), a local
32
+ * suite's own `jest.restoreAllMocks()` restores only ITS OWN jest-tracked
33
+ * spy — never this hook's swap, which isn't in Jest's mock registry at all.
34
+ * The residual edge case is narrower than a spyOn-based hook would have:
35
+ * async/deferred output (e.g. an rAF-queued warn) that fires after this
36
+ * test's `afterEach` has already restored `console.error`/`console.warn`
37
+ * back to whatever was there before this test's `beforeEach` — a normal,
38
+ * expected end-of-test boundary, not a cross-hook interaction — can land on
39
+ * the real console unrecorded and unchecked. It does not produce false
40
+ * failures.
41
+ *
42
+ * Chain-through, not shadow (fixed during 4.4's own gate-bite run): the
43
+ * "before" console.error/warn reference is captured FRESH inside
44
+ * `beforeEach`, immediately before this hook installs itself — not once at
45
+ * module load. A handful of CLI suites (figma-extract.test.ts,
46
+ * figma-push.test.ts) install a PERSISTENT module-top-level console mock of
47
+ * their own (no per-test beforeEach/afterEach — restored once in `afterAll`,
48
+ * if at all) and assert against its `.mock.calls`. Capturing "before" once
49
+ * at this module's own load time would have bound to the pristine native
50
+ * console — permanently bypassing those suites' own mocks for every test
51
+ * and silently breaking their `consoleOutput()`-style assertions.
52
+ *
53
+ * PLAIN PROPERTY SWAP, NOT `jest.spyOn` (also fixed during 4.4's gate-bite
54
+ * run — the reason this hook does NOT use jest.spyOn even though the header
55
+ * above still calls the general technique "spyOn/method swap"): `jest.spyOn`
56
+ * has a documented reuse rule — spying on a property that is ALREADY a Jest
57
+ * mock function returns and mutates THAT SAME mock object rather than
58
+ * wrapping a new one. Against the figma-extract/figma-push suites'
59
+ * persistent console.error mock, that reuse rule made `jest.spyOn(console,
60
+ * 'error').mockImplementation(fn)` overwrite THEIR mock's implementation
61
+ * with `fn` — and this hook's own "before" reference, bound to that same
62
+ * mutable mock object, then called back into `fn` on every invocation:
63
+ * infinite recursion, observed as a stack overflow on the very first
64
+ * `console.error` call in a chained suite. A plain property assignment
65
+ * (`console.error = wrapper; ...; console.error = original;`) sidesteps
66
+ * Jest's mock-identity bookkeeping entirely — the same technique the header
67
+ * above documents as version-agnostic ("plain jest.spyOn / method swap"),
68
+ * using the method-swap half specifically because it composes safely with
69
+ * another suite's pre-existing mock, which spyOn's reuse rule does not.
70
+ */
71
+
72
+ import allowlistData from './console-allowlist.json';
73
+ import * as path from 'path';
74
+
75
+ interface AllowlistEntry {
76
+ suite: string;
77
+ pattern: string;
78
+ reason: string;
79
+ }
80
+
81
+ const allowlist = allowlistData as AllowlistEntry[];
82
+
83
+ function isAllowed(suite: string, message: string): boolean {
84
+ return allowlist.some((entry) => {
85
+ if (entry.suite !== suite) return false;
86
+ try {
87
+ return new RegExp(entry.pattern).test(message);
88
+ } catch {
89
+ // A malformed pattern never silently passes output through.
90
+ return false;
91
+ }
92
+ });
93
+ }
94
+
95
+ function stringifyArg(value: unknown): string {
96
+ if (value instanceof Error) return value.stack || value.message;
97
+ if (typeof value === 'string') return value;
98
+ try {
99
+ return JSON.stringify(value);
100
+ } catch {
101
+ return String(value);
102
+ }
103
+ }
104
+
105
+ function formatArgs(args: unknown[]): string {
106
+ return args.map(stringifyArg).join(' ');
107
+ }
108
+
109
+ function currentSuitePath(): string {
110
+ // expect.getState().testPath is the absolute path to the currently
111
+ // running test file. Repo root is resolved relative to THIS file
112
+ // (src/__tests__/), so it's correct regardless of process cwd.
113
+ const testPath = expect.getState().testPath;
114
+ if (!testPath) return '';
115
+ const repoRoot = path.resolve(__dirname, '..', '..');
116
+ return path.relative(repoRoot, testPath).split(path.sep).join('/');
117
+ }
118
+
119
+ interface CapturedOutput {
120
+ method: 'error' | 'warn';
121
+ message: string;
122
+ }
123
+
124
+ let captured: CapturedOutput[] = [];
125
+ let originalError: typeof console.error | undefined;
126
+ let originalWarn: typeof console.warn | undefined;
127
+
128
+ beforeEach(() => {
129
+ captured = [];
130
+
131
+ // Capture whatever is CURRENTLY installed (native console, or another
132
+ // suite's own persistent mock) immediately before swapping — see the
133
+ // "Chain-through, not shadow" / "Plain property swap" header notes.
134
+ originalError = console.error;
135
+ originalWarn = console.warn;
136
+
137
+ console.error = (...args: unknown[]) => {
138
+ captured.push({ method: 'error', message: formatArgs(args) });
139
+ originalError!.apply(console, args);
140
+ };
141
+
142
+ console.warn = (...args: unknown[]) => {
143
+ captured.push({ method: 'warn', message: formatArgs(args) });
144
+ originalWarn!.apply(console, args);
145
+ };
146
+ });
147
+
148
+ afterEach(() => {
149
+ const suite = currentSuitePath();
150
+
151
+ // Targeted restore only — put back exactly what was there before this
152
+ // test's beforeEach ran (native console, or another suite's own mock).
153
+ if (originalError) console.error = originalError;
154
+ if (originalWarn) console.warn = originalWarn;
155
+
156
+ const unallowed = captured.filter((entry) => !isAllowed(suite, entry.message));
157
+ captured = [];
158
+
159
+ if (unallowed.length > 0) {
160
+ const details = unallowed
161
+ .map((entry) => ` [console.${entry.method}] ${entry.message}`)
162
+ .join('\n');
163
+ throw new Error(
164
+ `console-fail: unallowlisted console output in ${suite}:\n${details}\n\n` +
165
+ 'Legitimate expected noise: add a { suite, pattern, reason } entry to ' +
166
+ 'src/__tests__/console-allowlist.json. Genuine defect: fix the source.'
167
+ );
168
+ }
169
+ });
@@ -16,6 +16,22 @@ const PHILOSOPHY_PATH = path.resolve(__dirname, '../../../design-language/design
16
16
  const PRODUCT_FIXTURES = path.resolve(__dirname, '../../../product-mcp-server/src/__tests__/fixtures');
17
17
 
18
18
  describe('Spec 107 Integration: Design Language Context', () => {
19
+ let consoleErrorSpy: jest.SpyInstance;
20
+
21
+ beforeAll(() => {
22
+ // ProductIndexer logs indexing summaries via console.error (informational, not a failure).
23
+ // Installed in beforeAll (not beforeEach) so it's active before nested beforeAll hooks
24
+ // that construct/index a ProductIndexer run.
25
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ });
27
+
28
+ afterAll(() => {
29
+ consoleErrorSpy.mockRestore();
30
+ });
31
+
32
+ beforeEach(() => {
33
+ consoleErrorSpy.mockClear();
34
+ });
19
35
 
20
36
  describe('Application MCP — Design Philosophy', () => {
21
37
  let indexer: any;