Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
10cbefd
docs: align public launch positioning
pboling May 4, 2026
2f3220c
Align Ruby gem versions for fixture cleanup
pboling May 4, 2026
58e6f52
Add Ruby coverage for policy alignment slices
pboling May 4, 2026
edb2ee7
Align Ruby fixtures to runtime facts
pboling May 4, 2026
0c4486e
Add Ruby coverage for CHANGELOG normalization slice
pboling May 4, 2026
e9aecf0
Add Ruby coverage for README metadata slice
pboling May 4, 2026
fbe4e08
Add Ruby coverage for Markdown pruning slice
pboling May 4, 2026
1aec8fb
Add Ruby coverage for selector deletion slice
pboling May 4, 2026
49e842d
Add Ruby coverage for YAML snippet synchronization
pboling May 5, 2026
94f122a
Add Ruby coverage for managed text blocks
pboling May 5, 2026
da820de
Add Ruby coverage for YAML placeholder backfill
pboling May 5, 2026
5fbd13a
Move content recipe builders to fixtures
pboling May 5, 2026
bb67841
Add kettle-jem vNext thin slice
pboling May 5, 2026
e0d190f
Assert packaging contract in kettle-jem
pboling May 5, 2026
ba41b2f
Add kettle-jem funding metadata facts
pboling May 5, 2026
0fdbe30
Add kettle-jem Rakefile scaffold cleanup
pboling May 5, 2026
0874003
Add kettle-jem GitHub Actions CI recipe
pboling May 5, 2026
c7d5bf1
Add kettle-jem framework matrix workflow
pboling May 5, 2026
7a148b6
Add kettle-jem workflow snippet merging
pboling May 5, 2026
08a9150
Add kettle-jem coverage workflow snippets
pboling May 5, 2026
5ccfba0
Add kettle-jem generated coverage workflow
pboling May 5, 2026
1bbab5e
Add kettle-jem obsolete workflow cleanup
pboling May 5, 2026
adbaad9
Add kettle-jem FUNDING.yml sync
pboling May 5, 2026
c2ac3f8
Add kettle-jem Open Collective funding policy
pboling May 5, 2026
2219365
Add kettle-jem Open Collective file cleanup
pboling May 5, 2026
dc5d6a2
Add kettle-jem no-osc template preference
pboling May 5, 2026
ca2adf7
Add kettle-jem Open Collective env compatibility
pboling May 5, 2026
921b586
Add kettle-jem Open Collective org discovery
pboling May 5, 2026
bb91ef1
Add kettle-jem Open Collective token projection
pboling May 5, 2026
d504dce
Add kettle-jem template token application
pboling May 5, 2026
f5f7695
Add kettle-jem package template tokens
pboling May 5, 2026
df419db
Add kettle-jem minimum Ruby template token
pboling May 5, 2026
e8377d9
Use token resolver for kettle-jem templates
pboling May 5, 2026
51a6b25
Add kettle-jem author template tokens
pboling May 5, 2026
d2b8436
Add kettle-jem author token overrides
pboling May 5, 2026
83a7e54
Add kettle-jem author ORCID token
pboling May 5, 2026
ee1a850
Add kettle-jem forge user tokens
pboling May 5, 2026
10315c4
Add kettle-jem funding platform tokens
pboling May 5, 2026
436dc8c
Add kettle-jem social tokens
pboling May 5, 2026
4948274
Add kettle-jem license template tokens
pboling May 5, 2026
a5b4e01
Add kettle-jem project runtime tokens
pboling May 5, 2026
ac4c253
Add kettle-jem README logo tokens
pboling May 5, 2026
6b0e274
Add kettle-jem RuboCop template tokens
pboling May 5, 2026
8b5aa26
Add kettle-jem packaged template root
pboling May 5, 2026
576acdc
Package reference kettle-jem templates
pboling May 5, 2026
bf841d5
Bootstrap kettle-jem config from templates
pboling May 5, 2026
4f547f3
Add kettle-jem template strategy lookup
pboling May 5, 2026
16ad691
Use packaged RuboCop style template
pboling May 5, 2026
28c2b27
Add kettle-jem bundle gem system spec
pboling May 5, 2026
1cef740
Plan kettle-jem packaged template inventory
pboling May 5, 2026
b3307b3
Preserve README sections during template merge
pboling May 5, 2026
1a13f2b
Exercise README preservation in kettle-jem system spec
pboling May 5, 2026
d054298
Merge kettle-jem YAML and TOML templates
pboling May 5, 2026
ec0147e
Support YAML mapping sequences in template merge
pboling May 5, 2026
2b92ec0
Exercise packaged Dependabot YAML merge
pboling May 5, 2026
d2eb799
Merge kettle-jem Ruby-family templates
pboling May 5, 2026
7f71b14
Merge Ruby top-level DSL calls
pboling May 5, 2026
f3b44bb
Exercise packaged Gemfile merge
pboling May 5, 2026
1116980
Exercise packaged Rakefile merge
pboling May 5, 2026
d477fb2
Check stable packaged template idempotency
pboling May 5, 2026
f16d584
Tighten README heading preservation
pboling May 5, 2026
65753b3
Align system sandbox with kettle-jem reference
pboling May 5, 2026
6b0cada
Align selected template reruns with reference
pboling May 5, 2026
4d692d9
Use packaged LICENSE template in token spec
pboling May 5, 2026
b637e32
Use packaged FUNDING source preference templates
pboling May 5, 2026
9159be2
Use packaged cert strategy template
pboling May 5, 2026
07e69cc
Apply omitted packaged template inventory
pboling May 5, 2026
8b0f5fa
Preserve gemspec fields during template apply
pboling May 5, 2026
28e3bad
Add compact ruleset parser
pboling May 5, 2026
5fb2fc3
Preserve map key order during merges
pboling May 5, 2026
37e4441
Merge main into public launch alignment
pboling May 5, 2026
e3d5b28
Fix packaged scaffold hidden file expectations
pboling May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source "https://rubygems.org"
gemspec path: "gems/tree_haver"
gemspec path: "gems/ast-merge"
gemspec path: "gems/ast-template"
gemspec path: "gems/kettle-jem"
gemspec path: "gems/text-merge"
gemspec path: "gems/json-merge"
gemspec path: "gems/toml-merge"
Expand Down
44 changes: 28 additions & 16 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ PATH
PATH
remote: gems/ast-template
specs:
ast-template (0.0.0)
ast-merge (= 0.0.0)
ast-template (0.1.0)
ast-merge (= 0.1.0)

PATH
remote: gems/citrus-toml-merge
Expand All @@ -20,9 +20,9 @@ PATH
PATH
remote: gems/commonmarker-merge
specs:
commonmarker-merge (0.0.0)
commonmarker-merge (0.1.0)
commonmarker (~> 2.2)
markdown-merge (= 0.0.0)
markdown-merge (= 0.1.0)

PATH
remote: gems/go-merge
Expand All @@ -38,25 +38,35 @@ PATH
ast-merge (= 0.1.0)
tree_haver (= 0.1.0)

PATH
remote: gems/kettle-jem
specs:
kettle-jem (0.1.0)
ast-merge (= 0.1.0)
ruby-merge (= 0.1.0)
token-resolver (~> 1.0, >= 1.0.2)
toml-merge (= 0.1.0)
yaml-merge (= 0.1.0)

PATH
remote: gems/kramdown-merge
specs:
kramdown-merge (0.0.0)
kramdown-merge (0.1.0)
kramdown (~> 2.5)
markdown-merge (= 0.0.0)
markdown-merge (= 0.1.0)

PATH
remote: gems/markdown-merge
specs:
markdown-merge (0.0.0)
ast-merge (= 0.0.0)
tree_haver (= 0.0.0)
markdown-merge (0.1.0)
ast-merge (= 0.1.0)
tree_haver (= 0.1.0)

PATH
remote: gems/markly-merge
specs:
markly-merge (0.0.0)
markdown-merge (= 0.0.0)
markly-merge (0.1.0)
markdown-merge (= 0.1.0)
markly (~> 0.9)

PATH
Expand Down Expand Up @@ -177,6 +187,7 @@ DEPENDENCIES
commonmarker-merge!
go-merge!
json-merge!
kettle-jem!
kramdown-merge!
markdown-merge!
markly-merge!
Expand All @@ -196,19 +207,20 @@ DEPENDENCIES

CHECKSUMS
ast-merge (0.1.0)
ast-template (0.0.0)
ast-template (0.1.0)
citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650
citrus-toml-merge (0.1.0)
commonmarker (2.8.1-x86_64-linux) sha256=c8f2e903c6ee4e2c7e280aa8b49ef37c53202d388e3a2ee06dfdccc8c6fb634f
commonmarker-merge (0.0.0)
commonmarker-merge (0.1.0)
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
go-merge (0.1.0)
json-merge (0.1.0)
kettle-jem (0.1.0)
kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa
kramdown-merge (0.0.0)
markdown-merge (0.0.0)
kramdown-merge (0.1.0)
markdown-merge (0.1.0)
markly (0.16.0) sha256=6f70d79e385b1efc9e171f74c81628826259039fe6c778e03c3924c71dac5511
markly-merge (0.0.0)
markly-merge (0.1.0)
parslet (2.0.0) sha256=d45130695d39b43d7e6a91f4d2ec66b388a8d822bae38de9b4de9a5fbde1f606
parslet-toml-merge (0.1.0)
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
Expand Down
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
# Structured Merge Ruby
# StructuredMerge Ruby

Monorepo for the new Ruby implementation of the Structured Merge library
family.
Ruby implementation of the StructuredMerge contract.

This repository is a fresh implementation aligned to the current
cross-language spec and fixture corpus. The older Ruby gems in this workspace
remain reference material only.
This repository is one of four peer launch implementations: [Go](https://github.com/structuredmerge/structuredmerge-go), [TypeScript](https://github.com/structuredmerge/structuredmerge-typescript), [Rust](https://github.com/structuredmerge/structuredmerge-rust), and [Ruby](https://github.com/structuredmerge/structuredmerge-ruby). The language repos are not separate products. They consume the same public spec and shared fixture corpus so tools can choose the runtime surface that fits their environment.

Initial planned Ruby packages:
Project links:

- Website: <https://structuredmerge.org>
- Implementations overview: <https://structuredmerge.org/implementations.html>
- Conformance model: <https://structuredmerge.org/conformance.html>
- Specification: <https://github.com/structuredmerge/structuredmerge-spec>
- Shared fixtures: <https://github.com/structuredmerge/structuredmerge-fixtures>

## Workspace

This is a Ruby monorepo for StructuredMerge packages.

Initial packages:

- `tree-haver`
- `ast-merge`
- `text-merge`
- `json-merge`
- `toml-merge`
- `yaml-merge`
- `typescript-merge`
- `rust-merge`
- `go-merge`
- `ruby-merge`
- source-family packages for TypeScript, Rust, Go, and Ruby cases

## Conformance

Integration tests should consume the shared fixture corpus from the sibling `../structuredmerge-fixtures` checkout. A ruleset, fixture, diagnostic shape, or review outcome should mean the same thing whether exercised through Go, TypeScript, Rust, or Ruby.

Use the spec repository's conformance matrix for the current launch-readiness snapshot:

- <https://github.com/structuredmerge/structuredmerge-spec/blob/main/conformance-matrix.md>
- <https://github.com/structuredmerge/structuredmerge-spec/blob/main/IMPLEMENTATION_STATUS.md>

## Development

This repository follows the same slice-by-slice conformance path used by the
TypeScript, Rust, and Go monorepos.
Standard repo tasks are exposed through `mise` and native Ruby tooling.

Common checks:

- `mise run check`
- `bundle exec rake` or package-specific tests

Bundler path gems are the default isolation mechanism inside this monorepo. When this repository needs to consume sibling workspace projects outside the monorepo itself, prefer `nomono`-driven Bundler wiring rather than manual Ruby load-path changes.

Integration tests should consume the shared fixture corpus from the sibling
`../fixtures` repository rather than copying fixture data into this monorepo.
## Status

Bundler path gems are the default isolation mechanism inside this monorepo.
When this repository needs to consume sibling workspace projects outside the
monorepo itself, prefer `nomono`-driven Bundler wiring rather than manual Ruby
load-path changes.
Early implementation work. Public compatibility claims should be tied to shared fixtures and documented conformance status rather than runtime-specific assumptions.
138 changes: 93 additions & 45 deletions gems/ast-merge/lib/ast/merge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ module Merge
REVIEW_TRANSPORT_VERSION = 1
STRUCTURED_EDIT_TRANSPORT_VERSION = 1
TEMPLATE_TOKEN_CONFIG = Token::Resolver::Config.new(separators: ["|", ":"]).freeze
COMPACT_RULESET_REQUIRED_DIRECTIVES = %w[format owners match read attach].freeze
COMPACT_RULESET_SINGLETON_DIRECTIVES = %w[format owners match read attach comment_style render].freeze
COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES = %w[capability logical_owner repair surface delegate].freeze
COMPACT_RULESET_READ_VALUES = %w[source_augmented_portable_write native_read_portable_write native_mutation].freeze
COMPACT_RULESET_ATTACH_VALUES = %w[
layout_only
tracker_layout_merge
augmenter_preferred_tracker_layout
normalize_tracked_layout_merge
].freeze

module_function

Expand All @@ -28,6 +38,68 @@ def conformance_family_feature_profile_path(manifest, family)
entry && deep_dup(entry[:path])
end

def parse_compact_ruleset(source)
ruleset = { directives: [], comments: [] }
diagnostics = []
seen_directives = {}
seen_repeatable_keys = {}

source.to_s.split("\n").each_with_index do |raw_line, index|
line_number = index + 1
line = raw_line.strip
next if line.empty?

if line.start_with?("#")
ruleset[:comments] << line
next
end

name, *arguments = line.split(/\s+/)
path = line_number.to_s
unless compact_ruleset_identifier?(name)
diagnostics << compact_ruleset_diagnostic("invalid directive token #{name.inspect}", path)
next
end
unless compact_ruleset_known_directive?(name)
diagnostics << compact_ruleset_diagnostic("unknown directive #{name.inspect}", path)
next
end
if arguments.empty?
diagnostics << compact_ruleset_diagnostic("directive #{name.inspect} requires at least one argument", path)
next
end

arguments.each do |argument|
next if %w[true false].include?(argument) || compact_ruleset_identifier?(argument) || compact_ruleset_token?(argument)

diagnostics << compact_ruleset_diagnostic("invalid argument token #{argument.inspect}", path)
end

if COMPACT_RULESET_SINGLETON_DIRECTIVES.include?(name) && seen_directives.key?(name)
diagnostics << compact_ruleset_diagnostic(
"repeated singleton directive #{name.inspect} first seen on line #{seen_directives.fetch(name)}",
path
)
end
if COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES.include?(name)
key = [name, arguments.fetch(0)]
diagnostics << compact_ruleset_diagnostic("repeated #{name.inspect} key #{arguments.fetch(0).inspect}", path) if seen_repeatable_keys[key]
seen_repeatable_keys[key] = true
end
diagnostics << compact_ruleset_diagnostic("unknown read value #{arguments.fetch(0).inspect}", path) if name == "read" && !COMPACT_RULESET_READ_VALUES.include?(arguments.fetch(0))
diagnostics << compact_ruleset_diagnostic("unknown attach value #{arguments.fetch(0).inspect}", path) if name == "attach" && !COMPACT_RULESET_ATTACH_VALUES.include?(arguments.fetch(0))

seen_directives[name] = line_number
ruleset[:directives] << { name: name, arguments: arguments, line: line_number }
end

COMPACT_RULESET_REQUIRED_DIRECTIVES.each do |required|
diagnostics << compact_ruleset_diagnostic("missing required directive #{required.inspect}") unless seen_directives.key?(required)
end

diagnostics.empty? ? { ok: true, diagnostics: [], analysis: ruleset, policies: [] } : { ok: false, diagnostics: diagnostics, policies: [] }
end

def normalize_template_source_path(path)
return path.delete_suffix(".no-osc.example") if path.end_with?(".no-osc.example")
return path.delete_suffix(".example") if path.end_with?(".example")
Expand Down Expand Up @@ -1040,51 +1112,6 @@ def structured_edit_provider_execution_request(request:, provider_family:, provi
execution_request
end

def content_recipe_execution_request(recipe_name:, recipe_version:, relative_path:, provider_family:,
template_content:, destination_content:, steps:, provider_backend: nil, runtime_context: nil, metadata: nil)
request = {
recipe_name: recipe_name.to_s,
recipe_version: recipe_version.to_s,
relative_path: relative_path.to_s,
provider_family: provider_family.to_s,
template_content: template_content.to_s,
destination_content: destination_content.to_s,
steps: deep_dup(steps)
}
request[:provider_backend] = provider_backend.to_s if provider_backend
request[:runtime_context] = deep_dup(runtime_context) if runtime_context
request[:metadata] = deep_dup(metadata) if metadata
request
end

def content_recipe_execution_request_envelope(request)
{
kind: "content_recipe_execution_request",
version: STRUCTURED_EDIT_TRANSPORT_VERSION,
request: deep_dup(request)
}
end

def content_recipe_execution_report(request:, final_content:, changed:, step_reports:, diagnostics:, metadata: nil)
report = {
request: deep_dup(request),
final_content: final_content.to_s,
changed: changed ? true : false,
step_reports: deep_dup(step_reports),
diagnostics: deep_dup(diagnostics)
}
report[:metadata] = deep_dup(metadata) if metadata
report
end

def content_recipe_execution_report_envelope(report)
{
kind: "content_recipe_execution_report",
version: STRUCTURED_EDIT_TRANSPORT_VERSION,
report: deep_dup(report)
}
end

def structured_edit_provider_execution_request_envelope(execution_request)
{
kind: "structured_edit_provider_execution_request",
Expand Down Expand Up @@ -3392,6 +3419,27 @@ def diagnostic(severity, category, message, path: nil, review: nil)
end
private_class_method :diagnostic

def compact_ruleset_identifier?(value)
value.to_s.match?(/\A[A-Za-z][A-Za-z0-9_.-]*\z/)
end
private_class_method :compact_ruleset_identifier?

def compact_ruleset_token?(value)
value.to_s.match?(/\A[\x21\x24-\x7e]+\z/)
end
private_class_method :compact_ruleset_token?

def compact_ruleset_known_directive?(value)
COMPACT_RULESET_SINGLETON_DIRECTIVES.include?(value) ||
COMPACT_RULESET_REPEATABLE_KEYED_DIRECTIVES.include?(value)
end
private_class_method :compact_ruleset_known_directive?

def compact_ruleset_diagnostic(message, path = nil)
diagnostic("error", "configuration_error", message, path: path)
end
private_class_method :compact_ruleset_diagnostic

def join_comma(values)
values.join(", ")
end
Expand Down
Loading