@randomplay/data 0.0.4 → 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/index.mjs CHANGED
@@ -84,6 +84,18 @@ function buildSourceDocument(descriptor, options) {
84
84
  if (options.licenseNote !== void 0) candidate.licenseNote = options.licenseNote;
85
85
  return sourceDocumentSchema.parse(candidate);
86
86
  }
87
+ function buildSourceDocumentFromRegistryEntry(descriptor, registryEntry, options) {
88
+ if (registryEntry.sourceId !== descriptor.id) throw new Error(`source registry entry ${registryEntry.sourceId} does not match descriptor ${descriptor.id}`);
89
+ if (!registryEntry.contentHash.startsWith("sha256:")) throw new Error(`${registryEntry.sourceId}: source registry contentHash must be sha256-prefixed`);
90
+ if (registryEntry.liveVersionRef === "manifest.zzz.live" && !registryEntry.approvedLiveVersions?.includes(registryEntry.configuredLiveVersion)) throw new Error(`${registryEntry.sourceId}: approvedLiveVersions must include configuredLiveVersion`);
91
+ const sourceDocumentOptions = {
92
+ ...options,
93
+ sourceVersion: registryEntry.configuredLiveVersion
94
+ };
95
+ if (registryEntry.fetchedAt !== void 0) sourceDocumentOptions.fetchedAt = registryEntry.fetchedAt;
96
+ if (descriptor.fileNameHint !== void 0) sourceDocumentOptions.fileName = descriptor.fileNameHint;
97
+ return buildSourceDocument(descriptor, sourceDocumentOptions);
98
+ }
87
99
  function buildSourceRef(descriptor, options = {}) {
88
100
  const candidate = { sourceId: descriptor.id };
89
101
  if (options.sourceVersion !== void 0) candidate.sourceVersion = options.sourceVersion;
@@ -91,9 +103,901 @@ function buildSourceRef(descriptor, options = {}) {
91
103
  if (options.dataPath !== void 0) candidate.dataPath = options.dataPath;
92
104
  return sourceRefSchema.parse(candidate);
93
105
  }
106
+ function buildSourceRefForParsedRecord(batch, record) {
107
+ if (record.sourceAnchor === void 0 || record.sourceAnchor.length === 0) throw new Error(`${record.id}: sourceAnchor is required for SourceRef emission`);
108
+ if (record.dataPath === void 0 || record.dataPath.length === 0) throw new Error(`${record.id}: dataPath is required for SourceRef emission`);
109
+ return sourceRefSchema.parse({
110
+ sourceId: batch.sourceId,
111
+ sourceVersion: batch.sourceVersion,
112
+ sourceAnchor: record.sourceAnchor,
113
+ dataPath: record.dataPath
114
+ });
115
+ }
116
+ function buildSourceRefsForParsedBatch(batch) {
117
+ return batch.records.map((record) => buildSourceRefForParsedRecord(batch, record));
118
+ }
119
+ //#endregion
120
+ //#region src/nanoka.ts
121
+ const NANOKA_SOURCE_ID = "nanoka-zzz";
122
+ const NANOKA_PARSER_VERSION = "nanoka-source-v0.1.0";
123
+ function assertNanokaSnapshotManifest(manifest) {
124
+ if (manifest.schemaVersion !== "nanoka-fetch-manifest-v1") throw new Error("Unexpected nanoka snapshot schemaVersion");
125
+ if (manifest.sourceId !== "nanoka-zzz") throw new Error(`Unexpected nanoka sourceId: ${manifest.sourceId}`);
126
+ if (manifest.parserVersion !== "nanoka-source-v0.1.0") throw new Error(`Unexpected nanoka parserVersion: ${manifest.parserVersion}`);
127
+ if (manifest.formalLivePolicy.liveVersionRef !== "manifest.zzz.live") throw new Error("nanoka snapshot must be tied to manifest.zzz.live");
128
+ if (manifest.snapshotId !== manifest.formalLivePolicy.configuredLiveVersion) throw new Error("nanoka snapshotId must match configuredLiveVersion");
129
+ assertNanokaUrlPolicy(manifest);
130
+ const assetIds = manifest.assets.map((asset) => asset.id);
131
+ if (new Set(assetIds).size !== assetIds.length) throw new Error("nanoka snapshot asset ids must be unique");
132
+ if (!assetIds.includes("manifest")) throw new Error("nanoka snapshot must retain manifest.json");
133
+ if (!assetIds.includes("boss-index")) throw new Error("nanoka snapshot must retain boss.json index");
134
+ for (const asset of manifest.assets) {
135
+ if (asset.sourceVersion !== manifest.snapshotId) throw new Error(`${asset.id}: sourceVersion must match snapshotId`);
136
+ assertNanokaAssetUrlAllowed(manifest, asset);
137
+ if (asset.entityType === "sourceManifest" && asset.approvedForCleanedOutput) throw new Error("source manifest gates must not be marked cleaned-output evidence");
138
+ }
139
+ }
140
+ function assertNanokaUrlPolicy(manifest) {
141
+ if (manifest.urlPolicy.manifestUrl !== "https://static.nanoka.cc/manifest.json") throw new Error("nanoka urlPolicy manifestUrl must point to static.nanoka.cc manifest.json");
142
+ if (!manifest.urlPolicy.approvedIndexUrls.includes(`https://static.nanoka.cc/zzz/${manifest.snapshotId}/boss.json`)) throw new Error("nanoka urlPolicy must approve the live boss index");
143
+ for (const forbiddenIndexName of [
144
+ "beta",
145
+ "preview",
146
+ "leak",
147
+ "datamine"
148
+ ]) if (!manifest.urlPolicy.forbiddenIndexNames.includes(forbiddenIndexName)) throw new Error(`nanoka urlPolicy must forbid ${forbiddenIndexName}.json indexes`);
149
+ }
150
+ function assertNanokaAssetUrlAllowed(manifest, asset) {
151
+ let url;
152
+ try {
153
+ url = new URL(asset.url);
154
+ } catch {
155
+ throw new Error(`${asset.id}: invalid nanoka asset URL`);
156
+ }
157
+ if (url.origin !== "https://static.nanoka.cc") throw new Error(`${asset.id}: asset URL is not allowed by nanoka urlPolicy`);
158
+ if (asset.entityType === "sourceManifest") {
159
+ if (asset.url !== manifest.urlPolicy.manifestUrl) throw new Error(`${asset.id}: asset URL is not allowed by nanoka urlPolicy`);
160
+ return;
161
+ }
162
+ for (const forbiddenIndexName of manifest.urlPolicy.forbiddenIndexNames) if (url.pathname === `/zzz/${manifest.snapshotId}/${forbiddenIndexName}.json`) throw new Error(`${asset.id}: forbidden nanoka route in snapshot`);
163
+ const fileName = url.pathname.split("/").at(-1);
164
+ if (fileName !== void 0 && manifest.urlPolicy.forbiddenIndexNames.includes(fileName.replace(/\.json$/, ""))) throw new Error(`${asset.id}: forbidden nanoka route in snapshot`);
165
+ if (asset.entityType === "bossIndex") {
166
+ if (!manifest.urlPolicy.approvedIndexUrls.includes(asset.url)) throw new Error(`${asset.id}: asset URL is not allowed by nanoka urlPolicy`);
167
+ return;
168
+ }
169
+ if (!new RegExp(`^/zzz/${escapeRegExp(manifest.snapshotId)}/(?:zh|en|ja|ko)/(?:character|bangboo|monster|weapon|equipment|boss)/\\d+\\.json$`).test(url.pathname)) throw new Error(`${asset.id}: asset URL is not allowed by nanoka urlPolicy`);
170
+ }
171
+ function escapeRegExp(value) {
172
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
173
+ }
174
+ function nanokaSnapshotRecords(manifest) {
175
+ assertNanokaSnapshotManifest(manifest);
176
+ return manifest.assets.map((asset) => ({
177
+ id: asset.id,
178
+ sourceAnchor: asset.url,
179
+ dataPath: asset.localPath,
180
+ raw: {
181
+ entityType: asset.entityType,
182
+ entityId: asset.entityId,
183
+ language: asset.language,
184
+ sha256: asset.sha256,
185
+ approvedForCleanedOutput: asset.approvedForCleanedOutput,
186
+ evidenceUse: asset.evidenceUse
187
+ }
188
+ }));
189
+ }
190
+ function createNanokaSnapshotAdapter(options) {
191
+ assertNanokaSnapshotManifest(options.manifest);
192
+ return {
193
+ sourceId: NANOKA_SOURCE_ID,
194
+ async fetch(context) {
195
+ return {
196
+ sourceId: NANOKA_SOURCE_ID,
197
+ sourceVersion: options.manifest.snapshotId,
198
+ fetchedAt: context.now,
199
+ payload: options.manifest,
200
+ fileName: `data/source/raw/nanoka/zzz/${options.manifest.snapshotId}/fetch-manifest.json`,
201
+ contentType: "application/json"
202
+ };
203
+ },
204
+ async parse(raw) {
205
+ if (raw.sourceId !== "nanoka-zzz") throw new Error(`nanoka adapter raw sourceId mismatch: ${raw.sourceId}`);
206
+ if (raw.sourceVersion !== raw.payload.snapshotId) throw new Error("nanoka adapter raw sourceVersion must match payload snapshotId");
207
+ assertNanokaSnapshotManifest(raw.payload);
208
+ return {
209
+ sourceId: NANOKA_SOURCE_ID,
210
+ sourceVersion: raw.sourceVersion,
211
+ formalDataReady: false,
212
+ records: nanokaSnapshotRecords(raw.payload),
213
+ notes: ["Nanoka snapshot adapter is source-gate only in this slice.", "Formal cleaned data promotion waits for Phase 2 normalization and semantic mapping."]
214
+ };
215
+ }
216
+ };
217
+ }
218
+ //#endregion
219
+ //#region src/nanoka-bangboo-element.ts
220
+ const attributeLabelMap = {
221
+ 火: "fire",
222
+ 电: "electric",
223
+ 冰: "ice",
224
+ 物理: "physical",
225
+ 以太: "ether",
226
+ 烈霜: "frost",
227
+ 玄墨: "auricInk"
228
+ };
229
+ const damageAttributePattern = /造成[^。]*?(<color=[^>]+>(火|电|冰|物理|以太|烈霜|玄墨)属性伤害<\/color>)/g;
230
+ function deriveNanokaBangbooElement(source, options) {
231
+ const sourceBangbooId = requiredFinite$3(source.id, "id");
232
+ if (sourceBangbooId !== options.bangbooId) throw new Error(`nanoka Bangboo element id mismatch: source id ${sourceBangbooId} does not match requested Bangboo ${options.bangbooId}`);
233
+ const skills = requiredObject$2(source.skill, "skill");
234
+ const evidence = [];
235
+ for (const skillKey of Object.keys(skills).sort()) {
236
+ const levels = requiredObject$2(skills[skillKey]?.level, `skill.${skillKey}.level`);
237
+ for (const levelKey of sortedNumericKeys$1(levels, `skill.${skillKey}.level`)) {
238
+ const level = levels[levelKey];
239
+ const desc = requiredString$3(level.desc, `skill.${skillKey}.level.${levelKey}.desc`);
240
+ const sourceName = requiredString$3(level.name, `skill.${skillKey}.level.${levelKey}.name`);
241
+ for (const match of desc.matchAll(damageAttributePattern)) {
242
+ const matchedText = match[1];
243
+ const rawLabel = match[2];
244
+ if (matchedText === void 0 || rawLabel === void 0) continue;
245
+ evidence.push({
246
+ skillKey,
247
+ level: Number(levelKey),
248
+ sourceName,
249
+ rawLabel,
250
+ attribute: mapAttributeLabel(rawLabel, `skill.${skillKey}.level.${levelKey}.desc`),
251
+ matchedText,
252
+ sourcePath: `/skill/${skillKey}/level/${levelKey}/desc`
253
+ });
254
+ }
255
+ }
256
+ }
257
+ if (evidence.length === 0) throw new Error("Missing nanoka Bangboo element damage text in skill descriptions");
258
+ const attributes = new Set(evidence.map((item) => item.attribute));
259
+ if (attributes.size !== 1) throw new Error(`Conflicting nanoka Bangboo element evidence: ${[...attributes].join(", ")}`);
260
+ return {
261
+ sourceVersion: options.sourceVersion,
262
+ bangbooId: options.bangbooId,
263
+ attribute: evidence[0].attribute,
264
+ evidence,
265
+ runtimeCutoverReady: false
266
+ };
267
+ }
268
+ function mapAttributeLabel(value, path) {
269
+ const attribute = attributeLabelMap[value];
270
+ if (attribute === void 0) throw new Error(`Unmapped nanoka Bangboo element label ${value} at ${path}`);
271
+ return attribute;
272
+ }
273
+ function sortedNumericKeys$1(value, path) {
274
+ const keys = Object.keys(value);
275
+ if (keys.length === 0) throw new Error(`Missing nanoka Bangboo level entries ${path}`);
276
+ for (const key of keys) if (!/^\d+$/.test(key)) throw new Error(`Invalid numeric key ${path}.${key}`);
277
+ return keys.sort((left, right) => Number(left) - Number(right));
278
+ }
279
+ function requiredObject$2(value, path) {
280
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Missing object nanoka Bangboo element field ${path}`);
281
+ return value;
282
+ }
283
+ function requiredString$3(value, path) {
284
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Missing text nanoka Bangboo element field ${path}`);
285
+ return value;
286
+ }
287
+ function requiredFinite$3(value, path) {
288
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka Bangboo element field ${path}`);
289
+ return value;
290
+ }
291
+ //#endregion
292
+ //#region src/nanoka-da.ts
293
+ const weaknessCodeMap = {
294
+ "200": "physical",
295
+ "201": "fire",
296
+ "202": "ice",
297
+ "203": "electric",
298
+ "204": "wind",
299
+ "205": "ether"
300
+ };
301
+ function deriveNanokaDeadlyAssaultPeriod(index, detail, options) {
302
+ const periodId = String(detail.id);
303
+ const indexEntry = index[periodId];
304
+ if (indexEntry === void 0) throw new Error(`Missing Deadly Assault period ${periodId} from nanoka boss index`);
305
+ const beginAt = normalizeNanokaChinaDate(detail.begin_time, "begin_time");
306
+ const endAt = normalizeNanokaChinaDate(detail.end_time, "end_time");
307
+ const configuredLiveSnapshotDate = parseDate(options.configuredLiveSnapshotDate, "configuredLiveSnapshotDate");
308
+ if (Date.parse(beginAt) > configuredLiveSnapshotDate.getTime()) throw new Error(`Deadly Assault period ${periodId} begins after configured live snapshot date`);
309
+ if (normalizeNanokaChinaDate(indexEntry.begin, "index.begin") !== beginAt) throw new Error(`Deadly Assault period ${periodId} begin_time does not match boss index`);
310
+ if (normalizeNanokaChinaDate(indexEntry.end, "index.end") !== endAt) throw new Error(`Deadly Assault period ${periodId} end_time does not match boss index`);
311
+ return {
312
+ id: periodId,
313
+ title: requiredString$2(detail.name, "name"),
314
+ sourceVersion: options.sourceVersion,
315
+ beginAt,
316
+ endAt,
317
+ zones: sortedEntries(detail.zone).map(([zoneId, zone]) => normalizeZone(zoneId, zone)),
318
+ bossAdjustments: sortedEntries(detail.boss_adjust).map(([id, adjustment]) => normalizeBossAdjustment(id, adjustment)),
319
+ runtimeCutoverReady: false
320
+ };
321
+ }
322
+ function normalizeZone(zoneId, zone) {
323
+ return {
324
+ zoneId,
325
+ stageNumber: requiredFinite$2(zone.stage_num, `zone.${zoneId}.stage_num`),
326
+ name: requiredString$2(zone.name, `zone.${zoneId}.name`),
327
+ monsterLevel: requiredFinite$2(zone.monster_level, `zone.${zoneId}.monster_level`),
328
+ goalType: requiredFinite$2(zone.goal_type, `zone.${zoneId}.goal_type`),
329
+ rankGoals: {
330
+ s: requiredFinite$2(zone.s_rank_goal, `zone.${zoneId}.s_rank_goal`),
331
+ a: requiredFinite$2(zone.a_rank_goal, `zone.${zoneId}.a_rank_goal`),
332
+ b: requiredFinite$2(zone.b_rank_goal, `zone.${zoneId}.b_rank_goal`)
333
+ },
334
+ layerBuffs: sortedEntries(zone.layer_buff).map(([id, buff]) => normalizeBuff(id, buff)),
335
+ selectableBuffs: sortedEntries(zone.selectable_buff).map(([id, buff]) => normalizeBuff(id, buff)),
336
+ rooms: sortedEntries(zone.layer_room).map(([roomId, room]) => ({
337
+ roomId,
338
+ waves: requiredFinite$2(room.waves_num, `zone.${zoneId}.layer_room.${roomId}.waves_num`),
339
+ monsters: sortedEntries(room.monster_list).map(([slotId, monster]) => normalizeMonster(slotId, monster, room.monster_weakness))
340
+ }))
341
+ };
342
+ }
343
+ function normalizeBuff(id, buff) {
344
+ return {
345
+ id,
346
+ title: buff.title ?? "",
347
+ description: requiredString$2(buff.desc, `buff.${id}.desc`)
348
+ };
349
+ }
350
+ function normalizeMonster(slotId, monster, weakness) {
351
+ return {
352
+ slotId,
353
+ monsterId: requiredFinite$2(monster.id, `monster.${slotId}.id`),
354
+ name: requiredString$2(monster.name, `monster.${slotId}.name`),
355
+ elementProfile: monster.element,
356
+ weaknessAttributes: Object.keys(weakness).map((code) => weaknessCodeMap[code]).filter((attribute) => attribute !== void 0),
357
+ stats: {
358
+ hp: requiredFinite$2(monster.stats.hp, `monster.${slotId}.stats.hp`),
359
+ attack: requiredFinite$2(monster.stats.attack, `monster.${slotId}.stats.attack`),
360
+ defense: requiredFinite$2(monster.stats.defence, `monster.${slotId}.stats.defence`),
361
+ daze: requiredFinite$2(monster.stats.stun, `monster.${slotId}.stats.stun`),
362
+ anomalyBuildupResistance: requiredFinite$2(monster.stats.attribute_infliction, `monster.${slotId}.stats.attribute_infliction`)
363
+ }
364
+ };
365
+ }
366
+ function normalizeBossAdjustment(id, adjustment) {
367
+ return {
368
+ id,
369
+ hpAdjustmentRaw: requiredFinite$2(adjustment.hp, `boss_adjust.${id}.hp`),
370
+ attackAdjustmentRaw: requiredFinite$2(adjustment.atk, `boss_adjust.${id}.atk`),
371
+ operationScorePoints: requiredFinite$2(adjustment.points, `boss_adjust.${id}.points`)
372
+ };
373
+ }
374
+ function normalizeNanokaChinaDate(value, path) {
375
+ const match = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(value);
376
+ if (match === null) throw new Error(`Invalid nanoka date ${path}`);
377
+ return `${match[1]}T${match[2]}+08:00`;
378
+ }
379
+ function parseDate(value, path) {
380
+ const date = new Date(value);
381
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid date ${path}`);
382
+ return date;
383
+ }
384
+ function requiredString$2(value, path) {
385
+ if (typeof value !== "string") throw new Error(`Missing nanoka Deadly Assault text field ${path}`);
386
+ return value;
387
+ }
388
+ function requiredFinite$2(value, path) {
389
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka Deadly Assault field ${path}`);
390
+ return value;
391
+ }
392
+ function sortedEntries(record) {
393
+ return Object.entries(record).sort(([left], [right]) => left.localeCompare(right, "en", { numeric: true }));
394
+ }
395
+ //#endregion
396
+ //#region src/nanoka-enemy.ts
397
+ const enemyAttributes = new Set([
398
+ "physical",
399
+ "fire",
400
+ "ice",
401
+ "electric",
402
+ "ether",
403
+ "wind"
404
+ ]);
405
+ function deriveNanokaEnemyVariantMapping(detail, spec) {
406
+ if (detail.id !== spec.detailId) throw new Error(`${spec.cleanedEnemyId}: nanoka monster detail id mismatch`);
407
+ if (requiredFinite$1(detail.monster_id, `${spec.cleanedEnemyId}.monster_id`) !== spec.monsterInfoId) throw new Error(`${spec.cleanedEnemyId}: nanoka monster_info id mismatch`);
408
+ const info = detail.monster_info?.[String(spec.monsterInfoId)];
409
+ if (info === void 0) throw new Error(`${spec.cleanedEnemyId}: missing monster_info.${spec.monsterInfoId}`);
410
+ const name = requiredString$1(detail.name, `${spec.cleanedEnemyId}.name`);
411
+ if (name !== spec.expectedName) throw new Error(`${spec.cleanedEnemyId}: nanoka monster name mismatch`);
412
+ const codeName = requiredString$1(info.code_name, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.code_name`);
413
+ if (codeName !== spec.expectedCodeName) throw new Error(`${spec.cleanedEnemyId}: nanoka monster_info code_name mismatch`);
414
+ const tags = requiredStringArray(info.tag, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.tag`);
415
+ for (const tag of spec.requiredTags) if (!tags.includes(tag)) throw new Error(`${spec.cleanedEnemyId}: missing required monster_info tag ${tag}`);
416
+ const stats = requiredObject$1(info.stats, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats`);
417
+ return {
418
+ cleanedEnemyId: spec.cleanedEnemyId,
419
+ sourceVersion: spec.sourceVersion,
420
+ nanokaDetailId: detail.id,
421
+ nanokaMonsterInfoId: spec.monsterInfoId,
422
+ nanokaName: name,
423
+ nanokaCodeName: codeName,
424
+ nanokaGroupId: requiredFinite$1(detail.group_id, `${spec.cleanedEnemyId}.group_id`),
425
+ infoType: requiredString$1(info.type, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.type`),
426
+ tags,
427
+ goldenAnchors: [...spec.goldenAnchors],
428
+ variantSelectionRule: "detail.monster_id -> monster_info[monster_id]",
429
+ statsRaw: {
430
+ hp: requiredFinite$1(stats.hp, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.hp`),
431
+ attack: requiredFinite$1(stats.attack, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.attack`),
432
+ defense: requiredFinite$1(stats.defence, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.defence`),
433
+ daze: requiredFinite$1(stats.stun, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.stun`),
434
+ autoRecoverRate: requiredFinite$1(stats.auto_recover_rate, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.auto_recover_rate`),
435
+ baseBuildupRatio: requiredFinite$1(stats.base_buildup_ratio, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.stats.base_buildup_ratio`)
436
+ },
437
+ elementProfile: normalizeElementProfile(info.element, `${spec.cleanedEnemyId}.monster_info.${spec.monsterInfoId}.element`),
438
+ runtimeCutoverReady: false
439
+ };
440
+ }
441
+ function deriveNanokaEnemyVariantMappings(details, specs) {
442
+ const detailsById = new Map(details.map((detail) => [detail.id, detail]));
443
+ return specs.map((spec) => {
444
+ const detail = detailsById.get(spec.detailId);
445
+ if (detail === void 0) throw new Error(`${spec.cleanedEnemyId}: missing nanoka monster detail ${spec.detailId}`);
446
+ return deriveNanokaEnemyVariantMapping(detail, spec);
447
+ });
448
+ }
449
+ function normalizeElementProfile(value, path) {
450
+ const record = requiredObject$1(value, path);
451
+ const result = {};
452
+ for (const [key, raw] of Object.entries(record)) {
453
+ if (!enemyAttributes.has(key)) continue;
454
+ result[key] = requiredFinite$1(raw, `${path}.${key}`);
455
+ }
456
+ return result;
457
+ }
458
+ function requiredObject$1(value, path) {
459
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Missing object nanoka enemy field ${path}`);
460
+ return value;
461
+ }
462
+ function requiredString$1(value, path) {
463
+ if (typeof value !== "string") throw new Error(`Missing nanoka enemy text field ${path}`);
464
+ return value;
465
+ }
466
+ function requiredStringArray(value, path) {
467
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error(`Missing nanoka enemy string array field ${path}`);
468
+ return value;
469
+ }
470
+ function requiredFinite$1(value, path) {
471
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka enemy field ${path}`);
472
+ return value;
473
+ }
474
+ //#endregion
475
+ //#region src/nanoka-panel.ts
476
+ function deriveNanokaPanelValue(source, rule, options = {}) {
477
+ const promotionPhase = options.promotionPhase ?? "6";
478
+ const level = options.level ?? 60;
479
+ const phase = source.level[promotionPhase];
480
+ if (phase === void 0) throw new Error(`Missing nanoka level phase ${promotionPhase}`);
481
+ const base = requiredNumber$1(source.stats[rule.baseKey], `stats.${rule.baseKey}`);
482
+ const promotion = requiredNumber$1(phase[rule.levelKey], `level.${promotionPhase}.${rule.levelKey}`);
483
+ const growth = rule.growthKey === void 0 ? 0 : requiredNumber$1(source.stats[rule.growthKey], `stats.${rule.growthKey}`) * (level - 1) / 1e4;
484
+ return base + promotion + growth;
485
+ }
486
+ function requiredNumber$1(value, path) {
487
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka panel field ${path}`);
488
+ return value;
489
+ }
490
+ //#endregion
491
+ //#region src/nanoka-patch-history.ts
492
+ function deriveNanokaSnapshotDiffHistory(inputs, options) {
493
+ assertApprovedSnapshotDiffInputs(inputs, options);
494
+ const inputsByVersion = new Map(inputs.map((input) => [input.sourceVersion, input]));
495
+ const orderedInputs = options.approvedLiveVersions.map((version) => inputsByVersion.get(version)).filter((input) => input !== void 0);
496
+ const comparedPairs = [];
497
+ for (let index = 1; index < orderedInputs.length; index += 1) {
498
+ const from = orderedInputs[index - 1];
499
+ const to = orderedInputs[index];
500
+ comparedPairs.push({
501
+ fromVersion: from.sourceVersion,
502
+ toVersion: to.sourceVersion,
503
+ fromContentHash: from.contentHash,
504
+ toContentHash: to.contentHash,
505
+ changes: diffNumericLeaves(from.records, to.records)
506
+ });
507
+ }
508
+ return {
509
+ schemaVersion: "nanoka-snapshot-diff-history/v0.1",
510
+ sourceId: options.sourceId,
511
+ generatedAt: options.generatedAt,
512
+ diffKind: "snapshot-derived-numeric-diff",
513
+ ...options.latestResearchVersion === void 0 ? {} : { latestResearchVersion: options.latestResearchVersion },
514
+ approvedLiveVersions: [...options.approvedLiveVersions],
515
+ comparedPairs,
516
+ officialPatchNoteText: {
517
+ status: "not-found",
518
+ decision: "D-20 R4.a"
519
+ },
520
+ runtimeCutoverReady: false
521
+ };
522
+ }
523
+ function assertApprovedSnapshotDiffInputs(inputs, options) {
524
+ const approved = new Set(options.approvedLiveVersions);
525
+ for (const input of inputs) {
526
+ if (!approved.has(input.sourceVersion)) throw new Error(`snapshot-diff input ${input.sourceVersion} is not in approvedLiveVersions`);
527
+ if (input.sourceVersion === options.latestResearchVersion) throw new Error(`snapshot-diff input ${input.sourceVersion} is latest research-only`);
528
+ if (!/^sha256:[a-f0-9]{64}$/.test(input.contentHash)) throw new Error(`snapshot-diff input ${input.sourceVersion} must include sha256 contentHash`);
529
+ }
530
+ }
531
+ function diffNumericLeaves(from, to) {
532
+ const fromLeaves = numericLeaves(from);
533
+ const toLeaves = numericLeaves(to);
534
+ const paths = [...new Set([...fromLeaves.keys(), ...toLeaves.keys()])].sort((left, right) => left.localeCompare(right, "en", { numeric: true }));
535
+ const changes = [];
536
+ for (const path of paths) {
537
+ const before = fromLeaves.get(path);
538
+ const after = toLeaves.get(path);
539
+ if (before === void 0 && after !== void 0) changes.push({
540
+ kind: "new",
541
+ path,
542
+ after
543
+ });
544
+ else if (before !== void 0 && after === void 0) changes.push({
545
+ kind: "missing",
546
+ path,
547
+ before
548
+ });
549
+ else if (before !== void 0 && after !== void 0 && before !== after) changes.push({
550
+ kind: "changed",
551
+ path,
552
+ before,
553
+ after
554
+ });
555
+ }
556
+ return changes;
557
+ }
558
+ function numericLeaves(value, path = "") {
559
+ const leaves = /* @__PURE__ */ new Map();
560
+ collectNumericLeaves(value, path, leaves);
561
+ return leaves;
562
+ }
563
+ function collectNumericLeaves(value, path, leaves) {
564
+ if (typeof value === "number" && Number.isFinite(value)) {
565
+ leaves.set(path || "/", value);
566
+ return;
567
+ }
568
+ if (Array.isArray(value)) {
569
+ value.forEach((item, index) => collectNumericLeaves(item, `${path}/${index}`, leaves));
570
+ return;
571
+ }
572
+ if (typeof value === "object" && value !== null) for (const [key, item] of Object.entries(value)) collectNumericLeaves(item, `${path}/${escapeJsonPointer(key)}`, leaves);
573
+ }
574
+ function escapeJsonPointer(value) {
575
+ return value.replace(/~/g, "~0").replace(/\//g, "~1");
576
+ }
577
+ //#endregion
578
+ //#region src/nanoka-promotion-extra.ts
579
+ const promotionExtraPropRules = {
580
+ 11101: {
581
+ canonicalField: "maxHp",
582
+ unitRule: "raw"
583
+ },
584
+ 12101: {
585
+ canonicalField: "attack",
586
+ unitRule: "raw"
587
+ },
588
+ 20101: {
589
+ canonicalField: "critRate",
590
+ unitRule: "basis-points-to-ratio"
591
+ }
592
+ };
593
+ function deriveNanokaPromotionExtraStats(source, options) {
594
+ const sourceAgentId = requiredFinite(source.id, "id");
595
+ if (sourceAgentId !== options.agentId) throw new Error(`nanoka promotion extra agent id mismatch: source id ${sourceAgentId} does not match requested agent ${options.agentId}`);
596
+ const extraLevels = requiredObject(source.extra_level, "extra_level");
597
+ const stats = [];
598
+ for (const phaseKey of sortedNumericKeys(extraLevels, "extra_level")) {
599
+ const phaseRaw = extraLevels[phaseKey];
600
+ const phase = Number(phaseKey);
601
+ const maxLevel = requiredFinite(phaseRaw.max_level, `extra_level.${phaseKey}.max_level`);
602
+ const extras = requiredObject(phaseRaw.extra, `extra_level.${phaseKey}.extra`);
603
+ for (const propKey of sortedNumericKeys(extras, `extra_level.${phaseKey}.extra`)) {
604
+ const rawStat = extras[propKey];
605
+ const prop = requiredFinite(rawStat.prop, `extra_level.${phaseKey}.extra.${propKey}.prop`);
606
+ if (prop !== Number(propKey)) throw new Error(`extra_level.${phaseKey}.extra.${propKey}: prop id mismatch`);
607
+ const rule = promotionExtraPropRules[prop];
608
+ if (rule === void 0) throw new Error(`extra_level.${phaseKey}.extra.${propKey}: unmapped promotion extra prop ${prop}`);
609
+ const rawValue = requiredFinite(rawStat.value, `extra_level.${phaseKey}.extra.${propKey}.value`);
610
+ stats.push({
611
+ phase,
612
+ maxLevel,
613
+ prop,
614
+ canonicalField: rule.canonicalField,
615
+ sourceName: requiredString(rawStat.name, `extra_level.${phaseKey}.extra.${propKey}.name`),
616
+ rawValue,
617
+ normalizedValue: normalizePromotionExtraValue(rawValue, rule.unitRule),
618
+ unitRule: rule.unitRule,
619
+ sourcePath: `/extra_level/${phaseKey}/extra/${propKey}`
620
+ });
621
+ }
622
+ }
623
+ return {
624
+ sourceVersion: options.sourceVersion,
625
+ agentId: options.agentId,
626
+ stats,
627
+ runtimeCutoverReady: false
628
+ };
629
+ }
630
+ function normalizePromotionExtraValue(value, unitRule) {
631
+ if (unitRule === "basis-points-to-ratio") return value / 1e4;
632
+ return value;
633
+ }
634
+ function sortedNumericKeys(value, path) {
635
+ const keys = Object.keys(value);
636
+ if (keys.length === 0) throw new Error(`Missing nanoka promotion extra entries ${path}`);
637
+ for (const key of keys) if (!/^\d+$/.test(key)) throw new Error(`Invalid numeric key ${path}.${key}`);
638
+ return keys.sort((left, right) => Number(left) - Number(right));
639
+ }
640
+ function requiredObject(value, path) {
641
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Missing object nanoka promotion extra field ${path}`);
642
+ return value;
643
+ }
644
+ function requiredString(value, path) {
645
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Missing text nanoka promotion extra field ${path}`);
646
+ return value;
647
+ }
648
+ function requiredFinite(value, path) {
649
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka promotion extra field ${path}`);
650
+ return value;
651
+ }
652
+ //#endregion
653
+ //#region src/nanoka-resource.ts
654
+ const nanokaResourceUnitRules = {
655
+ maxAdrenaline: "stats.rp_max",
656
+ automaticAdrenalineAccumulation: "stats.rp_recover / 100",
657
+ resonanceRecovery: "fever_recovery / 1000",
658
+ resonanceRecoveryGrowth: "fever_recovery_growth / 1000",
659
+ adrenalineRecovery: "rp_recovery / 10000",
660
+ adrenalineRecoveryGrowth: "rp_recovery_growth / 10000"
661
+ };
662
+ function deriveNanokaAdrenalinePanel(source) {
663
+ return {
664
+ maxAdrenaline: requiredNumber(source.stats.rp_max, "stats.rp_max"),
665
+ automaticAdrenalineAccumulation: requiredNumber(source.stats.rp_recover, "stats.rp_recover") / 100
666
+ };
667
+ }
668
+ function deriveNanokaSkillResourceRecovery(param) {
669
+ return {
670
+ resonanceRecovery: requiredNumber(param.fever_recovery, "fever_recovery") / 1e3,
671
+ resonanceRecoveryGrowth: requiredNumber(param.fever_recovery_growth, "fever_recovery_growth") / 1e3,
672
+ adrenalineRecovery: requiredNumber(param.rp_recovery, "rp_recovery") / 1e4,
673
+ adrenalineRecoveryGrowth: requiredNumber(param.rp_recovery_growth, "rp_recovery_growth") / 1e4
674
+ };
675
+ }
676
+ function requiredNumber(value, path) {
677
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`Missing numeric nanoka resource field ${path}`);
678
+ return value;
679
+ }
680
+ //#endregion
681
+ //#region cleaned/runtime/game-data.json
682
+ var game_data_default = {
683
+ kind: "gameData",
684
+ schemaVersion: "cleaned-game-data-artifact/v0.1",
685
+ dataVersion: "fairy-v0.1.0-nanoka-runtime",
686
+ generatedAt: "2026-05-15T18:46:10+08:00",
687
+ sourceManifestPath: "data/source/source-manifest.json",
688
+ runtimeCutoverReady: true,
689
+ runtimeSourcePolicy: {
690
+ "primarySourceId": "nanoka-zzz",
691
+ "configuredLiveVersion": "2.8",
692
+ "deprecatedRuntimeSourceIds": [
693
+ "lo-user-excel",
694
+ "mihoyo-zzz-critical-assault",
695
+ "buhflipexplode-zzz-da",
696
+ "nanoka-zzz-boss-manual-2026-05-07"
697
+ ],
698
+ "archivedSourcesRuntimeAllowed": false,
699
+ "phase3ExitSyncId": "phase3-sync-002-g27-g28"
700
+ },
701
+ data: {
702
+ "schemaVersion": "fairy-game-data-v0.1.0",
703
+ "gameVersion": "ZZZ-2.8",
704
+ "dataVersion": "0.1.0",
705
+ "sourceVersion": "nanoka-zzz@2.8",
706
+ "generatedAt": "2026-05-15T18:46:10+08:00",
707
+ "sources": [{
708
+ "id": "nanoka-zzz",
709
+ "kind": "thirdPartySite",
710
+ "url": "https://static.nanoka.cc/manifest.json",
711
+ "gameVersion": "ZZZ-2.8",
712
+ "sourceVersion": "2.8",
713
+ "fetchedAt": "2026-05-15T17:15:00+08:00",
714
+ "parsedAt": "2026-05-15T18:46:10+08:00",
715
+ "parserVersion": "nanoka-runtime-cutover-v0.1.0",
716
+ "licenseNote": "Runtime cleaned data uses lo-user-approved nanoka live 2.8 evidence; archived Excel/D-17/D-12 sources are retained for audit only."
717
+ }],
718
+ "agents": { "1371": {
719
+ "id": "1371",
720
+ "label": {
721
+ "zh": "仪玄",
722
+ "en": "Yixuan"
723
+ },
724
+ "source": {
725
+ "sourceId": "nanoka-zzz",
726
+ "sourceVersion": "2.8",
727
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/character/1371.json",
728
+ "dataPath": "/"
729
+ },
730
+ "attribute": "auricInk",
731
+ "agentSpecialty": "rupture",
732
+ "baseStatsByLevel": { "60": {
733
+ "maxHp": 7953.8621,
734
+ "attack": 872.5748,
735
+ "defense": 441.1145,
736
+ "impact": 93,
737
+ "critRate": .05,
738
+ "critDamage": .5,
739
+ "anomalyMastery": 92,
740
+ "anomalyProficiency": 90,
741
+ "sheerForce": 795.38621
742
+ } },
743
+ "skillIds": ["1371001"],
744
+ "sourceAliases": [
745
+ "仪玄",
746
+ "Yixuan",
747
+ "Yixuan"
748
+ ]
749
+ } },
750
+ "skills": { "1371001": {
751
+ "id": "1371001",
752
+ "agentId": "1371",
753
+ "label": {
754
+ "zh": "普通攻击:霄云劲(一段)",
755
+ "en": "Basic Attack: Xiao Yun Jin (Hit 1)"
756
+ },
757
+ "source": {
758
+ "sourceId": "nanoka-zzz",
759
+ "sourceVersion": "2.8",
760
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/character/1371.json",
761
+ "dataPath": "/skill/basic/description/4/param/0/param/1371001"
762
+ },
763
+ "tags": ["basic"],
764
+ "attribute": "auricInk",
765
+ "segments": [{
766
+ "id": "1371001-hit-1",
767
+ "levelKey": "1",
768
+ "multiplierByLevel": { "1": .458 },
769
+ "dazeMultiplierByLevel": { "1": .286 },
770
+ "resonanceRecoveryByLevel": { "1": 71.5 },
771
+ "adrenalineRecoveryByLevel": { "1": .52 },
772
+ "damageType": "regular",
773
+ "hitCount": 1,
774
+ "defaultTags": ["basic"],
775
+ "source": {
776
+ "sourceId": "nanoka-zzz",
777
+ "sourceVersion": "2.8",
778
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/character/1371.json",
779
+ "dataPath": "/skill/basic/description/4/param/0/param/1371001"
780
+ }
781
+ }]
782
+ } },
783
+ "bangboos": { "54008": {
784
+ "id": "54008",
785
+ "label": {
786
+ "zh": "插头布",
787
+ "en": "Plugboo"
788
+ },
789
+ "source": {
790
+ "sourceId": "nanoka-zzz",
791
+ "sourceVersion": "2.8",
792
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/bangboo/54008.json",
793
+ "dataPath": "/"
794
+ },
795
+ "baseStatsByLevel": { "60": {
796
+ "maxHp": 4210.2983,
797
+ "attack": 8057.0996,
798
+ "defense": 723.8011,
799
+ "impact": 99,
800
+ "critRate": .05,
801
+ "critDamage": .5,
802
+ "anomalyMastery": 132
803
+ } },
804
+ "skillIds": ["5400801"],
805
+ "sourceAliases": [
806
+ "插头布",
807
+ "Plugboo",
808
+ "Plugboo"
809
+ ]
810
+ } },
811
+ "bangbooSkills": { "5400801": {
812
+ "id": "5400801",
813
+ "bangbooId": "54008",
814
+ "label": {
815
+ "zh": "电流狙击",
816
+ "en": "Electric Current Snipe"
817
+ },
818
+ "source": {
819
+ "sourceId": "nanoka-zzz",
820
+ "sourceVersion": "2.8",
821
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/bangboo/54008.json",
822
+ "dataPath": "/skill_prop/5400801"
823
+ },
824
+ "tags": ["special"],
825
+ "segments": [{
826
+ "id": "5400801-hit",
827
+ "levelKey": "1",
828
+ "multiplierByLevel": { "1": 5.12 },
829
+ "dazeMultiplierByLevel": { "1": 1.87 },
830
+ "damageType": "regular",
831
+ "hitCount": 1,
832
+ "defaultTags": ["special"],
833
+ "source": {
834
+ "sourceId": "nanoka-zzz",
835
+ "sourceVersion": "2.8",
836
+ "sourceAnchor": "data/source/raw/nanoka/zzz/2.8/zh/bangboo/54008.json",
837
+ "dataPath": "/skill_prop/5400801"
838
+ }
839
+ }]
840
+ } },
841
+ "wEngines": {},
842
+ "driveDiscs": {},
843
+ "enemies": {},
844
+ "resonium": {},
845
+ "modifiers": {},
846
+ "rules": {
847
+ "runtimePrimarySourceId": "nanoka-zzz",
848
+ "configuredLiveVersion": "2.8",
849
+ "runtimeCutoverReady": true,
850
+ "archivedRuntimeSourcesAllowed": false,
851
+ "driveDiscSlotAndSubstatTables": "out-of-scope:user-provided-snapshot-final-panel",
852
+ "implementationOwnedFormulaBoundary": ["rules.disorderFormula", "rules.disorderDazeLevelZone"]
853
+ },
854
+ "aliases": {
855
+ "fields": {
856
+ "defence": "defense",
857
+ "rp": "adrenaline",
858
+ "fever": "resonance"
859
+ },
860
+ "enumValues": {
861
+ "element_type.205": "auricInk",
862
+ "specialty.rupture": "rupture"
863
+ },
864
+ "sourceTerms": {
865
+ "玄墨": "auricInk",
866
+ "闪能": "adrenaline",
867
+ "喧响值": "resonance",
868
+ "电属性伤害": "electric"
869
+ }
870
+ }
871
+ }
872
+ };
873
+ //#endregion
874
+ //#region src/runtime-policy.ts
875
+ const NANOKA_RUNTIME_SOURCE_ID = "nanoka-zzz";
876
+ const NANOKA_RUNTIME_SOURCE_VERSION = "2.8";
877
+ const NANOKA_RUNTIME_DATA_VERSION = "fairy-v0.1.0-nanoka-runtime";
878
+ const ARCHIVED_RUNTIME_SOURCE_IDS = [
879
+ "lo-user-excel",
880
+ "mihoyo-zzz-critical-assault",
881
+ "buhflipexplode-zzz-da",
882
+ "nanoka-zzz-boss-manual-2026-05-07"
883
+ ];
884
+ function isObject(value) {
885
+ return typeof value === "object" && value !== null && !Array.isArray(value);
886
+ }
887
+ function isSourceRefCandidate(value) {
888
+ return isObject(value) && typeof value.sourceId === "string";
889
+ }
890
+ function collectSourceRefs(value, refs = []) {
891
+ if (Array.isArray(value)) {
892
+ for (const item of value) collectSourceRefs(item, refs);
893
+ return refs;
894
+ }
895
+ if (!isObject(value)) return refs;
896
+ if (isSourceRefCandidate(value)) {
897
+ const ref = { sourceId: value.sourceId };
898
+ if (typeof value.sourceVersion === "string") ref.sourceVersion = value.sourceVersion;
899
+ if (typeof value.sourceAnchor === "string") ref.sourceAnchor = value.sourceAnchor;
900
+ if (typeof value.dataPath === "string") ref.dataPath = value.dataPath;
901
+ refs.push(ref);
902
+ }
903
+ for (const item of Object.values(value)) collectSourceRefs(item, refs);
904
+ return refs;
905
+ }
906
+ function assert(condition, message) {
907
+ if (!condition) throw new Error(message);
908
+ }
909
+ function assertNanokaRuntimeGameDataArtifact(artifact) {
910
+ assert(isObject(artifact), "runtime game data artifact must be an object");
911
+ assert(artifact.kind === "gameData", "runtime artifact kind must be gameData");
912
+ assert(artifact.dataVersion === NANOKA_RUNTIME_DATA_VERSION, "runtime artifact dataVersion drifted");
913
+ assert(artifact.runtimeCutoverReady === true, "runtimeCutoverReady must be true after Phase 4 cutover");
914
+ const policy = artifact.runtimeSourcePolicy;
915
+ assert(isObject(policy), "runtimeSourcePolicy is required");
916
+ assert(policy.primarySourceId === NANOKA_RUNTIME_SOURCE_ID, "runtime primary source must be nanoka-zzz");
917
+ assert(policy.configuredLiveVersion === "2.8", "runtime sourceVersion must use configured live version");
918
+ assert(policy.archivedSourcesRuntimeAllowed === false, "archived sources must not be allowed at runtime");
919
+ assert(policy.phase3ExitSyncId === "phase3-sync-002-g27-g28", "runtime cutover must cite Phase 3 exit sync");
920
+ assert(JSON.stringify(policy.deprecatedRuntimeSourceIds) === JSON.stringify(ARCHIVED_RUNTIME_SOURCE_IDS), "deprecated runtime source id list drifted");
921
+ const data = parseGameData(artifact.data);
922
+ assert(data.sourceVersion === `${NANOKA_RUNTIME_SOURCE_ID}@2.8`, "runtime GameData sourceVersion must be nanoka-zzz@2.8");
923
+ assert(data.sources.length === 1, "runtime GameData must expose exactly one runtime source document");
924
+ assert(data.sources[0]?.id === NANOKA_RUNTIME_SOURCE_ID, "runtime GameData source document must be nanoka-zzz");
925
+ assert(data.sources[0]?.sourceVersion === "2.8", "runtime GameData source document must use configured live version");
926
+ const archivedSources = new Set(ARCHIVED_RUNTIME_SOURCE_IDS);
927
+ for (const ref of collectSourceRefs(data)) {
928
+ assert(!archivedSources.has(ref.sourceId), `runtime GameData must not reference archived source ${ref.sourceId}`);
929
+ assert(ref.sourceId === NANOKA_RUNTIME_SOURCE_ID, `runtime GameData must not reference non-nanoka source ${ref.sourceId}`);
930
+ assert(ref.sourceVersion === "2.8", `${ref.sourceAnchor ?? ref.sourceId}: runtime source refs must use configured live version`);
931
+ }
932
+ }
933
+ //#endregion
934
+ //#region src/runtime.ts
935
+ const nanokaRuntimeGameDataArtifact = game_data_default;
936
+ function getNanokaRuntimeGameData() {
937
+ assertNanokaRuntimeGameDataArtifact(nanokaRuntimeGameDataArtifact);
938
+ return nanokaRuntimeGameDataArtifact.data;
939
+ }
940
+ function getNanokaRuntimeSourcePolicy() {
941
+ assertNanokaRuntimeGameDataArtifact(nanokaRuntimeGameDataArtifact);
942
+ return nanokaRuntimeGameDataArtifact.runtimeSourcePolicy;
943
+ }
94
944
  //#endregion
95
945
  //#region src/sources.ts
96
946
  const dataSourceDescriptors = [
947
+ {
948
+ id: "nanoka-zzz",
949
+ kind: "thirdPartySite",
950
+ label: "nanoka ZZZ static JSON",
951
+ url: "https://static.nanoka.cc/manifest.json",
952
+ status: "readyForAdapter",
953
+ formalDataReady: false,
954
+ parserTargets: [
955
+ "agent and Bangboo base panel raw fields",
956
+ "skill parameter tables",
957
+ "Deadly Assault period / zone / buff / boss detail raw fields",
958
+ "Adrenaline and Resonance recovery raw fields",
959
+ "enemy monster_info variant mapping raw fields",
960
+ "Phase 3 Nicole/Yanagi/Penguinboo/Sharkboo candidate coverage",
961
+ "approved-live snapshot diff inputs"
962
+ ],
963
+ sourceVersionStrategy: "Resolve manifest.zzz.live, retain the configured live snapshot, and treat manifest.zzz.latest as research/drift only unless lo-user approves a version upgrade.",
964
+ discoveredAssets: [
965
+ "data/source/raw/nanoka/zzz/2.8/fetch-manifest.json",
966
+ "data/source/raw/nanoka/zzz/2.8/manifest.json",
967
+ "data/source/raw/nanoka/zzz/2.8/boss.json",
968
+ "data/source/raw/nanoka/zzz/2.8/zh/character/1021.json",
969
+ "data/source/raw/nanoka/zzz/2.8/zh/character/1371.json",
970
+ "data/source/raw/nanoka/zzz/2.8/zh/character/1031.json",
971
+ "data/source/raw/nanoka/zzz/2.8/zh/character/1221.json",
972
+ "data/source/raw/nanoka/zzz/2.8/zh/boss/69036.json",
973
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/30000.json",
974
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/30004.json",
975
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/200141.json",
976
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/200014.json",
977
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/200034.json",
978
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/30033.json",
979
+ "data/source/raw/nanoka/zzz/2.8/zh/monster/300211.json",
980
+ "data/source/raw/nanoka/zzz/2.8/zh/bangboo/53001.json",
981
+ "data/source/raw/nanoka/zzz/2.8/zh/bangboo/54001.json"
982
+ ],
983
+ fetchPolicy: {
984
+ mode: "httpGet",
985
+ maxRequestsPerMinute: 30,
986
+ cacheRequired: true,
987
+ conditionalRequestsPreferred: true,
988
+ requiresUserAgent: true
989
+ },
990
+ compliance: {
991
+ robotsTxt: "notChecked",
992
+ termsStatus: "requiresHumanReview",
993
+ redistribution: "cleanedDataOnly",
994
+ notes: [
995
+ "D-20 R1/R6 locks nanoka as the exclusive source for source-backed cleaned data.",
996
+ "Release artifacts default to manifest.zzz.live; latest/pre-release snapshots are research-only unless lo-user approves a version upgrade.",
997
+ "Phase 2 raw snapshot retention is for source-gate and adapter-skeleton verification only; runtime cutover waits for normalization, semantic mapping, and QA drift audit."
998
+ ]
999
+ }
1000
+ },
97
1001
  {
98
1002
  id: "lo-user-excel",
99
1003
  kind: "excel",
@@ -224,4 +1128,4 @@ function getDataSourceDescriptor(id) {
224
1128
  return descriptor;
225
1129
  }
226
1130
  //#endregion
227
- export { DATA_SOURCE_SKELETON_PARSER_VERSION, assertDiscoveryOnlyGameData, buildSourceDocument, buildSourceRef, createDiscoveryOnlyAdapter, createEmptyGameData, dataSourceDescriptors, getDataSourceDescriptor };
1131
+ export { ARCHIVED_RUNTIME_SOURCE_IDS, DATA_SOURCE_SKELETON_PARSER_VERSION, NANOKA_PARSER_VERSION, NANOKA_RUNTIME_DATA_VERSION, NANOKA_RUNTIME_SOURCE_ID, NANOKA_RUNTIME_SOURCE_VERSION, NANOKA_SOURCE_ID, assertApprovedSnapshotDiffInputs, assertDiscoveryOnlyGameData, assertNanokaRuntimeGameDataArtifact, assertNanokaSnapshotManifest, buildSourceDocument, buildSourceDocumentFromRegistryEntry, buildSourceRef, buildSourceRefForParsedRecord, buildSourceRefsForParsedBatch, createDiscoveryOnlyAdapter, createEmptyGameData, createNanokaSnapshotAdapter, dataSourceDescriptors, deriveNanokaAdrenalinePanel, deriveNanokaBangbooElement, deriveNanokaDeadlyAssaultPeriod, deriveNanokaEnemyVariantMapping, deriveNanokaEnemyVariantMappings, deriveNanokaPanelValue, deriveNanokaPromotionExtraStats, deriveNanokaSkillResourceRecovery, deriveNanokaSnapshotDiffHistory, diffNumericLeaves, getDataSourceDescriptor, getNanokaRuntimeGameData, getNanokaRuntimeSourcePolicy, nanokaResourceUnitRules, nanokaRuntimeGameDataArtifact, nanokaSnapshotRecords };