package service import ( "errors" "strings" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) const scumSchemaProbeExecutionKind = "sqlite.schema-probe" func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) { idempotencyKey = strings.TrimSpace(idempotencyKey) if idempotencyKey == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required") } instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } probe := plugin.SCUMLiveData.Probe if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin") } if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target") } endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe") } if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor") } binding, err := svc.runtimeBindingForServer(instance.ID) if errors.Is(err, repo.ErrNotFound) { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe") } if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData) if adapterVersion == "" { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared") } bounds := probe.Bounds if bounds.MaxObjects == 0 { bounds = domain.DefaultSCUMSchemaProbeBounds() } jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey) request := domain.SCUMSchemaProbeRequest{ RequestID: jobID, JobID: jobID, Binding: domain.SCUMBindingIdentity{ ServerInstanceID: instance.ID, RunBindingID: binding.ID, RunEndpointID: instance.RunEndpointID, PluginID: plugin.ID, PluginVersion: plugin.Version, AdapterVersion: adapterVersion, DatabaseIdentity: scumSchemaProbeDatabaseIdentity(probe.TargetKey), }, Bounds: bounds, RequestedAt: svc.now(), } if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ ServerInstanceID: instance.ID, DeclarationKey: probe.TargetKey, TargetKey: probe.TargetKey, Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds), MaxAttempts: 1, IdempotencyKey: idempotencyKey, PlatformScheduled: true, SQLiteSchemaProbe: &request, }) if err != nil { return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err } return request, result, nil } func scumSchemaProbeDatabaseIdentity(targetKey string) string { trimmed := strings.TrimSpace(targetKey) return strings.TrimPrefix(trimmed, "databases/") } func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool { expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey) for _, target := range profiles.DataTargets { if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" { return true } } return false } func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string { for _, gate := range manifest.CapabilityGates { if gate.Capability == domain.SCUMDataCapabilitySchemaProbe { return strings.TrimSpace(gate.AdapterVersion) } } for _, gate := range manifest.CapabilityGates { if strings.TrimSpace(gate.AdapterVersion) != "" { return strings.TrimSpace(gate.AdapterVersion) } } return "" } func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int { if bounds.TimeoutMS <= 0 { return 1 } seconds := (bounds.TimeoutMS + 999) / 1000 if seconds <= 0 { return 1 } return seconds } func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error { if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil { return err } expected := job.ExecutionInput.SQLiteSchemaProbe if expected == nil { return validationError("SQLite schema probe request is missing from leased job") } if result.JobID != job.ID || result.JobID != expected.JobID || result.RequestID != expected.RequestID { return validationError("SQLite schema probe result does not match leased job identity") } if !sameSCUMSchemaProbeBinding(result.Binding, expected.Binding) { return validationError("SQLite schema probe result does not match leased binding identity") } return nil } func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool { return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity }