@hegemonart/get-design-done 1.33.5 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +6 -3
- package/.claude-plugin/plugin.json +5 -2
- package/CHANGELOG.md +48 -0
- package/README.md +14 -0
- package/SKILL.md +1 -0
- package/agents/compose-executor.md +142 -0
- package/agents/design-authority-watcher.md +4 -0
- package/agents/design-context-builder.md +35 -1
- package/agents/design-verifier.md +14 -18
- package/agents/flutter-executor.md +147 -0
- package/agents/swift-executor.md +226 -0
- package/connections/android-emulator.md +107 -0
- package/connections/connections.md +6 -0
- package/connections/openrouter.md +86 -0
- package/connections/xcode-simulator.md +108 -0
- package/hooks/budget-enforcer.ts +103 -0
- package/package.json +3 -2
- package/reference/gdd-threat-model.md +63 -0
- package/reference/native-platforms.md +273 -0
- package/reference/openrouter-tier-mapping.md +98 -0
- package/reference/prices.openrouter.md +26 -0
- package/reference/registry.json +21 -0
- package/scripts/lib/authority-watcher/index.cjs +147 -0
- package/scripts/lib/budget-enforcer.cjs +16 -0
- package/scripts/lib/design-tokens/_native-shared.cjs +206 -0
- package/scripts/lib/design-tokens/compose.cjs +150 -0
- package/scripts/lib/design-tokens/flutter.cjs +128 -0
- package/scripts/lib/design-tokens/index.cjs +13 -0
- package/scripts/lib/design-tokens/swift.cjs +122 -0
- package/scripts/lib/openrouter/catalog-fetcher.cjs +326 -0
- package/scripts/lib/tier-resolver-openrouter.cjs +343 -0
- package/sdk/event-stream/types.ts +24 -2
- package/skills/openrouter-status/SKILL.md +86 -0
package/hooks/budget-enforcer.ts
CHANGED
|
@@ -93,6 +93,22 @@ interface BudgetEnforcerBackend {
|
|
|
93
93
|
reason: string | null;
|
|
94
94
|
};
|
|
95
95
|
modelFromResolved(resolved: unknown, agent: string): string | null;
|
|
96
|
+
// Plan 33.6-03 (SC#6): the canonical cost-row payload builder (the
|
|
97
|
+
// types.ts:237-designated emit site). Threads the optional `provider` tag
|
|
98
|
+
// ("openrouter" when the OpenRouter adapter resolved the model), omitting it
|
|
99
|
+
// when absent (back-compat).
|
|
100
|
+
buildCostEventPayload(args: {
|
|
101
|
+
runtime: string;
|
|
102
|
+
agent: string;
|
|
103
|
+
model_id: string | null;
|
|
104
|
+
tier: string | null;
|
|
105
|
+
tokens_in: number;
|
|
106
|
+
tokens_out: number;
|
|
107
|
+
cost_usd: number | null;
|
|
108
|
+
runtime_role?: 'host' | 'peer';
|
|
109
|
+
peer_id?: string | null;
|
|
110
|
+
provider?: string;
|
|
111
|
+
}): Record<string, unknown>;
|
|
96
112
|
}
|
|
97
113
|
const budgetBackend = nodeRequire('../scripts/lib/budget-enforcer.cjs') as BudgetEnforcerBackend;
|
|
98
114
|
// Plan 26-05: runtime detection for the cost-event runtime tag. Returns
|
|
@@ -175,6 +191,51 @@ const tierResolver = nodeRequire(
|
|
|
175
191
|
'../scripts/lib/tier-resolver.cjs',
|
|
176
192
|
) as TierResolverModule;
|
|
177
193
|
|
|
194
|
+
// Plan 33.6-03 (SC#6, D-08, D-12): OpenRouter tier-resolver adapter. When the
|
|
195
|
+
// user opts in (`.design/config.json#openrouter_enabled: true` OR
|
|
196
|
+
// `OPENROUTER_API_KEY` present), the hook consults this adapter FIRST for a
|
|
197
|
+
// resolved model; a non-null result routes to OpenRouter and tags the cost row
|
|
198
|
+
// `provider: "openrouter"`, a null result falls back to the native resolution
|
|
199
|
+
// path (unchanged default behavior). `resolve(tier, opts)` never throws.
|
|
200
|
+
interface TierResolverOpenRouterModule {
|
|
201
|
+
resolve(
|
|
202
|
+
tier: string,
|
|
203
|
+
opts?: { catalog?: unknown; models?: unknown; overrides?: unknown; cachePath?: string; configPath?: string; cwd?: string },
|
|
204
|
+
): string | null;
|
|
205
|
+
}
|
|
206
|
+
const tierResolverOpenRouter = nodeRequire(
|
|
207
|
+
'../scripts/lib/tier-resolver-openrouter.cjs',
|
|
208
|
+
) as TierResolverOpenRouterModule;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Plan 33.6-03 (SC#6 opt-in). OpenRouter is consulted ONLY when the user opts
|
|
212
|
+
* in — either `.design/config.json#openrouter_enabled === true` OR
|
|
213
|
+
* `OPENROUTER_API_KEY` is present in the environment. Best-effort + never
|
|
214
|
+
* throws: a missing/corrupt config degrades to "env var only". This keeps the
|
|
215
|
+
* default (no OpenRouter) behavior byte-identical for every existing user
|
|
216
|
+
* (D-08, D-12).
|
|
217
|
+
*
|
|
218
|
+
* @param cwd base dir for `.design/config.json` (default process.cwd())
|
|
219
|
+
*/
|
|
220
|
+
function isOpenRouterEnabled(cwd?: string): boolean {
|
|
221
|
+
if (
|
|
222
|
+
typeof process.env.OPENROUTER_API_KEY === 'string' &&
|
|
223
|
+
process.env.OPENROUTER_API_KEY.length > 0
|
|
224
|
+
) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const configPath = join(cwd ?? process.cwd(), '.design', 'config.json');
|
|
229
|
+
if (!existsSync(configPath)) return false;
|
|
230
|
+
const parsed = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
|
231
|
+
openrouter_enabled?: unknown;
|
|
232
|
+
};
|
|
233
|
+
return Boolean(parsed && parsed.openrouter_enabled === true);
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
178
239
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
179
240
|
|
|
180
241
|
/**
|
|
@@ -661,6 +722,11 @@ function emitCostRecorded(
|
|
|
661
722
|
tokens_in: number;
|
|
662
723
|
tokens_out: number;
|
|
663
724
|
cost_usd: number | null;
|
|
725
|
+
// Plan 33.6-03 SC#6 — optional resolution provider ("openrouter" when the
|
|
726
|
+
// OpenRouter adapter resolved the model). Additive/back-compat: omitted
|
|
727
|
+
// from the on-disk row when absent, so the legacy cost_recorded shape is
|
|
728
|
+
// preserved for every native-resolution + pre-33.6 spawn.
|
|
729
|
+
provider?: string;
|
|
664
730
|
},
|
|
665
731
|
cycle?: string,
|
|
666
732
|
): void {
|
|
@@ -677,6 +743,10 @@ function emitCostRecorded(
|
|
|
677
743
|
tokens_in: payload.tokens_in,
|
|
678
744
|
tokens_out: payload.tokens_out,
|
|
679
745
|
cost_usd: payload.cost_usd,
|
|
746
|
+
// Omit-when-absent (mirrors the .cjs buildCostEventPayload discipline).
|
|
747
|
+
...(typeof payload.provider === 'string' && payload.provider.length > 0
|
|
748
|
+
? { provider: payload.provider }
|
|
749
|
+
: {}),
|
|
680
750
|
},
|
|
681
751
|
};
|
|
682
752
|
try {
|
|
@@ -1149,6 +1219,36 @@ export async function main(): Promise<void> {
|
|
|
1149
1219
|
}
|
|
1150
1220
|
}
|
|
1151
1221
|
|
|
1222
|
+
// ── Plan 33.6-03 — OpenRouter resolution consultation (SC#6, D-08, D-12) ────
|
|
1223
|
+
//
|
|
1224
|
+
// When the user opts in (`.design/config.json#openrouter_enabled: true` OR
|
|
1225
|
+
// `OPENROUTER_API_KEY` present), consult the OpenRouter adapter for the
|
|
1226
|
+
// effective tier FIRST. A non-null result routes this spawn to OpenRouter:
|
|
1227
|
+
// we override the model id and tag the cost row `provider: "openrouter"`. A
|
|
1228
|
+
// null result (no key / catalog missing-or-stale / no match) falls through to
|
|
1229
|
+
// the native resolution that's already in `effectiveModelId` — so the default
|
|
1230
|
+
// (OpenRouter disabled) path is byte-identical to pre-33.6 behavior (D-08).
|
|
1231
|
+
// The adapter never throws; this whole branch is also wrapped defensively.
|
|
1232
|
+
let costProvider: string | undefined;
|
|
1233
|
+
if (isOpenRouterEnabled()) {
|
|
1234
|
+
try {
|
|
1235
|
+
const openrouterModel = tierResolverOpenRouter.resolve(effectiveTier);
|
|
1236
|
+
if (typeof openrouterModel === 'string' && openrouterModel.length > 0) {
|
|
1237
|
+
effectiveModelId = openrouterModel;
|
|
1238
|
+
costProvider = 'openrouter';
|
|
1239
|
+
// Reflect the OpenRouter pick into resolved_models so downstream
|
|
1240
|
+
// consumers see the actual model (mirrors the bandit override above).
|
|
1241
|
+
if (routerDecision !== undefined) {
|
|
1242
|
+
const rm = routerDecision.resolved_models ?? {};
|
|
1243
|
+
rm[agent] = openrouterModel;
|
|
1244
|
+
routerDecision.resolved_models = rm;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} catch {
|
|
1248
|
+
// Fail open — never let OpenRouter resolution block a spawn (D-08).
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1152
1252
|
// Compute runtime-aware cost via the shared backend. Failures return
|
|
1153
1253
|
// null cost; we emit the event regardless so the cost-aggregator sees
|
|
1154
1254
|
// the lookup attempt (Phase 22 events.jsonl tagging).
|
|
@@ -1169,6 +1269,9 @@ export async function main(): Promise<void> {
|
|
|
1169
1269
|
tokens_in: Number(toolInput._tokens_in_est ?? 0),
|
|
1170
1270
|
tokens_out: Number(toolInput._tokens_out_est ?? 0),
|
|
1171
1271
|
cost_usd: costLookup.cost_usd,
|
|
1272
|
+
// Plan 33.6-03 SC#6 — tag the row when OpenRouter resolved the model.
|
|
1273
|
+
// Omitted (undefined) on the native path → buildCostEventPayload drops it.
|
|
1274
|
+
...(costProvider !== undefined ? { provider: costProvider } : {}),
|
|
1172
1275
|
},
|
|
1173
1276
|
cycle,
|
|
1174
1277
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.1",
|
|
4
4
|
"description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
|
|
5
5
|
"author": "Hegemon",
|
|
6
6
|
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"ws": "^8.20.0"
|
|
114
114
|
},
|
|
115
115
|
"overrides": {
|
|
116
|
-
"fast-json-patch": "^3.1.1"
|
|
116
|
+
"fast-json-patch": "^3.1.1",
|
|
117
|
+
"qs": ">=6.15.2"
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -43,6 +43,7 @@ controls; the table names what crosses the line.
|
|
|
43
43
|
| gdd-state MCP `←` environment / config / tool input | Whoever sets `GDD_STATE_PATH` or supplies a tool-call payload, or authors `.design/config.json` | The `GDD_STATE_PATH` env value + the JSON tool-input payloads |
|
|
44
44
|
| Peer-CLI broker `↔` spawned child | A spawned peer CLI (Codex / Gemini / Cursor / Copilot / Qwen) and its stdout stream | The child's stdout JSON frames + the parent env handed to the child |
|
|
45
45
|
| Outbound call sites `↔` external host | The remote HTTP host / GitHub / Figma the call reaches | The outbound request payload + whatever the remote returns |
|
|
46
|
+
| OpenRouter catalog fetch `→` openrouter.ai | The OpenRouter `/models` API host (and any MITM on the path) | The `Authorization: Bearer <OPENROUTER_API_KEY>` request header + the untrusted `/models` JSON the host returns |
|
|
46
47
|
|
|
47
48
|
The event payloads that traverse the bus (and therefore the WS transport and
|
|
48
49
|
any persisted JSONL) are scrubbed at serialize time — see Component 4's
|
|
@@ -311,6 +312,68 @@ model).
|
|
|
311
312
|
|
|
312
313
|
---
|
|
313
314
|
|
|
315
|
+
## Component 6 — OpenRouter catalog fetcher (scripts/lib/openrouter/catalog-fetcher.cjs)
|
|
316
|
+
|
|
317
|
+
> Added in Phase 33.6 (OR-01, CONTEXT D-06). This is the runtime's **first
|
|
318
|
+
> plugin-side outbound REST client** — the issue-reporter (Component 5) reaches
|
|
319
|
+
> the network only through the user's `gh` CLI, and the WS transport (Component
|
|
320
|
+
> 4) is a *server*, not an outbound client. The catalog fetcher is the first
|
|
321
|
+
> first-party code to open an outbound HTTP request to a third-party host
|
|
322
|
+
> directly, which is why it lands only after the 33.5 audited baseline and the
|
|
323
|
+
> `scan:outbound` gate (33.5-04) are in place.
|
|
324
|
+
|
|
325
|
+
`scripts/lib/openrouter/catalog-fetcher.cjs` performs a read-only GET to the
|
|
326
|
+
OpenRouter model catalog (`https://openrouter.ai/api/v1/models`) through an
|
|
327
|
+
**injectable `fetchImpl`** (default global `fetch`), maps the response into the
|
|
328
|
+
`.design/cache/openrouter-models.json` cache shape, and writes it atomically.
|
|
329
|
+
The live fetch is opt-in — gated on `OPENROUTER_API_KEY` being present at
|
|
330
|
+
runtime; absent it, the fetcher returns cached-if-any-else-null and tier
|
|
331
|
+
resolution falls back to the native provider.
|
|
332
|
+
|
|
333
|
+
- **Assets:** The **`OPENROUTER_API_KEY`** (a billable provider credential) and
|
|
334
|
+
the integrity of the cached catalog the tier-resolver later trusts.
|
|
335
|
+
- **Entry points:** The **`/models` JSON the OpenRouter host returns** (untrusted
|
|
336
|
+
remote input the fetcher must parse), and the `OPENROUTER_BASE_URL` env (an
|
|
337
|
+
operator-supplied endpoint override).
|
|
338
|
+
- **STRIDE threats:**
|
|
339
|
+
- **Spoofing:** A spoofed `/models` endpoint (DNS/MITM, or a hostile
|
|
340
|
+
`OPENROUTER_BASE_URL`) could feed a forged catalog.
|
|
341
|
+
- **Tampering:** A malformed/oversized `/models` body could try to corrupt the
|
|
342
|
+
cache the resolver reads, or smuggle unexpected fields downstream.
|
|
343
|
+
- **Information disclosure:** **The headline risk** — leaking the
|
|
344
|
+
`OPENROUTER_API_KEY` by persisting it to the cache, logging it, or sending it
|
|
345
|
+
to an unintended host.
|
|
346
|
+
- **Denial of service:** A hung or slow host could stall the fetch; a giant
|
|
347
|
+
catalog could pressure memory.
|
|
348
|
+
- **Elevation of privilege:** A forged catalog could steer tier resolution to
|
|
349
|
+
an attacker-chosen model id.
|
|
350
|
+
- **Current mitigations:** The key is read from **`OPENROUTER_API_KEY` env only**,
|
|
351
|
+
sent **solely** as an `Authorization: Bearer` request header, and is **never
|
|
352
|
+
persisted to the cache nor written to any log seam** — the cache shape carries
|
|
353
|
+
only `id`/`name`/`context_length`/`pricing`, and the mapper keeps **only** those
|
|
354
|
+
fields, dropping everything else (the `/models` body is **mapped, never
|
|
355
|
+
eval'd**). The cache write is **atomic** (per-pid temp + rename) into the
|
|
356
|
+
**gitignored** `.design/cache/`, so a partial/corrupt fetch can't leave a
|
|
357
|
+
half-written catalog and the cache never enters git history. The fetcher
|
|
358
|
+
**never throws** (D-08): no key / fetch failure / parse failure all degrade to
|
|
359
|
+
cached-if-any-else-null, bounding the DoS surface, and retries are **bounded**
|
|
360
|
+
(max 3 attempts) on a jittered-backoff curve with `rate-guard` awareness.
|
|
361
|
+
Egress is **allowlisted** via `scripts/lib/openrouter/**` in
|
|
362
|
+
`scripts/security/outbound-allowlist.json` — the only sanctioned outbound site
|
|
363
|
+
in that subtree — so the 33.5 `scan:outbound` gate proves no un-approved egress
|
|
364
|
+
crept in. The **injectable `fetchImpl`** keeps the default `npm test` suite
|
|
365
|
+
hermetic (D-07) — no live network — and there is **no new HTTP dependency**
|
|
366
|
+
(global `fetch` + `sdk/primitives` only — D-10), avoiding both a new supply-chain
|
|
367
|
+
surface and the gate's `axios`/`node-fetch`/`undici` package patterns.
|
|
368
|
+
- **Residual risks:** None this phase leaves open. The catalog is advisory data
|
|
369
|
+
consumed by the tier-resolver heuristic (33.6-02), which already clamps to
|
|
370
|
+
GDD's `opus`/`sonnet`/`haiku` vocabulary and supports user overrides, so a
|
|
371
|
+
forged catalog cannot escalate beyond model-id selection within that bounded
|
|
372
|
+
set; a future hardening could pin the OpenRouter TLS cert or sign the cache,
|
|
373
|
+
but neither is required for the current trust model.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
314
377
|
## Residual-risk → closing-plan map
|
|
315
378
|
|
|
316
379
|
Every residual risk identified above is routed to the Phase 33.5 plan (or
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Native Platforms — Token-Bridge Spec
|
|
2
|
+
|
|
3
|
+
This reference is the **token→native-code bridge**: it specifies how the canonical
|
|
4
|
+
CSS-token form produced by the Phase-23 token engine
|
|
5
|
+
(`scripts/lib/design-tokens/`) maps onto the three native theme systems —
|
|
6
|
+
SwiftUI, Jetpack Compose, and Flutter — and pins down the **precision contract**
|
|
7
|
+
that defines what "token identity preserved" means for the deterministic
|
|
8
|
+
round-trip (`reference/native-platforms.md` is the authority that
|
|
9
|
+
`test/suite/native-token-bridge.test.cjs` asserts against).
|
|
10
|
+
|
|
11
|
+
It is the sibling of [`reference/platforms.md`](./platforms.md). Those two files
|
|
12
|
+
have distinct jobs and must not be confused:
|
|
13
|
+
|
|
14
|
+
| File | Job |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `reference/platforms.md` (Phase 19) | Interaction **conventions** — navigation, safe areas, gestures, native typography, haptics. *Behavioral* knowledge the executors reference when laying out a screen. |
|
|
17
|
+
| `reference/native-platforms.md` (Phase 34.1, this file) | The token→theme **bridge** — how a design token (`#3B82F6`, `16px`, `Inter`) becomes a SwiftUI `Color` / Compose `Color(0x…)` / Flutter `Color(0x…)`, plus the precision contract for the round-trip. *Structural* knowledge the emitters implement. |
|
|
18
|
+
|
|
19
|
+
Per **D-02** the bridge **extends** the Phase-23 engine with three new emitters
|
|
20
|
+
(`scripts/lib/design-tokens/{swift,compose,flutter}.cjs`) rather than forking a
|
|
21
|
+
separate native IR. There is one canonical token form (below) and one set of
|
|
22
|
+
readers; the native emitters are additional *sinks* on the same facade. Per
|
|
23
|
+
**D-10** the round-trip operates at the **token level** — deterministic emit +
|
|
24
|
+
re-extract with documented precision — never full-view parsing and never a live
|
|
25
|
+
simulator, so the default `npm test` stays green on any machine.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Purpose
|
|
30
|
+
|
|
31
|
+
Phase 19 shipped platform *references* but zero generators; Phase 23 shipped a
|
|
32
|
+
multi-source token reader (`css-vars` / `js-const` / `tailwind` / `figma`) that
|
|
33
|
+
all normalise to a single flat `{ tokens: Record<string, string> }` map. Phase
|
|
34
|
+
34.1 crosses from platform-knowledge into platform-execution: instead of each
|
|
35
|
+
native executor re-deriving "how does `#3B82F6` become a SwiftUI `Color`", the
|
|
36
|
+
mapping lives once here (the spec) and once in the emitters (the
|
|
37
|
+
implementation), and the executors consume it. This amortizes the Phase-23 token
|
|
38
|
+
investment across SwiftUI / Compose / Flutter.
|
|
39
|
+
|
|
40
|
+
The canonical CSS-token form is the **single input** to all three native
|
|
41
|
+
emitters. This spec maps that one input to three native theme systems and states
|
|
42
|
+
the precision each mapping preserves.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Canonical input shape
|
|
47
|
+
|
|
48
|
+
The emitters consume the exact map shape the Phase-23 readers return: a **flat
|
|
49
|
+
`{ tokens: Record<string, string> }` object** whose keys are the design-token
|
|
50
|
+
names with the leading `--` stripped (exactly as `css-vars.cjs` returns) and
|
|
51
|
+
whose values are the raw token values as strings.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
{ tokens: { "color-primary": "#3B82F6", "space-4": "16px", "font-family-body": "Inter, system-ui" } }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each emitter accepts either the full Phase-23 `TokenSet`
|
|
58
|
+
(`{ tokens, source?, format? }`) **or** a bare `{ tokens }` object — it reads
|
|
59
|
+
`tokenSet.tokens` and throws a `TypeError` only when no `.tokens` object is
|
|
60
|
+
present.
|
|
61
|
+
|
|
62
|
+
### Prefix → category inference
|
|
63
|
+
|
|
64
|
+
Token **category** is inferred from the key prefix. The emitters use this table
|
|
65
|
+
to decide whether a value is a color, a dimension, or a string:
|
|
66
|
+
|
|
67
|
+
| Key prefix | Category | Native treatment |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `color-` | color | hex → native channel form (§3–§5, §6 COLOR) |
|
|
70
|
+
| `space-`, `spacing-` | dimension | px → pt / dp / logical px (§6 DIMENSION) |
|
|
71
|
+
| `radius-` | dimension | px → pt / dp / logical px |
|
|
72
|
+
| `size-` | dimension | px → pt / dp / logical px |
|
|
73
|
+
| `font-`, `text-` | typography | string pass-through (family / weight / named size) |
|
|
74
|
+
| `shadow-` | other | string pass-through (composite values are non-mappable) |
|
|
75
|
+
| *(anything else)* | other | value-sniffed: a `#…` value is treated as color, an `Npx` value as dimension, otherwise string pass-through |
|
|
76
|
+
|
|
77
|
+
A value is **always** re-sniffed regardless of prefix, so a `#…` value under a
|
|
78
|
+
non-color prefix is still emitted as a color and a `Npx` value under a non-space
|
|
79
|
+
prefix is still emitted as a dimension. The prefix is the hint; the value is the
|
|
80
|
+
authority. This keeps the bridge robust against arbitrary token-naming schemes.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. SwiftUI mapping
|
|
85
|
+
|
|
86
|
+
Target: a Swift source string exposing an `enum` of static theme constants
|
|
87
|
+
(`enum GDDTheme { … }`) — colors as `Color`, dimensions as `CGFloat` points,
|
|
88
|
+
typography families as `String` (and an optional `Font` helper).
|
|
89
|
+
|
|
90
|
+
| Token | SwiftUI form |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| color `#RRGGBB` | `Color(red: R/255.0, green: G/255.0, blue: B/255.0, opacity: A/255.0)` from the 8-bit channels |
|
|
93
|
+
| dimension `Npx` | `CGFloat` point literal — integer `N` (pt) |
|
|
94
|
+
| font-family | `String` literal (`"Inter, system-ui"`) |
|
|
95
|
+
|
|
96
|
+
Illustrative (2-line) snippet:
|
|
97
|
+
|
|
98
|
+
```swift
|
|
99
|
+
static let colorPrimary = Color(red: 59.0/255.0, green: 130.0/255.0, blue: 246.0/255.0, opacity: 255.0/255.0)
|
|
100
|
+
static let space4: CGFloat = 16
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
SwiftUI uses normalized `0.0…1.0` channel fractions; to keep the round-trip
|
|
104
|
+
**exact** the emitter writes each channel as the 8-bit numerator over `255.0`
|
|
105
|
+
(e.g. `59.0/255.0`) rather than a pre-divided decimal — the re-extractor reads
|
|
106
|
+
the numerator back as the integer channel, avoiding float drift. The `Color` /
|
|
107
|
+
`Font` / `ViewModifier` consumption pattern (applying the constants to views) is
|
|
108
|
+
the executor's job; this emitter produces the *constants*.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 4. Jetpack Compose mapping
|
|
113
|
+
|
|
114
|
+
Target: a Kotlin source string with `Color` vals, a `Shapes` block (from
|
|
115
|
+
`radius-` tokens), a `Typography` block (from `font-`/`text-` tokens), and a
|
|
116
|
+
`MaterialTheme` wiring (`object GDDTheme { val Colors… ; val Shapes… ; val Typography… }`).
|
|
117
|
+
|
|
118
|
+
| Token | Compose form |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| color `#RRGGBB` | `Color(0xAARRGGBB)` long literal (alpha-first, 8 hex digits) |
|
|
121
|
+
| dimension `Npx` | `N.dp` (integer dp) |
|
|
122
|
+
| radius `Npx` | `RoundedCornerShape(N.dp)` inside `Shapes` |
|
|
123
|
+
| typography family | `String` (fed into a `TextStyle.fontFamily` slot / `Typography`) |
|
|
124
|
+
|
|
125
|
+
Illustrative (2-line) snippet:
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
val colorPrimary = Color(0xFF3B82F6)
|
|
129
|
+
val space4 = 16.dp
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Compose packs ARGB into a single `0xAARRGGBB` long; alpha is the high byte. The
|
|
133
|
+
re-extractor reads the 8 hex digits straight back to the channels.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 5. Flutter mapping
|
|
138
|
+
|
|
139
|
+
Target: a Dart source string building a `ThemeData` whose `colorScheme`
|
|
140
|
+
(`ColorScheme`) carries the color tokens and whose `textTheme` (`TextTheme`)
|
|
141
|
+
carries the typography tokens, plus a constants class
|
|
142
|
+
(`class GDDTheme { … }`).
|
|
143
|
+
|
|
144
|
+
| Token | Flutter form |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| color `#RRGGBB` | `Color(0xAARRGGBB)` (alpha-first, 8 hex digits) |
|
|
147
|
+
| dimension `Npx` | logical-px **double** — `N.0` |
|
|
148
|
+
| typography family | `String` (`fontFamily: 'Inter'`) |
|
|
149
|
+
|
|
150
|
+
Illustrative (2-line) snippet:
|
|
151
|
+
|
|
152
|
+
```dart
|
|
153
|
+
static const colorPrimary = Color(0xFF3B82F6);
|
|
154
|
+
static const space4 = 16.0;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Flutter measures in logical pixels and keeps the value as a `double` (`16.0`),
|
|
158
|
+
so — unlike pt/dp — Flutter dimensions are **not** rounded to integers; the
|
|
159
|
+
fractional part survives.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 6. PRECISION CONTRACT
|
|
164
|
+
|
|
165
|
+
This section is the crux. It defines, per value category, exactly what
|
|
166
|
+
information the emit → re-extract round-trip preserves. The test asserts token
|
|
167
|
+
identity **within this precision** — not bit-exact floats, not lossy
|
|
168
|
+
approximation. An emitter is correct **iff** `reextract(emit({tokens}))`
|
|
169
|
+
reproduces every token in the identity set under these rules.
|
|
170
|
+
|
|
171
|
+
### COLOR — 8-bit-per-channel, EXACT
|
|
172
|
+
|
|
173
|
+
- Accepted input forms: `#RGB`, `#RRGGBB`, `#RGBA`, `#RRGGBBAA` (case-insensitive).
|
|
174
|
+
- `#RGB` / `#RGBA` shorthand **expands** to `#RRGGBB` / `#RRGGBBAA` by
|
|
175
|
+
duplicating each nibble (`#3af` → `#33aaff`). This expansion is part of the
|
|
176
|
+
contract: the re-extractor recovers the **expanded** `#RRGGBB(AA)` form, so
|
|
177
|
+
`#3af` round-trips to `#33aaff` (canonically equal, the documented identity).
|
|
178
|
+
- Each channel is an 8-bit integer (0–255) and is preserved **exactly** — no
|
|
179
|
+
channel may be off by one. SwiftUI stores channels as `N.0/255.0` numerators;
|
|
180
|
+
Compose/Flutter store them in a `0xAARRGGBB` literal. Both forms recover the
|
|
181
|
+
identical 8-bit channels.
|
|
182
|
+
- **Alpha:** when the input has no alpha (`#RGB`/`#RRGGBB`) the emitted color is
|
|
183
|
+
**opaque** — alpha byte `0xFF` (Compose/Flutter) / `opacity: 255.0/255.0`
|
|
184
|
+
(SwiftUI). The re-extractor emits an alpha channel **only when the original
|
|
185
|
+
had one**: a 6-digit input round-trips to a 6-digit `#RRGGBB` (the implied
|
|
186
|
+
opaque alpha is dropped on the way back); an 8-digit input round-trips to the
|
|
187
|
+
8-digit `#RRGGBBAA`. This keeps `#3B82F6 → #3B82F6` an exact identity.
|
|
188
|
+
|
|
189
|
+
### DIMENSION — integer pt/dp, logical-px double
|
|
190
|
+
|
|
191
|
+
- Accepted input: `Npx` or a bare unit-less number (`16px`, `16`). The unit is
|
|
192
|
+
normalised to `px` on the canonical side.
|
|
193
|
+
- **iOS (pt) / Android (dp):** rounded to the nearest integer, **round-half-up**
|
|
194
|
+
(`15.5px` → `16`). Because rounding is lossy for non-integers, the round-trip
|
|
195
|
+
**identity set** is restricted to integer-px dimensions (e.g. `16px`), which
|
|
196
|
+
recover exactly: `16px → 16 (pt/dp) → 16px`.
|
|
197
|
+
- **Flutter (logical px):** kept as a `double` (`16px` → `16.0`), so Flutter
|
|
198
|
+
does **not** round and a fractional dimension survives. The re-extractor
|
|
199
|
+
recovers `Npx` by stripping the trailing `.0` for whole numbers.
|
|
200
|
+
- The re-extractor always recovers the canonical `Npx` string form, so the
|
|
201
|
+
emit→re-extract identity for an integer dimension is `"16px" === "16px"`.
|
|
202
|
+
|
|
203
|
+
### `rem` / `em` — passed through verbatim (non-mappable)
|
|
204
|
+
|
|
205
|
+
`rem`/`em` values depend on a root/element font-size that the token map does not
|
|
206
|
+
carry, so they are **not** converted. They are treated as **non-mappable**
|
|
207
|
+
(below): emitted verbatim into a raw slot and **excluded** from the round-trip
|
|
208
|
+
identity set. (A future plan may add an explicit base-size option; until then,
|
|
209
|
+
verbatim pass-through is the stated, deterministic behavior.)
|
|
210
|
+
|
|
211
|
+
### TYPOGRAPHY / NAMED VALUES — string pass-through
|
|
212
|
+
|
|
213
|
+
`font-family`, `font-weight`, named sizes, and any other string token are
|
|
214
|
+
emitted **verbatim** as a string literal and recovered **string-equal**
|
|
215
|
+
(`"Inter, system-ui" → "Inter, system-ui"`). No normalisation, no quoting
|
|
216
|
+
changes that alter the recovered string.
|
|
217
|
+
|
|
218
|
+
### NON-MAPPABLE — verbatim, EXCLUDED from the identity set
|
|
219
|
+
|
|
220
|
+
Values the emitter cannot represent as a native primitive — CSS `var(--x)`
|
|
221
|
+
references, `calc(…)` expressions, gradients (`linear-gradient(…)`), and `rem`/
|
|
222
|
+
`em` dimensions — are **passed through verbatim** into a raw-string slot
|
|
223
|
+
(a trailing comment such as `// non-mappable: <name> = <value>` or the language
|
|
224
|
+
equivalent) so no information is lost, and are **explicitly excluded** from the
|
|
225
|
+
round-trip identity assertion. The contract documents them as
|
|
226
|
+
"verbatim / not round-tripped": the test asserts the verbatim value appears in
|
|
227
|
+
the emitted source, and does **not** assert it survives re-extraction as a typed
|
|
228
|
+
token.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 7. The round-trip (what the test locks)
|
|
233
|
+
|
|
234
|
+
For each emitter the bridge guarantees:
|
|
235
|
+
|
|
236
|
+
1. **Determinism.** `emit(x) === emit(x)` byte-for-byte. Keys are iterated in a
|
|
237
|
+
stable sorted order; no `Date`, no `process.env`, no filesystem, no network in
|
|
238
|
+
the emit path (D-10).
|
|
239
|
+
2. **Identity within precision.** For the identity set (color + integer
|
|
240
|
+
dimension + typography), `reextract(emit({tokens}))` deep-equals `{tokens}`
|
|
241
|
+
under the precision rules above (8-bit color channels exact with `#RGB`
|
|
242
|
+
expansion; integer pt/dp; logical-px double; family/weight string-equal).
|
|
243
|
+
3. **Verbatim exclusion.** Non-mappable values appear verbatim in the source and
|
|
244
|
+
are not part of the identity assertion.
|
|
245
|
+
|
|
246
|
+
Each emitter module exports a symmetric re-extractor
|
|
247
|
+
(`reextractSwift` / `reextractCompose` / `reextractFlutter`) that parses the
|
|
248
|
+
emitted native source back into a `{ tokens }` map, so the round-trip is
|
|
249
|
+
deterministic and bijective on the identity set and reusable by the Phase-34.1
|
|
250
|
+
regression baseline.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 8. Registration
|
|
255
|
+
|
|
256
|
+
This reference is registered in
|
|
257
|
+
[`reference/registry.json`](./registry.json) as the `native-platforms` entry
|
|
258
|
+
(type `heuristic`, phase `34.1`) so the registry round-trip test
|
|
259
|
+
(`test/suite/reference-registry.test.cjs`) stays green — every `reference/*.md`
|
|
260
|
+
must be registered and resolve (D-05, the 33.5-01 lesson).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 9. Cross-references
|
|
265
|
+
|
|
266
|
+
- [`reference/platforms.md`](./platforms.md) — the interaction-conventions
|
|
267
|
+
sibling (navigation, safe areas, gestures, native typography). Executors read
|
|
268
|
+
**both**: this file for the token→theme bridge, that file for layout/behavior.
|
|
269
|
+
- `scripts/lib/design-tokens/` — the Phase-23 token engine this bridge extends
|
|
270
|
+
(`index.cjs` facade + `css-vars` / `js-const` / `tailwind` / `figma` readers +
|
|
271
|
+
the new `swift` / `compose` / `flutter` emitters).
|
|
272
|
+
- `test/fixtures/mapper-outputs/tokens.json` — the canonical token fixture the
|
|
273
|
+
round-trip test derives its map from.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# OpenRouter Tier-Mapping Heuristic
|
|
2
|
+
|
|
3
|
+
How `scripts/lib/tier-resolver-openrouter.cjs` maps GDD's tier vocabulary onto a
|
|
4
|
+
dynamic OpenRouter catalog model id. This document is the human-readable companion
|
|
5
|
+
to that adapter; the adapter's `resolve(tier, opts)` is the canonical, executable
|
|
6
|
+
source of the mapping. Phase 33.6, decision D-03 (heuristic + override), D-04
|
|
7
|
+
(tier vocabulary), D-08 (graceful-null → native fallback).
|
|
8
|
+
|
|
9
|
+
## What it maps
|
|
10
|
+
|
|
11
|
+
The plugin speaks one tier vocabulary everywhere a model tier is named in
|
|
12
|
+
frontmatter or config: `opus`, `sonnet`, `haiku` — the same `VALID_TIERS` the
|
|
13
|
+
Phase-26 `tier-resolver.cjs` enforces. OpenRouter, by contrast, exposes a flat
|
|
14
|
+
catalog of provider-prefixed model ids (`anthropic/claude-opus-4-7`,
|
|
15
|
+
`meta-llama/llama-3.1-8b-instruct`, `qwen/qwen-2.5-72b-instruct`, …). The adapter
|
|
16
|
+
bridges the two by assigning each GDD tier to one internal capability bucket and
|
|
17
|
+
then picking the catalog id that best fits that bucket.
|
|
18
|
+
|
|
19
|
+
The ROADMAP's SC#4 names the buckets `high` / `medium` / `low`; those are the
|
|
20
|
+
heuristic's INTERNAL labels. They map one-to-one to the public tiers (D-04):
|
|
21
|
+
|
|
22
|
+
- `opus` ← HIGH bucket
|
|
23
|
+
- `sonnet` ← MEDIUM bucket
|
|
24
|
+
- `haiku` ← LOW bucket
|
|
25
|
+
|
|
26
|
+
The adapter's public `resolve(tier)` always speaks `opus` / `sonnet` / `haiku`;
|
|
27
|
+
`high` / `medium` / `low` never leak across the API boundary.
|
|
28
|
+
|
|
29
|
+
## The buckets
|
|
30
|
+
|
|
31
|
+
- **opus (HIGH) = top-tier closed.** The most capable closed-vendor model in the
|
|
32
|
+
catalog — the priciest premium id from a closed namespace. This is the
|
|
33
|
+
"spare-no-expense, hardest reasoning" slot.
|
|
34
|
+
- **sonnet (MEDIUM) = mid / top-open.** A capable model that sits below the opus
|
|
35
|
+
pick — typically the mid-priced closed model, or the strongest open model when
|
|
36
|
+
no second closed tier is present. The everyday workhorse slot.
|
|
37
|
+
- **haiku (LOW) = cheap open.** The cheapest capable OPEN model — the
|
|
38
|
+
fast/inexpensive slot for high-volume, low-stakes calls.
|
|
39
|
+
|
|
40
|
+
## The signals
|
|
41
|
+
|
|
42
|
+
The heuristic is computed from fields already present on each catalog model, so it
|
|
43
|
+
stays deterministic for a fixed catalog (no clock, no randomness — important so the
|
|
44
|
+
33.6-04 golden baseline is stable):
|
|
45
|
+
|
|
46
|
+
- **Namespace (closed vs open).** The id prefix before the `/` names the vendor.
|
|
47
|
+
`anthropic`, `openai`, `google` are treated as CLOSED (premium, frontier).
|
|
48
|
+
`meta-llama`, `qwen`, `mistralai`, `deepseek` are treated as OPEN (commodity,
|
|
49
|
+
cheap). The closed/open split is the primary axis: opus and sonnet prefer closed,
|
|
50
|
+
haiku requires open.
|
|
51
|
+
- **Pricing.** Each model carries `pricing.prompt` / `pricing.completion` as string
|
|
52
|
+
decimals (USD per token). Parsed to Number, the completion price is the tie-break:
|
|
53
|
+
highest completion price wins the opus slot; lowest completion price wins the
|
|
54
|
+
haiku slot. Models with unparseable or missing pricing sort last.
|
|
55
|
+
- **Context length.** `context_length` is a secondary capability signal used only to
|
|
56
|
+
break a pricing tie (longer context is treated as more capable).
|
|
57
|
+
|
|
58
|
+
For the canonical fixture catalog (closed `anthropic/claude-opus-4-7` +
|
|
59
|
+
`anthropic/claude-sonnet-4-7`, open `meta-llama/llama-3.1-70b-instruct`,
|
|
60
|
+
`meta-llama/llama-3.1-8b-instruct`, `qwen/qwen-2.5-72b-instruct`) the heuristic
|
|
61
|
+
resolves opus → `anthropic/claude-opus-4-7` (top closed, highest completion price),
|
|
62
|
+
sonnet → `anthropic/claude-sonnet-4-7` (mid closed), and haiku →
|
|
63
|
+
`meta-llama/llama-3.1-8b-instruct` (cheapest open).
|
|
64
|
+
|
|
65
|
+
## The override escape hatch
|
|
66
|
+
|
|
67
|
+
The heuristic is a sensible default, not a straitjacket. A user can pin any tier to
|
|
68
|
+
an exact catalog id via `.design/config.json`:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
{
|
|
72
|
+
"openrouter_tier_overrides": {
|
|
73
|
+
"opus": "anthropic/claude-opus-4-7",
|
|
74
|
+
"haiku": "meta-llama/llama-3.1-8b-instruct"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
An override **wins** over the heuristic: when `openrouter_tier_overrides[tier]` is a
|
|
80
|
+
non-empty string, the adapter returns it verbatim — even if that id is not present
|
|
81
|
+
in the live catalog (the user's explicit choice is honored over catalog membership).
|
|
82
|
+
Tests inject the same map via `opts.overrides` instead of reading the live config
|
|
83
|
+
file, so the override path is exercised hermetically. The config read is best-effort:
|
|
84
|
+
a missing file, a missing key, or corrupt JSON degrades to an empty override map
|
|
85
|
+
rather than throwing.
|
|
86
|
+
|
|
87
|
+
## The graceful-null contract
|
|
88
|
+
|
|
89
|
+
OpenRouter is opt-in ALONGSIDE native provider auth — never OpenRouter-only (D-08).
|
|
90
|
+
When no catalog is available (no cache, an empty `models[]`, or a `readCatalog` that
|
|
91
|
+
returns null) AND no override applies to the requested tier, `resolve` returns
|
|
92
|
+
`null`. A `null` is not an error: it is the signal that the caller (the router /
|
|
93
|
+
budget-enforcer, wired in 33.6-03) should fall back to the native provider via the
|
|
94
|
+
existing `scripts/lib/tier-resolver.cjs` fallback chain. The adapter NEVER throws —
|
|
95
|
+
an unknown tier, a missing config, a corrupt cache, or garbage options all degrade to
|
|
96
|
+
`null` (or to an override when one applies). This keeps OpenRouter a strictly
|
|
97
|
+
additive capability: turning it off, or having it fail to fetch, can never break a
|
|
98
|
+
resolution that would have succeeded natively.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# OpenRouter — Catalog-Derived Price Snapshot
|
|
2
|
+
|
|
3
|
+
**Phase 33.6 (v1.33.6).** This file is a **catalog-derived snapshot** of OpenRouter per-model prices — it is **generated from** `.design/cache/openrouter-models.json` (the dynamic catalog fetched by `scripts/lib/openrouter/catalog-fetcher.cjs`), **not** a hand-maintained authority. The **live source of truth is the dynamic catalog**; this table is a derived, illustrative view that can go stale between catalog fetches.
|
|
4
|
+
|
|
5
|
+
Unlike the per-runtime tables under `reference/prices/` (Phase 26 D-08, hand-curated authority with provenance), OpenRouter's prices live in the upstream `/models` response and are refreshed on the 24h TTL. To inspect the current resolved prices, run `/gdd:openrouter-status` or read the cache directly. For the tier→model resolution heuristic see `reference/openrouter-tier-mapping.md`.
|
|
6
|
+
|
|
7
|
+
OpenRouter quotes prices **per token** (USD), for `prompt` (input) and `completion` (output) separately.
|
|
8
|
+
|
|
9
|
+
## Representative sample (per token, USD)
|
|
10
|
+
|
|
11
|
+
Derived from the fixture catalog at `test/fixtures/baselines/phase-33-6/openrouter-catalog.json` (a snapshot mirror of the cache shape). Actual live prices come from the catalog at fetch time.
|
|
12
|
+
|
|
13
|
+
| model id | prompt $/tok | completion $/tok |
|
|
14
|
+
|----------|--------------|------------------|
|
|
15
|
+
| `anthropic/claude-opus-4-7` | 0.000015 | 0.000075 |
|
|
16
|
+
| `anthropic/claude-sonnet-4-7` | 0.000003 | 0.000015 |
|
|
17
|
+
| `meta-llama/llama-3.1-70b-instruct` | 0.00000052 | 0.00000075 |
|
|
18
|
+
| `meta-llama/llama-3.1-8b-instruct` | 0.00000002 | 0.00000005 |
|
|
19
|
+
| `qwen/qwen-2.5-72b-instruct` | 0.00000038 | 0.0000004 |
|
|
20
|
+
|
|
21
|
+
## Notes
|
|
22
|
+
|
|
23
|
+
- **Derived view, not authority.** Do not hand-edit prices here to "fix" cost math — fix the catalog fetch instead. This file documents the *shape* and *source* of OpenRouter pricing for the registry round-trip and for human reference.
|
|
24
|
+
- **Per-token vs per-1M.** The native runtime tables (`reference/prices/<runtime>.md`) quote `input_per_1m` / `output_per_1m`; OpenRouter's catalog quotes per-token. Multiply by 1,000,000 to compare (e.g. `anthropic/claude-opus-4-7` ≈ $15 input / $75 output per 1M tokens).
|
|
25
|
+
- **Cost telemetry.** When a model is resolved via the OpenRouter adapter, the cost row tags `provider: openrouter` (Phase 33.6-03, SC#6) — see `scripts/lib/budget-enforcer.cjs#buildCostEventPayload`.
|
|
26
|
+
- **Drift.** The authority-watcher diffs the catalog weekly and surfaces `deprecated`/`withdrawn` models matching a configured `openrouter_tier_overrides` pin (SC#8) — see `scripts/lib/authority-watcher/index.cjs#diffOpenRouterCatalog`.
|
package/reference/registry.json
CHANGED
|
@@ -874,6 +874,27 @@
|
|
|
874
874
|
"type": "heuristic",
|
|
875
875
|
"phase": 33.5,
|
|
876
876
|
"description": "Phase 33.5 static security audit of GDD's shipped runtime surface (hooks/scripts/sdk/bin) — outbound-network call sites, secret-handling sites, and external-input surfaces; human-readable companion to scripts/security/outbound-allowlist.json (the canonical active-egress allowlist the 33.5-04 scan-outbound-network.cjs gate consumes) and reference/gdd-threat-model.md."
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"name": "openrouter-tier-mapping",
|
|
880
|
+
"path": "reference/openrouter-tier-mapping.md",
|
|
881
|
+
"type": "heuristic",
|
|
882
|
+
"phase": 33.6,
|
|
883
|
+
"description": "Phase 33.6 OpenRouter tier-mapping heuristic — maps GDD opus/sonnet/haiku onto OpenRouter catalog ids via closed-vs-open + pricing buckets (high/medium/low), with the .design/config.json#openrouter_tier_overrides escape hatch (override wins) and graceful-null → native fallback."
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
"name": "prices-openrouter",
|
|
887
|
+
"path": "reference/prices.openrouter.md",
|
|
888
|
+
"type": "data",
|
|
889
|
+
"phase": 33.6,
|
|
890
|
+
"description": "Phase 33.6 catalog-derived OpenRouter price sub-table — per-model prompt/completion $/tok snapshot of .design/cache/openrouter-models.json; derived view, the dynamic catalog is the source of truth (D-11 registry round-trip)."
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"name": "native-platforms",
|
|
894
|
+
"path": "reference/native-platforms.md",
|
|
895
|
+
"type": "heuristic",
|
|
896
|
+
"phase": 34.1,
|
|
897
|
+
"description": "Phase 34.1 token-bridge spec — maps the canonical CSS-token form (Phase 23) to SwiftUI Color/Font/ViewModifier, Jetpack Compose Color/Shapes/Typography/MaterialTheme, and Flutter ThemeData/ColorScheme/TextTheme, with the precision contract (color hex→8-bit channels exact, dimension px→pt/dp/logical px) defining token-identity for the round-trip."
|
|
877
898
|
}
|
|
878
899
|
]
|
|
879
900
|
}
|