munola 0.0.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b3a5c6e65edd8492967e458ddb0943a1557c96fe3600a268008f2f71972d5e6
4
- data.tar.gz: b834e5597e9c9a9de7ac5049196ede6d53afa0df44e347b0eade627cfc67c7d9
3
+ metadata.gz: 3fef04df27a6176d9c98ef6a96588ab444f63e92a6ef3c3c3fd73268af931de3
4
+ data.tar.gz: 8190ecb25656352775ea9ead33c18e04c7faddaac23d67644045398fea80ce26
5
5
  SHA512:
6
- metadata.gz: 18cfc7574cdd21c1efb323f8652e699fb0d232d9351b6bcdfa6996a2930a76e0de5eb485ef8cbe5ad56ebbe45b5b4b700fc9265f26ab1afb4062f33c9e4e9277
7
- data.tar.gz: 76d7c976e14c3d51d6427bd633e220add54d6ac236eadb624ea24a489d1e47978fd5d7ca44c26236a62a2c5a63ceb1985383b69416020f99b2227069a779d74b
6
+ metadata.gz: 7524cf02ef40ba7136a5ff531ffda09c2e1056b9a53b73710728be0ad64a7158d3aeccf94492a7401619776d2a44a07e22bcff7511ac775083112de200df4820
7
+ data.tar.gz: fcf9fc3d4ac887817ad8dab39f5c071d4569a282a12fa2720bd5f935da62a4793e6a833c45032cead51f653603baf675f78f2f064351a8bae23516f912a9e883
data/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ ## munola 0.4.4.2 and munola-rb 0.4.4.2 (2026-08-23)
2
+
3
+ munola follows the channel it drives. Its binary is now cut from the stable
4
+ channel after it took eight changes measured against a Ruby architecture
5
+ linter, so the gem's version moves with the release it fetches. The Rails
6
+ layer is its own gem, `munola-rb`, the way `enola-rb` is for the upstream
7
+ channel; `require "munola"` loads no Rails. A catalogue recipe the binary
8
+ already binds is no longer bound a second time.
9
+
10
+ ## enola 0.4.4.1 and enola-rb 0.4.4.1 (2026-08-23)
11
+
12
+ Wrapper fixes on the same upstream release. A test that set the channel put
13
+ back upstream rather than what it found, so on some orderings a channel gem's
14
+ own installation read as upstream. The Rails layer packaged its generator
15
+ through a glob wide enough to take another channel's; it now packages its own.
16
+ Both gems still drive upstream v0.4.4.
17
+
18
+ ## munola-rb 0.4.4.1 (2026-08-23)
19
+
20
+ The Rails layer over munola, what enola-rb is to enola: `rails generate
21
+ munola:install [--tenant-column COLUMN]` writes the starter laws, the
22
+ bindings the binary's own init picks and the munola catalogue, and the
23
+ enola:init, enola:snapshot and enola:check tasks drive munola's binary
24
+ because munola installs its own resolver. The generator and the railtie
25
+ move here out of the munola gem, so `require "munola"` loads no Rails and a
26
+ project that only drives the binary carries no Rails machinery. The recipe
27
+ catalogue, the detection and the tenant-column argument stay munola's.
28
+
29
+ `Enola.resolver_factory` is readable as well as writable, so whoever
30
+ replaces it can put back what was installed rather than what it assumed.
31
+
32
+ ## munola 0.4.4.1 (2026-08-23)
33
+
34
+ The wrapper over the munola channel, first release with an implementation.
35
+ `munola init` writes the starter laws and upstream's bindings the way
36
+ `enola:install` does, then the recipe catalogue from enola-guides, binds the
37
+ recipes the tree shows a need for, fills the tenant foreign-key template
38
+ from the schema, writes `mcp-arch.yaml` with Prism and Rubydex on and fetches
39
+ the Rubydex library; `munola --version` names the munola version, its
40
+ upstream base and the binary that answered; `MUNOLA_BINARY` names a binary
41
+ to drive until the first munola release is cut. The channel's tag shape is
42
+ `munola-v<version>` on the fork's releases.
43
+
44
+ ## enola 0.4.4 (2026-08-23)
45
+
46
+ The wrapper, first release with an implementation: a pure-Ruby `enola`
47
+ executable that drives upstream's v0.4.4. The binary is fetched on first use
48
+ from the release for the running platform, verified against the sha256 the
49
+ release publishes, and kept under `~/.cache/enola/upstream/0.4.4/`; a binary on
50
+ PATH is used only when it answers the pinned version; offline with an empty
51
+ cache is a refusal that names the cache and the release. Every argument and
52
+ exit code is forwarded; `--verbose` adds one stderr line naming channel,
53
+ version and where the binary came from; `enola --wrapper-probe` prints the
54
+ channel, pin, found version and which surfaces the binary answers.
55
+
56
+ Both providers by default: the gem vendors upstream's Prism provider script
57
+ at the pinned version and depends on the `prism` gem; `enola init` writes
58
+ `mcp-arch.yaml` with Prism (run by this Ruby) and Rubydex on, then runs the
59
+ binary's `constraints init`; the Rubydex engine library is fetched once per
60
+ binary version after the binary itself, a failure printed once as a named
61
+ skip. `Enola::Config.write_default` and `Enola::Providers` are the seams the
62
+ Rails layer and munola call.
63
+
64
+ ## enola-rb 0.4.4 (2026-08-23)
65
+
66
+ The Rails layer, first release with an implementation. `rails generate
67
+ enola:install` writes `enola/constraints/` from the enola-guides starter laws,
68
+ binds the recipes the binary's own `constraints init` picks, writes the rest
69
+ as commented bindings and ignores `.enola/`; the `enola:init`,
70
+ `enola:snapshot` and `enola:check` rake tasks drive the binary the `enola` gem
71
+ fetches. A surface the pinned binary lacks is refused by name with the remedy;
72
+ an absent binary still leaves the laws written. Depends on `enola` at the same
73
+ version and on `enola-guides`. The generator writes the provider config first,
74
+ so a fresh app's graph carries Prism and Rubydex facts from its first snapshot.
75
+
76
+ ## munola 0.0.0
77
+
78
+ Placeholder. `munola` is the same wrapper over another channel. Follows.
data/LICENSE CHANGED
@@ -187,7 +187,7 @@ APPENDIX: How to apply the Apache License to your work.
187
187
  same "printed page" as the copyright notice for easier
188
188
  identification within third-party archives.
189
189
 
190
- Copyright (c) 2026 Dejan Menges
190
+ Copyright (c) 2026 Muhamed Isabegovic
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
193
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -1,13 +1,130 @@
1
1
  # enola-rb
2
2
 
3
- Runs [enola](https://github.com/enola-labs/enola), the architecture-graph
4
- tool, from Ruby, with presets and defaults chosen for Rails stacks.
3
+ Four pure-Ruby gems, one repository, for running
4
+ [enola](https://github.com/enola-labs/enola), the architecture-graph tool,
5
+ from Ruby. None of them carries a binary or compiles anything.
5
6
 
6
- Two channels, picked in your Gemfile. `enola-rb` on its own drives whichever
7
- enola binary is on your path. Adding the `munola` gem switches it to the build
8
- this project ships, which carries capabilities not yet in an upstream release.
7
+ Two channels, the same layering on both: `enola` wraps the released upstream
8
+ binary and `enola-rb` adds the Rails layer on it; `munola` wraps the munola
9
+ binary and `munola-rb` adds the Rails layer on that. The Rails code is written
10
+ once, in `enola-rb`, and the munola gems add only what is munola's own.
9
11
 
10
- Presets come from
11
- [enola-guides](https://github.com/misabegovic/enola-guides).
12
+ ## `enola`
12
13
 
13
- Placeholder release. No implementation yet.
14
+ The wrapper of the released enola. The gem versions itself; the release it
15
+ drives is `Enola::UPSTREAM_VERSION`, v0.4.4 as of `enola 0.5.0`, and every
16
+ gemspec summary names it.
17
+
18
+ ```ruby
19
+ gem "enola"
20
+ ```
21
+
22
+ ```sh
23
+ bundle exec enola --generate .
24
+ bundle exec enola baseline pin .
25
+ bundle exec enola check .
26
+ ```
27
+
28
+ The first command that needs the binary downloads the release for your
29
+ platform (linux, darwin, windows; amd64, arm64), verifies it against the
30
+ sha256 file the same release publishes, and keeps it under
31
+ `~/.cache/enola/upstream/0.4.4/` (`ENOLA_CACHE_DIR` moves the root). An
32
+ `enola` already on your PATH is used only when it answers the pinned version;
33
+ offline with an empty cache is a refusal that names the cache and the release
34
+ rather than a fallback to whatever is installed. Nothing downloads at
35
+ `bundle install`.
36
+
37
+ Every argument and exit code is forwarded unchanged; the wrapper adds no flag
38
+ of its own to enola's surface. `--verbose` on any command writes one line to
39
+ stderr first, naming the channel, the version and where the binary came from.
40
+ `enola --wrapper-probe` prints the channel, the pin, the version the binary
41
+ answers and which surfaces it has (`constraints`, `providers`, `check`,
42
+ `hook`), read by running them, never by comparing version strings.
43
+
44
+ This gem is not an enola-labs release; it runs theirs.
45
+
46
+ ## `enola-rb`
47
+
48
+ The Rails layer over `enola`. It depends on `enola` at the same minor and on
49
+ [enola-guides](https://github.com/misabegovic/enola-guides), and adds nothing
50
+ to the binary's surface.
51
+
52
+ ```ruby
53
+ gem "enola-rb"
54
+ ```
55
+
56
+ ```sh
57
+ bin/rails generate enola:install
58
+ bin/rake enola:snapshot
59
+ bin/rake enola:check
60
+ ```
61
+
62
+ The generator writes `enola/constraints/` from the guides' starter laws (four
63
+ laws a Rails team keeps, each with its reason), asks the binary's own
64
+ `constraints init` to bind the shipped recipes whose roles resolve in the app,
65
+ writes every other shipped recipe as a commented binding to uncomment once the
66
+ directories exist, and ignores `.enola/`. It reads what `init` wrote rather
67
+ than its exit code, and when the binary cannot be fetched it still writes the
68
+ laws and says so. `enola:snapshot` generates and pins the baseline;
69
+ `enola:check` grades the working tree against it and fails on a new breach of
70
+ a declared law. A surface the pinned binary lacks is refused by name with the
71
+ remedy.
72
+
73
+ ## `munola`
74
+
75
+ One person's taste on top of enola: the same wrapper over another channel,
76
+ the builds cut from [a fork of enola](https://github.com/misabegovic/enola),
77
+ each release naming what differs from that upstream. The gem versions itself;
78
+ the channel release it fetches is `Munola::CHANNEL_VERSION`, which carries the
79
+ upstream it is built on plus a segment of its own (`0.4.4.2` on v0.4.4). It is
80
+ offered upstream where it fits and is not positioned against it. What it adds is the channel and the catalogue.
81
+
82
+ ```ruby
83
+ gem "munola"
84
+ ```
85
+
86
+ ```sh
87
+ bundle exec munola init . --tenant-column company_id
88
+ bundle exec munola --version
89
+ ```
90
+
91
+ `munola init` does what `enola:install` does, then what only munola carries:
92
+ it writes the recipe catalogue the `enola-guides` gem ships (Ember
93
+ conventions, data ownership, API boundaries, background work, a tenant
94
+ foreign key) into `enola/recipes/`, binds the recipes the tree justifies
95
+ (`ember-cli-build.js`, a schema with `app/models`, `config/routes.rb` with
96
+ `app/policies`, `app/tasks`, a column most tables share confirmed against
97
+ `db/schema.rb`) and writes every other one as a commented binding; it fills
98
+ the tenant template from the schema's own table names, never from an
99
+ inflection table; it writes `mcp-arch.yaml` with both Ruby providers on by
100
+ default, Prism through the script the `enola` gem carries and Rubydex built
101
+ into the binary, and fetches the Rubydex library, a failed fetch reported as a
102
+ named skip. It never asks a question. `munola --version` names the munola
103
+ version, the channel release and upstream it is built on, and which binary
104
+ answered.
105
+
106
+ The binary comes from the fork's releases the way `enola`'s comes from
107
+ upstream's, fetched on first use and verified against the sha256 the release
108
+ publishes; `MUNOLA_BINARY=/path/to/enola` names one to drive instead, and
109
+ every command says which answered. `munola` depends on `enola` at the same
110
+ minor and on `enola-guides` 0.3.1 or later; requiring it loads no Rails.
111
+
112
+ ## `munola-rb`
113
+
114
+ The Rails layer over `munola`, what `enola-rb` is to `enola`. It depends on
115
+ both at the same minor and adds one generator.
116
+
117
+ ```ruby
118
+ gem "munola-rb"
119
+ ```
120
+
121
+ ```sh
122
+ bin/rails generate munola:install --tenant-column company_id
123
+ bin/rake enola:snapshot
124
+ bin/rake enola:check
125
+ ```
126
+
127
+ The generator runs the same install `munola init` does. The rake tasks are
128
+ enola-rb's and need no munola copy: `munola` installs its own resolver when it
129
+ loads, so `enola:snapshot` and `enola:check` drive the munola binary in an app
130
+ that has this gem, and the upstream binary in one that does not.
data/exe/munola ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "munola"
5
+
6
+ exit Munola::CLI.start(ARGV)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Munola
6
+ # The recipes munola ships built in, read from the enola-guides gem so the
7
+ # content has one version line; the template carries a value the installer
8
+ # fills in from what the app shows.
9
+ module Catalogue
10
+ RECIPES = %w[ember-conventions data-ownership api-boundaries background-work tenant-foreign-key].freeze
11
+ TEMPLATES = %w[tenant-foreign-key].freeze
12
+ PLACEHOLDERS = { column: "TENANT_COLUMN", table: "TENANT_TABLE" }.freeze
13
+
14
+ def self.source_dir
15
+ require "enola-rb"
16
+ File.join(EnolaRb::Guides.root, "recipes")
17
+ end
18
+
19
+ def self.source(name)
20
+ path = File.join(source_dir, "#{name}.yaml")
21
+ raise Enola::Unavailable, "the installed enola-guides gem has no recipe #{name}; it needs 0.3.1 or later" unless File.exist?(path)
22
+
23
+ path
24
+ end
25
+
26
+ # Writes every catalogue recipe the project does not already have into
27
+ # enola/recipes/, the template with its values filled or, without them,
28
+ # left as it is so the placeholders show what to decide.
29
+ def self.write(root, tenant: nil)
30
+ dir = File.join(root, "enola", "recipes")
31
+ FileUtils.mkdir_p(dir)
32
+ RECIPES.each_with_object([]) do |name, written|
33
+ target = File.join(dir, "#{name}.yaml")
34
+ next if File.exist?(target)
35
+
36
+ body = File.read(source(name))
37
+ body = fill(body, tenant) if TEMPLATES.include?(name) && tenant
38
+ File.write(target, body)
39
+ written << target
40
+ end
41
+ end
42
+
43
+ def self.fill(body, tenant)
44
+ body.gsub(PLACEHOLDERS[:column], tenant.fetch(:column)).gsub(PLACEHOLDERS[:table], tenant.fetch(:table))
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Munola
4
+ CHANNEL = Enola::Channel.new(
5
+ name: "munola",
6
+ release_base: "https://github.com/misabegovic/enola/releases/download",
7
+ version: CHANNEL_VERSION,
8
+ asset_prefix: "enola",
9
+ tag_prefix: "munola-v"
10
+ ).freeze
11
+
12
+ # The binary this channel drives is fetched from the fork's releases; until
13
+ # the first one is cut, MUNOLA_BINARY names a binary to drive instead, and
14
+ # every command says which one answered.
15
+ class Resolver < Enola::Resolver
16
+ def initialize(override: ENV.fetch("MUNOLA_BINARY", nil), **options)
17
+ super(**options)
18
+ @override = override
19
+ end
20
+
21
+ def resolve
22
+ return super unless @override
23
+
24
+ raise Enola::Unavailable, "MUNOLA_BINARY names #{@override}, which is not executable" unless File.executable?(@override)
25
+
26
+ Found.new(@override, "MUNOLA_BINARY (#{Enola::Probe.new(@override).version || 'no version answered'})")
27
+ end
28
+ end
29
+
30
+ def self.resolve
31
+ Resolver.new(channel: CHANNEL).resolve
32
+ end
33
+
34
+ def self.channel_line(found = resolve)
35
+ "munola #{VERSION}, channel #{CHANNEL_VERSION} on enola #{UPSTREAM_VERSION}; binary via #{found.source} (#{found.path})"
36
+ end
37
+ end
data/lib/munola/cli.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Munola
6
+ module CLI
7
+ def self.start(argv, stdout: $stdout, stderr: $stderr)
8
+ case argv.first
9
+ when "--version"
10
+ found = Munola.resolve
11
+ out, = Open3.capture2e(found.path, "--version")
12
+ stdout.puts Munola.channel_line(found)
13
+ stdout.puts out.strip
14
+ 0
15
+ when "init"
16
+ init(argv.drop(1), stdout: stdout, stderr: stderr)
17
+ else
18
+ Enola::Runner.new(channel: CHANNEL, stderr: stderr).exec(argv)
19
+ end
20
+ rescue Enola::Error => e
21
+ stderr.puts "munola: #{e.message}"
22
+ 127
23
+ end
24
+
25
+ def self.init(args, stdout:, stderr:)
26
+ options = { tenant_column: nil }
27
+ parser = OptionParser.new do |opts|
28
+ opts.banner = "Usage: munola init [ROOT] [--tenant-column COLUMN]"
29
+ opts.on("--tenant-column COLUMN", "The column most tables carry; confirmed against db/schema.rb") { |v| options[:tenant_column] = v }
30
+ end
31
+ rest = parser.parse(args)
32
+ root = rest.first || Dir.pwd
33
+ Installer.new(root, tenant_column: options[:tenant_column], stderr: stderr).install.lines.each { |line| stdout.puts line }
34
+ 0
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Munola
4
+ # Reads the tree for what the binary's own init cannot see: which catalogue
5
+ # recipes have the directories their roles need, and which column most
6
+ # tables share. It reads files; it never asks.
7
+ class Detector
8
+ Tenant = Struct.new(:column, :table, :share, keyword_init: true)
9
+
10
+ def initialize(root)
11
+ @root = File.expand_path(root)
12
+ end
13
+
14
+ def bindings
15
+ found = []
16
+ found << "ember-conventions" if file?("ember-cli-build.js")
17
+ found << "data-ownership" if schema? && dir?("app/models")
18
+ found << "api-boundaries" if file?("config/routes.rb") && dir?("app/policies")
19
+ found << "background-work" if dir?("app/tasks")
20
+ found
21
+ end
22
+
23
+ def schema?
24
+ file?("db/schema.rb") || file?("db/structure.sql")
25
+ end
26
+
27
+ # The tenant column is the one present on most tables; a column the
28
+ # caller names is confirmed against the schema rather than trusted.
29
+ def tenant(column: nil)
30
+ tables = schema_tables
31
+ return nil if tables.empty?
32
+
33
+ counts = Hash.new(0)
34
+ tables.each_value { |columns| columns.uniq.each { |name| counts[name] += 1 } }
35
+ candidate = column || counts.select { |name, _| name.end_with?("_id") }.max_by { |name, count| [count, -name.length] }&.first
36
+ return nil unless candidate
37
+
38
+ share = counts.fetch(candidate, 0).fdiv(tables.size)
39
+ return nil if share < 0.5 && column.nil?
40
+
41
+ table = tenant_table(candidate, tables.keys)
42
+ Tenant.new(column: candidate, table: table, share: share)
43
+ end
44
+
45
+ private
46
+
47
+ def file?(rel)
48
+ File.file?(File.join(@root, rel))
49
+ end
50
+
51
+ def dir?(rel)
52
+ File.directory?(File.join(@root, rel))
53
+ end
54
+
55
+ def schema_tables
56
+ path = File.join(@root, "db", "schema.rb")
57
+ return {} unless File.file?(path)
58
+
59
+ tables = {}
60
+ current = nil
61
+ File.foreach(path) do |line|
62
+ if (m = line.match(/create_table\s+"([^"]+)"/))
63
+ current = m[1]
64
+ tables[current] = []
65
+ elsif current && (m = line.match(/^\s*t\.\w+\s+"([^"]+)"/))
66
+ tables[current] << m[1]
67
+ elsif line.strip == "end"
68
+ current = nil
69
+ end
70
+ end
71
+ tables
72
+ end
73
+
74
+ # company_id names companies, category_id names categories, person_id
75
+ # names people only if the schema has that table: the table is looked up
76
+ # among the schema's own names, never guessed from an inflection table.
77
+ def tenant_table(column, names)
78
+ stem = column.delete_suffix("_id")
79
+ candidates = [stem, "#{stem}s", "#{stem}es", stem.sub(/y\z/, "ies")]
80
+ names.find { |name| candidates.include?(name) }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "open3"
5
+ require "yaml"
6
+
7
+ module Munola
8
+ class Installer
9
+ Report = Struct.new(:written, :bound, :commented, :notes, keyword_init: true) do
10
+ def lines
11
+ out = written.map { |path| "wrote #{path}" }
12
+ out << "bound #{bound.join(', ')}" unless bound.empty?
13
+ out << "left commented, bind when the directories exist: #{commented.join(', ')}" unless commented.empty?
14
+ out + notes
15
+ end
16
+ end
17
+
18
+ def initialize(root, tenant_column: nil, binary: nil, stderr: $stderr)
19
+ @root = File.expand_path(root)
20
+ @tenant_column = tenant_column
21
+ @binary = binary
22
+ @stderr = stderr
23
+ @report = Report.new(written: [], bound: [], commented: [], notes: [])
24
+ end
25
+
26
+ # The Rails layer first (starter laws, the binary's own init, upstream's
27
+ # recipes commented), then what only munola carries: the catalogue, the
28
+ # bindings the tree justifies, the providers both on by default.
29
+ # enola-rb carries the installer this reuses; it is required here rather
30
+ # than at load time so `require "munola"` stays a wrapper and pulls in no
31
+ # Rails machinery for a project that only drives the binary.
32
+ def install
33
+ require "enola-rb"
34
+ rails = EnolaRb::Installer.new(@root, binary: resolved_binary, stderr: @stderr).install
35
+ @report.written.concat(rails.written)
36
+ @report.notes.concat(rails.notes)
37
+
38
+ detector = Detector.new(@root)
39
+ tenant = detector.tenant(column: @tenant_column)
40
+ values = tenant&.table ? { column: tenant.column, table: tenant.table } : nil
41
+ @report.written.concat(Catalogue.write(@root, tenant: values).map { |p| relative(p) })
42
+ bind(detector.bindings + (tenant&.table ? ["tenant-foreign-key"] : []))
43
+ note_tenant(tenant)
44
+ providers
45
+ @report.bound = (rails.bound + @bound).uniq
46
+ @report.commented = rails.unbound + @commented
47
+ @report
48
+ end
49
+
50
+ private
51
+
52
+ def resolved_binary
53
+ @binary ||= Munola.resolve.path
54
+ rescue Enola::Error
55
+ nil
56
+ end
57
+
58
+ def bindings_path
59
+ File.join(@root, "enola", "constraints", "recipes.yaml")
60
+ end
61
+
62
+ # A munola binary carries the catalogue itself, so its own `constraints
63
+ # init` may already have bound a recipe this would bind again, and two
64
+ # instances of one recipe expand to colliding rule ids. What the file
65
+ # already names is left alone and counted as bound.
66
+ def bind(names)
67
+ @bound = names & Catalogue::RECIPES
68
+ @commented = Catalogue::RECIPES - @bound
69
+ existing = File.exist?(bindings_path) ? File.read(bindings_path) : "use_recipe: []\n"
70
+ return if existing.include?("# munola catalogue")
71
+
72
+ doc = YAML.safe_load(existing) || {}
73
+ declared = Array(doc["use_recipe"]).map { |entry| entry["recipe"] }.compact
74
+ entries = Array(doc["use_recipe"]) + (@bound - declared).map { |name| { "recipe" => name, "as" => name, "mode" => "advisory" } }
75
+ body = YAML.dump({ "use_recipe" => entries }).sub(/\A---\n/, "")
76
+ tail = existing[/\n# Shipped recipes not bound.*\z/m].to_s
77
+ commented = @commented.map { |name| "# - recipe: #{name}\n# as: #{name}\n# mode: advisory\n" }.join
78
+ File.write(bindings_path, "#{body}#{tail}\n# munola catalogue recipes not bound because the tree shows no need for them yet.\n" \
79
+ "# Uncomment one to switch it on, then run `enola constraints lint`.\n#{commented}")
80
+ @report.written << relative(bindings_path) unless @report.written.include?(relative(bindings_path))
81
+ end
82
+
83
+ def note_tenant(tenant)
84
+ if tenant&.table
85
+ note("tenant column #{tenant.column} on #{(tenant.share * 100).round}% of tables, referencing #{tenant.table}; tenant-foreign-key written with it")
86
+ elsif tenant && tenant.share.zero?
87
+ note("#{tenant.column} appears on no table in db/schema.rb; tenant-foreign-key keeps its placeholders")
88
+ elsif tenant
89
+ note("tenant column #{tenant.column} found but no table named for it in the schema; tenant-foreign-key keeps its TENANT_TABLE placeholder")
90
+ elsif @tenant_column
91
+ note("#{@tenant_column} appears on no table in db/schema.rb; tenant-foreign-key keeps its placeholders")
92
+ else
93
+ note("no column sits on most tables; tenant-foreign-key keeps its placeholders, pass --tenant-column to name one")
94
+ end
95
+ end
96
+
97
+ def providers
98
+ path = ProvidersConfig.write(@root)
99
+ if path
100
+ @report.written << relative(path)
101
+ note("prism provider: the enola gem carries no provider script yet; mcp-arch.yaml names the slot") unless ProvidersConfig.prism_script
102
+ end
103
+ binary = resolved_binary
104
+ note(binary ? ProvidersConfig.fetch_rubydex(binary) : "rubydex library not fetched: no binary resolved; run `enola providers fetch rubydex` once it is")
105
+ end
106
+
107
+ def relative(path)
108
+ path.delete_prefix("#{@root}/")
109
+ end
110
+
111
+ def note(text)
112
+ @report.notes << text
113
+ @stderr.puts "munola: #{text}"
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "rbconfig"
5
+
6
+ module Munola
7
+ # Every munola project runs both Ruby providers by default: Prism through the
8
+ # script the enola gem carries, Rubydex built into the binary. The config is
9
+ # written once, never rewritten; the Rubydex library is fetched after the
10
+ # binary, and a fetch that fails is a named skip, never a silent absence.
11
+ module ProvidersConfig
12
+ RUBYDEX_VERSION = "0.4.0"
13
+
14
+ def self.write(root, ruby: RbConfig.ruby)
15
+ path = File.join(root, "mcp-arch.yaml")
16
+ return nil if File.exist?(path)
17
+
18
+ File.write(path, render(ruby))
19
+ path
20
+ end
21
+
22
+ def self.render(ruby)
23
+ lines = ["repo: .", "repos:", " - .", "providers:"]
24
+ script = prism_script
25
+ if script
26
+ lines += [" - name: prism", " command: [#{ruby.inspect}, #{script.inspect}]", " expected_version: \"0.1.0\""]
27
+ else
28
+ lines << " # prism: the enola gem on this machine carries no provider script; add one here to run it"
29
+ end
30
+ lines += [" - name: rubydex", " expected_version: \"#{RUBYDEX_VERSION}\""]
31
+ "#{lines.join("\n")}\n"
32
+ end
33
+
34
+ def self.prism_script
35
+ return nil unless defined?(Enola::Providers) && Enola::Providers.respond_to?(:prism_script)
36
+
37
+ script = Enola::Providers.prism_script
38
+ script if script && File.exist?(script)
39
+ end
40
+
41
+ def self.fetch_rubydex(binary)
42
+ out, status = Open3.capture2e(binary, "providers", "fetch", "rubydex")
43
+ return "rubydex #{RUBYDEX_VERSION} library in place" if status.success?
44
+
45
+ "rubydex library not fetched (#{out.lines.last.to_s.strip}); the provider reads as a named skip until `enola providers fetch rubydex` succeeds"
46
+ rescue SystemCallError => e
47
+ "rubydex library not fetched (#{e.message}); the provider reads as a named skip until `enola providers fetch rubydex` succeeds"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Munola
4
+ # UPSTREAM_VERSION names the enola release this channel is built on,
5
+ # CHANNEL_VERSION the fork release the gem fetches.
6
+ UPSTREAM_VERSION = "0.4.4"
7
+ CHANNEL_VERSION = "0.4.4.2"
8
+ VERSION = "0.5.0"
9
+ end
data/lib/munola.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "enola"
4
+ require_relative "munola/version"
5
+ require_relative "munola/channel"
6
+ require_relative "munola/catalogue"
7
+ require_relative "munola/detector"
8
+ require_relative "munola/providers_config"
9
+ require_relative "munola/installer"
10
+ require_relative "munola/cli"
11
+
12
+ Enola.channel = Munola::CHANNEL
13
+ Enola.resolver_factory = -> { Munola::Resolver.new(channel: Munola::CHANNEL) }
14
+
3
15
  module Munola
4
- VERSION = "0.0.0"
5
16
  end
metadata CHANGED
@@ -1,29 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: munola
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muhamed Isabegovic
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-21 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2026-08-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: enola
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: enola-rb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: enola-guides
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.1
13
55
  description: |
14
- Carries a build of enola, the architecture-graph tool, for projects that
15
- want capabilities not yet in an upstream release. Installing it alongside
16
- enola-rb switches that gem from the stock binary to this one. Placeholder
17
- release; no binary yet.
56
+ The same pure-Ruby wrapper as the enola gem over another channel: the
57
+ builds cut from a fork of enola, each release naming what differs. This
58
+ gem drives channel release 0.4.4.2, built on enola
59
+ 0.4.4. `munola init` writes the recipe catalogue the
60
+ enola-guides gem carries (Ember, data ownership, API boundaries,
61
+ background work, a tenant foreign key) into the project and binds the
62
+ recipes its tree justifies, and turns both Ruby providers on by default. The Rails generator lives in munola-rb. Offered
63
+ upstream where it fits; no binary here, nothing compiled.
18
64
  email:
19
65
  - m.isabegovic@hotmail.com
20
- executables: []
66
+ executables:
67
+ - munola
21
68
  extensions: []
22
69
  extra_rdoc_files: []
23
70
  files:
71
+ - CHANGELOG.md
24
72
  - LICENSE
25
73
  - README.md
74
+ - exe/munola
26
75
  - lib/munola.rb
76
+ - lib/munola/catalogue.rb
77
+ - lib/munola/channel.rb
78
+ - lib/munola/cli.rb
79
+ - lib/munola/detector.rb
80
+ - lib/munola/installer.rb
81
+ - lib/munola/providers_config.rb
82
+ - lib/munola/version.rb
27
83
  homepage: https://github.com/misabegovic/enola-rb
28
84
  licenses:
29
85
  - Apache-2.0
@@ -48,5 +104,6 @@ requirements: []
48
104
  rubygems_version: 3.5.22
49
105
  signing_key:
50
106
  specification_version: 4
51
- summary: Ships the enola build that enola-rb runs when upstream lacks a capability.
107
+ summary: 'One person''s taste on top of enola: a channel of its own and a recipe catalogue
108
+ bound by what the tree shows.'
52
109
  test_files: []