From cdbc75513dcf2df45a1ece5fb95ee828097aaa46 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 11:58:46 +0800 Subject: [PATCH 1/8] docs(deck): draft browser-first collection and recommendation integration --- docs/deck-integration.md | 86 ++++++++ docs/evidence/boxlens-onnx-wasm-20261001.json | 198 ++++++++++++++++++ src/lib/deck/runtime-plan.ts | 83 ++++++++ tests/deck-runtime-plan.test.ts | 41 ++++ 4 files changed, 408 insertions(+) create mode 100644 docs/deck-integration.md create mode 100644 docs/evidence/boxlens-onnx-wasm-20261001.json create mode 100644 src/lib/deck/runtime-plan.ts create mode 100644 tests/deck-runtime-plan.test.ts diff --git a/docs/deck-integration.md b/docs/deck-integration.md new file mode 100644 index 00000000..3ce84d26 --- /dev/null +++ b/docs/deck-integration.md @@ -0,0 +1,86 @@ +# 卡池截图与组卡:MoeNotes 接入方案(Draft) + +MoeNotes 是玩家入口。`ournotes-boxlens` 负责截图中的身份与可见培养字段,`ournotes-deck` 负责游戏规则、计分和搜索,MoeNotes 负责校对、目标选择、结果解释与本地保存。各层复用已有版本化契约,不在页面里另写一套评分公式。 + +这个 PR 提供运行端选择、数据版本绑定和 Worker 消息契约,以及对应回归。它尚未添加上线路由、模型运行时、权重附件或组卡页面;不会把可行性方案标成已上线功能。上游依赖是 [BoxLens PR #1](https://github.com/empty-sekai/ournotes-boxlens/pull/1) 与 [Deck PR #3](https://github.com/empty-sekai/ournotes-deck/pull/3),后续发布需要固定具体 commit 和文件 SHA256。 + +## 玩家路径 + +```mermaid +flowchart LR + A[选择成员和留影截图] --> B[浏览器识别 Worker] + B --> C[累积卡池与人工校对] + C --> D[确认培养与完整持有集合] + D --> E[选择歌曲、打法、目标] + E --> F[Rust WASM 搜索 Worker] + F --> G[五槽队伍、队长、留影与结果依据] +``` + +1. 玩家可以分批导入等级、特训、留影视图;重复截图去重。冲突保留原始来源,不能取较大的数字。选择文件不等于上传服务器,本地运行时只在设备里读图。 +2. 校对页展示身份、等级、卡阶、特训、技能等级及缺失字段,支持替换误识别身份、排除和补漏。修正记录与原始识别分别保存。 +3. 全局培养采用可复用玩家档案:角色 Rank、道具、VIP、回忆、活动上下文。截图未出现的字段不会默认为真实的最低或最高培养;批量设值需要玩家主动选择并显示假设。未确定完整持有集合时,结果标为“已确认卡池中的推荐”。 +4. 推荐页首先选歌曲和难度,再选玩家目标。直接展示五个物理槽位、第三槽队长、每槽留影与无留影状态。演出顺序遵循游戏的随机打乱,不能提示玩家靠排演出顺序取得保证收益。 +5. 结果先提供一组合法且精确计分的队伍,再允许继续优化。显示耗时、已评估候选、目标、条件、与玩家当前队伍的差值。每个卡池/校对/目标变更增加 `inputRevision`;旧响应不得恢复已失效队伍。 + +## 推荐目标 + +| 玩家意图 | Deck 指标 | 需要玩家声明 | 结果含义 | +|---|---|---|---| +| 单曲冲分 | `score` | Live 模式、谱面、打法、有限根分布 | 声明条件下的结算分数期望;理论 AP/Just 明确标示 | +| 达到目标分 | `scoreAtLeast` | 目标分与同上条件 | 达标概率,不等于平均分最高 | +| 达标后不追溢出分 | `cappedScore` | 分数上限 | `E(min(score, threshold))`;用于比较未达标损失 | +| 分数与结算血量 | `scoreAndLifeAtLeast` | 完整判定流、目标分、最低结算血量 | 同时满足两个终值的概率;不是已验证的失败/续关/退出模型 | +| 跳过演出 | `score` + `skip` | 歌曲、现有培养 | 游戏跳过公式下的分数,与手打 Live 分开 | +| 活动积分 | `clientEventPoints` | 活动时钟、计数与模式 | 客户端预估积分;评分榜与累计积分不能混为同一目标 | +| 指定奖励数量 | `conditionalClientEventItems` | 明确奖励选择与完整上下文 | 所给奖励条件下的客户端数量,服务器实际选择另有边界 | +| 查看综合力 | `power` | 可选歌曲/活动加成 | 队伍属性诊断指标,不能替代 Live 分数排序 | + +“稳过谱”需要失败生命周期与人的失误分布,当前不能由一个准确率百分比直接证明。“提升卡片最划算”需要经核验的培养成本与玩家预算;在该契约完成前只使用已确认培养,不静默假设满级。Battle/Arena 需要外部排名确认时间线;Solo 第一名、固定假设名次和真实对手排名必须分别标识。 + +## 浏览器运行决策 + +可以把权重发给浏览器,但需要分两条链路。 + +| 部件 | 首选实现 | 当前已具备 | 尚需接入与验证 | +|---|---|---|---| +| 身份编码与字段识别 | ONNX Runtime Web,WASM 基础后端,WebGPU 可选加速 | `encoder.onnx` 4,898,068 B;`fields.onnx` 1,356,782 B,合计约 6.25 MB 原始权重 | 浏览器模型加载/张量/前后处理一致性与清晰/压力截图验收 | +| 截图定位、裁切与匹配 | 专用 Worker,移植现有图像处理 | Python OpenCV SIFT/FLANN、模板/布局处理、索引与冲突合并 | 不能只传两份 ONNX 就声称完整 BoxLens;需确认自建 OpenCV WASM 支持所需算法,或单独训练定位模型并重新测覆盖 | +| 固定队伍复算 | 原 Rust 规则编译 WASM | 上游已有 `wasm/replay` 与原生/WASM 对拍 | MoeNotes 数据/Worker/渲染适配 | +| 完整推荐 | 原 Rust 搜索编译 WASM,独立 Worker | 生产 JSON 推荐 API 与有限分布搜索 | 新增 recommendation WASM export;统一浏览器时钟;取消/渐进输出与实际浏览器门禁 | +| 候选预筛 | Rust WASM;以后再评估 GPU 预筛 | 综合力 warmup、随机/局部提案与精确逐候选模拟 | 大池/手机延迟基准;统计权重不能取代技能、血量与随机交互 | + +ONNX Runtime 官方支持 [WASM 和 WebGPU](https://onnxruntime.ai/docs/tutorials/web/ep-webgpu.html)。WebGPU 的启用以实际能力、模型算子与设备测试为准;不能仅凭 `navigator.gpu` 存在认定模型可用。轻量模型的 GPU 上传/读回开销可能超过计算收益,先测热启动与整批截图耗时。 + +2026-10-01 的 [实际浏览器 smoke 报告](evidence/boxlens-onnx-wasm-20261001.json) 已验证现有两份权重可直接在 Chromium 153 + ONNX Runtime Web 1.30.0 的单线程 WASM 中运行:batch 1/8 共四组相同合成 float32 张量对照 Python CPU,最大绝对误差分别约 `5e-7`(encoder)与 `1.53e-5`(fields),按 `atol=1e-5, rtol=1e-4` 无不一致元素。云 CPU 上热 batch-1 encoder 5.7–7.0 ms、fields 3.5–3.6 ms。这只证明模型算子/形状/runtime 可行,不是完整截图准确率、WebGPU、手机 p95 或逐位浮点等价证明。 + +WASM 计分保留 Rust 的整数和声明 binary32 运算。暂不把精确计分改写成 GPU shader,避免引入新的浮点、取整和随机顺序差异。已有 WASM replay 对拍不是完整搜索可用的证明:目前多个搜索路径仍用 `std::time::Instant`,现有 bridge 没有导出 `recommend`。浏览器版本需替换全部 deadline/计时路径,并从 JSON 字节建会话,绕过 CLI 文件系统。参见 [Rust 目标平台限制](https://doc.rust-lang.org/stable/rustc/platform-support/wasm32-unknown-unknown.html)。 + +默认单 Worker + WASM,避免先要求站点全局跨源隔离。ONNX 多线程需要 [crossOriginIsolated](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html);启用前需检查 MoeNotes 的图片、音频、嵌入播放器与第三方资源。同步搜索期间 `postMessage(cancel)` 无法中断同一个阻塞调用:第一阶段通过终止专用 Worker 取消,第二阶段实现分片 `step`/进度快照,保留已完成候选。不能把 UI 的取消按钮当作引擎已支持优雅取消。 + +## 文件分发与缓存 + +用公开发布产物的 manifest 固定 region、masterVersion、模型/索引/DeckData SHA256、格式、文件大小、输入输出定义与相对资源路径。权重与 JS/WASM runtime 版本绑定;游戏图像和 master 沿用站点资源发布管线,不提交到源码仓库。不能把当前约 120 MB 的 Python 运行目录原封不动发给手机,它包含原生模板、图库和生产/复现资源,需要导出最小浏览器包。 + +- 首屏不下载模型。打开卡池工具才按需加载 runtime 和约 6.25 MB 权重,识别图库另计。 +- DeckData 分成区服规则核心与按歌曲加载的谱面;卡池和识别数据必须绑定同区服、同一 master 与实际文件哈希。跨区服重识别/迁移需要明确映射依据。 +- 模型、索引和谱面进入现有按内容哈希的浏览器缓存,沿用容量上限与清理入口;玩家档案独立存储、导入导出,不因清模型缓存丢失。新存储类型按 AGENTS.md 补全五个核心 locale。 +- 权重、下载进度、缓存占用与缺少 WebGPU 时的 WASM 路径对玩家可见。预期速度必须来自冷/热浏览器实测,不能从本机 Python 或 64 核 CNB 推算手机速度。 + +## 接口与回退 + +`src/lib/deck/runtime-plan.ts` 定义运行端选择和消息绑定。浏览器完整识别能力、ONNX provider 和浏览器完整搜索是三个独立 capability,不能互相代替。远端识别与远端搜索也分别要求截图上传/卡池上传选择;本地失败不自动上传。可选远端服务采用有界队列、取消和原始输入绑定,运行同一 Rust 模型。该 Draft 不定义未经实现的生产服务 URL。 + +Worker 传输 `rosterJson`、`requestJson`、`resultJson` 原始 UTF-8 文本。不能先经过 `JSON.parse`/`JSON.stringify` 丢失超过 `2^53` 的整数、活动时钟或明确 f32 数值 token;语法与重复键由严格边界校验。大文件用 Transferable ArrayBuffer 降低拷贝,首版协议文本用于保证输入语义。消息绑定 `jobId`、`inputRevision` 和 dataset identity,迟到响应及跨版本响应丢弃。 + +搜索返回 `Complete`/`TimedOut`/candidate termination 和 optimality 原样保留。“Complete”仅是所给卡池、约束、打法、外部条件、有限根分布下的证明;候选策略是精确计分的启发式推荐。平均分、范围、P10/P50/P90、达标概率使用整数质量和精确分数,展示层不参与排名。实际 TickCount 分布未知时不能称无条件最优。 + +## 实施与验收 + +1. 合入契约后发布上游版本化 SDK:推荐 WASM 导出、可用 clock、手动卡池/固定队伍复算和单曲 power/skip 对拍;此阶段不依赖 OCR。 +2. 按 MoeNotes route registry 添加卡池工具入口与 React island;复用卡面、名称本地化与现有缓存。先把校对/玩家档案/目标/五槽结果接齐,核心五语言同批完成。 +3. 移植完整截图识别,发布最小 manifest;清晰截图、重叠批次、冲突、遮挡、未知新卡、误识别纠正与漏卡补充均验收。两份 ONNX 的可运行 smoke 只覆盖神经网络步骤。 +4. 接入 bounded Live 搜索:产品目标先定热启动 2 秒内有首组候选、10 秒优化预算;这是待测验收目标。低端手机未达到时提供缩小范围/后台继续/明确远端选择,不能承诺“瞬间全池最优”。 +5. 浏览器端和原生端对相同 UTF-8 输入逐项比较槽位、留影、分数、逐根 outcome、精确期望和终止状态。必须包括大于 `2^53` 整数、不同 FPS、遗漏判定、撃奏随机、技能改变排序、取消与过期响应。 +6. 大池(真实区域全持有仅作声明 mock)与 7 成员/3 留影样本分别记录冷启动下载量、热启动 p50/p95、峰值内存、前台交互和推荐质量。小样本穷举差分证明不能替代大池手机速度验收。 + +数值模型的已验收范围以 Deck 的原生验证 manifest 为准;离线 1.0.1-25 证据不能自动认证 JP 1.0.4、所有 IFix、触控投影、完整失败/续关和服务器奖励行为。该方案通过共用原 Rust 模型减少漂移,同时保留尚未验证边界。 diff --git a/docs/evidence/boxlens-onnx-wasm-20261001.json b/docs/evidence/boxlens-onnx-wasm-20261001.json new file mode 100644 index 00000000..e5ca24cf --- /dev/null +++ b/docs/evidence/boxlens-onnx-wasm-20261001.json @@ -0,0 +1,198 @@ +{ + "format": "boxlens.onnx-wasm-feasibility/1", + "status": "passed", + "scope": "Synthetic deterministic normalized-range float32 tensors shared byte-for-byte between Python CPU and actual Chromium ONNX Runtime Web WASM. This is operator/shape/runtime feasibility, not screenshot preprocessing, retrieval/classification accuracy, full BoxLens portability, WebGPU feasibility or mobile performance.", + "versions": { + "pythonOrt": "1.30.0", + "onnxruntimeWeb": "1.30.0", + "npmIntegrity": "sha512-q0y+JrrtukXSzsBWEMccVfqX25LRmosXHF+CaRJmg8pZClzcV7svNc4rKY3jL02Vb7QmRMDs1SigqR4CXAfKYQ==", + "chromium": "153.0.8010.12", + "python": "3.11.16" + }, + "tolerance": { + "atol": 1e-05, + "rtol": 0.0001 + }, + "browser": { + "executionProvider": "wasm", + "threads": 1, + "crossOriginIsolated": false, + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.8010.12 Safari/537.36", + "onnxruntimeWebVersion": "1.30.0" + }, + "models": [ + { + "name": "encoder", + "input": "image", + "output": "output", + "modelBytes": 4898068, + "modelSha256": "a0a64115241a0eb22e2aab3b151ee3a3f57dc317d88524c027aaa443d051efa1", + "metadataInputs": [ + { + "name": "image", + "shape": [ + "batch", + 3, + 160, + 128 + ], + "type": "tensor(float)" + } + ], + "metadataOutputs": [ + { + "name": "output", + "shape": [ + "batch", + 128 + ], + "type": "tensor(float)" + } + ], + "cpuLoadMs": 23.462798912078142 + }, + { + "name": "fields", + "input": "image", + "output": "output", + "modelBytes": 1356782, + "modelSha256": "768889db7b475dce28ed949299d2e7a624faa3ee18f3c28bb22deac2bcdd1a8d", + "metadataInputs": [ + { + "name": "image", + "shape": [ + "batch", + 3, + 56, + 144 + ], + "type": "tensor(float)" + } + ], + "metadataOutputs": [ + { + "name": "output", + "shape": [ + "batch", + 106 + ], + "type": "tensor(float)" + } + ], + "cpuLoadMs": 5.2393360529094934 + } + ], + "comparisons": [ + { + "model": "encoder", + "batch": 1, + "shape": [ + 1, + 128 + ], + "loadMs": 517.3999998569489, + "firstInferenceMs": 47.89999985694885, + "warmInferenceMs": [ + 7, + 6.1000001430511475, + 5.700000047683716 + ], + "elements": 128, + "maxAbsError": 2.7567148208618164e-07, + "meanAbsError": 7.091443876561243e-08, + "maxRelativeErrorAbove1e-6": 0.0003364417634173316, + "nonFiniteElements": 0, + "mismatchedElementsAtTolerance": 0, + "top1Matches": 1, + "passed": true + }, + { + "model": "encoder", + "batch": 8, + "shape": [ + 8, + 128 + ], + "loadMs": 517.3999998569489, + "firstInferenceMs": 42.700000047683716, + "warmInferenceMs": [ + 38.5, + 36.09999990463257, + 39.09999990463257 + ], + "elements": 1024, + "maxAbsError": 4.991888999938965e-07, + "meanAbsError": 8.42149958657501e-08, + "maxRelativeErrorAbove1e-6": 0.00046617381848724885, + "nonFiniteElements": 0, + "mismatchedElementsAtTolerance": 0, + "top1Matches": 8, + "passed": true + }, + { + "model": "fields", + "batch": 1, + "shape": [ + 1, + 106 + ], + "loadMs": 11.700000047683716, + "firstInferenceMs": 7.099999904632568, + "warmInferenceMs": [ + 3.5, + 3.6000001430511475, + 3.5999999046325684 + ], + "elements": 106, + "maxAbsError": 1.1444091796875e-05, + "meanAbsError": 2.8520260217054834e-06, + "maxRelativeErrorAbove1e-6": 2.5544655154499504e-07, + "nonFiniteElements": 0, + "mismatchedElementsAtTolerance": 0, + "top1Matches": 1, + "passed": true + }, + { + "model": "fields", + "batch": 8, + "shape": [ + 8, + 106 + ], + "loadMs": 11.700000047683716, + "firstInferenceMs": 27, + "warmInferenceMs": [ + 26.200000047683716, + 26, + 26.100000143051147 + ], + "elements": 848, + "maxAbsError": 1.52587890625e-05, + "meanAbsError": 3.3041216292471255e-06, + "maxRelativeErrorAbove1e-6": 3.552802686131999e-07, + "nonFiniteElements": 0, + "mismatchedElementsAtTolerance": 0, + "top1Matches": 8, + "passed": true + } + ], + "samples": { + "models": 2, + "batchShapesPerModel": [ + 1, + 8 + ], + "inferencesPerShape": 4, + "timingQualification": "One first-run and three warm samples on one cloud Chromium CPU, not p50/p95 or mobile speed." + }, + "sourceSha256": { + "run_probe.py": "1838b9ae2c299237cacc76102d4fd537c524752f72c472f26f2edb7fbc9dc6bd", + "probe.html": "43a9eebf5f2d313f9c94cdeb3504466a3bbb3ebcf3aa042ee095217629ba3258", + "package.json": "ce4490b5ff5e2ab9bc693ecdba1b18b12e6624583235328ffb14b65f8d298847", + "package-lock.json": "580b3d773f40b7631aa1934f4aa94ac6477c905e9058cad8bfe641991893d493" + }, + "officialDocs": [ + "https://onnxruntime.ai/docs/tutorials/web/", + "https://onnxruntime.ai/docs/api/js/interfaces/InferenceSession.SessionOptions.html" + ] +} \ No newline at end of file diff --git a/src/lib/deck/runtime-plan.ts b/src/lib/deck/runtime-plan.ts new file mode 100644 index 00000000..5a7d535d --- /dev/null +++ b/src/lib/deck/runtime-plan.ts @@ -0,0 +1,83 @@ +/** Runtime selection for the proposed collection tool. This does not load a model or upload a screenshot. */ +export interface DeckDataIdentity { + region: string; + masterVersion: string; + deckDataSha256: string; +} + +export interface DeckRuntimeCapabilities { + /** Complete image decoding/localization/preprocessing/merge pipeline, beyond ONNX inference alone. */ + browserRecognition: boolean; + onnxWasm: boolean; + onnxWebGpu: boolean; + /** Recommendation export, clock and cancellation support have passed browser parity tests. */ + browserSolver: boolean; + remoteRecognition: boolean; + remoteSolver: boolean; +} + +export interface DeckRuntimeConsent { + uploadScreenshots: boolean; + uploadRoster: boolean; +} + +export interface DeckRuntimePlan { + recognition: "onnx-wasm" | "onnx-webgpu" | "remote"; + recommendation: "wasm-worker" | "remote"; + uploads: { screenshots: boolean; roster: boolean }; +} + +/** Unavailable local capabilities never implicitly authorize sending private inputs to a service. */ +export function planDeckRuntime( + capabilities: DeckRuntimeCapabilities, + consent: DeckRuntimeConsent, + preferWebGpu = false, +): DeckRuntimePlan { + const localRecognition = capabilities.browserRecognition && (capabilities.onnxWasm || capabilities.onnxWebGpu); + if (!localRecognition && !(capabilities.remoteRecognition && consent.uploadScreenshots)) { + throw new Error("Recognition requires a supported local runtime or explicit screenshot upload consent"); + } + if (!capabilities.browserSolver && !(capabilities.remoteSolver && consent.uploadRoster)) { + throw new Error("Recommendation requires a supported local runtime or explicit roster upload consent"); + } + const recognition = !localRecognition ? "remote" + : capabilities.onnxWebGpu && (preferWebGpu || !capabilities.onnxWasm) ? "onnx-webgpu" : "onnx-wasm"; + const recommendation = capabilities.browserSolver ? "wasm-worker" : "remote"; + return { recognition, recommendation, uploads: { screenshots: recognition === "remote", roster: recommendation === "remote" } }; +} + +export function assertDeckDataIdentity(roster: DeckDataIdentity, runtime: DeckDataIdentity): void { + for (const identity of [roster, runtime]) { + if (!identity.region || !identity.masterVersion || !/^[a-f0-9]{64}$/.test(identity.deckDataSha256)) { + throw new Error("Incomplete collection/model dataset identity"); + } + } + if (roster.region !== runtime.region || roster.masterVersion !== runtime.masterVersion || roster.deckDataSha256 !== runtime.deckDataSha256) { + throw new Error("Collection and runtime must bind the same region, master version and DeckData bytes"); + } +} + +/** Carry exact solver JSON as UTF-8 text; JSON.parse/stringify would round integers above 2^53. */ +export interface DeckWorkerRequest { + protocol: "moenotes.deck-worker/1"; + jobId: string; + inputRevision: number; + identity: DeckDataIdentity; + rosterJson: string; + requestJson: string; +} + +export interface DeckWorkerReply { + protocol: "moenotes.deck-worker/1"; + jobId: string; + inputRevision: number; + identity: DeckDataIdentity; + resultJson: string; +} + +/** A changed screenshot, review or target invalidates an in-flight result, even if cancellation races. */ +export function isCurrentDeckReply(request: DeckWorkerRequest, reply: DeckWorkerReply, currentRevision: number): boolean { + if (request.protocol !== reply.protocol || request.jobId !== reply.jobId || request.inputRevision !== reply.inputRevision || currentRevision !== reply.inputRevision) return false; + try { assertDeckDataIdentity(request.identity, reply.identity); } catch { return false; } + return true; +} diff --git a/tests/deck-runtime-plan.test.ts b/tests/deck-runtime-plan.test.ts new file mode 100644 index 00000000..c1fa4cf3 --- /dev/null +++ b/tests/deck-runtime-plan.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from "bun:test"; +import { assertDeckDataIdentity, isCurrentDeckReply, planDeckRuntime, type DeckRuntimeCapabilities, type DeckWorkerRequest } from "../src/lib/deck/runtime-plan"; + +const local: DeckRuntimeCapabilities = { browserRecognition: true, onnxWasm: true, onnxWebGpu: false, browserSolver: true, remoteRecognition: true, remoteSolver: true }; +const noUpload = { uploadScreenshots: false, uploadRoster: false }; +const identity = { region: "jp", masterVersion: "synthetic/1", deckDataSha256: "a".repeat(64) }; + +describe("collection runtime plan", () => { + test("WASM works without GPU and keeps both private inputs local", () => { + expect(planDeckRuntime(local, noUpload)).toEqual({ recognition: "onnx-wasm", recommendation: "wasm-worker", uploads: { screenshots: false, roster: false } }); + }); + test("WebGPU accelerates recognition while scoring remains the same WASM engine", () => { + expect(planDeckRuntime({ ...local, onnxWebGpu: true }, noUpload, true).recognition).toBe("onnx-webgpu"); + expect(planDeckRuntime({ ...local, onnxWebGpu: true }, noUpload, true).recommendation).toBe("wasm-worker"); + }); + test("ONNX alone is insufficient for screenshot recognition and cannot authorize uploads", () => { + expect(() => planDeckRuntime({ ...local, browserRecognition: false }, noUpload)).toThrow("explicit screenshot upload consent"); + }); + test("each remote input requires its own consent", () => { + expect(() => planDeckRuntime({ ...local, browserSolver: false }, { uploadScreenshots: true, uploadRoster: false })).toThrow("explicit roster upload consent"); + expect(planDeckRuntime({ ...local, browserRecognition: false }, { uploadScreenshots: true, uploadRoster: false }).uploads).toEqual({ screenshots: true, roster: false }); + }); + test("consent does not invent an available service", () => { + expect(() => planDeckRuntime({ ...local, browserSolver: false, remoteSolver: false }, { uploadScreenshots: true, uploadRoster: true })).toThrow(); + }); + test("same IDs from another region or updated master cannot enter the old model", () => { + expect(() => assertDeckDataIdentity(identity, { ...identity, region: "tw" })).toThrow(); + expect(() => assertDeckDataIdentity(identity, { ...identity, masterVersion: "synthetic/2" })).toThrow(); + expect(() => assertDeckDataIdentity(identity, { ...identity, deckDataSha256: "b".repeat(64) })).toThrow(); + expect(() => assertDeckDataIdentity(identity, { ...identity, deckDataSha256: "self-reported" })).toThrow(); + }); + test("late results cannot overwrite changed collection or target", () => { + const request: DeckWorkerRequest = { protocol: "moenotes.deck-worker/1", jobId: "job-1", inputRevision: 2, identity, rosterJson: "{}", requestJson: '{"maxCandidates":9007199254740993}' }; + const reply = { ...request, resultJson: "{}" }; + expect(isCurrentDeckReply(request, reply, 2)).toBe(true); + expect(isCurrentDeckReply(request, reply, 3)).toBe(false); + expect(isCurrentDeckReply(request, { ...reply, jobId: "job-2" }, 2)).toBe(false); + expect(isCurrentDeckReply(request, { ...reply, identity: { ...identity, region: "tw" } }, 2)).toBe(false); + expect(request.requestJson).toContain("9007199254740993"); + }); +}); From c2120b80000f85157443876433d5a8efa99b57bd Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 12:56:15 +0800 Subject: [PATCH 2/8] docs(deck): define model search and automatic data refresh gates --- docs/deck-design/data-update.md | 86 +++++ docs/deck-design/latest-apk-certificates.json | 40 ++ docs/deck-design/latest-apk-identity.json | 29 ++ docs/deck-design/latest-game-read.json | 14 + .../latest-native-source-binding.json | 26 ++ docs/deck-design/model-validation.md | 91 +++++ docs/deck-design/official-store-read.json | 16 + docs/deck-design/player-objectives.md | 86 +++++ docs/deck-design/search-design.md | 352 ++++++++++++++++++ .../deck-design/shared-evaluation-and-data.md | 96 +++++ docs/deck-integration.md | 62 ++- src/lib/deck/runtime-plan.ts | 9 +- tests/deck-runtime-plan.test.ts | 7 +- 13 files changed, 893 insertions(+), 21 deletions(-) create mode 100644 docs/deck-design/data-update.md create mode 100644 docs/deck-design/latest-apk-certificates.json create mode 100644 docs/deck-design/latest-apk-identity.json create mode 100644 docs/deck-design/latest-game-read.json create mode 100644 docs/deck-design/latest-native-source-binding.json create mode 100644 docs/deck-design/model-validation.md create mode 100644 docs/deck-design/official-store-read.json create mode 100644 docs/deck-design/player-objectives.md create mode 100644 docs/deck-design/search-design.md create mode 100644 docs/deck-design/shared-evaluation-and-data.md diff --git a/docs/deck-design/data-update.md b/docs/deck-design/data-update.md new file mode 100644 index 00000000..ced20b33 --- /dev/null +++ b/docs/deck-design/data-update.md @@ -0,0 +1,86 @@ +# 歌曲 meta、组卡与识别数据的自动更新 + +状态:2026-10-01,Draft 设计。修复既有数据发布流程可先进行;最新游戏建模和搜索策略的验收仍是正式组卡功能的前置条件。本文件不表示自动更新修复已部署。 + +## 当前实际链路与故障 + +StarMoe nnnotes 已有 `music-data.yml`:接收 `masterdata-updated`,每日补扫,并允许手动强制重建。它比较 master/resource/client 版本、resource hash、Deck/nnnotes commit 和 recipe,构建后运行 schema、来源、谱面、统计、音乐长度与消费端 smoke 检查。已有归档、发布开关和上传读回校验,应在此基础上修复。 + +截至本轮检查,[最近一次定时任务](https://github.com/StarMoe-org/nnnotes/actions/runs/36699637364)的 plan 成功、Build 步骤失败;之前三次 dispatch 也失败。具体失败原因由发布仓库修复 PR 记录。需要恢复成功的源读取、构建和检查,添加定时器本身不会解除故障。 + +MoeNotes 当前客户端直接读取 `music-data.json`。默认线上文件在本轮读取时为 TW、客户端 `1.0.1/25`、master `0b21c9f4a3911370d5f981fb894c7544`,SHA-256 为 `983896151d3ed69d9dfe26044225907e33be037e4b74589e110ded64780f8738`。这份文件含 85 首歌;网页能读取文件,不证明它仍是对应区服的当前版本。 + +用户提供的两张门槛截图分别与《青春コンプレックス》(`100021`)的上述 TW 门槛及既有 JP 快照的 `requiredScore` 完全相符。JP 的 `battleRequiredScore` 又是另一组值。回归必须保留区服和字段单位,不能将两张图直接解释为同一版本的算法修复。 + +## 更新单元 + +按区服分别形成冻结快照,TW 和 JP 的输出目录、队列、现行指针和检查基线都独立。界面语言不决定区服,ID 相同也不保证规则或数值相同。 + +| 身份 | 必须绑定的内容 | 变化时的处理 | +|---|---|---| +| 客户端规则 | 平台、版本/build、APK 签名、原生库和 metadata、激活 patch 集合、模型源码/产物和认证范围 | 重做受影响的原生对照;不继承旧客户端的认证标签 | +| master | 官方版本、manifest hash、被导出表的文件名与 hash 映射 | 同版本内容变化也重建;拒绝读到一半切换的快照 | +| 资源 | 资源版本/hash、catalog hash、谱面和音频资产 hash | 即使 master 不变,也重新导出受影响歌曲 | +| 计算 | evaluator commit/二进制 hash、exporter commit、schema/recipe、支持范围 | 模型修复也重新计算;不能仅等待游戏换 master | +| 消费与识别 | 消费端固定版本、识别权重/图库/索引、Snap profile 及依赖 | 验证版本兼容;新卡无识别索引时保留人工输入能力 | + +Version 观察记录与已发布快照分开保存。观察时间刷新不应重复重建;源版本、内容或计算 recipe 变化才触发构建。Version 请求失败时,记录最后成功观察与失败状态,不把缓存快照改成“当前最新”。 + +## 检测、构建与发布 + +```mermaid +flowchart LR + A[按区服版本通知与补扫] --> B[观察版本和内容身份] + B --> C[冻结 master 与 catalog 快照] + C --> D[同一 evaluator 导出歌曲与 Snap 数据] + D --> E[来源、支持范围和回归检查] + E --> F[上传不可变数据与资源] + F --> G[读回校验] + G --> H[切换该区服现行 manifest] +``` + +1. **检测**:复用 masterdata-sync 通知及定时补扫。通知只表示“需要检查”,不作为可信版本输入。日扫是当前兜底;需要更短延迟时,先测源同步间隔和构建耗时,再调补扫频率,避免重复大构建。 +2. **冻结**:读取源 index,核验 manifest 与每个所需文件 hash。下载使用确定快照路径;若服务只有可变路径,则下载后再次核对 index 内容身份。源在过程中变化时丢弃不一致结果并排队最新版本。 +3. **计算**:歌曲计算、固定队伍复算和组卡计算共用 Deck evaluator。导出完整谱面事实、音乐/计分时长、评级门槛和 Snap profile;线性统计只在已经证明适用的域保留,不能扩展为任意技能公式。 +4. **检查**:内容/schema/引用检查、当前消费端 smoke、支持范围检查和定向回归全部通过。发布前再次比较源身份;旧构建不覆盖新的源观察或现行快照。同区服普通与 prebuilt 发布使用同一个锁。 +5. **发布**:先上传按内容哈希命名的不可变数据、WASM 和 profile,再读回校验。最后只切换一个包含上述引用及身份的区服 manifest。归档保留用于复现和回退;读回失败时不切换指针。 + +现行 `music-data.json → build.json` 顺序不能视为跨对象事务:前者写入成功、后续检查失败时,可能出现文件已更新而 marker 仍旧的状态。迁移前仍以单个自包含 JSON 的 provenance 为其身份,不让客户端把旧 marker 和新文件拼起来。迁移后的客户端从唯一 manifest 读取不可变文件,校验实际字节后才激活完整快照;旧路径仅作兼容输出,不承担多资源一致性协议。 + +## 数据修正与新规则分别处理 + +自动刷新不能要求每次标题、门槛或已支持技能参数变化都重新恢复整份原生库,也不能让新枚举静默进入旧公式。 + +| 变化 | 自动处理 | +|---|---| +| 标题、歌曲归属、奖励门槛等源事实 | 核验新快照与映射后更新;保留字段原义,避免评分系数、每人要求分数和房间总分混用 | +| 已认证域内的技能数值、时长、谱面 | 运行依赖闭包和边界检查,用固定 evaluator 重算;回归通过后发布,并标明认证的适用域 | +| 新 effect/condition/target/cumulative 类型,或未认证的组合 | 发布已核验的源事实可以继续;受影响计算标为 unsupported,不产生虚假排行数值 | +| 原生客户端/patch 改变,或参数越出认证域 | 新源数据与旧计算认证分开;受影响 meta 保留最后通过快照及日期,等待最新原生差分 | +| exporter/模型修复 | 同一源快照重新计算,recipe/commit 与数据 hash 改变;回归通过后替换计算结果 | + +支持范围是结构化规则,包含版本、模式、技能种类/组合、参数域及验证报告 hash;不能只写一个全局 `verified=true`。标准技能榜、固定真实卡组榜和玩家持有卡池搜索榜分别声明输入。玩家选择 Snap 后,排行 profile identity 必须进入缓存键;更换歌曲时重新计算标签条件与综合力。 + +## 两首歌的定向回归 + +`100021`《青春コンプレックス》和 `100026`《Ave Mujica》作为维护用例,检查所有难度、原始门槛字段、计分音符转换、技能/fever 边界及音乐长度。回归基于版本化原始表和谱面,不硬编码榜单名次。 + +每次比较输出明确区分:源表差分、资产差分、evaluator 差分,以及相同输入下的消费端显示差分。原生差分使用同区服、同快照和实际生效客户端/patch。某个排行下降只有在 profile、打法、随机条件、模式和目标相同的情况下才是可比较的模型结果。 + +## 玩家看到的结果 + +玩家需要知道当前区服、数据日期、计算条件和结果是否仍可用。失败状态保留最后通过的数据,并展示最后成功版本和受影响计算范围;不能每次后台检查失败就清空已有榜单,也不能继续把旧榜单称为最新。 + +打开歌曲或组卡会话时固定完整数据身份。后台发现新版后提示更新,玩家选择切换时重建 roster 映射和模型会话、增加 inputRevision、丢弃旧响应。搜索中途不热替换 master。缓存按区服、数据 hash、模型 hash、profile 和 scenario 区分,玩家持有事实不因清理计算缓存丢失。 + +## Draft 验收 + +- 同 master 版本下 manifest/文件内容变化仍触发重建;仅观察时间变化不触发。 +- master、资源、evaluator/exporter、schema/recipe 和固定消费端改变都有独立的检测证据。 +- 两区服独立构建和发布;dispatch、补扫及 prebuilt 同区服发布互斥。 +- 源在下载或发布前切换、缺表、未知技能、hash/schema/smoke 失败都不激活混合快照。 +- 上传中断、读回失败和指针切换失败有可重试测试;回退只指向完整且通过检查的归档。 +- 自动任务的成功摘要记录 observed/built/published 三个身份与时间;失败任务保留无凭证的诊断及检查报告。 +- 两曲数据门槛回归与共享 evaluator 的 song/deck 同输入一致性回归通过;最新原生认证缺口单独列明。 + +先修复已有发布链并形成 Draft,再把快照协议纳入 MoeNotes 的正式接入。开启生产发布、迁移客户端与最新模型认证是不同的交付状态。 diff --git a/docs/deck-design/latest-apk-certificates.json b/docs/deck-design/latest-apk-certificates.json new file mode 100644 index 00000000..f7055983 --- /dev/null +++ b/docs/deck-design/latest-apk-certificates.json @@ -0,0 +1,40 @@ +{ + "schema": "ournotes.apk-certificate-read/1", + "scope": "APK v2/v3 signing block certificate fingerprints read and size framing checked; no cryptographic APK content signature verification performed", + "files": [ + { + "file": "base.apk", + "signingSchemes": [ + "0x7109871a", + "0xf05368c0" + ], + "certificateSha256": [ + "34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e" + ], + "cryptographicSignatureVerified": false + }, + { + "file": "split_config.arm64_v8a.apk", + "signingSchemes": [ + "0x7109871a", + "0xf05368c0" + ], + "certificateSha256": [ + "34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e" + ], + "cryptographicSignatureVerified": false + }, + { + "file": "split_UnityDataAssetPack.apk", + "signingSchemes": [ + "0x7109871a", + "0xf05368c0" + ], + "certificateSha256": [ + "34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e" + ], + "cryptographicSignatureVerified": false + } + ], + "sameCertificateAcrossSplits": true +} \ No newline at end of file diff --git a/docs/deck-design/latest-apk-identity.json b/docs/deck-design/latest-apk-identity.json new file mode 100644 index 00000000..bd09b141 --- /dev/null +++ b/docs/deck-design/latest-apk-identity.json @@ -0,0 +1,29 @@ +{ + "schema": "ournotes.latest-apk-identity/1", + "app": { + "package": "com.bushiroad.sirius", + "version_name": "1.0.4", + "version_code": 10053 + }, + "sourceSha256": "c4b8119a9115d0fadcf07bc32316e22cb11d10cdd39ca803d2b298513bc41534", + "sourceProvider": "APKPure", + "GooglePlayDeliveryVerified": false, + "files": [ + { + "file": "base.apk", + "sha256": "4a095057b6cdc473b7ff67d82e88198507ae9c3a5f4114786d5cd2ad5f340d44", + "manifestMatches": true + }, + { + "file": "split_UnityDataAssetPack.apk", + "sha256": "8396375d0a3990805cbbc0151feb3b13e966c93cfff4a848313cd11b5c76ddd2", + "manifestMatches": true + }, + { + "file": "split_config.arm64_v8a.apk", + "sha256": "940b70c049e32cdff488608bca8cd40b716e6bec80763b480a191904e7b2e237", + "manifestMatches": true + } + ], + "signatureVerification": "pending" +} \ No newline at end of file diff --git a/docs/deck-design/latest-game-read.json b/docs/deck-design/latest-game-read.json new file mode 100644 index 00000000..c13d8780 --- /dev/null +++ b/docs/deck-design/latest-game-read.json @@ -0,0 +1,14 @@ +{ + "schema": "ournotes.latest-game-read/1", + "observedAtUtc": "2026-10-01T04:12:06.443472+00:00", + "region": "jp", + "requestClientVersion": "1.0.4", + "source": "unvalidated client-extracted URL candidate, anonymous Version attempt", + "sourceSha256": "ca6c84247d398e0d839afd7b29d424ec2b4c041128e27768a1a13042df9c87e7", + "candidateCount": 1, + "serviceUrlValidation": "not established for this early attempt; it is not evidence that the configured official endpoint is unavailable", + "nativeEquivalence": "not established", + "status": "candidate-read-failed", + "errorType": "GameApiError", + "error": "game API call Version to the API root failed: UNAVAILABLE (server unreachable)" +} diff --git a/docs/deck-design/latest-native-source-binding.json b/docs/deck-design/latest-native-source-binding.json new file mode 100644 index 00000000..c89357c5 --- /dev/null +++ b/docs/deck-design/latest-native-source-binding.json @@ -0,0 +1,26 @@ +{ + "schema": "ournotes.latest-native-source-binding/1", + "package": "com.bushiroad.sirius", + "versionName": "1.0.4", + "versionCode": 10053, + "members": [ + { + "archive": "base.apk", + "member": "assets/bin/Data/Managed/Metadata/global-metadata.dat", + "bytes": 28386492, + "sha256": "bbf82e20b16ac39ad14c6d3547250efb84daa6ae2c754e46ae9bae5270f9386c", + "matchesExtractedCandidate": true, + "extractedSha256": "bbf82e20b16ac39ad14c6d3547250efb84daa6ae2c754e46ae9bae5270f9386c" + }, + { + "archive": "split_config.arm64_v8a.apk", + "member": "lib/arm64-v8a/libil2cpp.so", + "bytes": 207718136, + "sha256": "b1600689ee83c435b4a3eaa1ef0709b1ed27783eeda90dace814515a54e0a7ce", + "matchesExtractedCandidate": true, + "extractedSha256": "b1600689ee83c435b4a3eaa1ef0709b1ed27783eeda90dace814515a54e0a7ce" + } + ], + "runtimePatchState": "unconfirmed", + "semanticEquivalenceToOldNative": "not established" +} \ No newline at end of file diff --git a/docs/deck-design/model-validation.md b/docs/deck-design/model-validation.md new file mode 100644 index 00000000..a9eb0402 --- /dev/null +++ b/docs/deck-design/model-validation.md @@ -0,0 +1,91 @@ +# 最新游戏建模:证据矩阵与版本冻结门禁 + +状态:2026-10-01 只读模型审计及验证设计。当前没有完成 JP 最新客户端的数值模型认证。正式 SDK、网页组卡和搜索优化实现应等待版本身份与模型语义门禁;已产生的 SDK / UI 实验保留为未验收 PoC,不以实验能编译或旧版本差分通过替代前置证据。 + +## 两个输入门禁的当前状态 + +| 门禁 | 当前状态 | 已核实的事实 | 现实缺口与解除条件 | +|---|---|---|---| +| G1:目标客户端、平台与运行时 patch 身份 | **PARTIAL:Android 1.0.4 / build 10053 APK 与 SO / metadata 已绑定;恢复、签名完整验证、patch 与语义尚待验证** | [来源绑定](latest-native-source-binding.json)核实 APKPure 的完整 `10053` 包:XAPK manifest 和 AndroidManifest 一致为 `com.bushiroad.sirius`、`1.0.4` / `10053`;APK 文件 SHA 与 manifest 一致,SO / metadata 与下表逐项匹配。[证书记录](latest-apk-certificates.json)读取三个 split 的 v2/v3 证书,SHA 一致为 `34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e`,尚未完成 APK 内容签名密码学验证。[Apple 官方读取](official-store-read.json)另证明日本 iOS 商店 `1.0.4`,App ID `6771716739`,发布日期 `2026-09-29T11:38:20Z`;两平台证据独立。 | 对已绑定的最新 ARM64 SO 恢复保护区并校验 CRC、metadata、注册关系和原生执行;完成内容签名验证或保持明确限制;冻结实际生效 IFix 集合/哈希/加载条件,或证明指定实例没有生效 patch。取得完整 APK 解除了“没有最新包”的身份阻断,但未解除最新规则认证门禁。 | +| G2:当前 JP master / resource 的新鲜度与一致快照 | **UNAVAILABLE:当前版本未重新确认;最后可信快照可用于离线研究** | 本轮 Windows 与 CNB 的匿名 `MasterdataService/Version` 查询均返回 `UNAVAILABLE`;[读取记录](latest-game-read.json)不是有效 Version 响应。最后可信 2026-09-30 快照来自 `runtime-data/provenance.json`,版本见下表。 | 成功取得官方 Version 响应及其时间/来源,下载或核验对应 master/resource manifest 和所需文件哈希。将规则表、谱面、音频长度、活动上下文绑定同一个快照。未解除前不得把 `1.0.0.300` 描述为“2026-10-01 当前最新版”。 | + +两个门禁独立:读取到新 master 不会补齐客户端机器码证据;取得新 APK 也不会自动证明使用的是它在当前区服实际加载的 master、资源和 patch。**老 native + 新 master 只是一种明确的混合输入实验,不能代替最新客户端建模。** + +本轮原生恢复进展:最新保护区的三个 CRC 已逐项匹配,错误候选均失败;loader 的 275 个轮转与 1,096 个编码符号,已在最新 ARM64 对应函数中执行并与恢复结果比较一致。metadata 已校验 31 个表区和全部 47,081 个 string offsets 的单调性及边界,并修正恢复工具在头部 XOR 终点的错误。当前正在执行首次完整运行时启动验证。上述计数属于恢复与结构检查,**不是新增计分/技能原生用例**;完整启动、对象布局、激活 patch 及游戏语义仍须独立通过。 + +最后可信 JP 快照(来源为本轮 `runtime.tar` 内的 `runtime-data/provenance.json`,不是本轮成功的在线 Version 读取): + +| 输入 | 冻结值 | +|---|---| +| region | `jp` | +| masterVersion | `1.0.0.300/52355a9de56a475f691b10ef58acba58` | +| resourceVersion | `1.0.0.300` | +| resourceHash | `223330d93bcef18aa092c73dbe3ac64d` | +| officialCatalogSha256 | `409e188690ab1147fbdfbe69b15729d8f86e76f815388c06fef9a33072f0975e` | +| masterManifestSha256 | `f4534fe8bf340b7a9bd99eb9b43ab384e9e3895a7e1eb3113cbb371e65780735` | + +## 已有原生基线究竟是什么 + +[Deck 原生来源清单](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/validation/native-source-2026-09-30.json)记录:客户端 Android ARM64 **1.0.1-25**,区服 **TW**,master `0b21c9f4a3911370d5f981fb894c7544`,原始恢复 ELF SHA-256 `5a79dd5d9c479ed5a636193394f040f21a1f7b070d0869095e1af63b3f84ef88`。ELF、重定位内存映像、初始化只读快照和 Rust 源码的哈希是不同身份,不能互换。 + +[原生验证报告](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/native-validation.md)与其独立清单记录了音符/综合力/技能/逐帧普通与撃奏、排名、部分 PlayingState、特定 Touch 输入等对照。它们支持所声明输入和观察阶段上的精确一致,不能把旧 TW 验证日期更新成新 JP 认证日期。 + +[共同 root 原生链](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/recommendation-coverage.md)实际新增两组各 7,500 帧,总 15,000 原生帧、506,414 基线字段比较,仍是 `1.0.1-25` + TW master。R3 的两份显式数字步长请求复用了这些捕获并比较 18 个输出字段,新增原生帧为 0;其 144 个生产候选中,新增完整原生区分用例证明的是指定卡组,其余候选属于模型计算。 + +最新 JP 文件已与完整 Android `1.0.4` / `10053` 包绑定。以下 `_tmp/jp-apk` 副本的 hash 与 APK 内条目一致,可作为只读恢复输入;其原生语义尚未认证: + +| 文件 | 大小 | SHA-256 | 目前能证明什么 | +|---|---:|---|---| +| `_tmp/jp-apk/lib/arm64-v8a/libil2cpp.so` | 207,718,136 B | `b1600689ee83c435b4a3eaa1ef0709b1ed27783eeda90dace814515a54e0a7ce` | 匹配 `split_config.arm64_v8a.apk` 条目;身份区别于旧基线。整库 hash 不提供算分语义差异证明。 | +| `_tmp/jp-apk/assets/bin/Data/Managed/Metadata/global-metadata.dat` | 28,386,492 B | `bbf82e20b16ac39ad14c6d3547250efb84daa6ae2c754e46ae9bae5270f9386c` | 匹配 `base.apk` 条目;ACE 原始头标记 `0x12724394`,须按最新输入恢复及结构/运行时核实。 | +| `split_config.arm64_v8a.apk!/lib/arm64-v8a/libanort.so` | 1,883,272 B | `47a9ceacb4850b311972ffebdde37905a3f7baa5c5588f2be7b2e1d2e9333b74` | 最新保护 loader;恢复需从本文件重新定位配置/函数,旧版脚本的 VA、偏移及 key 不能直接复用。 | + +## 验证分母与证据层次 + +每行必须先定义分母,再报告 covered / not_run / incomplete / mismatch。没有最新快照时,最新规则分母记为 **TBD after G1/G2**,不填一个根据旧表猜出的“100%”。至少维护以下彼此独立的分母: + +- **规则分母**:目标版本实际可达的 mode/action、effect/condition/cumulative/target 类型、技能等级与数据形状、判定类型和资源格式。未在当前 JP master 出现的 Arena 等分支只保留 synthetic 身份,不能计入线上支持覆盖。 +- **分支分母**:关键函数正常/边界/异常路径,触发与不触发、释放与不释放、限次、池耗尽、生命归零、早/迟输入、取整/溢出与回退路径。列出每个缺失分支及其最小区分输入。 +- **交互分母**:按真实可持有成员/留影建立机制交互类别;逐卡单测不等于组合条件、持续效果、判定转换和随机调用顺序全覆盖。代表性用例必须记录选取理由,不能把抽样称为枚举全部卡组。 +- **动态实验分母**:独立输入 trace、原生执行帧、原生函数调用、观察字段数分别计数。复跑、typed 恢复、负控制和 WASM 对拍复用同一 capture 时不增加原生样本数。 +- **搜索分母**:合法物理队伍/配对/槽位域、根质量、目标和约束;独立穷举的候选数与点模型 native 证明分开。算法能找到同一模型的最优解,不证明该模型就是最新游戏。 + +验收状态建议使用 `verified-in-declared-domain`,附完整版本与覆盖范围;“全部完美还原”不能由大量零差异字段数推出。 + +## 模型验证矩阵 + +| 机制 | 旧版本已有证据与不能扩大的结论 | 最新版本必须核实的分母 / 区分用例 | 严格通过条件 | +|---|---|---|---| +| 综合力、培养、场景加成 | 旧基线数值原语与槽位证据;共同 root 新链的综合力 75,038 是注入控制量,未执行完整玩家 profile 合力构造器。 | 最新培养状态/等级/特训/卡阶/技能、角色 Rank/道具/VIP/回忆;歌曲属性/标签、队长、留影连携、Challenge/Arena/活动分支。新增字段、枚举、表列与合法状态边界逐项列出。 | 新版本对应函数/ABI/查表路径可定位;对相同合法输入逐项整数或 float32 位模式一致;模型默认值、真实玩家输入和缺失信息明确区分。 | +| Skip 与音符计分 | 旧 native 单元、评级与 skip 计算;不自动证明新模式仍允许 Skip。 | 当前各 mode/action 资格、分数类型/operate type 映射,连击/生命/幸运因子,取整、40 ms 帧边界、非计分 Pass 门控、正/负/溢出边界。 | 同输入执行最新 native 与 Rust,整数完全相等、声明 f32 字段 bit-equal;不通过容差掩盖分歧,Unknown/Unsupported 与原生异常区分记录。 | +| 整场普通 Live 与回退重算 | 旧混判 × 30/60/120 fps、长谱与同帧多判定证据;仅覆盖记录的卡/谱/输入。 | 真实早/迟判定、同帧顺序、rewind、技能触发/结束同帧,音频/技能长度与计分表长度的独立来源,Pass 回调、显示缓存与 settled score。 | 不移动帧、不删除失败字段,逐帧身份/阶段/分数/生命/连击/池与因子一致;额外复核终分相同但中途状态不同的负控制。 | +| 普通 / 留影 / 撃奏技能与条件 | 旧真实技能和 4011 定向成功分支;大量字段不等于所有真实卡交互或重复触发池耗尽已证。 | 最新 master 中所有使用的 effect/condition/cumulative/target/level 形成分母;AND/OR、选择门槛与持续状态、扩时/重叠/释放/限次/五池耗尽,多个技能改变同一状态的先后顺序。 | 最新代码可达分支与实际 master 形状逐项映射;核心被测逻辑不可 HLE 代替;触发结果、内部状态与效果应用三层各自通过。未知种类/组合不得默认成支持。 | +| 撃奏与排名阶段 | 旧 Combo/Just/Luck、Solo 时间查询与 Network 冻结快照、部分 packet 队列证据。 | 三种 mission 的 Start/End/Delay/Complete/Finish;Just 开关恢复、幸运/连击/累计 Just、排名前后两种 score、同帧区间完成、迟到/批量确认、每帧单结算。旧“第四 fever 开始异常”必须在目标版本重新确认。 | 最新逐阶段 native 对照;固定 Solo 名次、外部确认与真实对手模式分别标识。外部排名时间线只能证明条件结果,不能称为对手预测。 | +| 随机与成员/留影配对 | 旧 common-root 构造器、4 次 shuffle 与 Skill/Luck 子流、4011 f32 值及负控制;真实 TickCount 人口分布未知。 | 最新根来源、派生种子、洗牌算法、完整 post-shuffle 状态、真实留影跟随、更多 signed root/生命条件/概率与 Luck 交互;逐次返回值与调用顺序。 | 构造器→配对→条件→幸运→结算共享同一声明 root 的 native 对照通过;不得洗牌后重置或替换 root。有限根结果继续注明条件分布,不能据一个成功分支估计总体概率。 | +| 原始输入、窗口与 Assist | 旧仅一份 1,239 帧/50 事件 Touch 私有适配样本;生产完成判定流不等于完整设备触控。 | 最新投影/输入时钟、Flick/Trace/长条、同帧竞争、动态窗口/Assist、末尾/Force、判定 type/timing/原始 diff 与转换后结果。 | 输入同源、保留原始顺序/字段/阶段;设备资源适配与核心算法分别列明。完成判定流和物理 Touch 分两份契约与覆盖报告。 | +| 失败、继续、重试、退出 | 旧生命归零后的离线继续步进只约束计分状态。`scoreAndLifeAtLeast` 是结算分数与血量联合条件,不是通关概率。 | 各正式 mode 的零生命状态迁移、同帧 damage→heal/guard、失败暂停、继续的成本/恢复/扣分、重试的 RNG/池/缓存重置、退出/断线与结算门控。 | 真实 PlayingState / 生命周期 trace 完整;明确什么时候演出停止、是否登记成绩/奖励。仅比较 finalLife 不足,未证生命周期不得输出“稳过谱”。 | +| 活动、成绩登记、客户端奖励 | 旧客户端 preview 和部分模式结算证据;不能证明服务器实际奖励选择/发放或当前榜单规则。 | 当前活动时间、评级、计数、消耗倍率、失败门控、可登记的歌曲/难度/模式与最高记录规则;当期分数排名与 pt 收集分开。 | 客户端计算按最新冻结输入与阶段差分通过;服务器 / 官方规则证据单独认证。无服务端证据时维持 `client-preview` / `conditional` 标签。 | +| 搜索合法域、目标与完整性 | 现有独立穷举证明的是声明点模型上的 Top-K;candidate/cap 结果的点值精确而排名未证。 | 最新规则认证后的合法队伍/队长/留影/槽位与资格域,score/threshold/cap/joint-life 等目标、tie 顺序、共同根质量、未知字段、超时 partial、剪枝边界。 | 独立枚举和排序不复用剪枝/Top-K;结果、槽位、配对和精确 payoff 一致;剪枝有可采纳证明,等值 tie 不误剪;部分候选丢弃,终止/optimality 保真。搜索速度门禁在模型和策略冻结之后。 | + +## 新旧版本迁移怎样验 + +建立三个互不替代的版本运行格:旧 native + 旧 TW 输入是已有基线;**新 native + 新 JP 输入才是目标认证格**;旧 native + 新 JP 输入仅用于定位数据敏感性并标为混合实验。不能从第三格推导第二格的等价结论。 + +先对 manifest 与规则数据建立稳定 ID、字段、表 schema、枚举和引用差异清单,分类为新增/删除/数值变化/重命名/迁移未知。只按 ID 相同或表 hash 相同不能证明语义或账号数据可跨区服迁移。再由新 metadata/ABI/注册关系定位关键函数;旧 VA 和 TypeDefIndex 不能直接当新版本地址。保留相同、已变化和未解析的函数清单,关键路径的异常与间接调用也要检查。 + +对每项差异做最小区分输入,在旧、新 native 和对应 Rust 实现下记录结果;预期发生变化的案例不要求“旧新终分相同”,而要求新模型解释并精确复现新行为。新增 skill/mission/生命周期分支有自己的 native 实验;未变分支只有在实现、调用约定、常量/查表、patch 选择与拼接顺序等关键依赖都匹配时才能继承证明。 + +用户卡池迁移只保留已验证映射的身份与培养,不能把新上限当成玩家已经升级。模型/图库/master identity 改变时,旧识别来源与校对记录继续保留,缓存的 prepared pool / simulation / recommendation 失效;未映射卡、变化中的枚举和缺失的培养字段要求明确校对。相同 UTF-8 大整数/数字 token、重复字段处理、未知字段和缺失值契约另做兼容回归,不能通过 JS Number 往返或静默默认掩盖变化。 + +## 输入冻结与正式实现放行 + +每个受测版本记录 `region`、平台、appVersion/versionCode、应用签名/取得来源、APK/base/split SHA、SO/metadata SHA、恢复/重定位过程与映像 SHA、实际生效 patch 清单/哈希、master/resource Version 原始响应与时间、manifest SHA、相关表/谱面/BGM SHA。每次对照另记录 Rust 源快照/commit/dirty 状态、编译产物 SHA、工具版本、输入 trace/root/时钟、观察字段及阶段、HLE 清单和原始输出。 + +放行顺序: + +1. G1/G2 通过,并发布一个明确、可重查的目标版本快照;若主动选择研究旧版,则单独命名旧版研究,不宣称当前版本。 +2. 最新关键规则与状态模型认证,覆盖分母、差异和未支持范围可审阅;缺轨迹为 `not_run`、缺字段为 `incomplete`、真实不相等为 `mismatch`,任何一种都不写成通过。 +3. 确定玩家目标、输入与合法域,冻结搜索策略、可采纳上界与结束条件;小池独立 oracle 和必要差分通过。 +4. 才开始正式浏览器 SDK / Worker / UI 接入;原生与 WASM 的同源对拍证明运行端移植,不新增最新游戏原生证明。性能与首组候选时限是另一个验收项。 + +当前待解除的是**最新已绑定 APK 的原生恢复、规则与运行时 patch 认证,以及当前 master 新鲜度核验**。已授权在 CNB 进行最新原生恢复、最小执行和大规模差分;每阶段通过自身门禁后才扩大实验。未验收 PoC 和旧版实测保留各自身份,不推进 SDK 安装、编译或新的浏览器组卡验收。 diff --git a/docs/deck-design/official-store-read.json b/docs/deck-design/official-store-read.json new file mode 100644 index 00000000..393d505a --- /dev/null +++ b/docs/deck-design/official-store-read.json @@ -0,0 +1,16 @@ +{ + "schema": "ournotes.official-store-read/1", + "source": "https://itunes.apple.com/lookup?bundleId=com.bushiroad.sirius&country=jp", + "observedAtUtc": "2026-10-01T04:14:16.763244+00:00", + "resultCount": 1, + "results": [ + { + "bundleId": "com.bushiroad.sirius", + "version": "1.0.4", + "currentVersionReleaseDate": "2026-09-29T11:38:20Z", + "trackId": 6771716739, + "trackViewUrl": "https://apps.apple.com/jp/app/%E3%83%90%E3%83%B3%E3%83%89%E3%83%AA-%E3%82%A2%E3%83%AF%E3%83%BC%E3%83%8E%E3%83%BC%E3%83%84/id6771716739?uo=4", + "sellerName": "Bushiroad Inc." + } + ] +} \ No newline at end of file diff --git a/docs/deck-design/player-objectives.md b/docs/deck-design/player-objectives.md new file mode 100644 index 00000000..8e44db61 --- /dev/null +++ b/docs/deck-design/player-objectives.md @@ -0,0 +1,86 @@ +# 玩家目标与组卡模型前置设计 + +状态:2026-10-01 设计审计。现有 BoxLens / MoeNotes 实现保留为未提交 PoC;本文件不表示上线方案已经批准,不启动新的业务实现。先确定真实目标、规则与输入,再决定模型和搜索。 + +## 先把玩家的目标分清 + +玩家说“活动排名”,不能默认解释为累计活动 pt。现有 2026-09-30 官方补充公告捕获明确区分:该活动没有累计活动 pt 排名,排名采用歌曲分数。原始公告与最新变更由 root 继续核验;未完成前不声明当前活动的所有排名细则已经确认。 + +因此,“活动排名冲分”和“活动奖励 / pt 收集”必须是两个目标。日常冲分、稳定过线、跳过刷取、撃奏和有限资源培养也不能共用一个“综合力最高”按钮。 + +| 玩家目标 | 应最大化 / 最小化的量 | 推荐成立前要确认的真实规则 | 必需的额外玩家输入 | 玩家应看到的结果 | 现有模型边界与前置决定 | +|---|---|---|---|---|---| +| 日常冲分 | 指定歌曲、难度、模式和打法条件下的分数;明确是期望、低分位还是单次最高分 | 该模式可用动作、歌曲加成、技能、结算与随机机制;榜单是否存在及记录规则另核 | 当前培养与普通技能;歌曲 / 难度;理论打法或实际判定记录;参选 / 队长偏好 | 五个物理槽、第三槽队长、对应留影;条件分数范围;相对当前队伍改善;候选 / 已证明状态 | 可计算声明条件下的 native Live。当前有限随机根分布未知,AP 不等于玩家实战;不能把最幸运一局或人为最佳技能顺序当成玩家可执行方案 | +| 活动歌曲分数排名 | 与当期榜单实际登记的歌曲分数指标一致 | 官方公告当前指向歌曲分数排名;仍需核实哪些歌曲、难度 / 模式、单曲还是合集、最佳记录还是其他汇总、并列处理、活动时间、挑战限制及失误 / 断线结算 | 指定活动与目标榜;对应歌曲和难度;报名 / 解锁 / 挑战资格;实际可用次数与打法 | 每个适用榜单的可照摆队伍、可登记分数的模型条件与缺口;不拿活动 pt 冒充排名分 | 与 `clientEventPoints` 分开。当前模型能评估部分分数机制,但不能仅凭已有通用 score 指标推导当期真实排名名次;最新原始规则待 root 核验 | +| 活动 pt / 奖励收集 | 根据玩家意图选择每次 pt、每单位消耗 pt、每分钟收益或目标奖励进度 | 当期 pt 计算、评级、活动加成、消耗倍率、计数 / 窗口、挑战点转换和奖励来源;多个目标的消耗需同口径比较 | 实际活动计数、剩余资源、倍率选择、已取得奖励、时间预算;多人场景还需结果条件 | 每次客户端 pt 预览与明示消耗;若选择效率目标,显示同口径比值和必要假设;无法核实的服务器奖励不承诺 | 当前 `clientEventPoints` 是客户端每局预览,尚不是排名目标,也不是资源效率 / 多局调度器。`conditionalClientEventItems` 需要明确奖励行,不能预测服务器所选掉落 | +| 稳定达标 / 安全过关 | 达到指定分数、评级或任务条件的概率;玩家可另选保留血量、失败率或较小欠分 | 分数门槛与评级表;失败 / 重试 / Continue 的真实判定;目标是结算血量还是全程不失败 | 指定门槛;实际判定记录或经确认的多个打法场景;是否允许重试 / Continue;对低分和失败的容忍度 | 明确的达标条件、欠分与条件分布;若只有 AP 试算,标为理论达标比较 | `scoreAtLeast` 只反映给定判定流与随机根条件的分数达标;`cappedScore` 可用于欠分效用。`scoreAndLifeAtLeast` 只验证结算分数和结算血量,不证明从未归零,也不预测真人发挥。实战成功概率需要额外打法分布和失败流程证据 | +| 跳过演出 | 跳过分数、确定性评级、客户端 pt 或已明确的刷取效率 | 当前模式是否真实允许 Skip;门票 / 消耗、评级和奖励规则;不能把未允许的模式列为可操作建议 | 当前卡片与玩家加成;歌曲 / 难度;目标门槛;实际门票、消耗与活动状态 | 跳过分数 / 门槛结果和所需消耗;与演出队伍分开展示 | Power / Skip 不依赖普通演出技能顺序;对应技能不应成为该目标的无意义必填项。已检查域内的单调分数 / 阈值可使用现有 exact canonical 快路;pt / 奖励和效率不自动继承这个证明 | +| 撃奏冲分 / 多人表现 | 必须先选择最终分数、区间任务、排名条件或活动收益,不能把“开启撃奏”当完整目标 | 模式是否强制撃奏;任务、Just 区间、幸运、排名与冻结快照;多人确认的到达 / 应用机制;实际榜单规则 | 撃奏技能;对应模式和歌曲;判定 / 帧条件;Solo 或明确多人假设。多人结果时间线不由卡池截图取得 | 最终条件分数、区间表现、条件名次与依赖外部输入;显示原生随机机制下的物理队伍 | 现有模型支持有限根分布下部分撃奏机制。Battle / Arena 的输入排名是外部条件,不是由玩家卡组预测出来的真实对手排名;不能默认 Solo 第一。当前 JP 缺 Arena 行不能以 synthetic 验证替代线上可用性 | +| 有预算的培养建议 | 在玩家给定资源和偏好约束下,提高指定目标,或以最小消耗达标;输出成本 / 改善的 Pareto 选择 | 每种升级 / 特训 / 卡阶 / 技能 / 道具 / 角色等级 / 回忆的合法状态转移、材料成本、前置解锁、资源共享和购买限制 | 当前状态、所有相关材料 / 金币 / 票数、可用转换、预算;可升级的范围;不可消耗的稀缺物;真实目标与时间范围 | 当前队伍与具体升级步骤;逐资源成本、剩余预算、收益和替代方案;避免“全部满级”结果 | 现有请求不支持培养预算搜索;不能先用满级队伍再补一个成本数字。须先核状态转移 / 成本表,再设计联合培养与组卡搜索。对角色等级 / 道具等全局升级,要评估它对全部候选的影响 | +| 仅查看综合力 | 当前培养与指定歌曲 / 模式加成下的综合力 | 属性、标签、队长、留影连携、玩家全局与场景加成 | 当前培养、玩家全局状态;可选歌曲 / 场景 | 综合力和加成条件,可照摆队伍 | 可以作为玩家明确选择的独立目标或搜索候选来源;综合力候选不等于演出 / 活动最优证明 | + +“单次最高分”和“期望分数最高”也不同。若榜单记录最高一局且玩家准备重复挑战,目标可能是指定次数内的最佳登记分数或达到目标记录的机会。它需要核榜单规则、次数预算、局间随机独立性与打法变化,不能直接把单局均值乘次数,也不能无条件沿用单局达标概率。 + +## 输入应按目标需要收集 + +| 信息 | 当前清晰卡池列表截图能否取得 | 正确的后续处理 | +|---|---|---| +| 卡片身份 `(kind, master ID)` | 可识别已观察且匹配当前图库的成员 / 留影;不能证明未见卡不存在 | 保留原图、定位框、图库 / 模型版本和来源;误识别可改身份 / 明确删除,漏卡可补录;无法确定时不编造 ID | +| 等级 | 仅等级视图实际可读的值 | 其他能力 / 百分比视图不转换成等级;裁切、拒识和冲突保留未知 | +| 卡阶 | 图标完整且匹配通过时可读 | 不从稀有度或卡面推断;卡阶 0 模板是拒识用途,不输出合法已拥有 Rank 0 | +| 成员特训次数 | 仅特训视图实际可读的值 | 不从等级上限、另一张卡或默认值推断;当前契约 1–5 不擅自换算成自然计数 0–4 | +| 普通 / 撃奏技能等级 | 当前列表识别链不能取得 | 活跃目标需要哪类技能就补哪类;Power / Skip 不强迫填写未参与计算的技能。选目标后才生成缺失清单 | +| 全部角色等级、乐队道具、VIP、回忆 | 当前卡池列表不能取得 | 使用已有可靠玩家数据、专门截图或人工填写;基础状态只能是明确的试算条件,不标成实际玩家事实 | +| 完整持有集合与完整截图覆盖 | 不能取得 | 玩家可声明完整,或选择仅在已核对的观察子集推荐;子集结果不得标为全账号最优。声明和模型识别事实单独保存 | +| 当前活动计数 / 消耗 / 排名资格 / 剩余资源 | 当前卡池列表不能取得 | 按目标请求对应信息,活动时钟 / 资格 / 排名规则由最新规则源核对;不复制 mock 或其他活动数据 | +| 真人打法、错误分布、对手、网络排名、随机根总体 | 不能取得 | 理论条件、实际记录与外部假设分开;不知道就保留未知,不能从卡池规模或几张高分图估计真实成功概率 | +| 实际培养预算与材料成本 | 截图链目前不能取得 | 预算由玩家提供;成本由版本匹配规则取得。缺任一部分不输出具体花费方案 | + +现有客户端、模型和 OCR 都不是“所有字段必须填完”的理由。必填集合应来自已选目标的计算依赖:例如普通演出需要普通技能,撃奏开启才需要对应撃奏状态,Skip 不使用演出技能随机顺序。未使用字段保持未知,不能为了满足类型结构而写入 Rank 1 / 满级并把它保存为真实值。 + +## 持有事实与搜索卡池必须分开 + +以下三个动作不是同一件事: + +1. “这张图认错了 / 这张卡并非我的持有”:修改观察与持有事实,记录身份修正证据。 +2. “我拥有这张卡,但这次不想使用”:保留 owned 集合,用搜索约束排除。 +3. “我拥有它,但培养值还没填”:保持持有事实;补字段,或明确只在已核对候选中推荐并标注搜索范围不完整。 + +当前 PoC adapter 把导出候选卡池和 `ownedMemberCardIds / ownedSupportCardIds` 强制逐项一致,页面排除会同时收缩持有集合。正式设计不能照搬此耦合:回忆计数会读取 owned 集合,删掉“拥有但不参选”的卡可能改变全部候选的综合力。需要独立的持有 / 回忆事实、可评估候选和搜索约束。不能以“已确认完整持有”的全局复选框掩盖这种语义差别。 + +## 确认应防止错误推荐,而不是制造重填负担 + +推荐成立的检查顺序: + +1. 明确区服、活动 / 场景、真实玩家目标及选曲,绑定 master、谱面与模型版本。目标暂时不支持时先说明缺什么规则,不让玩家白填完整卡池。 +2. 上传与补图后保留观察、冲突、覆盖范围;合并去重不得消除分歧。同名文件不代表同一来源,新增截图不能静默覆写之前人工核对。 +3. 只列当前目标真正缺少的输入。对错误身份、未知培养与低置信度提供原图定位和可纠正入口,显示“尚缺哪项、哪种页面可以补”。 +4. 分别确认持有范围、实际培养事实和用户主动选择的试算假设。不要用一枚总复选框把原始观察、未填技能、完整账号覆盖和示例随机分布都升级成事实。 +5. 执行之前保存这次目标 / 卡池 / 玩家状态 / 假设的不可变快照。改卡、换歌、换目标、追加截图或改变活动规则后,旧推荐立即失效;迟到回复也必须绑定旧输入版本。 +6. 结果分层显示:实际当前状态 / 条件试算 / 子集推荐;候选排名 / 已证明排名;客户端预览 / 已核服务器规则。让玩家能直接摆五槽与留影,同时理解推荐适用于什么条件。 + +“只在已核对的卡中推荐”是可以设计的快速入口,但它必须保留完整 owned / 回忆事实,并解释可能漏掉更强的未评估卡。基础培养、示例随机根、假设多人名次只能生成带条件的比较结果。若关键未知会影响当前推荐排序,不能给出不带条件的“你的最强队”。 + +## 写模型 / 搜索前的决策门槛 + +| 决策 | 已知 | 仍待核实 / 决定 | 未通过时允许交付什么 | +|---|---|---|---| +| 当期活动排名目标 | 已保存官方补充指向歌曲分数排名,不存在所述累计 pt 排名 | 最新原始公告、活动 ID / 窗口、参赛歌 / 难度、榜单汇总、记录 / 并列 / 次数规则由 root 核 | 规则缺口与来源,不能上线“活动 pt 排名推荐” | +| 单局评分域 | 离线 1.0.1-25 原生单元及现有 solver 有版本化证据 | 当前区服 / 二进制 / 补丁等价与真实整局录制边界;不同目标是否涉及失败、断线或服务器处理 | 声明域内条件计算;不称当前线上整局完美还原 | +| 稳定目标 | 已有条件分数达标、欠分效用、结算血量效用 | 真人打法分布、全程失败规则、重试目标;用户想要哪个风险口径 | AP 或给定判定流下的条件比较,不称真人通过率 | +| 活动资源目标 | 已有客户端 per-atom pt / 条件奖励预览 | 真实消耗、效率分母、服务器奖励和多局状态转移 | 一局客户端预览;不承诺现实奖励或长期最优策略 | +| 截图输入范围 | 身份 / 等级 / 卡阶 / 可见特训及冲突契约已定义 | 新布局 / 降质覆盖、完整持有声明、哪些目标允许子集;未知全局状态如何补 | 观察集合和缺失清单;不推断全持有、满养成或零回忆 | +| 搜索输出语义 | 五槽、第三槽队长、paired Snap;候选搜索与条件穷尽的区别已定义 | 不同玩家目标的排序、并列、次数效用和允许动作;规模 / 时间预算真实测量 | 已精确评估的候选与边界;不能把快速候选叫全局最优 | +| 培养计划 | 当前状态的评分可作为转移后的评估器 | 版本化成本 / 转移图、共享资源、不可逆动作、预算和多目标偏好;联合搜索证明 | 需求清单 / 设计,不产生可执行花费建议 | +| 前端计算路径 | ONNX 网络与 Rust / Worker PoC 的技术边界可测 | 完整浏览器识别链、数据授权与版本更新、延迟、兼容性和本地 / 远端选择 | 技术可行性 PoC;不是玩家目标已正确或业务可上线的证明 | + +推荐最优性、模型与游戏一致性、OCR 正确性、输入完整性是不同证明。组件复用、80 个测试和截图到结果 E2E 验收可以证明特定实现路径可运行,不能替代上述规则和目标决策。 + +## 证据与来源边界 + +- 官方活动补充的保存正文来自 `@bang_dream_on`:[活动排名补充](https://x.com/bang_dream_on/status/2105100378702557677)、[后续活动说明回应](https://x.com/bang_dream_on/status/2105183476215648545)。本文件只使用捕获中明确的“累计 pt 排名不存在 / 歌曲分数排名”区分;最新公告与活动细则仍需独立核验。 +- 现有模型输入 / 目标 / 随机与结算边界:[recommendation-contract.md](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/recommendation-contract.md)、[recommendation-coverage.md](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/recommendation-coverage.md)。这些是实现和版本化验证范围,不是当前活动公告。 +- 截图可见字段、来源与 unknown / 冲突语义:[CONTRACT.md](https://github.com/empty-sekai/ournotes-boxlens/blob/6d3326d/docs/CONTRACT.md)、[MODEL_CARD.md](https://github.com/empty-sekai/ournotes-boxlens/blob/6d3326d/docs/MODEL_CARD.md)。当前 OCR 不读取普通 / 撃奏技能、全局玩家状态、预算或完整持有集合。 +- adapter 试验保留原始 Box 与修正证据,但全技能必填与候选 / owned 耦合属于 PoC 输入限制,不能作为正式产品必须如此的理由。正式契约应区分真实持有事实与搜索排除条件。 + +来源状态分为:已保存官方正文、版本化客户端 / 模型证据、玩家明确提供的事实、用户主动选择的条件假设,以及待核的最新线上规则。实现前应逐项标明,不能互相替代。 diff --git a/docs/deck-design/search-design.md b/docs/deck-design/search-design.md new file mode 100644 index 00000000..c81878a4 --- /dev/null +++ b/docs/deck-design/search-design.md @@ -0,0 +1,352 @@ +# OurNotes 组卡目标、搜索数学与验证门禁 + +日期:2026-10-01,Asia/Singapore。状态:**设计与只读审计,待最新版规则核验;不是已批准的实现或发布契约。** + +用户已要求先完成最新版游戏建模与搜索设计。本文件不授权继续修改 `src/`、`tests/`、浏览器 SDK 或页面。此前推荐入口、新指标、候选轮转与 WASM smoke 均降为 PoC 证据。PoC 的可运行、既有测试通过和同源 x64/WASM 一致性,分别不能证明最新版规则一致、全池最优或实战收益。 + +## 1. 首先需要解决的数学问题 + +1. 既有原生证据绑定离线客户端 **1.0.1-25**。最新版区服客户端、master、谱面和 IFix 的一致性未在本文件确认,不能把旧证据重新标成最新版。 +2. 玩家能选择物理队伍、队长、留影搭配和可能的养成行动;**不能选择原生随机技能顺序**。搜索必须把可控决策与环境随机变量分开。 +3. 固定判定流上更换随机根,只描述原生随机性。它不自动描述人的错判、连续漏判、疲劳、触控设备或动态判定窗对结果的影响。 +4. 综合力最高不是 Live 分数最高;分数期望最高不是最稳达标;活动加成最高不是活动收益最高。目标必须先定义可计算的终值或路径效用。 +5. 单次客户端活动点数预览不是实际服务器奖励,更不是跨多次演出的资源收益率。后者读取库存、上限、消耗和时钟,需要额外状态转移。 +6. 数值逐候选精确与搜索全局精确是两个不同证明。候选值正确不能为被截断的候选空间提供最优性证书。 + +## 2. 最新版规则待核清单 + +根代理负责取得最新版本身份,模型审计负责原生语义。搜索设计在以下输入完成前仅有条件成立。 + +| 编号 | 必须核实的规则 | 搜索/目标的具体影响 | 当前状态 | +|---|---|---|---| +| V1 | 区服、可获取的最新客户端版本、native binary SHA-256、IL2CPP metadata、IFix/热更清单、master manifest、谱面资产 SHA-256 | 模型与所有证明的身份;不同区服不能仅凭表名相同视为等价 | 待核,旧原生证据为 1.0.1-25 | +| D1 | 成员槽数、不同角色限制、队长槽、同卡变体/借卡限制、留影槽数与唯一性、空留影是否合法 | 合法决策域与枚举数量;不能由旧 UI 推断当前服务器约束 | 旧模型为五角色、槽 2 队长、留影最多一次;最新版待核 | +| R1 | 全五槽是否仍洗牌;留影是否跟随成员;root 生成、substream 派生、取随机数顺序、异常/条件检查是否消耗随机数 | 物理槽置换、等价折叠、缓存键和联合分布;不能优化一个原生不可控顺序 | 旧完整链有证据;最新版与真实 population law 待核 | +| S1 | Free/Mission/Battle/Arena/Challenge 的合法动作、撃奏强制条件、power song 与 skill-target song 的区别、任务覆盖/零值回退 | 场景解析、合法请求、power 上界、任务与谱面绑定 | 旧模型已区分;最新版及新增场景待核 | +| G1 | fever 范围、任务计数、冻结快照、Solo 与 Network 排名、group ordinal、packet arrival 与实际 settlement 顺序、live-finished 标志 | 撃奏终值、外部输入、帧序;固定名次不是对手预测 | 旧部分链有证据;最新版完整域待核 | +| N1 | 转换后音符/连击计数、有效音符类型、谱面等级倍率、技能事件数量与时间、迟判回退、演出结束长度 | score(power) 的单调域、Live 上界、完整轨迹判定 | 旧数值与有限链有证据;最新版 master/chart 待核 | +| L1 | life 归零是否立即失败、失败后技能/判定是否停止、续关代价与恢复、退出、assist 的动作影响 | 真正通关概率、稳定性、资源成本;terminal life 不能代替路径失败状态 | 当前没有完整失败/续关/退出证明 | +| E1 | 活动参数与结果时间、消费档、每次倍率、评级阶梯、日/活动上限、角色/留影加成、掉落选择与发奖 authority | 客户端点数、资源效率、跨多次状态;不能猜掉落 | 部分旧客户端预览有证据;最新与服务器规则待核 | +| U1 | 各养成等级/突破/卡阶/技能等级的合法状态、精确配方、共享资源、回忆/拥有数/玩家全局加成与变化 | 养成预算的决策变量、非独立成本及全局收益;不能静默满级 | 当前推荐无完整养成规划契约 | + +每项结论必须记录 `version identity → native/master evidence → supported domain → regression/original experiment`。仅版本号相同、函数名相同或 master 行数增加,均不能替代差分核验。若新版函数及数据与旧版逐项一致,可复用对应旧证明并记录核验哈希;未核验部分保持待核。 + +## 3. 固定输入、玩家决策与环境变量 + +固定输入记为 `Γ = (version bundle, master/chart, roster revision, player state, scenario binding, rules)`。 + +请求固定歌曲/难度、模式与活动时,不能在内部偷偷换曲、换难度、换消费或改变打法。要“自动选曲”必须把歌曲和动作明确加入决策域,并计入解锁、谱面长度、人工耗时与资源。 + +玩家决策为: + +`x = (physical members[5], leader, paired snaps[5], action, optional training plan z)`。 + +合法域 `D(Γ)` 由最新版游戏规则、已确认所有权、允许养成和玩家硬约束共同定义。OCR 未知身份/等级/技能等级不是一种合法默认事实。可做“已确认子池推荐”或对多个明确可能状态作鲁棒分析;不能把未识别卡排除后仍称完整卡池最优。 + +环境变量记为 `ω = (r, H, B, Q)`: + +- `r`:原生 root 及其 Skill/MemberShuffle/Luck 的共同派生状态。保留 signed roots、重复原子和整数质量。不能折叠正负根,不能在洗牌后重新 seed。 +- `H`:人类输入/失误轨迹及帧/音乐时钟。固定完成判定流是一个条件模型;原始 touch/FT 输入是另一个条件模型。 +- `B`:对手/队友成绩、断线、网络 packet 到达和排名规则。固定 aggregate confirmation timeline 是外部条件,不是因果对手模型。 +- `Q`:服务器奖励选择和外部资源变化;没有 authority 时只能保留条件,不得给假概率。 + +演出模型输出完整终值及必要路径状态: + +`T = Eval_Γ(x, ω) = (score, terminal life, fail/continue path if verified, Gekisou states, client counters, authority-qualified rewards)`。 + +所有期望先逐个 `ω` 得到 `T`,再对终值/路径效用加权。不能先取平均分,再给平均分套评级、掉落或达标阶梯。 + +### 3.1 概率分布必须单独声明 + +有限原子模型 `Ω = {(ω_i, w_i)}, w_i > 0` 的质量 `W = Σw_i`,对于整数效用: + +`E[u] = Σ w_i u(T_i) / W`。 + +这是所给离散条件下的精确期望,不等于游戏 root population law。几个示例根、发布种子序列或均匀随机整数,没有已知来源时只能命名为假设/敏感性模型。 + +不能默认 `r`、人类失误和外部排名独立。使用 `μ_r × ν_H × ρ_B` 必须声明独立假设;有共同时间/设备/网络来源时应输入联合原子或条件分布。现实误差统计即使拟合为离散权重,数值的精确分数也不消除统计估计误差。 + +人的准确率不能决定误判位于何处。连续漏判与均匀散布的相同 Miss 数会产生不同连击、life 与任务;判定窗技能还可能反过来改变错判分布。若改变队伍会改变判定窗/辅助,不能对所有队伍复用一个“已完成判定流”并称实战预测;应使用经核验的原始输入链或明确承认固定结果条件。 + +### 3.2 推荐选择和检验应使用不同随机样本 + +少量 roots 上优化物理槽位,可能只学会这些 roots 的 shuffle。设计上至少区分: + +- 搜索分布:决定本次 objective 和数值证书; +- 未参与选队的 sensitivity/held-out 分布:检验另一组声明条件,不能把结果继续当作原 objective; +- 经采集与验证的现实分布:来源、时间范围、样本量、拟合和不确定性另有报告。 + +逐个候选使用相同 joint atoms,是比较时的 common-random-number 控制,不代表样本具有现实代表性。扩充分布必须改变 input/model-law identity,并重新评估所有保留候选,不能把不同分母/样本规模的临时均值混排成“精确 Top-K”。 + +## 4. 玩家目标与目标函数 + +以下目标分别提供计算含义,不能用一个泛化“综合力分”承载。未满足依赖的目标不能默默退成另一个目标。 + +| 玩家目标 | 主要目标函数/约束 | 必需建模依赖 | 结果应怎样解释 | +|---|---|---|---| +| 当前培养下日常冲分 | `max E[S]` | 最新 Live、明确 H、μ_r;撃奏及外部 B 若开启 | 所给条件下平均结算分,不承诺人类 AP 或最佳原生顺序 | +| 稳定达到分数 t | `max P(S ≥ t)`;建议同概率时 `min E[(t-S)+]` 再 `max E[S]` | joint law;完整 H;若要求过谱还需完整失败模型 | 达分概率与未达时损失,不等于均分排序 | +| 只追目标、不奖励溢出 | `max E[min(S,t)] = t - E[(t-S)+]` | 与单曲分数相同 | 目标以下的预期差距;不能宣称风险已经完全消除 | +| 风险限制后冲分 | `P(S≥t) ≥ q` 下最大化 `E[S]`;或声明下尾 `CVaR_α(S)` | 概率模型、明确 q/α;无可行解需报告最大达标概率与差距 | 机会约束与纯达标目标分开,不用未声明的加权和 | +| 稳过谱 | `max P(native success path)`,之后分数/资源成本 | 原生失败/继续/退出与人类原始输入模型 | 当前不能由“terminal life > 0”认证 | +| 分数且保留结算血量 | `max P(S≥t ∧ L_terminal≥l)` | 声明完整轨迹与life模型 | 一个明确终值目标,不表示life从未归零或已经过谱 | +| 撃奏得分 | `max E[S including verified Gekisou settlement]` | fever、任务、原生共同RNG、Solo或Network排名模型 | 固定名次条件与真实对手竞技收益必须分开 | +| 本次活动积分 | `max E[P_client(T, frozen clocks/counters)]` | 最新活动阶梯、加成、消费、模式、结果时钟 | 客户端预览;服务器发放待 authority | +| 某资源本次收益 | `max E[R_resource(T,Q)]` | 服务器选择/掉落分布或已给定Q | Q仅被给定时输出条件资源量,不能推断掉落 | +| 活动资源效率 | 固定动作成本 c>0 时比较 `E[P]/c`;跨动作/连续刷取应优化明确时域的总收益和总成本 | 库存/上限、时间、失败消耗、消费档、发奖/时钟状态转移 | c=0不能除;`E[P/c]`与`E[P]/E[c]`也不一般相同 | +| 跳过 | `max f_skip(P)`,或上述单调分数目标 | 最新 Skip 全部常量与合法动作 | 当前培养下确定性公式;不能混用手打技能收益 | +| 综合力 | `max P` | 最新卡/玩家/leader/song/scenario参数 | 属性诊断指标,不等于 Live或活动收益 | +| 养成预算 | `max V(x,z)`,`C_resource(z) ≤ budget_resource` | 最新配方、合法状态、玩家库存、全局状态转移与当前所有权 | 返回具体升级步骤、资源向量、改前改后结果;当前尚不具备完整契约 | + +分位数取所给离散分布的加权分位。下尾 `CVaR_α` 必须在最差 `αW` 质量上计算,并允许最后一个原子仅贡献部分质量;不能把某一个 P10 数直接当 CVaR。风险参数、单位、平局偏好须冻结为 objective specification。 + +### 4.1 养成不能简化成每卡一个独立成本 + +训练计划 `z` 至少包含成员/留影状态变化与玩家全局道具、角色 Rank、VIP、回忆等变化。共享资源约束使用向量;一个“总费用”只有在玩家明确给出兑换/偏好时才合法。收益可能通过全局加成影响未选入队伍的卡,或通过解锁条件影响整个池。更换 `z` 后可能要重建 `Γ` 的 player-derived tables。 + +可分阶段设计:当前培养评估 → 明确给定目标状态的条件比较 → 原生配方的预算规划。前两阶段不能冒充第三阶段。状态支配仅能在所有相关效果、触发、cost和全局依赖均不变/单调的证明域内使用。 + +## 5. Top-K 身份、平局与可展示替代队伍 + +### 5.1 必须先选择输出问题 + +- **Physical Top-K**:不同物理成员位置、leader、留影搭配及允许的培养状态/动作构成不同决策。适用于有位置依赖的条件 Live 模型。 +- **Member-set Top-K**:每组五张成员 ID 只展示最优代表。只有代表在其 leader/snap/placement/plan 域内已求最优,才能宣称精确成员集合 Top-K。 +- **Candidate alternatives**:每个成员集合展示当前已找到的最好变体,标为 best observed。不能把去重后的五条候选解释成全局前五集合。 + +明确身份键:Physical为`(members[0..4] public IDs, snaps[0..4] public IDs/None, resolved training-state key, action key)`,leader由物理槽2定义(仅在最新版D1确认此规则后使用);Member-set为升序五成员public IDs,其内部代表按同一完整排名键求最优。培养计划的不同写法若解析为同一实际状态与成本,应规范化后去重;尚会改变后续资源/状态的差异不能丢。固定物理槽约束会进一步限制placement,不能在规范化时移动它。 + +玩家通常希望替代方案而非同队的五个排座。展示可分“不同成员集合”与“同队改队长/留影/培养”,但展示去重不能偷偷改变搜索证明的 identity。可证明的集合 Top-K应显式将 member-set 作为主域并求其内层最优。 + +### 5.2 排名键建议 + +正式契约待评审,建议用固定的词典序目标向量: + +1. 用户选定的主要目标,精确比较; +2. 该目标明确声明的次目标,例如稳定达标时预期短缺更小,再平均分更高; +3. 若是养成规划,声明的资源/成本偏好; +4. 综合力作为最后数值 tie-break,而不默认覆盖更相关的次目标; +5. stable public identity:成员 ID、leader、每槽留影 ID、培养状态/动作键的固定字典序。`None` 留影置于所有实际 ID之后。 + +PoC 当前使用主要 payoff → power →物理ID/snapID;改变为以上玩家偏好会改变平台区的 Top-K,必须升级排序契约及 oracle。不能把新偏好只加在UI排序中。候选池 dense index、OCR导入顺序、hash表迭代、线程调度和 root枚举顺序都不得定义公开 tie。 + +相同分布下期望可比较共同分母的整数分子。不同合理分母需约分/checked cross-multiply或任意精度;溢出是显式错误,不允许f64兜底。部分轨迹/部分分布不产生一个可与完整候选等价排序的结果。 + +## 6. OurNotes 搜索问题必须由语义分派 + +参考固定源码 [allium-deck lib.rs](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/src/lib.rs) 的入口、建池、只读pool和搜索分层;参考 [DeckProblem](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/src/search/problem.rs) 先判可交换槽位再选择放置搜索的思想。该版本是结构参考,不是 OurNotes 规则来源,也不是 allium当前发布状态声明。 + +建议语义层划分: + +`Dataset/RulesIdentity → Input/ScenarioResolver → FrozenPool → Point/TraceEvaluator → ObjectiveSpec/DeckProblem → Exact or CandidateSearch → TypedResult → WASM wrapper`。 + +- Evaluator是唯一游戏数学真源;评分、随机和事件公式不放在JS、OCR或渲染层。 +- ObjectiveSpec声明效用、次目标、随机分布和authority;DeckProblem声明legal域、placement、identity和可用证明traits。 +- FrozenPool缓存只允许固定Γ的成员属性与无决策依赖项,不把依赖整个队伍或升级计划的效果提前当常数。 +- 搜索排序、Top-K、上界和终止统一维护。WASM薄壳负责字节传输、分片调度和状态绑定,不另实现评分公式。 + +不可从 allium 复制 `best/specific` 技能顺序作为 OurNotes玩家能力;也不可把其活动分数、同角色挑战、支援或称号规则替换 OurNotes scene/snap/撃奏。 + +### 6.1 域大小与放置问题 + +若角色c有n_c张允许成员卡,则成员集合数量为 `e_5(n_1,...,n_C)`,即选五角色每角色一张的和。未固定leader且必须保留所有物理位置时,排列因子为`5!`。有s张允许留影,注入数量为: + +`Σ_(k=0..min(5,s)) C(5,k) × P(s,k)`。 + +总域还乘上允许的训练/动作状态;每个完整候选再需评估joint atoms及整首轨迹。实际角色分布与约束应从Γ计算,不能仅凭“63成员/64留影”推断搜索复杂度。 + +### 6.2 何时能折叠物理排列 + +综合力与合法 Skip的自由非leader位置在既有证明域可交换;任意有限root分布的Live一般不可交换。 + +即便原生顺序的**边际分布**看起来均匀,也不足以折叠:必须证明置换后的 `(performance order, remaining random state, all identity/condition observations)` **联合分布**对相关物理置换等变,并且leader和成员/留影绑定不变。共同root可使顺序与后续技能值/幸运相关。独立均匀顺序模型只能作为另一个明确命名的假设。 + +不满足该证明时,物理放置是内层决策。不能排序非leader ID之后忘记剩余排列,也不能用“每卡技能均值”先乘谱面位置权重宣称exact。 + +### 6.3 留影与成员不能只按综合力配对 + +Power层可使用已证明最大权匹配,并保留留影唯一性和canonical tie。Live留影会影响转换、life、持续/延长、概率、技能状态和撃奏,必须与placement及成员联合搜索。 + +只有证明同一成员处的两留影在全部声明轨迹、随机状态、条件副作用、row顺序、reset/释放与延长方面观察等价,才可折叠成技能class。一个“没有可见加分效果”的行仍可能消耗同一随机流或改变life查询缓存,不能仅按效果标签删除。最新版未清点的新行不进入既有class证明。 + +## 7. Exact 与 Candidate 的严格边界 + +### 7.1 Exact + +`Complete/Proven`只表示:冻结Γ、分布、硬约束、identity与排序下,所有合法决策已评估或被有效上界排除,输出正好规范Top-K。不能由“穷尽启发式proposals”“连续若干轮没有改进”或“当前最优数值稳定”推断完成。 + +合法域为空可证明Complete空结果。未知规则、非法状态、Unsupported技能、数据身份冲突、算术溢出分别是错误;不能变成无合法队伍或“降级成功”。只在玩家明确限制域时,才能输出该限定域的精确结果。 + +### 7.2 Candidate + +候选提案可使用近似效能、谱面权重、贪心、局部交换、beam或随机重启,但**所有公开结果**须由完整Evaluator在同一law下完成评估。近似值只是提案/访问顺序,不能冒充上界,更不能据其删除Exact合法域。 + +Candidate的proposal穷尽仍是heuristic。超时partial不进入Top-K;临时单根得分可作进度,但必须标明尚未成为完整候选。仅在另有有效全域上界时才能提供gap;无上界时gap=unknown,不能把相对某个baseline的提升叫“达到最优的百分比”。 + +## 8. 可证明上界与不能沿用的剪枝 + +所有上界都须绑定 `Γ/rules identity + objective traits + supported arithmetic domain`。域检查包括非负属性/倍率、有效谱面常量、life/转换可达性及i32/i64 wrap。原生wrap可以被Evaluator精确复现,但会破坏部分单调性,上界证明不能自动延伸到该域。 + +### 8.1 Power 上界 + +旧域的slot分解为: + +`S(m,s,L) = A(m) + LEAD_L(m) + W(m,s)`。 + +用允许留影的`Wmax(m)`放松唯一性;将leader条件效果按最大可达正贡献及累计次数上界,得到`u_L(m)`。对不同剩余角色取各自最佳卡的后缀前r项和,构成成员树UB。叶子再精算whole-deck leader条件与精确留影匹配。 + +必须重新核实最新版binary32/floor及整卡全局依赖,不能把标签相似当可加项。上界严格小于Kth主要目标才剪;等值分支继续展开,除非完整词典序次目标上界也足以排除。 + +### 8.2 Skip 的单调快路 + +在已检查域内,Skip是仅依赖power的非减`f(P)`。若主要目标是非减`g(f(P))`,而其余数值tie也与P相容,则 `(g(f(P)),P)` 和原 `(f(P),P)` 按P同序: + +- 不同power在阈值/封顶平台用power破平; +- 同power的score与payoff相同,沿用同一public identity tie。 + +故当前培养的`score`、`scoreAtLeast`、`cappedScore`可共享该证明。若增加“达到阈值后优先更低升级成本”、不同动作时长/消费、event加成或其他与P无单调关系的次目标,必须重新分派,不能继续用raw-score Top-K裁掉那些解。 + +### 8.3 Live 上界 + +候选内完整模拟保留共同RNG、frame rollback及life缓存。树层需要逐音符/区间乐观包络:power UB、可达judgements、combo范围、life factor、能覆盖该音符的正倍率、延长次数、撃奏range与rank bonus。每一项放松必须只增不减,覆盖最新版全部副作用和f32误差累计。 + +旧`snaps.rs`/`docs/search.md`有特定域包络与效果class证明,可作审计材料,不能未经复核转成新概率目标、动态touch或Network模型的全域UB。统计chart weights及对某些测试组准确的拟合值不构成可采纳性证明。 + +### 8.4 分分布原子的partial UB + +共同分母下,已完成原子贡献`N_done`,剩余原子分别有效用上界`U_i`时: + +`UB = N_done + Σ_remaining w_i U_i`。 + +二值概率可用`U_i=1`;cappedScore可用目标t;score可用合法终值类型/已证明更紧界;客户端点数/条件资源需由实际类型、表、authority和cap证明界。未知服务器奖励不能以0或猜测最大掉落作界。 + +UB严格低于Kth主要分子可停止这个候选。等值保留全部tie可能性。CVaR、机会约束、路径失败/多次刷取等非单一加性效用需独立partial包络,不能直接套均值公式。 + +### 8.5 支配与预筛 + +“power更高、裸skill更大、同角色”不构成OurNotes全目标支配。leader适配、music条件、不同触发、概率draw数、life、留影匹配、任务/外部排名及精确平局都可能破坏它。Exact只接受完整替换证明;否则预筛属于Candidate并记录被限定域。 + +## 9. 首候选、预算与深搜策略 + +模型到位后再选正式默认。以下是待验证搜索策略,不是性能承诺。 + +### 9.1 一份预算贯穿整条计算 + +公共deadline覆盖解析/验证、建池、预计算、warmup、proposals、逐原子、逐帧及结果复算。冷WASM加载和素材下载另有墙钟记录,不能用热搜索elapsed替代点击后耗时。 + +同时设node/proposal/candidate/simulation-work/memory上限,分别计数。timeLimit、candidateLimit、proposalExhausted、cancelled与modelError为不同退出原因。帧内操作多时仅每帧checkpoint仍可能超时,需要确定的work checkpoint;合作deadline的可超预算量应实测,不能称硬实时保证。 + +WASM分片`step(work budget)`负责让事件循环有机会接收取消,并保存同一Γ的frontier与已完成候选。直接终止Worker只表示取消;它不能返回一个此前未输出的完整Top-K。resume不可跨roster/law/dataset revision。 + +### 9.2 快速获得一个真正完成的候选 + +优先完整评估玩家当前队伍(如合法)作为baseline;没有当前队伍时,由约束构造五不同角色的合法队伍,并尝试少量leader/留影选择。首候选预算与优化预算分开记录;不先用全部时间预筛、枚举首个power队伍的所有排列或跑全部随机重启。 + +首候选仍要完成所有本次joint atoms。单次完整评估超过预算时,给出“预算不足以完成一个候选”的准确状态与可继续计算选择,不能显示前几根均值。未核验/Unsupported效果导致Evaluator失败时,报告模型域;不能偷偷丢卡后称原全池推荐。 + +### 9.3 Candidate 快搜应覆盖多种偏好 + +候选来源至少分成:当前队伍、power-seeded不同成员集合、score技能不同触发/类型、life/转换保护、任务适配、event适配,以及随机合法重启。各来源仅提案,不更改主要目标。 + +先轮流覆盖不同成员集合,然后做有界placement/leader/snap细化;留预算给单成员替换、双成员交换、leader切换、留影重配与重启。只有单卡局部改进可能被协同门槛困住,需成组邻域。 + +PoC的`5*powerSeeds`轮转只解决“首队120排列耗尽预算”这一失败,不能证明其分配比例最佳或胜过其他策略。正式比例应由第12节质量门禁决定,不能因为一次真实大池变好就定默认。 + +### 9.4 深搜 + +已核验Power/Skip先用exact分支界限。复杂Live可先候选暖启动获得有效下界,再用已证明UB访问更有希望的成员/leader/snap/placement子域。前沿应覆盖整个合法域;beam丢弃、固定候选池或只从elite邻域继续均不再是全域Exact。 + +要输出成员集合替代方案,内层对每集合联合求leader/留影/placement最佳代表,外层Top-K;内层未穷尽而没有证明时,代表只能best observed。任意时间证书至少包含当前Kth下界、未搜索域的最大UB、仍可能改善平局的域,以及objective/identity hash。 + +frontier/缓存保持有界:选择DFS可低内存,但需要证明整个前沿没有遗忘;best-bound heap内存耗尽时只能显式中止/转入保真分区,不得丢分支后声称Proven。 + +缓存键至少包含Γ、law、human/external轨迹、physical deck、training plan和point-model语义。PoC的仅root复用只对同一候选、相同其他环境纯输入有效;扩展到多条H/B时不能仍只按root缓存。 + +## 10. 必须保留的启发式失败反例 + +以下是数学反例/回归设计,不是最新版真实卡效果声明;真实fixture还需由已核验规则构造。 + +| 误用 | 反例 | 必须避免的错误结论 | +|---|---|---| +| 只按power保留前N张/队 | 高power但无适配技能;稍低power在主要音符窗口有强加分 | power预筛不是Live支配 | +| 均值代替稳定达标 | A有20%得0、80%得200,均值160;B恒150,均值150。目标150时A概率0.8,B概率1 | 冲分与稳达不可共享排序 | +| 给平均分套奖励 | score为0/100、权重1/3,均分75;80起奖励10。奖励均分为0,平均奖励为7.5 | event/奖励必须逐原子算 | +| 少量root训练即真实最优 | 示例root总把某物理槽放在高权技能位;另一组root选择别的槽 | finite-law最优可能过拟合示例root | +| 仅边际uniform消排列 | shuffle顺序均匀,但与后续skill值/lottery状态相关 | 需联合等变,而非边际计数 | +| 移除看起来不加分的snap行 | 该行概率检查消耗随机数,或life查询改缓存,改变后续效果 | inert须覆盖全部副作用 | +| 最大power snap匹配后才选order | 较低power配对把延长/转换放到更有价值的成员/触发位 | 留影、成员、placement联合选择 | +| 只做单卡局部替换 | 两张一起换满足leader条件/任务门槛,任一单换均下降 | 要有成组邻域或exact全域 | +| terminal life即稳过 | 轨迹中归零,随后模拟继续恢复成正值 | 需失败/继续路径,不可仅看终值 | +| 物理Top5去重即集合Top5 | 前五全是同组不同placement,去重只剩一组;第六至更远集合未搜索充分 | 展示identity与证明identity分开 | +| 目标平台按错误tie裁剪 | 达标概率相同但次目标/公开ID更优的队伍落在被丢分支 | 等值UB不能只按主要数值剪 | +| 低阶默认等级即识别事实 | 新卡技能/等级未知,被默认1或满级使结果翻转 | 记录假设/校对,不静默赋值 | +| 单次活动preview即资源效率 | 比较队伍位于不同消费档/计数上限/结果时间,或cost为0 | 需要动作与状态转移 | +| 启发式运行完即Exact | proposalLimit到达、连续无改进,但未覆盖技能型队伍 | 状态仍heuristic | + +## 11. 已证明域与PoC证据如何使用 + +当前本地材料:[旧搜索证明](../../bdon-pipeline-20261001/solver/pr-core/docs/search.md)、[推荐契约PoC](../../bdon-pipeline-20261001/solver/pr-core/docs/recommendation-contract.md)、[范围说明](../../bdon-pipeline-20261001/solver/pr-core/docs/recommendation-coverage.md)、[模型审计](../../bdon-pipeline-20261001/solver/pr-core/docs/model-audit-2026-10-01.md)。 + +- 旧Power/Skip上界与canonicalTop-K有独立穷举和域证明材料,前提绑定当时模型/数据/非负无wrap域。 +- 旧有限root物理oracle保留原生洗牌与共同随机状态;其Complete仅证明给定law、轨迹和支持域。 +- 旧原生ARM64逐字段比较证明特定版本、配置与链路,不认证所有效果/根/最新热更。 +- 新目标/summary/轮转及Skip平台测试是PoC级回归,不能替代新版原生证据或大池质量门禁。 +- 一个大池CLI速度样本和WASM smoke不能认证手机p95、完整网页链路、概率模型现实有效性或首候选服务等级。 + +本文件不引用旧比较次数作为新版覆盖比例。native bit equality、same-source跨平台一致、search oracle一致和human probability calibration分别登记,不能合计为一个“准确率”。 + +## 12. 进入正式实现前的门禁 + +### G0:版本与规则冻结 + +完成V1及所有本次目标涉及的待核项;输出版本化rule/effect inventory、master/chart identity与支持域。新增未知效果/条件/动作必须有处理策略,不能漏列。确认一个目标的完整依赖后才允许该目标进入正式实现;其他目标保持明确待核。 + +### G1:Point/Trace 模型对照 + +使用同一Γ、raw/finished inputs、frame clocks和root,在原始客户端与唯一Rust evaluator逐字段比较score、life、combo、转换、技能状态、RNG draws、任务快照与排名结算。记录独立输入数、原始哈希和差分字段;zero difference只覆盖被测域。 + +至少覆盖不同FPS/非恒定dt、迟判回退、life条件/恢复/guard、概率checker和共同root、snap/member绑定、强制/清理判定、Network早到/迟到/批到、目标阈值附近与可能wrap边界。完整过谱目标在失败/续关链验证前不得开放。 + +### G2:独立搜索oracle + +用小域独立枚举legal decisions、leader、snap injections、physical placements、训练变体/动作和joint atoms。复用已经核验的point evaluator可以验证搜索,但oracle不得复用生产Top-K、支配、class folding、matching或上界。 + +对每个声明solver family比较完整排序与identity,K=1/5/30/最大可行;包含固定/排除冲突、同角色变体、None snap、各种平局、全达标/全不达标平台、半途预算、零预算、分布signed/duplicates/大质量、mode与特殊零值回退。 + +### G3:上界与反证回归 + +对抽样/构造节点枚举其全部补全,断言UB不低于真实最大排名值;关闭各剪枝独立对拍。mutation控制应能抓住`<`误成`≤`、遗漏snap概率副作用、signed-root折叠、洗牌后reseeding、强行canonical非leader、power-only snap、错误资源/mean reward变换和浮点JSON重序列化。 + +每个新trait/等价class需要书面前提与反例测试。不以大量随机一致代替可采纳性证明。 + +### G4:大池质量与预算 + +固定最新版区域数据,区分真实确认玩家状态、匿名授权状态、全持有mock和合成对抗池。覆盖新手/中等/大池、不同角色重复度、life/概率/任务/event组合、K与所有目标。 + +能穷举的域报告objective regret、tie-aware Top-K和集合替代覆盖;概率报告百分点损失,CVaR/机会约束报告其具体效用差,不能混成一个“准确率”。不能穷举的大池比较当前队伍、power baseline、随机/局部、多来源候选、独立更长搜索及可用UB,最佳已知值只是下界。 + +规定不同root/joint-law敏感性,不反复调参使held-out变成训练集。Candidate须复算每个返回值;还要记录无候选率、完整候选数、来源/集合覆盖、partial舍弃、cache/memory峰值和budget overrun。 + +延迟在参考桌面及至少一档中端/低端移动浏览器分别测冷加载、热建池、首完整候选、迭代提升与总取消时延。正式门槛在测量计划中冻结,至少多次冷测及足够热重复才能报告经验p50/p95;三次CLI样本只作探针,不作为生产p95或默认算法依据。 + +### G5:WASM唯一数学与输入语义 + +浏览器唯一数学实现为原Rust核心编译WASM,JS只解析展示/调度。原始UTF-8输入保留大整数与f32 token;同源native仅作验证工具,不能成为另一套近似规则。对同一Γ逐项比较point输出、整首轨迹、候选ranking/identity、exact fractions与声明终止。 + +取消/分片/恢复不改变随机draw序、完整候选值或law;旧revision/版本响应不能恢复失效队伍。平台clock只决定预算,不参与模拟音乐/frame时钟。固定work-budget对拍与wall-budget性能测试分开,不能要求不同速度平台wall timeout下恰好访问相同候选数。 + +### G6:设计审阅与正式契约 + +模型审计、搜索证明及玩家目标分别独立审阅。先决定Stable的次目标、Physical与MemberSet输出、真实与假设law、训练域、错误/取消状态和支持模式,再定正式API/SDK;PoC旧JSON结构不自动约束新版正确契约。 + +过G0/G1/G2/G3后才能称本目标在已声明域内模型/Exact到位;过G4/G5/G6后才允许定正式快速推荐默认、WASM网页契约和端到端验收。源代码PoC保留原身份,不因文档规划或旧CI绿色升格。 + +## 13. 当前决策与下一项核实 + +当前保持实现冻结。先等待根代理的最新版身份与模型审计矩阵,把第2节逐项变成可复查结论。搜索侧优先确定三类问题: + +1. 当前培养的Power/Skip:核单调域与完整公开tie,形成可复用Exact族。 +2. 明确条件的普通/撃奏Live:固定joint law和外部输入,确定是否必须保留全部物理placement,以及能证明的live/snap上界。 +3. 人类稳定性、资源效率与养成预算:先补路径/状态/成本模型,再进入候选与Exact设计,禁止借一个新目标名字先接页面。 + +这三个结论完成后才比较候选来源与深搜分配,拿小域oracle和大池质量门禁定策略。截图→结果链路应消费已冻结模型与策略,而非反过来用网页已经需要哪些按钮决定游戏模型。 diff --git a/docs/deck-design/shared-evaluation-and-data.md b/docs/deck-design/shared-evaluation-and-data.md new file mode 100644 index 00000000..5ec6f1fc --- /dev/null +++ b/docs/deck-design/shared-evaluation-and-data.md @@ -0,0 +1,96 @@ +# 统一求值与最新日服数据契约 + +状态:数据调查与模型 / 搜索前置方案;正式页面实现继续冻结。 + +## 当前实际调查状态 + +- 完整日服 APK 对应 1.0.4、versionCode 10053,原生条目已按内容哈希绑定,见[来源记录](latest-native-source-binding.json)。是否可执行为 latest-native oracle 由模型线检查,不能仅凭文件名通过原生 gate。 +- 当前 nnnotes 源码真实支持 JP provider;本次研究配置尚需补齐日服配置,不能把配置缺项解释为“不支持日服”。 +- 按当前 `nnnotes.jp.Session.observe` 实际匿名调用 Version 返回 `UNAVAILABLE`,没有取得本次有效版本。错误类别已匿名记录;不是借用旧文件标成最新,也不是模型结果。 +- 2026-09-30 保存的日服数据是 master `1.0.0.300/52355a9de56a475f691b10ef58acba58`、资源版本 `1.0.0.300`。在新的 Version / manifest 确認前,它只能标为“已保存快照”。 + +正确链路是 `Config(provider=jp) → Session.observe → Version body + response metadata → 同一快照的 master / catalog / asset`。实际 request metadata 是 `x-platform=android`、`x-client-version`,不是自行猜测的 `x-client-platform`。CDN credential 仅保留在 Session 内存,配置地址保留私有目录,公开清单只含版本、哈希与状态。 + +## 一个引擎,三个遍历对象 + +```text +SharedEvaluationContext + version / native-model identity + resolved scenario + chart + duration clocks + physicalDeck + paired Snaps OR validated skill/effect profile + player / progression / targets + play + root law + external multiplayer/event conditions + ↓ +same Rust whole-live evaluator + ↓ +terminal atoms + metrics + scope + completion + +DeckSearch: 枚举合法编成 / leader / paired Snaps +SongRanking: 枚举已选范围的谱面 / 已明确技能 profile +FixedDeck: 只评估用户给定物理编成 +``` + +以上三者的不同是输入遍历与结果排序,不是另写歌曲分数公式。共享 context 要进入同一个整数 / binary32 / 帧事件与随机流实现;每次换歌必须重新 resolve 歌曲属性 / tag、难度、撃奏任务、队伍综合力和适用条件,不能把一首歌的 power 固定套到全部歌。 + +结果必须区分三个 scope: + +1. **固定卡组排行**:当前培养、成员与 paired Snap 已确定;按各歌实际加成逐首求值。 +2. **标准技能 profile 排行**:用户选择可验证的标准 profile,展示在这些条件下的歌曲特点,不表示实际账号持有或实战表现。 +3. **实际持有搜索排行**:卡池 / 培养 / 玩家状态参与每首歌的组卡搜索;快速候选与已证明结果分开,不把 profile 排行当成账号最优。 + +原生技能洗牌和随机效果属于求值输入,不能作为玩家可控制的“最佳技能顺序”。多个根 / 判定流的权重来源、实际时钟与多人条件必须随排行输出,缓存 key 也必须包含它们。 + +## 导出 / 消费核对矩阵 + +| 对象 | 当前 exporter / consumer 已提供 | 进入统一 context 还需要确认 | 验证与失败行为 | +|---|---|---|---| +| 快照身份 | nnnotes provenance 含 region、APK versionName/code、master version / 表哈希、catalog SHA、模型 commit | 本次有效 Version 观察、同一 manifest / source sidecar、latest-native 身份与支持域;decoded JSON 内容哈希与原始 master 文件哈希分开 | 版本或实际字节不匹配拒绝混用;只保存旧快照时不标最新 | +| 成员 / 留影 ID 与培养组 | `deckdata.TABLES` 包含成员、SupportCard、level / rank / awake 组、技能 ID | 每个 ID 的合法等级与卡阶对应行,rank 派生 Snap 普通 / 撃奏技能等级 | 未知身份和断链行报错;不以 max-power Snap 配对替代 Live 技能配对 | +| 歌曲属性 / tag / 难度 | full data 包含 MasterLiveMusic、Score IDs;song meta 有 musicType、bestMusicTagIds 与 easy/normal/hard/expert | 标准曲 / Arena / Challenge / Mission / Battle 对应的 resolved scenario;特殊行 ID 与 base song ID 分开 | score ID 必须属于选定 resolved song,不能只靠 difficulty 名称 join | +| 谱面与 note enumeration | chart TextAsset SHA 与 runtime id/op/judgementType/timeMs;原客户端枚举顺序保留 | converter / native 版本、raw fields 对求值的依赖及合法 enum;完整 play 覆盖全部评分 note / event | ID 重复、枚举缺口、来源 chart bytes 改变均拒绝;不能按时间重排后冒充原枚举 | +| 技能事件与 fever | chart skillEvents/timeMs、fever start/end、song 撃奏 missions | native 时钟、触发 / release、range ordinal、Finish8 与 multiplayer arrival/application | 逐事件 / range 对拍;超过支持 range、未知场景要 explicit unsupported | +| BGM 与 score 生命周期长度 | music meta 有 ACB cue length / waveform samples/rate/duration;chart `musicLengthMs` 当前是 last note + tail 派生值 | 原生到底使用 BGM 长度、score length、music length 的哪个时钟;liveFinished / deltaTime 语义;不可把 chart fallback 标成实测 BGM | 同时记录 raw BGM facts、chart-derived length、实际选择与来源;缺值保留 null;长度边界列入 native 对照 | +| 演出技能原始行 | LiveSkill / Effect、target、condition sets、cumulative、release、limit 与 reset 等 full master 表 | 引擎是否覆盖每种存在的 effect / trigger / condition / phase / target;profile 依赖闭包 | 分母是实际 master 语义签名和行,不是仅列已支持 effect 种类;缺分支单独记录 | +| Snap 普通与撃奏作用 | full TABLES 已包含 supportSkill01/02、GekisouSupport01/02 与 effect 表,以及 rank 对应等级 | 关联成员 / band / card type / skill category / mission,普通 vs 撃奏触发与执行时机;paired Snap 跟随 member 洗牌 | 先分别对拍纯 power 匹配、Live 成对作用及全场组合,再测搜索;不能把 power matching oracle 当 Live 配对 oracle | +| 曲目技能权重 | 当前 `chart-stats/2` kinds / seeds / offSeeds / rangeWeights,有误差 bound 和 check-deck 校验 | 哪些 effect/profile 可合法线性分解、在哪个 play / scenario / target 域有效;非线性 Snap profile 不适用 | 无 proof scope 或依赖不符合时走 whole-live;不能给 Snap 随手加一个 coefficient | +| 文字与展示 meta | nnnotes music-data 有五语言 title / credits / band / character / tag;MoeNotes 消费 song/chart facts | 内容语言 fallback、meta 的 source snapshot 与求值 snapshot;前端不能把未校验 profile 的名字当求值定义 | VM 只负责显示,分数与排序来自共享 evaluator。现有 card components 复用不构成评分正确性 gate | + +## Validated SnapEffectProfile 的最小契约 + +它必须是原始 master 行与依赖的可核查视图,不能只是 `{snapId, coefficient}`。 + +- identity:region、client / native-model、master、catalog / chart bytes、engine commit;profile 自身实际字节哈希。 +- origin:实际 SupportCard ID / rank 或明确标准 profile;区分普通 skill 01/02 和撃奏 support 01/02,不自动复制最大等级。 +- effect 行:skill ID、level、effect type / phase、值 / max 值、activationTime、trigger / execution timing、target IDs、condition / release groups、cumulative ID、effect / execute limits 与 reset groups。 +- dependency closure:linked character / band / card type / song tag / judgement / mission / member skill categories,condition sets 与 leaf condition 行,相关 settings。 +- coverage:原始行数与语义签名数、引擎支持 / 未覆盖列表、原生 oracle 验证版本及 case IDs;不存在验证的分支不写 validated。 +- applicability:普通 / 撃奏、resolved scenario、play / frame law、额外 peer/event 输入要求;是否可线性处理以及对应证明 / bound。 + +判定转换、回血、护盾、combo、累计 / 限次 / reset、扩时、随机 trigger 和共享随机流可能改变后续状态。这些必须 whole-live 重新运行。只有经过验证、满足完整依赖的线性域,才可复用权重作为透明快路;快路结果仍与共享 evaluator 对拍,不能形成独立公式分支。 + +## 当前消费端的已发现限制(证据先于修复) + +- `MoeNotes/src/lib/chart-data/ranking.ts` 明确以全队、5 秒、无 target / condition / limits 的 plain 2000 score-up 为模型域;`plainKind` 也只选择该种类。它目前不是可任意选择 Snap / 普通 / 撃奏 profile 的动态排行求值器。 +- 同文件对 sampled seed means、uniform permutations 和线性 time-efficiency 建模,`scenario.ts` 的 Great / Just 比例处理也是近似。不能将这些说明和公式直接推广到实际完整技能 profile、共同随机流或真人通过率。 +- `chart-data/client.ts` 当前只检查 object、songs 数组及已出现的 format;`MusicData` provenance types 也只保留部分字段。正式动态 meta 需要实际 snapshot / profile / engine 身份检查。这是契约缺口;本轮尚不绕过模型 gate 修改正式页面。 +- `nnnotes.deckdata.TABLES` 原始导出已经含 Snap 所需主要 master 表;“full data 没有 Snap effects”不成立。缺的是 validated profile / coverage / applicability 与统一全场求值接口,而不是给网页造简化系数。 +- `musicdata.chart_deck` 已检查 music ID、difficulty、level、note count、last note、derived length、skills、fevers、rank bonus 与 check bounds;这证明该导出检查域,并不证明 actual audio / lifecycle clock、所有 Snap 条件及最新客户端已经覆盖。 + +## 原生批次输入与覆盖分母 + +新版本的批次只在 **有效 Version + manifest / catalog 身份 + latest-native 可执行 oracle** 三个 gate 通过后配置为 current。之前可以准备清单、依赖闭包与只读校验器,但不得把 9/30 数据或旧中文 oracle 的 pass 重标为最新日服 pass。 + +| 批次 | 覆盖分母 | 输入构造 | 至少保留的反例 / 未覆盖分支 | +|---|---|---|---| +| D0 快照 / 导出一致性 | 所有实际歌曲、Score ID、chart SHA、原始表和引用边 | manifest→decoded→full export→Rust loader;原始 f32 token 不经 JS roundtrip | 缺表 / 缺列 / enum、跨版本 ID、缺 sidecar、derived vs real duration、表哈希与 decoded 内容不等价 | +| D1 全卡与培养 / power | 每张成员 / SupportCard、每个合法培养组值、歌曲属性与 tag 组合 | 最低 / 合法上限 / 临界相邻值;角色 / band / link 独立改变 | 只验证满级、最大P Snap、缺 tag / character / band 引用,无法表达的合法组值 | +| D2 Snap 普通效果 | 每个实际 support skill 01/02 的 level / effect 行及语义签名 | 同一 member 单独 paired / unpaired,target 命中 / 不命中、condition 真 / 假,普通 live | phase、release、cumulative、执行限次 / reset、边界 duration、转换 / life / combo / 扩时 / 随机等逐项分开 | +| D3 Snap 撃奏效果 | 每个 GekisouSupport01/02、rank level、mission 与 exec timing 组合 | range 前 / 中 / 后、Solo / 明确 multiplayer、Just / luck / combo 条件 | 忽略共同 Skill/Luck/Shuffle 流、跨 range 冻结快照、条件联动、network arrival 不等于应用帧 | +| D4 歌曲全场 | 全 Score ID × 实际可用场景;按依赖缩减仅须有证明 | AP / 明确错误流、时钟边界、实际配对 deck/profile;BGM / score lengths 原样声明 | 不可玩 >3 fever / 当前空 Arena、完整流遗漏、错误 stream 的活跃条件、patch / region 等价未证 | +| D5 组合与搜索 | 能改变排名的真实 member / Snap / profile 案例,以及候选 vs exhaustive 域 | 高P低收益 vs 低P高收益、不同 profile 排歌、同行配对 / 非leader物理槽交换 | 用单件 effect pass 代替组合、用power matching代替Live pairing、partial候选进入Top-K、快速候选被标最优 | + +每条 case 保存:快照与 oracle 身份、真实 master 引用、支路标签、全部输入、原生与共享引擎结果、bitwise / integer 比较方式、耗时和退出状态。失败、unsupported、未枚举、不可执行和版本 gate 未通过分别计数。分母和 pass 数绑定同一份输入清单,不能累加异版本或重复语义样本制造覆盖率。 + +## 依据 + +本轮已只读核查 nnnotes `src/nnnotes/{jp.py,gameapi.py,deckdata.py,musicdata.py}` 与 `docs/music-data.md`,MoeNotes `src/lib/chart-data/{ranking.ts,scenario.ts,types.ts,client.ts}`、`src/lib/music/data.ts`。尚未观察到本次成功的匿名最新 Version;也尚未将现有歌曲权重模型扩展为 Snap 全场模型。所有实现与最新对照必须在前述 gate 后继续。 diff --git a/docs/deck-integration.md b/docs/deck-integration.md index 3ce84d26..f46b76a1 100644 --- a/docs/deck-integration.md +++ b/docs/deck-integration.md @@ -1,8 +1,31 @@ # 卡池截图与组卡:MoeNotes 接入方案(Draft) -MoeNotes 是玩家入口。`ournotes-boxlens` 负责截图中的身份与可见培养字段,`ournotes-deck` 负责游戏规则、计分和搜索,MoeNotes 负责校对、目标选择、结果解释与本地保存。各层复用已有版本化契约,不在页面里另写一套评分公式。 +MoeNotes 是玩家入口。`ournotes-boxlens` 负责截图中的身份与可见培养字段,`ournotes-deck` 负责游戏规则、计分和搜索,MoeNotes 负责校对、目标选择、结果解释与本地保存。各层复用已有版本化契约,不在页面里另写一套评分公式。所有本轮 PR 保持 Draft,**最新游戏模型与搜索策略完成评审所需证据之前,冻结正式实现**。本轮先前写出的运行端/展示/识别接线代码仅保留为试验,不构成正式落地决定。 -这个 PR 提供运行端选择、数据版本绑定和 Worker 消息契约,以及对应回归。它尚未添加上线路由、模型运行时、权重附件或组卡页面;不会把可行性方案标成已上线功能。上游依赖是 [BoxLens PR #1](https://github.com/empty-sekai/ournotes-boxlens/pull/1) 与 [Deck PR #3](https://github.com/empty-sekai/ournotes-deck/pull/3),后续发布需要固定具体 commit 和文件 SHA256。 +这个 PR 提供运行端选择、数据版本绑定和 Worker 消息契约,以及对应回归。它尚未添加上线路由、模型运行时、权重附件或组卡页面;不会把可行性方案标成已上线功能。历史输入管线与推荐基线分别来自 BoxLens `6d3326d`、Deck `f755c9c`;其旧 PR 已关闭,本轮新增能力采用独立 Draft,后续发布需要固定具体 commit 和文件 SHA256。 + +## 正式实现的两道前置门槛 + +| 门槛 | 必须形成的结论 | 当前状态 | +|---|---|---| +| 最新版本建模 | 按区服冻结客户端/构建/签名、ELF/metadata、master/资源/谱面与激活补丁身份;核对关键规则、所有生效技能类型与模式;最新原生逐单元和逐帧差分,未验证内容显式列出 | **未通过**。日服 Android 1.0.4 / build 10053 完整包已找到,APK 与原生库/metadata 已按哈希绑定;仍需最新原生恢复/执行、patch 状态和语义对照。既有原生证明仍是 TW 1.0.1-25。今天的在线 Version 读取尚未成功,不能把 9/30 的 master 快照重标为当前 | +| 搜索策略定稿 | 玩家真实目标→目标函数/约束;人类打法与随机/网络条件;物理队伍身份和 tie;精确搜索的合法域/上界/穷举证明;候选搜索的覆盖、质量、预算与可解释限制 | **未通过**。现有 finite-root/单条打法 PoC 能精确算条件值,尚未证明真实种子总体、人类失误分布、大池全局最优或全部最新规则;快速基准只作为策略样本 | + +日服 iOS 版本的原始来源为 [官方 App Store](https://apps.apple.com/jp/app/id6771716739) 与 Apple Lookup,发布日期 `2026-09-29T11:38:20Z`。这不是 Android ELF、运行时 IFix 或线上 master 已完成认证的证据。版本更新日志写了“修复”也不能替代对应逻辑对照。 + +前置材料为 [最新模型验证矩阵](deck-design/model-validation.md)、[玩家目标与输入依赖](deck-design/player-objectives.md)、[搜索数学与质量门槛](deck-design/search-design.md)、[共享计算与 Snap 数据契约](deck-design/shared-evaluation-and-data.md)及[自动更新设计](deck-design/data-update.md)。先按这些材料补证、确定模型和策略,再选择 SDK 包装和页面实现;WASM 能跑、组件能渲染、mock 链路能完成都不能代替这两项。 + +## 歌曲计算、组卡与留影效果共用模型 + +两类计算使用同一入口:`Evaluate(snapshot, chart, physicalDeck/supportPairs, memberSkills, snapSkills, play, randomLaw, externalContext) → trace + metrics`。固定同一输入时,歌曲页、固定队伍复算和组卡候选必须得到相同的精确指标。`DeckSearch` 改变编成决策,`SongRanking` 改变歌曲/谱面或明确技能 profile,两个适配器复用核心结算,不各自维护公式。 + +歌曲 meta 纳入 **Snap 效果选择与动态排行** 的设计范围。profile 必须绑定具体生效技能行、等级/因子/时长、目标、配对成员及角色/乐队条件、模式、打法和随机条件,不能将一个脱离成员的“留影标签”当可计算实体。普通与撃奏 Snap 效果分别列出。排行还应区分固定真实卡组、标准技能假设和在玩家持有集合中再搜索的结果,不能混为同一榜单。 + +留影的综合力/属性连携、技能延长、判定转换、条件/限次、击奏与随机调用影响进入完整模拟;只在已证明线性域使用统计权重做预筛。最终值由共用模型复算。nnnotes 导出保存这些 profile/支持范围、谱面技能事件、fever、音符和分离的音乐/计分时长,以及模型/data/profile 的身份。MoeNotes 歌曲 meta 的版本、新鲜度、单位与排序语义是同一条验收链的维护对象。 + +自动更新复用 nnnotes 已有 masterdata 通知与定时补扫。按区服冻结 master、catalog、谱面和计算版本,内容 hash 或模型修复变化也触发重算;经过两曲定向回归与消费端检查后切换完整快照。源事实刷新与新技能/原生规则认证分开处理,未知效果不进入排行。当前任务已发现现有自动构建失败,需要修复,而不是单独增加一个定时器。正式方案与具体边界见 [自动更新设计](deck-design/data-update.md)。 + +原生实验按机制和输入空间分片,先验证最新版执行入口、完整 case ledger 与单 worker CPU/RSS,再按有效队列扩到 64 核或多机。没有有效批次的开发机停机;独立输入、原生调用、完整对局、字段比较和未覆盖分支分别记账。固定种子重复跑不会增加独立覆盖量。 ## 玩家路径 @@ -37,23 +60,40 @@ flowchart LR “稳过谱”需要失败生命周期与人的失误分布,当前不能由一个准确率百分比直接证明。“提升卡片最划算”需要经核验的培养成本与玩家预算;在该契约完成前只使用已确认培养,不静默假设满级。Battle/Arena 需要外部排名确认时间线;Solo 第一名、固定假设名次和真实对手排名必须分别标识。 +## 分层与现有组件复用 + +参考 [allium-deck 固定源码版本 `5c6dff7`](https://github.com/empty-sekai/allium-deck/tree/5c6dff7387e57384b9b2c989ab4f535cdd74f098):[核心模块](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/src/lib.rs)、[搜索问题分派](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/src/search/problem.rs)、[共享搜索预算](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/src/search/budget.rs) 和 [独立 WASM 薄壳](https://github.com/empty-sekai/allium-deck/blob/5c6dff7387e57384b9b2c989ab4f535cdd74f098/wasm/src/lib.rs)。借鉴稳定输入、问题分派、预算与适配方式;Our Notes 保留自己的第三槽队长、binary32、演出随机打乱与成员/留影配对规则。 + +| 层 | 归属与职责 | 不进入该层的内容 | +|---|---|---| +| 识别与证据 | BoxLens:定位、神经网络、字段、跨图冲突、原始来源 | 组卡排序和默认培养 | +| 数值模型 | Deck:Master/冻结 Pool、合法性、逐帧结算、活动 payoff | UI 名称、React、网页状态 | +| 搜索 | Deck:候选/精确策略、上界、统一 tie/Top-K、共享预算与终止语义 | 卡面展示、上传策略 | +| 运行端薄壳 | Deck `wasm/recommend`/CLI:一次解析数据、复用 roster、原 JSON 传输、浏览器 clock | 另写评分公式、内嵌固定 master | +| 产品编排 | MoeNotes `src/lib/deck`:玩家目标→严格请求、交互预算、版本绑定、Worker/缓存 | 实现技能效果或据统计权重替代整场模拟 | +| 展示 | MoeNotes `src/components/deck`:五槽/队长/pairedSnap、指标含义、候选状态 | 重排物理槽、以客户端浮点重新排名 | + +WASM 代码版本与区服 master 更新分别发布,loader 用 manifest 绑定一次会话。借鉴 allium-deck 的 handler 建池与独立 wrapper,但我们的 roster 绑定会话数据与 revision,换 master 必须重建/核对卡池。搜索预算应共用一个 deadline,预筛、warmup、逐根整场模拟都消耗同一预算;Top-K 顺序与可采纳上界集中维护。 + +推荐展示直接复用 `src/components/cards/MemberCardItem.tsx` 与 `src/components/support-cards/SupportCardItem.tsx`;它们已调用 `MemberCardArtwork`/`SupportCardArtwork`、卡框、属性、稀有度与详情跳转。展示适配只将推荐的 master ID 连接已有本地化 `CardViewModel`/`SupportCardViewModel`,不能自己拼图片 URL、改造另一套卡面组件或把 `totalPower` 列表静态值当实际推荐分数。无留影的 `null` 与尚未加载该 ID 的资源区别展示。培养编辑复用既有 `CardGrowthControls`,新增文案进入五个核心 locale。 + ## 浏览器运行决策 可以把权重发给浏览器,但需要分两条链路。 | 部件 | 首选实现 | 当前已具备 | 尚需接入与验证 | |---|---|---|---| -| 身份编码与字段识别 | ONNX Runtime Web,WASM 基础后端,WebGPU 可选加速 | `encoder.onnx` 4,898,068 B;`fields.onnx` 1,356,782 B,合计约 6.25 MB 原始权重 | 浏览器模型加载/张量/前后处理一致性与清晰/压力截图验收 | +| 身份编码与字段识别 | ONNX Runtime Web,WASM 后端 | `encoder.onnx` 4,898,068 B;`fields.onnx` 1,356,782 B,合计约 6.25 MB 原始权重 | 浏览器模型加载/张量/前后处理一致性与清晰/压力截图验收 | | 截图定位、裁切与匹配 | 专用 Worker,移植现有图像处理 | Python OpenCV SIFT/FLANN、模板/布局处理、索引与冲突合并 | 不能只传两份 ONNX 就声称完整 BoxLens;需确认自建 OpenCV WASM 支持所需算法,或单独训练定位模型并重新测覆盖 | -| 固定队伍复算 | 原 Rust 规则编译 WASM | 上游已有 `wasm/replay` 与原生/WASM 对拍 | MoeNotes 数据/Worker/渲染适配 | +| 固定队伍复算 | 原 Rust 规则编译 WASM | 上游已有 `wasm/replay` 与同源 Rust x64/WASM 对拍,另有 ARM64 分范围对照证据 | MoeNotes 数据/Worker/渲染适配 | | 完整推荐 | 原 Rust 搜索编译 WASM,独立 Worker | 生产 JSON 推荐 API 与有限分布搜索 | 新增 recommendation WASM export;统一浏览器时钟;取消/渐进输出与实际浏览器门禁 | -| 候选预筛 | Rust WASM;以后再评估 GPU 预筛 | 综合力 warmup、随机/局部提案与精确逐候选模拟 | 大池/手机延迟基准;统计权重不能取代技能、血量与随机交互 | +| 候选预筛 | Rust WASM | 综合力 warmup、随机/局部提案与精确逐候选模拟 | 大池/手机延迟基准;统计权重不能取代技能、血量与随机交互 | -ONNX Runtime 官方支持 [WASM 和 WebGPU](https://onnxruntime.ai/docs/tutorials/web/ep-webgpu.html)。WebGPU 的启用以实际能力、模型算子与设备测试为准;不能仅凭 `navigator.gpu` 存在认定模型可用。轻量模型的 GPU 上传/读回开销可能超过计算收益,先测热启动与整批截图耗时。 +按接入范围要求,识别、计分和搜索统一使用 WASM,运行端协议也只提供 WASM 和明确选择的远端服务。ONNX Runtime Web 的 [WASM 配置与加载说明](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html) 作为模型 runtime 依据。 -2026-10-01 的 [实际浏览器 smoke 报告](evidence/boxlens-onnx-wasm-20261001.json) 已验证现有两份权重可直接在 Chromium 153 + ONNX Runtime Web 1.30.0 的单线程 WASM 中运行:batch 1/8 共四组相同合成 float32 张量对照 Python CPU,最大绝对误差分别约 `5e-7`(encoder)与 `1.53e-5`(fields),按 `atol=1e-5, rtol=1e-4` 无不一致元素。云 CPU 上热 batch-1 encoder 5.7–7.0 ms、fields 3.5–3.6 ms。这只证明模型算子/形状/runtime 可行,不是完整截图准确率、WebGPU、手机 p95 或逐位浮点等价证明。 +2026-10-01 的 [实际浏览器 smoke 报告](evidence/boxlens-onnx-wasm-20261001.json) 已验证现有两份权重可直接在 Chromium 153 + ONNX Runtime Web 1.30.0 的单线程 WASM 中运行:batch 1/8 共四组相同合成 float32 张量对照 Python CPU,最大绝对误差分别约 `5e-7`(encoder)与 `1.53e-5`(fields),按 `atol=1e-5, rtol=1e-4` 无不一致元素。云 CPU 上热 batch-1 encoder 5.7–7.0 ms、fields 3.5–3.6 ms。这只证明模型算子/形状/runtime 可行,不是完整截图准确率、手机 p95 或逐位浮点等价证明。 -WASM 计分保留 Rust 的整数和声明 binary32 运算。暂不把精确计分改写成 GPU shader,避免引入新的浮点、取整和随机顺序差异。已有 WASM replay 对拍不是完整搜索可用的证明:目前多个搜索路径仍用 `std::time::Instant`,现有 bridge 没有导出 `recommend`。浏览器版本需替换全部 deadline/计时路径,并从 JSON 字节建会话,绕过 CLI 文件系统。参见 [Rust 目标平台限制](https://doc.rust-lang.org/stable/rustc/platform-support/wasm32-unknown-unknown.html)。 +WASM 计分保留 Rust 的整数和声明 binary32 运算,共用经过校准的浮点、取整和随机顺序实现。已有 WASM replay 对拍不是完整搜索可用的证明:目前多个搜索路径仍用 `std::time::Instant`,现有 bridge 没有导出 `recommend`。浏览器版本需替换全部 deadline/计时路径,并从 JSON 字节建会话,绕过 CLI 文件系统。参见 [Rust 目标平台限制](https://doc.rust-lang.org/stable/rustc/platform-support/wasm32-unknown-unknown.html)。 默认单 Worker + WASM,避免先要求站点全局跨源隔离。ONNX 多线程需要 [crossOriginIsolated](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html);启用前需检查 MoeNotes 的图片、音频、嵌入播放器与第三方资源。同步搜索期间 `postMessage(cancel)` 无法中断同一个阻塞调用:第一阶段通过终止专用 Worker 取消,第二阶段实现分片 `step`/进度快照,保留已完成候选。不能把 UI 的取消按钮当作引擎已支持优雅取消。 @@ -64,15 +104,15 @@ WASM 计分保留 Rust 的整数和声明 binary32 运算。暂不把精确计 - 首屏不下载模型。打开卡池工具才按需加载 runtime 和约 6.25 MB 权重,识别图库另计。 - DeckData 分成区服规则核心与按歌曲加载的谱面;卡池和识别数据必须绑定同区服、同一 master 与实际文件哈希。跨区服重识别/迁移需要明确映射依据。 - 模型、索引和谱面进入现有按内容哈希的浏览器缓存,沿用容量上限与清理入口;玩家档案独立存储、导入导出,不因清模型缓存丢失。新存储类型按 AGENTS.md 补全五个核心 locale。 -- 权重、下载进度、缓存占用与缺少 WebGPU 时的 WASM 路径对玩家可见。预期速度必须来自冷/热浏览器实测,不能从本机 Python 或 64 核 CNB 推算手机速度。 +- 权重、下载进度、缓存占用与 WASM 加载状态对玩家可见。预期速度必须来自冷/热浏览器实测,不能从本机 Python 或 64 核 CNB 推算手机速度。 ## 接口与回退 `src/lib/deck/runtime-plan.ts` 定义运行端选择和消息绑定。浏览器完整识别能力、ONNX provider 和浏览器完整搜索是三个独立 capability,不能互相代替。远端识别与远端搜索也分别要求截图上传/卡池上传选择;本地失败不自动上传。可选远端服务采用有界队列、取消和原始输入绑定,运行同一 Rust 模型。该 Draft 不定义未经实现的生产服务 URL。 -Worker 传输 `rosterJson`、`requestJson`、`resultJson` 原始 UTF-8 文本。不能先经过 `JSON.parse`/`JSON.stringify` 丢失超过 `2^53` 的整数、活动时钟或明确 f32 数值 token;语法与重复键由严格边界校验。大文件用 Transferable ArrayBuffer 降低拷贝,首版协议文本用于保证输入语义。消息绑定 `jobId`、`inputRevision` 和 dataset identity,迟到响应及跨版本响应丢弃。 +Worker 传输 `rosterJson`、`requestJson`、`resultJson` 原始 UTF-8 文本。不能先经过 `JSON.parse`/`JSON.stringify` 丢失超过 `2^53` 的整数、活动时钟或明确 f32 数值 token。新 bridge 需要在原始文本边界验证语法与递归重复键;现有 typed request 重复字段已有检查,但 Roster 的任意字典键仍需补回归,不能提前声称全部严格拒绝。大文件用 Transferable ArrayBuffer 降低拷贝,首版协议文本用于保证输入语义。消息绑定 `jobId`、`inputRevision` 和 dataset identity,迟到响应及跨版本响应丢弃。 -搜索返回 `Complete`/`TimedOut`/candidate termination 和 optimality 原样保留。“Complete”仅是所给卡池、约束、打法、外部条件、有限根分布下的证明;候选策略是精确计分的启发式推荐。平均分、范围、P10/P50/P90、达标概率使用整数质量和精确分数,展示层不参与排名。实际 TickCount 分布未知时不能称无条件最优。 +搜索返回 `Complete`/`TimedOut`/candidate termination 和 optimality 原样保留。“Complete”仅是所给卡池、约束、打法、外部条件、有限根分布下的证明;候选策略是精确计分的启发式推荐。平均分、范围、P10/P50/P90、达标概率使用整数质量和精确分数,展示层不参与排名。联合分数/血量目标的达标概率读取 `expectedPayoff`,`scoreSummary.probabilityAtLeast` 仅是分数达标,不能混用。实际 TickCount 分布未知时不能称无条件最优。 ## 实施与验收 diff --git a/src/lib/deck/runtime-plan.ts b/src/lib/deck/runtime-plan.ts index 5a7d535d..a54c6104 100644 --- a/src/lib/deck/runtime-plan.ts +++ b/src/lib/deck/runtime-plan.ts @@ -9,7 +9,6 @@ export interface DeckRuntimeCapabilities { /** Complete image decoding/localization/preprocessing/merge pipeline, beyond ONNX inference alone. */ browserRecognition: boolean; onnxWasm: boolean; - onnxWebGpu: boolean; /** Recommendation export, clock and cancellation support have passed browser parity tests. */ browserSolver: boolean; remoteRecognition: boolean; @@ -22,7 +21,7 @@ export interface DeckRuntimeConsent { } export interface DeckRuntimePlan { - recognition: "onnx-wasm" | "onnx-webgpu" | "remote"; + recognition: "onnx-wasm" | "remote"; recommendation: "wasm-worker" | "remote"; uploads: { screenshots: boolean; roster: boolean }; } @@ -31,17 +30,15 @@ export interface DeckRuntimePlan { export function planDeckRuntime( capabilities: DeckRuntimeCapabilities, consent: DeckRuntimeConsent, - preferWebGpu = false, ): DeckRuntimePlan { - const localRecognition = capabilities.browserRecognition && (capabilities.onnxWasm || capabilities.onnxWebGpu); + const localRecognition = capabilities.browserRecognition && capabilities.onnxWasm; if (!localRecognition && !(capabilities.remoteRecognition && consent.uploadScreenshots)) { throw new Error("Recognition requires a supported local runtime or explicit screenshot upload consent"); } if (!capabilities.browserSolver && !(capabilities.remoteSolver && consent.uploadRoster)) { throw new Error("Recommendation requires a supported local runtime or explicit roster upload consent"); } - const recognition = !localRecognition ? "remote" - : capabilities.onnxWebGpu && (preferWebGpu || !capabilities.onnxWasm) ? "onnx-webgpu" : "onnx-wasm"; + const recognition = localRecognition ? "onnx-wasm" : "remote"; const recommendation = capabilities.browserSolver ? "wasm-worker" : "remote"; return { recognition, recommendation, uploads: { screenshots: recognition === "remote", roster: recommendation === "remote" } }; } diff --git a/tests/deck-runtime-plan.test.ts b/tests/deck-runtime-plan.test.ts index c1fa4cf3..69a67977 100644 --- a/tests/deck-runtime-plan.test.ts +++ b/tests/deck-runtime-plan.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { assertDeckDataIdentity, isCurrentDeckReply, planDeckRuntime, type DeckRuntimeCapabilities, type DeckWorkerRequest } from "../src/lib/deck/runtime-plan"; -const local: DeckRuntimeCapabilities = { browserRecognition: true, onnxWasm: true, onnxWebGpu: false, browserSolver: true, remoteRecognition: true, remoteSolver: true }; +const local: DeckRuntimeCapabilities = { browserRecognition: true, onnxWasm: true, browserSolver: true, remoteRecognition: true, remoteSolver: true }; const noUpload = { uploadScreenshots: false, uploadRoster: false }; const identity = { region: "jp", masterVersion: "synthetic/1", deckDataSha256: "a".repeat(64) }; @@ -9,9 +9,8 @@ describe("collection runtime plan", () => { test("WASM works without GPU and keeps both private inputs local", () => { expect(planDeckRuntime(local, noUpload)).toEqual({ recognition: "onnx-wasm", recommendation: "wasm-worker", uploads: { screenshots: false, roster: false } }); }); - test("WebGPU accelerates recognition while scoring remains the same WASM engine", () => { - expect(planDeckRuntime({ ...local, onnxWebGpu: true }, noUpload, true).recognition).toBe("onnx-webgpu"); - expect(planDeckRuntime({ ...local, onnxWebGpu: true }, noUpload, true).recommendation).toBe("wasm-worker"); + test("missing ONNX WASM support does not silently switch the recognition runtime", () => { + expect(() => planDeckRuntime({ ...local, onnxWasm: false }, noUpload)).toThrow("explicit screenshot upload consent"); }); test("ONNX alone is insufficient for screenshot recognition and cannot authorize uploads", () => { expect(() => planDeckRuntime({ ...local, browserRecognition: false }, noUpload)).toThrow("explicit screenshot upload consent"); From 40007f529cb8e91751b35d08319dffd66276738e Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 12:57:42 +0800 Subject: [PATCH 3/8] docs(meta): record catalog mismatch and pinned source references --- docs/deck-design/data-update.md | 2 +- docs/deck-design/search-design.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deck-design/data-update.md b/docs/deck-design/data-update.md index ced20b33..37d7b62d 100644 --- a/docs/deck-design/data-update.md +++ b/docs/deck-design/data-update.md @@ -6,7 +6,7 @@ StarMoe nnnotes 已有 `music-data.yml`:接收 `masterdata-updated`,每日补扫,并允许手动强制重建。它比较 master/resource/client 版本、resource hash、Deck/nnnotes commit 和 recipe,构建后运行 schema、来源、谱面、统计、音乐长度与消费端 smoke 检查。已有归档、发布开关和上传读回校验,应在此基础上修复。 -截至本轮检查,[最近一次定时任务](https://github.com/StarMoe-org/nnnotes/actions/runs/36699637364)的 plan 成功、Build 步骤失败;之前三次 dispatch 也失败。具体失败原因由发布仓库修复 PR 记录。需要恢复成功的源读取、构建和检查,添加定时器本身不会解除故障。 +截至本轮检查,[最近一次定时任务](https://github.com/StarMoe-org/nnnotes/actions/runs/36699637364)的 plan 成功、Build 步骤失败;之前三次 dispatch 也失败。实际错误是谱面 `10010900` 的资产不存在,涉及 catalog 与源快照不一致。当前发布仓库 main `c5f39f8` 已加入按 snapshot resource version 选择 catalog、核对 hash 并禁止回退到不匹配 catalog 的修复;需要新的完整 dry run 验证,不重复复制已有修复。添加定时器本身不会解除此故障。 MoeNotes 当前客户端直接读取 `music-data.json`。默认线上文件在本轮读取时为 TW、客户端 `1.0.1/25`、master `0b21c9f4a3911370d5f981fb894c7544`,SHA-256 为 `983896151d3ed69d9dfe26044225907e33be037e4b74589e110ded64780f8738`。这份文件含 85 首歌;网页能读取文件,不证明它仍是对应区服的当前版本。 diff --git a/docs/deck-design/search-design.md b/docs/deck-design/search-design.md index c81878a4..c15ff5d0 100644 --- a/docs/deck-design/search-design.md +++ b/docs/deck-design/search-design.md @@ -285,7 +285,7 @@ frontier/缓存保持有界:选择DFS可低内存,但需要证明整个前 ## 11. 已证明域与PoC证据如何使用 -当前本地材料:[旧搜索证明](../../bdon-pipeline-20261001/solver/pr-core/docs/search.md)、[推荐契约PoC](../../bdon-pipeline-20261001/solver/pr-core/docs/recommendation-contract.md)、[范围说明](../../bdon-pipeline-20261001/solver/pr-core/docs/recommendation-coverage.md)、[模型审计](../../bdon-pipeline-20261001/solver/pr-core/docs/model-audit-2026-10-01.md)。 +已发布基线材料:[旧搜索证明](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/search.md)、[推荐契约](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/recommendation-contract.md)、[范围说明](https://github.com/empty-sekai/ournotes-deck/blob/f755c9c0138b0de841f432b8103a2ec02c9ce27d/docs/recommendation-coverage.md)。本轮模型审计与当前门禁见[验证矩阵](model-validation.md),未发布的PoC回归保留实验身份。 - 旧Power/Skip上界与canonicalTop-K有独立穷举和域证明材料,前提绑定当时模型/数据/非负无wrap域。 - 旧有限root物理oracle保留原生洗牌与共同随机状态;其Complete仅证明给定law、轨迹和支持域。 From 14eba89d35377305012e7fe4994eb2f7b6eecc7d Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 13:00:29 +0800 Subject: [PATCH 4/8] docs(model): record fresh JP boot and scoped source checks --- .../apk-signature-verification.json | 32 +++++++++++++++++++ docs/deck-design/latest-game-read.json | 19 ++++++----- docs/deck-design/model-validation.md | 4 ++- 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 docs/deck-design/apk-signature-verification.json diff --git a/docs/deck-design/apk-signature-verification.json b/docs/deck-design/apk-signature-verification.json new file mode 100644 index 00000000..1cf04127 --- /dev/null +++ b/docs/deck-design/apk-signature-verification.json @@ -0,0 +1,32 @@ +{ + "schema": "ournotes.apk-signature-verification/1", + "verifiedAtUtc": "2026-10-01T04:59:27.009200+00:00", + "verifier": "Debian apksigner", + "files": [ + { + "file": "base.apk", + "sha256": "4a095057b6cdc473b7ff67d82e88198507ae9c3a5f4114786d5cd2ad5f340d44", + "exitCode": 0, + "verified": true, + "v2": true, + "v3": true, + "signerCertificateSha256": "34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e", + "sourceStampVerified": false, + "logSha256": "c9b0d8b7991436a3a5bb61c4899bf4e13cc33a49c303b9b70d224680beb0bc79" + }, + { + "file": "split_config.arm64_v8a.apk", + "sha256": "940b70c049e32cdff488608bca8cd40b716e6bec80763b480a191904e7b2e237", + "exitCode": 0, + "verified": true, + "v2": true, + "v3": true, + "signerCertificateSha256": "34fd32c2860f454dd320930f6ba0876ea8cc8e60a3d8320b3277aa761072508e", + "sourceStampVerified": false, + "logSha256": "b7b9ddcc13c2b4e859dbb096d39e6cae9806d248f42283c3611f9b11e6905056" + } + ], + "unityDataSplitVerification": "not-run", + "officialPlayDeliveryComparison": "not-established", + "ruleEquivalence": "not-established" +} diff --git a/docs/deck-design/latest-game-read.json b/docs/deck-design/latest-game-read.json index c13d8780..3d378317 100644 --- a/docs/deck-design/latest-game-read.json +++ b/docs/deck-design/latest-game-read.json @@ -1,14 +1,17 @@ { "schema": "ournotes.latest-game-read/1", - "observedAtUtc": "2026-10-01T04:12:06.443472+00:00", + "collectedAtUtc": "2026-10-01T05:00:10.1314729Z", + "observationTime": "not recorded by source report; collection time is not query time", "region": "jp", "requestClientVersion": "1.0.4", - "source": "unvalidated client-extracted URL candidate, anonymous Version attempt", - "sourceSha256": "ca6c84247d398e0d839afd7b29d424ec2b4c041128e27768a1a13042df9c87e7", - "candidateCount": 1, - "serviceUrlValidation": "not established for this early attempt; it is not evidence that the configured official endpoint is unavailable", - "nativeEquivalence": "not established", - "status": "candidate-read-failed", + "source": "nnnotes.jp.Session.observe with client service URI index 2; sanitized CNB report", + "reportSha256": "286096821a95c5a6194df7244e0ad0d4b72ad5ed9e74a11535def333bd18fc03", + "inputApiCandidateIndex": 2, + "status": "query-failed", "errorType": "GameApiError", - "error": "game API call Version to the API root failed: UNAVAILABLE (server unreachable)" + "diagnosticCategories": [ + "UNAVAILABLE" + ], + "responseVersion": null, + "nativeEquivalence": "not established" } diff --git a/docs/deck-design/model-validation.md b/docs/deck-design/model-validation.md index a9eb0402..204dd377 100644 --- a/docs/deck-design/model-validation.md +++ b/docs/deck-design/model-validation.md @@ -11,7 +11,9 @@ 两个门禁独立:读取到新 master 不会补齐客户端机器码证据;取得新 APK 也不会自动证明使用的是它在当前区服实际加载的 master、资源和 patch。**老 native + 新 master 只是一种明确的混合输入实验,不能代替最新客户端建模。** -本轮原生恢复进展:最新保护区的三个 CRC 已逐项匹配,错误候选均失败;loader 的 275 个轮转与 1,096 个编码符号,已在最新 ARM64 对应函数中执行并与恢复结果比较一致。metadata 已校验 31 个表区和全部 47,081 个 string offsets 的单调性及边界,并修正恢复工具在头部 XOR 终点的错误。当前正在执行首次完整运行时启动验证。上述计数属于恢复与结构检查,**不是新增计分/技能原生用例**;完整启动、对象布局、激活 patch 及游戏语义仍须独立通过。 +本轮原生恢复进展:最新保护区的三个 CRC 已逐项匹配,错误候选均失败;loader 的 275 个轮转与 1,096 个编码符号,已在最新 ARM64 对应函数中执行并与恢复结果比较一致。metadata 已校验 31 个表区和全部 47,081 个 string offsets 的单调性及边界,并修正恢复工具在头部 XOR 终点的错误。首次全新运行时启动已返回 `il2cpp_init=1`、218 个 assemblies,包含 `App.Runtime.dll`;没有加载旧 snapshot,也没有 game-image HLE。启动稳定性复核、最新类/函数 ABI 和字段布局映射仍在进行。上述计数属于恢复、结构与启动检查,**不是新增计分/技能原生用例**;激活 patch 及游戏语义仍须独立通过。 + +[APK 内容签名检查](apk-signature-verification.json)另确认 `base.apk` 与 `split_config.arm64_v8a.apk` 的 apksigner 返回码为 0,v2/v3 均通过,signer certificate SHA 与证书记录一致。Unity 数据 split 的密码学检查尚未执行;SourceStamp 未通过验证,官方 Play delivery 字节比较也未建立。因此不将两个 split 的成功扩大成完整来源链或最新规则认证。 最后可信 JP 快照(来源为本轮 `runtime.tar` 内的 `runtime-data/provenance.json`,不是本轮成功的在线 Version 读取): From f1464e3acb81587612c560a5925e5291b4dc1f8e Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 13:04:58 +0800 Subject: [PATCH 5/8] docs(meta): distinguish measured profiles and deck-derived power --- docs/deck-design/shared-evaluation-and-data.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/deck-design/shared-evaluation-and-data.md b/docs/deck-design/shared-evaluation-and-data.md index 5ec6f1fc..3a9c2e43 100644 --- a/docs/deck-design/shared-evaluation-and-data.md +++ b/docs/deck-design/shared-evaluation-and-data.md @@ -40,6 +40,10 @@ FixedDeck: 只评估用户给定物理编成 原生技能洗牌和随机效果属于求值输入,不能作为玩家可控制的“最佳技能顺序”。多个根 / 判定流的权重来源、实际时钟与多人条件必须随排行输出,缓存 key 也必须包含它们。 +标准 profile 可以声明固定的测量综合力,但必须将其标为 `standardMeasurement`,与真实卡组按歌曲重新算出的 `computedDeck` 分开。这是共享 evaluator 的显式测量输入,不是网页另乘一个技能系数。任何依赖角色、乐队、卡型或配对的 Snap,仍需完整声明对应成员 context;效果选项由版本化的支持 profile 生成,不能让未验证的自由组合进入排行。选择实际卡组时则使用该卡组的真实培养和条件。 + +歌曲 meta 的“需要综合力”与“分数”也要区分。对任意 Snap profile,不能直接以目标分除以线性 score rate 得到最低综合力;只在该目标和参数域的单调性已经证明时,使用同一 evaluator 求门槛。尚未证明单调或不在线性域的 profile,可以展示明确测量综合力下的分数结果,最低综合力指标保持 unsupported。 + ## 导出 / 消费核对矩阵 | 对象 | 当前 exporter / consumer 已提供 | 进入统一 context 还需要确认 | 验证与失败行为 | From bacf5e58066dd10193c3c243a965e9d8408adc5f Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 13:09:29 +0800 Subject: [PATCH 6/8] docs(model): publish fresh runtime and native entry gate --- .../deck-design/latest-native-gate-summary.md | 38 +++++++++++++++++++ docs/deck-design/model-validation.md | 2 + 2 files changed, 40 insertions(+) create mode 100644 docs/deck-design/latest-native-gate-summary.md diff --git a/docs/deck-design/latest-native-gate-summary.md b/docs/deck-design/latest-native-gate-summary.md new file mode 100644 index 00000000..f897387f --- /dev/null +++ b/docs/deck-design/latest-native-gate-summary.md @@ -0,0 +1,38 @@ +# JP Android 1.0.4 / 10053 原生恢复与执行门禁 + +2026-10-01 在 CNB 完成。以下范围是 **offline-base;IFix 未加载**。线上实际生效 hotpatch 与当前 master 版本仍未认证;这份报告不声明最新线上计分模型等价。正式 SDK / UI 继续冻结。 + +## 来源与恢复链 + +完整 Android `10053` APK 与 extracted SO / metadata 来源绑定见 [来源清单](latest-native-source-binding.json)。base 与 ARM64 split 的 APK 内容签名验证见 [apksigner 报告](apk-signature-verification.json);三个 split 的证书解析见 [证书记录](latest-apk-certificates.json)。Unity asset split 的密码学验证未执行,source stamp 未验证;两项状态都不从 base 验证结果继承。 + +| 身份 | SHA-256 | +|---|---| +| 原始 `libil2cpp.so` | `b1600689ee83c435b4a3eaa1ef0709b1ed27783eeda90dace814515a54e0a7ce` | +| 原始 `global-metadata.dat` | `bbf82e20b16ac39ad14c6d3547250efb84daa6ae2c754e46ae9bae5270f9386c` | +| 原始 `libanort.so` | `47a9ceacb4850b311972ffebdde37905a3f7baa5c5588f2be7b2e1d2e9333b74` | +| 保护区恢复 SO | `e6c6970a2114020b4efaf2d6570803b1ddd62c9e90349de7e668a7d956bbbf8e` | +| loader table 恢复 SO | `6059f40b2f949cf028935dacf92df0636f7256150b8bb90e3b52ee1aabfebc70` | +| 完整结构核验的 decoded metadata | `8cf963bc5a32262c2ad3e21307dda14d144bcced6fe7731986933718dcf9a91d` | + +保护区配置从最新 ELF 的 section name / VA / file offset / size 与最新 loader 记录重新匹配。3/3 保护区 CRC 精确一致;错误候选的 3/3 CRC 均失败。恢复动态表时,4 个辅助函数使用完整 FDE 代码 SHA 与最新文件的唯一匹配重新绑定;当前 275 个 import 轮转、1,096 个符号恢复在最新 ARM64 函数中执行,与分析转换结果完全一致。只修改 `.rela.plt` / `.dynsym`,保护代码和只读规则数据保持逐字节不变。符号策略 callback 明确 stub 为 false,这只验证恢复算术,不能证明所有实际部署都会公开所有符号。 + +metadata 的 31/31 table 范围与顺序、47,081/47,081 字面量偏移的单调性和精确起末边界通过;错误 AES 候选的完整偏移不变量失败。复制来的通用 CN helper 在 `0xff8..0x1000` 留下两个未恢复偏移;本轮按 BDON 文件物理边界修正后通过完整不变量。只检查 31 个 header table 不能发现该缺口。初始不合格 metadata 没有计入通过。 + +## 真正执行与验证分母 + +Il2CppDumper 锁定 commit `6fd1d13933f9a14a0a927d816720120894da8d96`,SDK `10.0.100`。最新静态注册位置为 CodeRegistration `0xb69cb90`、MetadataRegistration `0xbc2b828`。这里的静态产物是方法定位入口;数值规则需要独立原生实验。 + +两次独立的完整 runtime boot 都使用上述最新恢复链,均 `il2cpp_init = 1`,取得 218 个 assemblies(包含 App.Runtime.dll),各自创建新的 snapshot。没有读取旧 TW snapshot,没有替换游戏 image 内函数。执行时间分别约 16.666 s / 16.331 s;外部 libc / 文件 / 单线程同步环境由明确的 harness adapter 提供。 + +一次先前使用 Unicorn 内部时间片的 boot 在 runtime allocator 路径失败。通过移除共享内部计时、在进程外设置 timeout 后,上述两次无时间片执行独立通过;失败没有算入通过分母。批量规则执行也须维持无内部时间片与外部 deadline。 + +通过最新原生 C API 实际取得 10 个目标 class 的字段 offset / type / flags、方法参数和返回类型。190/190 个方法的运行时 MethodInfo 首字 code pointer 与最新 dump 中相应 method entry 精确匹配,0 个 class unresolved。CardPower 的值类型大小与对齐另由 native `il2cpp_class_value_size` 读取。每个匹配 entry 附 64 字节代码前缀 SHA;它是明确长度的 entry 身份证据,不是完整函数语义 SHA。 + +这些分母分别是恢复区、字面量 offset、runtime boot、目标 class 与方法身份。**它们都不是已通过的计分、技能、击奏或失败生命周期用例数**。这些机制继续按 [模型矩阵](model-validation.md)逐项验证,并绑定单独的输入与观察字段分母。 + +## 允许继续的研究域 + +可以以已保存的 2026-09-30 JP master / resource 快照,开展“JP Android 1.0.4 / 10053 offline-base + saved JP snapshot”原生对照;明确冻结来源,不称 2026-10-01 当前线上版本。先做真实 entry 的 direct / runtime-invoke ABI pilot、卡力和计分原语,再扩展整场状态与随机 / 撃奏 / 失败生命周期。待单 worker 的准确性、负控制、耗时与 RSS 通过后,才将独立 case queue 分散到更多 worker。 + +本轮恢复工具和精简原始门禁保存在 `work/deck-improve-20261001/native-jp-10053`;此摘要不包含恢复 key、私人 API URL、账号数据或完整游戏 payload。 diff --git a/docs/deck-design/model-validation.md b/docs/deck-design/model-validation.md index 204dd377..5324176c 100644 --- a/docs/deck-design/model-validation.md +++ b/docs/deck-design/model-validation.md @@ -15,6 +15,8 @@ [APK 内容签名检查](apk-signature-verification.json)另确认 `base.apk` 与 `split_config.arm64_v8a.apk` 的 apksigner 返回码为 0,v2/v3 均通过,signer certificate SHA 与证书记录一致。Unity 数据 split 的密码学检查尚未执行;SourceStamp 未通过验证,官方 Play delivery 字节比较也未建立。因此不将两个 split 的成功扩大成完整来源链或最新规则认证。 +[最新原生执行门禁摘要](latest-native-gate-summary.md)进一步记录两次独立启动成功,以及最新 C API 解析的 10 个目标 class、190/190 个 MethodInfo code pointer 与 dump entry 一致、0 unresolved。字段 offset、参数与返回类型从最新原生 API 读取。当前可开始 `offline-base + saved JP snapshot` 的真实数值 pilot;线上 patch 和当前源新鲜度不从这些结构证据继承认证。 + 最后可信 JP 快照(来源为本轮 `runtime.tar` 内的 `runtime-data/provenance.json`,不是本轮成功的在线 Version 读取): | 输入 | 冻结值 | From 9697e97aacd0078499ab0b8382ee6cffa5fcd797 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 13:17:46 +0800 Subject: [PATCH 7/8] docs(meta): link draft refresh implementation and validation --- docs/deck-design/data-update.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/deck-design/data-update.md b/docs/deck-design/data-update.md index 37d7b62d..347c7ff2 100644 --- a/docs/deck-design/data-update.md +++ b/docs/deck-design/data-update.md @@ -2,6 +2,10 @@ 状态:2026-10-01,Draft 设计。修复既有数据发布流程可先进行;最新游戏建模和搜索策略的验收仍是正式组卡功能的前置条件。本文件不表示自动更新修复已部署。 +发布仓库已形成 [nnnotes Draft PR #7](https://github.com/StarMoe-org/nnnotes/pull/7):默认 TW/JP 独立矩阵、内容/消费端身份触发、manifest 一致性、发布前两次源复核、同区服普通/prebuilt 发布锁,以及逐首核对原始单人/房间门槛。验证为 177 passed、5 项可选环境测试 skipped,三份 workflow 通过 actionlint(未运行 shellcheck);TW 与已保存 JP 数据各 85 首的真实源门槛核对通过。 + +该 Draft 没有部署新矩阵,也没有实现下文的多资源原子 manifest 消费协议或任意 Snap 动态求值。当前 main 的 catalog 修复单独使用 [dry run](https://github.com/StarMoe-org/nnnotes/actions/runs/36817548100) 验证,不写生产数据;其完整结果仍待结束。 + ## 当前实际链路与故障 StarMoe nnnotes 已有 `music-data.yml`:接收 `masterdata-updated`,每日补扫,并允许手动强制重建。它比较 master/resource/client 版本、resource hash、Deck/nnnotes commit 和 recipe,构建后运行 schema、来源、谱面、统计、音乐长度与消费端 smoke 检查。已有归档、发布开关和上传读回校验,应在此基础上修复。 From 24d2ae4b2da10f26339c2a2c25e21b67c0b1e0a3 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Thu, 1 Oct 2026 14:42:53 +0800 Subject: [PATCH 8/8] docs: record updated TW thresholds and scoped native gates --- docs/deck-design/data-update.md | 8 +- .../deck-design/latest-native-gate-summary.md | 23 +- docs/deck-design/latest-tw-sentinel-diff.json | 264 ++++++++++++++++++ docs/deck-design/music-meta-refresh-design.md | 32 +++ 4 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 docs/deck-design/latest-tw-sentinel-diff.json create mode 100644 docs/deck-design/music-meta-refresh-design.md diff --git a/docs/deck-design/data-update.md b/docs/deck-design/data-update.md index 347c7ff2..2f720186 100644 --- a/docs/deck-design/data-update.md +++ b/docs/deck-design/data-update.md @@ -2,19 +2,19 @@ 状态:2026-10-01,Draft 设计。修复既有数据发布流程可先进行;最新游戏建模和搜索策略的验收仍是正式组卡功能的前置条件。本文件不表示自动更新修复已部署。 -发布仓库已形成 [nnnotes Draft PR #7](https://github.com/StarMoe-org/nnnotes/pull/7):默认 TW/JP 独立矩阵、内容/消费端身份触发、manifest 一致性、发布前两次源复核、同区服普通/prebuilt 发布锁,以及逐首核对原始单人/房间门槛。验证为 177 passed、5 项可选环境测试 skipped,三份 workflow 通过 actionlint(未运行 shellcheck);TW 与已保存 JP 数据各 85 首的真实源门槛核对通过。 +发布仓库已形成 [nnnotes Draft PR #7](https://github.com/StarMoe-org/nnnotes/pull/7):默认 TW/JP 独立矩阵、内容/消费端身份触发、manifest 一致性、发布前两次源复核、同区服普通/prebuilt 发布锁,以及逐首核对原始单人/房间门槛。验证为 183 passed、5 项可选环境测试 skipped,三份 workflow 通过 actionlint(未运行 shellcheck);TW 与已保存 JP 数据各 85 首的真实源门槛核对通过。 -该 Draft 没有部署新矩阵,也没有实现下文的多资源原子 manifest 消费协议或任意 Snap 动态求值。当前 main 的 catalog 修复单独使用 [dry run](https://github.com/StarMoe-org/nnnotes/actions/runs/36817548100) 验证,不写生产数据;其完整结果仍待结束。 +该 Draft 没有部署新矩阵,也没有实现下文的多资源原子 manifest 消费协议或任意 Snap 动态求值。当前 main 的 catalog 修复单独使用 [dry run](https://github.com/StarMoe-org/nnnotes/actions/runs/36817548100) 验证,不写生产数据;其完整 build/gates/dry-publish 已成功。 ## 当前实际链路与故障 StarMoe nnnotes 已有 `music-data.yml`:接收 `masterdata-updated`,每日补扫,并允许手动强制重建。它比较 master/resource/client 版本、resource hash、Deck/nnnotes commit 和 recipe,构建后运行 schema、来源、谱面、统计、音乐长度与消费端 smoke 检查。已有归档、发布开关和上传读回校验,应在此基础上修复。 -截至本轮检查,[最近一次定时任务](https://github.com/StarMoe-org/nnnotes/actions/runs/36699637364)的 plan 成功、Build 步骤失败;之前三次 dispatch 也失败。实际错误是谱面 `10010900` 的资产不存在,涉及 catalog 与源快照不一致。当前发布仓库 main `c5f39f8` 已加入按 snapshot resource version 选择 catalog、核对 hash 并禁止回退到不匹配 catalog 的修复;需要新的完整 dry run 验证,不重复复制已有修复。添加定时器本身不会解除此故障。 +截至本轮检查,[最近一次定时任务](https://github.com/StarMoe-org/nnnotes/actions/runs/36699637364)的 plan 成功、Build 步骤失败;之前三次 dispatch 也失败。实际错误是谱面 `10010900` 的资产不存在,涉及 catalog 与源快照不一致。当前发布仓库 main `c5f39f8` 已加入按 snapshot resource version 选择 catalog、核对 hash 并禁止回退到不匹配 catalog 的修复;本轮唯一完整 force/dry-run 已成功,不重复复制已有修复。添加定时器本身不会解除此故障。 MoeNotes 当前客户端直接读取 `music-data.json`。默认线上文件在本轮读取时为 TW、客户端 `1.0.1/25`、master `0b21c9f4a3911370d5f981fb894c7544`,SHA-256 为 `983896151d3ed69d9dfe26044225907e33be037e4b74589e110ded64780f8738`。这份文件含 85 首歌;网页能读取文件,不证明它仍是对应区服的当前版本。 -用户提供的两张门槛截图分别与《青春コンプレックス》(`100021`)的上述 TW 门槛及既有 JP 快照的 `requiredScore` 完全相符。JP 的 `battleRequiredScore` 又是另一组值。回归必须保留区服和字段单位,不能将两张图直接解释为同一版本的算法修复。 +用户提供的第二套单人门槛此前只能与已保存 JP 表对应。新的严格 TW 源核对确认 master `74639bc3f98486a22b1232f232213def` 同样已采用该组单人门槛,并将房间门槛更新为另一组值;不能继续只解释为区服差异。两曲 MasterLiveMusic 与各四个 MasterLiveMusicScore 行无字段差分;尚无新版谱面原始字节对照,不能声称 note-score 公式或谱面 bug 已复现。完整原始门槛 old→new 与 metadata/direct-GameAPI 观察边界见[两曲源表回归](music-meta-refresh-design.md)。 ## 更新单元 diff --git a/docs/deck-design/latest-native-gate-summary.md b/docs/deck-design/latest-native-gate-summary.md index f897387f..98e76bfb 100644 --- a/docs/deck-design/latest-native-gate-summary.md +++ b/docs/deck-design/latest-native-gate-summary.md @@ -1,6 +1,6 @@ # JP Android 1.0.4 / 10053 原生恢复与执行门禁 -2026-10-01 在 CNB 完成。以下范围是 **offline-base;IFix 未加载**。线上实际生效 hotpatch 与当前 master 版本仍未认证;这份报告不声明最新线上计分模型等价。正式 SDK / UI 继续冻结。 +2026-10-01 在 CNB 完成。以下范围是 **offline-base;IFix 未加载**。线上实际生效 hotpatch 与当前 master 版本仍未认证;这份报告不声明最新线上计分模型等价。完整截图到组卡的正式 SDK 尚未放行;Snap 排行 Draft 的 Worker / 展示证据应按其独立输入和支持范围阅读。 ## 来源与恢复链 @@ -36,3 +36,24 @@ Il2CppDumper 锁定 commit `6fd1d13933f9a14a0a927d816720120894da8d96`,SDK `10. 可以以已保存的 2026-09-30 JP master / resource 快照,开展“JP Android 1.0.4 / 10053 offline-base + saved JP snapshot”原生对照;明确冻结来源,不称 2026-10-01 当前线上版本。先做真实 entry 的 direct / runtime-invoke ABI pilot、卡力和计分原语,再扩展整场状态与随机 / 撃奏 / 失败生命周期。待单 worker 的准确性、负控制、耗时与 RSS 通过后,才将独立 case queue 分散到更多 worker。 本轮恢复工具和精简原始门禁保存在 `work/deck-improve-20261001/native-jp-10053`;此摘要不包含恢复 key、私人 API URL、账号数据或完整游戏 payload。 + +## 后续已经实际执行的 pilot + +- LiveRandom:6 个 signed root、每 root 4 个 stream、每 stream 5 个操作,共 120 个独立 event 输入;交错 stream、stream 顺序和 reset 重复形成 360 个实际原生 event 调用,连同 24 个派生 seed 及 setup/read 调用共 420 个 bound calls。原生重复 / 交错性质和变 root 负控制通过;实际输入包含 i32 两端及 float32 返回原字。与生产 Rust 的对拍由独立实验报告维护,不能用原生自重复当作跨模型一致。 +- NoteJudgement:最终有效 capture 为 25 个独立声明输入(窗口边界 ±1、中心与时间 i32 wrap)、50 个原生调用。每 case 先设置 position 再构造 native Note,并从 Note 实际持有的 position getter 回读时间,核实 `inputContractVerified`。特定 20 B / align 4 结果的 direct hidden-x8 返回与真实 runtime-invoke / object-unbox 逐字一致,5 个字段由 native API 核实。冻结 f755 生产 `raw::note_judgement` 的独立对拍为 25/25 inputs、125/125 fields、0 differences;比较阶段新增 native 调用 0、model 调用 25。初始 capture 因先构造 Note 再改变外部 position,未改变 native 缓存时间,出现 64 个字段差异,已归类 input-contract-invalid 并保留,未归类模型差异。修正与补契约 gate 的重复 capture 共进行 3 次有效重复(各 50 调用),加初始无效 capture,实际历史 NoteJudgement 调用为 200;仍只有 25 个有效独立输入。这是声明的 synthetic timing units 上的无状态判定,未执行真实谱面或 Touch 生命周期。 +- saved JP 数据:两份 MasterManifest SHA 均为 `f4534fe8bf340b7a9bd99eb9b43ab384e9e3895a7e1eb3113cbb371e65780735`,版本与 9/30 provenance 一致。最新客户端实际 Loader / CryptProvider 读取 MemberCard / LiveMusicScore / LiveJudgementTiming 三个二进制表,bin SHA 全部匹配 manifest;原生解密 / 解压后 payload 的 canonical JSON SHA 与独立 saved JSON 一致。63 / 340 / 276 行分别核实,9 个 first / middle / last ID 使用实际 native GetByID 一致,JSON 桥未知字段为 0。Unity JSON / 路径 / allocator 外部服务通过原生 icall registration 提供,game image 函数替换数为 0。没有认证其余 master 表、完整 JsonUtility 语义、整场技能或失败生命周期。 + +最新同文本 generic method usage 可指向不同程序集实例:例如 MasterLoader 的显示名同时对应 AdvSystem / App / Common。适配器现在拒绝重复显示名的自动选择,并用 native MethodInfo declaring type 与返回对象的真实类型绑定目标;方法身份不能只用反编译显示名。 + +- MemberDataContainer:6 个 signed root、5 张真实 saved-JP member master(character 各不相同),构造器 shuffle order 与独立 native Range(stream 3, max 5→2) 完全一致;shuffle 后四 stream 的下一次 float32 Value 也一致。实际为 6 次 constructor、24 次独立对照 Range、48 次 poststate Value。用户 / power 对象是明确的 placeholder,无留影,totalPower 75,038 是注入值;未执行完整 profile、skill factory 或留影跟随路径,不扩大为整条技能 / Luck 共同 root 链认证。 +- 原始 JP chart:从 JP9/30 专用缓存中解析已绑定 SHA 的官方 catalog,核实 addressable key `Live/MusicScore/0002/0002_01` 及唯一 bundle dependency;提取 `10000201` 的原始 TextAsset 1,277 B,SHA `d52034d539e10651eeb896d10c0d9ef2e5f5e054d115a920e8d6b0d7bdd52c1d` 与 saved JP export 的 asset SHA 逐字一致。cache bundle 为解密后 UnityFS,cache SHA 不能当成加密 CDN 字节 SHA。没有默认采用 TW cache。 +- 最新原生 chart parser:实际调用本版本 `App.Live.MusicScoreLoadUtility.ParseMusicScoreBytes`(新 entry `0x5a1ad8c`),取得 24 lanes / 364 notes;ID、timeMs、operateType 三列逐 note 与独立 saved JP export 对照,1,092 / 1,092 字段一致。Unity JSON bridge 调用为 0、game image HLE 为 0。当前声明 parser 参数 startNoteId 0、slideNoteComboRhythmicUnit 8、mirror false;未核实真实 LiveSettings factory 的参数选择,也未认证 lanes / judgement types、fever、skill events 或完整 gameplay。 + +## Whole Live / SkillManager 最小下一步 + +1. 从同一 saved JP manifest 加载歌曲、计分、Assist、combo、skill / target / condition 等实际所需表;每个被读取 bin 保留 manifest hash 和 native 解码 payload 身份。现已通过的三表和 chart projection 不重复采集来增加分母。 +2. 使用最新 MethodInfo / 签名和原生字段 API,构造真实 Local options 与 LiveSettings factory 路径,读取实际 rhythmicUnit / 窗口 / 计分 / Assist 设置。随后用已绑定 chart 构造 LiveMusicScore、LiveExecutor,在普通 Solo / 无技能声明域跑短轨迹,核实实际 note 时间、阶段、life、combo 与分数。 +3. 用户 / power placeholder 必须替换为明确培养状态的 native Local Member / Support 数据,验证真实 factory 的 member / support 配对与 native shuffle 后共享 RNG 状态,再构造 SkillInput / SkillManager 与 ordinary / support / Gekisou 技能。使用真实 JP 卡与技能 level / effect / condition / target 形成覆盖分母;不从构造成功推导 condition / effect 全部正确。 +4. 先以一个单 worker 的短轨迹核实 setup / enter / frame / trigger / settle 的观察阶段和负控制,再增加长谱、边界、重叠、pool 限额、Gekisou 排名和零生命 / 继续 / 重试生命周期。重放同一 capture 或更换运行端不增加独立 native trace 数。 + +当前实验阻断已经从“缺少最新包 / 原始 JP chart”转为**最新合法 gameplay / skill 上下文与调用边界尚未完成绑定和验收**:旧 capture 的数十个 TW VA、硬字段、generic display-name 选择与手工 vtable 不能直接套用;fresh adapter 当前拒绝未验证的手工 vtable 入口。C++ / managed 异常路径的 harness 支持也须在区分用例中明确,不能把 harness 退出当作游戏规则结果。现有 APK、metadata、saved JP 三表及真实 chart 已足够继续受控研究,当前在线 Version / IFix 缺口不会阻止这个离线域;它们仍阻止扩大为最新线上完整认证。 diff --git a/docs/deck-design/latest-tw-sentinel-diff.json b/docs/deck-design/latest-tw-sentinel-diff.json new file mode 100644 index 00000000..b0a98503 --- /dev/null +++ b/docs/deck-design/latest-tw-sentinel-diff.json @@ -0,0 +1,264 @@ +{ + "format": "ournotes.source-master-diff/1", + "collectedAt": "2026-10-01T06:10:30.967900+00:00", + "region": "tw", + "masterdataRegion": "hk-tw-mo", + "status": "metadata-snapshot-sha-verified-not-direct-gameapi-observation", + "indexSha256": "9c0083913f6189cda5c25eae2b6e4da8f5d39ced58bed692c8549083cd5d88c8", + "old": { + "masterVersion": "0b21c9f4a3911370d5f981fb894c7544", + "publishedMusicDataSha256": "983896151d3ed69d9dfe26044225907e33be037e4b74589e110ded64780f8738" + }, + "new": { + "masterVersion": "74639bc3f98486a22b1232f232213def", + "clientVersion": "1.0.1", + "resourceVersion": "1.0.0.201", + "resourceHash": null, + "manifestSha256": "5955c092fd1991f2996aad38bca6eea799e831907e7dee9c739895a03f7a587e", + "decodedFilesSha256": "ce42c57606a197ff9d37ba8a2bbaffd61250eb2e8e3f92ca397f5cc55c5503cb", + "tables": { + "MasterLiveMusic.json": { + "decodedSha256": "ebcdc732f40a7e0a4941a13591e26fc62cf27a1abadfa441b3b621df60b660a6", + "servedSha256": "fccbcb89680d5c5aaafc7fb6d2efc95eba5ffe773e45fc90bc9def8e27fff3f3" + }, + "MasterLiveScoreRank.json": { + "decodedSha256": "fd1a9eba69b1d78536fe42f7f08f11e7386988289b6b15cf7ed32b2f78ec3b96", + "servedSha256": "0b5789d9cc7f3184830da9c09a3ca67a8bf10e7c13b42be825187a9964312af2" + }, + "MasterLiveMusicScore.json": { + "decodedSha256": "b398406de414a17cc097a2f293b264a9208155b93e0b172d92a628aa4d8ebbd6", + "servedSha256": "8d390eae63ba6206b3a2adba7eac3c355e69ce9d805c344cccdcf95b5157e68c" + } + } + }, + "songCounts": { + "old": 85, + "new": 86, + "added": [ + 100109 + ], + "removed": [] + }, + "sentinels": [ + { + "musicId": 100021, + "musicChanges": {}, + "oldRanks": [ + { + "_id": 121, + "_group": 1100021, + "_liveScoreRank": 2, + "_requiredScore": 0, + "_battleLiveRequiredScore": 0 + }, + { + "_id": 122, + "_group": 1100021, + "_liveScoreRank": 3, + "_requiredScore": 415687, + "_battleLiveRequiredScore": 415687 + }, + { + "_id": 123, + "_group": 1100021, + "_liveScoreRank": 4, + "_requiredScore": 1204973, + "_battleLiveRequiredScore": 1204973 + }, + { + "_id": 124, + "_group": 1100021, + "_liveScoreRank": 5, + "_requiredScore": 3429867, + "_battleLiveRequiredScore": 3429867 + }, + { + "_id": 125, + "_group": 1100021, + "_liveScoreRank": 6, + "_requiredScore": 4638937, + "_battleLiveRequiredScore": 4638937 + }, + { + "_id": 126, + "_group": 1100021, + "_liveScoreRank": 7, + "_requiredScore": 5637904, + "_battleLiveRequiredScore": 5637904 + } + ], + "newRanks": [ + { + "_id": 121, + "_group": 1100021, + "_liveScoreRank": 2, + "_requiredScore": 0, + "_battleLiveRequiredScore": 0 + }, + { + "_id": 122, + "_group": 1100021, + "_liveScoreRank": 3, + "_requiredScore": 495117, + "_battleLiveRequiredScore": 879019 + }, + { + "_id": 123, + "_group": 1100021, + "_liveScoreRank": 4, + "_requiredScore": 1731771, + "_battleLiveRequiredScore": 3064079 + }, + { + "_id": 124, + "_group": 1100021, + "_liveScoreRank": 5, + "_requiredScore": 4389644, + "_battleLiveRequiredScore": 8031441 + }, + { + "_id": 125, + "_group": 1100021, + "_liveScoreRank": 6, + "_requiredScore": 6133727, + "_battleLiveRequiredScore": 11405008 + }, + { + "_id": 126, + "_group": 1100021, + "_liveScoreRank": 7, + "_requiredScore": 7577522, + "_battleLiveRequiredScore": 14303737 + } + ], + "charts": [ + { + "scoreId": 10002100, + "masterScoreChanges": {} + }, + { + "scoreId": 10002101, + "masterScoreChanges": {} + }, + { + "scoreId": 10002102, + "masterScoreChanges": {} + }, + { + "scoreId": 10002103, + "masterScoreChanges": {} + } + ] + }, + { + "musicId": 100026, + "musicChanges": {}, + "oldRanks": [ + { + "_id": 151, + "_group": 1100026, + "_liveScoreRank": 2, + "_requiredScore": 0, + "_battleLiveRequiredScore": 0 + }, + { + "_id": 152, + "_group": 1100026, + "_liveScoreRank": 3, + "_requiredScore": 432065, + "_battleLiveRequiredScore": 432065 + }, + { + "_id": 153, + "_group": 1100026, + "_liveScoreRank": 4, + "_requiredScore": 1174499, + "_battleLiveRequiredScore": 1174499 + }, + { + "_id": 154, + "_group": 1100026, + "_liveScoreRank": 5, + "_requiredScore": 3210324, + "_battleLiveRequiredScore": 3210324 + }, + { + "_id": 155, + "_group": 1100026, + "_liveScoreRank": 6, + "_requiredScore": 4318471, + "_battleLiveRequiredScore": 4318471 + }, + { + "_id": 156, + "_group": 1100026, + "_liveScoreRank": 7, + "_requiredScore": 5660978, + "_battleLiveRequiredScore": 5660978 + } + ], + "newRanks": [ + { + "_id": 151, + "_group": 1100026, + "_liveScoreRank": 2, + "_requiredScore": 0, + "_battleLiveRequiredScore": 0 + }, + { + "_id": 152, + "_group": 1100026, + "_liveScoreRank": 3, + "_requiredScore": 553488, + "_battleLiveRequiredScore": 863848 + }, + { + "_id": 153, + "_group": 1100026, + "_liveScoreRank": 4, + "_requiredScore": 1919628, + "_battleLiveRequiredScore": 3208525 + }, + { + "_id": 154, + "_group": 1100026, + "_liveScoreRank": 5, + "_requiredScore": 4468859, + "_battleLiveRequiredScore": 8108416 + }, + { + "_id": 155, + "_group": 1100026, + "_liveScoreRank": 6, + "_requiredScore": 6270852, + "_battleLiveRequiredScore": 11897425 + }, + { + "_id": 156, + "_group": 1100026, + "_liveScoreRank": 7, + "_requiredScore": 7778519, + "_battleLiveRequiredScore": 14630714 + } + ], + "charts": [ + { + "scoreId": 10002600, + "masterScoreChanges": {} + }, + { + "scoreId": 10002601, + "masterScoreChanges": {} + }, + { + "scoreId": 10002602, + "masterScoreChanges": {} + }, + { + "scoreId": 10002603, + "masterScoreChanges": {} + } + ] + } + ] +} diff --git a/docs/deck-design/music-meta-refresh-design.md b/docs/deck-design/music-meta-refresh-design.md new file mode 100644 index 00000000..3088126f --- /dev/null +++ b/docs/deck-design/music-meta-refresh-design.md @@ -0,0 +1,32 @@ +# 歌曲 meta 刷新与两曲源表回归 + +状态:2026-10-01。本次核对确认 TW 元数据快照的两曲原始评级门槛已改变;它不证明谱面字节或原生评分公式发生改变。代码方案仍为 Draft,不表示生产已经更新。 + +## 源与观察边界 + +当前线上 music-data 为 TW、客户端 1.0.1/25、master `0b21c9f4a3911370d5f981fb894c7544`,文件 SHA-256 `983896151d3ed69d9dfe26044225907e33be037e4b74589e110ded64780f8738`,85 首歌。 + +唯一 force/dry-run [36817548100](https://github.com/StarMoe-org/nnnotes/actions/runs/36817548100) 已完成 success,验证 main `c5f39f8` 的按源 resource version 固定 catalog 修复,未写生产。该任务读取 TW master `74639bc3f98486a22b1232f232213def`、241 文件。随后使用相同元数据服务与请求头读取 index、MasterManifest 和三张歌曲表;每个 decoded 文件 SHA 都与冻结 index 一致,manifest version 与 entry 一致。这是 **metadata 服务的已验证快照**,不是游戏 API Version 直接观察;最新 JP Version 查询仍不能据此标成成功。 + +新快照客户端 1.0.1、resource 1.0.0.201,86 首歌,新增 `100109`,未移除歌曲。它与旧失败任务缺少 `10010900` 资产的事实对应;新 main 完整 build/gates/dry-publish 已通过。Manifest SHA `5955c092fd1991f2996aad38bca6eea799e831907e7dee9c739895a03f7a587e`,index SHA `9c0083913f6189cda5c25eae2b6e4da8f5d39ced58bed692c8549083cd5d88c8`,decoded 文件映射 SHA `ce42c57606a197ff9d37ba8a2bbaffd61250eb2e8e3f92ca397f5cc55c5503cb`。 + +## 门槛差分与截图解释修正 + +表中顺序均为 C / B / A / S / SS;D 始终为 0。`requiredScore` 是原始单人门槛,`battleLiveRequiredScore` 是原始房间门槛;派生 `requiredPower` 不属于源表。 + +| 歌曲 / 原始字段 | 旧 TW 0b21… | 新 TW 74639… | +|---|---|---| +| 青春コンプレックス 100021 / 单人 | 415687 / 1204973 / 3429867 / 4638937 / 5637904 | 495117 / 1731771 / 4389644 / 6133727 / 7577522 | +| 青春コンプレックス 100021 / 房间 | 415687 / 1204973 / 3429867 / 4638937 / 5637904 | 879019 / 3064079 / 8031441 / 11405008 / 14303737 | +| Ave Mujica 100026 / 单人 | 432065 / 1174499 / 3210324 / 4318471 / 5660978 | 553488 / 1919628 / 4468859 / 6270852 / 7778519 | +| Ave Mujica 100026 / 房间 | 432065 / 1174499 / 3210324 / 4318471 / 5660978 | 863848 / 3208525 / 8108416 / 11897425 / 14630714 | + +此前只能证明第二张截图数字匹配已保存 JP 单人表;**新 TW 源证明它同样匹配 TW 新版单人门槛**。因此不能继续把两图差异仅归因区服。现已确认同区 TW 的原始门槛变化,同时仍须核对截图的歌曲、单人/房间单位与显示场景。截图并未证明新版门槛是新的 note-score 系数。 + +两曲的 `MasterLiveMusic` 行与各自四个 `MasterLiveMusicScore` 行在此次旧/新 TW 对照中没有字段差分。尚未做两个 catalog 下谱面 TextAsset 原始字节/notes/events/fevers 的完整对照,也未据官方公告确认修复机制;不能声称谱面 bug 或原生评分改动已复现。对应匿名来源与逐字段报告见 `latest-tw-sentinel-diff.json`。 + +## 自动刷新与评分接入 + +[nnnotes Draft #7](https://github.com/StarMoe-org/nnnotes/pull/7) 以 manifest、decoded 文件映射、resource、客户端、模型/exporter 与消费端 pin 触发重建,默认独立检查 TW/JP;发布前复核源,避免过期结果覆盖新源。最新验证 **183 passed / 5 skipped**,workflow actionlint 通过。新的 rank 参数可按原始源导出并检查,不应等待完全无关的原生语义认证,也不应手工降低两曲排名。 + +[nnnotes Draft #8](https://github.com/StarMoe-org/nnnotes/pull/8) 增加 `nnnotes.replay-labels/1` 的 13 表标签资源与 manifest SHA 指针,使普通/撃奏技能、实际等级描述、条件目标与卡图编号随同源快照更新。它没有评分公式;两曲、五个 Snap 与任意技能 profile 的分数均交给同一个 Rust whole-live evaluator。最新客户端/新增效果语义的原生 gate 仍独立,标签、数据新鲜度、评分正确性不能互相替代。