GitHub Issue: Implement VFileTree in Morphir.IR.Pipeline
Type: Feature
Labels: feature, file-architecture, priority-p0, pipeline
Priority: P0 (Blocks F# Backend and F# Frontend)
Milestone: v1.0.0
Estimated Effort: 1 week
Project: Morphir.IR.Pipeline (existing project - add new file)
Description
Implement VFileTree - a hierarchical file structure abstraction for multi-file projects in the Morphir.IR.Pipeline project. This will be the universal, primary abstraction for file operations across all morphir-dotnet components (F# Backend, F# Frontend, future backends/frontends).
Naming: VFile and VFileTree (inspired by unifiedJS VFile). More concise than VFile/VFileTree.
Key Decision: VFileTree is the PRIMARY abstraction (not a conversion from flat maps). This departs from morphir-elm's flat FileMap approach in favor of better scalability, UX, and structure preservation.
Related Design: Unified File Architecture
Context
What Exists Today
✅ VFile (already in Morphir.IR.Pipeline/File.fs):
- Virtual file with content, path, diagnostics, metadata
- Perfect for single-file operations
- Already used by Pipeline
What's Missing
❌ Multi-file support:
- No hierarchical structure (directories + files)
- No cross-file relationships
- No project-level metadata
Acceptance Criteria
Core Types (Add to Morphir.IR.Pipeline/FileTree.fs)
Type Definitions:
Required Fields:
Module Functions
Creation Functions (VFileTree module):
Query Functions:
Transformation Functions:
Conversion Functions (morphir-elm Compatibility):
I/O Functions:
Metadata Functions:
TreeConfig Type
Fields:
TreeStatistics Type
Fields:
Implementation Tasks
1. Create FileTree.fs
# Add new file to existing Morphir.IR.Pipeline project
touch src/Morphir.IR.Pipeline/FileTree.fs
# Update Morphir.IR.Pipeline.fsproj to include FileTree.fs AFTER File.fs
2. Define Core Types
namespace Morphir.IR.Pipeline
open System.Collections.Immutable
/// <summary>
/// Configuration for a file tree (project-level settings).
/// </summary>
type TreeConfig = {
/// <summary>Project name</summary>
Name: string option
/// <summary>Project version</summary>
Version: string option
/// <summary>Output format preference</summary>
OutputFormat: string option
/// <summary>Plugin-specific configuration</summary>
Extensions: ImmutableDictionary<string, obj>
}
/// <summary>
/// Content of a file tree node (file or directory).
/// </summary>
type TreeContent =
| File of file: VFile
| Directory of tree: VFileTree
/// <summary>
/// Represents a hierarchical tree of Morphir files.
/// This is the PRIMARY abstraction for multi-file operations in morphir-dotnet.
/// </summary>
and VFileTree = {
/// <summary>Root path (directory or project file)</summary>
Path: string
/// <summary>Tree content (files and subdirectories)</summary>
Content: TreeContent list
/// <summary>Tree-level metadata</summary>
Metadata: ImmutableDictionary<string, obj>
/// <summary>Project configuration</summary>
Config: TreeConfig
}
/// <summary>
/// Statistics about a file tree.
/// </summary>
type TreeStatistics = {
TotalFiles: int
TotalDirectories: int
ErrorCount: int
WarningCount: int
InfoCount: int
}
3. Implement Module Functions
[<RequireQualifiedAccess>]
module TreeConfig =
let empty: TreeConfig = {
Name = None
Version = None
OutputFormat = None
Extensions = ImmutableDictionary.Empty
}
let create (name: string) (version: string): TreeConfig =
{ empty with Name = Some name; Version = Some version }
[<RequireQualifiedAccess>]
module VFileTree =
/// Create an empty file tree
let empty: VFileTree = {
Path = "."
Content = []
Metadata = ImmutableDictionary.Empty
Config = TreeConfig.empty
}
/// Create a file tree with path and config
let create (path: string) (config: TreeConfig): VFileTree =
{ empty with Path = path; Config = config }
/// Add a file to the tree
let addFile (file: VFile) (tree: VFileTree): VFileTree =
{ tree with Content = tree.Content @ [ File file ] }
/// Add a subdirectory to the tree
let addDirectory (subtree: VFileTree) (tree: VFileTree): VFileTree =
{ tree with Content = tree.Content @ [ Directory subtree ] }
/// Build tree from list of files (flat structure)
let fromFiles (files: VFile list): VFileTree =
files
|> List.fold (fun tree file -> addFile file tree) empty
/// Get all files in the tree (flattened)
let rec allFiles (tree: VFileTree): VFile list =
tree.Content
|> List.collect (function
| File file -> [ file ]
| Directory subtree -> allFiles subtree)
/// Find a file by path
let rec findFile (path: string) (tree: VFileTree): VFile option =
tree.Content
|> List.tryPick (function
| File file when file.Path = Some path -> Some file
| Directory subtree -> findFile path subtree
| _ -> None)
/// Count total files (recursive)
let fileCount (tree: VFileTree): int =
tree |> allFiles |> List.length
/// Count total directories (recursive)
let rec directoryCount (tree: VFileTree): int =
tree.Content
|> List.sumBy (function
| File _ -> 0
| Directory subtree -> 1 + directoryCount subtree)
/// Check if tree has any errors
let hasErrors (tree: VFileTree): bool =
tree
|> allFiles
|> List.exists VFile.hasErrors
/// Collect all diagnostic messages from all files
let collectMessages (tree: VFileTree): VMessage list =
tree
|> allFiles
|> List.collect (fun file -> file.Messages)
/// Map a function over all files in the tree
let rec map (f: VFile -> VFile) (tree: VFileTree): VFileTree =
{
tree with
Content =
tree.Content
|> List.map (function
| File file -> File (f file)
| Directory subtree -> Directory (map f subtree))
}
/// Filter files in the tree
let rec filter (predicate: VFile -> bool) (tree: VFileTree): VFileTree =
{
tree with
Content =
tree.Content
|> List.choose (function
| File file when predicate file -> Some (File file)
| Directory subtree -> Some (Directory (filter predicate subtree))
| _ -> None)
}
/// Update a specific file by path
let rec updateFile (path: string) (f: VFile -> VFile) (tree: VFileTree): VFileTree =
{
tree with
Content =
tree.Content
|> List.map (function
| File file when file.Path = Some path -> File (f file)
| Directory subtree -> Directory (updateFile path f subtree)
| other -> other)
}
/// Flatten tree to morphir-elm style FileMap
let toFileMap (tree: VFileTree): Map<string, VFile> =
let rec flatten (basePath: string) (content: TreeContent list): (string * VFile) list =
content
|> List.collect (function
| File file ->
let fullPath =
match file.Path with
| Some p -> System.IO.Path.Combine(basePath, p)
| None -> basePath
[ (fullPath, file) ]
| Directory subtree ->
let dirPath = System.IO.Path.Combine(basePath, subtree.Path)
flatten dirPath subtree.Content)
tree.Content
|> flatten tree.Path
|> Map.ofList
/// Create tree from morphir-elm style FileMap (infer structure from paths)
let fromFileMap (fileMap: Map<string, VFile>): VFileTree =
// TODO: Implement grouping by directory path segments
// For now, return flat tree
{
Path = "."
Content = fileMap |> Map.toList |> List.map (snd >> File)
Metadata = ImmutableDictionary.Empty
Config = TreeConfig.empty
}
/// Flatten tree to simple string map (morphir-elm exact equivalent)
let toStringMap (tree: VFileTree): Map<string, string> =
tree
|> toFileMap
|> Map.map (fun _ file ->
match file.Content with
| Some content -> unbox<string> content
| None -> "")
/// Set tree-level metadata
let setMetadata (key: string) (value: obj) (tree: VFileTree): VFileTree =
{ tree with Metadata = tree.Metadata.SetItem(key, value) }
/// Get tree-level metadata
let getMetadata (key: string) (tree: VFileTree): obj option =
match tree.Metadata.TryGetValue(key) with
| true, value -> Some value
| false, _ -> None
/// Get tree statistics
let statistics (tree: VFileTree): TreeStatistics =
let files = allFiles tree
let messages = collectMessages tree
{
TotalFiles = fileCount tree
TotalDirectories = directoryCount tree
ErrorCount = messages |> List.filter (fun m -> m.Severity = Error || m.Severity = Fatal) |> List.length
WarningCount = messages |> List.filter (fun m -> m.Severity = Warning) |> List.length
InfoCount = messages |> List.filter (fun m -> m.Severity = Info) |> List.length
}
/// Write tree to disk
let writeToDisk (outputDir: string) (tree: VFileTree): Result<unit, string> =
try
tree
|> toFileMap
|> Map.iter (fun relPath file ->
let fullPath = System.IO.Path.Combine(outputDir, relPath)
let dir = System.IO.Path.GetDirectoryName(fullPath)
System.IO.Directory.CreateDirectory(dir) |> ignore
System.IO.File.WriteAllText(fullPath, file.Content |> Option.defaultValue ""))
Ok ()
with
| ex -> Error ex.Message
4. Write Tests
Create tests/Morphir.IR.Pipeline.Tests/FileTreeTests.fs:
module Morphir.IR.Pipeline.Tests.FileTreeTests
open TUnit.Core
open Morphir.IR.Pipeline
[<Test>]
let ``Create empty file tree`` () =
let tree = VFileTree.empty
tree.Path |> should equal "."
tree.Content |> should be empty
[<Test>]
let ``Add file to tree`` () =
let file = VFile.create "test.fs" "let x = 42"
let tree = VFileTree.empty |> VFileTree.addFile file
tree |> VFileTree.fileCount |> should equal 1
tree |> VFileTree.allFiles |> List.head |> should equal file
[<Test>]
let ``Add subdirectory to tree`` () =
let subTree = VFileTree.create "subdir" TreeConfig.empty
let tree = VFileTree.empty |> VFileTree.addDirectory subTree
tree |> VFileTree.directoryCount |> should equal 1
[<Test>]
let ``toFileMap flattens tree to Map`` () =
let file1 = VFile.create "file1.fs" "code1"
let file2 = VFile.create "file2.fs" "code2"
let subTree = VFileTree.empty |> VFileTree.addFile file2
let tree =
VFileTree.empty
|> VFileTree.addFile file1
|> VFileTree.addDirectory subTree
let fileMap = VFileTree.toFileMap tree
fileMap |> Map.count |> should equal 2
[<Test>]
let ``fromFiles builds flat tree`` () =
let files = [
VFile.create "file1.fs" "code1"
VFile.create "file2.fs" "code2"
]
let tree = VFileTree.fromFiles files
tree |> VFileTree.fileCount |> should equal 2
[<Test>]
let ``statistics calculates correctly`` () =
let fileWithError =
VFile.create "error.fs" "code"
|> VFile.error "Test error" None
let fileWithWarning =
VFile.create "warning.fs" "code"
|> VFile.warn "Test warning" None
let tree =
VFileTree.empty
|> VFileTree.addFile fileWithError
|> VFileTree.addFile fileWithWarning
let stats = VFileTree.statistics tree
stats.TotalFiles |> should equal 2
stats.ErrorCount |> should equal 1
stats.WarningCount |> should equal 1
5. Update Project File
Add to src/Morphir.IR.Pipeline/Morphir.IR.Pipeline.fsproj:
<ItemGroup>
<Compile Include="File.fs" />
<Compile Include="FileTree.fs" /> <!-- NEW -->
<!-- ... other files ... -->
</ItemGroup>
6. Documentation
Success Criteria
Dependencies
- None (extends existing Morphir.IR.Pipeline project)
Blocks
Related Documents
Ready for GitHub Copilot: This issue provides complete context for AI-assisted implementation.
Estimated Effort: 1 week (4-5 developer-days)
Priority: P0 (Critical - blocks backend and frontend work)
GitHub Issue: Implement VFileTree in Morphir.IR.Pipeline
Type: Feature
Labels:
feature,file-architecture,priority-p0,pipelinePriority: P0 (Blocks F# Backend and F# Frontend)
Milestone: v1.0.0
Estimated Effort: 1 week
Project: Morphir.IR.Pipeline (existing project - add new file)
Description
Implement
VFileTree- a hierarchical file structure abstraction for multi-file projects in theMorphir.IR.Pipelineproject. This will be the universal, primary abstraction for file operations across all morphir-dotnet components (F# Backend, F# Frontend, future backends/frontends).Naming: VFile and VFileTree (inspired by unifiedJS VFile). More concise than VFile/VFileTree.
Key Decision: VFileTree is the PRIMARY abstraction (not a conversion from flat maps). This departs from morphir-elm's flat FileMap approach in favor of better scalability, UX, and structure preservation.
Related Design: Unified File Architecture
Context
What Exists Today
✅ VFile (already in
Morphir.IR.Pipeline/File.fs):What's Missing
❌ Multi-file support:
Acceptance Criteria
Core Types (Add to
Morphir.IR.Pipeline/FileTree.fs)Type Definitions:
VFileTreerecord typeTreeContentdiscriminated union (File | Directory)TreeConfigfor project-level configurationRequired Fields:
Path: string- Root path (directory or project file)Content: TreeContent list- Files and subdirectoriesMetadata: ImmutableDictionary<string, obj>- Tree-level metadataConfig: TreeConfig- Project configurationModule Functions
Creation Functions (
VFileTreemodule):empty: VFileTree- Create empty treecreate: string -> TreeConfig -> VFileTree- Create with path and configaddFile: VFile -> VFileTree -> VFileTree- Add file to treeaddDirectory: VFileTree -> VFileTree -> VFileTree- Add subtreefromFiles: VFile list -> VFileTree- Build tree from file listQuery Functions:
allFiles: VFileTree -> VFile list- Flatten to all filesfindFile: string -> VFileTree -> VFile option- Find by pathfileCount: VFileTree -> int- Total files (recursive)directoryCount: VFileTree -> int- Total directorieshasErrors: VFileTree -> bool- Check for errors in any filecollectMessages: VFileTree -> VMessage list- All diagnosticsTransformation Functions:
map: (VFile -> VFile) -> VFileTree -> VFileTree- Transform all filesfilter: (VFile -> bool) -> VFileTree -> VFileTree- Filter filesupdateFile: string -> (VFile -> VFile) -> VFileTree -> VFileTree- Update specific fileConversion Functions (morphir-elm Compatibility):
toFileMap: VFileTree -> Map<string, VFile>- Flatten to flat map (morphir-elm compatible)fromFileMap: Map<string, VFile> -> VFileTree- Build tree from flat map (infer structure)toStringMap: VFileTree -> Map<string, string>- Exact morphir-elmDict FilePath FileContentequivalentI/O Functions:
writeToDisk: string -> VFileTree -> Result<unit, string>- Write tree to diskloadFromDisk: string -> Result<VFileTree, string>- Load tree from disk (future)Metadata Functions:
setMetadata: string -> obj -> VFileTree -> VFileTree- Set tree-level metadatagetMetadata: string -> VFileTree -> obj option- Get tree-level metadatastatistics: VFileTree -> TreeStatistics- Get tree statistics (file count, errors, warnings, etc.)TreeConfig Type
Fields:
Name: string option- Project nameVersion: string option- Project versionOutputFormat: string option- Output format preference (e.g., "fsharp", "csharp")Extensions: ImmutableDictionary<string, obj>- Plugin-specific configurationTreeStatistics Type
Fields:
TotalFiles: int- Total number of filesTotalDirectories: int- Total number of directoriesErrorCount: int- Total error messagesWarningCount: int- Total warning messagesInfoCount: int- Total info messagesImplementation Tasks
1. Create FileTree.fs
2. Define Core Types
3. Implement Module Functions
4. Write Tests
Create
tests/Morphir.IR.Pipeline.Tests/FileTreeTests.fs:5. Update Project File
Add to
src/Morphir.IR.Pipeline/Morphir.IR.Pipeline.fsproj:6. Documentation
Success Criteria
VFileAPIDependencies
Blocks
Related Documents
Ready for GitHub Copilot: This issue provides complete context for AI-assisted implementation.
Estimated Effort: 1 week (4-5 developer-days)
Priority: P0 (Critical - blocks backend and frontend work)