{"record":{"id":"5ce7aa72fb8f26fb","repo":"666ghj/MiroFish","slug":"workspace-is-not-a-directory","errorCode":null,"errorMessage":"workspace is not a directory","messagePattern":"workspace is not a directory","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":460,"sourceCode":"            raise StarHistoryError(\"first snapshot cannot predate reconstruction\")\n        if previous_day is not None and previous_day >= first_snapshot.date():\n            raise StarHistoryError(\"reconstruction dates must predate snapshots\")\n\n\ndef canonical_state_bytes(state: Mapping[str, Any]) -> bytes:\n    validate_state(state)\n    return (\n        json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + \"\\n\"\n    ).encode(\"utf-8\")\n\n\ndef _safe_workspace(workspace: Path) -> Path:\n    try:\n        root = workspace.resolve(strict=True)\n    except OSError as exc:\n        raise StarHistoryError(\"workspace does not exist\") from exc\n    if not root.is_dir():\n        raise StarHistoryError(\"workspace is not a directory\")\n    return root\n\n\ndef _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:\n    root = _safe_workspace(workspace)\n    if relative.is_absolute() or \"..\" in relative.parts:\n        raise StarHistoryError(\"output path escaped the workspace\")\n\n    current = root\n    for part in relative.parts[:-1]:\n        current = current / part\n        if current.is_symlink():\n            raise StarHistoryError(\"output directory cannot be a symbolic link\")\n    target = root / relative\n    if target.is_symlink():\n        raise StarHistoryError(\"output file cannot be a symbolic link\")\n    if create_parent:\n        try:","sourceCodeStart":442,"sourceCodeEnd":478,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L442-L478","documentation":"Raised by _safe_workspace() when the workspace path resolves successfully but is not a directory (root.is_dir() is false) — e.g. it is a regular file, fifo, or device. The workspace is expected to be the containing directory under which output paths are confined.","triggerScenarios":"Calling an output-writing code path with a workspace Path that names an existing file (e.g. pointing the workspace at the state JSON itself instead of its directory).","commonSituations":"Configuration confusion between the output file path and its parent directory; a file created at the location where the workspace directory was expected; swapped CLI arguments for workspace vs. file.","solutions":["Point the workspace at the directory that should contain outputs, not at a file","Remove the stray file occupying the workspace path if the directory was intended","Re-check the CLI/config argument order and semantics"],"exampleFix":"# before\nworkspace = Path(\"out/star-history.json\")  # this is a file\n# after\nworkspace = Path(\"out\")  # directory; pass the file name as the relative target","handlingStrategy":"validation","validationCode":"workspace = Path(config[\"workspace\"])\nif not workspace.is_dir():\n    raise NotADirectoryError(f\"workspace is not a directory: {workspace}\")","typeGuard":"from pathlib import Path\n\ndef is_workspace_dir(workspace: Path) -> bool:\n    return workspace.is_dir()","tryCatchPattern":"try:\n    write_output(workspace, relative, data)\nexcept StarHistoryError as exc:\n    if \"workspace is not a directory\" in str(exc):\n        # fix configuration: point workspace at the parent directory\n        ...","preventionTips":["Keep workspace (directory) and output target (relative filename) as separate config values","Document the distinction in the tool's config schema","Add a startup assertion that the workspace is a directory"],"tags":["filesystem","configuration"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}