Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions .github/workflows/lockstep-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: lockstep-check

# The gemspec derives its interscript-maps constraint from this gem's
# own version. When maps ships a new minor, bundler resolution fails
# everywhere with an opaque exit code — that ran silent for two weeks
# (2026-08-26 to 2026-09-08). This probe makes the drift a named,
# weekly alert instead.
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:

jobs:
resolve:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Clone the maps corpus as the path dependency
run: git clone --depth 1 https://github.com/interscript/maps ../maps
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
- name: Report both version numbers
run: |
echo "gem: $(ruby -Ilib -e 'require "interscript/version"; print Interscript::VERSION')"
echo "maps: $(ruby -e 'print File.read("../maps/interscript-maps.gemspec")[/INTERSCRIPT_MAPS_VERSION = "([^"]+)"/, 1]')"
- name: Resolve the lockfile against the corpus
run: bundle lock
Comment on lines +15 to +28
4 changes: 4 additions & 0 deletions .github/workflows/rake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# The Gemfile's path dependency needs ../maps to exist for bundler
# to resolve; linting itself never loads it. actions/checkout
# cannot write outside the workspace, hence the plain clone.
- run: git clone --depth 1 https://github.com/interscript/maps ../maps
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
Expand Down
16 changes: 6 additions & 10 deletions .standard.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# StandardRB config — enforce Ruby style on lib/, leave specs/bin/exe flexible for now.
# Existing violations in lib/ were auto-fixed in this PR; remaining violations are
# tracked in TODO.complete/06-standardrb-ruby-gems.md.
# Deliberate-design exclusions, each with its reason:
# - utils/helpers.rb: spec-example bookkeeping uses intentional globals
# - compiler/ruby.rb: the compiler's job is eval'ing generated code;
# debug capture uses intentional globals
ignore:
- "bin/**/*"
- "exe/**/*"
- "spec/**/*"
- "docs/**/*"
- "reference-docs/**/*"
- "pkg/**/*"
- "vendor/**/*"
- lib/interscript/utils/helpers.rb
- lib/interscript/compiler/ruby.rb
37 changes: 37 additions & 0 deletions TODO.impl/12-isc-dependencies-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# [COMPLETE 2026-09-08 — repro spec RED then GREEN; deps wired, constraint key fixed, library-import semantics restored] 12 — Ruby adapter drops ISC dependencies (P1)

## Goal
`NodeAdapter#build` wires the ISC document's dependencies into the
Node::Document (dependencies, dep_aliases, lazily-loaded documents) so
`run map.<alias>.stage.<name>` works — unblocking PR #769 and the
corpus-era rspec failures.

## Why
Root cause of the 79-failure batch (named 2026-09-08): the adapter
sets metadata/tests/aliases/stages/name and silently drops
`dependencies` — every cross-map run in an `.isc` document then
dereferences nil (`reverse_run` on nil at interpreter.rb:78 via the
run branch). Local runs never saw it: they resolved the installed
`interscript-maps-2.4.3` gem (`.imp`) because the gem's load path
prefers installed map gems over siblings.

## Spec (TDD)

1. RED: a spec that forces the ISC corpus onto the load path
(`Interscript.load_path` prepended with the maps checkout),
transliterates `bgnpcgn-ukr-Cyrl-Latn-2019` "Антон Олегович", and
asserts "Anton Olehovych" — currently raises the nil crash.
2. GREEN: `build` constructs `Node::Dependency` per ISC dependency —
`full_name`, `name` (the alias, symbolized), `import = false` (ISC
v1 carries no import marker; alias-having deps are reached through
`dep_aliases`, which `import` does not gate), `document` parsed
through the same dispatch the compiler uses (`.isc` → parse_isc,
else DSL.parse) so chains resolve recursively.
3. `doc.dep_aliases[name] = dep` mirroring the DSL (document.rb:40).

## Acceptance
- The repro spec green; PR #769's gallery parity green against the
real corpus (not the stale gem).
- Corpus-census rspec run: failure count measured before/after,
remainder categorized in the PR body (this fix may not cure all 79;
what remains is named, not hidden).
19 changes: 19 additions & 0 deletions TODO.impl/13-ruby-style-debt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# [COMPLETE 2026-09-08 — standardrb clean (0 offenses); autofix + hand fixes + reasoned inline disables for the deliberate $DEBUG/method_missing idioms] 13 — Ruby style debt unblock (P2)

## Goal
StandardRB can pass again, so the lint gate is real.

## Why
The StandardRB job never got past bundler resolution since the Gemfile
gained its path dependency — a full never-linted offense pile sits in
lib/. With item 12's CI repair, every PR now inherits the red lint.

## Spec
- `standardrb --fix` for the auto-fixable; the remainder fixed by hand
EXCEPT `Security/Open` (URI.open) — that one is a behavioral risk
(network fetch in rababa model download) and gets an inline
disable with a reason, not a blind rewrite.
- Zero diff beyond style; specs stay green.

## Acceptance
- `bundle exec standardrb` clean; rspec unchanged by the cleanup.
25 changes: 25 additions & 0 deletions TODO.impl/14-lockstep-drift-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# [COMPLETE 2026-09-08 — weekly lockstep-check workflow; failure mode evidenced this round] 14 — Lockstep drift guard (P2)

## Goal
A scheduled check that fails loudly when the maps gem version outruns
the runtime's lockstep constraint — the failure mode that ran silent
from 2026-08-26 to 2026-09-08.

## Why
The gemspec derives `interscript-maps ~> X.Y.0a` from the gem's own
version. When maps shipped 2.5.0, every CI job failed at bundler with
an opaque exit 6/13 and main sat red for two weeks. A weekly probe
that resolves the gemspec against maps main turns that into a named,
actionable alert.

## Spec
- Weekly workflow on interscript-ruby: clone maps, run
`bundle lock` against the Gemfile with the clone as the path
dependency; failure output includes both version numbers.
- No new dependencies; a single job.

## Acceptance
- The workflow runs green on the current pair (2.5.0/2.5.0) and
demonstrably fails when the constraint can't resolve (verified once
by temporarily pinning back — or trusted from this round's
evidence, stated either way).
17 changes: 17 additions & 0 deletions TODO.impl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TODO.impl — round three: the Ruby corpus era

Round two's discovery: the Ruby gem is the last runtime not working
against the ISC corpus — its adapter drops dependencies (79 rspec
failures, root cause named), its lint gate never ran, and the
version-lockstep break that caused both ran silent for two weeks.
Status: ALL 3 ITEMS COMPLETE (2026-09-08). This register closed that era.

| # | Item | Priority |
|---|------|----------|
| 12 | [ISC dependencies in the NodeAdapter](12-isc-dependencies-adapter.md) | P1 |
| 13 | [Style debt unblock](13-ruby-style-debt.md) | P2 |
| 14 | [Lockstep drift guard](14-lockstep-drift-guard.md) | P2 |

Completing 12 unblocks PR #769 (the gallery parity spec, held open
with the root cause). Standing rules unchanged: TDD where behavior is
touched, staged sets verified, no attribution trailers.
3 changes: 2 additions & 1 deletion exe/diagnose_parse_failures
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ fails.first(n).each do |path|
src = File.read(path)
lines = src.lines
# Binary search for first failing prefix
lo = 1; hi = lines.size
lo = 1
hi = lines.size
while lo < hi
mid = (lo + hi) / 2
snippet = lines[0...mid].join
Expand Down
2 changes: 1 addition & 1 deletion exe/interscript
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
require 'interscript/command'
require "interscript/command"

if ARGV.any? && !Interscript::Command.all_tasks.key?(ARGV.first)
ARGV.unshift :translit
Expand Down
9 changes: 4 additions & 5 deletions exe/verify_isc_deep
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# at the rule level (from/to/constraints), not just test counts.

require "interscript"
require "set"
require "interscript/isc"
require "json"

Expand Down Expand Up @@ -39,7 +38,7 @@ class DeepVerifier
end

# Compare tests
imp_tests = Set.new(imp_data[:tests])
Set.new(imp_data[:tests])
isc_tests = Set.new(isc_data[:tests])
missing = imp_data[:tests].reject { |t| isc_tests.include?(t) }
details << "missing #{missing.size} tests from ISC" if missing.any?
Expand Down Expand Up @@ -83,7 +82,7 @@ class DeepVerifier
metadata: metadata,
tests: tests,
aliases: aliases,
rule_counts: rule_counts,
rule_counts: rule_counts
}
rescue => e
warn "IMP fail #{path}: #{e.message[0..80]}"
Expand All @@ -108,7 +107,7 @@ class DeepVerifier
metadata: metadata,
tests: tests,
aliases: aliases,
rule_counts: rule_counts,
rule_counts: rule_counts
}
rescue => e
warn "ISC fail #{path}: #{e.message[0..80]}"
Expand Down Expand Up @@ -204,7 +203,7 @@ if $PROGRAM_NAME == __FILE__
results.transform_values do |r|
{
status: r.status,
details: r.details,
details: r.details
}
end
))
Expand Down
9 changes: 4 additions & 5 deletions exe/verify_isc_equivalence
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# Reports per-file equivalence status.

require "interscript"
require "set"
require "interscript/isc"
require "json"

Expand Down Expand Up @@ -31,7 +30,7 @@ class Verifier
isc_set = Set.new(isc_data[:tests])

missing_from_isc = imp_data[:tests].reject { |t| isc_set.include?(t) }
extra_in_isc = isc_data[:tests].reject { |t| imp_set.include?(t) }
isc_data[:tests].reject { |t| imp_set.include?(t) }

if missing_from_isc.any?
details << "missing #{missing_from_isc.size} tests from ISC"
Expand All @@ -55,7 +54,7 @@ class Verifier
{
tests_count: tests.size,
tests: tests,
stages_count: dsl.stages&.size || 0,
stages_count: dsl.stages&.size || 0
}
rescue => e
warn "IMP parse fail #{path}: #{e.message[0..80]}"
Expand All @@ -69,7 +68,7 @@ class Verifier
{
tests_count: doc[:tests].size,
tests: doc[:tests].map { |t| [t[:input], t[:expected]] },
stages_count: doc[:stages].size,
stages_count: doc[:stages].size
}
rescue => e
warn "ISC parse fail #{path}: #{e.message[0..80]}"
Expand Down Expand Up @@ -121,7 +120,7 @@ if $PROGRAM_NAME == __FILE__
status: r.status,
details: r.details,
imp: r.imp_data,
isc: r.isc_data,
isc: r.isc_data
}
end
))
Expand Down
8 changes: 4 additions & 4 deletions lib/interscript.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,13 @@ def rababa_provision(model_name, model_uri)
model_path = "#{write_path}/model-#{model_name}.onnx"

# Redownload every hour
if File.exist?(model_path) && File.mtime(model_path) + 3600 >= Time.now
model_path
else
unless File.exist?(model_path) && File.mtime(model_path) + 3600 >= Time.now
# standard:disable Security/Open (fetches a fixed interscript-hosted model URI, not user input)
data = URI.open(model_uri, encoding: "BINARY").read
# standard:enable Security/Open
File.binwrite(model_path, data)
model_path
end
model_path
end

def map_aliases
Expand Down
14 changes: 9 additions & 5 deletions lib/interscript/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ class Interscript::Compiler

def self.call(map, **kwargs)
if String === map
path = Interscript.locate(map) rescue nil
path = begin
Interscript.locate(map)
rescue
nil
end
map = if path&.end_with?(".isc")
parse_isc(path)
else
Interscript::DSL.parse(map)
end
parse_isc(path)
else
Interscript::DSL.parse(map)
end
end
compiler = new
compiler.compile(map, **kwargs)
Expand Down
22 changes: 10 additions & 12 deletions lib/interscript/compiler/json_ir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,12 @@ def serialise_document(doc)
# reference via alias() without listing the library as an explicit
# dependency in the dependency list.
Interscript.maps(libraries: true).each do |lib|
begin
libdoc = Interscript.parse(lib)
libdoc.aliases.each do |aname, defn|
all_aliases[aname.to_s] ||= serialise_item(defn.data)
end
rescue
# skip unparseable libraries
libdoc = Interscript.parse(lib)
libdoc.aliases.each do |aname, defn|
all_aliases[aname.to_s] ||= serialise_item(defn.data)
end
rescue
# skip unparseable libraries
end

# Document's own aliases override everything.
Expand All @@ -91,9 +89,9 @@ def serialise_metadata(metadata)
out = {}
metadata.data.each do |k, v|
out[k.to_s] = case v
when Symbol then v.to_s
else v
end
when Symbol then v.to_s
else v
end
end
out
end
Expand Down Expand Up @@ -155,7 +153,7 @@ def serialise_run_rule(rule)
doc_name = stage.map
if doc_name && @map.respond_to?(:dep_aliases) && @map.dep_aliases[doc_name.to_sym]
resolved = @map.dep_aliases[doc_name.to_sym].document
doc_name = resolved.name.to_s if resolved && resolved.respond_to?(:name)
doc_name = resolved.name.to_s if resolved.respond_to?(:name)
end
{
kind: "run",
Expand Down Expand Up @@ -203,7 +201,7 @@ def serialise_item(item)
when ::Range
{kind: "any_char_class", range: [item.value.first, item.value.last]}
when ::String
{kind: "any_char_class", chars: item.value.split("")}
{kind: "any_char_class", chars: item.value.chars}
else
data = item.data || []
{kind: "any", of: data.map { |i| serialise_item(i) }}
Expand Down
2 changes: 2 additions & 0 deletions lib/interscript/dsl/items.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
module Interscript::DSL::Items
include Interscript::DSL::SymbolMM

# standard:disable Style/MissingRespondToMissing (DSL method_missing catches unknown method names by design)
def method_missing sym, *args, **kwargs, &block
# standard:enable Style/MissingRespondToMissing
super if args.length > 0
super if kwargs.length > 0
super if /[?!=]\z/.match?(sym.to_s)
Expand Down
1 change: 1 addition & 0 deletions lib/interscript/dsl/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def initialize(yaml: false, map_name: "", library: true, &block)
when String, Integer, Date
@node[sym] = stuff.to_s
when NilClass
nil # accepted silently
else
warn "[#{@map_name}] Metadata key #{sym} expects a String, but #{stuff.class} was given"
@node[sym] = stuff.inspect
Expand Down
2 changes: 2 additions & 0 deletions lib/interscript/dsl/symbol_mm.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Interscript::DSL::SymbolMM
# standard:disable Style/MissingRespondToMissing (DSL method_missing catches unknown method names by design)
def method_missing sym, *args, **kwargs, &block
# standard:enable Style/MissingRespondToMissing
super if args.length > 0
super if kwargs.length > 0
super if /[?!=]\z/.match?(sym.to_s)
Expand Down
Loading
Loading