Skip to content

Commit 84c86fb

Browse files
qq9340100claude
andauthored
feat(spec): preview/trial 的 discovery 折叠改为显式声明,折叠表对 EnvironmentType 穷尽 (#6287) (#6610)
`EnvironmentTypeSchema` 有七个成员,`NODE_ENV_TO_DISCOVERY_ENVIRONMENT` (`packages/spec/src/api/discovery.zod.ts`)只为其中五个写了条目。`preview` 与 `trial` 一直靠 `resolveDiscoveryEnvironment` 末行的 `?? 'development'` 兜底落到 `development` —— 不是一条被写下来的决定,而是掉出表尾的副作用。 这张表的注释本来就写明它是给后来者读的,读表的人会以为它是全的;#6554 刚把 unset 一行收进这个 mapper,留下的正是这最后一条隐式边。 折叠目标 = `sandbox`(两者皆是),三条理据 —— 它们既不是 absent(宿主拒答) 也不是未识别拼写(猜测),而是**已声明的一等成员**,属第三类: 1. 本仓语义:environment 是被开通的运行容器(独立数据库、规范主机名、套餐档位、 按环境 RBAC,见 `cloud/environment.zod.ts`)。`preview` / `trial` 是这种东西, 不是 `development` / `dev` / `test` 描述的开发机与 CI 的一次性运行; `sandbox` 正是枚举里「已开通的准生产」那一档,`staging` 已按同样理由折在此处。 2. 姿态按收紧方向:`trial` 装着评估中客户的真实业务数据,答 `development` 是低报姿态 —— 与 #5673 / #5936 把 unset 翻成 `production` 所避免的是同类错误, 只低一档。两者都不是 `production`,报 `production` 会朝另一个方向答错。 3. 保住作者的区分:把环境标成 `preview` 的人手里本就有 `development` 和 `test` 而没有选;折到 `development` 会抹平这个选择携带的唯一信息。 漏补条目从此不编译:七行分组在 `satisfies Record<EnvironmentType, DiscoveryEnvironment>` 之后(operator 便利拼写 `prod` / `dev` 留在组外,否则穷尽标注写不出来)。逆向验证(方向先判后跑,两条都实读): 删 `preview` 行 → TS1360 `Property 'preview' is missing`;加非成员键 `qa` → TS2353 `'qa' does not exist in type`。选编译期而非运行期断言,是因为运行期断言看不见这个缺陷 —— 兜底与三条已声明的行都产出 `development`,一条运行期穷尽测试在坏状态下本来就是绿的。 `?? 'development'` 兜底保留,职责收窄并写进注释:只服务既非词表成员、也非 operator 简写的任意字符串(`qa`、`uat`、拼错)。#5936 的两条既有 pin(absent → `production`、 未识别 → `development`)规则未动且保持绿。 夹带的 fixture 三处按消费半径逐条重判(不是批量改写):`packages/spec`、 `packages/metadata-protocol`、`packages/runtime` 三个 test 都把 `preview` 当作「未识别拼写」的例子,而它现在是已声明成员 —— 规则保留,例子改成 真正未识别的 `uat`。其中 metadata-protocol 那条是 spec 范围的 sweep 看不到、 由消费者测试跑出来的(与 PR #5046 同一类)。 Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY Co-authored-by: Claude <noreply@anthropic.com>
1 parent bfe689b commit 84c86fb

6 files changed

Lines changed: 263 additions & 12 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `preview` / `trial` 的 discovery 折叠改为显式声明,并让折叠表对 EnvironmentType 穷尽 (#6287)
6+
7+
`EnvironmentTypeSchema` 有七个成员,而 `NODE_ENV_TO_DISCOVERY_ENVIRONMENT`
8+
(`api/discovery.zod.ts`)只为其中五个写了条目。`preview``trial`
9+
一直是靠 `resolveDiscoveryEnvironment` 末行的 `?? 'development'`
10+
兜底落到 `development` 的 —— 不是一条被写下来的决定,而是掉出表尾的副作用。
11+
这张表的注释本来就写明它是给后来者读的,读表的人会以为它是全的。
12+
13+
## 行为变化(唯一一处,消费者可见)
14+
15+
`resolveDiscoveryEnvironment` 对两个输入的返回值改变:
16+
17+
| `NODE_ENV`(或任何 operator 提供的字符串) | 之前(兜底) | 现在(声明) |
18+
|:---|:---|:---|
19+
| `preview` | `development` | `sandbox` |
20+
| `trial` | `development` | `sandbox` |
21+
22+
`/discovery``environment` 字段是机器可读面,客户端读它回答「我是不是在跟生产说话」,
23+
并据此决定要不要放宽破坏性操作的二次确认。折向 `sandbox` 的三条理由:
24+
25+
1. **它们在本仓语义里是什么。** 本仓的 environment 是被开通的运行容器 —— 独立数据库、
26+
规范主机名、套餐档位、按环境的 RBAC(`cloud/environment.zod.ts`)。`preview` /
27+
`trial` 是这种东西,不是 `development` / `dev` / `test` 所描述的开发机与 CI 的一次性运行;
28+
`sandbox` 正是这个枚举里「已开通的准生产」那一档。
29+
2. **姿态按收紧方向取。** `trial` 尤其装着评估中客户的真实业务数据,答 `development`
30+
**低报**姿态 —— 与 #5673 / #5936 把 unset 一行翻成 `production` 所要避免的是同一类错误,
31+
只是低一档。两者都不是 `production`:它们按定义就不是客户的生产部署,报 `production`
32+
会让这个字段唯一要回答的问题朝另一个方向答错。
33+
3. **它保住了作者的区分。** 把环境标成 `preview` 的人手里本来就有 `development``test`
34+
而没有选;折到 `development` 会把这个选择携带的唯一信息抹平。
35+
36+
其余五行、unset → `production`(#5673 / #5936)、未识别拼写 → `development`(#4828)
37+
三条规则一概未动。
38+
39+
## 漏补条目从此不编译
40+
41+
折叠表拆成两张:声明面 `Record<EnvironmentType, DiscoveryEnvironment>`(七个成员,穷尽),
42+
与 operator 便利拼写 `prod` / `dev`(不属于词表,单列以免污染穷尽标注),合并后仍是原来那张查找表。
43+
`EnvironmentTypeSchema` 加一个桶而不说它折向哪里,现在直接**编译不过**
44+
45+
这是**编译期**而非运行期断言,因为运行期断言看不见这个缺陷:兜底与三条已声明的行都产出
46+
`'development'`,所以调 `resolveDiscoveryEnvironment` 得到的答案在「有条目」与「`??` 现编」
47+
两种情况下完全一致 —— 一条运行期穷尽测试在 #6287 报告的那个坏状态下本来就是绿的。
48+
49+
`?? 'development'` 兜底保留,职责收窄为它真正服务的那一类:既不是词表成员、也不是
50+
operator 简写的任意字符串(`qa``uat`、拼错),`NODE_ENV` 是 operator 提供的自由文本,
51+
这一类是真实输入,把它降级到 `development` 正是「猜测不得声称 production」。

packages/metadata-protocol/src/discovery-schema-conformance.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,13 @@ describe('[#4828] getDiscovery() conforms to DiscoverySchema', () => {
254254
expect(DiscoverySchema.safeParse(discovery).success).toBe(true);
255255
});
256256

257-
it.each(['qa', 'preview', 'nonsense'])(
257+
// [#6287] `preview` left this list when it stopped being unrecognised: it is
258+
// a declared `EnvironmentTypeSchema` member and now has a stated fold
259+
// (`sandbox`), so asserting `development` for it here would assert the
260+
// opposite of the mapper's decision. The RULE these rows pin — an unknown
261+
// spelling degrades to `development` and never claims production — is
262+
// unchanged; only the examples had to be ones that are genuinely unknown.
263+
it.each(['qa', 'uat', 'nonsense'])(
258264
'NODE_ENV=%s is an unrecognised spelling — still development, never production (#4828)',
259265
async (raw) => {
260266
process.env.NODE_ENV = raw;

packages/runtime/src/discovery-schema-conformance.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,11 @@ describe('[#4828] getDiscoveryInfo() conforms to DiscoverySchema', () => {
322322
// two different rules and #5673 deliberately moved only the first — #4828's
323323
// "never CLAIM production on a guess" is untouched, and this case is the
324324
// guard against a later simplification collapsing them back into one.
325-
it.each(['qa', 'preview', 'uat', 'nonsense'])(
325+
// [#6287] `preview` dropped out of this list when it gained a declared fold
326+
// (`sandbox`) — it is an `EnvironmentTypeSchema` member, so it is no longer
327+
// an example of a spelling this repo does not recognise. The rule and its
328+
// remaining examples are untouched.
329+
it.each(['qa', 'uat', 'nonsense'])(
326330
'NODE_ENV=%s is an unrecognised spelling — still development, never production (#4828)',
327331
async (raw) => {
328332
process.env.NODE_ENV = raw;

packages/spec/src/api/discovery.test.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
type WellKnownCapabilities,
2020
type RouteHealthEntry,
2121
type RouteHealthReport,
22+
type DiscoveryEnvironment,
2223
} from './discovery.zod';
24+
import { EnvironmentTypeSchema, type EnvironmentType } from '../cloud/environment.zod';
2325

2426
describe('ApiRoutesSchema', () => {
2527
it('should accept valid minimal routes', () => {
@@ -1197,14 +1199,22 @@ describe('[#4828] resolveDiscoveryEnvironment (decision 4 — enum, not passthro
11971199
}
11981200
});
11991201

1202+
// [#6287] `preview` was one of this fixture's three examples until the fold
1203+
// table grew a row for it. The RULE is unchanged and still pinned — an
1204+
// unrecognised spelling never claims production — but `preview` is no longer
1205+
// an example of one: it is a declared `EnvironmentTypeSchema` member with a
1206+
// stated fold, so leaving it here would have asserted the opposite of what
1207+
// the source now says, and would have kept passing only because the declared
1208+
// answer happened to equal the fallback's. Re-spelled to inputs that are
1209+
// genuinely outside both the taxonomy and the operator shorthands.
12001210
it('never CLAIMS production for an unrecognized spelling (#4828)', () => {
1201-
for (const raw of ['qa', 'preview', 'nonsense']) {
1211+
for (const raw of ['qa', 'uat', 'nonsense']) {
12021212
expect(resolveDiscoveryEnvironment(raw), raw).toBe('development');
12031213
}
12041214
});
12051215

12061216
it('every mapped result actually satisfies the declared enum', () => {
1207-
for (const raw of ['production', 'prod', 'sandbox', 'staging', 'development', 'dev', 'test', 'qa', '']) {
1217+
for (const raw of ['production', 'prod', 'sandbox', 'staging', 'development', 'dev', 'test', 'preview', 'trial', 'qa', '']) {
12081218
const parsed = DiscoverySchema.parse({
12091219
name: 'ObjectStack',
12101220
version: '1.0.0',
@@ -1218,3 +1228,100 @@ describe('[#4828] resolveDiscoveryEnvironment (decision 4 — enum, not passthro
12181228
}
12191229
});
12201230
});
1231+
1232+
/**
1233+
* [#6287] Every `EnvironmentType` member folds by DECLARATION, not by fallback.
1234+
*
1235+
* Before this, five of the seven members had a row in the fold table and
1236+
* `preview` / `trial` fell through `?? 'development'` — a fold nobody had
1237+
* written down, in a table whose whole purpose is to be read by the next
1238+
* author. The repair is a row each, and a type that keeps the table total.
1239+
*/
1240+
describe('[#6287] the fold table is total over EnvironmentType', () => {
1241+
/**
1242+
* The declared fold per bucket. This mirrors the source table on purpose: it
1243+
* is the RUNTIME half of the pin and it proves the *values*, so a silent
1244+
* re-aim of any row (`staging` quietly moving to `development`, say) fails
1245+
* here rather than in a consumer.
1246+
*/
1247+
const declaredFold: Record<EnvironmentType, DiscoveryEnvironment> = {
1248+
production: 'production',
1249+
sandbox: 'sandbox',
1250+
development: 'development',
1251+
test: 'development',
1252+
staging: 'sandbox',
1253+
preview: 'sandbox',
1254+
trial: 'sandbox',
1255+
};
1256+
1257+
it('resolves every EnvironmentTypeSchema member to its declared fold', () => {
1258+
const members = EnvironmentTypeSchema.options as readonly EnvironmentType[];
1259+
// Anti-vacuity: `.options` comes through the lazySchema Proxy, and a broken
1260+
// read would make the loop below assert nothing at all.
1261+
expect(Array.isArray(members)).toBe(true);
1262+
expect(members.length).toBeGreaterThan(0);
1263+
for (const member of members) {
1264+
expect(resolveDiscoveryEnvironment(member), member).toBe(declaredFold[member]);
1265+
}
1266+
});
1267+
1268+
it('folds preview and trial to sandbox — the two rows this issue added', () => {
1269+
// Both are provisioned environments (isolated database, hostname, plan
1270+
// tier, per-environment RBAC), not the developer-class runs `development`
1271+
// and `test` describe — and `trial` in particular holds an evaluating
1272+
// customer's real data, so `development` would understate the posture a
1273+
// client reads this flag to judge.
1274+
expect(resolveDiscoveryEnvironment('preview')).toBe('sandbox');
1275+
expect(resolveDiscoveryEnvironment('trial')).toBe('sandbox');
1276+
// Declared rows go through the same operator-input normalization as the
1277+
// rest of the table, not a second path.
1278+
expect(resolveDiscoveryEnvironment(' PREVIEW ')).toBe('sandbox');
1279+
expect(resolveDiscoveryEnvironment('Trial')).toBe('sandbox');
1280+
});
1281+
1282+
it('leaves the #5936 two-rule split standing (absence ≠ unrecognised)', () => {
1283+
// Neither new row may be read as loosening those. Absence is still the host
1284+
// declining to answer; an unrecognised spelling is still a guess.
1285+
expect(resolveDiscoveryEnvironment(undefined)).toBe('production');
1286+
expect(resolveDiscoveryEnvironment('preview-2')).toBe('development');
1287+
});
1288+
1289+
it('rejects a fold table that misses a member — the exhaustiveness gate itself', () => {
1290+
// ⚠️ This assertion is made by `tsc`, not by vitest, and that is the point.
1291+
// A RUNTIME exhaustiveness test cannot see the defect #6287 reported: the
1292+
// fallback and three declared rows all produce `'development'`, so calling
1293+
// `resolveDiscoveryEnvironment('preview')` returned an identical answer
1294+
// whether a row existed or the `??` invented one. Such a test would have
1295+
// passed on the broken state. `Record<EnvironmentType, …>` compares the KEY
1296+
// SET, which is the actual claim, so the gate lives on the source table's
1297+
// type annotation and this is its negative control.
1298+
//
1299+
// What it does and does not cover, stated exactly:
1300+
// - It DOES fail both ways on the enum's membership. Add a bucket to
1301+
// `EnvironmentTypeSchema` and the six-key literal below is missing two,
1302+
// so the source table must grow a row before anything compiles. REMOVE
1303+
// `trial` from the enum and this literal becomes complete, the directive
1304+
// goes UNUSED, and tsc reports TS2578 — a vacuously-passing negative
1305+
// control cannot hide here.
1306+
// - It does NOT catch someone widening the source annotation itself (to
1307+
// `Record<string, …>`, say). No type-level pin can: the table is not
1308+
// exported, and exporting it to satisfy a test would put a private
1309+
// lookup on this package's public surface. That edit is a deliberate,
1310+
// visible change to a line whose own comment forbids it, not the silent
1311+
// drift #6287 was about — the drift was a member that nobody had to
1312+
// touch anything to omit.
1313+
// `packages/spec`'s test layer IS type-checked (`tsconfig.test.json`, named
1314+
// in the `typecheck` script since #5286) and this file carries no entry in
1315+
// `test-typecheck-debt.json`, so the directive is live, not phantom.
1316+
// @ts-expect-error [#6287] `trial` has no fold — a partial table must not type-check.
1317+
const missingTrial: Record<EnvironmentType, DiscoveryEnvironment> = {
1318+
production: 'production',
1319+
sandbox: 'sandbox',
1320+
development: 'development',
1321+
test: 'development',
1322+
staging: 'sandbox',
1323+
preview: 'sandbox',
1324+
};
1325+
expect(Object.keys(missingTrial)).toHaveLength(6);
1326+
});
1327+
});

packages/spec/src/api/discovery.zod.ts

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import { z } from 'zod';
44
import { HttpMethod } from '../shared/http.zod';
5+
// [#6287] Type-only: erased at compile time, so this carries no runtime edge
6+
// from `api/` to `cloud/`. It is what makes the NODE_ENV fold table provably
7+
// total over the environment taxonomy rather than total by inspection.
8+
import type { EnvironmentType } from '../cloud/environment.zod';
59

610
/**
711
* Service Status Enum
@@ -311,17 +315,52 @@ export type DiscoveryEnvironment = z.input<typeof DiscoveryEnvironmentSchema>;
311315
* | `development`, `dev` | `development` | exact / short spelling |
312316
* | `test` | `development` | ephemeral developer-class run (vitest/CI), not a provisioned pre-production copy |
313317
* | `staging` | `sandbox` | pre-production and production-LIKE; certainly not `production`, and `sandbox` is the enum's pre-production member |
318+
* | `preview` | `sandbox` | a PROVISIONED environment (own database, hostname, plan tier, per-environment RBAC), not a developer's machine — same class as `staging` (#6287) |
319+
* | `trial` | `sandbox` | a provisioned environment holding an evaluating customer's real business data; developer-class would understate it (#6287) |
314320
* | unset / blank | `production` | the host declined to say; every other reader of that absence already says `production`, and of the two ways to be wrong, calling a real production deployment `development` is the dangerous one (#5673, #5936) |
315321
* | anything else | `development` | an unrecognised spelling is a GUESS, and this function never claims `production` on a guess (#4828) |
316322
*
317323
* The last two rows carry the whole safety argument, and they point opposite
318-
* ways on purpose. An unrecognised spelling (`qa`, `preview`) degrades to
324+
* ways on purpose. An unrecognised spelling (`qa`, `uat`) degrades to
319325
* `development`, so nothing here ever advertises `production` for an
320326
* environment it failed to recognise. **Absence is not a guess** — it is the
321327
* host declining to answer, and the conservative response to that is
322328
* `production`: `environment` is machine-readable, and a client may skip
323329
* production warnings or loosen a destructive action's confirmation on it.
324330
*
331+
* ## `preview` and `trial` are a THIRD case — declared, not absent, not a guess (#6287)
332+
*
333+
* Until #6287 those two reached `development` through the `??` fallback rather
334+
* than through a decision, so this table declared five of the seven
335+
* `EnvironmentTypeSchema` members and let the other two fall off the end. That
336+
* is the same shape the two rows above exist to separate: the fallback answers
337+
* for spellings this repo has never heard of, and a first-class member of our
338+
* OWN taxonomy is not one of those. It is neither the host declining to answer
339+
* nor a guess — it is a bucket we ship, and where it folds is ours to state.
340+
*
341+
* Both fold to `sandbox`, and the reasoning is the same for each:
342+
*
343+
* 1. **What they are here.** In this repo's taxonomy an environment is a
344+
* provisioned runtime container — isolated database, canonical hostname,
345+
* plan tier, per-environment RBAC (`cloud/environment.zod.ts`). A `preview`
346+
* or `trial` environment is that, not an ephemeral developer-class run. The
347+
* `development` bucket is reserved for the machine-and-CI class (`development`,
348+
* `dev`, `test`); `sandbox` is the enum's provisioned pre-production member,
349+
* which is what these two are.
350+
* 2. **Posture, in the tightening direction.** `trial` in particular holds an
351+
* evaluating customer's real business data. `environment` is read to decide
352+
* whether to soften a destructive action's confirmation, so answering
353+
* `development` there understates the posture — the same *kind* of error the
354+
* unset row was flipped to avoid (#5673, #5936), one bucket down. Neither is
355+
* `production`: they are by definition not the customer's production
356+
* deployment, and claiming otherwise would make the flag's one question
357+
* ("am I talking to production?") answer wrongly in the other direction.
358+
* 3. **It keeps the author's distinction.** An operator who tags an environment
359+
* `preview` had `development` and `test` available and did not pick them.
360+
* Folding `preview` onto `development` erases the only information that
361+
* choice carried; folding it onto `sandbox` preserves it as far as a
362+
* three-member enum can.
363+
*
325364
* The unset row moved from `development` to `production` in #5673 (maintainer
326365
* ruling 2026-08-06) and moved HERE, into the shared mapper, in #5936 (ruling
327366
* 2026-08-07, direction 1). #5673 could only reach its own producer — the
@@ -337,15 +376,43 @@ export type DiscoveryEnvironment = z.input<typeof DiscoveryEnvironmentSchema>;
337376
* folded that into its default (it tests with `||`). Had this treated blank as
338377
* "anything else" the two producers would have drifted again on exactly that
339378
* input — the drift this consolidation exists to end.
379+
*
380+
* ## The taxonomy half of this table is EXHAUSTIVE, and tsc keeps it so (#6287)
381+
*
382+
* The seven `EnvironmentTypeSchema` rows are grouped behind a
383+
* `satisfies Record<EnvironmentType, DiscoveryEnvironment>`: adding a bucket to
384+
* that enum without deciding where it folds does not compile, and a key that is
385+
* not a member does not compile either. The operator shorthands (`prod`, `dev`)
386+
* sit outside that group precisely so the check stays writable — they are
387+
* convenience spellings, not members.
388+
*
389+
* It is a COMPILE-time check rather than a runtime assertion because a runtime
390+
* one cannot see this defect: the fallback and three of the seven rows all
391+
* produce `'development'`, so calling `resolveDiscoveryEnvironment` returns an
392+
* indistinguishable answer whether a row exists or the `??` invented it. A
393+
* runtime exhaustiveness test would have passed on the exact state #6287
394+
* reported. `tsc` compares the KEY SET, which is the actual claim. The negative
395+
* control for it lives in `discovery.test.ts`.
340396
*/
341397
const NODE_ENV_TO_DISCOVERY_ENVIRONMENT: Readonly<Record<string, DiscoveryEnvironment>> = {
342-
production: 'production',
398+
// The seven declared `EnvironmentTypeSchema` buckets. The `satisfies` is the
399+
// gate described above: every member must appear, and nothing that is not a
400+
// member may (#6287).
401+
...({
402+
production: 'production',
403+
sandbox: 'sandbox',
404+
development: 'development',
405+
test: 'development',
406+
staging: 'sandbox',
407+
preview: 'sandbox',
408+
trial: 'sandbox',
409+
} satisfies Record<EnvironmentType, DiscoveryEnvironment>),
410+
411+
// Operator convenience spellings — deliberately OUTSIDE the block above,
412+
// because they are not taxonomy members and folding them in would make the
413+
// exhaustiveness check unwritable.
343414
prod: 'production',
344-
sandbox: 'sandbox',
345-
staging: 'sandbox',
346-
development: 'development',
347415
dev: 'development',
348-
test: 'development',
349416
};
350417

351418
/**
@@ -369,6 +436,13 @@ const NODE_ENV_TO_DISCOVERY_ENVIRONMENT: Readonly<Record<string, DiscoveryEnviro
369436
export function resolveDiscoveryEnvironment(raw?: string | null): DiscoveryEnvironment {
370437
const spelling = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
371438
if (spelling === '') return 'production';
439+
// [#6287] The fallback's ONE remaining job: a spelling that is not a declared
440+
// `EnvironmentType` member and not an operator shorthand — `qa`, `uat`, a
441+
// typo. It no longer silently answers for members of our own taxonomy; the
442+
// table above is total over them and `tsc` keeps it that way, so a future
443+
// bucket cannot reach this line by being forgotten. Keep it: `NODE_ENV` is an
444+
// arbitrary operator string, so "anything else" is a real input class, and
445+
// degrading it to `development` is what stops a guess claiming `production`.
372446
return NODE_ENV_TO_DISCOVERY_ENVIRONMENT[spelling] ?? 'development';
373447
}
374448

0 commit comments

Comments
 (0)