From 896f70786028899af02b4b3aa443c64fde8e569f Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Tue, 17 Mar 2026 22:39:36 +0100 Subject: [PATCH 1/9] Initial creation of the wrapper gem --- .github/workflows/test.yml | 19 +++++- .tool-versions | 1 + gem/.gitignore | 15 +++++ gem/.rspec | 3 + gem/.rubocop.yml | 16 +++++ gem/Gemfile | 6 ++ gem/Gemfile.lock | 107 ++++++++++++++++++++++++++++++++++ gem/README.md | 35 +++++++++++ gem/Rakefile | 12 ++++ gem/bin/console | 11 ++++ gem/bin/setup | 8 +++ gem/lib/triangulum.rb | 8 +++ gem/lib/triangulum/version.rb | 5 ++ gem/spec/spec_helper.rb | 15 +++++ gem/spec/triangulum_spec.rb | 7 +++ gem/triangulum.gemspec | 47 +++++++++++++++ 16 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 gem/.gitignore create mode 100644 gem/.rspec create mode 100644 gem/.rubocop.yml create mode 100644 gem/Gemfile create mode 100644 gem/Gemfile.lock create mode 100644 gem/README.md create mode 100644 gem/Rakefile create mode 100644 gem/bin/console create mode 100644 gem/bin/setup create mode 100644 gem/lib/triangulum.rb create mode 100644 gem/lib/triangulum/version.rb create mode 100644 gem/spec/spec_helper.rb create mode 100644 gem/spec/triangulum_spec.rb create mode 100644 gem/triangulum.gemspec diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 915e97d..30bcccd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: - 'main' pull_request: jobs: - test: + typescript: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -16,3 +16,20 @@ jobs: - run: npm ci - run: npm run build - run: npm run test + + ruby: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.4.7' + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: Run the default task + run: bundle exec rake diff --git a/.tool-versions b/.tool-versions index 1b05724..cce1df0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ nodejs 24.13.0 +ruby 3.4.7 diff --git a/gem/.gitignore b/gem/.gitignore new file mode 100644 index 0000000..b41e419 --- /dev/null +++ b/gem/.gitignore @@ -0,0 +1,15 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# intellij +/.idea/ +*.iml + +# rspec failure tracking +.rspec_status diff --git a/gem/.rspec b/gem/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/gem/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/gem/.rubocop.yml b/gem/.rubocop.yml new file mode 100644 index 0000000..5fd0b03 --- /dev/null +++ b/gem/.rubocop.yml @@ -0,0 +1,16 @@ +plugins: + - rubocop-rake + - rubocop-rspec + +AllCops: + TargetRubyVersion: 3.1 + NewCops: enable + +Gemspec/DevelopmentDependencies: + EnforcedStyle: gemspec + +Metrics: + Enabled: false + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes diff --git a/gem/Gemfile b/gem/Gemfile new file mode 100644 index 0000000..2e0652b --- /dev/null +++ b/gem/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Specify your gem's dependencies in triangulum.gemspec +gemspec diff --git a/gem/Gemfile.lock b/gem/Gemfile.lock new file mode 100644 index 0000000..b51f106 --- /dev/null +++ b/gem/Gemfile.lock @@ -0,0 +1,107 @@ +PATH + remote: . + specs: + triangulum (0.0.0) + +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.9) + public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + bigdecimal (4.0.1) + date (3.5.1) + diff-lcs (1.6.2) + erb (6.0.2) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.19.1) + json-schema (6.2.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + mcp (0.8.0) + json-schema (>= 4.1) + parallel (1.27.0) + parser (3.3.10.2) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio + public_suffix (7.0.5) + racc (1.8.1) + rainbow (3.1.1) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + rubocop (1.85.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + mcp (~> 0.6) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-rake (0.7.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.6.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + ruby-progressbar (1.13.0) + stringio (3.2.0) + tsort (0.2.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + irb + rake (~> 13.0) + rspec (~> 3.0) + rubocop (~> 1.21) + rubocop-rake (~> 0.7.0) + rubocop-rspec (~> 3.0) + triangulum! + +BUNDLED WITH + 2.6.9 diff --git a/gem/README.md b/gem/README.md new file mode 100644 index 0000000..523bada --- /dev/null +++ b/gem/README.md @@ -0,0 +1,35 @@ +# Triangulum + +TODO: Delete this and the text below, and describe your gem + +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/triangulum`. To experiment with that code, run `bin/console` for an interactive prompt. + +## Installation + +TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. + +Install the gem and add to the application's Gemfile by executing: + +```bash +bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` + +If bundler is not being used to manage dependencies, install the gem by executing: + +```bash +gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +``` + +## Usage + +TODO: Write usage instructions here + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/triangulum. diff --git a/gem/Rakefile b/gem/Rakefile new file mode 100644 index 0000000..4964751 --- /dev/null +++ b/gem/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +require 'rubocop/rake_task' + +RuboCop::RakeTask.new + +task default: %i[spec rubocop] diff --git a/gem/bin/console b/gem/bin/console new file mode 100644 index 0000000..1432b05 --- /dev/null +++ b/gem/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'triangulum' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require 'irb' +IRB.start(__FILE__) diff --git a/gem/bin/setup b/gem/bin/setup new file mode 100644 index 0000000..dce67d8 --- /dev/null +++ b/gem/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/gem/lib/triangulum.rb b/gem/lib/triangulum.rb new file mode 100644 index 0000000..bc86911 --- /dev/null +++ b/gem/lib/triangulum.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative 'triangulum/version' + +module Triangulum + class Error < StandardError; end + # Your code goes here... +end diff --git a/gem/lib/triangulum/version.rb b/gem/lib/triangulum/version.rb new file mode 100644 index 0000000..8c585f5 --- /dev/null +++ b/gem/lib/triangulum/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Triangulum + VERSION = '0.0.0' +end diff --git a/gem/spec/spec_helper.rb b/gem/spec/spec_helper.rb new file mode 100644 index 0000000..9606817 --- /dev/null +++ b/gem/spec/spec_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'triangulum' + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/gem/spec/triangulum_spec.rb b/gem/spec/triangulum_spec.rb new file mode 100644 index 0000000..2e358b7 --- /dev/null +++ b/gem/spec/triangulum_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe Triangulum do + it 'has a version number' do + expect(Triangulum::VERSION).not_to be_nil + end +end diff --git a/gem/triangulum.gemspec b/gem/triangulum.gemspec new file mode 100644 index 0000000..7415e62 --- /dev/null +++ b/gem/triangulum.gemspec @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative 'lib/triangulum/version' + +Gem::Specification.new do |spec| + spec.name = 'triangulum' + spec.version = Triangulum::VERSION + spec.authors = ['Niklas van Schrick'] + spec.email = ['mc.taucher2003@gmail.com'] + + spec.summary = 'Triangulum is the CodeZero validation layer' + spec.homepage = 'https://github.com/code0-tech/triangulum' + spec.required_ruby_version = '>= 3.1.0' + + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = spec.homepage + spec.metadata['changelog_uri'] = "#{spec.homepage}/releases" + spec.metadata['rubygems_mfa_required'] = 'true' + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) + end + end + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ['lib'] + + # Uncomment to register a new dependency of your gem + # spec.add_dependency "example-gem", "~> 1.0" + + # For more information and examples about making a new gem, check out our + # guide at: https://bundler.io/guides/creating_gem.html + + spec.add_development_dependency 'irb' + spec.add_development_dependency 'rake', '~> 13.0' + + spec.add_development_dependency 'rspec', '~> 3.0' + + spec.add_development_dependency 'rubocop', '~> 1.21' + spec.add_development_dependency 'rubocop-rake', '~> 0.7.0' + spec.add_development_dependency 'rubocop-rspec', '~> 3.0' +end From 4cb432a48a9e883f6a50d6a26d7fce8b53b05ace Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:05:23 +0100 Subject: [PATCH 2/9] Add process entrypoint --- entrypoint/.gitignore | 2 + entrypoint/mapper.ts | 126 ++++++++++++++++++++++++++++++++ entrypoint/package-lock.json | 81 ++++++++++++++++++++ entrypoint/package.json | 13 ++++ entrypoint/readSingle.ts | 39 ++++++++++ entrypoint/single-validation.ts | 10 +++ entrypoint/tsconfig.json | 9 +++ 7 files changed, 280 insertions(+) create mode 100644 entrypoint/.gitignore create mode 100644 entrypoint/mapper.ts create mode 100644 entrypoint/package-lock.json create mode 100644 entrypoint/package.json create mode 100644 entrypoint/readSingle.ts create mode 100644 entrypoint/single-validation.ts create mode 100644 entrypoint/tsconfig.json diff --git a/entrypoint/.gitignore b/entrypoint/.gitignore new file mode 100644 index 0000000..2720d9d --- /dev/null +++ b/entrypoint/.gitignore @@ -0,0 +1,2 @@ +*.iml +node_modules/ diff --git a/entrypoint/mapper.ts b/entrypoint/mapper.ts new file mode 100644 index 0000000..45a5ee8 --- /dev/null +++ b/entrypoint/mapper.ts @@ -0,0 +1,126 @@ +import {SingleValidationInputData} from "./readSingle"; +import { + DataType, + Flow, + FunctionDefinition, + LiteralValue, + NodeFunction, + NodeParameter, + NodeParameterValue, ParameterDefinition, +} from "@code0-tech/sagittarius-graphql-types"; +import { + DefinitionDataType, + NodeFunction as TucanaNodeFunction, + NodeParameter as TucanaNodeParameter, RuntimeFunctionDefinition, RuntimeParameterDefinition, + ValidationFlow, +} from "@code0-tech/tucana/shared"; +import {toAllowedValue} from "@code0-tech/tucana/helpers"; + +export type TriangulumFlowValidationInput = { + flow?: Flow, + functions?: FunctionDefinition[], + dataTypes?: DataType[] +} + +export function mapToFlowValidation(data: SingleValidationInputData): TriangulumFlowValidationInput { + return { + flow: mapFlow(data.flow!), + functions: data.functions.map(mapFunctionDefinition), + dataTypes: data.dataTypes.map(mapDataType) + } +} + +function gid(type: string, id: bigint | number) { + return `gid://sagittarius/${type}/${Number(id)}`; +} + +function mapFlow(flow: ValidationFlow): Flow { + return { + __typename: "Flow", + id: gid('Flow', flow.flowId) as Flow['id'], + inputType: flow.inputType, + returnType: flow.returnType, + startingNodeId: gid('NodeFunction', flow.startingNodeId) as NodeFunction['id'], + nodes: { + nodes: flow.nodeFunctions.map(mapNodeFunction) + } + }; +} + +function mapNodeFunction(nodeFunction: TucanaNodeFunction): NodeFunction { + return { + id: gid('NodeFunction', nodeFunction.databaseId) as NodeFunction['id'], + functionDefinition: { + identifier: nodeFunction.runtimeFunctionId + }, + nextNodeId: nodeFunction.nextNodeId ? gid('NodeFunction', nodeFunction.nextNodeId) as NodeFunction['id'] : undefined, + parameters: { + nodes: nodeFunction.parameters.map(mapNodeParameter) + } + } +} + +function mapNodeParameter(nodeParameter: TucanaNodeParameter): NodeParameter { + let value: NodeParameterValue = {} + const nodeParameterValue = nodeParameter.value?.value + + if (nodeParameterValue?.oneofKind === 'literalValue') { + value = { + __typename: 'LiteralValue', + value: toAllowedValue(nodeParameterValue.literalValue) + }; + } else if (nodeParameterValue?.oneofKind === 'referenceValue') { + const target = nodeParameterValue.referenceValue.target; + + value = { + __typename: 'ReferenceValue', + referencePath: nodeParameterValue.referenceValue.paths.map(p => ({ + __typename: 'ReferencePath', + path: p.path, + arrayIndex: Number(p.arrayIndex) + })) + } + + if (target.oneofKind === 'nodeId') { + value.nodeFunctionId = gid('NodeFunction', target.nodeId) as NodeFunction['id'] + } else if (target.oneofKind === 'inputType') { + value.nodeFunctionId = gid('NodeFunction', target.inputType.nodeId) as NodeFunction['id'] + value.parameterIndex = Number(target.inputType.parameterIndex) + value.inputIndex = Number(target.inputType.inputIndex) + } + } else if (nodeParameterValue?.oneofKind === 'nodeFunctionId') { + value = { + __typename: 'NodeFunctionIdWrapper', + id: gid('NodeFunction', nodeParameterValue.nodeFunctionId) as NodeFunction['id'] + } + } + + return { + id: gid('NodeParameter', nodeParameter.databaseId) as NodeParameter['id'], + value + } +} + +function mapFunctionDefinition(functionDefinition: RuntimeFunctionDefinition): FunctionDefinition { + return { + identifier: functionDefinition.runtimeName, + signature: functionDefinition.signature, + parameterDefinitions: { + nodes: functionDefinition.runtimeParameterDefinitions.map(mapParameterDefinition) + } + } +} + +function mapParameterDefinition(parameterDefinition: RuntimeParameterDefinition): ParameterDefinition { + return { + identifier: parameterDefinition.runtimeName, + } +} + +function mapDataType(dataType: DefinitionDataType): DataType { + return { + identifier: dataType.identifier, + type: dataType.type, + genericKeys: dataType.genericKeys + }; +} \ No newline at end of file diff --git a/entrypoint/package-lock.json b/entrypoint/package-lock.json new file mode 100644 index 0000000..3ef2b25 --- /dev/null +++ b/entrypoint/package-lock.json @@ -0,0 +1,81 @@ +{ + "name": "@code0-tech/triangulum-entrypoint", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@code0-tech/triangulum-entrypoint", + "version": "0.0.0", + "dependencies": { + "@code0-tech/triangulum": "file:../", + "@code0-tech/tucana": "0.0.62", + "@protobuf-ts/runtime": "^2.11.1" + }, + "devDependencies": { + "bun-types": "^1.3.11" + } + }, + "..": { + "name": "@code0-tech/triangulum", + "version": "0.1.0", + "devDependencies": { + "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2385560645-52d09772ef7058a833cf32edc393ce95668b8404", + "@types/node": "^25.3.2", + "@typescript/vfs": "^1.6.4", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-dts": "^4.5.4", + "vitest": "^4.0.18" + }, + "peerDependencies": { + "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2385560645-52d09772ef7058a833cf32edc393ce95668b8404", + "@typescript/vfs": "^1.6.4", + "typescript": "^5.9.3" + } + }, + "node_modules/@code0-tech/triangulum": { + "resolved": "..", + "link": true + }, + "node_modules/@code0-tech/tucana": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/@code0-tech/tucana/-/tucana-0.0.62.tgz", + "integrity": "sha512-OLdGT0FSGxzlaGxVKnnvioaDovNZf1wdwofCoSx8nsT8fe7z24/IQLKSOkEYEF9ds3F8JXim7fiB+k3T2qky8Q==", + "license": "Apache-2.0" + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/bun-types": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", + "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/entrypoint/package.json b/entrypoint/package.json new file mode 100644 index 0000000..3f37dcc --- /dev/null +++ b/entrypoint/package.json @@ -0,0 +1,13 @@ +{ + "name": "@code0-tech/triangulum-entrypoint", + "version": "0.0.0", + "private": true, + "dependencies": { + "@code0-tech/triangulum": "file:../", + "@code0-tech/tucana": "0.0.62", + "@protobuf-ts/runtime": "^2.11.1" + }, + "devDependencies": { + "bun-types": "^1.3.11" + } +} diff --git a/entrypoint/readSingle.ts b/entrypoint/readSingle.ts new file mode 100644 index 0000000..9eeb867 --- /dev/null +++ b/entrypoint/readSingle.ts @@ -0,0 +1,39 @@ +import { + DefinitionDataType, + RuntimeFunctionDefinition, + ValidationFlow +} from "@code0-tech/tucana/shared"; + +export type SingleValidationInputData = { + flow?: ValidationFlow, + functions: RuntimeFunctionDefinition[], + dataTypes: DefinitionDataType[] +}; + +export async function readSingleValidation(input: AsyncIterable) { + const data: SingleValidationInputData = { + functions: [], + dataTypes: [] + }; + + let parsingState = 0; + + for await (const line of input) { + if(line === '') { + parsingState++; + continue; + } + + const message = Uint8Array.fromBase64(line); + if(parsingState === 0) { + data.flow = ValidationFlow.fromBinary(message); + } else if(parsingState === 1) { + data.functions.push(RuntimeFunctionDefinition.fromBinary(message)); + } else if(parsingState === 2) { + data.dataTypes.push(DefinitionDataType.fromBinary(message)); + } + } + + return data; +} + diff --git a/entrypoint/single-validation.ts b/entrypoint/single-validation.ts new file mode 100644 index 0000000..c474b31 --- /dev/null +++ b/entrypoint/single-validation.ts @@ -0,0 +1,10 @@ +import {readSingleValidation} from "./readSingle"; +import {mapToFlowValidation} from "./mapper"; +import {getFlowValidation} from "@code0-tech/triangulum"; + +const data = await readSingleValidation(console); +const validationInput = mapToFlowValidation(data); + +const result = getFlowValidation(validationInput.flow, validationInput.functions, validationInput.dataTypes) + +console.info(JSON.stringify(result)); diff --git a/entrypoint/tsconfig.json b/entrypoint/tsconfig.json new file mode 100644 index 0000000..cd2d3d6 --- /dev/null +++ b/entrypoint/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "bundler", + "module": "esnext", + "target": "esnext", + "types": ["bun-types"] + } +} From baa52c4ccb2e64c9319679da09bdb1c5f8834db1 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:06:43 +0100 Subject: [PATCH 3/9] Add validation class and bun platform config --- gem/Gemfile.lock | 21 +++++++- gem/lib/triangulum.rb | 8 ++- gem/lib/triangulum/bun.rb | 24 +++++++++ gem/lib/triangulum/validation.rb | 87 ++++++++++++++++++++++++++++++++ gem/triangulum.gemspec | 23 +++------ 5 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 gem/lib/triangulum/bun.rb create mode 100644 gem/lib/triangulum/validation.rb diff --git a/gem/Gemfile.lock b/gem/Gemfile.lock index b51f106..884a787 100644 --- a/gem/Gemfile.lock +++ b/gem/Gemfile.lock @@ -2,6 +2,10 @@ PATH remote: . specs: triangulum (0.0.0) + base64 (~> 0.3) + json (~> 2.19) + open3 (~> 0.2) + tucana (~> 0.0, >= 0.0.62) GEM remote: https://rubygems.org/ @@ -9,10 +13,19 @@ GEM addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) + base64 (0.3.0) bigdecimal (4.0.1) date (3.5.1) diff-lcs (1.6.2) erb (6.0.2) + google-protobuf (4.34.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) + grpc (1.78.1-x86_64-linux-gnu) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) io-console (0.8.2) irb (1.17.0) pp (>= 0.6.0) @@ -27,6 +40,7 @@ GEM lint_roller (1.1.0) mcp (0.8.0) json-schema (>= 4.1) + open3 (0.2.1) parallel (1.27.0) parser (3.3.10.2) ast (~> 2.4.1) @@ -84,23 +98,26 @@ GEM lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) ruby-progressbar (1.13.0) + rubyzip (2.4.1) stringio (3.2.0) tsort (0.2.0) + tucana (0.0.62) + grpc (~> 1.64) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) PLATFORMS - ruby x86_64-linux DEPENDENCIES - irb + irb (~> 1.17) rake (~> 13.0) rspec (~> 3.0) rubocop (~> 1.21) rubocop-rake (~> 0.7.0) rubocop-rspec (~> 3.0) + rubyzip (~> 2.3) triangulum! BUNDLED WITH diff --git a/gem/lib/triangulum.rb b/gem/lib/triangulum.rb index bc86911..a10b050 100644 --- a/gem/lib/triangulum.rb +++ b/gem/lib/triangulum.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true -require_relative 'triangulum/version' +require 'base64' +require 'json' +require 'open3' module Triangulum class Error < StandardError; end - # Your code goes here... end + +require_relative 'triangulum/version' +require_relative 'triangulum/validation' diff --git a/gem/lib/triangulum/bun.rb b/gem/lib/triangulum/bun.rb new file mode 100644 index 0000000..286abbe --- /dev/null +++ b/gem/lib/triangulum/bun.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Triangulum + # @!visibility private + module Bun + VERSION = 'v1.3.11' + + # rubocop:disable Layout/LineLength + # rubygems platform name => [bun release zip filename, sha256 checksum] + NATIVE_PLATFORMS = { + 'arm64-darwin' => %w[bun-darwin-aarch64.zip 6f5a3467ed9caec4795bf78cd476507d9f870c7d57b86c945fcb338126772ffc], + 'x86_64-darwin' => %w[bun-darwin-x64.zip c4fe2b9247218b0295f24e895aaec8fee62e74452679a9026b67eacbd611a286], + 'x86_64-linux-gnu' => %w[bun-linux-x64.zip 8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed], + 'x86_64-linux-musl' => %w[bun-linux-x64-musl.zip b0fce3bc4fab52f26a1e0d8886dc07fd0c0eb2a274cb343b59c83a2d5997b5b1], + 'aarch64-linux-gnu' => %w[bun-linux-aarch64.zip d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf], + 'aarch64-linux-musl' => %w[bun-linux-aarch64-musl.zip 0f5bf5dc3f276053196274bb84f90a44e2fa40c9432bd6757e3247a8d9476a3d] + }.freeze + # rubocop:enable Layout/LineLength + + def self.download_url(filename) + "https://github.com/oven-sh/bun/releases/download/bun-#{VERSION}/#{filename}" + end + end +end diff --git a/gem/lib/triangulum/validation.rb b/gem/lib/triangulum/validation.rb new file mode 100644 index 0000000..7525c26 --- /dev/null +++ b/gem/lib/triangulum/validation.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Triangulum + # == Triangulum::Validation + # This class implements the validation using the typescript package + class Validation + class TriangulumFailed < Triangulum::Error + end + + Result = Struct.new(:valid?, :return_type, :diagnostics) + Diagnostic = Struct.new(:message, :code, :severity, :node_id, :parameter_index) + + ENTRYPOINT = File.expand_path('js/single-validation.js', __dir__) + BUN_EXE = Dir.glob(File.expand_path('../../exe/*/bun', __dir__)).find do |path| + platform = Gem::Platform.new(File.basename(File.dirname(path))) + Gem::Platform.match_gem?(platform, Gem::Platform.local.to_s) + end + + attr_reader :flow, :runtime_function_definitions, :data_types + + def initialize(flow, runtime_function_definitions, data_types) + @flow = flow + @runtime_function_definitions = runtime_function_definitions + @data_types = data_types + end + + def validate + input = serialize_input + + output = run_ts_triangulum(input) + + parse_output(output) + end + + private + + def run_ts_triangulum(input) + stdout_s, stderr_s, status = Open3.capture3( + BUN_EXE, 'run', ENTRYPOINT, + stdin_data: input + ) + + raise TriangulumFailed, "OUT:\n#{stdout_s}\n\nERR:\n#{stderr_s}" unless status.success? + + stdout_s + end + + def serialize_input + input = [] + + input << Base64.strict_encode64(flow.to_proto) + input << '' + + runtime_function_definitions.each do |rfd| + input << Base64.strict_encode64(rfd.to_proto) + end + + input << '' + + data_types.each do |dt| + input << Base64.strict_encode64(dt.to_proto) + end + + input << '' + + input.join("\n") + end + + def parse_output(output) + json = JSON.parse(output, symbolize_names: true) + + Result.new( + json[:isValid], + json[:returnType], + json[:diagnostics].map do |diagnostic| + Diagnostic.new( + diagnostic[:message], + diagnostic[:code], + diagnostic[:severity], + diagnostic[:nodeId], + diagnostic[:parameterIndex] + ) + end + ) + end + end +end diff --git a/gem/triangulum.gemspec b/gem/triangulum.gemspec index 7415e62..f7736f9 100644 --- a/gem/triangulum.gemspec +++ b/gem/triangulum.gemspec @@ -7,37 +7,30 @@ Gem::Specification.new do |spec| spec.version = Triangulum::VERSION spec.authors = ['Niklas van Schrick'] spec.email = ['mc.taucher2003@gmail.com'] + spec.license = 'MIT' spec.summary = 'Triangulum is the CodeZero validation layer' spec.homepage = 'https://github.com/code0-tech/triangulum' spec.required_ruby_version = '>= 3.1.0' spec.metadata['homepage_uri'] = spec.homepage - spec.metadata['source_code_uri'] = spec.homepage spec.metadata['changelog_uri'] = "#{spec.homepage}/releases" spec.metadata['rubygems_mfa_required'] = 'true' # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - gemspec = File.basename(__FILE__) - spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| - ls.readlines("\x0", chomp: true).reject do |f| - (f == gemspec) || - f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) - end - end + spec.files = Dir.glob('lib/**/*', base: __dir__).select { |f| File.file?(File.join(__dir__, f)) } + ['README.md'] spec.bindir = 'exe' spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" + spec.add_dependency 'base64', '~> 0.3' + spec.add_dependency 'json', '~> 2.19' + spec.add_dependency 'open3', '~> 0.2' + spec.add_dependency 'tucana', '~> 0.0', '>= 0.0.62' - # For more information and examples about making a new gem, check out our - # guide at: https://bundler.io/guides/creating_gem.html - - spec.add_development_dependency 'irb' + spec.add_development_dependency 'irb', '~> 1.17' spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rubyzip', '~> 2.3' spec.add_development_dependency 'rspec', '~> 3.0' From c491dd89af6b9a78e461a85b905dadec99db8d21 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:07:42 +0100 Subject: [PATCH 4/9] Add build tasks to Rakefile --- gem/.gitignore | 6 ++++ gem/Rakefile | 92 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/gem/.gitignore b/gem/.gitignore index b41e419..7890f7a 100644 --- a/gem/.gitignore +++ b/gem/.gitignore @@ -7,6 +7,12 @@ /spec/reports/ /tmp/ +# bun binaries +/exe/*/bun + +# built entrypoints +/lib/triangulum/js/ + # intellij /.idea/ *.iml diff --git a/gem/Rakefile b/gem/Rakefile index 4964751..21be7e1 100644 --- a/gem/Rakefile +++ b/gem/Rakefile @@ -2,6 +2,9 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' +require 'rubygems/package_task' +require 'open-uri' +require 'digest' RSpec::Core::RakeTask.new(:spec) @@ -9,4 +12,91 @@ require 'rubocop/rake_task' RuboCop::RakeTask.new -task default: %i[spec rubocop] +task default: %i[prepare_build spec rubocop] + +require_relative 'lib/triangulum/bun' + +TRIANGULUM_GEMSPEC = Bundler.load_gemspec('triangulum.gemspec') + +task prepare_build: %i[download build:entrypoints] + +task package: %i[clobber prepare_build] + +desc 'Clean downloaded bun binaries' +task 'clobber:bun' do + Triangulum::Bun::NATIVE_PLATFORMS.each_key do |platform| + bun_path = File.join('exe', platform, 'bun') + rm bun_path if File.exist?(bun_path) + end +end + +task clobber: %i[clobber:bun] + +exepaths = [] + +Triangulum::Bun::NATIVE_PLATFORMS.each do |platform, (zip_filename, expected_checksum)| + TRIANGULUM_GEMSPEC.dup.tap do |gemspec| + exedir = File.join('exe', platform) + exepath = File.join(exedir, 'bun') + + exepaths << exepath + + gemspec.platform = platform + gemspec.files += [exepath] + + gem_path = Gem::PackageTask.new(gemspec).define + desc "Build the #{platform} gem" + task "gem:#{platform}" => [gem_path] + + directory exedir + file exepath => [exedir] do + url = Triangulum::Bun.download_url(zip_filename) + warn "Downloading #{exepath} from #{url} ..." + + URI.open(url) do |remote| # rubocop:disable Security/Open + zip_data = remote.read + + actual_checksum = Digest::SHA256.hexdigest(zip_data) + unless actual_checksum == expected_checksum + raise "Checksum mismatch for #{zip_filename}: expected #{expected_checksum}, got #{actual_checksum}" + end + + # Bun zips contain a directory with the binary inside + require 'zip' + Zip::File.open_buffer(zip_data) do |zip| + bun_entry = zip.find { |e| File.basename(e.name) == 'bun' } + raise "bun binary not found in #{zip_filename}" unless bun_entry + + File.binwrite(exepath, bun_entry.get_input_stream.read) + end + end + + FileUtils.chmod(0o755, exepath, verbose: true) + end + end +end + +desc 'Download all bun binaries' +task download: exepaths + +desc 'Bundle the TypeScript entrypoints into single JS files' +task 'build:entrypoints' do + rm_rf 'lib/triangulum/js' + directory 'lib/triangulum/js' + + entrypoints = %w[single-validation] + + entrypoints.each do |entrypoint| + entrypoint_src = File.expand_path("../entrypoint/#{entrypoint}.ts", __dir__) + entrypoint_dst = File.expand_path("lib/triangulum/js/#{entrypoint}.js", __dir__) + + sh 'bun', 'build', entrypoint_src, '--outfile', entrypoint_dst, '--target', 'bun' + end +end + +desc 'Push all platform gems to RubyGems' +task 'push:all' do + Dir['pkg/*.gem'].each do |gem_file| + sh 'gem', 'push', gem_file + end +end From cd0cb7c43561cbd08e31be8c5a08f0f7e574feac Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:08:24 +0100 Subject: [PATCH 5/9] Add basic specs --- gem/spec/spec_helper.rb | 9 +- gem/spec/support/protobuf_factories.rb | 82 ++++++++++++++ gem/spec/triangulum/validation_spec.rb | 146 +++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 gem/spec/support/protobuf_factories.rb create mode 100644 gem/spec/triangulum/validation_spec.rb diff --git a/gem/spec/spec_helper.rb b/gem/spec/spec_helper.rb index 9606817..31056e4 100644 --- a/gem/spec/spec_helper.rb +++ b/gem/spec/spec_helper.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true require 'triangulum' +require 'tucana' + +Tucana.load_protocol(:shared) + +Dir[File.join(__dir__, 'support/**/*.rb')].each { |f| require f } RSpec.configure do |config| - # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' - - # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! + config.include ProtobufFactories config.expect_with :rspec do |c| c.syntax = :expect diff --git a/gem/spec/support/protobuf_factories.rb b/gem/spec/support/protobuf_factories.rb new file mode 100644 index 0000000..808a51b --- /dev/null +++ b/gem/spec/support/protobuf_factories.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module ProtobufFactories + def build_flow(node_functions:, starting_node_id: 1) + Tucana::Shared::ValidationFlow.new( + flow_id: 1, + project_id: 1, + type: 'test', + starting_node_id: starting_node_id, + node_functions: node_functions, + project_slug: 'test' + ) + end + + def literal_value(value) + Tucana::Shared::NodeValue.new( + literal_value: Tucana::Shared::Value.from_ruby(value) + ) + end + + def reference_node(node_id:, paths: []) + Tucana::Shared::NodeValue.new( + reference_value: Tucana::Shared::ReferenceValue.new( + node_id: node_id, + paths: paths + ) + ) + end + + def node_function_value(node_id) + Tucana::Shared::NodeValue.new(node_function_id: node_id) + end + + def node(id:, function_id:, parameters: [], next_node_id: nil) + Tucana::Shared::NodeFunction.new( + database_id: id, + runtime_function_id: function_id, + parameters: parameters, + next_node_id: next_node_id, + definition_source: 'test' + ) + end + + def param(id:, runtime_parameter_id:, value: nil) + Tucana::Shared::NodeParameter.new( + database_id: id, + runtime_parameter_id: runtime_parameter_id, + value: value + ) + end + + def default_data_types + [ + Tucana::Shared::DefinitionDataType.new(identifier: 'LIST', type: 'T[]', generic_keys: ['T']), + Tucana::Shared::DefinitionDataType.new(identifier: 'NUMBER', type: 'number'), + Tucana::Shared::DefinitionDataType.new(identifier: 'STRING', type: 'string'), + Tucana::Shared::DefinitionDataType.new(identifier: 'CONSUMER', type: '(item:R) => void', generic_keys: ['R']), + Tucana::Shared::DefinitionDataType.new(identifier: 'RUNNABLE', type: '() => void') + ] + end + + def default_functions + [ + Tucana::Shared::RuntimeFunctionDefinition.new( + runtime_name: 'std::math::add', + signature: '(a: NUMBER, b: NUMBER): NUMBER' + ), + Tucana::Shared::RuntimeFunctionDefinition.new( + runtime_name: 'std::list::at', + signature: '(list: LIST, index: NUMBER): R' + ), + Tucana::Shared::RuntimeFunctionDefinition.new( + runtime_name: 'std::control::for_each', + signature: '(list: LIST, consumer: CONSUMER): void' + ), + Tucana::Shared::RuntimeFunctionDefinition.new( + runtime_name: 'std::control::return', + signature: '(value: R): R' + ) + ] + end +end diff --git a/gem/spec/triangulum/validation_spec.rb b/gem/spec/triangulum/validation_spec.rb new file mode 100644 index 0000000..18155c8 --- /dev/null +++ b/gem/spec/triangulum/validation_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +RSpec.describe Triangulum::Validation do + let(:data_types) { default_data_types } + let(:functions) { default_functions } + + describe '#validate' do + it 'validates a simple valid flow' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::math::add', + parameters: [ + param(id: 1, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 2, runtime_parameter_id: 'b', value: literal_value(0)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.valid?).to be true + expect(result.diagnostics).to be_empty + end + + it 'detects type errors in parameters' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::math::add', + parameters: [ + param(id: 1, runtime_parameter_id: 'a', value: literal_value('not a number')), + param(id: 2, runtime_parameter_id: 'b', value: literal_value(10)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.valid?).to be false + expect(result.diagnostics).not_to be_empty + expect(result.diagnostics.first.message).to include('number') + end + + it 'validates a flow with references between nodes' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::math::add', + next_node_id: 2, + parameters: [ + param(id: 1, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 2, runtime_parameter_id: 'b', value: literal_value(2)) + ] + ), + node( + id: 2, + function_id: 'std::math::add', + parameters: [ + param(id: 3, runtime_parameter_id: 'a', value: reference_node(node_id: 1)), + param(id: 4, runtime_parameter_id: 'b', value: literal_value(3)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.valid?).to be true + expect(result.diagnostics).to be_empty + end + + it 'validates a flow with nested scopes' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::control::for_each', + parameters: [ + param(id: 1, runtime_parameter_id: 'list', value: literal_value([1, 2, 3])), + param(id: 2, runtime_parameter_id: 'consumer', value: node_function_value(2)) + ] + ), + node( + id: 2, + function_id: 'std::math::add', + parameters: [ + param(id: 3, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 4, runtime_parameter_id: 'b', value: literal_value(2)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.valid?).to be true + end + + it 'returns diagnostics with node_id and parameter_index' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::math::add', + parameters: [ + param(id: 1, runtime_parameter_id: 'a', value: literal_value('string')), + param(id: 2, runtime_parameter_id: 'b', value: literal_value(10)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.valid?).to be false + diagnostic = result.diagnostics.find { |d| d.parameter_index == 0 } + expect(diagnostic).not_to be_nil + expect(diagnostic.node_id).not_to be_nil + end + + it 'returns the return type' do + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'std::math::add', + parameters: [ + param(id: 1, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 2, runtime_parameter_id: 'b', value: literal_value(2)) + ] + ) + ] + ) + + result = described_class.new(flow, functions, data_types).validate + + expect(result.return_type).to eq('void') + end + end +end From 2c6843424a3ddc5d2b00fdcdab2ca8383cb27b20 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:08:45 +0100 Subject: [PATCH 6/9] Update workflows to include gem --- .github/workflows/publish.yml | 41 ++++++++++++++++++++++++++++++++++- .github/workflows/test.yml | 22 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d07cdcd..1535740 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,7 +6,7 @@ on: tags: - '*' jobs: - publish: + npm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -18,3 +18,42 @@ jobs: - run: npm ci - run: npm run build - run: npm publish --tag latest + + rubygems: + runs-on: ubuntu-latest + defaults: + run: + working-directory: gem + steps: + # Set up + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4.7' + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: ".tool-versions" + - uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + working-directory: . + - run: npm run build + working-directory: . + - run: npm ci + working-directory: entrypoint + - name: Install gems + run: bundle install + - name: Set version + run: sed -i "s/VERSION = '0.0.0'/VERSION = '${GITHUB_REF_NAME#v}'/" lib/triangulum/version.rb && bundle + - name: Package gems + run: bundle exec rake package + + # Release + - uses: rubygems/configure-rubygems-credentials@v1.0.0 + - name: Publish gem + run: bundle exec rake push:all + - name: Wait for release + run: gem exec rubygems-await pkg/*.gem diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30bcccd..2be7dd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,12 +24,32 @@ jobs: matrix: ruby: - '3.4.7' + defaults: + run: + working-directory: gem steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: ".tool-versions" + - uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + working-directory: . + - run: npm run build + working-directory: . + - run: npm ci + working-directory: entrypoint + - name: Install gems + run: bundle install - name: Run the default task run: bundle exec rake + - name: Build all gems + run: bundle exec rake package From fe69232a4e24aab72478dc8775c54b6ddb887ff9 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 02:09:23 +0100 Subject: [PATCH 7/9] Readme and general housekeeping --- .tool-versions | 1 + gem/.rubocop.yml | 6 ++++ gem/README.md | 74 +++++++++++++++++++++++++++++++++++++----------- gem/bin/console | 0 gem/bin/setup | 8 ------ 5 files changed, 65 insertions(+), 24 deletions(-) mode change 100644 => 100755 gem/bin/console delete mode 100644 gem/bin/setup diff --git a/.tool-versions b/.tool-versions index cce1df0..f8d866b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ nodejs 24.13.0 ruby 3.4.7 +bun 1.3.11 diff --git a/gem/.rubocop.yml b/gem/.rubocop.yml index 5fd0b03..f1f716b 100644 --- a/gem/.rubocop.yml +++ b/gem/.rubocop.yml @@ -12,5 +12,11 @@ Gemspec/DevelopmentDependencies: Metrics: Enabled: false +RSpec/ExampleLength: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes diff --git a/gem/README.md b/gem/README.md index 523bada..ac29362 100644 --- a/gem/README.md +++ b/gem/README.md @@ -1,35 +1,77 @@ -# Triangulum +# Triangulum Ruby Gem -TODO: Delete this and the text below, and describe your gem - -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/triangulum`. To experiment with that code, run `bin/console` for an interactive prompt. +Ruby bindings for the [Triangulum](https://github.com/code0-tech/triangulum) validation layer. This gem wraps the TypeScript library and a bundled [Bun](https://bun.sh) runtime to validate flows using the TypeScript compiler. ## Installation -TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. - -Install the gem and add to the application's Gemfile by executing: +Add to your Gemfile: -```bash -bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +```ruby +gem 'triangulum' ``` -If bundler is not being used to manage dependencies, install the gem by executing: +Platform-specific gems are published for: + +- `arm64-darwin` (macOS Apple Silicon) +- `x86_64-darwin` (macOS Intel) +- `x86_64-linux-gnu` (Linux x64) +- `x86_64-linux-musl` (Linux x64 musl) +- `aarch64-linux-gnu` (Linux ARM64) +- `aarch64-linux-musl` (Linux ARM64 musl) + +If Bundler doesn't automatically select the correct platform gem, add your platform: ```bash -gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG +bundle lock --add-platform x86_64-linux-gnu ``` ## Usage -TODO: Write usage instructions here +```ruby +result = Triangulum::Validation.new(flow, runtime_function_definitions, data_types).validate + +result.valid? # => true / false +result.return_type # => "void" +result.diagnostics # => [Triangulum::Validation::Diagnostic, ...] +``` + +The arguments are [Tucana](https://github.com/code0-tech/tucana) protobuf objects: + +- `flow` — `Tucana::Shared::ValidationFlow` +- `runtime_function_definitions` — `Array` +- `data_types` — `Array` + +### Diagnostics + +Each diagnostic contains: + +| Field | Description | +|---|---| +| `message` | Human-readable error description | +| `code` | TypeScript diagnostic code | +| `severity` | `"error"` or `"warning"` | +| `node_id` | ID of the node that caused the error | +| `parameter_index` | Index of the parameter that caused the error | ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +Prerequisites: [Bun](https://bun.sh) installed locally for building the entrypoint. + +```bash +cd gem +bundle install +bundle exec rake prepare_build # downloads bun binaries + builds JS entrypoint +bundle exec rake # run tests and rubocop +``` + +### Building platform gems + +```bash +bundle exec rake package +``` -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). +This will download bun binaries (with SHA256 checksum verification) for all supported platforms and build a `.gem` file for each. -## Contributing +## License -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/triangulum. +See [LICENSE](../LICENSE). diff --git a/gem/bin/console b/gem/bin/console old mode 100644 new mode 100755 diff --git a/gem/bin/setup b/gem/bin/setup deleted file mode 100644 index dce67d8..0000000 --- a/gem/bin/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' -set -vx - -bundle install - -# Do any other automated setup that you need to do here From 375fcf09edd88603e795e0648aa494ca0ef80a84 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 15:01:55 +0100 Subject: [PATCH 8/9] Refactor gem build --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- gem/Rakefile | 92 +------------------------------- gem/lib/triangulum/bun.rb | 24 --------- gem/lib/triangulum/validation.rb | 25 +++++---- gem/rakelib/bun.rb | 19 +++++++ gem/rakelib/entrypoints.rake | 16 ++++++ gem/rakelib/package.rake | 64 ++++++++++++++++++++++ gem/rakelib/push.rake | 8 +++ gem/triangulum.gemspec | 2 - 10 files changed, 125 insertions(+), 129 deletions(-) delete mode 100644 gem/lib/triangulum/bun.rb create mode 100644 gem/rakelib/bun.rb create mode 100644 gem/rakelib/entrypoints.rake create mode 100644 gem/rakelib/package.rake create mode 100644 gem/rakelib/push.rake diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1535740..9d8d5fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,7 +26,7 @@ jobs: working-directory: gem steps: # Set up - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2be7dd4..89f83f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: ruby: runs-on: ubuntu-latest - name: Ruby ${{ matrix.ruby }} + name: ruby ${{ matrix.ruby }} strategy: matrix: ruby: diff --git a/gem/Rakefile b/gem/Rakefile index 21be7e1..d232347 100644 --- a/gem/Rakefile +++ b/gem/Rakefile @@ -2,101 +2,11 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' -require 'rubygems/package_task' -require 'open-uri' -require 'digest' - -RSpec::Core::RakeTask.new(:spec) - require 'rubocop/rake_task' +RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new task default: %i[prepare_build spec rubocop] - -require_relative 'lib/triangulum/bun' - -TRIANGULUM_GEMSPEC = Bundler.load_gemspec('triangulum.gemspec') - task prepare_build: %i[download build:entrypoints] - task package: %i[clobber prepare_build] - -desc 'Clean downloaded bun binaries' -task 'clobber:bun' do - Triangulum::Bun::NATIVE_PLATFORMS.each_key do |platform| - bun_path = File.join('exe', platform, 'bun') - rm bun_path if File.exist?(bun_path) - end -end - -task clobber: %i[clobber:bun] - -exepaths = [] - -Triangulum::Bun::NATIVE_PLATFORMS.each do |platform, (zip_filename, expected_checksum)| - TRIANGULUM_GEMSPEC.dup.tap do |gemspec| - exedir = File.join('exe', platform) - exepath = File.join(exedir, 'bun') - - exepaths << exepath - - gemspec.platform = platform - gemspec.files += [exepath] - - gem_path = Gem::PackageTask.new(gemspec).define - desc "Build the #{platform} gem" - task "gem:#{platform}" => [gem_path] - - directory exedir - file exepath => [exedir] do - url = Triangulum::Bun.download_url(zip_filename) - warn "Downloading #{exepath} from #{url} ..." - - URI.open(url) do |remote| # rubocop:disable Security/Open - zip_data = remote.read - - actual_checksum = Digest::SHA256.hexdigest(zip_data) - unless actual_checksum == expected_checksum - raise "Checksum mismatch for #{zip_filename}: expected #{expected_checksum}, got #{actual_checksum}" - end - - # Bun zips contain a directory with the binary inside - require 'zip' - Zip::File.open_buffer(zip_data) do |zip| - bun_entry = zip.find { |e| File.basename(e.name) == 'bun' } - raise "bun binary not found in #{zip_filename}" unless bun_entry - - File.binwrite(exepath, bun_entry.get_input_stream.read) - end - end - - FileUtils.chmod(0o755, exepath, verbose: true) - end - end -end - -desc 'Download all bun binaries' -task download: exepaths - -desc 'Bundle the TypeScript entrypoints into single JS files' -task 'build:entrypoints' do - rm_rf 'lib/triangulum/js' - directory 'lib/triangulum/js' - - entrypoints = %w[single-validation] - - entrypoints.each do |entrypoint| - entrypoint_src = File.expand_path("../entrypoint/#{entrypoint}.ts", __dir__) - entrypoint_dst = File.expand_path("lib/triangulum/js/#{entrypoint}.js", __dir__) - - sh 'bun', 'build', entrypoint_src, '--outfile', entrypoint_dst, '--target', 'bun' - end -end - -desc 'Push all platform gems to RubyGems' -task 'push:all' do - Dir['pkg/*.gem'].each do |gem_file| - sh 'gem', 'push', gem_file - end -end diff --git a/gem/lib/triangulum/bun.rb b/gem/lib/triangulum/bun.rb deleted file mode 100644 index 286abbe..0000000 --- a/gem/lib/triangulum/bun.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Triangulum - # @!visibility private - module Bun - VERSION = 'v1.3.11' - - # rubocop:disable Layout/LineLength - # rubygems platform name => [bun release zip filename, sha256 checksum] - NATIVE_PLATFORMS = { - 'arm64-darwin' => %w[bun-darwin-aarch64.zip 6f5a3467ed9caec4795bf78cd476507d9f870c7d57b86c945fcb338126772ffc], - 'x86_64-darwin' => %w[bun-darwin-x64.zip c4fe2b9247218b0295f24e895aaec8fee62e74452679a9026b67eacbd611a286], - 'x86_64-linux-gnu' => %w[bun-linux-x64.zip 8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed], - 'x86_64-linux-musl' => %w[bun-linux-x64-musl.zip b0fce3bc4fab52f26a1e0d8886dc07fd0c0eb2a274cb343b59c83a2d5997b5b1], - 'aarch64-linux-gnu' => %w[bun-linux-aarch64.zip d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf], - 'aarch64-linux-musl' => %w[bun-linux-aarch64-musl.zip 0f5bf5dc3f276053196274bb84f90a44e2fa40c9432bd6757e3247a8d9476a3d] - }.freeze - # rubocop:enable Layout/LineLength - - def self.download_url(filename) - "https://github.com/oven-sh/bun/releases/download/bun-#{VERSION}/#{filename}" - end - end -end diff --git a/gem/lib/triangulum/validation.rb b/gem/lib/triangulum/validation.rb index 7525c26..c384e21 100644 --- a/gem/lib/triangulum/validation.rb +++ b/gem/lib/triangulum/validation.rb @@ -7,8 +7,11 @@ class Validation class TriangulumFailed < Triangulum::Error end - Result = Struct.new(:valid?, :return_type, :diagnostics) - Diagnostic = Struct.new(:message, :code, :severity, :node_id, :parameter_index) + class BunNotFound < Triangulum::Error + end + + Result = Struct.new(:valid?, :return_type, :diagnostics, keyword_init: true) + Diagnostic = Struct.new(:message, :code, :severity, :node_id, :parameter_index, keyword_init: true) ENTRYPOINT = File.expand_path('js/single-validation.js', __dir__) BUN_EXE = Dir.glob(File.expand_path('../../exe/*/bun', __dir__)).find do |path| @@ -35,6 +38,8 @@ def validate private def run_ts_triangulum(input) + raise BunNotFound, "No bundled bun binary found for #{Gem::Platform.local}" if BUN_EXE.nil? + stdout_s, stderr_s, status = Open3.capture3( BUN_EXE, 'run', ENTRYPOINT, stdin_data: input @@ -70,15 +75,15 @@ def parse_output(output) json = JSON.parse(output, symbolize_names: true) Result.new( - json[:isValid], - json[:returnType], - json[:diagnostics].map do |diagnostic| + valid?: json[:isValid], + return_type: json[:returnType], + diagnostics: json[:diagnostics].map do |diagnostic| Diagnostic.new( - diagnostic[:message], - diagnostic[:code], - diagnostic[:severity], - diagnostic[:nodeId], - diagnostic[:parameterIndex] + message: diagnostic[:message], + code: diagnostic[:code], + severity: diagnostic[:severity], + node_id: diagnostic[:nodeId], + parameter_index: diagnostic[:parameterIndex] ) end ) diff --git a/gem/rakelib/bun.rb b/gem/rakelib/bun.rb new file mode 100644 index 0000000..7ae7372 --- /dev/null +++ b/gem/rakelib/bun.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +BUN_VERSION = 'v1.3.11' + +# rubocop:disable Layout/LineLength +# rubygems platform name => [bun release zip filename, sha256 checksum] +BUN_PLATFORMS = { + 'arm64-darwin' => %w[bun-darwin-aarch64.zip 6f5a3467ed9caec4795bf78cd476507d9f870c7d57b86c945fcb338126772ffc], + 'x86_64-darwin' => %w[bun-darwin-x64.zip c4fe2b9247218b0295f24e895aaec8fee62e74452679a9026b67eacbd611a286], + 'x86_64-linux-gnu' => %w[bun-linux-x64.zip 8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed], + 'x86_64-linux-musl' => %w[bun-linux-x64-musl.zip b0fce3bc4fab52f26a1e0d8886dc07fd0c0eb2a274cb343b59c83a2d5997b5b1], + 'aarch64-linux-gnu' => %w[bun-linux-aarch64.zip d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf], + 'aarch64-linux-musl' => %w[bun-linux-aarch64-musl.zip 0f5bf5dc3f276053196274bb84f90a44e2fa40c9432bd6757e3247a8d9476a3d] +}.freeze +# rubocop:enable Layout/LineLength + +def bun_download_url(filename) + "https://github.com/oven-sh/bun/releases/download/bun-#{BUN_VERSION}/#{filename}" +end diff --git a/gem/rakelib/entrypoints.rake b/gem/rakelib/entrypoints.rake new file mode 100644 index 0000000..5a312bb --- /dev/null +++ b/gem/rakelib/entrypoints.rake @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +desc 'Bundle the TypeScript entrypoints into single JS files' +task 'build:entrypoints' do + rm_rf 'lib/triangulum/js' + directory 'lib/triangulum/js' + + entrypoints = %w[single-validation] + + entrypoints.each do |entrypoint| + entrypoint_src = File.expand_path("../../entrypoint/#{entrypoint}.ts", __dir__) + entrypoint_dst = File.expand_path("../lib/triangulum/js/#{entrypoint}.js", __dir__) + + sh 'bun', 'build', entrypoint_src, '--outfile', entrypoint_dst, '--target', 'bun' + end +end diff --git a/gem/rakelib/package.rake b/gem/rakelib/package.rake new file mode 100644 index 0000000..57ee216 --- /dev/null +++ b/gem/rakelib/package.rake @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rubygems/package_task' +require 'open-uri' +require 'digest' +require_relative 'bun' + +desc 'Clean downloaded bun binaries' +task 'clobber:bun' do + BUN_PLATFORMS.each_key do |platform| + bun_path = File.join('exe', platform, 'bun') + rm bun_path if File.exist?(bun_path) + end +end + +task clobber: %i[clobber:bun] + +exepaths = [] + +TRIANGULUM_GEMSPEC = Bundler.load_gemspec('triangulum.gemspec') + +BUN_PLATFORMS.each do |platform, (zip_filename, expected_checksum)| + TRIANGULUM_GEMSPEC.dup.tap do |gemspec| + exedir = File.join('exe', platform) + exepath = File.join(exedir, 'bun') + + exepaths << exepath + + gemspec.platform = platform + gemspec.files += [exepath] + + gem_path = Gem::PackageTask.new(gemspec).define + desc "Build the #{platform} gem" + task "gem:#{platform}" => [gem_path] + + directory exedir + file exepath => [exedir] do + url = bun_download_url(zip_filename) + warn "Downloading #{exepath} from #{url} ..." + + URI.open(url) do |remote| # rubocop:disable Security/Open + zip_data = remote.read + + actual_checksum = Digest::SHA256.hexdigest(zip_data) + unless actual_checksum == expected_checksum + raise "Checksum mismatch for #{zip_filename}: expected #{expected_checksum}, got #{actual_checksum}" + end + + require 'zip' + Zip::File.open_buffer(zip_data) do |zip| + bun_entry = zip.find { |e| File.basename(e.name) == 'bun' } + raise "bun binary not found in #{zip_filename}" unless bun_entry + + File.binwrite(exepath, bun_entry.get_input_stream.read) + end + end + + FileUtils.chmod(0o755, exepath, verbose: true) + end + end +end + +desc 'Download all bun binaries' +task download: exepaths diff --git a/gem/rakelib/push.rake b/gem/rakelib/push.rake new file mode 100644 index 0000000..3aa282b --- /dev/null +++ b/gem/rakelib/push.rake @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +desc 'Push all platform gems to RubyGems' +task 'push:all' do + Dir['pkg/*.gem'].each do |gem_file| + sh 'gem', 'push', gem_file + end +end diff --git a/gem/triangulum.gemspec b/gem/triangulum.gemspec index f7736f9..2db7f51 100644 --- a/gem/triangulum.gemspec +++ b/gem/triangulum.gemspec @@ -19,8 +19,6 @@ Gem::Specification.new do |spec| # Specify which files should be added to the gem when it is released. spec.files = Dir.glob('lib/**/*', base: __dir__).select { |f| File.file?(File.join(__dir__, f)) } + ['README.md'] - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] spec.add_dependency 'base64', '~> 0.3' From a51e7519e22bf972e7b15b2d83dd55bf08122004 Mon Sep 17 00:00:00 2001 From: Niklas van Schrick Date: Sun, 22 Mar 2026 15:11:13 +0100 Subject: [PATCH 9/9] Normalize Gemfile to ruby platform --- gem/Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gem/Gemfile.lock b/gem/Gemfile.lock index 884a787..c23f0b4 100644 --- a/gem/Gemfile.lock +++ b/gem/Gemfile.lock @@ -18,12 +18,12 @@ GEM date (3.5.1) diff-lcs (1.6.2) erb (6.0.2) - google-protobuf (4.34.1-x86_64-linux-gnu) + google-protobuf (4.34.1) bigdecimal rake (~> 13.3) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) - grpc (1.78.1-x86_64-linux-gnu) + grpc (1.78.1) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) io-console (0.8.2) @@ -108,7 +108,7 @@ GEM unicode-emoji (4.2.0) PLATFORMS - x86_64-linux + ruby DEPENDENCIES irb (~> 1.17)