using System; using System.IO; namespace IslaApocalypse.Core { /// /// ⚠⚠ THE PERMANENT FILE-SAFETY RULES, ENFORCED IN CODE. /// → `Design - Tooling - Iteration and Batching.md` § FILE-SAFETY rules — permanent. /// /// ═══ THE RULES ═══ /// /// 1. NO DELETION in the runtime root or anywhere under batches/. /// 2. INTERMEDIATES PERSIST in a scratch/ subfolder that is NEVER cleaned. /// 3. THE DEVELOPER'S PLACED FILES ARE NEVER TOUCHED. /// 4. ANY DELETION IS NAMED EXPLICITLY IN THE RUN REPORT. /// /// ═══ WHY THIS IS CODE AND NOT A README LINE ═══ /// /// These were born from a real incident: an executor admitted it had been deleting the /// developer's staged test blueprint. It was owned and fixed IN CODE. A rule that depends on an /// executor remembering it will eventually meet an executor who does not — so the rule is a /// throw, not a sentence. /// /// ═══ HOW TO USE IT ═══ /// /// There is no Delete() convenience here on purpose. Any code that removes a file calls /// first, with a reason, and the reason goes in the run report. /// Deleting without asking is the thing being prevented; making it one line easier to ask is /// the whole mechanism. /// /// Nothing deletes anything this phase. /// public static class FileSafety { /// /// Refuse the deletion unless it is provably outside every protected root, and record why. /// Throws when the path is protected. /// /// The file or directory a caller intends to remove. /// /// Why. Required, non-blank — it is what the run report has to print. "cleanup" is not a /// reason; "regenerable legacy blueprint, 22 GB, nothing reads it" is. /// public static void AssertDeletable(string path, string reason) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("A path is required.", nameof(path)); if (string.IsNullOrWhiteSpace(reason)) throw new ArgumentException( "A deletion needs a stated reason — it must be named explicitly in the run report. " + "If you cannot name one, that is the answer.", nameof(reason)); string full = Path.GetFullPath(path); foreach (string protectedRoot in ProtectedRoots()) { if (IsWithin(full, protectedRoot)) throw new UnauthorizedAccessException( $"REFUSED: '{full}' is inside a protected root ('{protectedRoot}'). " + "No deletion is permitted in the runtime root or under batches/ — intermediates persist. " + $"(Stated reason was: {reason})"); } } /// /// The roots nothing may delete from. The batches root and the resolved output/user roots — /// i.e. everything a generation run and a human's browsing of it depend on. /// public static string[] ProtectedRoots() => new[] { Path.GetFullPath(ToolingPaths.BatchesRoot), Path.GetFullPath(ToolingPaths.OutputDir), Path.GetFullPath(ToolingPaths.BlueprintPath), Path.GetFullPath(ToolingPaths.UserDataDir), }; private static bool IsWithin(string candidate, string root) { string c = candidate.TrimEnd(Path.DirectorySeparatorChar); string r = root.TrimEnd(Path.DirectorySeparatorChar); return string.Equals(c, r, StringComparison.Ordinal) || c.StartsWith(r + Path.DirectorySeparatorChar, StringComparison.Ordinal); } } }