@bitmagic/asset-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forger-client.d.ts +42 -0
- package/dist/forger-client.d.ts.map +1 -0
- package/dist/forger-client.js +135 -0
- package/dist/forger-client.js.map +1 -0
- package/dist/generators/animation.d.ts +102 -0
- package/dist/generators/animation.d.ts.map +1 -0
- package/dist/generators/animation.js +234 -0
- package/dist/generators/animation.js.map +1 -0
- package/dist/generators/character.d.ts +40 -0
- package/dist/generators/character.d.ts.map +1 -0
- package/dist/generators/character.js +134 -0
- package/dist/generators/character.js.map +1 -0
- package/dist/generators/image.d.ts +43 -0
- package/dist/generators/image.d.ts.map +1 -0
- package/dist/generators/image.js +74 -0
- package/dist/generators/image.js.map +1 -0
- package/dist/generators/skybox.d.ts +18 -0
- package/dist/generators/skybox.d.ts.map +1 -0
- package/dist/generators/skybox.js +57 -0
- package/dist/generators/skybox.js.map +1 -0
- package/dist/generators/sound-effect.d.ts +31 -0
- package/dist/generators/sound-effect.d.ts.map +1 -0
- package/dist/generators/sound-effect.js +73 -0
- package/dist/generators/sound-effect.js.map +1 -0
- package/dist/http.d.ts +13 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +29 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/result.d.ts +6 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +9 -0
- package/dist/result.js.map +1 -0
- package/dist/terminal-failure.d.ts +152 -0
- package/dist/terminal-failure.d.ts.map +1 -0
- package/dist/terminal-failure.js +203 -0
- package/dist/terminal-failure.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +12 -0
- package/dist/validation.js.map +1 -0
- package/dist/world-patch.d.ts +25 -0
- package/dist/world-patch.d.ts.map +1 -0
- package/dist/world-patch.js +59 -0
- package/dist/world-patch.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal-failure contract between game-play-agent and its clients.
|
|
3
|
+
*
|
|
4
|
+
* Import path matters: this module is deliberately dependency-free and is
|
|
5
|
+
* published as the `@bitmagic/asset-core/terminal-failure` subpath. Browser
|
|
6
|
+
* consumers (Creator) MUST import that subpath rather than the package root —
|
|
7
|
+
* the root barrel re-exports the asset generators, which drag in zod, the
|
|
8
|
+
* forger client, and node http helpers.
|
|
9
|
+
*
|
|
10
|
+
* A "terminal failure" is the single reason an operation stopped without
|
|
11
|
+
* delivering what the user asked for. Exactly one is produced per failed
|
|
12
|
+
* operation, and it travels on both the live edit stream and the reconnect /
|
|
13
|
+
* replay stream so a client that dropped off mid-run learns the same reason as
|
|
14
|
+
* one that stayed connected.
|
|
15
|
+
*
|
|
16
|
+
* Why this lives in a shared package rather than being duplicated: the previous
|
|
17
|
+
* hand-maintained copies drifted in production — the creator's error union grew
|
|
18
|
+
* an `insufficient_sparks` member that the agent's never had, so the agent could
|
|
19
|
+
* not emit the value the client was waiting for. One enum is enforced by the
|
|
20
|
+
* compiler; two are enforced by memory.
|
|
21
|
+
*
|
|
22
|
+
* What the wire carries is FACTS (code + structured detail). What the wire does
|
|
23
|
+
* NOT carry is copy — no title, no user-facing message, no `retryable` flag.
|
|
24
|
+
* Presentation is derived from `code` by the client, so an old agent can never
|
|
25
|
+
* dictate new product copy and a new code cannot ship without the client
|
|
26
|
+
* deciding how to say it.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Every way an operation can terminally fail.
|
|
30
|
+
*
|
|
31
|
+
* The organising rule is **one code per distinct remedy**, not one per stack
|
|
32
|
+
* trace. `spark_floor_reached` and `spark_service_unavailable` are separate
|
|
33
|
+
* because only the first is fixed by buying sparks; `stream_malformed` and
|
|
34
|
+
* `agent_internal_error` are separate because the second is our bug and should
|
|
35
|
+
* be reported rather than retried forever.
|
|
36
|
+
*
|
|
37
|
+
* Adding a member is a four-step change — see
|
|
38
|
+
* `game-play-agent/docs/terminal-failures.md`. The client's presentation table
|
|
39
|
+
* is asserted exhaustive against this array, so a missing entry fails the build
|
|
40
|
+
* rather than shipping an unexplained error.
|
|
41
|
+
*/
|
|
42
|
+
export declare const TERMINAL_FAILURE_CODES: readonly ["spark_floor_reached", "spark_service_unavailable", "provider_rate_limited", "provider_unavailable", "context_overflow", "agent_timeout", "stream_malformed", "agent_internal_error", "agent_unavailable", "operation_not_found", "connection_lost", "setup_failed", "changes_rolled_back", "no_changes_applied", "user_cancelled"];
|
|
43
|
+
export type TerminalFailureCode = (typeof TERMINAL_FAILURE_CODES)[number];
|
|
44
|
+
/**
|
|
45
|
+
* Structured facts about a failure, discriminated by the same `code` as the
|
|
46
|
+
* envelope. Only codes with facts worth acting on have an arm; the rest carry
|
|
47
|
+
* no detail. Keeping the discriminator on the detail itself means a narrowed
|
|
48
|
+
* `detail` stays type-safe when passed around on its own.
|
|
49
|
+
*/
|
|
50
|
+
export type TerminalFailureDetail = {
|
|
51
|
+
readonly code: 'spark_floor_reached';
|
|
52
|
+
readonly balance: number;
|
|
53
|
+
readonly floor: number;
|
|
54
|
+
readonly shortfall?: number;
|
|
55
|
+
} | {
|
|
56
|
+
readonly code: 'spark_service_unavailable';
|
|
57
|
+
readonly failureCount: number;
|
|
58
|
+
readonly lastStatus?: number;
|
|
59
|
+
} | {
|
|
60
|
+
readonly code: 'provider_rate_limited';
|
|
61
|
+
readonly statusCode?: number;
|
|
62
|
+
readonly provider?: string;
|
|
63
|
+
} | {
|
|
64
|
+
readonly code: 'provider_unavailable';
|
|
65
|
+
readonly statusCode?: number;
|
|
66
|
+
readonly provider?: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly code: 'agent_timeout';
|
|
69
|
+
readonly timeoutSec: number;
|
|
70
|
+
readonly extended: boolean;
|
|
71
|
+
} | {
|
|
72
|
+
readonly code: 'agent_unavailable';
|
|
73
|
+
readonly source: 'reaper' | 'disk-restore' | 'reconnect';
|
|
74
|
+
readonly lastEventAgeMs?: number;
|
|
75
|
+
} | {
|
|
76
|
+
readonly code: 'setup_failed';
|
|
77
|
+
readonly step?: string;
|
|
78
|
+
} | {
|
|
79
|
+
readonly code: 'changes_rolled_back';
|
|
80
|
+
readonly gate: 'type-check' | 'world-json';
|
|
81
|
+
readonly errors: readonly string[];
|
|
82
|
+
} | {
|
|
83
|
+
readonly code: 'no_changes_applied';
|
|
84
|
+
readonly subReason: 'zero-tools' | 'replayed-recap' | 'tools-but-no-files';
|
|
85
|
+
};
|
|
86
|
+
/** Current envelope version. Bump only on a breaking field change. */
|
|
87
|
+
export declare const TERMINAL_FAILURE_VERSION = 1;
|
|
88
|
+
/**
|
|
89
|
+
* A terminal failure, described once, at the moment it is known.
|
|
90
|
+
*/
|
|
91
|
+
export interface TerminalFailure {
|
|
92
|
+
/** Envelope version. An unrecognised value degrades to `agent_internal_error`. */
|
|
93
|
+
readonly v: number;
|
|
94
|
+
/** The discriminator. The only field a UI should branch on. */
|
|
95
|
+
readonly code: TerminalFailureCode;
|
|
96
|
+
/**
|
|
97
|
+
* The operation id, which is also the edit session id. Always present:
|
|
98
|
+
* it is what a user quotes in a bug report and what fetches the session log.
|
|
99
|
+
*/
|
|
100
|
+
readonly operationId: string;
|
|
101
|
+
/**
|
|
102
|
+
* Raw, untranslated, developer-facing text. Belongs in a "technical details"
|
|
103
|
+
* block and in analytics. Never use it as a title or as the primary message —
|
|
104
|
+
* it is frequently a provider error string.
|
|
105
|
+
*/
|
|
106
|
+
readonly technical: string;
|
|
107
|
+
/** ISO-8601 instant the failure was decided. */
|
|
108
|
+
readonly at: string;
|
|
109
|
+
/** Per-code structured facts, when the code has any. */
|
|
110
|
+
readonly detail?: TerminalFailureDetail;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The pre-contract error union that clients already understand.
|
|
114
|
+
*
|
|
115
|
+
* Kept on the wire alongside the envelope so a Creator build from before this
|
|
116
|
+
* contract — and the mobile handler, which reads `errorType` directly — keeps
|
|
117
|
+
* working unchanged. Derived from `code` via {@link legacyErrorTypeFor} rather
|
|
118
|
+
* than computed independently, so the two can no longer disagree.
|
|
119
|
+
*/
|
|
120
|
+
export type LegacyErrorType = 'rate_limit' | 'timeout' | 'api_error' | 'context_overflow' | 'stream_error' | 'insufficient_sparks';
|
|
121
|
+
export declare function legacyErrorTypeFor(code: TerminalFailureCode): LegacyErrorType;
|
|
122
|
+
export declare function isTerminalFailureCode(value: unknown): value is TerminalFailureCode;
|
|
123
|
+
/** Inputs to {@link buildTerminalFailure}. `at` is injectable so tests are deterministic. */
|
|
124
|
+
export interface BuildTerminalFailureInput {
|
|
125
|
+
readonly code: TerminalFailureCode;
|
|
126
|
+
readonly operationId: string;
|
|
127
|
+
readonly technical: string;
|
|
128
|
+
readonly detail?: TerminalFailureDetail;
|
|
129
|
+
readonly at?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Construct an envelope. The only supported way to make one — callers that
|
|
133
|
+
* classify domain errors (thrown values, spark state, outcome gates) should do
|
|
134
|
+
* so in their own layer and funnel the result through here.
|
|
135
|
+
*/
|
|
136
|
+
export declare function buildTerminalFailure(input: BuildTerminalFailureInput): TerminalFailure;
|
|
137
|
+
/**
|
|
138
|
+
* Parse an envelope received over the wire.
|
|
139
|
+
*
|
|
140
|
+
* Returns `null` only when there is no envelope to parse (absent or not an
|
|
141
|
+
* object). Anything that IS an envelope but carries an unknown `code` or
|
|
142
|
+
* version is degraded to `agent_internal_error` with `technical` preserved,
|
|
143
|
+
* rather than dropped — a client that silently discarded an error it did not
|
|
144
|
+
* recognise would reintroduce exactly the blank-error problem this contract
|
|
145
|
+
* exists to remove.
|
|
146
|
+
*
|
|
147
|
+
* `onDegraded` is called when that happens so the caller can log it. It is a
|
|
148
|
+
* parameter rather than a `console` call because this package is consumed in
|
|
149
|
+
* both a browser bundle and a pino-logging server.
|
|
150
|
+
*/
|
|
151
|
+
export declare function parseTerminalFailure(value: unknown, onDegraded?: (reason: string) => void): TerminalFailure | null;
|
|
152
|
+
//# sourceMappingURL=terminal-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-failure.d.ts","sourceRoot":"","sources":["../src/terminal-failure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal-failure contract between game-play-agent and its clients.
|
|
3
|
+
*
|
|
4
|
+
* Import path matters: this module is deliberately dependency-free and is
|
|
5
|
+
* published as the `@bitmagic/asset-core/terminal-failure` subpath. Browser
|
|
6
|
+
* consumers (Creator) MUST import that subpath rather than the package root —
|
|
7
|
+
* the root barrel re-exports the asset generators, which drag in zod, the
|
|
8
|
+
* forger client, and node http helpers.
|
|
9
|
+
*
|
|
10
|
+
* A "terminal failure" is the single reason an operation stopped without
|
|
11
|
+
* delivering what the user asked for. Exactly one is produced per failed
|
|
12
|
+
* operation, and it travels on both the live edit stream and the reconnect /
|
|
13
|
+
* replay stream so a client that dropped off mid-run learns the same reason as
|
|
14
|
+
* one that stayed connected.
|
|
15
|
+
*
|
|
16
|
+
* Why this lives in a shared package rather than being duplicated: the previous
|
|
17
|
+
* hand-maintained copies drifted in production — the creator's error union grew
|
|
18
|
+
* an `insufficient_sparks` member that the agent's never had, so the agent could
|
|
19
|
+
* not emit the value the client was waiting for. One enum is enforced by the
|
|
20
|
+
* compiler; two are enforced by memory.
|
|
21
|
+
*
|
|
22
|
+
* What the wire carries is FACTS (code + structured detail). What the wire does
|
|
23
|
+
* NOT carry is copy — no title, no user-facing message, no `retryable` flag.
|
|
24
|
+
* Presentation is derived from `code` by the client, so an old agent can never
|
|
25
|
+
* dictate new product copy and a new code cannot ship without the client
|
|
26
|
+
* deciding how to say it.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Every way an operation can terminally fail.
|
|
30
|
+
*
|
|
31
|
+
* The organising rule is **one code per distinct remedy**, not one per stack
|
|
32
|
+
* trace. `spark_floor_reached` and `spark_service_unavailable` are separate
|
|
33
|
+
* because only the first is fixed by buying sparks; `stream_malformed` and
|
|
34
|
+
* `agent_internal_error` are separate because the second is our bug and should
|
|
35
|
+
* be reported rather than retried forever.
|
|
36
|
+
*
|
|
37
|
+
* Adding a member is a four-step change — see
|
|
38
|
+
* `game-play-agent/docs/terminal-failures.md`. The client's presentation table
|
|
39
|
+
* is asserted exhaustive against this array, so a missing entry fails the build
|
|
40
|
+
* rather than shipping an unexplained error.
|
|
41
|
+
*/
|
|
42
|
+
export const TERMINAL_FAILURE_CODES = [
|
|
43
|
+
/** Balance fell below the mid-processing floor. Buying sparks fixes it. */
|
|
44
|
+
'spark_floor_reached',
|
|
45
|
+
/** The billing service could not be reached. The user's balance is fine. */
|
|
46
|
+
'spark_service_unavailable',
|
|
47
|
+
/** Upstream model provider rate-limited us. */
|
|
48
|
+
'provider_rate_limited',
|
|
49
|
+
/** Upstream model provider returned 5xx / was unavailable. */
|
|
50
|
+
'provider_unavailable',
|
|
51
|
+
/** Accumulated thread history exceeded the model's context window. */
|
|
52
|
+
'context_overflow',
|
|
53
|
+
/** Our own watchdog stopped the run. */
|
|
54
|
+
'agent_timeout',
|
|
55
|
+
/** The model stream itself was malformed — a genuine protocol failure. */
|
|
56
|
+
'stream_malformed',
|
|
57
|
+
/** An unhandled exception escaped the agent. Our bug; cite the operationId. */
|
|
58
|
+
'agent_internal_error',
|
|
59
|
+
/** The agent process died or was evicted mid-run (OOM, restart, reaper). */
|
|
60
|
+
'agent_unavailable',
|
|
61
|
+
/** The operation aged out and can no longer be resumed. */
|
|
62
|
+
'operation_not_found',
|
|
63
|
+
/** The client could not re-establish the stream after retrying. */
|
|
64
|
+
'connection_lost',
|
|
65
|
+
/** Failed before the agent started; nothing was spent. */
|
|
66
|
+
'setup_failed',
|
|
67
|
+
/** Edits were produced and then discarded by an outcome gate. */
|
|
68
|
+
'changes_rolled_back',
|
|
69
|
+
/** The run completed but produced no edits at all. */
|
|
70
|
+
'no_changes_applied',
|
|
71
|
+
/** The user stopped it. Terminal, but deliberately silent. */
|
|
72
|
+
'user_cancelled',
|
|
73
|
+
];
|
|
74
|
+
/** Current envelope version. Bump only on a breaking field change. */
|
|
75
|
+
export const TERMINAL_FAILURE_VERSION = 1;
|
|
76
|
+
/**
|
|
77
|
+
* Map a code onto the legacy union. Exhaustive by construction: adding a code
|
|
78
|
+
* without a legacy value is a compile error, not a runtime surprise.
|
|
79
|
+
*
|
|
80
|
+
* Note `spark_service_unavailable` deliberately maps to `api_error`, not
|
|
81
|
+
* `insufficient_sparks`. A billing outage used to be reported to users as "out
|
|
82
|
+
* of sparks" with a Buy button that could not possibly help.
|
|
83
|
+
*/
|
|
84
|
+
const LEGACY_ERROR_TYPE = {
|
|
85
|
+
spark_floor_reached: 'insufficient_sparks',
|
|
86
|
+
spark_service_unavailable: 'api_error',
|
|
87
|
+
provider_rate_limited: 'rate_limit',
|
|
88
|
+
provider_unavailable: 'api_error',
|
|
89
|
+
context_overflow: 'context_overflow',
|
|
90
|
+
agent_timeout: 'timeout',
|
|
91
|
+
stream_malformed: 'stream_error',
|
|
92
|
+
agent_internal_error: 'stream_error',
|
|
93
|
+
agent_unavailable: 'stream_error',
|
|
94
|
+
operation_not_found: 'stream_error',
|
|
95
|
+
connection_lost: 'stream_error',
|
|
96
|
+
setup_failed: 'stream_error',
|
|
97
|
+
changes_rolled_back: 'stream_error',
|
|
98
|
+
no_changes_applied: 'stream_error',
|
|
99
|
+
// Never emitted as an error frame; present only to keep the map total.
|
|
100
|
+
user_cancelled: 'stream_error',
|
|
101
|
+
};
|
|
102
|
+
export function legacyErrorTypeFor(code) {
|
|
103
|
+
return LEGACY_ERROR_TYPE[code];
|
|
104
|
+
}
|
|
105
|
+
export function isTerminalFailureCode(value) {
|
|
106
|
+
return typeof value === 'string' && TERMINAL_FAILURE_CODES.includes(value);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Construct an envelope. The only supported way to make one — callers that
|
|
110
|
+
* classify domain errors (thrown values, spark state, outcome gates) should do
|
|
111
|
+
* so in their own layer and funnel the result through here.
|
|
112
|
+
*/
|
|
113
|
+
export function buildTerminalFailure(input) {
|
|
114
|
+
const failure = {
|
|
115
|
+
v: TERMINAL_FAILURE_VERSION,
|
|
116
|
+
code: input.code,
|
|
117
|
+
operationId: input.operationId,
|
|
118
|
+
technical: input.technical,
|
|
119
|
+
at: input.at ?? new Date().toISOString(),
|
|
120
|
+
...(input.detail ? { detail: input.detail } : {}),
|
|
121
|
+
};
|
|
122
|
+
return failure;
|
|
123
|
+
}
|
|
124
|
+
function isRecord(value) {
|
|
125
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Required fields per detail arm, by name and primitive type.
|
|
129
|
+
*
|
|
130
|
+
* Only the REQUIRED fields are listed — optional ones are passed through
|
|
131
|
+
* untouched. This exists so a malformed detail is dropped rather than handed to
|
|
132
|
+
* presentation copy, where a missing `balance` would render as the literal
|
|
133
|
+
* string "undefined" in front of a user. Codes with no arm are absent.
|
|
134
|
+
*/
|
|
135
|
+
const REQUIRED_DETAIL_FIELDS = {
|
|
136
|
+
spark_floor_reached: [['balance', 'number'], ['floor', 'number']],
|
|
137
|
+
spark_service_unavailable: [['failureCount', 'number']],
|
|
138
|
+
agent_timeout: [['timeoutSec', 'number'], ['extended', 'boolean']],
|
|
139
|
+
agent_unavailable: [['source', 'string']],
|
|
140
|
+
changes_rolled_back: [['gate', 'string'], ['errors', 'array']],
|
|
141
|
+
no_changes_applied: [['subReason', 'string']],
|
|
142
|
+
};
|
|
143
|
+
function detailMatchesCode(code, detail) {
|
|
144
|
+
if (detail.code !== code)
|
|
145
|
+
return false;
|
|
146
|
+
const required = REQUIRED_DETAIL_FIELDS[code];
|
|
147
|
+
if (!required)
|
|
148
|
+
return true;
|
|
149
|
+
return required.every(([field, kind]) => kind === 'array' ? Array.isArray(detail[field]) : typeof detail[field] === kind);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Parse an envelope received over the wire.
|
|
153
|
+
*
|
|
154
|
+
* Returns `null` only when there is no envelope to parse (absent or not an
|
|
155
|
+
* object). Anything that IS an envelope but carries an unknown `code` or
|
|
156
|
+
* version is degraded to `agent_internal_error` with `technical` preserved,
|
|
157
|
+
* rather than dropped — a client that silently discarded an error it did not
|
|
158
|
+
* recognise would reintroduce exactly the blank-error problem this contract
|
|
159
|
+
* exists to remove.
|
|
160
|
+
*
|
|
161
|
+
* `onDegraded` is called when that happens so the caller can log it. It is a
|
|
162
|
+
* parameter rather than a `console` call because this package is consumed in
|
|
163
|
+
* both a browser bundle and a pino-logging server.
|
|
164
|
+
*/
|
|
165
|
+
export function parseTerminalFailure(value, onDegraded) {
|
|
166
|
+
if (!isRecord(value))
|
|
167
|
+
return null;
|
|
168
|
+
const operationId = typeof value.operationId === 'string' ? value.operationId : '';
|
|
169
|
+
const technical = typeof value.technical === 'string' ? value.technical : '';
|
|
170
|
+
const at = typeof value.at === 'string' ? value.at : new Date().toISOString();
|
|
171
|
+
const version = typeof value.v === 'number' ? value.v : TERMINAL_FAILURE_VERSION;
|
|
172
|
+
if (!isTerminalFailureCode(value.code) || version !== TERMINAL_FAILURE_VERSION) {
|
|
173
|
+
onDegraded?.(`unrecognised terminal failure (code=${String(value.code)}, v=${String(value.v)}) — degrading to agent_internal_error`);
|
|
174
|
+
return {
|
|
175
|
+
v: TERMINAL_FAILURE_VERSION,
|
|
176
|
+
code: 'agent_internal_error',
|
|
177
|
+
operationId,
|
|
178
|
+
technical: technical || `Unrecognised failure code: ${String(value.code)}`,
|
|
179
|
+
at,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
let detail;
|
|
183
|
+
if (isRecord(value.detail)) {
|
|
184
|
+
if (detailMatchesCode(value.code, value.detail)) {
|
|
185
|
+
detail = value.detail;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// The code is still trustworthy, so keep the failure and drop only the
|
|
189
|
+
// facts. Silently keeping malformed detail is what puts "undefined" in
|
|
190
|
+
// front of a user.
|
|
191
|
+
onDegraded?.(`terminal failure ${value.code} carried a malformed detail — dropping it`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
v: TERMINAL_FAILURE_VERSION,
|
|
196
|
+
code: value.code,
|
|
197
|
+
operationId,
|
|
198
|
+
technical,
|
|
199
|
+
at,
|
|
200
|
+
...(detail ? { detail } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=terminal-failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-failure.js","sourceRoot":"","sources":["../src/terminal-failure.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single change to world.json, returned as DATA rather than applied.
|
|
3
|
+
*
|
|
4
|
+
* Returning patches instead of writing them is what lets one generator serve
|
|
5
|
+
* both the pod-backed web lane (which writes into a session directory) and the
|
|
6
|
+
* CLI lane (which writes a local file).
|
|
7
|
+
*
|
|
8
|
+
* `set`/`push` address paths inside `worldProfileData`; `setRoot`/`pushRoot`
|
|
9
|
+
* address paths from the JSON root.
|
|
10
|
+
*/
|
|
11
|
+
export interface DataWorldPatch {
|
|
12
|
+
type: 'set' | 'push' | 'setRoot' | 'pushRoot';
|
|
13
|
+
path: string[];
|
|
14
|
+
value: unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Suppress the game reload this write would otherwise trigger. Use when a
|
|
17
|
+
* later patch in the same batch is the authoritative one that should own the
|
|
18
|
+
* single reload.
|
|
19
|
+
*/
|
|
20
|
+
skipReload?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Replace the array item matching `matchKeys`, or append when none matches.
|
|
24
|
+
*
|
|
25
|
+
* `matchKeys` names fields **of `value`**: `matchKeys: ['id']` means "the item
|
|
26
|
+
* whose `id` equals `value.id`". Every upsert in this codebase is field
|
|
27
|
+
* equality, so naming the fields is enough — the host builds the predicate
|
|
28
|
+
* from this data via `buildUpsertPredicate`, and the function itself never
|
|
29
|
+
* crosses a serialization boundary. That is what makes upsert expressible in a
|
|
30
|
+
* patch a CLI process can receive as JSON.
|
|
31
|
+
*
|
|
32
|
+
* Upsert-by-id is what makes regenerating an asset UPGRADE every placed
|
|
33
|
+
* instance instead of duplicating it — emitting a plain `pushRoot` instead is a
|
|
34
|
+
* behaviour change, not a workaround.
|
|
35
|
+
*
|
|
36
|
+
* Every `matchKeys` field must be a primitive (string, number, boolean, bigint,
|
|
37
|
+
* symbol, or null) on `value`. `buildUpsertPredicate` compares with `===`, so a
|
|
38
|
+
* matchKey naming an object- or array-valued field (e.g. `['position']`,
|
|
39
|
+
* `['fitBox']`) would compare by reference against a freshly-constructed
|
|
40
|
+
* `value` and never match — silently degrading the upsert into an append on
|
|
41
|
+
* every regeneration. `validateWorldPatch` rejects such patches.
|
|
42
|
+
*/
|
|
43
|
+
export interface UpsertRootPatch {
|
|
44
|
+
type: 'upsertRoot';
|
|
45
|
+
path: string[];
|
|
46
|
+
matchKeys: string[];
|
|
47
|
+
value: unknown;
|
|
48
|
+
skipReload?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export type WorldPatch = DataWorldPatch | UpsertRootPatch;
|
|
51
|
+
/**
|
|
52
|
+
* The logging surface a generator needs. Structurally satisfied by a pino
|
|
53
|
+
* logger, so game-play-agent passes its own `createLogger(...)` result directly.
|
|
54
|
+
* asset-core never constructs a logger itself.
|
|
55
|
+
*
|
|
56
|
+
* Each method carries pino's two call shapes. The object-first overload matters
|
|
57
|
+
* for errors: `warn({ err }, msg)` serializes the Error with its stack as a
|
|
58
|
+
* structured field, where `warn(\`${err.message}\`)` throws the stack away and
|
|
59
|
+
* leaves nothing for a log query to key on.
|
|
60
|
+
*/
|
|
61
|
+
export interface CoreLogger {
|
|
62
|
+
info(message: string, ...rest: unknown[]): void;
|
|
63
|
+
info(context: Record<string, unknown>, message: string): void;
|
|
64
|
+
warn(message: string, ...rest: unknown[]): void;
|
|
65
|
+
warn(context: Record<string, unknown>, message: string): void;
|
|
66
|
+
error(message: string, ...rest: unknown[]): void;
|
|
67
|
+
error(context: Record<string, unknown>, message: string): void;
|
|
68
|
+
}
|
|
69
|
+
/** The subset of the Asset Forger client that generators depend on. */
|
|
70
|
+
export interface ForgerClient {
|
|
71
|
+
hasCredentials(): boolean;
|
|
72
|
+
getMissingCredentialsMessage(): string;
|
|
73
|
+
post<T = unknown>(endpoint: string, payload: unknown, loggerName: string): Promise<T>;
|
|
74
|
+
postJobAndWait<T = unknown>(jobsEndpoint: string, payload: unknown, loggerName: string): Promise<T>;
|
|
75
|
+
}
|
|
76
|
+
/** Everything a generator needs from its host. No filesystem, no framework. */
|
|
77
|
+
export interface GenerateDeps {
|
|
78
|
+
forger: ForgerClient;
|
|
79
|
+
/** Human-readable progress line. The web lane forwards it to the SSE stream. */
|
|
80
|
+
onProgress: (message: string) => void;
|
|
81
|
+
logger: CoreLogger;
|
|
82
|
+
}
|
|
83
|
+
/** Fields every generator result carries. Generators extend this with their own. */
|
|
84
|
+
export interface AssetResultBase {
|
|
85
|
+
success: boolean;
|
|
86
|
+
message: string;
|
|
87
|
+
patches: WorldPatch[];
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;IAC9C,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,eAAe,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAChE;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,cAAc,IAAI,OAAO,CAAC;IAC1B,4BAA4B,IAAI,MAAM,CAAC;IACvC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtF,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACrG;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,gFAAgF;IAChF,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Render a zod failure as one line naming each offending field.
|
|
4
|
+
*
|
|
5
|
+
* Generators return this through `fail()`, so a caller in either lane gets the
|
|
6
|
+
* same actionable message — the field name is what makes it actionable.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatZodIssues(error: ZodError): string;
|
|
9
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAEpC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAIvD"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a zod failure as one line naming each offending field.
|
|
3
|
+
*
|
|
4
|
+
* Generators return this through `fail()`, so a caller in either lane gets the
|
|
5
|
+
* same actionable message — the field name is what makes it actionable.
|
|
6
|
+
*/
|
|
7
|
+
export function formatZodIssues(error) {
|
|
8
|
+
return error.issues
|
|
9
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
10
|
+
.join('; ');
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAe;IAC7C,OAAO,KAAK,CAAC,MAAM;SAChB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;SACvE,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UpsertRootPatch, WorldPatch } from './types.js';
|
|
2
|
+
/** Narrow a patch to the upsert kind. */
|
|
3
|
+
export declare function isUpsertRootPatch(patch: WorldPatch): patch is UpsertRootPatch;
|
|
4
|
+
/**
|
|
5
|
+
* Check a patch's internal consistency. Returns an error message, or null when
|
|
6
|
+
* the patch is well-formed.
|
|
7
|
+
*
|
|
8
|
+
* The absent-or-undefined rule is not decoration: with `matchKeys: ['id']` and a
|
|
9
|
+
* `value` whose `id` is missing or explicitly `undefined`, a naive predicate
|
|
10
|
+
* matches the first item that ALSO lacks an id, silently overwriting an
|
|
11
|
+
* unrelated asset. Absent and explicit-`undefined` are treated identically
|
|
12
|
+
* because `JSON.stringify` drops `undefined`-valued keys: a patch built
|
|
13
|
+
* in-process with `value: { id: undefined }` would validate here but arrive in
|
|
14
|
+
* the CLI lane with no `id` key at all — checking `in` alone would accept the
|
|
15
|
+
* patch in one lane and reject the identical patch in the other.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateWorldPatch(patch: WorldPatch): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The equality predicate an upsert patch describes: an item matches when every
|
|
20
|
+
* key in `matchKeys` is present on it and equal to the same field of `value`.
|
|
21
|
+
*
|
|
22
|
+
* Call `validateWorldPatch` first — this assumes a well-formed patch.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildUpsertPredicate(patch: UpsertRootPatch): (item: unknown) => boolean;
|
|
25
|
+
//# sourceMappingURL=world-patch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world-patch.d.ts","sourceRoot":"","sources":["../src/world-patch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9D,yCAAyC;AACzC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,IAAI,eAAe,CAE7E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CA6BnE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CASvF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Narrow a patch to the upsert kind. */
|
|
2
|
+
export function isUpsertRootPatch(patch) {
|
|
3
|
+
return patch.type === 'upsertRoot';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Check a patch's internal consistency. Returns an error message, or null when
|
|
7
|
+
* the patch is well-formed.
|
|
8
|
+
*
|
|
9
|
+
* The absent-or-undefined rule is not decoration: with `matchKeys: ['id']` and a
|
|
10
|
+
* `value` whose `id` is missing or explicitly `undefined`, a naive predicate
|
|
11
|
+
* matches the first item that ALSO lacks an id, silently overwriting an
|
|
12
|
+
* unrelated asset. Absent and explicit-`undefined` are treated identically
|
|
13
|
+
* because `JSON.stringify` drops `undefined`-valued keys: a patch built
|
|
14
|
+
* in-process with `value: { id: undefined }` would validate here but arrive in
|
|
15
|
+
* the CLI lane with no `id` key at all — checking `in` alone would accept the
|
|
16
|
+
* patch in one lane and reject the identical patch in the other.
|
|
17
|
+
*/
|
|
18
|
+
export function validateWorldPatch(patch) {
|
|
19
|
+
if (!isUpsertRootPatch(patch))
|
|
20
|
+
return null;
|
|
21
|
+
const where = `upsertRoot at ${patch.path.join('.')}`;
|
|
22
|
+
if (patch.matchKeys.length === 0) {
|
|
23
|
+
return `${where}: matchKeys must not be empty`;
|
|
24
|
+
}
|
|
25
|
+
const { value } = patch;
|
|
26
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
27
|
+
return `${where}: value must be a non-null object to match on its fields`;
|
|
28
|
+
}
|
|
29
|
+
const record = value;
|
|
30
|
+
const missing = patch.matchKeys.filter((key) => !(key in record) || record[key] === undefined);
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
return `${where}: matchKeys [${missing.join(', ')}] absent or undefined on value — matching on undefined would overwrite an unrelated item`;
|
|
33
|
+
}
|
|
34
|
+
const nonPrimitive = patch.matchKeys.filter((key) => {
|
|
35
|
+
const v = record[key];
|
|
36
|
+
return v !== null && (typeof v === 'object' || typeof v === 'function');
|
|
37
|
+
});
|
|
38
|
+
if (nonPrimitive.length > 0) {
|
|
39
|
+
return `${where}: matchKeys [${nonPrimitive.join(', ')}] are non-primitive on value — reference equality would append a duplicate instead of replacing the existing item`;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The equality predicate an upsert patch describes: an item matches when every
|
|
45
|
+
* key in `matchKeys` is present on it and equal to the same field of `value`.
|
|
46
|
+
*
|
|
47
|
+
* Call `validateWorldPatch` first — this assumes a well-formed patch.
|
|
48
|
+
*/
|
|
49
|
+
export function buildUpsertPredicate(patch) {
|
|
50
|
+
const target = patch.value;
|
|
51
|
+
const keys = patch.matchKeys;
|
|
52
|
+
return (item) => {
|
|
53
|
+
if (typeof item !== 'object' || item === null)
|
|
54
|
+
return false;
|
|
55
|
+
const record = item;
|
|
56
|
+
return keys.every((key) => key in record && record[key] === target[key]);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=world-patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world-patch.js","sourceRoot":"","sources":["../src/world-patch.ts"],"names":[],"mappings":"AAEA,yCAAyC;AACzC,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,OAAO,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE3C,MAAM,KAAK,GAAG,iBAAiB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAEtD,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,GAAG,KAAK,+BAA+B,CAAC;IACjD,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,GAAG,KAAK,0DAA0D,CAAC;IAC5E,CAAC;IAED,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC;IAC/F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,GAAG,KAAK,gBAAgB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,0FAA0F,CAAC;IAC9I,CAAC;IAED,MAAM,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;QAClD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,GAAG,KAAK,gBAAgB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,mHAAmH,CAAC;IAC5K,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAsB;IACzD,MAAM,MAAM,GAAG,KAAK,CAAC,KAAgC,CAAC;IACtD,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC;IAE7B,OAAO,CAAC,IAAa,EAAW,EAAE;QAChC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC5D,MAAM,MAAM,GAAG,IAA+B,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC;AACJ,CAAC"}
|