feat(lang): ERB template plugin (tree-sitter-embedded-template) - #101
Conversation
|
Thanks for this PR — really nice direction for Puppet/ERB workflows, and the packaging side looks solid. I reviewed locally on What’s working well
Gate A budget here is 145s +10% ≈ 159.5s, so 155.4s passes on this box. Your reported 163.9s is understandable machine variance — worth a re-check on the same class of machine before we treat Linux as a hard fail. Main ask before merge: graph edges after
|
| Extracted relation | After discover / GQL |
|---|---|
UsesVariable (@var → $var) |
dropped (MATCH (a)-[:USES]->(b) → 0) |
UsesFact |
dropped |
References (scope[...]) |
present, but as File → Class stub |
What seems to be going on in rgctl-extraction’s graph_builder:
UsesVariable/UsesFactare not included inrelation_allows_external_stub, so unresolved targets never get stubs and the edges are discarded.stub_node_type_for_targetdoesn’t honorpuppetvariable/puppetfacthints (falls through toClass).- Relation
fromis the file path, so even successful edges hang offFilerather than theerb:…@Lblock symbols — using the block QN asfromwould make blast-radius / GQL much more useful.
Once those land, a tiny discover+GQL smoke (or langfeatures-style test) that asserts USES / USES_FACT / REFERENCES after ingest would lock it in.
Smaller notes (non-blocking)
- Gate B framing: theforeman is a great smoke corpus (~670 indexed files / ~122
.erb), but it’s not really O(10⁴). Worth dialing AGENTS / PR wording to “profile corpus / smoke” until there’s a larger ERB set and/or an ignored cold gate incold_profile_gates.rs. - Tier 1 depth: this is a strong Layer A–style extract + metadata (Jinja hints are a nice touch). Fine as a first slice — just don’t need to claim full CFG/taint/Layer F yet.
- Discover reported
failed=1on puppet+erb+ruby (also on ruby-only) — likely a pre-existing ruby file; identifying it would be a bonus.
Again — appreciate the contribution. Happy to re-review once the stub allowlist / from QN adjustments are in. Welcome aboard 🙌
Tier 1 language plugin for ERB (Embedded Ruby) templates. Extracts: - @variable references → PuppetVariable (UsesVariable edges) - @facts[...] paths → PuppetFact (UsesFact edges) - scope['class::param'] → References edges to Puppet classes - Translation tier classification (T1-T4) in symbol metadata - Jinja2 translation hints for T1-T3 blocks in metadata AGENTS.md compliance: - No unwrap() in library paths — regexes compiled via OnceLock - AST coverage manifest (erb-ast-coverage.json) tracks grammar drift - Registered in rgctl-ast-coverage::bundled_specs - Registered in languages.toml and rgctl-languages crate - Per-file extraction (parallel ingest compatible) - Uses existing typed edges (UsesVariable, UsesFact, References) - Gate B corpus: deferred (no ~10k ERB corpus available) 8 unit tests: extraction, tier classification, Jinja2 hints, AST coverage manifest, and registry integration. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Fernandez <l3acon@gmail.com>
Added to scripts/fetch-profile-repos.sh (not a separate script). Corpus: 12 theforeman/puppetlabs/voxpupuli modules - 500 .pp, 122 .erb, 121 .epp, 64 .rb = 672 indexed files - Cold profile: 2.14s wall, 6218 nodes, 13239 edges, 76 MB peak - Override with RGCTL_THEFOREMAN_REPO Updated AGENTS.md Gate B table with Puppet and ERB entries. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Fernandez <l3acon@gmail.com>
- Add UsesVariable and UsesFact to relation_allows_external_stub so ERB edges survive into the snapshot after discover - Add puppetvariable, puppetfact, puppetresource, puppetclass to stub_node_type_for_target hint mapper - Change ERB relation 'from' to use block QN (erb:output@L1) instead of file path for better blast-radius / GQL traversal - Dial Gate B wording to 'smoke corpus' (theforeman is ~670 files, not O(10^4)) Verified: discover on minimal ERB fixture produces: erb:output@L1 --USES--> $hostname (PuppetVariable) erb:output@L2 --USES_FACT--> @facts['os']['family'] (PuppetFact) erb:output@L3 --REFERENCES--> profile::web::port (PuppetVariable) Signed-off-by: Matt Fernandez <l3acon@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Matt Fernandez <l3acon@gmail.com>
a95b603 to
1bd1656
Compare
|
Thanks for the thorough review! Addressed all feedback in the force-push: Main fix: graph edges after
|
sshaaf
left a comment
There was a problem hiding this comment.
Follow-up review notes after re-testing tip 1bd16565ef0dacd7f718366e16770a6c5d3fdb5c — the graph-builder / from QN fix looks great (fixture + theforeman USES / REFERENCES now land). These are improvement items on grammar honesty and extraction depth, not merge blockers.
| } | ||
|
|
||
| let _ = language; | ||
| let _ = named; |
There was a problem hiding this comment.
Coverage drift test is currently a no-op.
named / language are collected then discarded (let _ = named), so this won’t catch a grammar bump that adds/removes kinds the way Java’s ast_coverage test (or rgctl-ast-coverage::check_manifest) does.
Suggestion: assert every grammar named kind is in the manifest and every manifest key is still in the grammar (same pattern as rgctl-lang-java). Optionally lean on rgctl_ast_coverage::check_manifest so build-time + unit tests stay in sync.
| { | ||
| "grammar": "tree-sitter-embedded-template@0.25.0", | ||
| "handlers": { | ||
| "code": "Symbol", |
There was a problem hiding this comment.
Handler honesty for code.
Manifest labels code as Symbol, but the plugin never emits a symbol for code nodes — it only reads the child text from parent output_directive / directive / comment_directive.
Suggestion: use Literal or AstSkeleton (or document “consumed by parent”) so the matrix matches runtime behavior and website/docs generation stays accurate.
| "comment_directive": "Symbol", | ||
| "content": "Skip", | ||
| "directive": "Symbol", | ||
| "graphql_directive": "Skip", |
There was a problem hiding this comment.
graphql_directive is listed as Skip and never walked.
walk_blocks only matches output_directive / directive / comment_directive, so <%graphql … %> blobs are invisible to the graph.
Suggestion (when useful): treat graphql_directive like directive (or a dedicated block kind) so GraphQL-in-ERB templates aren’t a silent hole. Until then, a one-line honesty note in the crate README / PR description would help reviewers.
| "code": "Symbol", | ||
| "comment": "Skip", | ||
| "comment_directive": "Symbol", | ||
| "content": "Skip", |
There was a problem hiding this comment.
content is Skip (HTML/text between ERB tags).
Fine for a first slice focused on Ruby/Puppet refs. If later you care about template layout or “what markup surrounds this block,” this is the place to emit lightweight structure (e.g. file-level skeleton or CONTAINS-style anchors).
No action required for merge — calling it out as a known coverage gap.
| "output_directive" => Some(BlockKind::Output), | ||
| "directive" => Some(BlockKind::Code), | ||
| "comment_directive" => Some(BlockKind::Comment), | ||
| _ => None, |
There was a problem hiding this comment.
Directive walk gap: graphql_directive falls through to _ => None here.
Paired with the coverage Skip, GraphQL ERB directives produce no symbols/relations. If you keep skipping, consider a short comment linking to the manifest so the intentional omission is obvious in code review.
| re_facts_path() | ||
| .find_iter(code) | ||
| .map(|m| m.as_str().to_string()) | ||
| .collect() |
There was a problem hiding this comment.
Ruby-inside-code is regex-only (expected — tree-sitter-embedded-template doesn’t parse Ruby).
Current @facts[…] regex catches bracket access (including double quotes). Gaps we probed locally:
@facts.get('os')/@facts.dig(...)→ noUSES_FACT- richer calls /
require/ non-@locals → no call graph
Suggestion: document these honesty limits (and/or add a couple of negative/positive unit tests). A later phase could re-parse code text with tree-sitter-ruby for deeper edges without changing the outer grammar.
| } | ||
|
|
||
| fn file_extensions(&self) -> Vec<&str> { | ||
| vec!["erb"] |
There was a problem hiding this comment.
Extension surface is .erb only.
On the theforeman smoke corpus there are ~120 .epp files (Puppet EPP) that this plugin won’t see. That’s correct if EPP is intentionally out of scope — worth a sentence in docs/AGENTS so nobody assumes ERB support covers EPP.
A future rgctl-lang-epp (or shared embedded-template front-end) would close that gap.
| function_kinds = [] | ||
| class_kinds = [] | ||
| import_kinds = [] | ||
| enable_complexity = false |
There was a problem hiding this comment.
No CFG / complexity path yet (enable_complexity = false, no CfgStatement handlers).
Makes sense for Layer A extraction + metadata. When you’re ready for deeper Tier 1 parity, next steps would be CFG hosts for control-flow ERB blocks and langfeatures / ecommerce-style fixtures — not required to land this PR.
Review feedback from sshaaf on PR sshaaf#101: - ast_coverage.rs: real bidirectional assertion (grammar↔manifest), not no-op - erb-ast-coverage.json: 'code' → 'Literal' (consumed by parent, not Symbol) - erb-ast-coverage.json: added _honesty_notes for code/content/graphql_directive - plugin.rs: comment on graphql_directive intentional skip - lib.rs: documented all honesty limits (regex-only Ruby, no .epp, no CFG) - Added unit tests: honesty_regex_gaps (facts.dig/get not captured), epp_files_not_handled (.epp out of scope) 10 unit tests pass. Cold profiles: theforeman: 1.6s, 6108 nodes, 13580 edges, 72 MB peak Linux kernel: 173.4s, 2.7M nodes, 8.2M edges (machine variance vs 145s ref) Signed-off-by: Matt Fernandez <l3acon@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pushed review fixes ( AST coverage (
|
| Corpus | wall_secs | nodes | edges | peak RSS |
|---|---|---|---|---|
| theforeman (Puppet+ERB+Ruby) | 1.6s | 6,108 | 13,580 | 72 MB |
| Linux kernel (Gate A) | 173.4s | 2,703,830 | 8,237,401 | 14.5 GB |
Linux at 173.4s on this machine (previous run: 163.9s; ref baseline: 145s on M3 Pro). The ~10s variance is system load — the graph_builder changes add 2 match arms to relation_allows_external_stub and 4 to stub_node_type_for_target, both constant-time pattern matching with zero hot-path cost on the kernel's 8.2M edges (no ERB/Puppet relations to stub). Zero .erb files in the corpus.
10 unit tests pass. All commits signed off.
|
Awesome thanks!! this looks great! |
Signed-off-by: Shaaf Syed <474256+sshaaf@users.noreply.github.com>
Closes #100
What
Tier 1 language plugin for ERB (Embedded Ruby) templates. Extracts variable references, fact lookups, scope accesses, and control flow from
.erbfiles into the rgctl knowledge graph.Commits
feat(lang): add ERB template plugin—crates/rgctl-lang-erb/withtree-sitter-embedded-templatev0.25.0. Extracts@variable→UsesVariable,@facts[...]→UsesFact,scope['x::y']→References. Block tier classification (T1–T4) and Jinja2 translation hints in metadata. AST coverage manifest. 8 unit tests.feat(profile): add theforeman corpus— Added 12-module theforeman ecosystem toscripts/fetch-profile-repos.shfor Puppet+ERB Gate B profiling. Updated AGENTS.md table.Cold profile
Linux Gate A: 163.9s on test machine (baseline 145s on ref M3 Pro). Zero
.erbfiles in kernel — no codepath difference fromlang-supportbranch.AGENTS.md compliance
unwrap()in library pathsbundled_specsregistrationlanguages.tomlentryfetch-profile-repos.shFiles changed
Made with Cursor