{"record":{"id":"1cc2a9e2a765d27d","repo":"github/spec-kit","slug":"invalid-events-expected-a-mapping","errorCode":null,"errorMessage":"Invalid events: expected a mapping","messagePattern":"Invalid events: expected a mapping","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":1781,"sourceCode":"            install_integration_events(integration, project_root, manifest, events_map)\n            manifest.save()\n        except Exception as exc:\n            logger.warning(\"Failed to refresh events for '%s': %s\", key, exc)\n            failures.append((key, str(exc)))\n\n    if failures:\n        raise EventRefreshError(failures)\n\n\n# -- Manifest validation ---------------------------------------------------\n\ndef validate_events(data: dict[str, Any]) -> None:\n    \"\"\"Validate ``events`` field in extension manifest data.\"\"\"\n    from .extensions import ValidationError\n\n    events = data.get(\"events\")\n    if \"events\" in data and not isinstance(events, dict):\n        raise ValidationError(\"Invalid events: expected a mapping\")\n    if events:\n        for event_name, event_config in events.items():\n            if not isinstance(event_config, dict):\n                raise ValidationError(\n                    f\"Invalid event '{event_name}': expected a mapping\"\n                )\n            command = event_config.get(\"command\")\n            # #17: command must be a non-empty string. A truthy non-string\n            # (e.g. command: [foo]) would pass a bare truthiness check and\n            # later render into invalid native configuration.\n            if not isinstance(command, str) or not command.strip():\n                raise ValidationError(\n                    f\"Event '{event_name}' missing required 'command' string\"\n                )\n            if event_name not in CANONICAL_EVENTS:\n                raise ValidationError(\n                    f\"Unknown event '{event_name}': \"\n                    f\"must be one of {sorted(CANONICAL_EVENTS)}\"","sourceCodeStart":1763,"sourceCodeEnd":1799,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L1763-L1799","documentation":"Thrown by validate_events() while validating the `events` field of an extension manifest: `events` is present but is not a YAML mapping. The events system requires event-name -> config mappings; a list, string, or scalar at that position cannot be interpreted.","triggerScenarios":"An extension manifest contains `events:` followed by a sequence or scalar, e.g. `events: [pre_tool_use]` or `events: pre_tool_use`, and the manifest is loaded/validated during install, list, or event refresh.","commonSituations":"Authoring `events:` as a list of event names; a YAML indentation slip that attaches the events value to the wrong key; converting from another config format that models events as an array.","solutions":["Rewrite `events` as a mapping keyed by event name with a per-event config mapping.","Validate the manifest locally with yaml.safe_load before installing to confirm events is a dict.","Re-install the extension."],"exampleFix":"# before\nevents:\n  - pre_tool_use\n  - post_tool_use\n\n# after\nevents:\n  pre_tool_use:\n    command: \"./hook.sh\"\n  post_tool_use:\n    command: \"./after.sh\"","handlingStrategy":"type-guard","validationCode":"data = yaml.safe_load(open(manifest_path))\nevents = data.get(\"events\") if isinstance(data, dict) else None\nif events is not None and not isinstance(events, dict):\n    raise SystemExit(\"events must be a mapping of event-name -> config\")","typeGuard":"def is_events_mapping(events) -> bool:\n    return events is None or isinstance(events, dict)","tryCatchPattern":"except ValidationError as e:\n    if \"expected a mapping\" in str(e) and \"events\" in str(e):\n        rewrite_events_as_mapping()","preventionTips":["Model events as a dict keyed by event name, never a list.","Round-trip manifests through yaml.safe_load in CI to catch shape errors."],"tags":["events","validation","yaml","manifest"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}