diff --git a/.github/workflows/rake.yml b/.github/workflows/rake.yml index d22ed01d..db29f65e 100644 --- a/.github/workflows/rake.yml +++ b/.github/workflows/rake.yml @@ -37,6 +37,16 @@ jobs: - name: Bootstrap packages run: ruby bootstrap.rb + # bootstrap clones interscript-ruby REMOTE MAIN into ruby/ — for + # PRs that would test main's code instead of the PR. Overlay the + # actual ref under test onto the bootstrapped sibling layout. + - name: Overlay the ref under test onto ruby/ + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.ref || github.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + path: ruby + clean: false - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/lib/interscript/isc/grammar/isc.artifact.json b/lib/interscript/isc/grammar/isc.artifact.json deleted file mode 100644 index 28224c3b..00000000 --- a/lib/interscript/isc/grammar/isc.artifact.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"0.1.0","grammar":"InterscriptIsc","shape":"parsanol-tree/v2","binding_version":1,"entries":{"isc":{"root":"isc_source","grammar":{"atoms":[{"Entity":{"atom":25}},{"Entity":{"atom":8}},{"Re":{"pattern":"[ ]"}},{"Re":{"pattern":"[\\x09]"}},{"Re":{"pattern":"[\\x0a]"}},{"Re":{"pattern":"[\\x0b]"}},{"Re":{"pattern":"[\\x0c]"}},{"Re":{"pattern":"[\\x0d]"}},{"Alternative":{"atoms":[2,3,4,5,6,7]}},{"Entity":{"atom":23}},{"Str":{"pattern":"#"}},{"Entity":{"atom":17}},{"Re":{"pattern":"[\\x0d]"}},{"Re":{"pattern":"[\\x0a]"}},{"Sequence":{"atoms":[12,13]}},{"Re":{"pattern":"[\\x0a]"}},{"Re":{"pattern":"[\\x0d]"}},{"Alternative":{"atoms":[14,15,16]}},{"Lookahead":{"atom":11,"positive":false}},{"Entity":{"atom":20}},{"Re":{"pattern":"[\\x00-\\u{10ffff}]"}},{"Sequence":{"atoms":[18,19]}},{"Repetition":{"atom":21,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[10,22]}},{"Alternative":{"atoms":[1,9]}},{"Repetition":{"atom":24,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":0,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":868}},{"Str":{"pattern":"system"}},{"Entity":{"atom":25}},{"Entity":{"atom":97}},{"Entity":{"atom":85}},{"Str":{"pattern":"\""}},{"Entity":{"atom":82}},{"Entity":{"atom":71}},{"Str":{"pattern":"\\"}},{"Entity":{"atom":38}},{"Str":{"pattern":"n"}},{"Named":{"name":"newline","atom":37}},{"Entity":{"atom":41}},{"Str":{"pattern":"r"}},{"Named":{"name":"carriage_return","atom":40}},{"Entity":{"atom":44}},{"Str":{"pattern":"t"}},{"Named":{"name":"tab","atom":43}},{"Entity":{"atom":47}},{"Str":{"pattern":"\""}},{"Named":{"name":"dquote","atom":46}},{"Entity":{"atom":50}},{"Str":{"pattern":"\\"}},{"Named":{"name":"backslash","atom":49}},{"Entity":{"atom":60}},{"Str":{"pattern":"u"}},{"Entity":{"atom":57}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[\\x41-\\x46]"}},{"Re":{"pattern":"[\\x61-\\x66]"}},{"Alternative":{"atoms":[54,55,56]}},{"Repetition":{"atom":53,"min":4,"max":4,"tag":"Repetition"}},{"Named":{"name":"unicode","atom":58}},{"Sequence":{"atoms":[52,59]}},{"Entity":{"atom":66}},{"Str":{"pattern":"U"}},{"Entity":{"atom":57}},{"Repetition":{"atom":63,"min":4,"max":8,"tag":"Repetition"}},{"Named":{"name":"unicode","atom":64}},{"Sequence":{"atoms":[62,65]}},{"Entity":{"atom":69}},{"Str":{"pattern":"U"}},{"Named":{"name":"u_lone","atom":68}},{"Alternative":{"atoms":[36,39,42,45,48,51,61,67]}},{"Sequence":{"atoms":[35,70]}},{"Entity":{"atom":79}},{"Str":{"pattern":"\\"}},{"Str":{"pattern":"\""}},{"Alternative":{"atoms":[73,74]}},{"Lookahead":{"atom":75,"positive":false}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[76,77]}},{"Repetition":{"atom":78,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"run","atom":72}},{"Alternative":{"atoms":[34,80]}},{"Repetition":{"atom":81,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"string","atom":33}},{"Str":{"pattern":"\""}},{"Sequence":{"atoms":[32,83,84]}},{"Entity":{"atom":96}},{"Str":{"pattern":"'"}},{"Entity":{"atom":93}},{"Str":{"pattern":"'"}},{"Lookahead":{"atom":89,"positive":false}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[90,91]}},{"Repetition":{"atom":92,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"string","atom":88}},{"Str":{"pattern":"'"}},{"Sequence":{"atoms":[87,94,95]}},{"Alternative":{"atoms":[31,86]}},{"Named":{"name":"system_code","atom":30}},{"Entity":{"atom":25}},{"Repetition":{"atom":99,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Entity":{"atom":8}},{"Entity":{"atom":23}},{"Alternative":{"atoms":[103,104]}},{"Repetition":{"atom":105,"min":0,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":102,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":860}},{"Entity":{"atom":25}},{"Repetition":{"atom":109,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":322}},{"Str":{"pattern":"metadata"}},{"Entity":{"atom":25}},{"Repetition":{"atom":113,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":116,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":317}},{"Entity":{"atom":316}},{"Entity":{"atom":25}},{"Repetition":{"atom":120,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":143}},{"Str":{"pattern":"description"}},{"Entity":{"atom":25}},{"Repetition":{"atom":124,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":127,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":138}},{"Str":{"pattern":"\\"}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[130,131]}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":133,"positive":false}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[134,135]}},{"Alternative":{"atoms":[132,136]}},{"Repetition":{"atom":137,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"description","atom":129}},{"Entity":{"atom":106}},{"Repetition":{"atom":140,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[123,125,126,128,139,141,142]}},{"Entity":{"atom":183}},{"Str":{"pattern":"relations"}},{"Entity":{"atom":25}},{"Repetition":{"atom":146,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":149,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":177}},{"Entity":{"atom":25}},{"Repetition":{"atom":152,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":162}},{"Str":{"pattern":"supersedes"}},{"Str":{"pattern":"superseded_by"}},{"Str":{"pattern":"based_on"}},{"Str":{"pattern":"basis_for"}},{"Str":{"pattern":"alias_of"}},{"Str":{"pattern":"adopted_from"}},{"Str":{"pattern":"related_to"}},{"Alternative":{"atoms":[155,156,157,158,159,160,161]}},{"Named":{"name":"type","atom":154}},{"Entity":{"atom":25}},{"Entity":{"atom":97}},{"Named":{"name":"system","atom":165}},{"Entity":{"atom":173}},{"Entity":{"atom":25}},{"Str":{"pattern":"note"}},{"Entity":{"atom":25}},{"Entity":{"atom":97}},{"Named":{"name":"note","atom":171}},{"Sequence":{"atoms":[168,169,170,172]}},{"Repetition":{"atom":167,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":25}},{"Repetition":{"atom":175,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[153,163,164,166,174,176]}},{"Repetition":{"atom":151,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"relations","atom":178}},{"Entity":{"atom":106}},{"Repetition":{"atom":180,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[145,147,148,150,179,181,182]}},{"Entity":{"atom":192}},{"Str":{"pattern":"system_status"}},{"Entity":{"atom":25}},{"Str":{"pattern":"current"}},{"Str":{"pattern":"former"}},{"Str":{"pattern":"inactive"}},{"Alternative":{"atoms":[187,188,189]}},{"Named":{"name":"system_status","atom":190}},{"Sequence":{"atoms":[185,186,191]}},{"Entity":{"atom":201}},{"Str":{"pattern":"code_status"}},{"Entity":{"atom":25}},{"Str":{"pattern":"preferred"}},{"Str":{"pattern":"proposed"}},{"Str":{"pattern":"deprecated"}},{"Alternative":{"atoms":[196,197,198]}},{"Named":{"name":"code_status","atom":199}},{"Sequence":{"atoms":[194,195,200]}},{"Entity":{"atom":218}},{"Str":{"pattern":"specification"}},{"Entity":{"atom":25}},{"Entity":{"atom":207}},{"Entity":{"atom":97}},{"Named":{"name":"specification","atom":206}},{"Entity":{"atom":212}},{"Str":{"pattern":","}},{"Entity":{"atom":25}},{"Repetition":{"atom":210,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[209,211]}},{"Entity":{"atom":207}},{"Sequence":{"atoms":[208,213]}},{"Repetition":{"atom":214,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[205,215]}},{"Named":{"name":"specification","atom":216}},{"Sequence":{"atoms":[203,204,217]}},{"Entity":{"atom":242}},{"Str":{"pattern":"notes"}},{"Entity":{"atom":25}},{"Repetition":{"atom":221,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":224,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":236}},{"Entity":{"atom":25}},{"Repetition":{"atom":227,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"note"}},{"Entity":{"atom":25}},{"Repetition":{"atom":230,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":97}},{"Named":{"name":"note","atom":232}},{"Entity":{"atom":25}},{"Repetition":{"atom":234,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[228,229,231,233,235]}},{"Repetition":{"atom":226,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"notes","atom":237}},{"Entity":{"atom":106}},{"Repetition":{"atom":239,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[220,222,223,225,238,240,241]}},{"Entity":{"atom":256}},{"Str":{"pattern":"provenance"}},{"Entity":{"atom":25}},{"Repetition":{"atom":245,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":249}},{"Entity":{"atom":97}},{"Named":{"name":"provenance","atom":248}},{"Entity":{"atom":212}},{"Entity":{"atom":249}},{"Sequence":{"atoms":[250,251]}},{"Repetition":{"atom":252,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[247,253]}},{"Named":{"name":"provenance","atom":254}},{"Sequence":{"atoms":[244,246,255]}},{"Entity":{"atom":312}},{"Entity":{"atom":272}},{"Entity":{"atom":263}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[260,261,262]}},{"Entity":{"atom":269}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[265,266,267,268]}},{"Repetition":{"atom":264,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[259,270]}},{"Named":{"name":"identifier","atom":271}},{"Named":{"name":"field_name","atom":258}},{"Entity":{"atom":282}},{"Entity":{"atom":279}},{"Re":{"pattern":"[ ]"}},{"Re":{"pattern":"[\\x09]"}},{"Alternative":{"atoms":[276,277]}},{"Repetition":{"atom":278,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":23}},{"Alternative":{"atoms":[275,280]}},{"Repetition":{"atom":281,"min":0,"max":null,"tag":"Repetition"}},{"Entity":{"atom":296}},{"Entity":{"atom":97}},{"Entity":{"atom":294}},{"Re":{"pattern":"[\\x0a]"}},{"Lookahead":{"atom":286,"positive":false}},{"Str":{"pattern":"}"}},{"Lookahead":{"atom":288,"positive":false}},{"Str":{"pattern":"{"}},{"Lookahead":{"atom":290,"positive":false}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[287,289,291,292]}},{"Repetition":{"atom":293,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"raw","atom":285}},{"Alternative":{"atoms":[284,295]}},{"Named":{"name":"field_value","atom":283}},{"Entity":{"atom":307}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":300,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":138}},{"Named":{"name":"field_block","atom":302}},{"Entity":{"atom":106}},{"Repetition":{"atom":304,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[299,301,303,305,306]}},{"Entity":{"atom":310}},{"Entity":{"atom":17}},{"Lookahead":{"atom":309,"positive":true}},{"Alternative":{"atoms":[297,298,308]}},{"Sequence":{"atoms":[273,274,311]}},{"Alternative":{"atoms":[122,144,184,193,202,219,243,257]}},{"Entity":{"atom":25}},{"Repetition":{"atom":314,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[121,313,315]}},{"Repetition":{"atom":119,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"metadata","atom":118}},{"Entity":{"atom":106}},{"Repetition":{"atom":319,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[112,114,115,117,318,320,321]}},{"Entity":{"atom":569}},{"Str":{"pattern":"aliases"}},{"Entity":{"atom":25}},{"Repetition":{"atom":325,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":328,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":564}},{"Entity":{"atom":563}},{"Entity":{"atom":25}},{"Repetition":{"atom":332,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":272}},{"Named":{"name":"name","atom":334}},{"Entity":{"atom":25}},{"Repetition":{"atom":336,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"="}},{"Entity":{"atom":25}},{"Repetition":{"atom":339,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":559}},{"Entity":{"atom":557}},{"Entity":{"atom":97}},{"Entity":{"atom":346}},{"Str":{"pattern":"none"}},{"Named":{"name":"none","atom":345}},{"Entity":{"atom":355}},{"Str":{"pattern":"boundary"}},{"Str":{"pattern":"line_start"}},{"Str":{"pattern":"line_end"}},{"Str":{"pattern":"word_boundary"}},{"Str":{"pattern":"space"}},{"Str":{"pattern":"non_boundary"}},{"Alternative":{"atoms":[348,349,350,351,352,353]}},{"Named":{"name":"primitive","atom":354}},{"Entity":{"atom":358}},{"Str":{"pattern":"any_character"}},{"Named":{"name":"any_char","atom":357}},{"Entity":{"atom":556}},{"Str":{"pattern":"any"}},{"Str":{"pattern":"("}},{"Entity":{"atom":106}},{"Repetition":{"atom":362,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":551}},{"Entity":{"atom":375}},{"Entity":{"atom":97}},{"Named":{"name":"lo","atom":366}},{"Entity":{"atom":106}},{"Repetition":{"atom":368,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":".."}},{"Entity":{"atom":106}},{"Repetition":{"atom":371,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":97}},{"Named":{"name":"hi","atom":373}},{"Sequence":{"atoms":[367,369,370,372,374]}},{"Entity":{"atom":542}},{"Entity":{"atom":97}},{"Named":{"name":"single","atom":377}},{"Str":{"pattern":"["}},{"Entity":{"atom":106}},{"Repetition":{"atom":380,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":383}},{"Entity":{"atom":529}},{"Entity":{"atom":392}},{"Entity":{"atom":106}},{"Repetition":{"atom":385,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"+"}},{"Entity":{"atom":106}},{"Repetition":{"atom":388,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[386,387,389]}},{"Entity":{"atom":25}},{"Alternative":{"atoms":[390,391]}},{"Entity":{"atom":400}},{"Str":{"pattern":"to"}},{"Str":{"pattern":"before"}},{"Str":{"pattern":"after"}},{"Str":{"pattern":"not_before"}},{"Str":{"pattern":"not_after"}},{"Str":{"pattern":"}"}},{"Alternative":{"atoms":[394,395,396,397,398,399]}},{"Lookahead":{"atom":393,"positive":false}},{"Entity":{"atom":407}},{"Entity":{"atom":272}},{"Entity":{"atom":106}},{"Repetition":{"atom":404,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"="}},{"Sequence":{"atoms":[403,405,406]}},{"Lookahead":{"atom":402,"positive":false}},{"Entity":{"atom":413}},{"Str":{"pattern":"\""}},{"Str":{"pattern":"'"}},{"Entity":{"atom":263}},{"Alternative":{"atoms":[410,411,412]}},{"Lookahead":{"atom":409,"positive":true}},{"Entity":{"atom":525}},{"Entity":{"atom":430}},{"Str":{"pattern":"capture"}},{"Str":{"pattern":"("}},{"Entity":{"atom":106}},{"Repetition":{"atom":419,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":425}},{"Sequence":{"atoms":[384,401,408,414,415]}},{"Repetition":{"atom":422,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[342,423]}},{"Named":{"name":"concatenation","atom":424}},{"Named":{"name":"capture_inner","atom":421}},{"Entity":{"atom":106}},{"Repetition":{"atom":427,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[417,418,420,426,428,429]}},{"Entity":{"atom":441}},{"Str":{"pattern":"maybe"}},{"Str":{"pattern":"("}},{"Entity":{"atom":106}},{"Repetition":{"atom":434,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":425}},{"Named":{"name":"maybe_inner","atom":436}},{"Entity":{"atom":106}},{"Repetition":{"atom":438,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[432,433,435,437,439,440]}},{"Entity":{"atom":452}},{"Str":{"pattern":"some"}},{"Str":{"pattern":"("}},{"Entity":{"atom":106}},{"Repetition":{"atom":445,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":425}},{"Named":{"name":"some_inner","atom":447}},{"Entity":{"atom":106}},{"Repetition":{"atom":449,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[443,444,446,448,450,451]}},{"Entity":{"atom":461}},{"Str":{"pattern":"upcase"}},{"Str":{"pattern":"downcase"}},{"Str":{"pattern":"title_case"}},{"Str":{"pattern":"reverse"}},{"Str":{"pattern":"strip"}},{"Str":{"pattern":"swapcase"}},{"Alternative":{"atoms":[454,455,456,457,458,459]}},{"Named":{"name":"function","atom":460}},{"Entity":{"atom":476}},{"Entity":{"atom":475}},{"Str":{"pattern":"ref"}},{"Str":{"pattern":"("}},{"Entity":{"atom":106}},{"Repetition":{"atom":466,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":471}},{"Entity":{"atom":470}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Named":{"name":"digit","atom":469}},{"Entity":{"atom":106}},{"Repetition":{"atom":472,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[464,465,467,468,473,474]}},{"Named":{"name":"ref","atom":463}},{"Entity":{"atom":524}},{"Entity":{"atom":513}},{"Str":{"pattern":"parallel"}},{"Str":{"pattern":"sequence"}},{"Str":{"pattern":"stage"}},{"Str":{"pattern":"compose"}},{"Str":{"pattern":"separate"}},{"Str":{"pattern":"system"}},{"Str":{"pattern":"metadata"}},{"Str":{"pattern":"aliases"}},{"Str":{"pattern":"tests"}},{"Str":{"pattern":"notes"}},{"Str":{"pattern":"description"}},{"Str":{"pattern":"any_character"}},{"Str":{"pattern":"authority"}},{"Str":{"pattern":"dependency"}},{"Str":{"pattern":"run"}},{"Str":{"pattern":"sub"}},{"Str":{"pattern":"before"}},{"Str":{"pattern":"after"}},{"Str":{"pattern":"not_before"}},{"Str":{"pattern":"not_after"}},{"Str":{"pattern":"any"}},{"Str":{"pattern":"none"}},{"Str":{"pattern":"boundary"}},{"Str":{"pattern":"line_start"}},{"Str":{"pattern":"line_end"}},{"Str":{"pattern":"word_boundary"}},{"Str":{"pattern":"downcase"}},{"Str":{"pattern":"upcase"}},{"Str":{"pattern":"title_case"}},{"Str":{"pattern":"capture"}},{"Str":{"pattern":"maybe"}},{"Str":{"pattern":"some"}},{"Str":{"pattern":"ref"}},{"Str":{"pattern":"name"}},{"Alternative":{"atoms":[479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512]}},{"Lookahead":{"atom":478,"positive":false}},{"Entity":{"atom":272}},{"Str":{"pattern":"."}},{"Entity":{"atom":272}},{"Named":{"name":"qualified_name","atom":517}},{"Sequence":{"atoms":[516,518]}},{"Repetition":{"atom":519,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Lookahead":{"atom":521,"positive":false}},{"Sequence":{"atoms":[514,515,520,522]}},{"Named":{"name":"alias","atom":523}},{"Alternative":{"atoms":[343,344,347,356,359,416,431,442,453,462,477]}},{"Sequence":{"atoms":[384,401,408,414,415]}},{"Repetition":{"atom":526,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[342,527]}},{"Named":{"name":"concatenation","atom":528}},{"Entity":{"atom":212}},{"Entity":{"atom":25}},{"Alternative":{"atoms":[530,531]}},{"Entity":{"atom":383}},{"Sequence":{"atoms":[532,533]}},{"Repetition":{"atom":534,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[382,535]}},{"Named":{"name":"list","atom":536}},{"Entity":{"atom":106}},{"Repetition":{"atom":538,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"]"}},{"Sequence":{"atoms":[379,381,537,539,540]}},{"Alternative":{"atoms":[378,541]}},{"Entity":{"atom":550}},{"Entity":{"atom":355}},{"Lookahead":{"atom":544,"positive":false}},{"Entity":{"atom":513}},{"Lookahead":{"atom":546,"positive":false}},{"Entity":{"atom":272}},{"Sequence":{"atoms":[545,547,548]}},{"Named":{"name":"alias_ref","atom":549}},{"Alternative":{"atoms":[365,376,543]}},{"Named":{"name":"any","atom":364}},{"Entity":{"atom":106}},{"Repetition":{"atom":553,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[360,361,363,552,554,555]}},{"Alternative":{"atoms":[343,344,347,356,359,416,431,442,453,462,477]}},{"Sequence":{"atoms":[342,527]}},{"Named":{"name":"concatenation","atom":558}},{"Named":{"name":"value","atom":341}},{"Entity":{"atom":25}},{"Repetition":{"atom":561,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[333,335,337,338,340,560,562]}},{"Repetition":{"atom":331,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"aliases","atom":330}},{"Entity":{"atom":106}},{"Repetition":{"atom":566,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[324,326,327,329,565,567,568]}},{"Entity":{"atom":608}},{"Str":{"pattern":"tests"}},{"Entity":{"atom":25}},{"Repetition":{"atom":572,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":575,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":603}},{"Entity":{"atom":602}},{"Entity":{"atom":25}},{"Repetition":{"atom":579,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":97}},{"Named":{"name":"input","atom":581}},{"Entity":{"atom":589}},{"Entity":{"atom":25}},{"Repetition":{"atom":584,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"->"}},{"Entity":{"atom":25}},{"Repetition":{"atom":587,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[585,586,588]}},{"Entity":{"atom":97}},{"Named":{"name":"expected","atom":590}},{"Entity":{"atom":598}},{"Entity":{"atom":25}},{"Str":{"pattern":"note"}},{"Entity":{"atom":25}},{"Entity":{"atom":97}},{"Named":{"name":"note","atom":596}},{"Sequence":{"atoms":[593,594,595,597]}},{"Repetition":{"atom":592,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":25}},{"Repetition":{"atom":600,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[580,582,583,591,599,601]}},{"Repetition":{"atom":578,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"tests","atom":577}},{"Entity":{"atom":106}},{"Repetition":{"atom":605,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[571,573,574,576,604,606,607]}},{"Entity":{"atom":842}},{"Str":{"pattern":"stage"}},{"Entity":{"atom":616}},{"Str":{"pattern":"("}},{"Entity":{"atom":272}},{"Named":{"name":"stage_name","atom":613}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[612,614,615]}},{"Entity":{"atom":621}},{"Entity":{"atom":25}},{"Entity":{"atom":272}},{"Named":{"name":"stage_name","atom":619}},{"Sequence":{"atoms":[618,620]}},{"Alternative":{"atoms":[611,617]}},{"Entity":{"atom":106}},{"Repetition":{"atom":623,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":626,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":837}},{"Entity":{"atom":836}},{"Entity":{"atom":106}},{"Repetition":{"atom":630,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":734}},{"Str":{"pattern":"sequence"}},{"Entity":{"atom":106}},{"Repetition":{"atom":634,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":637,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":729}},{"Entity":{"atom":728}},{"Entity":{"atom":106}},{"Repetition":{"atom":641,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":709}},{"Entity":{"atom":694}},{"Str":{"pattern":"sub"}},{"Entity":{"atom":106}},{"Repetition":{"atom":646,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":649,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"from"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"from","atom":653}},{"Entity":{"atom":106}},{"Repetition":{"atom":655,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"to"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"to","atom":659}},{"Entity":{"atom":25}},{"Entity":{"atom":687}},{"Entity":{"atom":668}},{"Str":{"pattern":"after"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"after","atom":666}},{"Sequence":{"atoms":[664,665,667]}},{"Entity":{"atom":674}},{"Str":{"pattern":"before"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"before","atom":672}},{"Sequence":{"atoms":[670,671,673]}},{"Entity":{"atom":680}},{"Str":{"pattern":"not_before"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"not_before","atom":678}},{"Sequence":{"atoms":[676,677,679]}},{"Entity":{"atom":686}},{"Str":{"pattern":"not_after"}},{"Entity":{"atom":25}},{"Entity":{"atom":559}},{"Named":{"name":"not_after","atom":684}},{"Sequence":{"atoms":[682,683,685]}},{"Alternative":{"atoms":[663,669,675,681]}},{"Sequence":{"atoms":[661,662]}},{"Repetition":{"atom":688,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"constraints","atom":689}},{"Entity":{"atom":106}},{"Repetition":{"atom":691,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[645,647,648,650,651,652,654,656,657,658,660,690,692,693]}},{"Entity":{"atom":708}},{"Str":{"pattern":"sub"}},{"Entity":{"atom":25}},{"Entity":{"atom":557}},{"Named":{"name":"from","atom":698}},{"Entity":{"atom":25}},{"Entity":{"atom":557}},{"Named":{"name":"to","atom":701}},{"Entity":{"atom":25}},{"Entity":{"atom":687}},{"Sequence":{"atoms":[703,704]}},{"Repetition":{"atom":705,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"constraints","atom":706}},{"Sequence":{"atoms":[696,697,699,700,702,707]}},{"Alternative":{"atoms":[644,695]}},{"Entity":{"atom":724}},{"Str":{"pattern":"#"}},{"Re":{"pattern":"[\\x0a]"}},{"Lookahead":{"atom":712,"positive":false}},{"Entity":{"atom":20}},{"Sequence":{"atoms":[713,714]}},{"Repetition":{"atom":715,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[711,716]}},{"Named":{"name":"comment","atom":717}},{"Entity":{"atom":513}},{"Lookahead":{"atom":719,"positive":false}},{"Entity":{"atom":272}},{"Sequence":{"atoms":[720,721]}},{"Named":{"name":"noop","atom":722}},{"Alternative":{"atoms":[718,723]}},{"Alternative":{"atoms":[643,710]}},{"Entity":{"atom":106}},{"Repetition":{"atom":726,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[642,725,727]}},{"Repetition":{"atom":640,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"sequence","atom":639}},{"Entity":{"atom":106}},{"Repetition":{"atom":731,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[633,635,636,638,730,732,733]}},{"Entity":{"atom":747}},{"Str":{"pattern":"parallel"}},{"Entity":{"atom":106}},{"Repetition":{"atom":737,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"{"}},{"Entity":{"atom":106}},{"Repetition":{"atom":740,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":729}},{"Named":{"name":"parallel","atom":742}},{"Entity":{"atom":106}},{"Repetition":{"atom":744,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[736,738,739,741,743,745,746]}},{"Entity":{"atom":766}},{"Str":{"pattern":"run"}},{"Entity":{"atom":25}},{"Entity":{"atom":758}},{"Str":{"pattern":"map."}},{"Entity":{"atom":272}},{"Named":{"name":"dep","atom":753}},{"Str":{"pattern":".stage."}},{"Entity":{"atom":272}},{"Named":{"name":"stage","atom":756}},{"Sequence":{"atoms":[752,754,755,757]}},{"Entity":{"atom":764}},{"Str":{"pattern":"stage."}},{"Entity":{"atom":272}},{"Named":{"name":"stage","atom":761}},{"Sequence":{"atoms":[760,762]}},{"Named":{"name":"run_stage_only","atom":763}},{"Alternative":{"atoms":[751,759]}},{"Sequence":{"atoms":[749,750,765]}},{"Entity":{"atom":778}},{"Str":{"pattern":"separate"}},{"Named":{"name":"separate","atom":768}},{"Entity":{"atom":776}},{"Entity":{"atom":25}},{"Str":{"pattern":"separator"}},{"Entity":{"atom":25}},{"Entity":{"atom":557}},{"Named":{"name":"separator","atom":774}},{"Sequence":{"atoms":[771,772,773,775]}},{"Repetition":{"atom":770,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[769,777]}},{"Entity":{"atom":813}},{"Entity":{"atom":784}},{"Str":{"pattern":"downcase"}},{"Str":{"pattern":"upcase"}},{"Alternative":{"atoms":[781,782]}},{"Named":{"name":"case","atom":783}},{"Entity":{"atom":812}},{"Str":{"pattern":"title_case"}},{"Named":{"name":"case","atom":786}},{"Entity":{"atom":810}},{"Entity":{"atom":25}},{"Entity":{"atom":808}},{"Entity":{"atom":803}},{"Entity":{"atom":802}},{"Entity":{"atom":272}},{"Named":{"name":"kwarg_name","atom":793}},{"Entity":{"atom":106}},{"Repetition":{"atom":795,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":":"}},{"Entity":{"atom":106}},{"Repetition":{"atom":798,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":97}},{"Named":{"name":"kwarg_value","atom":800}},{"Sequence":{"atoms":[794,796,797,799,801]}},{"Named":{"name":"kwarg","atom":792}},{"Entity":{"atom":212}},{"Entity":{"atom":803}},{"Sequence":{"atoms":[804,805]}},{"Repetition":{"atom":806,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[791,807]}},{"Named":{"name":"case_kwargs","atom":790}},{"Sequence":{"atoms":[789,809]}},{"Repetition":{"atom":788,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[787,811]}},{"Alternative":{"atoms":[780,785]}},{"Entity":{"atom":818}},{"Str":{"pattern":"compose"}},{"Str":{"pattern":"decompose"}},{"Alternative":{"atoms":[815,816]}},{"Named":{"name":"compose","atom":817}},{"Entity":{"atom":828}},{"Str":{"pattern":"rababa"}},{"Named":{"name":"funcall_name","atom":820}},{"Entity":{"atom":826}},{"Entity":{"atom":25}},{"Entity":{"atom":808}},{"Named":{"name":"funcall_kwargs","atom":824}},{"Sequence":{"atoms":[823,825]}},{"Repetition":{"atom":822,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[821,827]}},{"Entity":{"atom":831}},{"Entity":{"atom":709}},{"Named":{"name":"bare_rule","atom":830}},{"Entity":{"atom":724}},{"Alternative":{"atoms":[632,735,748,767,779,814,819,829,832]}},{"Entity":{"atom":106}},{"Repetition":{"atom":834,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[631,833,835]}},{"Repetition":{"atom":629,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"stage","atom":628}},{"Entity":{"atom":106}},{"Repetition":{"atom":839,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Sequence":{"atoms":[610,622,624,625,627,838,840,841]}},{"Entity":{"atom":856}},{"Str":{"pattern":"dependency"}},{"Entity":{"atom":25}},{"Entity":{"atom":97}},{"Named":{"name":"target","atom":846}},{"Entity":{"atom":854}},{"Entity":{"atom":25}},{"Str":{"pattern":"as"}},{"Entity":{"atom":25}},{"Entity":{"atom":272}},{"Named":{"name":"alias","atom":852}},{"Sequence":{"atoms":[849,850,851,853]}},{"Repetition":{"atom":848,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[844,845,847,855]}},{"Alternative":{"atoms":[111,323,570,609,843]}},{"Entity":{"atom":25}},{"Repetition":{"atom":858,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[110,857,859]}},{"Repetition":{"atom":108,"min":0,"max":null,"tag":"Repetition"}},{"Named":{"name":"body","atom":861}},{"Entity":{"atom":106}},{"Repetition":{"atom":863,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"}"}},{"Entity":{"atom":25}},{"Repetition":{"atom":866,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[28,29,98,100,101,107,862,864,865,867]}},{"Named":{"name":"system","atom":27}},{"Entity":{"atom":25}},{"Repetition":{"atom":870,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[26,869,871]}}],"root":872},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule newline: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 2 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 2 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 2 and 16 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 2 and 32 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 3 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 3 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 3 and 16 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 3 and 32 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 4 and 30 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 5 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 5 and 16 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 5 and 32 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 6 and 16 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 6 and 32 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 7 and 31 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 8 and 12 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 8 and 13 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 8 and 18 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 8 and 21 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 9 and 29 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 10 and 19 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 10 and 20 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 10 and 22 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 10 and 34 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 11 and 14 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 11 and 27 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 12 and 13 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 12 and 18 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 12 and 21 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 13 and 18 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 13 and 21 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 14 and 27 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 15 and 33 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 16 and 32 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 17 and 23 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 18 and 21 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 19 and 20 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 19 and 22 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 19 and 34 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 20 and 22 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 20 and 34 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 22 and 34 are order-dependent (shared first bytes; ordered choice is decisive)","rule keyword: branches 24 and 25 are order-dependent (shared first bytes; ordered choice is decisive)","rule relation_type: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule relation_type: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule relation_type: branches 5 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule md_field_value: branches 1 and 2 are order-dependent (first set not statically known)","rule item_sep: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule any_arg: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule any_arg: branches 1 and 3 are order-dependent (first set not statically known)","rule any_arg: branches 2 and 3 are order-dependent (first set not statically known)","rule cont_stop: branches 4 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 1 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 2 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 3 and 8 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 3 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 3 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 4 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 4 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 5 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 6 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 7 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 8 and 9 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 8 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 9 and 10 are order-dependent (shared first bytes; ordered choice is decisive)","rule item_atom: branches 9 and 11 are order-dependent (first set not statically known)","rule item_atom: branches 10 and 11 are order-dependent (first set not statically known)","rule constraint: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule rule: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule comment_item: branches 1 and 2 are order-dependent (first set not statically known)"]},"default_entry":"isc","render":{},"derive":{},"tests":[],"docs":{},"source":"# Interscript ISC grammar — parsanol grammar language (PG).\n#\n# Line-oriented port of lib/interscript/isc/grammar/concerns/*.rb.\n# The capture tree must reproduce the Ruby-DSL parser's tree exactly:\n# - repetitions are ABNF prefixes (1*x, *x)\n# - negation is !x, positive lookahead is &x\n# - captures at use sites (rule_ref as name) nest inner captures\n# the same way parslet .as does\n#\n# Consumed by the interscript gem (artifact parse) and parsanol npm.\n\ngrammar InterscriptIsc version \"0.1.0\" {\n\n # -- lexical primitives -------------------------------------------------\n\n chr = %x00-10FFFF\n space_char = %x20 / %x09 / %x0A / %x0B / %x0C / %x0D\n newline = %x0D.0A / %x0A / %x0D\n line_comment = \"#\" *(!(newline) chr)\n whitespace = 1*(space_char / line_comment)\n ws = *(space_char / line_comment)\n inline_space = 1*(%x20 / %x09)\n inline_ws = *(inline_space / line_comment)\n hexd = %x30-39 / %x41-46 / %x61-66\n digit = %x30-39\n\n ident_first = %x41-5A / %x61-7A / %x5F\n ident_rest = %x41-5A / %x61-7A / %x30-39 / %x5F\n identifier = (ident_first 0*ident_rest) as identifier\n\n keyword = \"parallel\" / \"sequence\" / \"stage\" / \"compose\" / \"separate\" / \"system\" / \"metadata\" / \"aliases\" / \"tests\" / \"notes\" / \"description\" / \"any_character\" / \"authority\" / \"dependency\" / \"run\" / \"sub\" / \"before\" / \"after\" / \"not_before\" / \"not_after\" / \"any\" / \"none\" / \"boundary\" / \"line_start\" / \"line_end\" / \"word_boundary\" / \"downcase\" / \"upcase\" / \"title_case\" / \"capture\" / \"maybe\" / \"some\" / \"ref\" / \"name\"\n\n # -- string literals ----------------------------------------------------\n\n esc_nl = (\"n\") as newline\n esc_cr = (\"r\") as carriage_return\n esc_tab = (\"t\") as tab\n esc_dq = \"\\\"\" as dquote\n esc_bs = \"\\\\\" as backslash\n esc_uni4 = \"u\" (4hexd) as unicode\n esc_uni8 = \"U\" (4*8hexd) as unicode\n esc_u_lone = \"U\" as u_lone\n escape_sequence = \"\\\\\" (esc_nl / esc_cr / esc_tab / esc_dq / esc_bs / esc_uni4 / esc_uni8 / esc_u_lone)\n\n raw_run = 1*(!(\"\\\\\" / \"\\\"\") chr)\n dq_parts = *(escape_sequence / (raw_run as run))\n double_quoted = \"\\\"\" (dq_parts as string) \"\\\"\"\n sq_body = *(!\"'\" chr)\n single_quoted = \"'\" (sq_body as string) \"'\"\n quoted_string = double_quoted / single_quoted\n\n arrow = [whitespace] \"->\" [whitespace]\n comma_ws = \",\" [whitespace]\n\n # -- metadata -----------------------------------------------------------\n\n md_authority = \"authority\" whitespace ((quoted_string) as authority)\n md_source_spelling = \"source_spelling\" whitespace ((quoted_string) as source_spelling)\n md_target_spelling = \"target_spelling\" whitespace ((quoted_string) as target_spelling)\n md_identifying = \"identifying\" whitespace ((quoted_string) as identifying)\n md_name = \"name\" whitespace ((quoted_string) as name)\n md_spec_value = quoted_string as specification\n md_specification = \"specification\" whitespace ((md_spec_value *(comma_ws md_spec_value)) as specification)\n md_description = \"description\" [whitespace] \"{\" [ws] (raw_text as description) [ws] \"}\"\n md_system_status = \"system_status\" whitespace ((\"current\" / \"former\" / \"inactive\") as system_status)\n md_code_status = \"code_status\" whitespace ((\"preferred\" / \"proposed\" / \"deprecated\") as code_status)\n\n relation_type = \"supersedes\" / \"superseded_by\" / \"based_on\" / \"basis_for\" / \"alias_of\" / \"adopted_from\" / \"related_to\"\n relation_note = whitespace \"note\" whitespace (quoted_string as note)\n relation_item = [whitespace] (relation_type as type) whitespace (quoted_string as system) [(relation_note)] [whitespace]\n md_relations = \"relations\" [whitespace] \"{\" [ws] ((*(relation_item)) as relations) [ws] \"}\"\n\n note_item = [whitespace] \"note\" [whitespace] (quoted_string as note) [whitespace]\n md_notes = \"notes\" [whitespace] \"{\" [ws] ((*(note_item)) as notes) [ws] \"}\"\n\n md_provenance_value = quoted_string as provenance\n md_provenance = \"provenance\" [whitespace] ((md_provenance_value *(comma_ws md_provenance_value)) as provenance)\n\n md_empty_field = &(newline)\n md_raw_field_value = 1*(!%x0A !\"}\" !\"{\" chr)\n md_field_value = quoted_string / (md_raw_field_value as raw)\n md_field_block = \"{\" [ws] (raw_text as field_block) [ws] \"}\"\n md_generic_field = (identifier as field_name) inline_ws ((md_field_value as field_value) / md_field_block / md_empty_field)\n\n metadata_field = [whitespace] (md_description / md_relations / md_system_status / md_code_status / md_specification / md_notes / md_provenance / md_generic_field) [whitespace]\n md_items = *(metadata_field)\n metadata_block = \"metadata\" [whitespace] \"{\" [ws] (md_items as metadata) [ws] \"}\"\n\n raw_text = *(\"\\\\\" chr / (!\"}\" chr))\n\n # -- aliases ------------------------------------------------------------\n\n alias_decl = [whitespace] (identifier as name) [whitespace] \"=\" [whitespace] (item as value) [whitespace]\n alias_items = *(alias_decl)\n aliases_block = \"aliases\" [whitespace] \"{\" [ws] (alias_items as aliases) [ws] \"}\"\n\n # -- dependencies -------------------------------------------------------\n\n dep_alias = whitespace \"as\" whitespace (identifier as alias)\n dependency_decl = \"dependency\" whitespace (quoted_string as target) [(dep_alias)]\n\n # -- item expressions ---------------------------------------------------\n\n zw_primitive = ((\"boundary\") / (\"line_start\") / (\"line_end\") / (\"word_boundary\") / (\"space\") / (\"non_boundary\")) as primitive\n fn_lit = ((\"upcase\") / (\"downcase\") / (\"title_case\") / (\"reverse\") / (\"strip\") / (\"swapcase\")) as function\n any_char_lit = (\"any_character\") as any_char\n none_lit = (\"none\") as none\n\n digit_cap = digit as digit\n capture_reference_body = \"ref\" \"(\" [ws] digit_cap [ws] \")\"\n capture_reference = capture_reference_body as ref\n\n item_ref = identifier \".\" (identifier as qualified_name)\n alias_reference = (!(keyword) identifier [(\".\" (identifier as qualified_name))] !\"{\" ) as alias\n\n item_seq = item_atom\n item_sep = ([ws] \"+\" [ws]) / whitespace\n item_cont = &item_atom\n\n range_arg = (quoted_string as lo) [ws] \"..\" [ws] (quoted_string as hi)\n list_item = item\n list_items = (list_item *((comma_ws / whitespace) list_item))\n set_arg = ((quoted_string as single)) / (\"[\" [ws] ((list_item *((comma_ws / whitespace) list_item)) as list) [ws] \"]\")\n alias_arg = (!zw_primitive !(keyword) identifier) as alias_ref\n any_arg = range_arg / set_arg / alias_arg\n any_ctor = \"any\" \"(\" [ws] (any_arg as any) [ws] \")\"\n capture_ctor = \"capture\" \"(\" [ws] (item as capture_inner) [ws] \")\"\n maybe_ctor = \"maybe\" \"(\" [ws] (item as maybe_inner) [ws] \")\"\n some_ctor = \"some\" \"(\" [ws] (item as some_inner) [ws] \")\"\n\n item_atom_start = \"\\\"\" / \"'\" / ident_first\n cont_stop = \"to\" / \"before\" / \"after\" / \"not_before\" / \"not_after\" / \"}\"\n alias_decl_stop = identifier [ws] \"=\"\n item_atom = quoted_string / none_lit / zw_primitive / any_char_lit / any_ctor / capture_ctor / maybe_ctor / some_ctor / fn_lit / capture_reference / alias_reference\n item = (item_atom 0*(item_sep !cont_stop !(alias_decl_stop) &item_atom_start item_atom)) as concatenation\n\n # -- rules and stages ---------------------------------------------------\n\n constraint_after = \"after\" whitespace (item as after)\n constraint_before = \"before\" whitespace (item as before)\n constraint_not_before = \"not_before\" whitespace (item as not_before)\n constraint_not_after = \"not_after\" whitespace (item as not_after)\n constraint = constraint_after / constraint_before / constraint_not_before / constraint_not_after\n constraints = *(whitespace constraint)\n\n compact_rule = \"sub\" whitespace (item_atom as from) whitespace (item_atom as to) ((*(whitespace constraint)) as constraints)\n\n block_rule = \"sub\" [ws] \"{\" [ws] \"from\" whitespace (item as from) [ws] \"to\" whitespace (item as to) ((*(whitespace constraint)) as constraints) [ws] \"}\"\n\n rule = block_rule / compact_rule\n\n comment_item = ((\"#\" *(!%x0A chr)) as comment) / ((!(keyword) identifier) as noop)\n rule_line = [ws] (rule / comment_item) [ws]\n rule_items = *(rule_line)\n sequence_block = \"sequence\" [ws] \"{\" [ws] (rule_items as sequence) [ws] \"}\"\n parallel_block = \"parallel\" [ws] \"{\" [ws] (rule_items as parallel) [ws] \"}\"\n\n separator_part = whitespace \"separator\" whitespace (item_atom as separator)\n separate_directive = (\"separate\") as separate [(separator_part)]\n case_directive = ((\"downcase\") / (\"upcase\")) as case\n case_kwargs_part = whitespace (kwarg_list as case_kwargs)\n title_case_directive = (\"title_case\") as case [(case_kwargs_part)]\n string_case_directive = case_directive / title_case_directive\n compose_directive = ((\"compose\") / (\"decompose\")) as compose\n kwarg = (identifier as kwarg_name) [ws] \":\" [ws] ((quoted_string) as kwarg_value)\n kwarg_item = (kwarg) as kwarg\n kwarg_list = (kwarg_item *(comma_ws kwarg_item))\n funcall_kwargs_part = whitespace (kwarg_list as funcall_kwargs)\n rababa_directive = (\"rababa\") as funcall_name [(funcall_kwargs_part)]\n run_dep = \"map.\" (identifier as dep) \".stage.\" (identifier as stage)\n run_stage_only = (\"stage.\" (identifier as stage)) as run_stage_only\n run_rule = \"run\" whitespace ((run_dep) / run_stage_only)\n\n bare_rule = (rule) as bare_rule\n stage_item = [ws] (sequence_block / parallel_block / run_rule / separate_directive / string_case_directive / compose_directive / rababa_directive / bare_rule / comment_item) [ws]\n stage_items = *(stage_item)\n stage_name_paren = \"(\" (identifier as stage_name) \")\"\n stage_name_ws = whitespace (identifier as stage_name)\n stage_block = \"stage\" (stage_name_paren / stage_name_ws) [ws] \"{\" [ws] (stage_items as stage) [ws] \"}\"\n\n # -- tests ---------------------------------------------------------------\n\n test_note = whitespace \"note\" whitespace (quoted_string as note)\n test_line = [whitespace] (quoted_string as input) arrow (quoted_string as expected) [(test_note)] [whitespace]\n test_items = *(test_line)\n tests_block = \"tests\" [whitespace] \"{\" [ws] (test_items as tests) [ws] \"}\"\n\n # -- system --------------------------------------------------------------\n\n block_item = [whitespace] (metadata_block / aliases_block / tests_block / stage_block / dependency_decl) [whitespace]\n body_items = *(block_item)\n system_block = \"system\" whitespace ((quoted_string) as system_code) [whitespace] \"{\" [ws] ((*(block_item)) as body) [ws] \"}\" [whitespace]\n isc_source = [whitespace] ((system_block) as system) [whitespace]\n\n entry isc: isc_source\n}\n","checksum":"sha256:ae411976ee55f727a1ebbf22820cff8000c1d858eb1d4008305ef0b7e48403ad"} diff --git a/lib/interscript/ml.rb b/lib/interscript/ml.rb index 0288dad1..a6697509 100644 --- a/lib/interscript/ml.rb +++ b/lib/interscript/ml.rb @@ -19,7 +19,7 @@ module Interscript::ML # Expected to be loaded before IMF/Byt5Onnx/PlaneModel entry points. def self.require_optional!(gem_name, feature: gem_name) require feature - rescue LoadError => e + rescue LoadError raise Interscript::ExternalUtilError, "the neural layer needs the #{gem_name} gem: gem install #{gem_name} (or use a bundle including it)" end diff --git a/lib/interscript/ml/byt5_onnx.rb b/lib/interscript/ml/byt5_onnx.rb index 6a9f837e..6c2df340 100644 --- a/lib/interscript/ml/byt5_onnx.rb +++ b/lib/interscript/ml/byt5_onnx.rb @@ -1,4 +1,4 @@ -require 'tmpdir' +require "tmpdir" module Interscript::ML # Byte-level seq2seq (ByT5 family) inference over ONNX sessions from an @@ -11,24 +11,24 @@ def initialize(zip_path) Interscript::ML.require_optional!("onnxruntime") @manifest = IMF.manifest(zip_path) graphs = IMF.verify_and_read(zip_path) - @tmpdir = Dir.mktmpdir('secryst-imf') - @encoder = build_session(graphs, 'encoder.onnx') - @kv = @manifest['decoder'] == 'kv' && graphs.key?('decoder-kv.onnx') - @decoder = build_session(graphs, @kv ? 'decoder-kv.onnx' : 'decoder.onnx') + @tmpdir = Dir.mktmpdir("secryst-imf") + @encoder = build_session(graphs, "encoder.onnx") + @kv = @manifest["decoder"] == "kv" && graphs.key?("decoder-kv.onnx") + @decoder = build_session(graphs, @kv ? "decoder-kv.onnx" : "decoder.onnx") @pasts = @kv ? zero_pasts : {} end def translate(text, max_seq_length: 256) ids = IMF.encode(text) - return '' if ids.length == 1 # only the trailing EOS: empty input + return "" if ids.length == 1 # only the trailing EOS: empty input - hidden = @encoder.predict({ input_ids: [ids] })['last_hidden_state'] + hidden = @encoder.predict({input_ids: [ids]})["last_hidden_state"] tokens = @kv ? greedy_kv(hidden, max_seq_length) : greedy_plain(hidden, max_seq_length) IMF.first_alternates(IMF.decode(tokens)) end def id - @manifest['id'] + @manifest["id"] end private @@ -43,7 +43,7 @@ def build_session(graphs, name) def zero_pasts @decoder.inputs.each_with_object({}) do |meta, pasts| - next unless meta[:name].start_with?('past_') + next unless meta[:name].start_with?("past_") shape = meta[:shape] # [batch, heads, past_seq, d_kv]; dynamic dims are Strings heads = shape[1].is_a?(Integer) ? shape[1] : 4 d_kv = shape[3].is_a?(Integer) ? shape[3] : 8 @@ -71,13 +71,13 @@ def greedy_kv(hidden, max_seq_length) generated = [] max_seq_length.times do results = @decoder.predict( - { input_ids: [current], encoder_hidden_states: hidden }.merge(pasts) + {input_ids: [current], encoder_hidden_states: hidden}.merge(pasts) ) - token = argmax(results['logits'].first.last) + token = argmax(results["logits"].first.last) break if token == IMF::EOS_ID generated << token - pasts = pasts.keys.to_h { |name| [name, results[name.sub('past_', 'present_')]] } + pasts = pasts.keys.to_h { |name| [name, results[name.sub("past_", "present_")]] } current = [token] end generated @@ -88,13 +88,13 @@ def greedy_plain(hidden, max_seq_length) generated = [] max_seq_length.times do logits = @decoder.predict( - { input_ids: [decoder_ids], encoder_hidden_states: hidden } - )['logits'] + {input_ids: [decoder_ids], encoder_hidden_states: hidden} + )["logits"] token = argmax(logits.first.last) break if token == IMF::EOS_ID generated << token - decoder_ids = decoder_ids + [token] + decoder_ids += [token] end generated end diff --git a/lib/interscript/ml/imf.rb b/lib/interscript/ml/imf.rb index 6f120a8f..b0a4ec2c 100644 --- a/lib/interscript/ml/imf.rb +++ b/lib/interscript/ml/imf.rb @@ -32,7 +32,7 @@ def encode(text) # corpora emit '/'-separated alternates per token; the runtime # returns the primary choice only. def first_alternates(text) - text.split.map { |tok| tok.split('/', 2).first }.join(' ') + text.split.map { |tok| tok.split("/", 2).first }.join(" ") end def decode(token_ids) @@ -112,7 +112,7 @@ def resolve(model_id, index_url: nil) if channel.start_with?("file://") FileUtils.cp(channel.sub(%r{\Afile://}, ""), tmp) else - URI.open(channel) { |remote| IO.copy_stream(remote, tmp) } + URI.parse(channel).open { |remote| IO.copy_stream(remote, tmp) } end end actual = Digest::SHA256.file(tmp).hexdigest @@ -140,14 +140,14 @@ def download_parts(entry, tmp) if part["url"].start_with?("file://") File.open(source, "rb", &open_stream) else - URI.open(source, "rb", &open_stream) + URI.parse(source).open("rb", &open_stream) end unless digest.hexdigest == part["sha256"] raise RegistryError, "part #{index} of #{entry["filename"]} sha256 mismatch: got #{digest.hexdigest}, index says #{part["sha256"]}" end end end - rescue StandardError + rescue File.delete(tmp) if File.file?(tmp) raise end @@ -167,10 +167,10 @@ def load_index(source) # HTTP sources must ship a sibling .sha256 sidecar; verify the # body against it before parsing (GitHub Releases index assets do). def fetch_index_with_sidecar(source) - body = URI.open(source, "rb") { |remote| remote.read } + body = URI.parse(source).open("rb") { |remote| remote.read } sidecar = begin - URI.open("#{source}.sha256") { |remote| remote.read } + URI.parse("#{source}.sha256").open { |remote| remote.read } rescue OpenURI::HTTPError => e raise RegistryError, "index sha256 sidecar missing: #{source}.sha256 (#{e.message})" end diff --git a/lib/interscript/ml/model.rb b/lib/interscript/ml/model.rb index f11394aa..36053c5b 100644 --- a/lib/interscript/ml/model.rb +++ b/lib/interscript/ml/model.rb @@ -5,30 +5,30 @@ class Model def self.from_file(model_file) # A models.yaml model id resolves (download -> verify -> cache) # through the same contract as the Python/TS runtimes. - model_file = IMF.resolve(model_file) unless model_file.to_s.end_with?('.zip') || File.file?(model_file.to_s) + model_file = IMF.resolve(model_file) unless model_file.to_s.end_with?(".zip") || File.file?(model_file.to_s) model_file = Provisioning.locate(model_file) Zip::File.open(model_file) do |zip_file| - metadata = zip_file.glob('metadata.yaml').first + metadata = zip_file.glob("metadata.yaml").first metadata = YAML.safe_load(metadata.get_input_stream.read) if metadata # IMF v1: the Interscript Model Format zip. - return Byt5Onnx.new(model_file) if metadata && metadata['format'] == 'imf-v1' + return Byt5Onnx.new(model_file) if metadata && metadata["format"] == "imf-v1" - name = metadata && metadata['name'] + name = metadata && metadata["name"] # Modern byte-level seq2seq (ByT5 family): encoder.onnx + decoder.onnx. - return Byt5Onnx.new(model_file) if name == 'byt5' + return Byt5Onnx.new(model_file) if name == "byt5" # Legacy single-file ONNX transformer zips (vocabs.yaml based). - vocabs = zip_file.glob('vocabs.yaml').first - raise 'vocabs.yaml is missing in model zip!' unless vocabs + vocabs = zip_file.glob("vocabs.yaml").first + raise "vocabs.yaml is missing in model zip!" unless vocabs vocabs = YAML.safe_load(vocabs.get_input_stream.read) - input_vocab = Vocab.new(vocabs['input'], specials: []) - target_vocab = Vocab.new(vocabs['target'], specials: []) + input_vocab = Vocab.new(vocabs["input"], specials: []) + target_vocab = Vocab.new(vocabs["target"], specials: []) - onnx = zip_file.glob('*.onnx').first - raise 'onnx model file is missing in model zip!' unless onnx + onnx = zip_file.glob("*.onnx").first + raise "onnx model file is missing in model zip!" unless onnx Onnx.new(onnx.get_input_stream.read, input_vocab, target_vocab) end end @@ -41,11 +41,11 @@ def initialize(model_path_or_bytes, input_vocab, target_vocab) end def call(input, output, opts) - @model.predict({ src: input, tgt: output }.merge(opts))['output'] + @model.predict({src: input, tgt: output}.merge(opts))["output"] end def argmax(*args) - self.call(*args).map { |i| i.flatten.each_with_index.max[1] } + call(*args).map { |i| i.flatten.each_with_index.max[1] } end end end diff --git a/lib/interscript/ml/plane.rb b/lib/interscript/ml/plane.rb index a95f09be..12445cb2 100644 --- a/lib/interscript/ml/plane.rb +++ b/lib/interscript/ml/plane.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'tmpdir' -require 'json' +require "tmpdir" +require "json" module Interscript module ML @@ -16,7 +16,7 @@ class PlaneModel attr_reader :classes, :k_passes def initialize(graph:, classes:, k_passes: 2) - Interscript::ML.require_optional!('onnxruntime') + Interscript::ML.require_optional!("onnxruntime") @classes = classes.freeze @n_classes = classes.length @mask_id = @n_classes @@ -27,8 +27,8 @@ def initialize(graph:, classes:, k_passes: 2) @class_to_id = classes.each_with_index.to_h.freeze # the onnxruntime gem loads from paths, not bytes (as byt5_onnx): # verified bytes go to a tmpfile owned by this instance - @graph_tmpdir = Dir.mktmpdir('isx-plane') - path = File.join(@graph_tmpdir, 'plane.onnx') + @graph_tmpdir = Dir.mktmpdir("isx-plane") + path = File.join(@graph_tmpdir, "plane.onnx") File.binwrite(path, graph) @sess = OnnxRuntime::Model.new(path) end @@ -58,7 +58,7 @@ def split_planes(text) else close.call skeleton << ch - classes << '' + classes << "" end end close.call @@ -75,18 +75,18 @@ def translate(text, preserve_diacritics: false) ch = skeleton[i] n = ch.bytesize cls = if user_cls.empty? - votes = tok_preds[pos, n] || [] - cid = votes.tally.max_by { |_, c| c }&.first || @n_classes - cid < @n_classes ? @classes[cid] : '' - else - user_cls # user diacritics round-trip byte-exactly - end + votes = tok_preds[pos, n] || [] + cid = votes.tally.max_by { |_, c| c }&.first || @n_classes + (cid < @n_classes) ? @classes[cid] : "" + else + user_cls # user diacritics round-trip byte-exactly + end pos += n cls end out = +"" skeleton.each_char.with_index do |ch, i| - out << (ch == "\x00" ? final[i] : ch + final[i]) + out << ((ch == "\x00") ? final[i] : ch + final[i]) end out end @@ -102,7 +102,7 @@ def translate_plain(text) votes = preds[pos, nbytes] || [] cid = votes.tally.max_by { |_, c| c }&.first || @n_classes pos += nbytes - cid < @n_classes ? @classes[cid] : '' + (cid < @n_classes) ? @classes[cid] : "" end PlaneModel.render_plane(text, char_preds) end @@ -111,7 +111,7 @@ def k_pass_decode(ids, plane0) plane = plane0 preds = nil @k_passes.times do - logits = @sess.predict({ 'input_ids' => [ids], 'plane_ids' => [plane] })['class_logits'][0] + logits = @sess.predict({"input_ids" => [ids], "plane_ids" => [plane]})["class_logits"][0] preds = logits.each_with_index.map { |row, i| row.index(row.max) } plane = preds end @@ -129,11 +129,11 @@ def decode_pinned(skeleton, char_classes) pid = @class_to_id.fetch(cls, @mask_id) ch.bytesize.times { per_token_pin << pid } end - plane0 = per_token_pin.map { |p| p == @mask_id ? @mask_id : p } + plane0 = per_token_pin.map { |p| (p == @mask_id) ? @mask_id : p } k_pass_decode(ids, plane0) end - def self.render_plane(skeleton, char_classes) + def self.render_plane(skeleton, char_classes) # rubocop:disable Lint/IneffectiveAccessModifier skeleton.each_char.with_index.map do |ch, i| ch + char_classes[i].to_s end.join @@ -142,14 +142,15 @@ def self.render_plane(skeleton, char_classes) # The zip contract: metadata.yaml (kind=plane, k_passes, member # sha256s) + plane.onnx + classes.json, every member sha-verified # before the session is created. + # rubocop:disable Lint/IneffectiveAccessModifier def self.from_zip(data) - require 'zip' + require "zip" zf = Zip::File.open_buffer(data) names = zf.entries.map(&:name) - meta = YAML.safe_load(zf.read('metadata.yaml'), permitted_classes: [], aliases: false) - raise IMF::FormatError, "plane zip: kind=#{meta['kind'].inspect}, expected 'plane'" if meta['kind'] != 'plane' + meta = YAML.safe_load(zf.read("metadata.yaml"), permitted_classes: [], aliases: false) + raise IMF::FormatError, "plane zip: kind=#{meta["kind"].inspect}, expected 'plane'" if meta["kind"] != "plane" - members = meta['members'] || {} + members = meta["members"] || {} plane_graph = nil classes_bytes = nil %w[plane.onnx classes.json].each do |member| @@ -161,11 +162,11 @@ def self.from_zip(data) got = Digest::SHA256.hexdigest(bytes) raise IMF::FormatError, "plane zip: #{member} sha256 mismatch (#{got})" if want != got - plane_graph = bytes if member == 'plane.onnx' - classes_bytes = bytes if member == 'classes.json' + plane_graph = bytes if member == "plane.onnx" + classes_bytes = bytes if member == "classes.json" end classes = JSON.parse(classes_bytes) - new(graph: plane_graph, classes: classes, k_passes: meta.fetch('k_passes', 2)) + new(graph: plane_graph, classes: classes, k_passes: meta.fetch("k_passes", 2)) ensure zf&.close end @@ -173,6 +174,8 @@ def self.from_zip(data) module_function + # rubocop:enable Lint/IneffectiveAccessModifier + def render_plane(skeleton, char_classes) PlaneModel.render_plane(skeleton, char_classes) end diff --git a/lib/interscript/ml/provisioning.rb b/lib/interscript/ml/provisioning.rb index 2c58bec8..a6e3be9d 100644 --- a/lib/interscript/ml/provisioning.rb +++ b/lib/interscript/ml/provisioning.rb @@ -1,8 +1,8 @@ -require 'open-uri' -require 'uri' -require 'fileutils' -require 'digest/sha2' -require 'yaml' +require "open-uri" +require "uri" +require "fileutils" +require "digest/sha2" +require "yaml" module Interscript::ML # Module Interscript::ML::Provisioning is to provision remote models locally and to @@ -37,7 +37,7 @@ def prepare_environment ([ENV["INTERSCRIPT_ML_DATA"] || ENV["SECRYST_DATA"]] + possible_paths).compact.each do |path| FileUtils.mkdir_p(path) - @write_path = path unless @write_path + @write_path ||= path rescue ensure @read_paths << path if File.readable?(path) @@ -48,11 +48,11 @@ def prepare_environment # Now, let's locate the first Secrystfile to be found path = Dir.pwd secrystfilepath = loop do - break unless path =~ %r{[/\\]} + break unless %r{[/\\]}.match?(path) if File.readable?(path + "/Secrystfile") break path + "/Secrystfile" end - path = path.sub(%r{[/\\][^/\\]*?\z}, '') + path = path.sub(%r{[/\\][^/\\]*?\z}, "") end # It's found, so let's parse it. @@ -70,12 +70,12 @@ def prepare_environment @loaded_remotes = @remotes.map do |uri| cache_path = "#{@write_path}/remotes/#{Digest::SHA256.hexdigest(uri)}.yaml" if !File.exist?(cache_path) - data = URI.open(uri).read + data = URI.parse(uri).open.read File.write(cache_path, data) elsif File.mtime(cache_path) + 60 < Time.now begin # Just *try* to download it. - data = URI.open(uri).read + data = URI.parse(uri).open.read File.write(cache_path, data) rescue end @@ -99,7 +99,7 @@ def self.candidate_paths def locate(name) # Shortcut this if user gave a filename. - return name if name =~ %r{[.\\/]} + return name if %r{[.\\/]}.match?(name) prepare_environment @@ -130,24 +130,24 @@ def locate(name) def download(model, remote:) uri = model.uri - if uri.start_with?("./") || uri.start_with?("../") + if uri.start_with?("./", "../") remote_uri = remote.uri - if remote_uri =~ %r{\A/|\A\w:[\\/]} - remote_uri = "file://"+remote_uri + if %r{\A/|\A\w:[\\/]}.match?(remote_uri) + remote_uri = "file://" + remote_uri end uri = URI(remote_uri).merge(uri).to_s - uri = uri.sub(%r{\Afile://}, '') + uri = uri.sub(%r{\Afile://}, "") end warn "* Downloading a Secryst model #{model.name}:#{model.version} from #{uri}..." - data = URI.open(uri).read + data = URI.parse(uri).open.read path = @write_path + "/models/" + model.name FileUtils.mkdir_p path File.write(path + "/version", model.version) File.write(path + "/model.zip", data) - return path + "/model.zip" + path + "/model.zip" end class Remotefile @@ -166,7 +166,7 @@ def resolve(name) end end - class Model < Struct.new(:name, :uri, :version, keyword_init: true) + class Model < Struct.new(:name, :uri, :version) end end @@ -174,7 +174,7 @@ class Secrystfile def initialize(path) @path = path @models, @remotes = [], [] - self.instance_eval(File.read(path), path) + instance_eval(File.read(path), path) end def model(name) diff --git a/lib/interscript/ml/translator.rb b/lib/interscript/ml/translator.rb index c7c8fca4..5adb352c 100644 --- a/lib/interscript/ml/translator.rb +++ b/lib/interscript/ml/translator.rb @@ -6,7 +6,7 @@ class Translator attr_accessor :model def initialize(model_file:) - @device = 'cpu' + @device = "cpu" @model = Model.from_file(model_file) end @@ -14,9 +14,9 @@ def translate(phrase, max_seq_length: 100) return @model.translate(phrase, max_seq_length: max_seq_length) if @model.is_a?(Byt5Onnx) Interscript::ML.require_optional!("numo-narray", feature: "numo/narray") - input = [''] + phrase.chars + [''] + input = [""] + phrase.chars + [""] input = Numo::NArray[input.map { |i| @model.input_vocab.stoi[i] }].transpose - output = Numo::NArray[[@model.target_vocab.stoi['']]] + output = Numo::NArray[[@model.target_vocab.stoi[""]]] src_key_padding_mask = input.transpose.eq(1) max_seq_length.times do |i| @@ -27,12 +27,12 @@ def translate(phrase, max_seq_length: 100) src_key_padding_mask: src_key_padding_mask, tgt_key_padding_mask: tgt_key_padding_mask, memory_key_padding_mask: src_key_padding_mask) - break if @model.target_vocab.itos[prediction[i]] == '' + break if @model.target_vocab.itos[prediction[i]] == "" output = Numo::NArray.concatenate([output, Numo::NArray[[prediction[i]]]]) end - output[1..-1].to_a.flatten.map { |i| @model.target_vocab.itos[i] }.join('') + output[1..].to_a.flatten.map { |i| @model.target_vocab.itos[i] }.join("") end end end diff --git a/lib/interscript/ml/vocab.rb b/lib/interscript/ml/vocab.rb index 4eebbddd..435baa19 100644 --- a/lib/interscript/ml/vocab.rb +++ b/lib/interscript/ml/vocab.rb @@ -29,7 +29,6 @@ def initialize( @stoi = {} end - # stoi is simply a reverse dict for itos @itos.each_with_index do |tok, i| @stoi[tok] = i @@ -45,4 +44,4 @@ def length end alias_method :size, :length end -end \ No newline at end of file +end diff --git a/lib/interscript/stdlib/functions/secryst_adapter.rb b/lib/interscript/stdlib/functions/secryst_adapter.rb index 89293602..f2da15c2 100644 --- a/lib/interscript/stdlib/functions/secryst_adapter.rb +++ b/lib/interscript/stdlib/functions/secryst_adapter.rb @@ -29,8 +29,6 @@ def build_translator(model_key) end Interscript::ML::Translator.new(model_file: model_key) end - - end end end diff --git a/lib/interscript/version.rb b/lib/interscript/version.rb index 04ecbb3f..c45cf4e0 100644 --- a/lib/interscript/version.rb +++ b/lib/interscript/version.rb @@ -1,3 +1,3 @@ module Interscript - VERSION = "3.0.1" + VERSION = "3.1.0" end diff --git a/spec/interscript/isc/parg_artifact_spec.rb b/spec/interscript/isc/parg_artifact_spec.rb index 52681c76..0c2a0579 100644 --- a/spec/interscript/isc/parg_artifact_spec.rb +++ b/spec/interscript/isc/parg_artifact_spec.rb @@ -13,6 +13,7 @@ skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact) it "parses every corpus map identically via artifact and DSL" do + skip "artifact not shipped (parsanol#5 — compiler mistranslates; DSL is the engine)" unless File.exist?(Interscript::Isc::Parser::ARTIFACT_PATH) failures = [] maps.each do |path| diff --git a/spec/interscript/isc/parser_spec.rb b/spec/interscript/isc/parser_spec.rb index b4c5c488..2385b44d 100644 --- a/spec/interscript/isc/parser_spec.rb +++ b/spec/interscript/isc/parser_spec.rb @@ -65,6 +65,7 @@ it "parses through the shipped .parg artifact when parsanol has PARG" do skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact) + skip "artifact not shipped (parsanol#5 — compiler mistranslates; DSL is the engine)" unless File.exist?(Interscript::Isc::Parser::ARTIFACT_PATH) tree = described_class.parse(src) doc = Interscript::Isc::DocumentBuilder.build(tree, filename: "test.isc") @@ -74,6 +75,7 @@ it "produces byte-identical trees via artifact and DSL paths" do skip "parsanol lacks PARG (needs >= 1.3.61)" unless Interscript::Isc::Parser.respond_to?(:artifact) + skip "artifact not shipped (parsanol#5 — compiler mistranslates; DSL is the engine)" unless File.exist?(Interscript::Isc::Parser::ARTIFACT_PATH) artifact_tree = described_class.parse_with_artifact(src) dsl_tree = described_class.parse_with_dsl(src) diff --git a/spec/interscript/ml/byt5_onnx_spec.rb b/spec/interscript/ml/byt5_onnx_spec.rb index efc810e8..357a329e 100644 --- a/spec/interscript/ml/byt5_onnx_spec.rb +++ b/spec/interscript/ml/byt5_onnx_spec.rb @@ -1,17 +1,17 @@ -require 'spec_helper' +require "spec_helper" require "interscript/ml/byt5_onnx" RSpec.describe Interscript::ML::Byt5Onnx do - it 'uses the ByT5 byte conventions' do + it "uses the ByT5 byte conventions" do expect(Interscript::ML::IMF::PAD_ID).to eq(0) expect(Interscript::ML::IMF::EOS_ID).to eq(1) end - describe 'byte round-trip (as used for generated output)' do - it 'packs generated bytes back into UTF-8 text' do - text = 'ភាសាខ្មែរ' + describe "byte round-trip (as used for generated output)" do + it "packs generated bytes back into UTF-8 text" do + text = "ភាសាខ្មែរ" bytes = text.bytes - expect(bytes.pack('C*').force_encoding(Encoding::UTF_8)).to eq(text) + expect(bytes.pack("C*").force_encoding(Encoding::UTF_8)).to eq(text) end end end diff --git a/spec/interscript/ml/golden_spec.rb b/spec/interscript/ml/golden_spec.rb index 1c2116b0..e0a70b1b 100644 --- a/spec/interscript/ml/golden_spec.rb +++ b/spec/interscript/ml/golden_spec.rb @@ -51,7 +51,7 @@ # gem's ORT build to close it, don't erode the default tiers artifact_bound = ENV["SECRYST_E2E_CORPUS_BOUND"]&.to_f corpus_bound = artifact_bound || - (ENV["SECRYST_E2E_SMOKE"] == "1" ? 0.02 : 0.005) + ((ENV["SECRYST_E2E_SMOKE"] == "1") ? 0.02 : 0.005) expect(total_diffs.to_f / total_chars).to be < corpus_bound end end diff --git a/spec/interscript/ml/imf_spec.rb b/spec/interscript/ml/imf_spec.rb index 11e02e82..9c42f50e 100644 --- a/spec/interscript/ml/imf_spec.rb +++ b/spec/interscript/ml/imf_spec.rb @@ -1,59 +1,60 @@ require "spec_helper" require "zip" -require 'interscript/ml' -require 'fileutils' -require 'tmpdir' -require 'json' +require "interscript/ml" +require "fileutils" +require "tmpdir" +require "json" RSpec.describe Interscript::ML::IMF do - it 'uses the canonical ByT5 table (byte + 3, trailing EOS)' do - expect(described_class.encode('rok')).to eq([117, 114, 110, 1]) - expect(described_class.decode([117, 114, 110])).to eq('rok') - expect(described_class.decode([117, 1, 114])).to eq('r') + it "uses the canonical ByT5 table (byte + 3, trailing EOS)" do + expect(described_class.encode("rok")).to eq([117, 114, 110, 1]) + expect(described_class.decode([117, 114, 110])).to eq("rok") + expect(described_class.decode([117, 1, 114])).to eq("r") end - describe 'with the tiny IMF fixture zip' do - let(:zip_path) { File.expand_path('../fixtures/tiny-imf.zip', __dir__) } + describe "with the tiny IMF fixture zip" do + let(:zip_path) { File.expand_path("../fixtures/tiny-imf.zip", __dir__) } - it 'parses the manifest and verifies graph checksums' do + it "parses the manifest and verifies graph checksums" do meta = described_class.manifest(zip_path) - expect(meta['format']).to eq('imf-v1') - expect(meta['tokenizer']).to eq('bytes') + expect(meta["format"]).to eq("imf-v1") + expect(meta["tokenizer"]).to eq("bytes") graphs = described_class.verify_and_read(zip_path) - expect(graphs.keys.sort).to eq(['decoder.onnx', 'encoder.onnx']) + expect(graphs.keys.sort).to eq(["decoder.onnx", "encoder.onnx"]) end - it 'loads sessions from verified bytes' do + it "loads sessions from verified bytes" do model = Interscript::ML::Byt5Onnx.new(zip_path) - expect(model.id).to eq('tiny-1.0') + expect(model.id).to eq("tiny-1.0") end - it 'decodes a real zip against the shared golden set', :e2e do - zip = ENV['SECRYST_E2E_ZIP'] - golden = ENV['SECRYST_GOLDEN'] - skip 'set SECRYST_E2E_ZIP and SECRYST_GOLDEN for the end-to-end run' unless zip && golden + it "decodes a real zip against the shared golden set", :e2e do + zip = ENV["SECRYST_E2E_ZIP"] + golden = ENV["SECRYST_GOLDEN"] + skip "set SECRYST_E2E_ZIP and SECRYST_GOLDEN for the end-to-end run" unless zip && golden translator = Interscript::ML::Translator.new(model_file: zip) ok = 0 total = 0 File.readlines(golden).each do |line| row = JSON.parse(line) total += 1 - ok += 1 if translator.translate(row['input'], max_seq_length: 128) == row['output'] + ok += 1 if translator.translate(row["input"], max_seq_length: 128) == row["output"] end expect(ok).to eq(total) end end - it 'rejects a tampered zip loudly' do + it "rejects a tampered zip loudly" do + skip "windows: rubyzip finalizer holds the tmp zip open (EACCES on mktmpdir cleanup)" if Gem.win_platform? Dir.mktmpdir do |tmp| - source = File.expand_path('../fixtures/tiny-imf.zip', __dir__) - tampered = File.join(tmp, 'tampered.zip') - require 'zip' + source = File.expand_path("../fixtures/tiny-imf.zip", __dir__) + tampered = File.join(tmp, "tampered.zip") + require "zip" Zip::File.open(source) do |src| Zip::File.open(tampered, create: true) do |dst| src.entries.each do |e| - if e.name == 'encoder.onnx' - dst.get_output_stream(e.name) { |io| io.write('corrupted-bytes') } + if e.name == "encoder.onnx" + dst.get_output_stream(e.name) { |io| io.write("corrupted-bytes") } else dst.get_output_stream(e.name) { |io| io.write(e.get_input_stream.read) } end @@ -65,15 +66,15 @@ end end - describe '.resolve' do - it 'installs a verified copy into the cache from a local index' do + describe ".resolve" do + it "installs a verified copy into the cache from a local index" do Dir.mktmpdir do |tmp| - channel = File.join(tmp, 'channel') + channel = File.join(tmp, "channel") FileUtils.mkdir_p(channel) - zip_path = File.expand_path('../fixtures/tiny-imf.zip', __dir__) - FileUtils.cp(zip_path, File.join(channel, 'tiny.zip')) - require 'digest' - index = File.join(tmp, 'models.yaml') + zip_path = File.expand_path("../fixtures/tiny-imf.zip", __dir__) + FileUtils.cp(zip_path, File.join(channel, "tiny.zip")) + require "digest" + index = File.join(tmp, "models.yaml") File.write(index, <<~YAML) version: 1 models: @@ -82,41 +83,41 @@ url: file://#{channel}/tiny.zip sha256: #{Digest::SHA256.file(zip_path).hexdigest} YAML - cache = File.join(tmp, 'cache') - result = described_class.resolve('tiny-1.0', index_url: index) if false + cache = File.join(tmp, "cache") + # env-based cache (the public API reads ENV at call time) - ENV['SECRYST_CACHE'] = cache + ENV["SECRYST_CACHE"] = cache begin - installed = described_class.resolve('tiny-1.0', index_url: index) - expect(installed).to eq(File.join(cache, 'models', 'tiny-1.0', 'tiny.zip')) + installed = described_class.resolve("tiny-1.0", index_url: index) + expect(installed).to eq(File.join(cache, "models", "tiny-1.0", "tiny.zip")) expect(File.file?(installed)).to be(true) - FileUtils.rm_f(File.join(channel, 'tiny.zip')) - expect(described_class.resolve('tiny-1.0', index_url: index)).to eq(installed) + FileUtils.rm_f(File.join(channel, "tiny.zip")) + expect(described_class.resolve("tiny-1.0", index_url: index)).to eq(installed) ensure - ENV.delete('SECRYST_CACHE') + ENV.delete("SECRYST_CACHE") end end end - it 'raises for unknown ids' do + it "raises for unknown ids" do Dir.mktmpdir do |tmp| - index = File.join(tmp, 'models.yaml') + index = File.join(tmp, "models.yaml") File.write(index, "version: 1\nmodels: {}\n") - expect { described_class.resolve('nope-1.0', index_url: index) } + expect { described_class.resolve("nope-1.0", index_url: index) } .to raise_error(Interscript::ML::IMF::RegistryError, /unknown model id/) end end - it 'assembles and verifies split parts (the >2GiB GitHub cap path)' do + it "assembles and verifies split parts (the >2GiB GitHub cap path)" do Dir.mktmpdir do |tmp| - channel = File.join(tmp, 'channel') + channel = File.join(tmp, "channel") FileUtils.mkdir_p(channel) - zip_path = File.expand_path('../fixtures/tiny-imf.zip', __dir__) + zip_path = File.expand_path("../fixtures/tiny-imf.zip", __dir__) blob = File.binread(zip_path) part_a, part_b = blob[0, (blob.bytesize / 2 + 3)], blob[(blob.bytesize / 2 + 3)..] - File.binwrite(File.join(channel, 'tiny.zip.part-00'), part_a) - File.binwrite(File.join(channel, 'tiny.zip.part-01'), part_b) - index = File.join(tmp, 'models.yaml') + File.binwrite(File.join(channel, "tiny.zip.part-00"), part_a) + File.binwrite(File.join(channel, "tiny.zip.part-01"), part_b) + index = File.join(tmp, "models.yaml") File.write(index, <<~YAML) version: 1 models: @@ -131,29 +132,29 @@ sha256: #{Digest::SHA256.hexdigest(part_b)} size: #{part_b.bytesize} YAML - cache = File.join(tmp, 'cache') - ENV['SECRYST_CACHE'] = cache + cache = File.join(tmp, "cache") + ENV["SECRYST_CACHE"] = cache begin - installed = described_class.resolve('tiny-1.0', index_url: index) + installed = described_class.resolve("tiny-1.0", index_url: index) expect(File.binread(installed)).to eq(blob) - FileUtils.rm_f(File.join(channel, 'tiny.zip.part-00')) - expect(described_class.resolve('tiny-1.0', index_url: index)).to eq(installed) + FileUtils.rm_f(File.join(channel, "tiny.zip.part-00")) + expect(described_class.resolve("tiny-1.0", index_url: index)).to eq(installed) ensure - ENV.delete('SECRYST_CACHE') + ENV.delete("SECRYST_CACHE") end end end - it 'rejects a corrupt part by index' do + it "rejects a corrupt part by index" do Dir.mktmpdir do |tmp| - channel = File.join(tmp, 'channel') + channel = File.join(tmp, "channel") FileUtils.mkdir_p(channel) - zip_path = File.expand_path('../fixtures/tiny-imf.zip', __dir__) + zip_path = File.expand_path("../fixtures/tiny-imf.zip", __dir__) blob = File.binread(zip_path) part_a, part_b = blob[0, 7], blob[7..] - File.binwrite(File.join(channel, 'tiny.zip.part-00'), part_a) - File.binwrite(File.join(channel, 'tiny.zip.part-01'), part_b) - index = File.join(tmp, 'models.yaml') + File.binwrite(File.join(channel, "tiny.zip.part-00"), part_a) + File.binwrite(File.join(channel, "tiny.zip.part-01"), part_b) + index = File.join(tmp, "models.yaml") File.write(index, <<~YAML) version: 1 models: @@ -168,12 +169,12 @@ sha256: #{Digest::SHA256.hexdigest(part_b)} size: #{part_b.bytesize} YAML - ENV['SECRYST_CACHE'] = File.join(tmp, 'cache') + ENV["SECRYST_CACHE"] = File.join(tmp, "cache") begin - expect { described_class.resolve('tiny-1.0', index_url: index) } + expect { described_class.resolve("tiny-1.0", index_url: index) } .to raise_error(Interscript::ML::IMF::RegistryError, /part 0 .* sha256 mismatch/) ensure - ENV.delete('SECRYST_CACHE') + ENV.delete("SECRYST_CACHE") end end end @@ -201,7 +202,11 @@ port = server.addr[1] thread = Thread.new do loop do - client = server.accept rescue break + client = begin + server.accept + rescue + break + end request = client.readline client.each_line { |line| break if line == "\r\n" } path = request.split(" ")[1] diff --git a/spec/interscript/ml/plane_spec.rb b/spec/interscript/ml/plane_spec.rb index 6b39a1fd..daf4dd56 100644 --- a/spec/interscript/ml/plane_spec.rb +++ b/spec/interscript/ml/plane_spec.rb @@ -11,16 +11,16 @@ describe ".from_zip" do it "verifies member sha256s and rejects tampering" do expect { described_class.from_zip(File.read(zip)) }.not_to raise_error - require 'zip' - require 'stringio' + require "zip" + require "stringio" src = Zip::File.open(zip) bad = Zip::File.open_buffer(StringIO.new(File.binread(zip))) - bad_entry = bad.entries.find { |e| e.name == 'plane.onnx' } + bad_entry = bad.entries.find { |e| e.name == "plane.onnx" } tampered = bad_entry.get_input_stream.read + "x" buf = Zip::OutputStream.write_buffer do |out| src.entries.each do |e| out.put_next_entry(e.name) - out.write(e.name == 'plane.onnx' ? tampered : e.get_input_stream.read) + out.write((e.name == "plane.onnx") ? tampered : e.get_input_stream.read) end end.string expect { described_class.from_zip(buf) }.to raise_error(Interscript::ML::IMF::FormatError, /sha256 mismatch/)