ace-support-nav 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/config.yml +33 -0
  3. data/.ace-defaults/nav/protocols/guide-sources/ace-support-nav.yml +7 -0
  4. data/.ace-defaults/nav/protocols/guide.yml +69 -0
  5. data/.ace-defaults/nav/protocols/prompt.yml +39 -0
  6. data/.ace-defaults/nav/protocols/skill-sources/ace-support-nav.yml +19 -0
  7. data/.ace-defaults/nav/protocols/skill.yml +22 -0
  8. data/.ace-defaults/nav/protocols/tmpl-sources/ace-support-nav.yml +7 -0
  9. data/.ace-defaults/nav/protocols/tmpl.yml +55 -0
  10. data/.ace-defaults/nav/protocols/wfi-sources/ace-support-nav.yml +7 -0
  11. data/.ace-defaults/nav/protocols/wfi.yml +61 -0
  12. data/CHANGELOG.md +231 -0
  13. data/LICENSE +21 -0
  14. data/README.md +48 -0
  15. data/Rakefile +12 -0
  16. data/docs/demo/ace-support-nav-getting-started.gif +0 -0
  17. data/docs/demo/ace-support-nav-getting-started.tape.yml +28 -0
  18. data/exe/ace-nav +49 -0
  19. data/handbook/workflow-instructions/test.wfi.md +14 -0
  20. data/lib/ace/support/nav/atoms/extension_inferrer.rb +134 -0
  21. data/lib/ace/support/nav/atoms/gem_resolver.rb +59 -0
  22. data/lib/ace/support/nav/atoms/path_normalizer.rb +52 -0
  23. data/lib/ace/support/nav/atoms/uri_parser.rb +62 -0
  24. data/lib/ace/support/nav/cli/commands/create.rb +114 -0
  25. data/lib/ace/support/nav/cli/commands/list.rb +122 -0
  26. data/lib/ace/support/nav/cli/commands/resolve.rb +187 -0
  27. data/lib/ace/support/nav/cli/commands/sources.rb +112 -0
  28. data/lib/ace/support/nav/cli.rb +66 -0
  29. data/lib/ace/support/nav/models/handbook_source.rb +73 -0
  30. data/lib/ace/support/nav/models/protocol_source.rb +104 -0
  31. data/lib/ace/support/nav/models/resource.rb +46 -0
  32. data/lib/ace/support/nav/models/resource_uri.rb +78 -0
  33. data/lib/ace/support/nav/molecules/config_loader.rb +275 -0
  34. data/lib/ace/support/nav/molecules/handbook_scanner.rb +204 -0
  35. data/lib/ace/support/nav/molecules/protocol_scanner.rb +434 -0
  36. data/lib/ace/support/nav/molecules/resource_resolver.rb +134 -0
  37. data/lib/ace/support/nav/molecules/source_registry.rb +133 -0
  38. data/lib/ace/support/nav/organisms/command_delegator.rb +122 -0
  39. data/lib/ace/support/nav/organisms/navigation_engine.rb +180 -0
  40. data/lib/ace/support/nav/version.rb +9 -0
  41. data/lib/ace/support/nav.rb +104 -0
  42. metadata +228 -0
data/exe/ace-nav ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ace/support/nav"
6
+ require "ace/support/cli"
7
+
8
+ def translate_legacy_args(args)
9
+ return ["--help"] if args.empty?
10
+
11
+ if args.include?("--sources")
12
+ return ["sources"] + args.reject { |arg| arg == "--sources" }
13
+ end
14
+
15
+ return args unless args.include?("--create")
16
+
17
+ create_index = args.index("--create")
18
+ uri = args[create_index + 1]
19
+ target = args[create_index + 2]
20
+ translated = ["create"]
21
+ consumed = [create_index]
22
+
23
+ if uri && !uri.start_with?("-")
24
+ translated << uri
25
+ consumed << create_index + 1
26
+ end
27
+
28
+ if target && !target.start_with?("-")
29
+ translated << target
30
+ consumed << create_index + 2
31
+ end
32
+
33
+ args.each_with_index do |arg, index|
34
+ translated << arg unless consumed.include?(index)
35
+ end
36
+
37
+ translated
38
+ end
39
+
40
+ # Start CLI with exception-based exit code handling (per ADR-023)
41
+ begin
42
+ argv = translate_legacy_args(ARGV.dup)
43
+ Ace::Support::Cli::Runner.new(Ace::Support::Nav::CLI).call(args: argv)
44
+ rescue SystemExit => e
45
+ exit(e.status)
46
+ rescue Ace::Support::Cli::Error => e
47
+ warn e.message
48
+ exit(e.exit_code)
49
+ end
@@ -0,0 +1,14 @@
1
+ # Test Workflow
2
+
3
+ ## Goal
4
+ Test the new protocol source registration system.
5
+
6
+ ## Process Steps
7
+ 1. Register sources via YAML files
8
+ 2. Discover sources dynamically
9
+ 3. Resolve resources using the new system
10
+
11
+ ## Success Criteria
12
+ - [x] Sources can be registered per protocol
13
+ - [x] Multiple sources are discovered
14
+ - [x] Resources are resolved correctly
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Nav
6
+ module Atoms
7
+ # Infers file extensions for protocol-based resource resolution
8
+ # Implements DWIM (Do What I Mean) extension inference
9
+ class ExtensionInferrer
10
+ # Default extension inference order when not configured
11
+ DEFAULT_FALLBACK_ORDER = %w[
12
+ protocol_shorthand
13
+ protocol_full
14
+ generic_markdown
15
+ bare
16
+ ].freeze
17
+
18
+ class << self
19
+ # Generate candidate extensions for a pattern based on protocol config
20
+ # @param pattern [String] The search pattern (e.g., "markdown-style")
21
+ # @param protocol_extensions [Array<String>] Protocol-specific extensions (e.g., [".g.md", ".guide.md"])
22
+ # @param enabled [Boolean] Whether extension inference is enabled
23
+ # @param fallback_order [Array<String>] Order of extension types to try
24
+ # @return [Array<String>] List of candidate patterns to try
25
+ def infer_extensions(pattern, protocol_extensions: [], enabled: true, fallback_order: DEFAULT_FALLBACK_ORDER)
26
+ return [pattern] unless enabled
27
+ return [pattern] if pattern.empty?
28
+
29
+ # Guard against nil fallback_order
30
+ fallback_order ||= DEFAULT_FALLBACK_ORDER
31
+
32
+ # Extract shorthand extensions from protocol extensions
33
+ # e.g., from [".g.md", ".guide.md"] extract [".g"]
34
+ shorthand_extensions = extract_shorthand_extensions(protocol_extensions)
35
+
36
+ candidates = []
37
+
38
+ fallback_order.each do |fallback_type|
39
+ case fallback_type
40
+ when "protocol_shorthand"
41
+ # Try with shorthand extensions first (e.g., ".g")
42
+ shorthand_extensions.each do |ext|
43
+ candidate = "#{pattern}#{ext}"
44
+ candidates << candidate unless candidates.include?(candidate)
45
+ end
46
+ when "protocol_full"
47
+ # Try with full protocol extensions (e.g., ".g.md", ".guide.md")
48
+ protocol_extensions.each do |ext|
49
+ candidate = "#{pattern}#{ext}"
50
+ candidates << candidate unless candidates.include?(candidate)
51
+ end
52
+ when "generic_markdown"
53
+ # Try with generic markdown extension (e.g., ".md")
54
+ candidate = "#{pattern}.md"
55
+ candidates << candidate unless candidates.include?(candidate)
56
+ when "bare"
57
+ # Try with no extension
58
+ candidates << pattern unless candidates.include?(pattern)
59
+ end
60
+ end
61
+
62
+ candidates
63
+ end
64
+
65
+ # Check if a pattern already includes an extension from the list
66
+ # @param pattern [String] The pattern to check
67
+ # @param extensions [Array<String>] List of extensions to check against
68
+ # @return [Boolean] True if pattern ends with any of the extensions
69
+ def has_extension?(pattern, extensions)
70
+ return false if pattern.nil? || pattern.empty?
71
+ return false if extensions.nil? || extensions.empty?
72
+
73
+ extensions.any? { |ext| pattern.end_with?(ext) }
74
+ end
75
+
76
+ # Extract shorthand extensions from full protocol extensions
77
+ # e.g., from [".g.md", ".guide.md"] extract [".g"]
78
+ # @param protocol_extensions [Array<String>] Full protocol extensions
79
+ # @return [Array<String>] Shorthand extensions
80
+ def extract_shorthand_extensions(protocol_extensions)
81
+ return [] if protocol_extensions.nil? || protocol_extensions.empty?
82
+
83
+ shorthand_extensions = []
84
+
85
+ protocol_extensions.each do |ext|
86
+ # Extract the part before the first dot after the initial dot
87
+ # e.g., ".g.md" -> ".g", ".guide.md" -> ".guide"
88
+ parts = ext.split(".")
89
+ if parts.length >= 2
90
+ shorthand = ".#{parts[1]}"
91
+ shorthand_extensions << shorthand unless shorthand_extensions.include?(shorthand)
92
+ end
93
+ end
94
+
95
+ shorthand_extensions
96
+ end
97
+
98
+ # Get the base pattern without any extension
99
+ # @param pattern [String] The pattern to process
100
+ # @param extensions [Array<String>] List of extensions to strip
101
+ # @return [String] Pattern without extension
102
+ def strip_extension(pattern, extensions)
103
+ return pattern if extensions.nil? || extensions.empty?
104
+
105
+ result = pattern.dup
106
+ extensions.each do |ext|
107
+ result = result.sub(/#{Regexp.escape(ext)}\z/, "") if result.end_with?(ext)
108
+ end
109
+
110
+ result
111
+ end
112
+ end
113
+
114
+ # Instance method wrappers for backwards compatibility (deprecated)
115
+ def infer_extensions(...)
116
+ self.class.infer_extensions(...)
117
+ end
118
+
119
+ def has_extension?(...)
120
+ self.class.has_extension?(...)
121
+ end
122
+
123
+ def extract_shorthand_extensions(...)
124
+ self.class.extract_shorthand_extensions(...)
125
+ end
126
+
127
+ def strip_extension(...)
128
+ self.class.strip_extension(...)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+
5
+ module Ace
6
+ module Support
7
+ module Nav
8
+ module Atoms
9
+ # Discovers ace-* gems and their handbook paths
10
+ class GemResolver
11
+ def find_ace_gems
12
+ ace_gems = []
13
+
14
+ Gem::Specification.each do |spec|
15
+ next unless spec.name.start_with?("ace-")
16
+
17
+ gem_info = {
18
+ name: spec.name,
19
+ version: spec.version.to_s,
20
+ path: spec.gem_dir,
21
+ handbook_path: File.join(spec.gem_dir, "handbook")
22
+ }
23
+
24
+ # Check if handbook exists
25
+ gem_info[:has_handbook] = Dir.exist?(gem_info[:handbook_path])
26
+
27
+ ace_gems << gem_info
28
+ end
29
+
30
+ ace_gems
31
+ end
32
+
33
+ def find_gem_by_name(gem_name)
34
+ spec = Gem::Specification.find_by_name(gem_name)
35
+ return nil unless spec
36
+
37
+ {
38
+ name: spec.name,
39
+ version: spec.version.to_s,
40
+ path: spec.gem_dir,
41
+ handbook_path: File.join(spec.gem_dir, "handbook"),
42
+ has_handbook: Dir.exist?(File.join(spec.gem_dir, "handbook"))
43
+ }
44
+ rescue Gem::LoadError
45
+ nil
46
+ end
47
+
48
+ def gem_handbook_path(gem_name)
49
+ gem_info = find_gem_by_name(gem_name)
50
+ return nil unless gem_info
51
+ return nil unless gem_info[:has_handbook]
52
+
53
+ gem_info[:handbook_path]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Nav
6
+ module Atoms
7
+ # Normalizes and expands paths
8
+ class PathNormalizer
9
+ def normalize(path)
10
+ return nil if path.nil? || path.empty?
11
+
12
+ # Expand home directory
13
+ path = File.expand_path(path) if path.start_with?("~")
14
+
15
+ # Resolve relative paths
16
+ path = File.expand_path(path) unless path.start_with?("/")
17
+
18
+ path
19
+ end
20
+
21
+ def join_paths(*parts)
22
+ File.join(*parts.compact)
23
+ end
24
+
25
+ def dirname(path)
26
+ File.dirname(path)
27
+ end
28
+
29
+ def basename(path, suffix = nil)
30
+ suffix ? File.basename(path, suffix) : File.basename(path)
31
+ end
32
+
33
+ def extname(path)
34
+ File.extname(path)
35
+ end
36
+
37
+ def exists?(path)
38
+ File.exist?(path)
39
+ end
40
+
41
+ def directory?(path)
42
+ File.directory?(path)
43
+ end
44
+
45
+ def file?(path)
46
+ File.file?(path)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../molecules/config_loader"
4
+
5
+ module Ace
6
+ module Support
7
+ module Nav
8
+ module Atoms
9
+ # Parses resource URIs
10
+ class UriParser
11
+ def initialize(config_loader: nil)
12
+ @config_loader = config_loader || Molecules::ConfigLoader.new
13
+ end
14
+
15
+ def parse(uri_string)
16
+ return nil unless uri_string.is_a?(String)
17
+ return nil unless uri_string.include?("://")
18
+
19
+ parts = uri_string.split("://", 2)
20
+ protocol = parts[0]
21
+ rest = parts[1]
22
+
23
+ return nil unless valid_protocol?(protocol)
24
+ return {protocol: protocol, source: nil, path: nil} if rest.nil? || rest.empty?
25
+
26
+ # Check for source-specific syntax (@source/path or @source)
27
+ if rest.start_with?("@")
28
+ parse_source_specific(protocol, rest)
29
+ else
30
+ {protocol: protocol, source: nil, path: rest}
31
+ end
32
+ end
33
+
34
+ def valid_protocol?(protocol)
35
+ @config_loader.valid_protocol?(protocol)
36
+ end
37
+
38
+ def valid_protocols
39
+ @config_loader.valid_protocols
40
+ end
41
+
42
+ def extract_protocol(uri_string)
43
+ return nil unless uri_string.include?("://")
44
+ uri_string.split("://", 2)[0]
45
+ end
46
+
47
+ private
48
+
49
+ def parse_source_specific(protocol, rest)
50
+ # Format: @source/path or just @source
51
+ if rest.include?("/")
52
+ source_parts = rest.split("/", 2)
53
+ {protocol: protocol, source: source_parts[0], path: source_parts[1]}
54
+ else
55
+ {protocol: protocol, source: rest, path: nil}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../organisms/navigation_engine"
6
+
7
+ module Ace
8
+ module Support
9
+ module Nav
10
+ module CLI
11
+ module Commands
12
+ # ace-support-cli Command class for the create command
13
+ class Create < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+
16
+ desc <<~DESC.strip
17
+ Create resource from template
18
+
19
+ SYNTAX:
20
+ ace-nav create [URI] [TARGET] [OPTIONS]
21
+
22
+ EXAMPLES:
23
+
24
+ # Create from workflow template
25
+ $ ace-nav create wfi://my-workflow
26
+
27
+ # Create from template to specific file
28
+ $ ace-nav create tmpl://custom ./output.md
29
+
30
+ CONFIGURATION:
31
+
32
+ Global config: ~/.ace/nav/config.yml
33
+ Project config: .ace/nav/config.yml
34
+ Example: ace-support-nav/.ace-defaults/nav/config.yml
35
+
36
+ OUTPUT:
37
+
38
+ Creates resource at specified path or default location
39
+ Exit codes: 0 (success), 1 (error)
40
+ DESC
41
+
42
+ example [
43
+ "wfi://my-workflow # Create from workflow template",
44
+ "tmpl://custom ./output.md # Create from template to file"
45
+ ]
46
+
47
+ argument :uri, required: true, desc: "Template URI"
48
+ argument :target, required: false, desc: "Target file path"
49
+
50
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
51
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
52
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
53
+
54
+ def call(uri:, target: nil, **options)
55
+ # Initialize instance variables for use in private methods
56
+ @uri = uri
57
+ @target = target
58
+ @options = options
59
+ @engine = Organisms::NavigationEngine.new
60
+
61
+ execute
62
+ end
63
+
64
+ def execute
65
+ display_config_summary
66
+
67
+ result = @engine.create(@uri, @target)
68
+
69
+ if result[:error]
70
+ raise Ace::Support::Cli::Error.new(result[:error])
71
+ end
72
+
73
+ puts "Created: #{result[:created]}"
74
+ puts "From: #{result[:from]}" if @options[:verbose]
75
+ end
76
+
77
+ private
78
+
79
+ def display_config_summary
80
+ return if @options[:quiet]
81
+
82
+ require "ace/core"
83
+ Ace::Core::Atoms::ConfigSummary.display(
84
+ command: "create",
85
+ config: load_effective_config,
86
+ defaults: default_config,
87
+ options: @options,
88
+ quiet: false
89
+ )
90
+ end
91
+
92
+ def load_effective_config
93
+ # Use Ace::Support::Nav.config which already handles the cascade
94
+ require_relative "../../../nav"
95
+ Ace::Support::Nav.config
96
+ end
97
+
98
+ def default_config
99
+ # Use centralized gem_root from Nav module (avoids path depth duplication)
100
+ defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
101
+
102
+ if File.exist?(defaults_path)
103
+ require "yaml"
104
+ YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
105
+ else
106
+ {}
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../organisms/navigation_engine"
6
+
7
+ module Ace
8
+ module Support
9
+ module Nav
10
+ module CLI
11
+ module Commands
12
+ # ace-support-cli Command class for the list command
13
+ class List < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+
16
+ desc <<~DESC.strip
17
+ List matching resources
18
+
19
+ SYNTAX:
20
+ ace-nav list [PATTERN] [OPTIONS]
21
+
22
+ EXAMPLES:
23
+
24
+ # List all workflows
25
+ $ ace-nav list 'wfi://*'
26
+
27
+ # List templates with pattern
28
+ $ ace-nav list 'tmpl://@ace-*/*'
29
+
30
+ # Tree format
31
+ $ ace-nav list wfi:// --tree
32
+
33
+ CONFIGURATION:
34
+
35
+ Global config: ~/.ace/nav/config.yml
36
+ Project config: .ace/nav/config.yml
37
+ Example: ace-support-nav/.ace-defaults/nav/config.yml
38
+
39
+ OUTPUT:
40
+
41
+ Table format with columns: URI, path, type
42
+ Use --tree for hierarchical format
43
+ Exit codes: 0 (success), 1 (error)
44
+ DESC
45
+
46
+ example [
47
+ "'wfi://*' # List all workflows",
48
+ "'tmpl://@ace-*/*' # List templates with pattern",
49
+ "wfi:// --tree # Tree format"
50
+ ]
51
+
52
+ argument :pattern, required: true, desc: "Pattern to match resources"
53
+
54
+ option :tree, type: :boolean, desc: "Display resources in tree format"
55
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
56
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
57
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
58
+
59
+ def call(pattern:, **options)
60
+ # Initialize instance variables for use in private methods
61
+ @pattern = pattern
62
+ @options = options
63
+ @engine = Organisms::NavigationEngine.new
64
+
65
+ execute
66
+ end
67
+
68
+ def execute
69
+ display_config_summary
70
+
71
+ resources = @engine.list(@pattern, tree: @options[:tree], verbose: @options[:verbose])
72
+
73
+ if resources.empty?
74
+ raise Ace::Support::Cli::Error.new("No resources found matching: #{@pattern}")
75
+ end
76
+
77
+ if @options[:verbose]
78
+ require "json"
79
+ puts JSON.pretty_generate(resources)
80
+ else
81
+ resources.each { |resource| puts resource }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def display_config_summary
88
+ return if @options[:quiet]
89
+
90
+ require "ace/core"
91
+ Ace::Core::Atoms::ConfigSummary.display(
92
+ command: "list",
93
+ config: load_effective_config,
94
+ defaults: default_config,
95
+ options: @options,
96
+ quiet: false
97
+ )
98
+ end
99
+
100
+ def load_effective_config
101
+ # Use Ace::Support::Nav.config which already handles the cascade
102
+ require_relative "../../../nav"
103
+ Ace::Support::Nav.config
104
+ end
105
+
106
+ def default_config
107
+ # Use centralized gem_root from Nav module (avoids path depth duplication)
108
+ defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
109
+
110
+ if File.exist?(defaults_path)
111
+ require "yaml"
112
+ YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
113
+ else
114
+ {}
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end