Add SCUM current-service map asset contract

This commit is contained in:
npc0-hue
2026-08-13 10:19:39 +08:00
parent 5331143806
commit 25a823f298
7 changed files with 341 additions and 2 deletions
@@ -55,6 +55,13 @@
- Added focused companion tests for successful login/logout, failed login, partial, undecodable, oversized, malformed lines, rotation/copy-truncate overlap across a new generation, restart/resume acknowledgements, duplicate transport/logical delivery, out-of-order delivery, and absence of network/coordinate material in parsed event storage/fingerprints.
- Verification passed: `(cd plugins/examples/scum-server-plugin/companion && go test ./...)` and `(cd plugins && npm run validate:manifest)`.
### Map asset implementation evidence (2026-08-13)
- Added plugin-owned SCUM current-service coordinate-map metadata and transform assets under `assets/scum-live/map/`, digest-referenced from `scumLiveData.mapAssets` as `sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b` and `sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478`.
- The packaged asset is explicitly limited to first-party-generated coordinate metadata and the observed current-service coordinate envelope from `evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md`; no unauthorized SCUM base-map artwork is shipped, and `positions.read` remains disabled until compatible evidence enables it.
- Added manifest-validator checks and fixture tests for map metadata/schema compatibility, transform adapter/schema fingerprint matching, declared bounds/image consistency, known-point projection fixtures, non-finite/out-of-bounds rejection, and digest-preserving adapter incompatibility.
- Verification passed: `(cd plugins && npm test -- manifest-validation.test.ts)` and `(cd plugins && npm run validate:manifest)`.
- [x] 3.1 Add SDK and manifest types for versioned log parsers, SQLite query assets, parameter/result schemas, capability-specific schema fingerprints, sync cadence/limits, map metadata, typed RCON templates, gift item catalogs, and guarded mutation declarations.
- [x] 3.2 Extend plugin validation to require asset digests, contained package paths, unique template keys, bounded parameters/results, compatible adapter versions, and explicit permission bindings, and to reject raw caller-supplied SQL, RCON, XML, paths, or undeclared parameters.
- [x] 3.3 Capture sanitized authentic login-log fixtures from the active service and bind their expected events to server, Run binding, plugin version, parser version/digest, a transport cursor `(source identity, stream generation, sequence)`, and a separate privacy-safe logical event identity stable across rotation overlap.
@@ -62,7 +69,7 @@
- [x] 3.5 Add parameterized, read-only player identity/detail/economy/session-enrichment query assets and exact result schemas only for joins and fields proven by the probe.
- [x] 3.6 Add parameterized squad/member, vehicle, flag/territory, and position query assets and exact result schemas, keeping ambiguous ranks, ownership, coordinates, and missing numeric values null.
- [x] 3.7 Add query-asset tests for single SELECT/CTE or approved introspection boundaries, parameter binding, pagination/cursors, timeout/row/byte limits, schema-version matching, and rejection of DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, and multi-statement input.
- [ ] 3.8 Package the authorized SCUM map asset, identity/version, verified world bounds, layer metadata, and coordinate transform, with fixture tests for known points, out-of-bounds/non-finite coordinates, and adapter incompatibility.
- [x] 3.8 Package the authorized SCUM map asset, identity/version, verified world bounds, layer metadata, and coordinate transform, with fixture tests for known points, out-of-bounds/non-finite coordinates, and adapter incompatibility.
- [ ] 3.9 Declare only verified typed RCON templates for supported Fame/currency/notification/gift operations and a version-scoped gift item catalog; omit any command whose execution and confirmation semantics remain unknown.
- [ ] 3.10 Declare a guarded preserving XML mutation only after the real XML source and named attributes are proven; expose `855` only as a reviewed named-attribute preset and never as a database column, generic integer field, or guessed mapping.
- [x] 3.11 Add immutable asset/digest declarations and plugin package validation; defer generated Run-package execution wiring until the complete protocol/result envelope and independent Run capability evidence in group 4 are frozen.
@@ -0,0 +1,46 @@
{
"key": "scum-current-service-coordinate-map",
"mapVersion": "scum-current-service-coordinate-map-v1",
"adapterVersion": "scum-live-data-v1",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"authorization": {
"redistribution": "first-party-generated-coordinate-metadata",
"baseMapArtwork": "not-packaged",
"renderingAvailability": "unavailable-until-authorized-base-map"
},
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
},
"layers": [
{
"key": "players",
"capability": "positions.read",
"subjectType": "player",
"label": "Players",
"sourceQueryKey": "scum-positions-read"
},
{
"key": "vehicles",
"capability": "positions.read",
"subjectType": "vehicle",
"label": "Vehicles",
"sourceQueryKey": "scum-positions-read"
},
{
"key": "flags",
"capability": "positions.read",
"subjectType": "flag",
"label": "Flags",
"sourceQueryKey": "scum-positions-read"
}
],
"transformAssetPath": "assets/scum-live/map/current-service-transform.json"
}
@@ -0,0 +1,49 @@
{
"key": "scum-current-service-coordinate-transform",
"transformVersion": "scum-current-service-transform-v1",
"mapAssetKey": "scum-current-service-coordinate-map",
"adapterVersion": "scum-live-data-v1",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"coordinateSystem": "current-service-observed-entity-envelope",
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
},
"axisMapping": {
"x": "left-to-right",
"y": "bottom-to-top-inverted-for-image-y"
},
"formula": {
"pixelX": "((x - minX) / (maxX - minX)) * (width - 1)",
"pixelY": "((maxY - y) / (maxY - minY)) * (height - 1)"
},
"validation": {
"rejectNonFinite": true,
"rejectOutOfBounds": true,
"acceptBoundaryPoints": true
},
"fixtures": [
{
"name": "observed-minimum-corner",
"world": { "x": -901009.4375, "y": -883992.5625 },
"pixel": { "x": 0, "y": 4095 }
},
{
"name": "observed-maximum-corner",
"world": { "x": 612580.0625, "y": 615977.375 },
"pixel": { "x": 4095, "y": 0 }
},
{
"name": "observed-center",
"world": { "x": -144214.6875, "y": -134007.59375 },
"pixel": { "x": 2047.5, "y": 2047.5 }
}
]
}
@@ -558,6 +558,28 @@
"timeoutMs": 2000,
"maxResultBytes": 262144
}
],
"mapAssets": [
{
"key": "scum-current-service-coordinate-map",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/map/current-service-map.json",
"digest": "sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"metadataSchemaRef": "schemas/scum-live/map-metadata.schema.json",
"transformAssetPath": "assets/scum-live/map/current-service-transform.json",
"transformDigest": "sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478",
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
}
}
]
},
"permissions": [
@@ -653,6 +675,14 @@
{
"path": "assets/scum-live/queries/positions-read.json",
"mode": 384
},
{
"path": "assets/scum-live/map/current-service-map.json",
"mode": 384
},
{
"path": "assets/scum-live/map/current-service-transform.json",
"mode": 384
}
],
"productionLifecycle": {
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMCurrentServiceMapMetadata",
"type": "object",
"additionalProperties": false,
"required": ["key", "mapVersion", "adapterVersion", "requiredSchemaFingerprint", "sourceEvidence", "authorization", "worldBounds", "image", "layers", "transformAssetPath"],
"properties": {
"key": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]{0,119}$" },
"mapVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
"adapterVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
"requiredSchemaFingerprint": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
"sourceEvidence": { "type": "string", "minLength": 1, "maxLength": 240 },
"authorization": {
"type": "object",
"additionalProperties": false,
"required": ["redistribution", "baseMapArtwork", "renderingAvailability"],
"properties": {
"redistribution": { "const": "first-party-generated-coordinate-metadata" },
"baseMapArtwork": { "const": "not-packaged" },
"renderingAvailability": { "const": "unavailable-until-authorized-base-map" }
}
},
"worldBounds": {
"type": "object",
"additionalProperties": false,
"required": ["minX", "minY", "maxX", "maxY"],
"properties": { "minX": { "type": "number" }, "minY": { "type": "number" }, "maxX": { "type": "number" }, "maxY": { "type": "number" } }
},
"image": {
"type": "object",
"additionalProperties": false,
"required": ["width", "height"],
"properties": { "width": { "type": "integer", "minimum": 1 }, "height": { "type": "integer", "minimum": 1 } }
},
"layers": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "capability", "subjectType", "label", "sourceQueryKey"],
"properties": {
"key": { "enum": ["players", "vehicles", "flags"] },
"capability": { "const": "positions.read" },
"subjectType": { "enum": ["player", "vehicle", "flag"] },
"label": { "type": "string", "minLength": 1, "maxLength": 40 },
"sourceQueryKey": { "const": "scum-positions-read" }
}
}
},
"transformAssetPath": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,240}$" }
}
}
+72
View File
@@ -1150,6 +1150,77 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
}
if (sha256FileDigest(absoluteAssetPath) !== expectedDigest) errors.push(`${digestLocation}: digest does not match packaged asset content`);
};
const readSCUMJSONAsset = (assetPath: string | undefined, assetLocation: string): Record<string, unknown> | undefined => {
if (!manifestDir || !assetPath || !isSafeRelativePathRef(assetPath)) return undefined;
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
if (!fs.existsSync(absoluteAssetPath) || !fs.statSync(absoluteAssetPath).isFile()) return undefined;
try {
const value = readJson(absoluteAssetPath);
if (!value || typeof value !== "object" || Array.isArray(value)) {
errors.push(`${assetLocation}: asset content must be a JSON object`);
return undefined;
}
return value as Record<string, unknown>;
} catch {
errors.push(`${assetLocation}: asset content must be valid JSON`);
return undefined;
}
};
const mapBoundsMatch = (candidate: unknown, bounds: MapAsset["worldBounds"]): boolean => {
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate) || !bounds) return false;
const value = candidate as { minX?: unknown; minY?: unknown; maxX?: unknown; maxY?: unknown };
return value.minX === bounds.minX && value.minY === bounds.minY && value.maxX === bounds.maxX && value.maxY === bounds.maxY;
};
const mapImageMatch = (candidate: unknown, image: MapAsset["image"]): boolean => {
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate) || !image) return false;
const value = candidate as { width?: unknown; height?: unknown };
return value.width === image.width && value.height === image.height;
};
const requireSCUMMapAssetJSONCompatibility = (mapAsset: MapAsset, itemLocation: string): void => {
const metadata = readSCUMJSONAsset(mapAsset.assetPath, `${itemLocation}.assetPath`);
if (metadata) {
if (metadata.key !== mapAsset.key) errors.push(`${itemLocation}.assetPath: metadata key must match the map asset declaration`);
if (!adapterPattern.test(String(metadata.mapVersion ?? ""))) errors.push(`${itemLocation}.assetPath: metadata mapVersion must be a bounded version`);
if (metadata.adapterVersion !== mapAsset.adapterVersion) errors.push(`${itemLocation}.assetPath: metadata adapterVersion must match the map asset declaration`);
if (metadata.requiredSchemaFingerprint !== mapAsset.requiredSchemaFingerprint) errors.push(`${itemLocation}.assetPath: metadata schema fingerprint must match the map asset declaration`);
if (metadata.transformAssetPath !== mapAsset.transformAssetPath) errors.push(`${itemLocation}.assetPath: metadata transformAssetPath must match the map asset declaration`);
if (!mapBoundsMatch(metadata.worldBounds, mapAsset.worldBounds)) errors.push(`${itemLocation}.assetPath: metadata worldBounds must match the map asset declaration`);
if (!mapImageMatch(metadata.image, mapAsset.image)) errors.push(`${itemLocation}.assetPath: metadata image dimensions must match the map asset declaration`);
const layers = metadata.layers;
if (!Array.isArray(layers) || layers.length === 0) {
errors.push(`${itemLocation}.assetPath: metadata must declare at least one map layer`);
} else {
const layerKeys = new Set<string>();
for (const [layerIndex, layer] of layers.entries()) {
const layerLocation = `${itemLocation}.assetPath.layers[${layerIndex}]`;
if (!layer || typeof layer !== "object" || Array.isArray(layer)) {
errors.push(`${layerLocation}: layer metadata must be an object`);
continue;
}
const candidate = layer as { key?: unknown; capability?: unknown; subjectType?: unknown; sourceQueryKey?: unknown };
const layerKey = String(candidate.key ?? "");
if (layerKeys.has(layerKey)) errors.push(`${layerLocation}.key: duplicate layer key ${layerKey}`);
layerKeys.add(layerKey);
if (!logicalKeyPattern.test(layerKey)) errors.push(`${layerLocation}.key: must be a safe logical key`);
if (candidate.capability !== "positions.read") errors.push(`${layerLocation}.capability: map layers must use positions.read`);
if (!["player", "vehicle", "flag"].includes(String(candidate.subjectType ?? ""))) errors.push(`${layerLocation}.subjectType: must be player, vehicle, or flag`);
if (candidate.sourceQueryKey !== "scum-positions-read") errors.push(`${layerLocation}.sourceQueryKey: must reference the packaged positions query`);
}
}
}
const transform = readSCUMJSONAsset(mapAsset.transformAssetPath, `${itemLocation}.transformAssetPath`);
if (transform) {
if (transform.mapAssetKey !== mapAsset.key) errors.push(`${itemLocation}.transformAssetPath: transform mapAssetKey must match the map asset declaration`);
if (!adapterPattern.test(String(transform.transformVersion ?? ""))) errors.push(`${itemLocation}.transformAssetPath: transformVersion must be a bounded version`);
if (transform.adapterVersion !== mapAsset.adapterVersion) errors.push(`${itemLocation}.transformAssetPath: transform adapterVersion must match the map asset declaration`);
if (transform.requiredSchemaFingerprint !== mapAsset.requiredSchemaFingerprint) errors.push(`${itemLocation}.transformAssetPath: transform schema fingerprint must match the map asset declaration`);
if (!mapBoundsMatch(transform.worldBounds, mapAsset.worldBounds)) errors.push(`${itemLocation}.transformAssetPath: transform worldBounds must match the map asset declaration`);
if (!mapImageMatch(transform.image, mapAsset.image)) errors.push(`${itemLocation}.transformAssetPath: transform image dimensions must match the map asset declaration`);
const validation = transform.validation as { rejectNonFinite?: unknown; rejectOutOfBounds?: unknown; acceptBoundaryPoints?: unknown } | undefined;
if (validation?.rejectNonFinite !== true || validation?.rejectOutOfBounds !== true || validation?.acceptBoundaryPoints !== true) errors.push(`${itemLocation}.transformAssetPath.validation: must reject non-finite/out-of-bounds coordinates and accept boundary points`);
if (!Array.isArray(transform.fixtures) || transform.fixtures.length === 0) errors.push(`${itemLocation}.transformAssetPath.fixtures: transform must include known-point fixtures`);
}
};
const requireUniqueAssetKeys = (assets: Asset[] | undefined, collection: string): void => {
const seenKeys = new Set<string>();
for (const [index, asset] of (assets ?? []).entries()) {
@@ -1255,6 +1326,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
const bounds = mapAsset.worldBounds;
if (!bounds || !Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY) || !Number.isFinite(bounds.maxX) || !Number.isFinite(bounds.maxY) || (bounds.minX ?? 0) >= (bounds.maxX ?? 0) || (bounds.minY ?? 0) >= (bounds.maxY ?? 0)) errors.push(`${itemLocation}.worldBounds: must define finite increasing bounds`);
if (!Number.isInteger(mapAsset.image?.width) || (mapAsset.image?.width ?? 0) < 1 || !Number.isInteger(mapAsset.image?.height) || (mapAsset.image?.height ?? 0) < 1) errors.push(`${itemLocation}.image: width and height must be positive integers`);
requireSCUMMapAssetJSONCompatibility(mapAsset, itemLocation);
}
requireUniqueAssetKeys(liveData.giftCatalogs, "giftCatalogs");
for (const [index, catalog] of (liveData.giftCatalogs ?? []).entries()) {
+82 -1
View File
@@ -120,6 +120,23 @@ type SCUMSQLiteQueryAsset = {
};
};
type SCUMMapBounds = { minX: number; minY: number; maxX: number; maxY: number };
type SCUMMapImage = { width: number; height: number };
type SCUMMapTransformFixture = { name: string; world: { x: number; y: number }; pixel: { x: number; y: number } };
type SCUMMapTransformAsset = {
key?: string;
transformVersion?: string;
mapAssetKey?: string;
adapterVersion?: string;
requiredSchemaFingerprint?: string;
worldBounds: SCUMMapBounds;
image: SCUMMapImage;
validation?: { rejectNonFinite?: boolean; rejectOutOfBounds?: boolean; acceptBoundaryPoints?: boolean };
fixtures?: SCUMMapTransformFixture[];
};
type SCUMMapProjection = { ok: true; pixel: { x: number; y: number } } | { ok: false; reason: "non-finite" | "out-of-bounds" };
type JSONSchemaObject = {
type?: unknown;
additionalProperties?: boolean;
@@ -146,6 +163,14 @@ function sha256FixtureDigest(fixtureDir: string, relativePath: string): string {
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(path.join(fixtureDir, relativePath))).digest("hex")}`;
}
function projectSCUMMapCoordinate(transform: SCUMMapTransformAsset, world: { x: number; y: number }): SCUMMapProjection {
const { minX, minY, maxX, maxY } = transform.worldBounds;
const { width, height } = transform.image;
if (![world.x, world.y, minX, minY, maxX, maxY, width, height].every(Number.isFinite)) return { ok: false, reason: "non-finite" };
if (world.x < minX || world.x > maxX || world.y < minY || world.y > maxY) return { ok: false, reason: "out-of-bounds" };
return { ok: true, pixel: { x: ((world.x - minX) / (maxX - minX)) * (width - 1), y: ((maxY - world.y) / (maxY - minY)) * (height - 1) } };
}
function listProductionGoFiles(directory: string): string[] {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const target = path.join(directory, entry.name);
@@ -384,6 +409,58 @@ describe("plugin manifest validation", () => {
for (const statement of allowedReadBoundaries) expect(statement).not.toMatch(unsafeSCUMSQLiteReadStatementPattern);
});
it("packages SCUM map metadata and tested coordinate transform assets", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration };
const assetPaths = new Set(manifest.assetFiles?.map((file) => file.path) ?? []);
const mapAssets = manifest.scumLiveData.mapAssets ?? [];
expect(mapAssets.map((asset) => asset.key)).toEqual(["scum-current-service-coordinate-map"]);
const mapAsset = mapAssets[0];
expect(mapAsset).toMatchObject({ adapterVersion: "scum-live-data-v1", requiredSchemaFingerprint: currentSCUMSchemaFingerprint, metadataSchemaRef: "schemas/scum-live/map-metadata.schema.json", image: { width: 4096, height: 4096 } });
expect([...assetPaths]).toEqual(expect.arrayContaining([mapAsset.assetPath, mapAsset.transformAssetPath]));
const mapAssetPath = path.join(pluginDir, mapAsset.assetPath);
const transformAssetPath = path.join(pluginDir, mapAsset.transformAssetPath);
expect(`sha256:${crypto.createHash("sha256").update(fs.readFileSync(mapAssetPath)).digest("hex")}`).toBe(mapAsset.digest);
expect(`sha256:${crypto.createHash("sha256").update(fs.readFileSync(transformAssetPath)).digest("hex")}`).toBe(mapAsset.transformDigest);
const metadata = JSON.parse(fs.readFileSync(mapAssetPath, "utf8")) as Record<string, any>;
const metadataSchema = JSON.parse(fs.readFileSync(path.join(pluginDir, mapAsset.metadataSchemaRef), "utf8"));
const validateMetadata = new Ajv2020({ allErrors: true }).compile(metadataSchema);
if (!validateMetadata(metadata)) throw new Error(`SCUM map metadata schema failed: ${JSON.stringify(validateMetadata.errors)}`);
expect(metadata).toMatchObject({ key: mapAsset.key, adapterVersion: mapAsset.adapterVersion, requiredSchemaFingerprint: currentSCUMSchemaFingerprint, transformAssetPath: mapAsset.transformAssetPath, worldBounds: mapAsset.worldBounds, image: mapAsset.image });
expect(metadata.authorization).toMatchObject({ redistribution: "first-party-generated-coordinate-metadata", baseMapArtwork: "not-packaged", renderingAvailability: "unavailable-until-authorized-base-map" });
expect(metadata.layers.map((layer: { key: string; subjectType: string }) => `${layer.key}:${layer.subjectType}`)).toEqual(["players:player", "vehicles:vehicle", "flags:flag"]);
const transform = JSON.parse(fs.readFileSync(transformAssetPath, "utf8")) as SCUMMapTransformAsset;
expect(transform).toMatchObject({ mapAssetKey: mapAsset.key, adapterVersion: mapAsset.adapterVersion, requiredSchemaFingerprint: currentSCUMSchemaFingerprint, worldBounds: mapAsset.worldBounds, image: mapAsset.image, validation: { rejectNonFinite: true, rejectOutOfBounds: true, acceptBoundaryPoints: true } });
expect(transform.fixtures?.map((fixture) => fixture.name)).toEqual(["observed-minimum-corner", "observed-maximum-corner", "observed-center"]);
for (const fixture of transform.fixtures ?? []) {
const projection = projectSCUMMapCoordinate(transform, fixture.world);
expect(projection.ok).toBe(true);
if (projection.ok) {
expect(projection.pixel.x).toBeCloseTo(fixture.pixel.x, 6);
expect(projection.pixel.y).toBeCloseTo(fixture.pixel.y, 6);
}
}
for (const world of [{ x: Number.NaN, y: 0 }, { x: Number.POSITIVE_INFINITY, y: 0 }, { x: 0, y: Number.NEGATIVE_INFINITY }]) expect(projectSCUMMapCoordinate(transform, world)).toEqual({ ok: false, reason: "non-finite" });
for (const world of [{ x: mapAsset.worldBounds.minX - 1, y: mapAsset.worldBounds.minY }, { x: mapAsset.worldBounds.maxX + 1, y: mapAsset.worldBounds.maxY }, { x: mapAsset.worldBounds.minX, y: mapAsset.worldBounds.minY - 1 }, { x: mapAsset.worldBounds.maxX, y: mapAsset.worldBounds.maxY + 1 }]) expect(projectSCUMMapCoordinate(transform, world)).toEqual({ ok: false, reason: "out-of-bounds" });
});
it("rejects SCUM map transform adapter and schema incompatibility", () => {
const errors = validateTemporaryScumCompanionManifest((manifest, fixtureDir) => {
const mapAsset = manifest.scumLiveData.mapAssets[0];
const transformPath = mapAsset.transformAssetPath;
const transform = JSON.parse(fs.readFileSync(path.join(fixtureDir, transformPath), "utf8"));
transform.adapterVersion = "scum-live-data-v2";
transform.requiredSchemaFingerprint = `sha256:${"0".repeat(64)}`;
writeFixtureJSON(fixtureDir, transformPath, transform);
mapAsset.transformDigest = sha256FixtureDigest(fixtureDir, transformPath);
});
expect(errors.some((error) => error.includes("transform adapterVersion must match the map asset declaration"))).toBe(true);
expect(errors.some((error) => error.includes("transform schema fingerprint must match the map asset declaration"))).toBe(true);
});
it("requires SCUM schema probe targets to have generated Run workspace data targets", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
manifest.runtimeProfiles.dataTargets = [];
@@ -413,7 +490,11 @@ 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) writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
for (const assetPath of [assetPaths[0], assetPaths[1], assetPaths[2], assetPaths[3], assetPaths[6]]) writeFixtureJSON(fixtureDir, assetPath, { packaged: true });
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);
manifest.scumLiveData = {
...manifest.scumLiveData,