inquirex-tools 0.8.0 → 0.9.5
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 +4 -4
- data/.env.encrypted +11 -0
- data/.env.example +5 -0
- data/.envrc +1 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.sops.yaml +6 -0
- data/.sopsy.sha +1 -0
- data/.sopsy.yml +11 -0
- data/docs/badges/coverage_badge.svg +21 -0
- data/docs/monorepo-migration.md +163 -0
- data/exe/inquirex +3 -2
- data/exe/inquirexor +1 -0
- data/justfile +158 -0
- data/lib/inquirex/tools/changelogs.rb +21 -3
- data/lib/inquirex/tools/commands/changelogs.rb +5 -3
- data/lib/inquirex/tools/commands/create_release.rb +56 -0
- data/lib/inquirex/tools/commands/publish.rb +50 -0
- data/lib/inquirex/tools/commands/versions_bump.rb +34 -0
- data/lib/inquirex/tools/commands/versions_check.rb +36 -0
- data/lib/inquirex/tools/commands.rb +11 -3
- data/lib/inquirex/tools/publisher.rb +229 -0
- data/lib/inquirex/tools/releaser.rb +163 -0
- data/lib/inquirex/tools/version.rb +1 -1
- data/lib/inquirex/tools/version_bumper.rb +230 -0
- data/lib/inquirex/tools/version_checker.rb +113 -0
- data/lib/inquirex/tools/workspace.rb +30 -27
- data/lib/inquirex/tools.rb +12 -5
- metadata +35 -3
- data/lib/inquirex/tools/commands/versions.rb +0 -41
- data/lib/inquirex/tools/versions.rb +0 -125
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8f0e3e2dc5bcc9d45d3c9cba64f8a7d96400b6d1296d316ff56460ece229166
|
|
4
|
+
data.tar.gz: 3ee07acd2e8e731115e0a5552bbbdaeea2979198195e396fc946765f8b6deef6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81ca8eed0f60037439fd3c8ab6aafe9b35114082f2c02db488bf279a3d52577c7fb7c8c7e04a41ab48e0c8ab6be1e8e71e324d32b566cbc865ce861e4855601e
|
|
7
|
+
data.tar.gz: 3adc9867dab085361badf7b00ff38ef9965345efa2ef610bddb27e35e2a34945ebaa2459bdf068e18118cd2121ec67f952564f2f9df562e00d53df49ebb63578
|
data/.env.encrypted
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#ENC[AES256_GCM,data:4mpgKeRs3A2HXmt/FjaIn561sybb9L9oX7gVygSpct//wHWjT5I+lT3Vf5UlMGTv42RKAAL5CgF+SRr4bni33A6CeU3ODQ3vYAo=,iv:oP7xJ9m3C0ezlTznZ1NkqCvu52MLHpDnTkmc7Q0lhhY=,tag:7SoYBTYtcIex2jgqECrAuA==,type:comment]
|
|
2
|
+
#ENC[AES256_GCM,data:89N9zu88zeviY33xeiZOFaLTDxv51jf8SmuZ07NVzAOUDdQAPkKuYkqvOOZpYXjYTDJVY16yYLMYGSmVq8f5rbmWDYC/gJn31m0=,iv:01XkrGj3H6uf6tIdHZxSEpJ/a8LVpo/TidFgRHTJUXo=,tag:lTMSW/QgGaWub1pZrre9TA==,type:comment]
|
|
3
|
+
#ENC[AES256_GCM,data:nYuulT9DlDl+zOPlhK0IqJSahj1KiunqenUbfzPllxxEvJNqMUKU/kVaK73YbaDSUPdAeRXbFH3trz8CQ4uBR5yjQCnv12DD9VVjXw==,iv:7hWr2ajKbKA7lPqtBsmvUycJTF69PVcHUk1kA0T6kXo=,tag:2fnWYnft/u2hVkadn5dsdw==,type:comment]
|
|
4
|
+
#ENC[AES256_GCM,data:kDDhc1ZMB8McDQ2dcEnvT24UWRUq/WNKJ/QuhFpO54K6P0IXFUbmBoqIK2X408HPcJLz9etlw7NAZt1AaQ==,iv:+BRUu6mSooyQt+bazHq/dgXVISTjEfiIJTyUR0N9XgI=,tag:pU6kCwETMJ4neWSHLDBX9g==,type:comment]
|
|
5
|
+
OP_ACCOUNT=ENC[AES256_GCM,data:QLsZSJQqdNSFAIqVDBVHh4kO9TrP9Hm4CZA=,iv:qlqTEsjHfV5AowWnsgWYjU2udMgFmXsYF3GK1PIjzsw=,tag:PUivzRC+pPufSY5P+HG9Rg==,type:str]
|
|
6
|
+
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IER3YUVVQSBBMHphcXd3\nNmtHWjkxVDdwZDZON213c3hmeEpGbW9GSmszM2ZWUGxxRWoyQwpReTZ3bzZFeEJM\nWlF0VVFTNGVnaFlmMVRSUmtoVEZKOCtVRlU1VjYya25FCi0tLSBJTEtJcTducjM4\nSndWNmZub1hiMCsxMTFwbkpSTXVtZnNKemt6UGpzS1BRCptxoolSJ98Ngajq1/pI\nWo0FMY9hEPh5m1m0lBcWBRqbnr23APyywtt89b0UOKhRHhf9KMw6zTfk+KQC3wj0\nNug=\n-----END AGE ENCRYPTED FILE-----\n
|
|
7
|
+
sops_age__list_0__map_recipient=age1se1qd8tpc9v42uuqz974uxx84mdz0aejujrzssmyjl6fvmwgx3gm62wjjxjyws
|
|
8
|
+
sops_lastmodified=2026-08-06T01:38:07Z
|
|
9
|
+
sops_mac=ENC[AES256_GCM,data:4J6BN+p3cXmbGMOfmjXtkPDvRI1jlflrEObvC30TwwrbBXhBW+WLSUOiGM8aVC+pSCzCNykpjkKw47ElMA8jD927xMg1BaglWTtbFPaqVgCVXXnM0c1XL6+oWa80QULZGXViG4W7c4JTYxzqsvhGEpKx0CUFO43GKibx/BaUgAo=,iv:+F76/yNF/kctS0844U7IS9RUyMIg4fkltqlbS+l9mrQ=,tag:rwtNHQpXnEuDLl77qMesZw==,type:str]
|
|
10
|
+
sops_unencrypted_suffix=_unencrypted
|
|
11
|
+
sops_version=3.13.2
|
data/.env.example
ADDED
data/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PATH_add bin
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Relaxed.Ruby.Style
|
|
2
|
+
## Version 2.5
|
|
3
|
+
|
|
4
|
+
Style/Alias:
|
|
5
|
+
Enabled: false
|
|
6
|
+
StyleGuide: https://relaxed.ruby.style/#stylealias
|
|
7
|
+
|
|
8
|
+
Style/AsciiComments:
|
|
9
|
+
Enabled: false
|
|
10
|
+
StyleGuide: https://relaxed.ruby.style/#styleasciicomments
|
|
11
|
+
|
|
12
|
+
Style/BeginBlock:
|
|
13
|
+
Enabled: false
|
|
14
|
+
StyleGuide: https://relaxed.ruby.style/#stylebeginblock
|
|
15
|
+
|
|
16
|
+
Style/BlockDelimiters:
|
|
17
|
+
Enabled: false
|
|
18
|
+
StyleGuide: https://relaxed.ruby.style/#styleblockdelimiters
|
|
19
|
+
|
|
20
|
+
Style/CommentAnnotation:
|
|
21
|
+
Enabled: false
|
|
22
|
+
StyleGuide: https://relaxed.ruby.style/#stylecommentannotation
|
|
23
|
+
|
|
24
|
+
Style/Documentation:
|
|
25
|
+
Enabled: false
|
|
26
|
+
StyleGuide: https://relaxed.ruby.style/#styledocumentation
|
|
27
|
+
|
|
28
|
+
Layout/DotPosition:
|
|
29
|
+
Enabled: false
|
|
30
|
+
StyleGuide: https://relaxed.ruby.style/#layoutdotposition
|
|
31
|
+
|
|
32
|
+
Style/DoubleNegation:
|
|
33
|
+
Enabled: false
|
|
34
|
+
StyleGuide: https://relaxed.ruby.style/#styledoublenegation
|
|
35
|
+
|
|
36
|
+
Style/EndBlock:
|
|
37
|
+
Enabled: false
|
|
38
|
+
StyleGuide: https://relaxed.ruby.style/#styleendblock
|
|
39
|
+
|
|
40
|
+
Style/FormatString:
|
|
41
|
+
Enabled: false
|
|
42
|
+
StyleGuide: https://relaxed.ruby.style/#styleformatstring
|
|
43
|
+
|
|
44
|
+
Style/IfUnlessModifier:
|
|
45
|
+
Enabled: false
|
|
46
|
+
StyleGuide: https://relaxed.ruby.style/#styleifunlessmodifier
|
|
47
|
+
|
|
48
|
+
Style/Lambda:
|
|
49
|
+
Enabled: false
|
|
50
|
+
StyleGuide: https://relaxed.ruby.style/#stylelambda
|
|
51
|
+
|
|
52
|
+
Style/ModuleFunction:
|
|
53
|
+
Enabled: false
|
|
54
|
+
StyleGuide: https://relaxed.ruby.style/#stylemodulefunction
|
|
55
|
+
|
|
56
|
+
Style/MultilineBlockChain:
|
|
57
|
+
Enabled: false
|
|
58
|
+
StyleGuide: https://relaxed.ruby.style/#stylemultilineblockchain
|
|
59
|
+
|
|
60
|
+
Style/NegatedIf:
|
|
61
|
+
Enabled: false
|
|
62
|
+
StyleGuide: https://relaxed.ruby.style/#stylenegatedif
|
|
63
|
+
|
|
64
|
+
Style/NegatedWhile:
|
|
65
|
+
Enabled: false
|
|
66
|
+
StyleGuide: https://relaxed.ruby.style/#stylenegatedwhile
|
|
67
|
+
|
|
68
|
+
Style/NumericPredicate:
|
|
69
|
+
Enabled: false
|
|
70
|
+
StyleGuide: https://relaxed.ruby.style/#stylenumericpredicate
|
|
71
|
+
|
|
72
|
+
Style/ParallelAssignment:
|
|
73
|
+
Enabled: false
|
|
74
|
+
StyleGuide: https://relaxed.ruby.style/#styleparallelassignment
|
|
75
|
+
|
|
76
|
+
Style/PercentLiteralDelimiters:
|
|
77
|
+
Enabled: false
|
|
78
|
+
StyleGuide: https://relaxed.ruby.style/#stylepercentliteraldelimiters
|
|
79
|
+
|
|
80
|
+
Style/PerlBackrefs:
|
|
81
|
+
Enabled: false
|
|
82
|
+
StyleGuide: https://relaxed.ruby.style/#styleperlbackrefs
|
|
83
|
+
|
|
84
|
+
Style/Semicolon:
|
|
85
|
+
Enabled: false
|
|
86
|
+
StyleGuide: https://relaxed.ruby.style/#stylesemicolon
|
|
87
|
+
|
|
88
|
+
Style/SignalException:
|
|
89
|
+
Enabled: false
|
|
90
|
+
StyleGuide: https://relaxed.ruby.style/#stylesignalexception
|
|
91
|
+
|
|
92
|
+
Style/SingleLineBlockParams:
|
|
93
|
+
Enabled: false
|
|
94
|
+
StyleGuide: https://relaxed.ruby.style/#stylesinglelineblockparams
|
|
95
|
+
|
|
96
|
+
Style/SingleLineMethods:
|
|
97
|
+
Enabled: false
|
|
98
|
+
StyleGuide: https://relaxed.ruby.style/#stylesinglelinemethods
|
|
99
|
+
|
|
100
|
+
Layout/SpaceBeforeBlockBraces:
|
|
101
|
+
Enabled: false
|
|
102
|
+
StyleGuide: https://relaxed.ruby.style/#layoutspacebeforeblockbraces
|
|
103
|
+
|
|
104
|
+
Layout/SpaceInsideParens:
|
|
105
|
+
Enabled: false
|
|
106
|
+
StyleGuide: https://relaxed.ruby.style/#layoutspaceinsideparens
|
|
107
|
+
|
|
108
|
+
Style/SpecialGlobalVars:
|
|
109
|
+
Enabled: false
|
|
110
|
+
StyleGuide: https://relaxed.ruby.style/#stylespecialglobalvars
|
|
111
|
+
|
|
112
|
+
Style/StringLiterals:
|
|
113
|
+
Enabled: false
|
|
114
|
+
StyleGuide: https://relaxed.ruby.style/#stylestringliterals
|
|
115
|
+
|
|
116
|
+
Style/TrailingCommaInArguments:
|
|
117
|
+
Enabled: false
|
|
118
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarguments
|
|
119
|
+
|
|
120
|
+
Style/TrailingCommaInArrayLiteral:
|
|
121
|
+
Enabled: false
|
|
122
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarrayliteral
|
|
123
|
+
|
|
124
|
+
Style/TrailingCommaInHashLiteral:
|
|
125
|
+
Enabled: false
|
|
126
|
+
StyleGuide: https://relaxed.ruby.style/#styletrailingcommainhashliteral
|
|
127
|
+
|
|
128
|
+
Style/SymbolArray:
|
|
129
|
+
Enabled: false
|
|
130
|
+
StyleGuide: http://relaxed.ruby.style/#stylesymbolarray
|
|
131
|
+
|
|
132
|
+
Style/WhileUntilModifier:
|
|
133
|
+
Enabled: false
|
|
134
|
+
StyleGuide: https://relaxed.ruby.style/#stylewhileuntilmodifier
|
|
135
|
+
|
|
136
|
+
Style/WordArray:
|
|
137
|
+
Enabled: false
|
|
138
|
+
StyleGuide: https://relaxed.ruby.style/#stylewordarray
|
|
139
|
+
|
|
140
|
+
Lint/AmbiguousRegexpLiteral:
|
|
141
|
+
Enabled: false
|
|
142
|
+
StyleGuide: https://relaxed.ruby.style/#lintambiguousregexpliteral
|
|
143
|
+
|
|
144
|
+
Lint/AssignmentInCondition:
|
|
145
|
+
Enabled: false
|
|
146
|
+
StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
|
|
147
|
+
|
|
148
|
+
Layout/LineLength:
|
|
149
|
+
Enabled: false
|
|
150
|
+
|
|
151
|
+
Metrics:
|
|
152
|
+
Enabled: false
|
|
153
|
+
|
data/.sops.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Managed by sopsy. Maps encrypted files to their age recipients.
|
|
2
|
+
creation_rules:
|
|
3
|
+
- path_regex: '\.env\.encrypted$'
|
|
4
|
+
age: 'age1se1qd8tpc9v42uuqz974uxx84mdz0aejujrzssmyjl6fvmwgx3gm62wjjxjyws'
|
|
5
|
+
- path_regex: '\.encrypted$'
|
|
6
|
+
age: 'age1se1qd8tpc9v42uuqz974uxx84mdz0aejujrzssmyjl6fvmwgx3gm62wjjxjyws'
|
data/.sopsy.sha
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b95fff7b1445f7690843e41678e2676916ec792390828e5755e7ec554812aece
|
data/.sopsy.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
recipients:
|
|
2
|
+
- name: admin
|
|
3
|
+
public_key: age1se1qd8tpc9v42uuqz974uxx84mdz0aejujrzssmyjl6fvmwgx3gm62wjjxjyws
|
|
4
|
+
username: kig
|
|
5
|
+
break_glass: false
|
|
6
|
+
encrypted_globs:
|
|
7
|
+
- '*.encrypted'
|
|
8
|
+
- .env.encrypted
|
|
9
|
+
- config/*.encrypted.yaml
|
|
10
|
+
sops_version: 3.13.2
|
|
11
|
+
join_request_ttl: 72h
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
|
|
3
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
4
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
5
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<mask id="a">
|
|
8
|
+
<rect width="99" height="20" rx="3" fill="#fff"/>
|
|
9
|
+
</mask>
|
|
10
|
+
<g mask="url(#a)">
|
|
11
|
+
<path fill="#555" d="M0 0h63v20H0z"/>
|
|
12
|
+
<path fill="#4c1" d="M63 0h36v20H63z"/>
|
|
13
|
+
<path fill="url(#b)" d="M0 0h99v20H0z"/>
|
|
14
|
+
</g>
|
|
15
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
16
|
+
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
17
|
+
<text x="31.5" y="14">coverage</text>
|
|
18
|
+
<text x="80" y="15" fill="#010101" fill-opacity=".3">97%</text>
|
|
19
|
+
<text x="80" y="14">97%</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Monorepo Migration Plan
|
|
2
|
+
|
|
3
|
+
Status: **proposed**, not started. Execute only after the open PRs listed under [Sequencing](#sequencing) have merged.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The Inquirex ecosystem implements the same semantics in two languages and enforces agreement by convention. Three independent implementations of "canonicalize an LLM answer against a question's allowed values" exist today:
|
|
8
|
+
|
|
9
|
+
| Implementation | Repo | Exact | Case-insensitive value | Label fallback | Strips whitespace |
|
|
10
|
+
| ------------------------- | --------------- | ----- | ---------------------- | -------------- | ----------------- |
|
|
11
|
+
| `Node#resolve_option` | inquirex | yes | yes | yes | no |
|
|
12
|
+
| `Adapter#canonical_value` | inquirex-llm | yes | yes | **no** | **yes** |
|
|
13
|
+
| `canonicalOption` | inquirex-widget | yes | yes | yes | **no** |
|
|
14
|
+
|
|
15
|
+
They disagree on real inputs. `"US citizen or permanent resident"` resolves through the gem and the widget but returns `nil` through the LLM adapter. `" us_person "` resolves only through the LLM adapter. Nothing in CI can observe this, because no CI job sees more than one of them.
|
|
16
|
+
|
|
17
|
+
This is the failure mode the family already has scar tissue for. The gem shipped `required false` in 0.7.0 and no consumer knew, because a verb present in one package and missing in another does not fail loudly — it silently drops data. The lockstep version rule exists to make "which combination has this bug?" a lookup rather than an investigation.
|
|
18
|
+
|
|
19
|
+
The rule is not actually enforced. Every inter-family gemspec declares `~> 0.6`, which admits any 0.x from 0.6 upward. `inquirex-llm`'s checked-in `Gemfile.lock` currently resolves `inquirex-llm (0.9.2)` against `inquirex (0.8.0)`.
|
|
20
|
+
|
|
21
|
+
A monorepo does not fix divergence by itself. It makes the fix — one shared fixture set, executed by every implementation in one CI run — cheap enough to actually build.
|
|
22
|
+
|
|
23
|
+
## Scope
|
|
24
|
+
|
|
25
|
+
**In**, as one public repository:
|
|
26
|
+
|
|
27
|
+
- `inquirex` — the DSL, engine, and rule AST
|
|
28
|
+
- `inquirex-llm` — LLM verbs
|
|
29
|
+
- `inquirex-widget` — the embeddable TypeScript widget
|
|
30
|
+
- `inquirex-tty` — terminal adapter
|
|
31
|
+
- `inquirex-tools` — release and version tooling (largely absorbed; see below)
|
|
32
|
+
|
|
33
|
+
**Out**, staying as they are:
|
|
34
|
+
|
|
35
|
+
- `qualified-at` — the commercial Rails application. Private, its own release cadence, and it consumes the libraries as published packages. Folding a revenue product into the open-source repository forces a visibility decision on the whole tree and buys nothing.
|
|
36
|
+
- `inquirex-webui` — private, React, a distinct product (visual builder). Revisit once the library tree has settled.
|
|
37
|
+
- `inquirex-presentation`, `inquirex-slides`, `claude-setup` — unrelated artifacts.
|
|
38
|
+
|
|
39
|
+
The in-scope set is exactly the packages bound by the lockstep rule, minus `inquirex-webui`, plus `inquirex-tty` (which PR #2 in this repo brings into lockstep).
|
|
40
|
+
|
|
41
|
+
## Target layout
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
inquirex/
|
|
45
|
+
packages/
|
|
46
|
+
inquirex/ # gem: DSL, engine, rules
|
|
47
|
+
inquirex-llm/ # gem: LLM verbs
|
|
48
|
+
inquirex-tty/ # gem: terminal adapter
|
|
49
|
+
inquirex-widget/ # npm: TypeScript widget
|
|
50
|
+
conformance/
|
|
51
|
+
fixtures/ # language-neutral JSON cases
|
|
52
|
+
ruby/ # runner: executes fixtures against the gems
|
|
53
|
+
typescript/ # runner: executes fixtures against the widget
|
|
54
|
+
tools/ # release tooling (from inquirex-tools)
|
|
55
|
+
justfile # top-level recipes; per-package justfiles remain
|
|
56
|
+
.github/workflows/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each package keeps its own gemspec / package.json, test suite, and justfile. The monorepo adds a layer above them, it does not merge their build systems.
|
|
60
|
+
|
|
61
|
+
## Migration mechanics
|
|
62
|
+
|
|
63
|
+
History is preserved. Do not squash the repositories into a single import commit.
|
|
64
|
+
|
|
65
|
+
For each repository, in the order listed under Sequencing:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# 1. Clone a scratch copy — never run filter-repo against your working checkout
|
|
69
|
+
git clone git@github.com:inquirex/<pkg>.git /tmp/mono/<pkg>
|
|
70
|
+
cd /tmp/mono/<pkg>
|
|
71
|
+
|
|
72
|
+
# 2. Rewrite every path to sit under packages/<pkg>/
|
|
73
|
+
git filter-repo --to-subdirectory-filter packages/<pkg>
|
|
74
|
+
|
|
75
|
+
# 3. Graft it into the monorepo as an unrelated history
|
|
76
|
+
cd /path/to/inquirex-monorepo
|
|
77
|
+
git remote add <pkg> /tmp/mono/<pkg>
|
|
78
|
+
git fetch <pkg>
|
|
79
|
+
git merge --allow-unrelated-histories <pkg>/main -m "Import <pkg> under packages/"
|
|
80
|
+
git remote remove <pkg>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`git filter-repo` requires a fresh clone and refuses to run on a repo with an origin unless forced — that is a guardrail, not an obstacle. Install with `brew install git-filter-repo`.
|
|
84
|
+
|
|
85
|
+
Path rewriting means `git log --follow packages/inquirex/lib/inquirex/node.rb` walks the full pre-migration history. Blame survives. Existing tags survive but become ambiguous across packages — retag as `<pkg>/vX.Y.Z` after the import, or accept that pre-migration tags refer to a single package's history.
|
|
86
|
+
|
|
87
|
+
### What happens to the old repositories
|
|
88
|
+
|
|
89
|
+
Archive, do not delete. Each keeps its issues, its pull requests, and its clone URLs. GitHub archived repos stay readable and redirect nothing, so add a line to each README pointing at the monorepo path. RubyGems and npm metadata should be updated at the next release to point `source_code_uri` / `repository.url` at `inquirex/inquirex` with the appropriate `directory`.
|
|
90
|
+
|
|
91
|
+
Both npm and RubyGems support a subdirectory hint:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
// packages/inquirex-widget/package.json
|
|
95
|
+
"repository": { "type": "git", "url": "git+https://github.com/inquirex/inquirex.git", "directory": "packages/inquirex-widget" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# packages/inquirex/inquirex.gemspec
|
|
100
|
+
spec.metadata["source_code_uri"] = "https://github.com/inquirex/inquirex/tree/main/packages/inquirex"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Versioning and release
|
|
104
|
+
|
|
105
|
+
One version number for the tree, which is what the lockstep rule always wanted. A single `VERSION` file at the root; each gemspec and `package.json` reads from it at build time.
|
|
106
|
+
|
|
107
|
+
This deletes most of `inquirex-tools`: cross-repo version checking and bumping becomes `just version <increment>` operating on one file. Keep the release recipes (tagging, GitHub release, publishing); drop the multi-repo coordination.
|
|
108
|
+
|
|
109
|
+
Fix the constraint hole at the same time. Inter-package dependencies move from `~> 0.6` to `= <VERSION>` or `~> <MAJOR>.<MINOR>`, so bundler cannot resolve a mismatched pair. Do this **in the release that first ships from the monorepo**, not before — tightening `inquirex-llm` to `~> 0.9` while `inquirex` 0.9.x is unpublished breaks `bundle install`.
|
|
110
|
+
|
|
111
|
+
## CI
|
|
112
|
+
|
|
113
|
+
One workflow with path filters, so a widget-only change does not run the Ruby matrix:
|
|
114
|
+
|
|
115
|
+
- `ruby.yml` — triggers on `packages/inquirex*/**` and `conformance/**`; runs each gem's suite
|
|
116
|
+
- `typescript.yml` — triggers on `packages/inquirex-widget/**` and `conformance/**`; runs `bun test`, typecheck, lint, build
|
|
117
|
+
- `conformance.yml` — triggers on **everything**; runs the shared fixtures against both language runners
|
|
118
|
+
|
|
119
|
+
The third workflow is the point of the exercise. It must not be path-filtered.
|
|
120
|
+
|
|
121
|
+
## The conformance suite
|
|
122
|
+
|
|
123
|
+
A fixture is a JSON document describing a flow, a sequence of inputs, and the expected resulting state:
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
{
|
|
127
|
+
"name": "extraction resolves a label to its form value",
|
|
128
|
+
"flow": { "...": "flow definition" },
|
|
129
|
+
"steps": [
|
|
130
|
+
{ "extract": { "residency_status": "US citizen or permanent resident" } }
|
|
131
|
+
],
|
|
132
|
+
"expect": {
|
|
133
|
+
"answers": { "residency_status": "us_person" },
|
|
134
|
+
"current_step": "income_types",
|
|
135
|
+
"suggestions": {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each language ships a thin runner that loads a fixture, drives its own engine, and asserts the expected state. Adding a case means adding one JSON file; both languages are then obliged to agree or fail.
|
|
141
|
+
|
|
142
|
+
Seed it with the divergence table above — those are three cases that fail today. Then extend to transition resolution, `skip_if`, accumulators, and multi-select suggestion handling.
|
|
143
|
+
|
|
144
|
+
This is the deliverable that justifies the migration. Everything else is filing.
|
|
145
|
+
|
|
146
|
+
## Sequencing
|
|
147
|
+
|
|
148
|
+
1. Merge the open work first. Migrating on top of open pull requests strands them: `inquirex-widget` #6-#9, `inquirex` #5/#6, `inquirex-llm` #3, `inquirex-tools` #1/#2, `inquirex-tty` #10. Close or rebase the stale ones (`inquirex-tty` #6 is conflicting and failing; `inquirex` #5 and `inquirex-tools` #1 appear superseded).
|
|
149
|
+
1. Publish 0.9.2 from the existing repositories, so a known-good release exists on both registries before anything moves.
|
|
150
|
+
1. Create the monorepo and import packages in dependency order: `inquirex`, `inquirex-llm`, `inquirex-tty`, `inquirex-widget`, `inquirex-tools`.
|
|
151
|
+
1. Wire up the top-level justfile and per-language CI. Prove every existing suite still passes unchanged.
|
|
152
|
+
1. Build the conformance harness with the three known-divergent cases. Watch them fail.
|
|
153
|
+
1. Reconcile the three implementations. Decide the canonical rules — label fallback yes or no, whitespace stripping yes or no — and make all three obey.
|
|
154
|
+
1. Move to a single root `VERSION`, tighten the inter-package constraints, release 0.10.0 from the monorepo.
|
|
155
|
+
1. Archive the old repositories with a pointer in each README.
|
|
156
|
+
|
|
157
|
+
Steps 1-2 are prerequisites. Steps 3-4 are reversible: if the imported tree is unsatisfactory, delete it — the original repositories are untouched until step 8.
|
|
158
|
+
|
|
159
|
+
## Open decisions
|
|
160
|
+
|
|
161
|
+
- **`inquirex-webui`** — in or out. It is bound by the lockstep rule but is private and a different product. Leaving it out means the rule stays partly manual.
|
|
162
|
+
- **Visibility** — the monorepo must be public for the gems to remain credible open source. That is only tenable if nothing private is imported, which the scope above respects.
|
|
163
|
+
- **Canonical matching rules** — the reconciliation in step 6 is a semantics decision, not a mechanical one. Whitespace stripping is almost certainly correct everywhere. The label fallback is the real question: it is most needed in `inquirex-llm`, which is the one implementation lacking it.
|
data/exe/inquirex
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
# binary; this gem owns the bare name so cross-family tooling has one entry
|
|
6
6
|
# point and no two gems install a binary called `inquirex`.
|
|
7
7
|
#
|
|
8
|
-
# inquirex versions
|
|
9
|
-
# inquirex versions --
|
|
8
|
+
# inquirex versions-check # lockstep parity across the family
|
|
9
|
+
# inquirex versions-check --preflight # parity plus git release readiness
|
|
10
|
+
# inquirex versions-bump 0.8.0 # move every lockstep package
|
|
10
11
|
# inquirex changelogs
|
|
11
12
|
#
|
|
12
13
|
# Inside a checkout, prefer the repo's own Gemfile over whatever rubygems has
|
data/exe/inquirexor
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inquirex
|
data/justfile
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Tell 'just' to run bash, source our setup script, then execute the recipe
|
|
2
|
+
set shell := ["bash", "-c"]
|
|
3
|
+
|
|
4
|
+
version := `grep VERSION lib/inquirex/tools/version.rb | awk '{print $3}' | tr -d '"' | tr -d '\n'`
|
|
5
|
+
rbenv := 'eval "$(rbenv init - bash)"; bundle exec '
|
|
6
|
+
repo := 'git@github.com:inquirex/inquirex-tools.git'
|
|
7
|
+
|
|
8
|
+
# 1Password secret reference for the RubyGems TOTP, and the full path to `op`
|
|
9
|
+
# because a recipe does not inherit an interactive shell's PATH.
|
|
10
|
+
# The account is NOT named here: it is a private 1Password address and this
|
|
11
|
+
# file is public. `op` reads it from OP_ACCOUNT, exported by the ecosystem
|
|
12
|
+
# root's .envrc, which lives outside every repo. Two accounts are
|
|
13
|
+
# registered and only one holds the open-source-repos vault, so without
|
|
14
|
+
# OP_ACCOUNT the read can resolve against the wrong one, return nothing,
|
|
15
|
+
# and publish silently falls back to prompting mid-release.
|
|
16
|
+
op := '/opt/homebrew/bin/op'
|
|
17
|
+
otp_ref := 'op://open-source-repos/ruby-gems/one-time password?attribute=otp'
|
|
18
|
+
|
|
19
|
+
gem_name := 'inquirex-tools'
|
|
20
|
+
gem_file := 'pkg/' + gem_name + '-' + version + '.gem'
|
|
21
|
+
gem_url := 'https://rubygems.org/gems/' + gem_name
|
|
22
|
+
|
|
23
|
+
[no-exit-message]
|
|
24
|
+
recipes:
|
|
25
|
+
just --choose
|
|
26
|
+
|
|
27
|
+
# Sync all dependencies
|
|
28
|
+
install:
|
|
29
|
+
bin/setup
|
|
30
|
+
|
|
31
|
+
build: install
|
|
32
|
+
|
|
33
|
+
# Lint and reformat files
|
|
34
|
+
lint:
|
|
35
|
+
{{ rbenv }} rubocop
|
|
36
|
+
|
|
37
|
+
# Lint and reformat files (-a) — pass -A as an argument
|
|
38
|
+
format *args:
|
|
39
|
+
{{ rbenv }} rubocop -a {{ args }}
|
|
40
|
+
/usr/bin/find . -name '*.md' -exec mdformat --wrap no {} \; -print
|
|
41
|
+
|
|
42
|
+
# Run all the tests
|
|
43
|
+
test *args:
|
|
44
|
+
export ENVIRONMENT=test; {{ rbenv }} rspec {{args}}
|
|
45
|
+
|
|
46
|
+
# Run tests with coverage
|
|
47
|
+
test-coverage *args:
|
|
48
|
+
export ENVIRONMENT=test; export COVERAGE=true; {{ rbenv }} rspec {{ args }}
|
|
49
|
+
|
|
50
|
+
ci: lint test-coverage
|
|
51
|
+
|
|
52
|
+
alias check-all := ci
|
|
53
|
+
|
|
54
|
+
clean:
|
|
55
|
+
#!/usr/bin/env bash
|
|
56
|
+
@find . -name .DS_Store -delete -print || true
|
|
57
|
+
@rm -rf tmp/*
|
|
58
|
+
|
|
59
|
+
# Run all lefthook pre-commit hooks
|
|
60
|
+
lefthook:
|
|
61
|
+
{{ rbenv }} lefthook run pre-commit --all-files
|
|
62
|
+
|
|
63
|
+
# Print current gem version
|
|
64
|
+
version:
|
|
65
|
+
@echo "{{ version }}"
|
|
66
|
+
|
|
67
|
+
# Clobber
|
|
68
|
+
clobber:
|
|
69
|
+
{{ rbenv }} rake clobber
|
|
70
|
+
|
|
71
|
+
# Generate documentation
|
|
72
|
+
doc:
|
|
73
|
+
#!/usr/bin/env bash
|
|
74
|
+
{{ rbenv }} rake doc
|
|
75
|
+
|
|
76
|
+
# `gem push` rather than `rake release`: release also guards the tree, tags and
|
|
77
|
+
# pushes git — which `just release` does deliberately and separately — and it
|
|
78
|
+
# gives no way to pass a 2FA code, so it always stopped to prompt.
|
|
79
|
+
#
|
|
80
|
+
# The code comes from 1Password unless one is passed in:
|
|
81
|
+
#
|
|
82
|
+
# just publish # read the code from 1Password
|
|
83
|
+
# just publish 123456 # use this code
|
|
84
|
+
#
|
|
85
|
+
# `just publish-all` in inquirex-tools passes one, because a TOTP is single-use:
|
|
86
|
+
# four gems reading the same 30-second window would have the second push
|
|
87
|
+
# rejected as a replay.
|
|
88
|
+
#
|
|
89
|
+
# Build the .gem and push it to RubyGems, non-interactively
|
|
90
|
+
publish otp="": build
|
|
91
|
+
#!/usr/bin/env bash
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
eval "$(rbenv init - bash 2>/dev/null || true)"
|
|
94
|
+
|
|
95
|
+
mkdir -p pkg
|
|
96
|
+
gem build {{ gem_name }}.gemspec --output "{{ gem_file }}"
|
|
97
|
+
|
|
98
|
+
# `|| true` is load-bearing: under `set -e` a failed `op read` — not signed
|
|
99
|
+
# in to 1Password, item renamed, op not installed — would abort the recipe
|
|
100
|
+
# before the prompting fallback below could run.
|
|
101
|
+
otp="{{ otp }}"
|
|
102
|
+
[[ -n "${otp}" ]] || otp=$({{ op }} read "{{ otp_ref }}" 2>/dev/null || true)
|
|
103
|
+
|
|
104
|
+
if [[ -n "${otp}" ]]; then
|
|
105
|
+
gem push "{{ gem_file }}" --otp "${otp}"
|
|
106
|
+
else
|
|
107
|
+
echo "rubygems: no OTP available — gem push will prompt if 2FA is required."
|
|
108
|
+
gem push "{{ gem_file }}"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Only reachable when the push succeeded: `set -e` aborts the recipe on a
|
|
112
|
+
# non-zero `gem push`, so the page never opens for a release that failed.
|
|
113
|
+
echo "published {{ gem_name }} {{ version }} → {{ gem_url }}"
|
|
114
|
+
open "{{ gem_url }}" 2>/dev/null || xdg-open "{{ gem_url }}" 2>/dev/null || true
|
|
115
|
+
|
|
116
|
+
# Publish every package in the family, gems and npm packages alike.
|
|
117
|
+
#
|
|
118
|
+
# A thin wrapper: the ordering, the per-gem 2FA code and the stop-on-failure
|
|
119
|
+
# rule all live in Inquirex::Tools::Publisher, where they are unit-tested. This
|
|
120
|
+
# recipe only supplies the environment — OP_ACCOUNT, decrypted from
|
|
121
|
+
# .env.encrypted — that the Ruby side cannot obtain for itself.
|
|
122
|
+
#
|
|
123
|
+
# Only this repo carries the sopsy setup. Each package's `just publish` runs as
|
|
124
|
+
# a grandchild process and inherits the exported value, so the other five repos
|
|
125
|
+
# stay free of an encrypted env and a Touch ID prompt they do not need.
|
|
126
|
+
#
|
|
127
|
+
# just publish-all # publish for real
|
|
128
|
+
# just publish-all --dry-run # print the commands, run nothing
|
|
129
|
+
#
|
|
130
|
+
# Publish every package in the family (--dry-run to preview)
|
|
131
|
+
publish-all *args:
|
|
132
|
+
#!/usr/bin/env bash
|
|
133
|
+
set -euo pipefail
|
|
134
|
+
eval "$(rbenv init - bash 2>/dev/null || true)"
|
|
135
|
+
|
|
136
|
+
# `|| true`: a missing key, a locked Secure Enclave or no sopsy at all must
|
|
137
|
+
# not abort a release. Without OP_ACCOUNT the run still works — `op` falls
|
|
138
|
+
# back to the default account, and failing that, prompts.
|
|
139
|
+
if [[ -f .env.encrypted ]]; then
|
|
140
|
+
if secrets=$(sopsy decrypt .env.encrypted 2>/dev/null); then
|
|
141
|
+
set -a
|
|
142
|
+
eval "$(printf '%s\n' "${secrets}" | sed -E '/^#/d; /^$/d')"
|
|
143
|
+
set +a
|
|
144
|
+
echo "sopsy: OP_ACCOUNT=${OP_ACCOUNT:-<unset>}"
|
|
145
|
+
else
|
|
146
|
+
echo "sopsy: could not decrypt .env.encrypted — continuing without OP_ACCOUNT" >&2
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
bundle exec exe/inquirex publish {{ args }}
|
|
151
|
+
|
|
152
|
+
# Tag v{{ version }}, publish the GH release, & refresh the Homebrew tap.
|
|
153
|
+
release:
|
|
154
|
+
git fetch --tags
|
|
155
|
+
git tag -f "v{{ version }}"
|
|
156
|
+
git push -f --tags
|
|
157
|
+
gh release delete -y "v{{ version }}" --repo {{ repo }} 2>/dev/null || true
|
|
158
|
+
gh release create "v{{ version }}" --generate-notes --repo {{ repo }}
|
|
@@ -18,11 +18,29 @@ module Inquirex
|
|
|
18
18
|
# Never overwritten by generation, in any repo.
|
|
19
19
|
PROTECTED = %w[INQUIREX_VERSION_LOG.md].freeze
|
|
20
20
|
|
|
21
|
+
# Reads a GitHub token from the `gh` session rather than a stored secret,
|
|
22
|
+
# so nothing here handles a credential in plain text.
|
|
23
|
+
DEFAULT_TOKEN_READER = -> { `gh auth token 2>/dev/null`.strip }
|
|
24
|
+
|
|
25
|
+
# Runs the generator in one repository, discarding its very chatty output.
|
|
26
|
+
DEFAULT_RUNNER = lambda { |dir, env|
|
|
27
|
+
system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
# @param workspace [Workspace]
|
|
22
31
|
# @param out [IO]
|
|
23
|
-
|
|
32
|
+
# @param token_reader [#call] returns a GitHub token, or "" when there is
|
|
33
|
+
# no session. Injected so a spec never needs one.
|
|
34
|
+
# @param runner [#call] invoked with (dir, env) to run the generator.
|
|
35
|
+
# Injected so a spec never shells out to it.
|
|
36
|
+
def initialize(workspace: Workspace.new,
|
|
37
|
+
out: $stdout,
|
|
38
|
+
token_reader: DEFAULT_TOKEN_READER,
|
|
39
|
+
runner: DEFAULT_RUNNER)
|
|
24
40
|
@workspace = workspace
|
|
25
41
|
@out = out
|
|
42
|
+
@token_reader = token_reader
|
|
43
|
+
@runner = runner
|
|
26
44
|
end
|
|
27
45
|
|
|
28
46
|
# @return [Array<String>] every repo carrying a generator config
|
|
@@ -41,7 +59,7 @@ module Inquirex
|
|
|
41
59
|
# @param names [Array<String>] repositories, or all configured when empty
|
|
42
60
|
# @return [Boolean] true when every repository succeeded
|
|
43
61
|
def generate(*names)
|
|
44
|
-
token =
|
|
62
|
+
token = @token_reader.call.to_s.strip
|
|
45
63
|
if token.empty?
|
|
46
64
|
@out.puts "ERROR: `gh auth token` returned nothing — run `gh auth login`"
|
|
47
65
|
return false
|
|
@@ -69,7 +87,7 @@ module Inquirex
|
|
|
69
87
|
|
|
70
88
|
@out.print format(" %<name>-16s ", name: name)
|
|
71
89
|
env = { "CHANGELOG_GITHUB_TOKEN" => token, "RBENV_VERSION" => ENV.fetch("RBENV_VERSION", DEFAULT_RUBY) }
|
|
72
|
-
if
|
|
90
|
+
if @runner.call(dir, env)
|
|
73
91
|
@out.puts "ok (#{line_count(File.join(dir, "CHANGELOG.md"))} lines)"
|
|
74
92
|
true
|
|
75
93
|
else
|
|
@@ -7,12 +7,14 @@ module Inquirex
|
|
|
7
7
|
class Changelogs < Dry::CLI::Command
|
|
8
8
|
desc "Regenerate CHANGELOG.md from merged PRs"
|
|
9
9
|
|
|
10
|
-
argument :repos,
|
|
11
|
-
|
|
10
|
+
argument :repos,
|
|
11
|
+
type: :array,
|
|
12
|
+
required: false,
|
|
13
|
+
desc: "Repositories to regenerate (default: all configured)"
|
|
12
14
|
|
|
13
15
|
example [
|
|
14
16
|
" # every repo with a .github_changelog_generator",
|
|
15
|
-
"inquirex-
|
|
17
|
+
"inquirex-widget # just one"
|
|
16
18
|
]
|
|
17
19
|
|
|
18
20
|
# The GitHub token comes from `gh auth token`, so no secret is stored
|