class_name AccessVerifier extends RefCounted const Rules := preload("res://src/core/rules.gd") const AccessPolicy := preload("res://src/core/access_policy.gd") const AccessArtifact := preload("res://src/core/access_artifact.gd") ## Evaluates access artifacts against a venue AccessPolicy. ## Pure logic: no UI, no Node dependency, fully headless-testable. ## Result dictionary contract: ## { granted: bool, reasons: Array[String], issues: Array[String], ## decisive_artifacts: Array[String], checked_artifacts: Array[String] } func _init() -> void: pass ## Verify a single person's set of artifacts (person_artifacts: Array[AccessArtifact]). func verify(person_artifacts: Array, policy: AccessPolicy, already_screened: bool = false) -> Dictionary: var reasons: Array[String] = [] var issues: Array[String] = [] var checked: Array[String] = [] for artifact in person_artifacts: if not (artifact is AccessArtifact): continue checked.append(artifact.summary()) if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID: reasons.append("Recognized by guard") elif artifact.status == Rules.ArtifactStatus.VALID: reasons.append("Valid %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) elif artifact.status == Rules.ArtifactStatus.EXPIRED: issues.append("Expired %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) elif artifact.status == Rules.ArtifactStatus.COUNTERFEIT: issues.append("Suspected counterfeit %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) elif artifact.status == Rules.ArtifactStatus.MISMATCHED: issues.append("%s does not match person" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) elif artifact.status == Rules.ArtifactStatus.AMBIGUOUS: issues.append("Ambiguous %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) elif artifact.status == Rules.ArtifactStatus.MISSING: issues.append("Missing %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact")) # Age check. if policy.minimum_age > 0 and require_age_info(person_artifacts, policy): var age := person_age(person_artifacts) if age < policy.minimum_age: issues.append("Underage (%d < %d)" % [age, policy.minimum_age]) return _result(false, reasons, issues, checked) # Wand screening is processed by the interaction layer (false alarms), but a # policy can hard-require it; a refused screen blocks entry. if policy.wand_screening_required and not already_screened and not person_screened(person_artifacts): issues.append("Screening required") return _result(false, reasons, issues, checked) # Recognition short-circuits if present and valid. var recognized := false for artifact in person_artifacts: if artifact is AccessArtifact: if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID: recognized = true if recognized: return _result(true, reasons, issues, checked) # Every required artifact kind must have a valid match (AND admission), # unless the policy grants on any single valid artifact (OR admission). var any_satisfied := false for kind in policy.required_artifacts.keys(): var satisfied := false for artifact in person_artifacts: if artifact is AccessArtifact and artifact.kind == kind: if artifact.status == Rules.ArtifactStatus.VALID: satisfied = true any_satisfied = any_satisfied or satisfied if not satisfied: issues.append("Missing valid %s" % Rules.ACCESS_KIND_NAMES.get(kind, "credential")) if not policy.grant_if_any_required: return _result(false, reasons, issues, checked) if policy.grant_if_any_required and not any_satisfied: return _result(false, reasons, issues, checked) if policy.require_id: var id_ok := false for artifact in person_artifacts: if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID and artifact.status == Rules.ArtifactStatus.VALID: id_ok = true if not id_ok: issues.append("Valid ID required") return _result(false, reasons, issues, checked) return _result(true, reasons, issues, checked) func _result(granted: bool, reasons: Array, issues: Array, checked: Array) -> Dictionary: return { "granted": granted, "reasons": reasons, "issues": issues, "decisive_artifacts": reasons.duplicate(), "checked_artifacts": checked, } func require_age_info(person_artifacts: Array, policy: AccessPolicy) -> bool: if policy.minimum_age <= 0: return false var id_present := false for artifact in person_artifacts: if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID: id_present = true return id_present func person_age(person_artifacts: Array) -> int: for artifact in person_artifacts: if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID: var age: Variant = artifact.attributes.get("age", -1) if age is int and age > 0: return age return -1 func person_screened(person_artifacts: Array) -> bool: for artifact in person_artifacts: if artifact is AccessArtifact: if artifact.kind == Rules.AccessKind.CREDENTIAL and artifact.attributes.get("screened", false) == true: return true return false