scint 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54c3f7baaea3992bcba60136b76f3b08caa580717d82906ebed3a0a69fb23314
4
- data.tar.gz: 246966d0ba167f43b8c0aee1205e95377afec245dca33060fe3fcce202d0c3ab
3
+ metadata.gz: 4b276421c3f10fed0f5211f84930f1280c73b4b087891177b2146eeb25c4ff03
4
+ data.tar.gz: 70c5d92d2d14c747f71499196499a4ddf58a38e578ac7ae4e27deb3d645da2dc
5
5
  SHA512:
6
- metadata.gz: 84c58296b1c391e68503090ea3889a3a469c8f63ca6c289710dc6a5f986cd33059aec6a61038ab14c2f653aefaa1675815d2e13a5ba3fb49dad6fe825cf8d3a5
7
- data.tar.gz: 1cde1ae7ff37856bf44a4f909e024cb094ba1436fe062536aa4c73860ce0aa7c2494c2f5dd09ca14af5be8b226d8ada36ec212278b08a55902da708d734bea3d
6
+ metadata.gz: 855cf61d3a05a3bc5e4fa886a8e9243a307ed471051c374196733d71190a88b09ac2119915c0444edb7d3cdcc07f9111b4d8c552208d38ddc4ee1f2cf60adda1
7
+ data.tar.gz: 816ae198c494d1acc655f17bca68f29663d1df1e346c404af65ac90783b02d94473a067055beb0be7ce57c7e00300eddc3962ffc7d2fb2bb5f809c612598b7c4
data/FEATURES.md CHANGED
@@ -10,4 +10,8 @@
10
10
  - the installation process involves compilation. We attempt to have compilation happen while its not blocking other operations, but also only one compilation at a time
11
11
  - we have a book keeping object that governs the worker pools and that's present during each step (fetch, extract, compile, install) and recieves the tasks for each phase from the workers.
12
12
  - i suspect that we need to fork of a worker for compilation which we then have to communicate with via some rpc format. simple "-> CALL method, <- RESULT:\n...." type line protocol through stdin/out might work well enough there.
13
+ - cache validity is defined by cached tree + spec marshal + versioned manifest, scoped by ABI (with `gem.build_complete` for native extensions)
14
+ - cached manifests are versioned JSON with deterministic ordering and a file list for fast materialization
15
+ - git cache slugs are deterministic hashes of the normalized source URI with collision detection + telemetry
16
+ - legacy cached entries without a manifest remain read-compatible but emit telemetry for deprecation
13
17
 
data/README.md CHANGED
@@ -1,265 +1,209 @@
1
1
  # Scint
2
2
 
3
- Scint is an experimental Bundler/RubyGems replacement focused on high-throughput installs with a global cache and a fast local materialization step.
3
+ ```
4
+ tobi ~/t/fizzy ❯❯❯ scint install
5
+ 💎 Scintellating Gemfile into ./.bundle (scint 0.7.1, ruby 4.0.1)
6
+
7
+ Fetching index https://rubygems.org
4
8
 
5
- It is written in pure Ruby with no dependencies; Ruby is plenty fast for this job.
9
+ 162 gems installed total (162 cached). (1.42s, 15 workers used)
10
+ ```
6
11
 
7
- Scint is designed for full backwards compatibility with Bundler workflows:
12
+ Scint is an experimental Bundler replacement. Pure Ruby, no dependencies.
8
13
 
9
- 1. It reads `Gemfile` and `Gemfile.lock`.
10
- 2. It writes standard `Gemfile.lock`.
11
- 3. It interoperates with Bundler runtime layout. For example, `BUNDLE_PATH=".bundle" bundle exec ...` is expected to work.
12
- 4. The intent is identical behavior with better install and execution throughput.
14
+ It reads your `Gemfile` and `Gemfile.lock`, writes standard `Gemfile.lock`, and materializes into `.bundle/` so `bundle exec` keeps working. Same behavior, much faster installs.
13
15
 
14
- The core idea is:
16
+ The core idea:
15
17
 
16
18
  1. Prepare artifacts once in a global cache (`~/.cache/scint`).
17
- 2. Materialize project-local runtime state into `.bundle/` as efficiently as possible (hardlinks where possible).
18
- 3. Execute work in explicit concurrent phases coordinated by a scheduler session.
19
-
20
- ## Why Scint
19
+ 2. Materialize into `.bundle/` via clonefile/hardlink/copy.
20
+ 3. Warm installs are effectively instant because no fetch, extract, or compile happens when the cache is populated.
21
21
 
22
- `scint` comes from *scintillation*: short, high-energy flashes rather than continuous glow.
22
+ ## Why "Scint"
23
23
 
24
- That maps directly to the runtime model:
24
+ From *scintillation*: short, high-energy flashes.
25
25
 
26
26
  1. Event-driven scheduling.
27
27
  2. Burst parallelism where safe.
28
28
  3. Tight phase boundaries with clear handoffs.
29
29
 
30
- ## CLI
31
-
32
- ```bash
33
- scint install
34
- scint exec <command>
35
- scint cache list
36
- scint cache clear
37
- scint cache dir
38
- ```
39
-
40
- Benchmark helpers:
41
-
42
- ```bash
43
- bin/scint-vs-bundler [--force] [--test-root /tmp/scint-tests] /path/to/project
44
- bin/scint-bench-matrix [--force] --root /path/to/projects
45
- ```
46
-
47
- `bin/scint-bench-matrix` is a generic runner for a root directory where each
48
- immediate subdirectory is a Ruby project under git with both `Gemfile` and
49
- `Gemfile.lock`. It runs bundler cold/warm and scint cold/warm via
50
- `bin/scint-vs-bundler` and writes:
51
-
52
- 1. `logs/bench-<timestamp>/summary.tsv`
53
- 2. `logs/bench-<timestamp>/table.md`
30
+ ## Install Pipeline
54
31
 
55
- Optional project smoke test convention:
32
+ Every gem flows through a deterministic cache pipeline. Resolution decides *what* to install; the pipeline defines *how*.
56
33
 
57
- 1. If `<root>/<project>-test.sh` exists, matrix runs it after the benchmark.
58
- 2. Execution is:
59
- `cd <root>/<project> && scint exec ../<project>-test.sh`
60
- 3. The script runs against the warm scint install and is included in
61
- `summary.tsv`/`table.md` status.
62
-
63
- Performance and IO diagnostics:
64
-
65
- ```bash
66
- # Ruby sampling profile (JSON)
67
- SCINT_PROFILE=/tmp/scint-profile.json SCINT_PROFILE_HZ=400 scint install --force
68
-
69
- # Ruby-level IO trace (JSONL)
70
- SCINT_IO_TRACE=/tmp/scint-io.jsonl scint install --force
71
-
72
- # Summarize high-volume IO operations for quick LLM review
73
- scint-io-summary /tmp/scint-io.jsonl
74
-
75
- # Syscall-level trace (Linux strace / macOS dtruss)
76
- scint-syscall-trace /tmp/scint-sys.log -- scint install --force
77
- ```
78
-
79
- Compatibility example:
80
-
81
- ```bash
82
- BUNDLE_PATH=".bundle" bundle exec ruby -v
34
+ ```mermaid
35
+ flowchart TD
36
+ A["Fetch + Assemble"]
37
+ B[Compile]
38
+ C[Promote]
39
+ D[Materialize]
40
+
41
+ A -->|"gems: download .gem, unpack\ngit: clone, checkout, export"| B
42
+ B -->|"native extensions built\ninside assembling tree"| C
43
+ C -->|"atomic move to cached/\nwrite .spec.marshal + manifest"| D
44
+ D -->|"clonefile / hardlink / copy\nfrom cached/ to .bundle/"| E[Done]
83
45
  ```
84
46
 
85
- Defaults:
86
-
87
- 1. Local install/runtime directory: `.bundle/`
88
- 2. Global cache root: `~/.cache/scint` (or `XDG_CACHE_HOME`)
89
-
90
- ## Install Architecture (Target)
91
-
92
- Scint should have one clear cache lifecycle:
47
+ ### Phase details
93
48
 
94
- 1. `inbound`
95
- 2. `assembling`
96
- 3. `cached`
97
- 4. `materialize`
49
+ 1. **Fetch + Assemble** into `inbound/` and `assembling/`
50
+ - Download gem payloads to `inbound/gems/`.
51
+ - Clone/fetch git repos into `inbound/gits/<sha256-slug>/`.
52
+ - Unpack `.gem` files into `assembling/<abi>/<full_name>/`.
53
+ - For git sources: checkout, submodules, then export the tree into `assembling/<abi>/<full_name>/`.
54
+ 2. **Compile** in `assembling/`
55
+ - Native extension builds happen inside the assembling directory so outputs are part of the final tree.
56
+ 3. **Promote** atomically to `cached/`
57
+ - Move `assembling/<abi>/<full_name>/` to `cached/<abi>/<full_name>/`.
58
+ - Write `cached/<abi>/<full_name>.spec.marshal` and `.manifest`.
59
+ - Failed builds never reach `cached/`.
60
+ 4. **Materialize** to `.bundle/`
61
+ - Clonefile/reflink/hardlink/copy from `cached/<abi>/`.
62
+ - No rebuild if the cached artifact is valid.
98
63
 
99
- Resolution/planning still decides *what* to install; this pipeline defines *how* each artifact becomes globally reusable.
64
+ One truth source for warm installs: `cached/<abi>`.
100
65
 
101
- ### Phase Contract
66
+ ## Scheduler
102
67
 
103
- 1. Fetch into `inbound`
104
- - Gem payloads go to `inbound/gems/`.
105
- - Git repositories go to `inbound/gits/` using deterministic names (for example `https_github_com__tobi__try`).
106
- 2. Assemble into `assembling`
107
- - For `.gem` sources: unpack into `assembling/<abi>/<full_name>/`.
108
- - For git sources: fetch/checkout/submodules in `inbound/gits`, then export/copy the selected tree into `assembling/<abi>/<full_name>/`.
109
- 3. Compile in `assembling`
110
- - Native extension build happens inside the assembling directory so successful outputs are part of the final cached tree.
111
- 4. Promote atomically to `cached`
112
- - On success, move `assembling/<abi>/<full_name>/` to `cached/<abi>/<full_name>/`.
113
- - Write `cached/<abi>/<full_name>.spec.marshal`.
114
- - Write optional manifest metadata for fast materialization.
115
- 5. Materialize to project path (`.bundle` or `BUNDLE_PATH`)
116
- - Use clonefile/reflink/hardlink/copy fallback from `cached/<abi>/...`.
117
- - Do not rebuild if cached artifact is already complete.
118
-
119
- This gives one primary truth source for warm installs: `cached/<abi>`.
68
+ The `Scheduler` is the install session object. It owns the job graph, worker pool, and phase coordination.
120
69
 
121
70
  ```mermaid
122
- flowchart LR
123
- A[Resolve + Plan] --> B[Fetch to inbound]
124
- B --> C[Assemble in assembling]
125
- C --> D[Compile in assembling]
126
- D --> E[Promote to cached]
127
- E --> F[Materialize to .bundle]
128
- F --> G[Write Runtime + Lockfile]
71
+ flowchart TD
72
+ subgraph "Early Parallel"
73
+ FI[fetch_index]
74
+ GC[git_clone]
75
+ end
76
+
77
+ FI --> R[resolve]
78
+ GC --> R
79
+
80
+ R --> P[plan]
81
+
82
+ subgraph "Install DAG"
83
+ DL[download] --> EX[extract]
84
+ EX --> LN[link]
85
+ LN --> BE[build_ext]
86
+ BE --> BS[binstub]
87
+ LN --> BS
88
+ end
89
+
90
+ P --> DL
91
+ P --> LN2["link (cached)"]
92
+ LN2 --> BS2[binstub]
129
93
  ```
130
94
 
131
- ## Scheduler as Session Object
132
-
133
- The `Scheduler` is more than a queue: it is the install *session object*.
134
- It owns global execution state and coordinates workers with phase-aware semantics.
135
-
136
- The scheduler tracks:
137
-
138
- 1. Job graph and dependencies
139
- 2. Priority classes by job type
140
- 3. Worker pool scaling
141
- 4. Job state transitions (`pending`, `running`, `completed`, `failed`)
142
- 5. Follow-up chaining (for phase handoff)
143
- 6. Fail-fast abort state and error collection
144
- 7. Progress/stats snapshots used by reporting
95
+ Job priorities (lower = dispatched first):
145
96
 
146
- Workers do not own global install strategy. They execute task payloads with context supplied by scheduler enqueuing and phase sequencing.
147
-
148
- ```mermaid
149
- sequenceDiagram
150
- participant U as User
151
- participant C as scint install
152
- participant S as Scheduler(Session)
153
- participant W as WorkerPool
154
- participant T as Task Worker
155
-
156
- U->>C: scint install
157
- C->>S: start(max_workers, fail_fast)
158
- C->>S: enqueue(fetch_index/git/...)
159
- S->>W: dispatch ready jobs
160
- W->>T: execute payload
161
- T-->>S: complete/fail + result
162
- S-->>C: wait_for phase completion
163
- C->>S: enqueue next phase (download/link/build_ext)
164
- S-->>C: stats + errors + aborted?
165
- C-->>U: summary + lockfile/runtime outputs
166
- ```
97
+ | Priority | Job Type | Concurrency |
98
+ |----------|----------|-------------|
99
+ | 0 | `fetch_index` | all workers |
100
+ | 1 | `git_clone` | all workers |
101
+ | 2 | `resolve` | 1 |
102
+ | 3 | `download` | bounded (default 8) |
103
+ | 4 | `extract` | IO-limited |
104
+ | 5 | `link` | IO-limited |
105
+ | 6 | `build_ext` | CPU-limited (slots x make -j) |
106
+ | 7 | `binstub` | 1 |
167
107
 
168
- ## Job Lifecycle
108
+ Workers start at 1, scale dynamically up to `cpu_count * 2` (max 50). Compile concurrency is tuned separately: a small number of slots each running `make -jN` to keep CPU saturated without thrashing.
169
109
 
170
- ```mermaid
171
- stateDiagram-v2
172
- [*] --> pending
173
- pending --> running: dependencies satisfied + worker slot available
174
- running --> completed: success
175
- running --> failed: exception / command failure
176
- completed --> [*]
177
- failed --> [*]
178
- ```
110
+ Fail-fast mode aborts scheduling after the first hard failure.
179
111
 
180
- ## Data Layout (Target)
112
+ ## Data Layout
181
113
 
182
114
  Global cache (`~/.cache/scint`):
183
115
 
184
- ```text
116
+ ```
185
117
  ~/.cache/scint/
186
118
  inbound/
187
- gems/
188
- <full_name>.gem
189
- gits/
190
- <deterministic_repo_slug>/
119
+ gems/<full_name>.gem
120
+ gits/<sha256-slug>/
191
121
  assembling/
192
- <ruby-abi>/
193
- <full_name>/
122
+ <ruby-abi>/<full_name>/
194
123
  cached/
195
124
  <ruby-abi>/
196
125
  <full_name>/
197
126
  <full_name>.spec.marshal
198
127
  <full_name>.manifest
199
128
  index/
129
+ <source-slug>/
200
130
  ```
201
131
 
202
- Example ABI key and gem directory:
203
-
204
- 1. `cached/ruby-3.4.5-arm64-darwin24/zlib-3.2.1/`
205
- 2. `cached/ruby-3.4.5-arm64-darwin24/zlib-3.2.1.spec.marshal`
132
+ ABI key example: `ruby-4.0.1-arm64-darwin25`
206
133
 
207
134
  Project-local runtime (`.bundle/`):
208
135
 
209
- 1. `ruby/<major.minor.0>/gems/` materialized gem trees
210
- 2. `ruby/<major.minor.0>/specifications/` gemspecs
211
- 3. `ruby/<major.minor.0>/bin/` gem binstubs
212
- 4. `bin/` project-level wrappers
213
- 5. `scint.lock.marshal` runtime lock for `scint exec`
136
+ ```
137
+ .bundle/
138
+ ruby/<major.minor.0>/
139
+ gems/ # materialized gem trees
140
+ specifications/ # gemspecs
141
+ bin/ # gem binstubs
142
+ bin/ # project-level wrappers
143
+ scint.lock.marshal
144
+ ```
145
+
146
+ Cache root precedence: `SCINT_CACHE` > `XDG_CACHE_HOME/scint` > `~/.cache/scint`.
214
147
 
215
- ## Warm Path Guarantees
148
+ ## Cache Validity
216
149
 
217
- Required behavior:
150
+ A cached artifact is valid when:
218
151
 
219
- 1. If `cached/<abi>/<full_name>/` exists and is valid, no fetch/extract/compile occurs for that gem.
220
- 2. Deleting only `.bundle/` should trigger only materialization work.
221
- 3. Materialization should be IO-bound and close to instantaneous on warm cache.
222
- 4. Incomplete assemblies must never be promoted; promotion is atomic.
152
+ 1. `cached/<abi>/<full_name>/` exists.
153
+ 2. `.spec.marshal` exists and loads.
154
+ 3. `.manifest` exists, parses, schema version is supported, and fields match.
155
+ 4. If the gem has native extensions, the build-complete marker exists.
223
156
 
224
- ## Concurrency Model
157
+ Invalid entries are rebuilt through the full pipeline. Legacy entries without manifests are read-compatible but emit telemetry for deprecation tracking.
225
158
 
226
- Scint parallelizes all non-conflicting work aggressively:
159
+ ## Warm Path Guarantees
160
+
161
+ 1. If `cached/<abi>/<full_name>/` is valid, no fetch/extract/compile occurs.
162
+ 2. Deleting `.bundle/` triggers only materialization.
163
+ 3. Materialization is IO-bound and close to instantaneous on warm cache.
164
+ 4. Incomplete assemblies are never promoted.
165
+
166
+ ## CLI
227
167
 
228
- 1. Index fetch and git clone start early.
229
- 2. Downloads can chain follow-up link tasks.
230
- 3. Planner ordering prioritizes large downloads first to keep pipeline saturated.
231
- 4. Build-ext runs after link readiness to make dependencies visible.
232
- 5. Fail-fast mode aborts scheduling of new work after the first hard failure.
168
+ ```bash
169
+ scint install # install from Gemfile.lock
170
+ scint add <gem> # add to Gemfile and install
171
+ scint remove <gem> # remove from Gemfile and install
172
+ scint exec <cmd> # run command in bundle context
173
+ scint cache list # list cached gems
174
+ scint cache clear # clear global cache
175
+ scint cache dir # print cache root
176
+ ```
233
177
 
234
- ## Error Model
178
+ Options: `--jobs N`, `--path P`, `--verbose`, `--force`.
235
179
 
236
- Scint is designed to be explicit on failure:
180
+ `scint exec` sets `GEM_HOME`/`GEM_PATH`, injects load paths from `scint.lock.marshal`, and execs the command. Bundler compatibility: `BUNDLE_PATH=".bundle" bundle exec` works against the same layout.
237
181
 
238
- 1. Install exits non-zero on failures.
239
- 2. Native build failures include full captured command output.
240
- 3. Final summary reports installed/failed/skipped counts.
241
- 4. `.gitignore` warning is emitted when `.bundle/` is not ignored.
182
+ ## Diagnostics
242
183
 
243
- ## `scint exec` Runtime
184
+ ```bash
185
+ # Ruby sampling profile
186
+ SCINT_PROFILE=/tmp/profile.json scint install --force
244
187
 
245
- `scint exec` sets runtime env and load paths from `scint.lock.marshal`, then `exec`s the target command.
188
+ # Ruby IO trace
189
+ SCINT_IO_TRACE=/tmp/io.jsonl scint install --force
246
190
 
247
- Key behaviors:
191
+ # Summarize IO trace
192
+ scint-io-summary /tmp/io.jsonl
248
193
 
249
- 1. Injects runtime load paths and bundler compatibility shim.
250
- 2. Sets `GEM_HOME`/`GEM_PATH` to the local `.bundle` runtime.
251
- 3. Prefers local `.bundle/bin` executables.
252
- 4. Rebuilds runtime lock from `Gemfile.lock` + installed gems when possible if missing.
194
+ # Syscall trace (strace/dtruss)
195
+ scint-syscall-trace /tmp/sys.log -- scint install --force
196
+ ```
253
197
 
254
- ## Aspirational Direction
198
+ ## Benchmarking
255
199
 
256
- The current architecture already separates phases and scheduling concerns. Planned direction is to make this even more explicit:
200
+ ```bash
201
+ bin/scint-vs-bundler [--force] /path/to/project
202
+ bin/scint-bench-matrix [--force] --root /path/to/projects
203
+ ```
257
204
 
258
- 1. First-class session object API around scheduler state and phase transitions.
259
- 2. Isolated compile worker process with a simple line-protocol RPC (`CALL` / `RESULT`) for stronger fault isolation.
260
- 3. More deterministic bulk operations for extraction/linking.
261
- 4. Better per-phase telemetry for latency and saturation analysis.
205
+ `scint-bench-matrix` runs cold/warm benchmarks for every project subdirectory and writes `logs/bench-<timestamp>/summary.tsv` and `table.md`. If `<root>/<project>-test.sh` exists, it runs as a smoke test after the benchmark.
262
206
 
263
207
  ## Status
264
208
 
265
- Scint is experimental and optimized for architecture iteration speed. Behavior and internals may change quickly.
209
+ Experimental. Optimized for architecture iteration speed.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.0
@@ -23,10 +23,28 @@ module Scint
23
23
  File.join(@root, "inbound")
24
24
  end
25
25
 
26
+ def inbound_gems_dir
27
+ File.join(inbound_dir, "gems")
28
+ end
29
+
30
+ def inbound_gits_dir
31
+ File.join(inbound_dir, "gits")
32
+ end
33
+
34
+ def assembling_dir
35
+ File.join(@root, "assembling")
36
+ end
37
+
38
+ def cached_dir
39
+ File.join(@root, "cached")
40
+ end
41
+
42
+ # Legacy extracted cache (read-compat only).
26
43
  def extracted_dir
27
44
  File.join(@root, "extracted")
28
45
  end
29
46
 
47
+ # Legacy extension cache (read-compat only).
30
48
  def ext_dir
31
49
  File.join(@root, "ext")
32
50
  end
@@ -36,7 +54,7 @@ module Scint
36
54
  end
37
55
 
38
56
  def git_dir
39
- File.join(inbound_dir, "git")
57
+ inbound_gits_dir
40
58
  end
41
59
 
42
60
  # Isolated gem home used while compiling native extensions during install.
@@ -52,17 +70,40 @@ module Scint
52
70
  # -- Per-spec paths ------------------------------------------------------
53
71
 
54
72
  def inbound_path(spec)
55
- File.join(inbound_dir, "#{full_name(spec)}.gem")
73
+ File.join(inbound_gems_dir, "#{full_name(spec)}.gem")
74
+ end
75
+
76
+ def assembling_path(spec, abi_key = Platform.abi_key)
77
+ File.join(assembling_dir, abi_key, full_name(spec))
78
+ end
79
+
80
+ def cached_abi_dir(abi_key = Platform.abi_key)
81
+ File.join(cached_dir, abi_key)
82
+ end
83
+
84
+ def cached_path(spec, abi_key = Platform.abi_key)
85
+ File.join(cached_dir, abi_key, full_name(spec))
56
86
  end
57
87
 
88
+ def cached_spec_path(spec, abi_key = Platform.abi_key)
89
+ File.join(cached_dir, abi_key, "#{full_name(spec)}.spec.marshal")
90
+ end
91
+
92
+ def cached_manifest_path(spec, abi_key = Platform.abi_key)
93
+ File.join(cached_dir, abi_key, "#{full_name(spec)}.manifest")
94
+ end
95
+
96
+ # Legacy extracted cache (read-compat only).
58
97
  def extracted_path(spec)
59
98
  File.join(extracted_dir, full_name(spec))
60
99
  end
61
100
 
101
+ # Legacy extracted gemspec cache (read-compat only).
62
102
  def spec_cache_path(spec)
63
103
  File.join(extracted_dir, "#{full_name(spec)}.spec.marshal")
64
104
  end
65
105
 
106
+ # Legacy extension cache (read-compat only).
66
107
  def ext_path(spec, abi_key = Platform.abi_key)
67
108
  File.join(ext_dir, abi_key, full_name(spec))
68
109
  end
@@ -80,13 +121,13 @@ module Scint
80
121
 
81
122
  def git_path(uri)
82
123
  slug = git_slug(uri)
83
- File.join(git_dir, "repos", slug)
124
+ File.join(git_dir, slug)
84
125
  end
85
126
 
86
127
  def git_checkout_path(uri, revision)
87
128
  slug = git_slug(uri)
88
129
  rev = revision.to_s.gsub(/[^0-9A-Za-z._-]/, "_")
89
- File.join(git_dir, "checkouts", slug, rev)
130
+ File.join(git_dir, slug, "checkouts", rev)
90
131
  end
91
132
 
92
133
  # -- Helpers -------------------------------------------------------------
@@ -109,10 +150,19 @@ module Scint
109
150
  private
110
151
 
111
152
  def default_root
153
+ return Scint.cache_root if Scint.respond_to?(:cache_root)
154
+
155
+ explicit = ENV["SCINT_CACHE"]
156
+ return File.expand_path(explicit) unless explicit.nil? || explicit.empty?
157
+
112
158
  base = ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
113
159
  File.join(base, "scint")
114
160
  end
115
161
 
162
+ # Slug rules are defined in README.md (Cache Validity + Manifest Specification).
163
+ # - Index slugs prefer host/path when available, otherwise fall back to a hash.
164
+ # - Hash slugs are deterministic but must be paired with manifest checks for
165
+ # collision detection.
116
166
  def slugify_uri(str)
117
167
  uri = URI.parse(str) rescue nil
118
168
  if uri && uri.host
@@ -125,8 +175,19 @@ module Scint
125
175
  end
126
176
  end
127
177
 
178
+ # Git slugs are SHA256 of the normalized URI string (uri.to_s), truncated
179
+ # to 16 hex chars. Callers must validate `source.uri` in the manifest to
180
+ # detect collisions and fall back to a longer hash if needed.
128
181
  def git_slug(uri)
129
- Digest::SHA256.hexdigest(uri.to_s)[0, 16]
182
+ normalized = normalize_uri(uri)
183
+ Digest::SHA256.hexdigest(normalized)[0, 16]
184
+ end
185
+
186
+ def normalize_uri(uri)
187
+ return uri.to_s if uri.is_a?(URI)
188
+ URI.parse(uri.to_s).to_s
189
+ rescue URI::InvalidURIError
190
+ uri.to_s
130
191
  end
131
192
  end
132
193
  end