Align SCUM adapter release gates

This commit is contained in:
npc0-hue
2026-08-13 12:16:58 +08:00
parent 381b59ecae
commit 36ed8822c3
5 changed files with 79 additions and 23 deletions
@@ -382,17 +382,17 @@
}
},
"capabilityGates": [
{ "capability": "schema-probe", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前绑定的 Run 证明通用只读 schema probe 可用。" },
{ "capability": "players.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务 schema evidence 与版本化 adapter 匹配。" },
{ "capability": "player-details.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务玩家详情 join 与字段含义验证。" },
{ "capability": "squads.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务队伍表与 rank/leader 含义验证。" },
{ "capability": "squad-members.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务队伍成员 join 唯一性验证。" },
{ "capability": "vehicles.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务载具 identity、ownership 和坐标验证。" },
{ "capability": "flags.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务旗子 ownership 与 territory 关系验证。" },
{ "capability": "positions.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务坐标范围、cadence 和 map transform 验证。" },
{ "capability": "profile-xml.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待真实 XML 来源、字段名、backup 和离线安全证据验证。" },
{ "capability": "economy-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待 Fame/currency command 与 readback confirmation 验证。" },
{ "capability": "gift-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待 gift item catalog、transport 和 conclusive receipt 验证。" }
{ "capability": "schema-probe", "gate": "enabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "evidenceStatus": "compatible", "safeReason": "当前绑定 durable schema probe 已返回匹配的 redacted schema fingerprint。" },
{ "capability": "players.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:b3359f3c5939a410a98ddfccb920791044df76da4f8e5df4e726a9fe58423520", "sha256:d9c262d0b4f1b600b608c8279a6dfb052de1a605fe1286ddc15f74892eace4fc"], "evidenceStatus": "missing", "safeReason": "schema 和 asset 已匹配;等待 SQLite-template terminal envelope 验证后启用。" },
{ "capability": "player-details.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:c980807b736f8988cd7c37ecb97c72e5e707050450ebc8c539dccec06c60e1f8", "sha256:919c5f68f74fdb0743fa5d9b59aeecd003dcdac5bc51e52d3a486ed1045c9a48"], "evidenceStatus": "missing", "safeReason": "详情和经济只读 asset 已匹配;等待 terminal envelope 验证后启用。" },
{ "capability": "squads.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e7746ac4f4ea86038c1a035087154d4dc3a622fdb14b3ad94b45fec819486f50"], "evidenceStatus": "missing", "safeReason": "队伍只读 asset 已匹配;rankleader 语义保持 nullable,等待 envelope 验证。" },
{ "capability": "squad-members.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e49b634d8a63b0d1f979706ff4a99050c11b5661d494f72897e89da6554f0654"], "evidenceStatus": "missing", "safeReason": "成员 join asset 已匹配;等待 SQLite-template terminal envelope 验证后启用。" },
{ "capability": "vehicles.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e2362451c3a4392adeca38c5e81055674f78e982cee710a33717b3d06bb78dc3"], "evidenceStatus": "missing", "safeReason": "载具 identity 和坐标 asset 已匹配;ownership 保持 nullable,等待 envelope 验证。" },
{ "capability": "flags.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:42ddc6e8a237b7d576834845d1b1d5b2a2fc087668579eac0e3dc08ce2941000"], "evidenceStatus": "missing", "safeReason": "旗子和 base join asset 已匹配;territory 语义保持 gated,等待 envelope 验证。" },
{ "capability": "positions.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:88629de001e6882ce9e72bf5f0b17770d408c665869d04cd768c2e3fdfb9f790", "sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b", "sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478"], "evidenceStatus": "missing", "safeReason": "坐标 query 和 map transform 已匹配;等待 envelope 验证且不声明 sub-10s cadence。" },
{ "capability": "profile-xml.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 XML patch safety、backup、offline、readback 和 855 映射验证。" },
{ "capability": "economy-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 Famecurrency command 的确认读回语义验证。" },
{ "capability": "gift-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 gift catalog、transport 和 conclusive receipt 验证。" }
],
"logParsers": [
{
+21 -1
View File
@@ -1123,7 +1123,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
if (gate.gate === "enabled") {
if (gate.evidenceStatus !== "compatible") errors.push(`${gateLocation}.evidenceStatus: enabled gates require compatible evidence`);
if (!/^(sha256:)?[a-fA-F0-9]{16,128}$/.test(gate.requiredSchemaFingerprint ?? "")) errors.push(`${gateLocation}.requiredSchemaFingerprint: enabled gates require a schema fingerprint`);
if (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
if (capability !== "schema-probe" && (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0)) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
}
if (gate.gate === "disabled" && gate.evidenceStatus === "compatible") errors.push(`${gateLocation}.evidenceStatus: disabled gates must not claim compatible evidence`);
}
@@ -1136,6 +1136,21 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
if (!digestPattern.test(asset.digest ?? "")) errors.push(`${assetLocation}.digest: must be a sha256 digest`);
if (asset.assetPath && asset.digest) validateSCUMAssetDigest(asset.assetPath, asset.digest, `${assetLocation}.digest`);
};
const gateByCapability = new Map(gates.map((gate) => [gate.capability ?? "", gate]));
const requireCapabilityGateMatch = (capability: string | undefined, adapterVersion: string | undefined, schemaFingerprint: string | undefined, digests: Array<string | undefined>, itemLocation: string): void => {
const safeCapability = capability ?? "";
const gate = gateByCapability.get(safeCapability);
if (!gate) {
errors.push(`${itemLocation}: capability ${safeCapability} must have a release gate`);
return;
}
if (gate.adapterVersion !== adapterVersion) errors.push(`${itemLocation}: capability gate adapterVersion must match the asset adapterVersion`);
if (schemaFingerprint && gate.requiredSchemaFingerprint !== schemaFingerprint) errors.push(`${itemLocation}: capability gate schema fingerprint must match the asset requirement`);
const gateDigests = new Set(gate.requiredAssetDigests ?? []);
for (const digest of digests) {
if (digestPattern.test(digest ?? "") && !gateDigests.has(digest ?? "")) errors.push(`${itemLocation}: capability gate must include asset digest ${digest}`);
}
};
const validateSCUMAssetDigest = (assetPath: string, expectedDigest: string, digestLocation: string): void => {
if (!manifestDir || !isSafeRelativePathRef(assetPath) || !digestPattern.test(expectedDigest)) return;
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
@@ -1265,6 +1280,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(query, itemLocation);
if (!readCapabilities.has(query.capability ?? "")) errors.push(`${itemLocation}.capability: must be a read capability`);
if (!fingerprintPattern.test(query.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(query.capability, query.adapterVersion, query.requiredSchemaFingerprint, [query.digest], itemLocation);
requireRefs(query as Record<string, unknown>, itemLocation, ["parameterSchemaRef", "resultSchemaRef"]);
if (!Number.isInteger(query.maxRows) || (query.maxRows ?? 0) < 1 || (query.maxRows ?? 0) > 500) errors.push(`${itemLocation}.maxRows: must be between 1 and 500`);
if (!Number.isInteger(query.timeoutMs) || (query.timeoutMs ?? 0) < 1 || (query.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1288,6 +1304,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(template, itemLocation);
if (!["economy-command.write", "gift-command.write"].includes(template.capability ?? "")) errors.push(`${itemLocation}.capability: must be economy-command.write or gift-command.write`);
if (template.requiredSchemaFingerprint && !fingerprintPattern.test(template.requiredSchemaFingerprint)) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(template.capability, template.adapterVersion, template.requiredSchemaFingerprint, [template.digest], itemLocation);
if (template.permission !== "server.game-client.command" || !declaredPermissions.has("server.game-client.command")) errors.push(`${itemLocation}.permission: must require declared server.game-client.command`);
requireRefs(template as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
if (!Number.isInteger(template.timeoutMs) || (template.timeoutMs ?? 0) < 1 || (template.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1302,6 +1319,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
requireAsset(mutation, itemLocation);
if (mutation.capability !== "profile-xml.write") errors.push(`${itemLocation}.capability: must be profile-xml.write`);
if (!fingerprintPattern.test(mutation.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch(mutation.capability, mutation.adapterVersion, mutation.requiredSchemaFingerprint, [mutation.digest], itemLocation);
if (mutation.permission !== "server.game-client.maintenance" || !declaredPermissions.has("server.game-client.maintenance")) errors.push(`${itemLocation}.permission: must require declared server.game-client.maintenance`);
requireRefs(mutation as Record<string, unknown>, itemLocation, ["payloadSchemaRef", "resultSchemaRef", "confirmationSchemaRef"]);
if (!Number.isInteger(mutation.timeoutMs) || (mutation.timeoutMs ?? 0) < 1 || (mutation.timeoutMs ?? 0) > 60000) errors.push(`${itemLocation}.timeoutMs: must be between 1 and 60000`);
@@ -1317,6 +1335,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const itemLocation = `${location}.mapAssets[${index}]`;
requireAsset(mapAsset, itemLocation);
if (!fingerprintPattern.test(mapAsset.requiredSchemaFingerprint ?? "")) errors.push(`${itemLocation}.requiredSchemaFingerprint: must be a schema fingerprint`);
requireCapabilityGateMatch("positions.read", mapAsset.adapterVersion, mapAsset.requiredSchemaFingerprint, [mapAsset.digest, mapAsset.transformDigest], itemLocation);
requireRefs(mapAsset as Record<string, unknown>, itemLocation, ["metadataSchemaRef"]);
if (!mapAsset.transformAssetPath || !isSafeRelativePathRef(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be a contained package-relative path`);
if (mapAsset.transformAssetPath && !assetFiles.has(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be declared in manifest.assetFiles`);
@@ -1332,6 +1351,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const itemLocation = `${location}.giftCatalogs[${index}]`;
requireAsset(catalog, itemLocation);
if (!adapterPattern.test(catalog.catalogVersion ?? "")) errors.push(`${itemLocation}.catalogVersion: must be a bounded catalog version`);
requireCapabilityGateMatch("gift-command.write", catalog.adapterVersion, undefined, [catalog.digest], itemLocation);
requireRefs(catalog as Record<string, unknown>, itemLocation, ["itemSchemaRef"]);
if (!Array.isArray(catalog.transportTemplateKeys) || catalog.transportTemplateKeys.length === 0) errors.push(`${itemLocation}.transportTemplateKeys: must reference at least one typed RCON transport template`);
for (const templateKey of catalog.transportTemplateKeys ?? []) {
+38 -9
View File
@@ -294,14 +294,31 @@ describe("plugin manifest validation", () => {
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(false);
});
it("keeps SCUM database-backed live-data gates disabled until current-service evidence exists", () => {
it("matches SCUM live-data gates to the current-service adapter matrix", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration; remoteAccess: { runCapabilities: string[] } };
expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe");
expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe");
expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" });
expect(manifest.runtimeProfiles?.dataTargets?.find((target) => target.key === "scum-database")).toMatchObject({ kind: "sqlite.snapshot", transportKey: "scum-database", sourceRootKey: "server-root", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" });
expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true);
const gates = new Map(manifest.scumLiveData.capabilityGates.map((gate) => [gate.capability, gate]));
expect(gates.get("schema-probe")).toMatchObject({ gate: "enabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, evidenceStatus: "compatible" });
const queryDigestsByCapability = new Map<string, string[]>();
for (const query of manifest.scumLiveData.sqliteQueries ?? []) {
const digests = queryDigestsByCapability.get(query.capability) ?? [];
digests.push(query.digest);
queryDigestsByCapability.set(query.capability, digests);
}
const mapAsset = manifest.scumLiveData.mapAssets?.[0];
if (mapAsset) queryDigestsByCapability.set("positions.read", [...(queryDigestsByCapability.get("positions.read") ?? []), mapAsset.digest, mapAsset.transformDigest]);
for (const [capability, digests] of queryDigestsByCapability) {
const gate = gates.get(capability as any);
expect(gate).toMatchObject({ gate: "disabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, evidenceStatus: "missing" });
for (const digest of digests) expect(gate?.requiredAssetDigests).toContain(digest);
}
expect([...gates.keys()]).toEqual(expect.arrayContaining(["players.read", "player-details.read", "squads.read", "squad-members.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
for (const capability of ["profile-xml.write", "economy-command.write", "gift-command.write"] as const) expect(gates.get(capability)).toMatchObject({ gate: "disabled", adapterVersion: "scum-live-data-v1", evidenceStatus: "missing" });
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
});
@@ -483,7 +500,7 @@ describe("plugin manifest validation", () => {
it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" };
manifest.scumLiveData.capabilityGates[1] = { capability: "players.read", gate: "enabled", adapterVersion: "scum-live-data-v1", evidenceStatus: "missing", safeReason: "waiting for current service evidence" };
});
expect(errors.some((error) => error.includes("enabled gates require compatible evidence"))).toBe(true);
expect(errors.some((error) => error.includes("enabled gates require a schema fingerprint"))).toBe(true);
@@ -503,21 +520,33 @@ describe("plugin manifest validation", () => {
];
const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => {
manifest.assetFiles = [...manifest.assetFiles, ...assetPaths.map((assetPath) => ({ path: assetPath, mode: 384 }))];
for (const assetPath of [assetPaths[0], assetPaths[1], assetPaths[2], assetPaths[3], assetPaths[6]]) writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
for (const assetPath of [assetPaths[0], assetPaths[1], assetPaths[2], assetPaths[3], assetPaths[6]]) writeFixtureJSON(fixtureDir, assetPath, { packaged: true, assetPath });
const mapBounds = { minX: -100000, minY: -100000, maxX: 100000, maxY: 100000 };
const mapImage = { width: 4096, height: 4096 };
writeFixtureJSON(fixtureDir, assetPaths[4], { key: "island-map", mapVersion: "island-map-v1", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, sourceEvidence: "fixture", authorization: { redistribution: "first-party-generated-coordinate-metadata", baseMapArtwork: "not-packaged", renderingAvailability: "unavailable-until-authorized-base-map" }, worldBounds: mapBounds, image: mapImage, layers: [{ key: "players", capability: "positions.read", subjectType: "player", label: "Players", sourceQueryKey: "scum-positions-read" }], transformAssetPath: assetPaths[5] });
writeFixtureJSON(fixtureDir, assetPaths[5], { key: "island-transform", transformVersion: "island-transform-v1", mapAssetKey: "island-map", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, worldBounds: mapBounds, image: mapImage, validation: { rejectNonFinite: true, rejectOutOfBounds: true, acceptBoundaryPoints: true }, fixtures: [{ name: "origin", world: { x: 0, y: 0 }, pixel: { x: 2047.5, y: 2047.5 } }] });
const digest = (assetPath: string) => sha256FixtureDigest(fixtureDir, assetPath);
const giftGrantDigest = digest(assetPaths[2]);
const mutationDigest = digest(assetPaths[3]);
const mapDigest = digest(assetPaths[4]);
const transformDigest = digest(assetPaths[5]);
const catalogDigest = digest(assetPaths[6]);
manifest.scumLiveData = {
...manifest.scumLiveData,
capabilityGates: [
{ capability: "schema-probe", gate: "disabled", adapterVersion: "scum-live-data-v1", evidenceStatus: "missing", safeReason: "waiting for current service evidence" },
{ capability: "players.read", gate: "disabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, requiredAssetDigests: [digest(assetPaths[1])], evidenceStatus: "missing", safeReason: "waiting for query execution evidence" },
{ capability: "positions.read", gate: "disabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, requiredAssetDigests: [mapDigest, transformDigest], evidenceStatus: "missing", safeReason: "waiting for map execution evidence" },
{ capability: "gift-command.write", gate: "disabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, requiredAssetDigests: [giftGrantDigest, catalogDigest], evidenceStatus: "missing", safeReason: "waiting for gift command evidence" },
{ capability: "profile-xml.write", gate: "disabled", adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: fingerprint, requiredAssetDigests: [mutationDigest], evidenceStatus: "missing", safeReason: "waiting for mutation safety evidence" }
],
logParsers: [{ key: "login-parser", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[0], digest: digest(assetPaths[0]), parserVersion: "login-v1", sourceKey: "scum-login-events", eventType: "scum.login", eventSchemaRef: "schemas/scum-live/login-event.schema.json", maxLineBytes: 4096, cursorPolicy: "source-generation-sequence", privacy: { stripNetworkIdentifiers: true, logicalEventIdentity: "native-or-sanitized-fields" } }],
sqliteQueries: [{ key: "players-read", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[1], digest: digest(assetPaths[1]), capability: "players.read", requiredSchemaFingerprint: fingerprint, transportKey: "scum-database", targetKey: "scum-database", parameterSchemaRef: "schemas/scum-live/players-read.parameters.schema.json", resultSchemaRef: "schemas/scum-live/players-read.result.schema.json", maxRows: 100, timeoutMs: 5000, maxResultBytes: 65536 }],
syncCadences: [{ capability: "players.read", intervalSeconds: 300, jitterPercent: 20, timeoutMs: 5000, maxConcurrentPerServer: 1 }],
typedRconTemplates: [{ key: "gift-grant", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[2], digest: digest(assetPaths[2]), capability: "gift-command.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-management", targetKey: "scum-management", permission: "server.game-client.command", payloadSchemaRef: "schemas/scum-live/gift-grant.payload.schema.json", resultSchemaRef: "schemas/scum-live/gift-grant.result.schema.json", confirmationSchemaRef: "schemas/scum-live/gift-grant.confirmation.schema.json", timeoutMs: 5000, maxPayloadBytes: 4096 }],
guardedMutations: [{ key: "profile-xml-patch", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[3], digest: digest(assetPaths[3]), capability: "profile-xml.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-database", targetKey: "scum-database", permission: "server.game-client.maintenance", payloadSchemaRef: "schemas/scum-live/profile-xml-patch.payload.schema.json", resultSchemaRef: "schemas/scum-live/profile-xml-patch.result.schema.json", confirmationSchemaRef: "schemas/scum-live/profile-xml-patch.confirmation.schema.json", timeoutMs: 10000, maxPayloadBytes: 8192, maxRowsAffected: 1, safety: { requiresExpectedChecksum: true, requiresBackupEvidence: true, requiresOfflineOrMaintenance: true, requiresReadAfterWrite: true } }],
mapAssets: [{ key: "island-map", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[4], digest: digest(assetPaths[4]), requiredSchemaFingerprint: fingerprint, metadataSchemaRef: "schemas/scum-live/map-metadata.schema.json", transformAssetPath: assetPaths[5], transformDigest: digest(assetPaths[5]), worldBounds: { minX: -100000, minY: -100000, maxX: 100000, maxY: 100000 }, image: { width: 4096, height: 4096 } }],
giftCatalogs: [{ key: "starter-gifts", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[6], digest: digest(assetPaths[6]), catalogVersion: "catalog-v1", itemSchemaRef: "schemas/scum-live/gift-item.schema.json", transportTemplateKeys: ["gift-grant"] }]
typedRconTemplates: [{ key: "gift-grant", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[2], digest: giftGrantDigest, capability: "gift-command.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-management", targetKey: "scum-management", permission: "server.game-client.command", payloadSchemaRef: "schemas/scum-live/gift-grant.payload.schema.json", resultSchemaRef: "schemas/scum-live/gift-grant.result.schema.json", confirmationSchemaRef: "schemas/scum-live/gift-grant.confirmation.schema.json", timeoutMs: 5000, maxPayloadBytes: 4096 }],
guardedMutations: [{ key: "profile-xml-patch", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[3], digest: mutationDigest, capability: "profile-xml.write", requiredSchemaFingerprint: fingerprint, transportKey: "scum-database", targetKey: "scum-database", permission: "server.game-client.maintenance", payloadSchemaRef: "schemas/scum-live/profile-xml-patch.payload.schema.json", resultSchemaRef: "schemas/scum-live/profile-xml-patch.result.schema.json", confirmationSchemaRef: "schemas/scum-live/profile-xml-patch.confirmation.schema.json", timeoutMs: 10000, maxPayloadBytes: 8192, maxRowsAffected: 1, safety: { requiresExpectedChecksum: true, requiresBackupEvidence: true, requiresOfflineOrMaintenance: true, requiresReadAfterWrite: true } }],
mapAssets: [{ key: "island-map", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[4], digest: mapDigest, requiredSchemaFingerprint: fingerprint, metadataSchemaRef: "schemas/scum-live/map-metadata.schema.json", transformAssetPath: assetPaths[5], transformDigest, worldBounds: { minX: -100000, minY: -100000, maxX: 100000, maxY: 100000 }, image: { width: 4096, height: 4096 } }],
giftCatalogs: [{ key: "starter-gifts", adapterVersion: "scum-live-data-v1", assetPath: assetPaths[6], digest: catalogDigest, catalogVersion: "catalog-v1", itemSchemaRef: "schemas/scum-live/gift-item.schema.json", transportTemplateKeys: ["gift-grant"] }]
};
});
expect(errors).toEqual([]);