From 8a94d83e7e019a4f5e5bb6fc059207cf526d2e4e Mon Sep 17 00:00:00 2001 From: MangoPig Date: Mon, 22 Jun 2026 11:13:06 +0100 Subject: [PATCH] Fix: align POSIX hierarchy contract --- Backend/internal/bootstrap/service.go | 9 +++ Backend/internal/bootstrap/service_test.go | 7 ++ Backend/internal/posixproj/projector.go | 70 ++++++++++++------ Backend/internal/posixproj/projector_test.go | 78 ++++++++++++++++++++ Documentation/POSIX-Structure.md | 14 ++++ 5 files changed, 154 insertions(+), 24 deletions(-) diff --git a/Backend/internal/bootstrap/service.go b/Backend/internal/bootstrap/service.go index f56bda1..346b388 100644 --- a/Backend/internal/bootstrap/service.go +++ b/Backend/internal/bootstrap/service.go @@ -978,6 +978,7 @@ func (service *Service) ensureBootstrapPOSIXSkeleton( departmentPath, teamPath, projectPath, + filepath.Join(projectPath, "children"), filepath.Join(projectPath, "tree"), filepath.Join(usersPath, "personals"), } { @@ -1041,6 +1042,14 @@ func (service *Service) ensureBootstrapPOSIXSkeleton( return fmt.Errorf("write project home.json: %w", err) } + if err := writeJSONFile(filepath.Join(projectPath, "acl.json"), map[string]any{ + "version": 1, + "inherits": true, + "rules": []any{}, + }); err != nil { + return fmt.Errorf("write project acl.json: %w", err) + } + if err := writeJSONFile(filepath.Join(usersPath, "settings.json"), map[string]any{ "primaryAdminId": admin.ID, }); err != nil { diff --git a/Backend/internal/bootstrap/service_test.go b/Backend/internal/bootstrap/service_test.go index 1eca971..3d880e1 100644 --- a/Backend/internal/bootstrap/service_test.go +++ b/Backend/internal/bootstrap/service_test.go @@ -52,6 +52,8 @@ func TestEnsureBootstrapPOSIXSkeletonInitializesEmptyRoot(t *testing.T) { filepath.Join(rootPath, "departments", "department-primary-department", "teams", "team-primary-team", "users.json"), filepath.Join(rootPath, "projects", "project-primary-project", "settings.json"), filepath.Join(rootPath, "projects", "project-primary-project", "home.json"), + filepath.Join(rootPath, "projects", "project-primary-project", "acl.json"), + filepath.Join(rootPath, "projects", "project-primary-project", "children"), filepath.Join(rootPath, "projects", "project-primary-project", "tree"), filepath.Join(rootPath, "users", "settings.json"), filepath.Join(rootPath, "users", "data.json"), @@ -90,6 +92,11 @@ func TestEnsureBootstrapPOSIXSkeletonInitializesEmptyRoot(t *testing.T) { t.Fatalf("expected project settings type project, got %#v", projectSettings["type"]) } + projectACL := readJSONFileForTest[map[string]any](t, filepath.Join(rootPath, "projects", "project-primary-project", "acl.json")) + if projectACL["inherits"] != true { + t.Fatalf("expected project acl to inherit by default, got %#v", projectACL["inherits"]) + } + usersSettings := readJSONFileForTest[map[string]any](t, filepath.Join(rootPath, "users", "settings.json")) if usersSettings["primaryAdminId"] != "admin-1" { t.Fatalf("expected primary admin id admin-1, got %#v", usersSettings["primaryAdminId"]) diff --git a/Backend/internal/posixproj/projector.go b/Backend/internal/posixproj/projector.go index 76b955a..6a9e0b9 100644 --- a/Backend/internal/posixproj/projector.go +++ b/Backend/internal/posixproj/projector.go @@ -321,18 +321,17 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No func deriveScope(relPath string, rootScope Scope) Scope { scope := rootScope parts := strings.Split(relPath, "/") - if len(parts) >= 2 && parts[0] == "departments" && strings.HasPrefix(parts[1], "department-") { - scope.DepartmentSlug = strings.TrimPrefix(parts[1], "department-") - } - if len(parts) >= 4 && parts[0] == "departments" && parts[2] == "teams" && strings.HasPrefix(parts[3], "team-") { - scope.DepartmentSlug = strings.TrimPrefix(parts[1], "department-") - scope.TeamSlug = strings.TrimPrefix(parts[3], "team-") - } - if len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-") { - scope.ProjectSlug = strings.TrimPrefix(parts[1], "project-") - } - if len(parts) >= 3 && parts[0] == "users" && parts[1] == "personals" && strings.HasPrefix(parts[2], "personal-") { - scope.PersonalSlug = strings.TrimPrefix(parts[2], "personal-") + for _, part := range parts { + switch { + case strings.HasPrefix(part, "department-"): + scope.DepartmentSlug = strings.TrimPrefix(part, "department-") + case strings.HasPrefix(part, "team-"): + scope.TeamSlug = strings.TrimPrefix(part, "team-") + case strings.HasPrefix(part, "project-"): + scope.ProjectSlug = strings.TrimPrefix(part, "project-") + case strings.HasPrefix(part, "personal-"): + scope.PersonalSlug = strings.TrimPrefix(part, "personal-") + } } return scope } @@ -344,6 +343,13 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) { fileRole = strings.TrimSuffix(name, filepath.Ext(name)) } + hasChildrenAncestor := pathContainsSegment(parts, "children") + hasTreeAncestor := pathContainsSegment(parts, "tree") + parentName := "" + if len(parts) >= 2 { + parentName = parts[len(parts)-2] + } + switch { case relPath == "settings.json": return "tenant", "settings" @@ -392,26 +398,33 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) { return "department", fileRole case len(parts) >= 2 && parts[0] == "projects" && strings.HasPrefix(parts[1], "project-"): if isDir { - if len(parts) == 2 { + if strings.HasPrefix(name, "project-") { return "project", "" } - if len(parts) == 3 && parts[2] == "tree" { + if name == "children" { + return "project_children", "" + } + if name == "tree" { return "project_tree", "" } - if strings.Contains(relPath, "/tree/") || strings.HasSuffix(relPath, "/tree") { - if strings.HasPrefix(name, "folder-") { - return "folder", "" - } - if strings.HasPrefix(name, "item-") { - return "item", "" - } + if hasChildrenAncestor && strings.HasPrefix(name, "folder-") { + return "hierarchy_folder", "" + } + if hasTreeAncestor && strings.HasPrefix(name, "folder-") { + return "folder", "" + } + if hasTreeAncestor && strings.HasPrefix(name, "item-") { + return "item", "" } } - if strings.Contains(relPath, "/tree/") { - if strings.HasPrefix(parts[len(parts)-2], "item-") { + if hasChildrenAncestor && strings.HasPrefix(parentName, "folder-") { + return "hierarchy_folder", fileRole + } + if hasTreeAncestor { + if strings.HasPrefix(parentName, "item-") { return "item", fileRole } - if strings.HasPrefix(parts[len(parts)-2], "folder-") { + if strings.HasPrefix(parentName, "folder-") { return "folder", fileRole } } @@ -456,6 +469,15 @@ func classifyPath(relPath string, isDir bool) (logicalType, fileRole string) { } } +func pathContainsSegment(parts []string, target string) bool { + for _, part := range parts { + if part == target { + return true + } + } + return false +} + func projectionParentPath(relPath string) *string { if relPath == "" || relPath == rootProjectionPath { return nil diff --git a/Backend/internal/posixproj/projector_test.go b/Backend/internal/posixproj/projector_test.go index 6fc97a0..a2ee812 100644 --- a/Backend/internal/posixproj/projector_test.go +++ b/Backend/internal/posixproj/projector_test.go @@ -13,7 +13,10 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) { mustMkdirAll(t, filepath.Join(root, "catalog", "packs")) mustMkdirAll(t, filepath.Join(root, "catalog", "standalone")) mustMkdirAll(t, filepath.Join(root, "departments", "department-primary-department", "teams", "team-primary-team")) + mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "children")) + mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "tree")) mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree")) + mustMkdirAll(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap")) mustMkdirAll(t, filepath.Join(root, "users", "personals")) mustWriteJSON(t, filepath.Join(root, "settings.json"), map[string]any{ @@ -57,6 +60,48 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) { "type": "project-home", "project": "primary-project", }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "acl.json"), map[string]any{ + "inherits": true, + "rules": []any{}, + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "folder.json"), map[string]any{ + "name": "Design", + "slug": "design", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "acl.json"), map[string]any{ + "inherits": true, + "rules": []any{}, + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "settings.json"), map[string]any{ + "id": "project-2", + "name": "Web Project", + "slug": "web", + "type": "project", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "home.json"), map[string]any{ + "type": "project-home", + "project": "web", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "children", "folder-design", "children", "project-web", "acl.json"), map[string]any{ + "inherits": true, + "rules": []any{}, + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "folder.json"), map[string]any{ + "name": "Docs", + "slug": "docs", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "item.json"), map[string]any{ + "id": "item-1", + "name": "Roadmap", + "slug": "roadmap", + "type": "board", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "schema.json"), map[string]any{ + "type": "object", + }) + mustWriteJSON(t, filepath.Join(root, "projects", "project-primary-project", "tree", "folder-docs", "item-roadmap", "data.json"), map[string]any{ + "title": "Roadmap", + }) mustWriteJSON(t, filepath.Join(root, "users", "settings.json"), map[string]any{ "primaryAdminId": "admin-1", }) @@ -119,6 +164,39 @@ func TestScanRootBuildsProjectedNodesFromBootstrapShape(t *testing.T) { t.Fatalf("unexpected project tree node: %#v", projectTree) } + projectChildren := index["projects/project-primary-project/children"] + if projectChildren.LogicalType != "project_children" || projectChildren.NodeKind != NodeKindDirectory { + t.Fatalf("unexpected project children node: %#v", projectChildren) + } + + hierarchyFolder := index["projects/project-primary-project/children/folder-design"] + if hierarchyFolder.LogicalType != "hierarchy_folder" || hierarchyFolder.ProjectSlug != "primary-project" { + t.Fatalf("unexpected hierarchy folder node: %#v", hierarchyFolder) + } + + hierarchyFolderACL := index["projects/project-primary-project/children/folder-design/acl.json"] + if hierarchyFolderACL.LogicalType != "hierarchy_folder" || hierarchyFolderACL.FileRole != "acl" { + t.Fatalf("unexpected hierarchy folder acl classification: %#v", hierarchyFolderACL) + } + + childProjectSettings := index["projects/project-primary-project/children/folder-design/children/project-web/settings.json"] + if childProjectSettings.LogicalType != "project" || childProjectSettings.ProjectSlug != "web" { + t.Fatalf("unexpected child project classification: %#v", childProjectSettings) + } + + treeFolder := index["projects/project-primary-project/tree/folder-docs"] + if treeFolder.LogicalType != "folder" || treeFolder.ProjectSlug != "primary-project" { + t.Fatalf("unexpected tree folder node: %#v", treeFolder) + } + + treeItem := index["projects/project-primary-project/tree/folder-docs/item-roadmap/item.json"] + if treeItem.LogicalType != "item" || treeItem.FileRole != "item" { + t.Fatalf("unexpected tree item classification: %#v", treeItem) + } + if treeItem.ResourceSlug != "roadmap" { + t.Fatalf("expected tree item resource slug roadmap, got %#v", treeItem.ResourceSlug) + } + usersData := index["users/data.json"] if usersData.LogicalType != "users" || usersData.FileRole != "data" { t.Fatalf("unexpected users data classification: %#v", usersData) diff --git a/Documentation/POSIX-Structure.md b/Documentation/POSIX-Structure.md index 5c72cba..eeb6241 100644 --- a/Documentation/POSIX-Structure.md +++ b/Documentation/POSIX-Structure.md @@ -1,5 +1,7 @@ # POSIX Structure +[Filetree Link](https://tree.nathanfriend.com/?s=(%27optiUs!(%27fancy!Yue~fullPath!fbq~YailingSlash!Yue~rootDot!fbq)~R(%27R%27PJ%20or%20OrganizatiU%20%7Bqrver%7DM*46layout6cNlog7packs7*pack37A2*enYies75W5A_standbUe7WA6HwH30LZ2teamw5teamGL5Z6FwF30LKTC058T5C7XFG5LXKXTXC7XI7I05QG5BN2*8QG5BN6Z04_dN_pJw*pJGlayout2L*KI70%27)~vEiU!%271%27)*%20%200M52_*3-%3Cslug%3E4qttings5**69M*7%2F08VG*V259.jsUA5manifestBQ25*schema25*dCchildrenEersFprojectG305HdepartmentI*YeeJEUbKhome2L*42M%5CnNataQitemRsource!Tacl2UonVfolderW*app37X55YtrZusE_90balqsews0%01wqb_ZYXWVUTRQNMLKJIHGFECBA987654320*) + ``` markdown Personal or Organization (server)/ ├── settings.json @@ -26,6 +28,18 @@ Personal or Organization (server)/ │ └── project-/ │ ├── settings.json │ ├── home.json +│ ├── acl.json +│ ├── children/ +│ │ └── folder-/ +│ │ ├── folder.json +│ │ ├── acl.json +│ │ └── children/ +│ │ └── project-/ +│ │ ├── settings.json +│ │ ├── home.json +│ │ ├── acl.json +│ │ ├── children/ +│ │ └── tree/ │ └── tree/ │ ├── item-/ │ │ ├── item.json