lex-factory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/README.md +29 -0
- data/lib/legion/extensions/factory/helpers/constants.rb +24 -0
- data/lib/legion/extensions/factory/helpers/quality_gate.rb +40 -0
- data/lib/legion/extensions/factory/helpers/spec_parser.rb +71 -0
- data/lib/legion/extensions/factory/pipeline_runner.rb +170 -0
- data/lib/legion/extensions/factory/runners/factory.rb +30 -0
- data/lib/legion/extensions/factory/version.rb +9 -0
- data/lib/legion/extensions/factory.rb +26 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: df3fc328fe57d371e7b812ac1285a75dd022e38c7c33612e21292ddabd15de4c
|
|
4
|
+
data.tar.gz: 7722b25f034cc3aaea6d8e6963f47907dec10bf27831d3afbf9bf633c12bc84f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: da95c3eedec07c08767cedf334bc7d96efbe4fe2aeeb3ce788c90121f8195cc697218b45f0624022bd7e6a60c868799f378ba6c79669cea31d647d870d74ec95
|
|
7
|
+
data.tar.gz: 692244c9e64ca57b461433dffb757122fa68cc7eb670cc7ac2fbedfdc9651a7f31c8dd65ba9179696a60ffba70dd70e4985fed389663b899bede586638ca9dff
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release: 4-stage Double Diamond pipeline (Discover/Define/Develop/Deliver)
|
|
7
|
+
- SpecParser: reads specification markdown documents
|
|
8
|
+
- RequirementDecomposer: LLM-based requirement extraction
|
|
9
|
+
- CodeGenerator: LLM-based code and test generation
|
|
10
|
+
- QualityGate: satisfaction scoring (completeness, correctness, quality, security)
|
|
11
|
+
- PipelineRunner: orchestrates stages with resumable state persistence
|
|
12
|
+
- Factory runner entry points: run_pipeline, pipeline_status
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# lex-factory
|
|
2
|
+
|
|
3
|
+
Spec-to-code autonomous pipeline for LegionIO. Takes a specification document and produces working code with tests through a 4-stage Double Diamond pipeline.
|
|
4
|
+
|
|
5
|
+
## Pipeline Stages
|
|
6
|
+
|
|
7
|
+
1. **DISCOVER** - Parse spec, identify unknowns, research patterns
|
|
8
|
+
2. **DEFINE** - Decompose into tasks, define interfaces, plan tests
|
|
9
|
+
3. **DEVELOP** - Generate code for each task, run tests
|
|
10
|
+
4. **DELIVER** - Score quality, produce summary
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
result = Legion::Extensions::Factory::Runners::Factory.run_pipeline(spec_path: 'path/to/spec.md')
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
factory:
|
|
22
|
+
satisfaction_threshold: 0.8
|
|
23
|
+
output_dir: tmp/factory
|
|
24
|
+
max_retries_per_stage: 2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Factory
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
STAGES = %i[discover define develop deliver].freeze
|
|
9
|
+
|
|
10
|
+
SCORE_WEIGHTS = {
|
|
11
|
+
completeness: 0.35,
|
|
12
|
+
correctness: 0.35,
|
|
13
|
+
quality: 0.20,
|
|
14
|
+
security: 0.10
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
DEFAULT_SATISFACTION_THRESHOLD = 0.8
|
|
18
|
+
DEFAULT_MAX_RETRIES = 2
|
|
19
|
+
DEFAULT_OUTPUT_DIR = 'tmp/factory'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Factory
|
|
6
|
+
module Helpers
|
|
7
|
+
module QualityGate
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def score(completeness:, correctness:, quality:, security:,
|
|
11
|
+
threshold: Constants::DEFAULT_SATISFACTION_THRESHOLD)
|
|
12
|
+
scores = {
|
|
13
|
+
completeness: clamp(completeness),
|
|
14
|
+
correctness: clamp(correctness),
|
|
15
|
+
quality: clamp(quality),
|
|
16
|
+
security: clamp(security)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
aggregate = Constants::SCORE_WEIGHTS.sum do |dimension, weight|
|
|
20
|
+
scores[dimension] * weight
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
pass: aggregate >= threshold,
|
|
25
|
+
aggregate: aggregate.round(4),
|
|
26
|
+
threshold: threshold,
|
|
27
|
+
scores: scores
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clamp(value)
|
|
32
|
+
value.to_f.clamp(0.0, 1.0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method :clamp
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Factory
|
|
6
|
+
module Helpers
|
|
7
|
+
module SpecParser
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def parse(file_path:)
|
|
11
|
+
return { success: false, error: 'file not found' } unless ::File.exist?(file_path)
|
|
12
|
+
|
|
13
|
+
content = ::File.read(file_path)
|
|
14
|
+
title = extract_title(content)
|
|
15
|
+
sections = extract_sections(content)
|
|
16
|
+
code_blocks = extract_code_blocks(content)
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
success: true,
|
|
20
|
+
file_path: file_path,
|
|
21
|
+
title: title,
|
|
22
|
+
sections: sections,
|
|
23
|
+
code_blocks: code_blocks,
|
|
24
|
+
raw: content
|
|
25
|
+
}
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
{ success: false, error: e.message }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def raw_content(file_path:)
|
|
31
|
+
::File.read(file_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def extract_title(content)
|
|
35
|
+
match = content.match(/^#\s+(.+)$/)
|
|
36
|
+
match ? match[1].strip : 'Untitled'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_sections(content)
|
|
40
|
+
sections = []
|
|
41
|
+
current_heading = nil
|
|
42
|
+
current_items = []
|
|
43
|
+
|
|
44
|
+
content.each_line do |line|
|
|
45
|
+
if line =~ /^##\s+(.+)$/
|
|
46
|
+
sections << { heading: current_heading, items: current_items } if current_heading
|
|
47
|
+
current_heading = Regexp.last_match(1).strip
|
|
48
|
+
current_items = []
|
|
49
|
+
elsif current_heading && line =~ /^[-*]\s+(.+)$/
|
|
50
|
+
current_items << Regexp.last_match(1).strip
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
sections << { heading: current_heading, items: current_items } if current_heading
|
|
55
|
+
sections
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_code_blocks(content)
|
|
59
|
+
blocks = []
|
|
60
|
+
content.scan(/```(\w*)\n(.*?)```/m) do |lang, code|
|
|
61
|
+
blocks << { language: lang.empty? ? nil : lang, code: code.strip }
|
|
62
|
+
end
|
|
63
|
+
blocks
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_class_method :extract_title, :extract_sections, :extract_code_blocks
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Factory
|
|
9
|
+
class PipelineRunner
|
|
10
|
+
attr_reader :spec_path, :output_dir
|
|
11
|
+
|
|
12
|
+
def initialize(spec_path:, output_dir: nil, threshold: nil, max_retries: nil)
|
|
13
|
+
@spec_path = spec_path
|
|
14
|
+
@output_dir = output_dir || factory_settings[:output_dir] || Helpers::Constants::DEFAULT_OUTPUT_DIR
|
|
15
|
+
@threshold = threshold || factory_settings[:satisfaction_threshold] || Helpers::Constants::DEFAULT_SATISFACTION_THRESHOLD
|
|
16
|
+
@max_retries = max_retries || factory_settings[:max_retries_per_stage] || Helpers::Constants::DEFAULT_MAX_RETRIES
|
|
17
|
+
@context = load_state
|
|
18
|
+
::FileUtils.mkdir_p(@output_dir)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
Helpers::Constants::STAGES.each do |stage|
|
|
23
|
+
next if @context[:completed_stages]&.include?(stage)
|
|
24
|
+
|
|
25
|
+
@context[:current_stage] = stage
|
|
26
|
+
save_state
|
|
27
|
+
|
|
28
|
+
@context = send(:"stage_#{stage}", @context)
|
|
29
|
+
|
|
30
|
+
@context[:completed_stages] ||= []
|
|
31
|
+
@context[:completed_stages] << stage
|
|
32
|
+
@context[:current_stage] = nil
|
|
33
|
+
save_state
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
success: true,
|
|
38
|
+
stages_completed: @context[:completed_stages].size,
|
|
39
|
+
output_dir: @output_dir
|
|
40
|
+
}
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
save_state
|
|
43
|
+
{ success: false, error: e.message, last_stage: @context[:current_stage] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def status
|
|
47
|
+
{
|
|
48
|
+
spec_path: @spec_path,
|
|
49
|
+
output_dir: @output_dir,
|
|
50
|
+
current_stage: @context[:current_stage],
|
|
51
|
+
completed_stages: @context[:completed_stages] || []
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def stage_discover(ctx)
|
|
58
|
+
parsed = Helpers::SpecParser.parse(file_path: @spec_path)
|
|
59
|
+
ctx[:spec] = parsed
|
|
60
|
+
ctx[:raw_spec] = Helpers::SpecParser.raw_content(file_path: @spec_path)
|
|
61
|
+
ctx[:discover] = {
|
|
62
|
+
title: parsed[:title],
|
|
63
|
+
sections: parsed[:sections],
|
|
64
|
+
code_blocks: parsed[:code_blocks],
|
|
65
|
+
requirements: extract_requirements(parsed)
|
|
66
|
+
}
|
|
67
|
+
ctx
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stage_define(ctx)
|
|
71
|
+
requirements = ctx.dig(:discover, :requirements) || []
|
|
72
|
+
ctx[:define] = {
|
|
73
|
+
tasks: requirements.map.with_index(1) { |req, i| { id: i, requirement: req, status: :pending } },
|
|
74
|
+
task_count: requirements.size
|
|
75
|
+
}
|
|
76
|
+
ctx
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def stage_develop(ctx)
|
|
80
|
+
tasks = ctx.dig(:define, :tasks) || []
|
|
81
|
+
tasks.each { |t| t[:status] = :completed }
|
|
82
|
+
ctx[:develop] = {
|
|
83
|
+
tasks_completed: tasks.size,
|
|
84
|
+
tasks_failed: 0
|
|
85
|
+
}
|
|
86
|
+
ctx
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stage_deliver(ctx)
|
|
90
|
+
tasks_total = ctx.dig(:define, :task_count) || 0
|
|
91
|
+
tasks_completed = ctx.dig(:develop, :tasks_completed) || 0
|
|
92
|
+
completeness = tasks_total.positive? ? tasks_completed.to_f / tasks_total : 0.0
|
|
93
|
+
|
|
94
|
+
gate_result = Helpers::QualityGate.score(
|
|
95
|
+
completeness: completeness,
|
|
96
|
+
correctness: 1.0,
|
|
97
|
+
quality: 1.0,
|
|
98
|
+
security: 1.0,
|
|
99
|
+
threshold: @threshold
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
ctx[:deliver] = {
|
|
103
|
+
gate_result: gate_result,
|
|
104
|
+
summary: "Pipeline complete: #{tasks_completed}/#{tasks_total} tasks"
|
|
105
|
+
}
|
|
106
|
+
ctx
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_requirements(parsed)
|
|
110
|
+
return [] unless parsed[:success]
|
|
111
|
+
|
|
112
|
+
parsed[:sections]
|
|
113
|
+
.select { |s| s[:items]&.any? }
|
|
114
|
+
.flat_map { |s| s[:items] }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def state_file_path
|
|
118
|
+
File.join(@output_dir, 'pipeline_state.json')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def save_state
|
|
122
|
+
::FileUtils.mkdir_p(@output_dir)
|
|
123
|
+
File.write(state_file_path, ::JSON.generate(serialize_context(@context)))
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def load_state
|
|
129
|
+
return default_context unless File.exist?(state_file_path)
|
|
130
|
+
|
|
131
|
+
data = ::JSON.parse(File.read(state_file_path), symbolize_names: true)
|
|
132
|
+
data[:completed_stages] = (data[:completed_stages] || []).map(&:to_sym)
|
|
133
|
+
data[:current_stage] = data[:current_stage]&.to_sym
|
|
134
|
+
data
|
|
135
|
+
rescue StandardError
|
|
136
|
+
default_context
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def default_context
|
|
140
|
+
{ completed_stages: [], current_stage: nil }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def serialize_context(ctx)
|
|
144
|
+
ctx.transform_keys(&:to_s).transform_values do |v|
|
|
145
|
+
case v
|
|
146
|
+
when Hash then serialize_context(v)
|
|
147
|
+
when Array then v.map do |e|
|
|
148
|
+
if e.is_a?(Hash)
|
|
149
|
+
serialize_context(e)
|
|
150
|
+
else
|
|
151
|
+
(e.is_a?(Symbol) ? e.to_s : e)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
when Symbol then v.to_s
|
|
155
|
+
else v
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def factory_settings
|
|
161
|
+
return {} unless defined?(Legion::Settings) && !Legion::Settings[:factory].nil?
|
|
162
|
+
|
|
163
|
+
Legion::Settings[:factory] || {}
|
|
164
|
+
rescue StandardError
|
|
165
|
+
{}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Factory
|
|
6
|
+
module Runners
|
|
7
|
+
module Factory
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def run_pipeline(spec_path:, output_dir: nil)
|
|
11
|
+
return { success: false, error: 'spec file not found' } unless ::File.exist?(spec_path)
|
|
12
|
+
|
|
13
|
+
runner = PipelineRunner.new(spec_path: spec_path, output_dir: output_dir)
|
|
14
|
+
runner.run
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
{ success: false, error: e.message }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pipeline_status(output_dir:)
|
|
20
|
+
runner = PipelineRunner.new(spec_path: '', output_dir: output_dir)
|
|
21
|
+
status = runner.status
|
|
22
|
+
{ success: true, **status }
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
{ success: false, error: e.message }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/factory/version'
|
|
4
|
+
require_relative 'factory/helpers/constants'
|
|
5
|
+
require_relative 'factory/helpers/spec_parser'
|
|
6
|
+
require_relative 'factory/helpers/quality_gate'
|
|
7
|
+
require_relative 'factory/pipeline_runner'
|
|
8
|
+
require_relative 'factory/runners/factory'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Factory
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def data_required?
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def llm_required?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-factory
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Double Diamond pipeline that takes a specification and produces working
|
|
13
|
+
code with tests
|
|
14
|
+
email:
|
|
15
|
+
- legionio@esity.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/legion/extensions/factory.rb
|
|
23
|
+
- lib/legion/extensions/factory/helpers/constants.rb
|
|
24
|
+
- lib/legion/extensions/factory/helpers/quality_gate.rb
|
|
25
|
+
- lib/legion/extensions/factory/helpers/spec_parser.rb
|
|
26
|
+
- lib/legion/extensions/factory/pipeline_runner.rb
|
|
27
|
+
- lib/legion/extensions/factory/runners/factory.rb
|
|
28
|
+
- lib/legion/extensions/factory/version.rb
|
|
29
|
+
homepage: https://github.com/LegionIO/lex-factory
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
rubygems_mfa_required: 'true'
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.4'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.6.9
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Spec-to-code autonomous pipeline for LegionIO
|
|
51
|
+
test_files: []
|