@hegemonart/get-design-done 1.30.6 → 1.31.5

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 (175) hide show
  1. package/.claude-plugin/marketplace.json +6 -3
  2. package/.claude-plugin/plugin.json +5 -2
  3. package/CHANGELOG.md +105 -0
  4. package/NOTICE +224 -0
  5. package/README.md +22 -1
  6. package/SKILL.md +1 -0
  7. package/agents/design-authority-watcher.md +1 -1
  8. package/agents/perf-analyzer.md +2 -2
  9. package/bin/gdd-mcp +78 -0
  10. package/bin/gdd-sdk +34 -24
  11. package/bin/gdd-state-mcp +78 -0
  12. package/{README.de.md → docs/i18n/README.de.md} +1 -1
  13. package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
  14. package/{README.it.md → docs/i18n/README.it.md} +1 -1
  15. package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
  16. package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
  17. package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
  18. package/hooks/_hook-emit.js +1 -1
  19. package/hooks/budget-enforcer.ts +5 -5
  20. package/hooks/context-exhaustion.ts +2 -2
  21. package/hooks/gdd-precompact-snapshot.js +3 -3
  22. package/hooks/gdd-read-injection-scanner.ts +2 -2
  23. package/hooks/gdd-sessionstart-recap.js +1 -1
  24. package/hooks/gdd-turn-closeout.js +1 -1
  25. package/package.json +24 -10
  26. package/recipes/.gitkeep +0 -0
  27. package/reference/schemas/recipe.schema.json +33 -0
  28. package/scripts/cli/gdd-events.mjs +5 -5
  29. package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
  30. package/scripts/lib/cli/index.ts +22 -160
  31. package/scripts/lib/connection-probe/index.cjs +1 -1
  32. package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
  33. package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
  34. package/scripts/lib/error-classifier.cjs +24 -227
  35. package/scripts/lib/event-stream/index.ts +25 -193
  36. package/scripts/lib/figma-extract/digest.cjs +430 -0
  37. package/scripts/lib/figma-extract/parse-url.cjs +87 -0
  38. package/scripts/lib/figma-extract/payload-schema.json +108 -0
  39. package/scripts/lib/figma-extract/pull.cjs +394 -0
  40. package/scripts/lib/figma-extract/receiver.cjs +273 -0
  41. package/scripts/lib/figma-extract/render-md.cjs +143 -0
  42. package/scripts/lib/figma-extract/styles-resolver.cjs +147 -0
  43. package/scripts/lib/figma-extract/walk.cjs +100 -0
  44. package/scripts/lib/gdd-errors/index.ts +24 -213
  45. package/scripts/lib/gdd-state/index.ts +23 -161
  46. package/scripts/lib/health-mirror/index.cjs +88 -1
  47. package/scripts/lib/iteration-budget.cjs +23 -199
  48. package/scripts/lib/jittered-backoff.cjs +24 -107
  49. package/scripts/lib/lockfile.cjs +23 -195
  50. package/scripts/lib/logger/index.ts +1 -1
  51. package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
  52. package/scripts/lib/perf-analyzer/index.cjs +1 -1
  53. package/scripts/lib/pipeline-runner/index.ts +4 -4
  54. package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
  55. package/scripts/lib/prompt-dedup/index.cjs +1 -1
  56. package/scripts/lib/rate-guard.cjs +2 -2
  57. package/scripts/lib/recipe-loader.cjs +142 -0
  58. package/scripts/lib/session-runner/errors.ts +3 -3
  59. package/scripts/lib/session-runner/index.ts +3 -3
  60. package/scripts/lib/session-runner/transcript.ts +1 -1
  61. package/scripts/lib/tool-scoping/index.ts +1 -1
  62. package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
  63. package/scripts/mcp-servers/gdd-state/server.ts +28 -282
  64. package/sdk/README.md +45 -0
  65. package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
  66. package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
  67. package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
  68. package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
  69. package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
  70. package/sdk/cli/index.js +8091 -0
  71. package/sdk/cli/index.ts +172 -0
  72. package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
  73. package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
  74. package/sdk/errors/index.ts +218 -0
  75. package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
  76. package/sdk/event-stream/index.ts +197 -0
  77. package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
  78. package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
  79. package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
  80. package/sdk/index.ts +19 -0
  81. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
  82. package/sdk/mcp/gdd-mcp/server.js +1924 -0
  83. package/sdk/mcp/gdd-mcp/server.ts +325 -0
  84. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
  85. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
  86. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
  87. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
  88. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
  89. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
  90. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
  91. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
  92. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
  93. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
  94. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
  95. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
  96. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
  97. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
  98. package/sdk/mcp/gdd-state/server.js +2790 -0
  99. package/sdk/mcp/gdd-state/server.ts +294 -0
  100. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
  101. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
  102. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
  103. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
  104. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
  105. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
  106. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
  107. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
  108. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
  109. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
  110. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
  111. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
  112. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
  113. package/sdk/primitives/error-classifier.cjs +232 -0
  114. package/sdk/primitives/iteration-budget.cjs +205 -0
  115. package/sdk/primitives/jittered-backoff.cjs +112 -0
  116. package/sdk/primitives/lockfile.cjs +201 -0
  117. package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
  118. package/sdk/state/index.ts +167 -0
  119. package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
  120. package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
  121. package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
  122. package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
  123. package/skills/figma-extract/SKILL.md +64 -0
  124. package/skills/health/SKILL.md +10 -0
  125. package/skills/quality-gate/SKILL.md +2 -2
  126. package/scripts/aggregate-agent-metrics.ts +0 -282
  127. package/scripts/bootstrap-manifest.txt +0 -3
  128. package/scripts/bootstrap.sh +0 -80
  129. package/scripts/build-distribution-bundles.cjs +0 -549
  130. package/scripts/build-intel.cjs +0 -486
  131. package/scripts/codegen-schema-types.ts +0 -149
  132. package/scripts/detect-stale-refs.cjs +0 -107
  133. package/scripts/e2e/run-headless.ts +0 -514
  134. package/scripts/extract-changelog-section.cjs +0 -58
  135. package/scripts/gsd-cleanup-incubator.cjs +0 -367
  136. package/scripts/injection-patterns.cjs +0 -58
  137. package/scripts/lint-agentskills-spec.cjs +0 -457
  138. package/scripts/release-smoke-test.cjs +0 -200
  139. package/scripts/rollback-release.sh +0 -42
  140. package/scripts/run-injection-scanner-ci.cjs +0 -83
  141. package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
  142. package/scripts/tests/test-authority-watcher-diff.sh +0 -113
  143. package/scripts/tests/test-motion-provenance.sh +0 -64
  144. package/scripts/validate-frontmatter.ts +0 -409
  145. package/scripts/validate-incubator-scope.cjs +0 -133
  146. package/scripts/validate-schemas.ts +0 -401
  147. package/scripts/validate-skill-length.cjs +0 -283
  148. package/scripts/verify-version-sync.cjs +0 -30
  149. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
  150. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
  151. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
  152. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
  153. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
  154. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
  155. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
  156. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
  157. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
  158. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
  159. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
  160. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
  161. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
  162. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
  163. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
  164. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
  165. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
  166. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
  167. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
  168. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
  169. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
  170. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
  171. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
  172. /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
  173. /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
  174. /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
  175. /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
@@ -1,197 +1,29 @@
1
- // scripts/lib/event-stream/index.ts — public API for the Phase 20+
2
- // telemetry stream (Plan 20-06, SDK-08).
1
+ // scripts/lib/event-stream/index.ts — GDD-DEPRECATION-SHIM (Plan 31-5-06, SDK-05, D-02).
3
2
  //
4
- // Consumers import ONLY from this file. The internal parts
5
- // (`./types.ts`, `./writer.ts`, `./emitter.ts`) are implementation
6
- // detail; changing them without updating this export surface is a
7
- // breaking change for downstream plans (20-05 MCP handlers, 20-13 hooks).
3
+ // Thin deprecation shim. The real implementation moved to
4
+ // sdk/event-stream/index.ts in Plan 31-5-04 (SDK consolidation). This file
5
+ // is re-created at the OLD path so undocumented EXTERNAL importers (anyone
6
+ // who reached into node_modules/@hegemonart/get-design-done/scripts/lib/
7
+ // event-stream/index.ts directly) keep working for one minor grace window.
8
8
  //
9
- // Surface:
10
- // * appendEvent(ev) — persist + broadcast one event
11
- // * getWriter(opts?) lazy singleton EventWriter
12
- // * getBus() — lazy singleton EventBus
13
- // * reset() — clear module-level singletons (tests)
14
- // * types — BaseEvent, KnownEvent, and every pre-
15
- // registered subtype (StateMutationEvent,
16
- // StateTransitionEvent, …).
17
-
18
- import { hostname } from 'node:os';
19
-
20
- import { EventBus } from './emitter.ts';
21
- import type { Unsubscribe, EventHandler } from './emitter.ts';
22
- import { EventWriter } from './writer.ts';
23
- import type { WriterOptions } from './writer.ts';
24
- import type { BaseEvent, EventMeta } from './types.ts';
25
-
26
- export type {
27
- BaseEvent,
28
- EventMeta,
29
- KnownEvent,
30
- StateMutationEvent,
31
- StateTransitionEvent,
32
- StageEnteredEvent,
33
- StageExitedEvent,
34
- HookFiredEvent,
35
- ErrorEvent,
36
- WaveStartedEvent,
37
- WaveCompletedEvent,
38
- BlockerAddedEvent,
39
- DecisionAddedEvent,
40
- MustHaveAddedEvent,
41
- ParallelismVerdictEvent,
42
- CostUpdateEvent,
43
- RateLimitEvent,
44
- ApiRetryEvent,
45
- CompactBoundaryEvent,
46
- McpProbeEvent,
47
- ReflectionProposedEvent,
48
- ConnectionStatusChangeEvent,
49
- ToolCallStartedEvent,
50
- ToolCallCompletedEvent,
51
- AgentSpawnEvent,
52
- AgentOutcomeEvent,
53
- // Phase 27 / Plan 27-08 — peer-CLI delegation events (D-09).
54
- RuntimeRole,
55
- PeerCallStartedEvent,
56
- PeerCallCompleteEvent,
57
- PeerCallFailedEvent,
58
- } from './types.ts';
59
- export {
60
- KNOWN_EVENT_TYPES,
61
- // Phase 27 / Plan 27-08 — symbolic constants for peer-CLI event names.
62
- PEER_CALL_STARTED,
63
- PEER_CALL_COMPLETE,
64
- PEER_CALL_FAILED,
65
- PEER_CALL_EVENT_TYPES,
66
- DEFAULT_RUNTIME_ROLE,
67
- } from './types.ts';
68
- export { EventBus } from './emitter.ts';
69
- export type { EventHandler, Unsubscribe } from './emitter.ts';
70
- export { EventWriter, DEFAULT_EVENTS_PATH, DEFAULT_MAX_LINE_BYTES } from './writer.ts';
71
- export type { WriterOptions } from './writer.ts';
72
- export { readEvents, aggregate } from './reader.ts';
73
- export type { ReadEventsOptions, AggregateResult } from './reader.ts';
74
-
75
- /**
76
- * Lazily-constructed module-level singletons. `getWriter()` honors the
77
- * first `opts` it receives; subsequent calls with different options are
78
- * ignored. Tests that need to vary options across runs should call
79
- * {@link reset} between runs.
80
- */
81
- let defaultWriter: EventWriter | null = null;
82
- let defaultBus: EventBus | null = null;
83
- /**
84
- * Cached host name. `os.hostname()` is cheap but not free (syscall on
85
- * some platforms) and we stamp it onto every event; compute once.
86
- */
87
- let cachedHost: string | null = null;
88
-
89
- /**
90
- * Return the module-level default writer, constructing it on first
91
- * call. Passing `opts` on subsequent calls is a no-op (the first
92
- * caller wins); that matches the "single shared file per process"
93
- * intent.
94
- */
95
- export function getWriter(opts?: WriterOptions): EventWriter {
96
- if (defaultWriter === null) {
97
- // Honor GDD_EVENTS_PATH env var as the first-choice default path
98
- // when the caller doesn't pass an explicit `opts.path`. Lets test
99
- // harnesses and Plan 21-11's E2E subprocess steer the on-disk
100
- // stream into a fixture-specific directory without chdir'ing the
101
- // entire process. Explicit `opts.path` always wins.
102
- const envPath: string | undefined = process.env['GDD_EVENTS_PATH'];
103
- const finalOpts: WriterOptions =
104
- opts?.path === undefined && envPath !== undefined && envPath.length > 0
105
- ? { ...(opts ?? {}), path: envPath }
106
- : (opts ?? {});
107
- defaultWriter = new EventWriter(finalOpts);
108
- }
109
- return defaultWriter;
110
- }
111
-
112
- /** Return the module-level default bus, constructing it on first call. */
113
- export function getBus(): EventBus {
114
- if (defaultBus === null) {
115
- defaultBus = new EventBus();
116
- }
117
- return defaultBus;
118
- }
119
-
120
- /**
121
- * Persist `ev` to the on-disk JSONL stream AND broadcast it to the
122
- * in-process bus. This is the normal emission path for every Phase 20+
123
- * event producer.
124
- *
125
- * Ordering:
126
- * 1. Stamp `_meta` (pid/host/source) if the caller didn't supply it.
127
- * 2. Persist via `getWriter().append(ev)` — sync, never throws.
128
- * 3. Broadcast via `getBus().emit(ev.type, ev)` AND `emit('*', ev)`
129
- * so typed subscribers and `subscribeAll` observers both see it.
130
- *
131
- * Bus emission can still throw if a subscriber handler throws; we
132
- * intentionally surface that rather than silently swallowing, since a
133
- * failing handler is a programming bug, not an expected runtime
134
- * condition. Plan 20-13's hooks wrap their handler bodies defensively
135
- * for this reason.
136
- */
137
- export function appendEvent(ev: BaseEvent): void {
138
- // Stamp writer-injected metadata if absent. We don't clone the full
139
- // event — callers typically build it fresh per emission — but we do
140
- // need to ensure `_meta` is present by the time we persist.
141
- if (ev._meta === undefined) {
142
- if (cachedHost === null) {
143
- try {
144
- cachedHost = hostname();
145
- } catch {
146
- cachedHost = 'unknown';
147
- }
148
- }
149
- const meta: EventMeta = {
150
- pid: process.pid,
151
- host: cachedHost,
152
- source: 'event-stream',
153
- };
154
- ev._meta = meta;
155
- }
156
-
157
- // Persist first. Bus emission is synchronous; if a subscriber throws
158
- // after we've persisted, the durable record is already safe.
159
- getWriter().append(ev);
160
-
161
- const bus = getBus();
162
- bus.emit(ev.type, ev);
163
- bus.emit('*', ev);
164
- }
165
-
166
- /**
167
- * Reset module-level singletons. Intended for tests that want a fresh
168
- * writer (e.g. pointed at a new temp directory) or a fresh bus (e.g.
169
- * to assert isolation between test cases).
170
- *
171
- * Safe to call from production code but the intended caller is a test.
172
- * `appendEvent()` will lazily reconstruct both singletons on the next
173
- * emission.
174
- */
175
- export function reset(): void {
176
- if (defaultBus !== null) {
177
- defaultBus.removeAllListeners();
178
- }
179
- defaultWriter = null;
180
- defaultBus = null;
181
- }
182
-
183
- // Re-export `subscribe`/`subscribeAll` convenience: some callers only
184
- // need to subscribe, not emit, and `getBus().subscribe(…)` reads fine
185
- // but the shorter form keeps consumer code terse.
186
- /** Convenience: subscribe to one event type on the default bus. */
187
- export function subscribe<T extends BaseEvent = BaseEvent>(
188
- type: T['type'],
189
- handler: EventHandler<T>,
190
- ): Unsubscribe {
191
- return getBus().subscribe<T>(type, handler);
9
+ // REMOVED IN v1.33.0 (D-02). Grace window: 1.31.5 ships with shims →
10
+ // 1.32.0 still has them 1.33.0 removes them. Internal callers already use
11
+ // the sdk/ path (Plan 31-5-04/05) this shim is external-only; 31-5-10's
12
+ // no-stale-internal-refs guard excludes files carrying the
13
+ // GDD-DEPRECATION-SHIM marker above.
14
+ //
15
+ // Runs under --experimental-strip-types, so `export *` re-export is
16
+ // strip-types-clean.
17
+
18
+ import { emitWarning } from 'node:process';
19
+
20
+ let warned = false;
21
+ if (!warned) {
22
+ warned = true;
23
+ emitWarning(
24
+ 'scripts/lib/event-stream/index.ts is deprecated; import sdk/event-stream instead. Removed in v1.33.0.',
25
+ 'DeprecationWarning',
26
+ );
192
27
  }
193
28
 
194
- /** Convenience: subscribe to every event on the default bus. */
195
- export function subscribeAll(handler: EventHandler<BaseEvent>): Unsubscribe {
196
- return getBus().subscribeAll(handler);
197
- }
29
+ export * from '../../../sdk/event-stream/index.ts';
@@ -0,0 +1,430 @@
1
+ 'use strict';
2
+ /**
3
+ * Plan 31-02 — productionized from spike 001 digest.mjs (orchestration + token extraction).
4
+ *
5
+ * DIGEST stage of the two-stage pipeline (decision D-01: extract → digest stay
6
+ * separated). This module reads ONLY the raw/ cache that pull.cjs (31-01) wrote;
7
+ * it performs ZERO network calls, so it can re-run against an existing cache
8
+ * without re-pulling (idempotent / off-line).
9
+ *
10
+ * Three-path token assembly (decision D-04):
11
+ * Path A — Variables API body (rawDir/variables.json without the plugin marker)
12
+ * Path B — styles resolver (rawDir/styles.json) — pluggable seam; 31-03 ships
13
+ * the real two-step /styles + /nodes?ids= resolver
14
+ * Path C — plugin sync (rawDir/variables.json WITH the receiver marker, written
15
+ * by 31-06's localhost receiver)
16
+ * Resolution priority on name collision: Variables > plugin sync > styles.
17
+ * The --prefer-styles escape inverts the chain to prefer styles.
18
+ *
19
+ * Pure CommonJS, no external deps, no network.
20
+ *
21
+ * Per-component slicing (decision D-08):
22
+ * digest({..., component}) — when `component` is a name or glob (e.g.
23
+ * 'Sample/Button', 'Sample/*', 'Form/Butt?'), the digest renders ONLY
24
+ * the matching component(s) — a ~500-token slice instead of the full
25
+ * ~16K digest. The filter is strictly ADDITIVE: omitting `component`
26
+ * reproduces 31-02's full-digest behavior unchanged. Glob support is a
27
+ * few lines of in-file string work (no external glob dependency).
28
+ *
29
+ * Exports:
30
+ * digest(opts) — async orchestrator (reads raw/, writes digest/)
31
+ * assembleTokens(opts) — pure three-path merge by priority
32
+ * DEFAULT_TOKEN_PRIORITY — ['variables','plugin','styles'] (D-04)
33
+ * globToRegExp(pattern) — minimal glob→RegExp (D-08 component filter)
34
+ */
35
+
36
+ const fs = require('node:fs/promises');
37
+ const path = require('node:path');
38
+ const { collectComponents } = require('./walk.cjs');
39
+ const { renderDesignMd } = require('./render-md.cjs');
40
+
41
+ // D-04: Variables > plugin sync > styles.
42
+ const DEFAULT_TOKEN_PRIORITY = ['variables', 'plugin', 'styles'];
43
+
44
+ // Receiver-written payload marker (31-06 contract). A rawDir/variables.json that
45
+ // carries this top-level field is the plugin's Path-C payload, NOT the Figma
46
+ // Variables API body.
47
+ const PLUGIN_PAYLOAD_MARKER = 'gdd-plugin';
48
+
49
+ // ── component filter (D-08) ────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Translate a minimal glob pattern into an anchored, case-sensitive RegExp.
53
+ *
54
+ * Supported wildcards (the only two the acceptance criterion needs):
55
+ * `*` → `.*` (zero or more of any char)
56
+ * `?` → `.` (exactly one char)
57
+ * Every other character — including regex metacharacters like `.`/`/`/`(`/`+`
58
+ * and the Figma `Sample/Path/Name` separators — is treated LITERALLY. We escape
59
+ * the whole pattern first, then re-activate the escaped `\*`/`\?` placeholders,
60
+ * so e.g. the `.` in 'Sample.Icon' is literal and does NOT over-match 'Sample/Icon'.
61
+ *
62
+ * Matching is exact (anchored `^...$`) and case-sensitive — Figma component
63
+ * names are case-significant, and an exact name with no wildcard must match only
64
+ * that one component.
65
+ *
66
+ * @param {string} pattern a component name or glob (e.g. 'Button*', 'Form/*')
67
+ * @returns {RegExp}
68
+ */
69
+ function globToRegExp(pattern) {
70
+ // Escape ALL regex metacharacters (incl. * and ? for now).
71
+ const escaped = String(pattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72
+ // Re-activate the wildcards: escaped '\*' → '.*', escaped '\?' → '.'.
73
+ const body = escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
74
+ return new RegExp(`^${body}$`);
75
+ }
76
+
77
+ /**
78
+ * Collect the set of token names that are RELEVANT to a set of components, so a
79
+ * slice can carry just those tokens instead of the full ~hundreds-of-tokens
80
+ * catalog (which would blow past the ~500-token budget). Relevance = a token
81
+ * whose name is referenced by a component's prop default/options, variant name,
82
+ * or the component name itself. When nothing is determinable we return an empty
83
+ * set and the slice renders component shape only — keeping the slice bounded
84
+ * regardless of catalog size (D-08 size guarantee).
85
+ *
86
+ * @param {Array} matched matched component entries (from collectComponents)
87
+ * @param {Array} tokens the full assembled token list
88
+ * @returns {Array} the subset of `tokens` referenced by the matched components
89
+ */
90
+ function tokensForComponents(matched, tokens) {
91
+ if (!Array.isArray(tokens) || tokens.length === 0) return [];
92
+ // Build a haystack of strings drawn from the matched components.
93
+ const haystack = [];
94
+ for (const c of matched) {
95
+ if (c.name) haystack.push(c.name);
96
+ for (const v of c.variants || []) haystack.push(v);
97
+ for (const p of c.props || []) {
98
+ if (p.name) haystack.push(p.name);
99
+ if (p.default !== undefined) haystack.push(String(p.default));
100
+ for (const o of p.options || []) haystack.push(String(o));
101
+ }
102
+ }
103
+ const blob = haystack.join('\n');
104
+ return tokens.filter((t) => t && t.name !== undefined && blob.includes(t.name));
105
+ }
106
+
107
+ // ── helpers (Path A) ─────────────────────────────────────────────────────────
108
+
109
+ /** Convert a Figma {r,g,b,a?} (0..1 floats) colour to a hex string. */
110
+ function rgbToHex({ r, g, b, a }) {
111
+ const to = (v) => Math.round(v * 255).toString(16).padStart(2, '0');
112
+ const hex = `#${to(r)}${to(g)}${to(b)}`;
113
+ return a !== undefined && a < 1 ? `${hex}${to(a)}` : hex;
114
+ }
115
+
116
+ /**
117
+ * Path A — extract tokens from a Figma Variables API body
118
+ * (`/v1/files/:key/variables/local`). Mirrors the spike's extractTokensFromVariables.
119
+ * @param {object|null} vars the Variables API response (has .meta.{variables,variableCollections})
120
+ * @returns {Array<{name,type,collection?,modes?}>}
121
+ */
122
+ function extractTokensFromVariables(vars) {
123
+ if (!vars || !vars.meta) return [];
124
+ const collections = vars.meta.variableCollections || {};
125
+ const variables = vars.meta.variables || {};
126
+ const tokens = [];
127
+ for (const v of Object.values(variables)) {
128
+ const collection = collections[v.variableCollectionId];
129
+ const modes = collection?.modes || [];
130
+ const valuesByMode = {};
131
+ for (const mode of modes) {
132
+ const raw = v.valuesByMode?.[mode.modeId];
133
+ if (raw && typeof raw === 'object' && 'r' in raw) {
134
+ valuesByMode[mode.name] = rgbToHex(raw);
135
+ } else if (raw && raw.type === 'VARIABLE_ALIAS') {
136
+ valuesByMode[mode.name] = `{${variables[raw.id]?.name || raw.id}}`;
137
+ } else {
138
+ valuesByMode[mode.name] = raw;
139
+ }
140
+ }
141
+ tokens.push({
142
+ name: v.name,
143
+ type: v.resolvedType,
144
+ collection: collection?.name,
145
+ modes: valuesByMode,
146
+ });
147
+ }
148
+ return tokens;
149
+ }
150
+
151
+ /**
152
+ * Path C — normalize a receiver-written plugin payload into the common token
153
+ * shape. The plugin (D-13) emits ALL local variables; we accept either a
154
+ * pre-shaped `tokens[]` array or the raw `variables`/`meta` form and pass it
155
+ * through extractTokensFromVariables when needed.
156
+ * @param {object|null} payload variables.json carrying source:'gdd-plugin'
157
+ * @returns {Array}
158
+ */
159
+ function normalizePluginPayload(payload) {
160
+ if (!payload) return [];
161
+ // Preferred shape: the plugin already emits a flat tokens[] array.
162
+ if (Array.isArray(payload.tokens)) return payload.tokens;
163
+ // Fallback: it carries a Variables-API-like body — reuse Path A extraction.
164
+ if (payload.meta) return extractTokensFromVariables(payload);
165
+ return [];
166
+ }
167
+
168
+ // ── three-path merge (D-04) ──────────────────────────────────────────────────
169
+
170
+ /**
171
+ * Merge the three token sources by priority. On a NAME collision the
172
+ * higher-priority source wins.
173
+ *
174
+ * Implementation note: we iterate the priority chain HIGHEST-first and only set
175
+ * a name the first time we see it (skip-if-present), so the highest-priority
176
+ * source's entry is the one that survives.
177
+ *
178
+ * @param {object} opts
179
+ * @param {Array} [opts.variables] Path A tokens
180
+ * @param {Array} [opts.pluginVariables] Path C tokens
181
+ * @param {Array} [opts.styleTokens] Path B tokens
182
+ * @param {boolean} [opts.preferStyles] D-04 escape — move styles to the front
183
+ * @returns {Array} merged tokens (insertion order follows the priority chain)
184
+ */
185
+ function assembleTokens({ variables, pluginVariables, styleTokens, preferStyles } = {}) {
186
+ const bySource = {
187
+ variables: Array.isArray(variables) ? variables : [],
188
+ plugin: Array.isArray(pluginVariables) ? pluginVariables : [],
189
+ styles: Array.isArray(styleTokens) ? styleTokens : [],
190
+ };
191
+ const priority = preferStyles
192
+ ? ['styles', 'variables', 'plugin']
193
+ : DEFAULT_TOKEN_PRIORITY;
194
+
195
+ const merged = new Map();
196
+ for (const source of priority) {
197
+ for (const tok of bySource[source]) {
198
+ if (!tok || tok.name === undefined) continue;
199
+ if (!merged.has(tok.name)) merged.set(tok.name, tok);
200
+ }
201
+ }
202
+ return [...merged.values()];
203
+ }
204
+
205
+ // ── orchestrator ─────────────────────────────────────────────────────────────
206
+
207
+ /** Read+parse a JSON file from the raw cache; return null if absent/unreadable. */
208
+ async function readJson(rawDir, name) {
209
+ try {
210
+ const body = await fs.readFile(path.join(rawDir, `${name}.json`), 'utf8');
211
+ return JSON.parse(body);
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Run the digest: read raw/ cache → walk (variant rollup) → 3-path token
219
+ * assembly → render DESIGN.md + write tokens.json + components.json.
220
+ *
221
+ * @param {object} opts
222
+ * @param {string} opts.rawDir raw/ cache dir produced by pull.cjs (31-01) — REQUIRED
223
+ * @param {string} opts.outDir dir to write DESIGN.md/tokens.json/components.json — REQUIRED for writes
224
+ * @param {Function} [opts.stylesResolver] fn(file, styles) → styleTokens[] (Path B; 31-03 provides real impl)
225
+ * @param {boolean} [opts.preferStyles] D-04 escape hatch
226
+ * @param {string} [opts.fetchedAtOverride] deterministic provenance header for tests
227
+ * @param {string} [opts.component] D-08 — name or glob; when set, render a
228
+ * per-component SLICE (~500 tokens) of only
229
+ * the matching component(s). Additive: when
230
+ * absent, the full-digest path is unchanged.
231
+ * @returns {Promise<object>}
232
+ * full: { ok:true, counts, bytes, outDir }
233
+ * sliced: { ok:true, sliced:true, matched:[names], counts:{components,tokens}, bytes, outDir, note? }
234
+ * error: { ok:false, error }
235
+ */
236
+ async function digest({ rawDir, outDir, stylesResolver, preferStyles, fetchedAtOverride, component } = {}) {
237
+ if (!rawDir) {
238
+ return { ok: false, error: 'rawDir is required — run pull.cjs first' };
239
+ }
240
+
241
+ // (1) Required input — graceful guard (mirrors spike). NEVER throws.
242
+ const file = await readJson(rawDir, 'file');
243
+ if (!file) {
244
+ return { ok: false, error: 'raw/file.json not found — run pull.cjs first' };
245
+ }
246
+
247
+ // (2) Optional inputs.
248
+ const variablesRaw = await readJson(rawDir, 'variables');
249
+ const styles = await readJson(rawDir, 'styles');
250
+ const meta = await readJson(rawDir, '_meta');
251
+
252
+ // Distinguish Path A (Variables API body) from Path C (receiver plugin payload)
253
+ // by the receiver marker. Only one of the two is populated from variables.json.
254
+ let apiVariables = null;
255
+ let pluginPayload = null;
256
+ if (variablesRaw && variablesRaw.source === PLUGIN_PAYLOAD_MARKER) {
257
+ pluginPayload = variablesRaw; // Path C
258
+ } else {
259
+ apiVariables = variablesRaw; // Path A (may be null)
260
+ }
261
+
262
+ // (3) Components + widgets — variant rollup is default-on (D-02).
263
+ const { components, widgets } = collectComponents(file.document);
264
+
265
+ // (4) Three token paths.
266
+ const pathATokens = extractTokensFromVariables(apiVariables);
267
+ const styleTokens = stylesResolver ? await stylesResolver(file, styles) : [];
268
+ const pluginVariables = normalizePluginPayload(pluginPayload);
269
+
270
+ // (5) Merge by priority (D-04).
271
+ const tokens = assembleTokens({
272
+ variables: pathATokens,
273
+ pluginVariables,
274
+ styleTokens,
275
+ preferStyles,
276
+ });
277
+
278
+ // (6) Provenance — fetched_at is injectable for deterministic output.
279
+ const fileMeta = {
280
+ file_key: meta?.file_key,
281
+ fetched_at: fetchedAtOverride !== undefined ? fetchedAtOverride : meta?.fetched_at,
282
+ name: file.name,
283
+ };
284
+
285
+ // (6b) D-08 — per-component SLICE. When `component` is provided we short-circuit
286
+ // the full digest and render only the matching component(s) + their relevant
287
+ // tokens. This is additive: the block below is skipped entirely when `component`
288
+ // is undefined, so the full-digest path (step 7) stays byte-identical.
289
+ if (component !== undefined && component !== null && component !== '') {
290
+ const rx = globToRegExp(component);
291
+ const matched = components.filter((c) => rx.test(c.name));
292
+ // Only tokens referenced by the matched components — keeps the slice ~500
293
+ // tokens instead of dumping the whole catalog.
294
+ const sliceTokens = tokensForComponents(matched, tokens);
295
+ const sliceMd = renderDesignMd({
296
+ tokens: sliceTokens,
297
+ components: matched,
298
+ widgets: [], // a per-component slice omits page/widget noise
299
+ fileMeta,
300
+ });
301
+ if (outDir) {
302
+ await fs.mkdir(outDir, { recursive: true });
303
+ // Write the slice to DESIGN.md (the SKILL/e2e read whatever digest writes).
304
+ await fs.writeFile(path.join(outDir, 'DESIGN.md'), sliceMd);
305
+ }
306
+ const result = {
307
+ ok: true,
308
+ sliced: true,
309
+ matched: matched.map((c) => c.name),
310
+ counts: { components: matched.length, tokens: sliceTokens.length },
311
+ bytes: { designMd: Buffer.byteLength(sliceMd, 'utf8') },
312
+ outDir,
313
+ };
314
+ if (matched.length === 0) result.note = `no component matched ${component}`;
315
+ return result;
316
+ }
317
+
318
+ // (7) Render + write artifacts (D-09: digest/ is commit-able).
319
+ const designMd = renderDesignMd({ tokens, components, widgets, fileMeta });
320
+ const tokensJson = JSON.stringify(tokens, null, 2);
321
+ const componentsJson = JSON.stringify(components, null, 2);
322
+
323
+ if (outDir) {
324
+ await fs.mkdir(outDir, { recursive: true });
325
+ await fs.writeFile(path.join(outDir, 'DESIGN.md'), designMd);
326
+ await fs.writeFile(path.join(outDir, 'tokens.json'), tokensJson);
327
+ await fs.writeFile(path.join(outDir, 'components.json'), componentsJson);
328
+ }
329
+
330
+ return {
331
+ ok: true,
332
+ counts: {
333
+ tokens: tokens.length,
334
+ components: components.length,
335
+ widgets: widgets.length,
336
+ },
337
+ bytes: {
338
+ designMd: Buffer.byteLength(designMd, 'utf8'),
339
+ tokensJson: Buffer.byteLength(tokensJson, 'utf8'),
340
+ componentsJson: Buffer.byteLength(componentsJson, 'utf8'),
341
+ },
342
+ outDir,
343
+ };
344
+ }
345
+
346
+ module.exports = {
347
+ digest,
348
+ assembleTokens,
349
+ DEFAULT_TOKEN_PRIORITY,
350
+ // exported for unit reuse / downstream (31-08 --component, 31-03 normalization parity)
351
+ extractTokensFromVariables,
352
+ normalizePluginPayload,
353
+ PLUGIN_PAYLOAD_MARKER,
354
+ // D-08 component filter — exported for unit reuse / downstream slicing tools.
355
+ globToRegExp,
356
+ tokensForComponents,
357
+ };
358
+
359
+ // ── CLI entry (31-08) ──────────────────────────────────────────────────────────
360
+ //
361
+ // Thin argv wrapper invoked by the figma-extract SKILL (31-07) and the e2e test
362
+ // (31-10). The callable API (digest/assembleTokens/…) above is the import surface;
363
+ // this block runs ONLY when the file is executed directly. Flag → option map
364
+ // (contract from 31-02 SUMMARY, extended with --component for D-08):
365
+ //
366
+ // --raw <dir> rawDir (required) raw cache dir written by pull.cjs
367
+ // --out <dir> outDir (required) digest artifact output dir
368
+ // --prefer-styles preferStyles:true D-04 escape — styles-first priority
369
+ // --component <name> component D-08 per-component slice (name or glob)
370
+ //
371
+ // D-10: this block NEVER reads, logs, or persists FIGMA_TOKEN — digest is offline
372
+ // and token-free by construction; the CLI only echoes counts/paths.
373
+
374
+ /** Minimal flag parser for the four supported options (no external dep). */
375
+ function parseArgv(argv) {
376
+ const opts = {};
377
+ for (let i = 0; i < argv.length; i++) {
378
+ const a = argv[i];
379
+ if (a === '--raw') opts.rawDir = argv[++i];
380
+ else if (a === '--out') opts.outDir = argv[++i];
381
+ else if (a === '--prefer-styles') opts.preferStyles = true;
382
+ else if (a === '--component') opts.component = argv[++i];
383
+ else if (a === '--help' || a === '-h') opts.help = true;
384
+ }
385
+ return opts;
386
+ }
387
+
388
+ const CLI_USAGE =
389
+ 'Usage: node digest.cjs --raw <dir> --out <dir> [--prefer-styles] [--component <name|glob>]';
390
+
391
+ if (require.main === module) {
392
+ (async () => {
393
+ const opts = parseArgv(process.argv.slice(2));
394
+ if (opts.help) {
395
+ process.stdout.write(`${CLI_USAGE}\n`);
396
+ return;
397
+ }
398
+ if (!opts.rawDir || !opts.outDir) {
399
+ process.stderr.write(`${CLI_USAGE}\n--raw and --out are required.\n`);
400
+ process.exitCode = 2;
401
+ return;
402
+ }
403
+ const res = await digest({
404
+ rawDir: opts.rawDir,
405
+ outDir: opts.outDir,
406
+ preferStyles: opts.preferStyles,
407
+ component: opts.component,
408
+ });
409
+ if (!res.ok) {
410
+ process.stderr.write(`digest failed: ${res.error}\n`);
411
+ process.exitCode = 1;
412
+ return;
413
+ }
414
+ if (res.sliced) {
415
+ const summary =
416
+ res.matched.length > 0
417
+ ? `sliced ${res.counts.components} component(s): ${res.matched.join(', ')} (${res.counts.tokens} tokens) → ${res.outDir}`
418
+ : `${res.note} → ${res.outDir} (empty slice)`;
419
+ process.stdout.write(`${summary}\n`);
420
+ } else {
421
+ process.stdout.write(
422
+ `digest ok: ${res.counts.components} components, ${res.counts.tokens} tokens, ${res.counts.widgets} widgets → ${res.outDir}\n`
423
+ );
424
+ }
425
+ })().catch((err) => {
426
+ // Never leak a token; surface only the message.
427
+ process.stderr.write(`digest error: ${err && err.message ? err.message : err}\n`);
428
+ process.exitCode = 1;
429
+ });
430
+ }