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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2027915b1b69d07af80a30a63207731809243ae607da736ea2d558c2748c6c1f
4
- data.tar.gz: 6c35d199ad702ffd061d90bf5fa5ebe17da1fc8b1f0d7722e3f8bf37676b3e4b
3
+ metadata.gz: d8f0e3e2dc5bcc9d45d3c9cba64f8a7d96400b6d1296d316ff56460ece229166
4
+ data.tar.gz: 3ee07acd2e8e731115e0a5552bbbdaeea2979198195e396fc946765f8b6deef6
5
5
  SHA512:
6
- metadata.gz: a97efe3bc05fc24445e9b40920ec073055305e26c92764a7977cea0152cd33a25c28ccca97cf3e129ced24150b5957f0a137c57bc10172c1054cea7a5300ddfb
7
- data.tar.gz: 0a975ec1e72ad597bf19827d9fe5f5afe68523bfea5809729a23ae184ab61a380b7aeff9128df1e984042c307a9de6c4ea464c660bf668474e731aa78779664e
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
@@ -0,0 +1,5 @@
1
+ # Example environment variables for this project.
2
+ # Copy this file to `.env`, fill in real values, then encrypt with sopsy.
3
+ # `.env` itself is gitignored and must never be committed in plaintext.
4
+ DATABASE_URL=postgres://localhost:5432/myapp
5
+ API_KEY=replace-me
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 # lockstep parity across the family
9
- # inquirex versions --set 0.8.0
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
- def initialize(workspace: Workspace.new, out: $stdout)
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 = `gh auth token 2>/dev/null`.strip
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 system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
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, type: :array, required: false,
11
- desc: "Repositories to regenerate (default: all configured)"
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-js # just one"
17
+ "inquirex-widget # just one"
16
18
  ]
17
19
 
18
20
  # The GitHub token comes from `gh auth token`, so no secret is stored