@kaisers-io/refs 0.11.0 → 0.12.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.
- package/CHANGELOG.md +222 -1
- package/dist/refs.mjs +41 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,226 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-09-08
|
|
11
|
+
|
|
12
|
+
### Upgrading
|
|
13
|
+
|
|
14
|
+
Run `refs sync` after updating. Two things are worth expecting on that first run.
|
|
15
|
+
|
|
16
|
+
**Packages may be reported as `missing` that are still on disk.** Negated workspace patterns are
|
|
17
|
+
applied now, so a package a repository excludes (`!examples/vue/2*`) is no longer a workspace
|
|
18
|
+
member — and an entry registered for one before this release no longer verifies. Nothing is removed
|
|
19
|
+
automatically; the finding names the entry and leaves the decision alone.
|
|
20
|
+
|
|
21
|
+
**Packages that arrived upstream since the last sync are reported.** Only those: a package the
|
|
22
|
+
configuration never had and that did not arrive in the fetched range stays unmentioned, however
|
|
23
|
+
long it has been there.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`refs sync` now reports a package that arrived upstream, and `refs edit --create` registers
|
|
28
|
+
it.** The drift probe checked the packages the configuration already had, so a package added
|
|
29
|
+
upstream after `refs add` stayed invisible — and there was no command to register one either:
|
|
30
|
+
`refs add` refuses an already-tracked ref, and every `refs edit` mode needs an entry to edit. The
|
|
31
|
+
only instruction anyone could give was "hand-edit `config.toml`".
|
|
32
|
+
|
|
33
|
+
`refs sync` answers "did upstream gain a package?" from the range it just fetched, not by
|
|
34
|
+
comparing a scan against the configuration. That distinction is the whole design: a scan cannot
|
|
35
|
+
tell a package that just arrived from one the ref's owner deliberately never tracked, because
|
|
36
|
+
there is no inventory of what was there before — the fetch range is that inventory. A ref whose
|
|
37
|
+
owner tracks 3 packages out of 140 hears about the other 137 exactly never. `refs doctor` lists
|
|
38
|
+
every unregistered member instead, because it was asked to.
|
|
39
|
+
|
|
40
|
+
The question it asks of that range is about package NAMES, not manifest paths. A package
|
|
41
|
+
renamed in place modifies its manifest rather than adding one, and a package merely moved to
|
|
42
|
+
another directory adds one without being new — so a path-based reading is wrong in both
|
|
43
|
+
directions. Only the manifests the range actually changed have to be read out of history: an
|
|
44
|
+
untouched manifest is byte-identical at both ends, so the name it carries now is the name it
|
|
45
|
+
carried before.
|
|
46
|
+
|
|
47
|
+
The repair is a command now rather than a config fragment, with the ref key filled in so it
|
|
48
|
+
runs as printed:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
refs edit 'github.com/acme/alpha' --package '@acme/new' --create --path 'packages/new' \
|
|
52
|
+
--description "<what it is>"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It is a distinct mode, not an upsert — an ordinary field edit naming an unregistered package
|
|
56
|
+
still fails with `not_found`, so a typo in `--package` can never become a new entry. The
|
|
57
|
+
finding carries `name` and `path`, both verified against the checkout and both shell-quoted
|
|
58
|
+
(being verified makes a value true, not shell-safe: `zPackagePath` permits `$()` and a manifest
|
|
59
|
+
`name` is checked only for being non-empty), and deliberately no description: a manifest description is untrusted third-party content, and copying it moves it
|
|
60
|
+
into a file refs later reads as its own configuration. The skill instructs agents to propose the
|
|
61
|
+
registration and wait for the user to agree, rather than run it on their own initiative.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **Workspace patterns are matched by `minimatch`, the matcher npm itself uses.** Hand-written
|
|
66
|
+
matching disagreed with the real resolvers in five distinct ways, each found only after the last
|
|
67
|
+
was fixed: extglob (`@(a|b)`) read as a literal directory name, trailing slashes treated
|
|
68
|
+
symmetrically where `minimatch` is asymmetric, repeated separators silently matching nothing, and
|
|
69
|
+
two more. Delegating removes that class of defect rather than the current instance of it.
|
|
70
|
+
|
|
71
|
+
Measured before choosing: `picomatch` disagrees with `minimatch` on ten of 154 comparisons over
|
|
72
|
+
the shapes this scanner supports, exactly on trailing slashes and repeated separators — so it is
|
|
73
|
+
not a drop-in. pnpm matches through `picomatch` but normalizes first, and was measured to agree
|
|
74
|
+
with `minimatch` on every one of those shapes, so one matcher covers both ecosystems.
|
|
75
|
+
|
|
76
|
+
Walking stays here: containment guards, the diagnostics that say why a scan came up short, and
|
|
77
|
+
the deliberate one-level depth policy are unchanged. `minimatch` answers only whether a path
|
|
78
|
+
matches a pattern. Nothing new is installed by `refs` users — the CLI publishes a bundle with no
|
|
79
|
+
dependencies — and that bundle grows by 24 KB.
|
|
80
|
+
|
|
81
|
+
One behaviour improves as a consequence: a negation in a shape this scanner cannot WALK
|
|
82
|
+
(`!packages/{a,b}`) is now applied, because applying an exclusion needs matching and never
|
|
83
|
+
walking. It used to be reported as unsupported and silently ignored.
|
|
84
|
+
|
|
85
|
+
- **Negated workspace patterns are applied instead of ignored.** `!packages/fixtures` was dropped
|
|
86
|
+
as an unsupported shape (a v1 simplification), so `refs add` registered packages the repository
|
|
87
|
+
had explicitly excluded, and every finding about a repository declaring one was silenced —
|
|
88
|
+
TanStack Query declares two, and all hundred of its packages came back `unverifiable`. Negations
|
|
89
|
+
are now expanded exactly like inclusive patterns and subtracted from the result, which makes the
|
|
90
|
+
scan an accurate statement of membership rather than an approximation of one.
|
|
91
|
+
|
|
92
|
+
A wildcard inside the last segment (`examples/vue/2*`) is supported too, since that is the shape
|
|
93
|
+
real repositories exclude by, and a negation nobody can expand costs every finding about the
|
|
94
|
+
repository rather than just the paths it names. Glob syntax that is still unimplemented —
|
|
95
|
+
`{a,b}`, `?`, `[…]` — now reports `unsupported_pattern` rather than reading as a literal
|
|
96
|
+
directory name and silently matching nothing.
|
|
97
|
+
|
|
98
|
+
- **A monorepo can now be resolved by the name in its own root manifest.** Workspace detection
|
|
99
|
+
expands the globs a repository declares, and a workspace root is not one of its own targets — so a
|
|
100
|
+
root that names itself was registered nowhere, and `refs resolve @acme/toolkit` came back empty
|
|
101
|
+
for a repository that was tracked all along. `refs add` now registers a named root at `path: "."`
|
|
102
|
+
alongside the workspace members.
|
|
103
|
+
|
|
104
|
+
Both pnpm and Yarn address a workspace root by that name (`pnpm --filter <root-name>`,
|
|
105
|
+
`yarn workspace <root-name>`); npm and Turborepo use a positional handle instead. Of eighteen
|
|
106
|
+
well-known monorepos surveyed, eight carry a name someone would plausibly use for the repository
|
|
107
|
+
and ten carry a throwaway like `root` or `monorepo-root` — which is what settles it: registering
|
|
108
|
+
the name costs nothing where it is a throwaway, since nobody resolves `"root"`, and answers the
|
|
109
|
+
question where it is not.
|
|
110
|
+
|
|
111
|
+
Two things this deliberately does not do. A repository that declares no workspaces is untouched:
|
|
112
|
+
`refs add`'s npm fallback owns that shape, and probing the root there would displace a locator it
|
|
113
|
+
did not choose — as it also would where a workspace declaration selects nothing, so the package
|
|
114
|
+
named in an `npm:<pkg>` source survives there too. And where a workspace member already claims the
|
|
115
|
+
root's name — `@remix-run/react-router` is a real example, in a repository that also publishes
|
|
116
|
+
`react-router` — the member wins and the root is simply not registered, which costs that
|
|
117
|
+
repository nothing it had before. That rule lives in detection itself rather than in `refs add`,
|
|
118
|
+
so relocation agrees with registration: a member that moves is still found uniquely, instead of
|
|
119
|
+
becoming ambiguous against a same-named root and leaving `resolve` with no path for a package
|
|
120
|
+
that is plainly there. And a root is never reported as a package's new location: its name is an
|
|
121
|
+
alias for the repository, so a member that upstream deletes is reported as gone rather than as
|
|
122
|
+
having moved to the repository root — which would have sent a caller to the wrong directory and
|
|
123
|
+
described a move that never happened.
|
|
124
|
+
|
|
125
|
+
Refs tracked before this change keep the package map they were given, and no command adds one
|
|
126
|
+
entry to an existing ref — `refs add` refuses a tracked ref, `refs edit --package` needs an entry
|
|
127
|
+
to edit. So `refs sync` and `refs doctor`'s `config-drift` check now report a root the
|
|
128
|
+
configuration does not register, with the entry to add — including the path registration would
|
|
129
|
+
actually use, which is the member's rather than the root's where a workspace member declares the
|
|
130
|
+
same name. That costs one manifest read per ref, and a workspace scan only where there is
|
|
131
|
+
something to report; it is asked only of refs that already register packages, since a plain
|
|
132
|
+
reference repository registers none on purpose and is left alone.
|
|
133
|
+
|
|
134
|
+
The root package takes the ref's own description when its manifest carries none, which is the
|
|
135
|
+
ordinary case for a private workspace root. That is not the per-package fallback `refs add`
|
|
136
|
+
otherwise refuses: the root is not a package beside the repository, it is that repository.
|
|
137
|
+
|
|
138
|
+
- **A failed lookup no longer reads as an absent repository.** `refs resolve` exits `4` when a query
|
|
139
|
+
matches nothing, and the message ended "run refs list, or add it: `refs add <url>`". That second
|
|
140
|
+
half is a guess: a query can miss every route while the repository is tracked perfectly well under
|
|
141
|
+
another identifier — a monorepo root whose own package name was never registered, for instance.
|
|
142
|
+
An agent read the suggestion as confirmation and told someone a repository they had tracked was
|
|
143
|
+
not tracked, then stopped.
|
|
144
|
+
|
|
145
|
+
The message now states what was searched and points at evidence rather than prescribing a fix, and
|
|
146
|
+
`--json` carries a `reason` on `resolve`'s routing misses: `unmatched_query` (nothing matched, by
|
|
147
|
+
any route), `package_not_registered` (the ref is tracked and registers no such package), or
|
|
148
|
+
`ref_not_registered` (a canonical git url named an absent ref — the one case where adding it is
|
|
149
|
+
the right answer, since only a canonical url establishes which ref was meant). There is deliberately no reason meaning "this repository does not exist", because nothing
|
|
150
|
+
refs can observe establishes that; and `reason` is absent on every other `not_found`, where its
|
|
151
|
+
absence means no narrowing is available rather than being a fourth value.
|
|
152
|
+
|
|
153
|
+
The skill's instruction changed with it. It used to say exit `4` means the ref is not tracked; it
|
|
154
|
+
now says exit `4` means the query matched no route, and requires a second lookup before any
|
|
155
|
+
conclusion. `refs resolve --ref <ref>`'s own miss also stopped suggesting a bare `refs show`,
|
|
156
|
+
which reports a package count and no names — it now suggests `--packages`, which actually shows
|
|
157
|
+
the map the reader was sent to inspect.
|
|
158
|
+
|
|
159
|
+
- **A stale-lock reclaim could delete a lock another process was using.** refs reclaims a lock left
|
|
160
|
+
behind by a crashed process. The check that decided a lock was abandoned and the removal that
|
|
161
|
+
acted on it were two separate steps, and in the gap between them the lock could legitimately
|
|
162
|
+
become somebody else's: the original holder releases, a waiting process takes the same path, and
|
|
163
|
+
the reclaim then deletes a lock that is actively in use. Both processes go on to `reset --hard`
|
|
164
|
+
the same checkout.
|
|
165
|
+
|
|
166
|
+
Three changes close it for processes running this version:
|
|
167
|
+
|
|
168
|
+
- **Only a process the operating system reports as gone is reclaimed from automatically.** A lock
|
|
169
|
+
whose lease has run out but whose process still answers is now reported rather than taken — a
|
|
170
|
+
live process can release at any instant, and that release is what opened the gap. Same for a
|
|
171
|
+
lock whose metadata never finished being written, or carries no usable identity.
|
|
172
|
+
- **The acquisition is re-identified after the death check.** Proving the recorded process gone is
|
|
173
|
+
not enough on its own: the metadata is read first and the process probed after, so the path can
|
|
174
|
+
change hands in between and the probe then answers about the departed owner. Re-reading the
|
|
175
|
+
identity immediately before the removal is what ties the two together.
|
|
176
|
+
- **The marker that stops two reclaims colliding no longer expires.** It used to be taken over
|
|
177
|
+
after two seconds, so it only excluded a reclaim fast enough to finish inside that window; a
|
|
178
|
+
suspended one lost its marker mid-work and a second reclaim started on the same lock. Age is
|
|
179
|
+
not evidence of abandonment.
|
|
180
|
+
|
|
181
|
+
The protocol's own markers also moved into `locks/.claims/` and `locks/.tombstones/`, which no
|
|
182
|
+
lock name can reach — lock names must start with a letter or digit. That removes a collision
|
|
183
|
+
where a repository literally named `foo.steal-claim` produced the marker path of the lock for
|
|
184
|
+
`foo`, and with it the name-shape guessing `refs doctor` needed to tell the two apart.
|
|
185
|
+
|
|
186
|
+
**What this costs.** Three situations no longer recover on their own and need one explicit
|
|
187
|
+
command, which `refs doctor` prints along with the condition for running it safely — stop every
|
|
188
|
+
refs process on that home first, suspended ones included: a crash after the operating system has
|
|
189
|
+
reused the process id, a crash before the lock finished writing its metadata, and a crash while a
|
|
190
|
+
reclaim was starting.
|
|
191
|
+
|
|
192
|
+
The messages changed to match. `refs doctor`'s `locks` check separates a lock refs will reclaim
|
|
193
|
+
by itself from one it will not, and the failure message when a lock cannot be acquired no longer
|
|
194
|
+
says "already reclaimable — retry" for a lock nothing will ever reclaim. It also stopped
|
|
195
|
+
describing the window as the thing that frees a lock: waiting does not, and only a recorded
|
|
196
|
+
process the operating system reports as gone does.
|
|
197
|
+
|
|
198
|
+
**What it does not fix.** A refs process running an _older_ version follows none of this and
|
|
199
|
+
reclaims on its own terms. And no lock protocol can help when refs is hard-killed while its `git`
|
|
200
|
+
child survives: the successor's lock is honest about the lock, not about the directory.
|
|
201
|
+
|
|
202
|
+
- **Two unrelated refs could share one lock.** The per-ref lock name replaced `/` with `_`, and `_`
|
|
203
|
+
is legal inside a ref key — so `github.com/acme_tools/widget` and `github.com/acme/tools_widget`
|
|
204
|
+
both derived `ref.github.com_acme_tools_widget`. The two then serialized against each other:
|
|
205
|
+
`sync` fans out four refs at a time and the loser failed on a lock conflict after the timeout,
|
|
206
|
+
`resolve`'s verification could block on a sync of a ref it has nothing to do with, and `doctor`'s
|
|
207
|
+
`config-drift` check reported the wrong ref as busy.
|
|
208
|
+
|
|
209
|
+
The name is now injective. A key containing no `_` encodes exactly as before, so it keeps the
|
|
210
|
+
lock name refs has always written for it; a key containing one moves into an escaped form under
|
|
211
|
+
`ref._`, where `_` becomes `_u` and `/` becomes `_s`. The two forms cannot collide, because a ref
|
|
212
|
+
key always starts with `[a-z0-9]` and so a plain name never begins `ref._`.
|
|
213
|
+
|
|
214
|
+
A lock name is one directory entry, so a key long enough to overflow one now falls back to a
|
|
215
|
+
digest under `ref.__` rather than failing `mkdir` with `ENAMETOOLONG`. The budget reserves room
|
|
216
|
+
for the sibling entries the steal protocol derives from a lock name — a name that could be
|
|
217
|
+
created but not renamed to its tombstone would strand an abandoned lock that nothing could then
|
|
218
|
+
reclaim, leaving the ref blocked until someone deleted the directory by hand. Both were already
|
|
219
|
+
true before this change, for keys past roughly 200 characters; reaching it needs a self-hosted
|
|
220
|
+
url, since no forge allows a path that long.
|
|
221
|
+
|
|
222
|
+
**One caveat if you run refs concurrently across an upgrade.** The lock name changes for a ref
|
|
223
|
+
whose key contains `_`, and for one long enough to reach the digest form — roughly 200
|
|
224
|
+
characters, either way. For such a ref, a process from any earlier release (the old scheme dates
|
|
225
|
+
to 0.1.1) derives a different name from a new one, so for the length of that overlap the two
|
|
226
|
+
would not exclude each other. The window is a mid-upgrade concurrent run on the same refs home;
|
|
227
|
+
if that is a situation you can be in, let running operations finish before upgrading. Every other
|
|
228
|
+
ref keeps its name and is unaffected.
|
|
229
|
+
|
|
10
230
|
## [0.11.0] - 2026-08-31
|
|
11
231
|
|
|
12
232
|
### Changed
|
|
@@ -689,7 +909,8 @@ trusted-publishing pipeline end to end.
|
|
|
689
909
|
installed git hooks.
|
|
690
910
|
- Agent skill (`skills/refs/`) documenting the investigate/add/maintain workflows.
|
|
691
911
|
|
|
692
|
-
[Unreleased]: https://github.com/kaisers-io/refs/compare/v0.
|
|
912
|
+
[Unreleased]: https://github.com/kaisers-io/refs/compare/v0.12.0...HEAD
|
|
913
|
+
[0.12.0]: https://github.com/kaisers-io/refs/compare/v0.11.0...v0.12.0
|
|
693
914
|
[0.11.0]: https://github.com/kaisers-io/refs/compare/v0.10.0...v0.11.0
|
|
694
915
|
[0.10.0]: https://github.com/kaisers-io/refs/compare/v0.9.0...v0.10.0
|
|
695
916
|
[0.9.0]: https://github.com/kaisers-io/refs/compare/v0.8.3...v0.9.0
|