@kaisers-io/refs 0.12.0 → 0.13.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +181 -1
  2. package/dist/refs.mjs +60 -52
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -5,7 +5,187 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.13.1] - 2026-09-12
9
+
10
+ ### Upgrading
11
+
12
+ **A repository declaring a workspace pattern like `crates/*/js` will suddenly report packages refs
13
+ could not see before.** The pattern was classified as unsupported, which also made the whole scan
14
+ unreliable and stood the unregistered-package pass down for that ref. `refs doctor` may therefore
15
+ name members on a ref that reported nothing for months. They are not new; they were invisible.
16
+ Register the ones worth routing to, and leave the rest.
17
+
18
+ ### Added
19
+
20
+ - **Every drift finding that has a repair now prints it as a command.** `unregistered` has printed
21
+ a runnable `refs edit --create` since the probe shipped; `missing` and `relocated` described the
22
+ repair in prose and left the reader to construct it. The asymmetry was not deliberate — one of
23
+ the two repairs did not exist as a command at all. `refs doctor` now ends those findings with
24
+ `refs edit --package='<name>' --remove '<ref>'` and
25
+ `refs edit --package='<name>' '<ref>' 'path' '<new>'`, quoted for a shell like every other
26
+ command refs prints. A new path the configuration could not hold is still reported, without a command
27
+ that would only fail validation.
28
+
29
+ Every printed `refs edit` now attaches its option values with `=` and, where a positional would
30
+ otherwise begin with `-`, ends the options with `--`. Quoting gets a value past the shell; refs'
31
+ own parser reads it next, and `refs edit '<ref>' path '-new/pkg'` — a path `zPackagePath` accepts
32
+ — arrived correctly quoted and exited `2` with `unknown option '-new/pkg'`. The terminator is
33
+ printed only where it is needed, so appending `--json` to an ordinary printed line still works.
34
+
35
+ - **`refs edit <ref> --package <name> --remove` unregisters a package.** The counterpart to
36
+ `--create`, and the repair the `missing` finding needed: a package that left a repository's
37
+ workspaces leaves an entry behind that `refs resolve` keeps answering with, pointing at a path no
38
+ checkout has. The only instruction refs could give was to hand-edit `config.toml`. Removal
39
+ touches configuration alone and never consults a checkout — it has to work while the checkout is
40
+ stale, absent, or still carrying the directory. Removing the last entry drops the `packages`
41
+ table rather than leaving an empty one, which `add` and the drift probe read differently.
42
+
43
+ ### Fixed
44
+
45
+ - **A workspace pattern whose wildcard is not in its last segment is expanded.** `crates/*/js` —
46
+ the spelling a Rust/JS monorepo uses — was reported as `unsupported_pattern` and every package
47
+ beneath it went undetected. That pattern costs one `readdir` and a literal probe per child, the
48
+ same shape `packages/*` has, so the budget that refused it was measuring the wrong thing. Two
49
+ wildcards still mean two levels and `packages/*/nested/*` is still refused. Measured on a
50
+ repository declaring `crates/*/js` and `turbopack/crates/*/js`: 41 packages detected instead of
51
+ 37, with no diagnostics; three other monorepos detect exactly what they did before.
52
+
53
+ - **`tag_format` is detected from every tag, not from the top twenty of a refname sort.** Detection
54
+ counted formats among the first 20 tags of `git tag --sort=-version:refname`. That sort is
55
+ version-aware over the whole refname, so in a monorepo that tags per package it groups by prefix:
56
+ those twenty are one package's tags, not a sample of the repository's. Measured against two real
57
+ repositories, `refs add` proposed `create-astro@{version}` for a repository with 839
58
+ `astro@{version}` tags, and `keystatic@{version}` — a package whose last tag is `0.0.36` and whose
59
+ directory no longer exists — for one where `@keystatic/core@{version}` is the live package with 183 of
60
+ them. Both now resolve correctly, and the two repositories whose detection was already right are
61
+ unchanged.
62
+
63
+ A tag list that could not be read whole now yields no candidate at all rather than one derived
64
+ from whatever survived. `git tag` output is capped at the subprocess stream limit, and the note
65
+ saying so was written to stderr and never read — which did not matter while only twenty lines
66
+ were needed, and does once the count is a claim about all of them.
67
+
68
+ What this establishes is historical plurality, not which package is primary or still alive: a
69
+ retired package with a thousand tags still outvotes its replacement's hundred. The two-phase flow
70
+ shows the candidate for exactly that reason.
71
+
72
+ ## [0.13.0] - 2026-09-12
73
+
74
+ ### Upgrading
75
+
76
+ **`refs add <url> --description "…"` now refuses a source with workspace members.** It holds one
77
+ description, about the repository, and none for a package — and a package's description is no
78
+ longer read out of its manifest, so there is nothing for it to fall back on. Such a source exits
79
+ `3`, listing every package the proposal will need a description for and printing the two-phase
80
+ commands to run instead, with the source quoted into them. The shortcut still finalizes a
81
+ repository with no detected packages, and one whose only detected package is the root at `.`.
82
+
83
+ If a script calls the one-shot against a monorepo, it will start failing. The two-phase flow is the
84
+ replacement and always was the documented agent path.
85
+
86
+ **Package descriptions already in `config.toml` are left exactly as they are.** A description
87
+ imported from a manifest before this release stays; nothing distinguishes it from one someone
88
+ wrote, so nothing rewrites it. To review them: `refs show <ref> --packages --json`, and
89
+ `refs edit <ref> --package <name> description "…"` to replace one.
90
+
91
+ **A ref key containing an unpaired surrogate is now rejected.** No configuration written by refs
92
+ can contain one — TOML has no escape for it — so this only affects a hand-built key.
93
+
94
+ ### Fixed
95
+
96
+ - **`refs add` says when it could not fully inspect a repository's declared workspaces.** A
97
+ repository declaring `packages/**` — an ordinary spelling the pattern classifier cannot expand —
98
+ produced a proposal missing every member, with no indication why. That is not self-explanatory:
99
+ a thin `packages` record is also what an ordinary repository produces, and a detected root makes
100
+ even a memberless result look complete. The configuration was then built without those packages,
101
+ and the drift probe returns immediately for a ref that configures none, so nothing downstream
102
+ could recover it either. Both `--dry-run` and the `--description` one-shot now carry a warning
103
+ naming what stopped the scan — an unexpandable pattern, an unreadable manifest, a candidate that
104
+ could not be inspected — alongside any clone warning rather than instead of it.
105
+
106
+ - **A drift probe that could not finish looking says so, instead of reporting `ok`.** The pass that
107
+ finds packages a checkout declares and the configuration does not have stands down whenever
108
+ workspace detection may have missed something — an unreadable manifest, a pattern the classifier
109
+ cannot expand. That conservatism is right: a second declaration of the same name could be behind
110
+ the obstacle, and naming one path from a partial view would prescribe something registration
111
+ might not do. What was wrong is that it then said nothing at all, so `refs doctor` answered
112
+ `config-drift: ok — every configured package path resolves` while an unregistered package sat in
113
+ the checkout. One malformed `package.json` anywhere in a monorepo was enough, permanently.
114
+
115
+ Both discovery passes now report the obstacle. `structure` gains `discovery_incomplete`, naming
116
+ what stopped them (`packages/c: manifest_unreadable`), and its `status` is `unknown` rather than
117
+ `ok`. The findings about configured entries are unaffected — that half never needed the scan to
118
+ begin.
119
+
120
+ - **A ref whose key carries `@`, a space or any non-ASCII character can be locked, and therefore
121
+ synced.** `zRefKey` admits every character but `/`, `\`, `%` and `:`; the lock alphabet is far
122
+ narrower, and the derived lock name was passed through unchanged. Such a ref could be added and
123
+ read but never locked — so `refs sync`, `refs remove`, `refs resolve`'s package verification and
124
+ `refs doctor`'s drift check all failed for it, with a message naming the lock name rather than
125
+ the ref. A name the lock alphabet will not accept now falls back to the same bounded digest form
126
+ a too-long name already used. Keys the alphabet does admit keep their readable name unchanged.
127
+ Reachable through a self-hosted url or a hand-edited `config.toml`; no forge allows these
128
+ characters in a repository path.
129
+ - **A ref key carrying an unpaired surrogate is rejected.** Such a character is a valid JavaScript
130
+ string and has no UTF-8 encoding, so everything that writes the key out — the checkout directory,
131
+ the digest a lock name can fall back to — silently substituted U+FFFD for it. Keys ending
132
+ U+D800, U+D801 and U+FFFD therefore shared one directory and one lock name, and two unrelated
133
+ refs would have serialized against each other. No real config could carry one: TOML has no
134
+ escape for an unpaired surrogate.
135
+ - **A failed sync no longer persists an unbounded amount of remote output into `state.json`.**
136
+ `last_error` holds the failure's message, most often git's own output — which quotes what git was
137
+ working on, ref names among them, chosen by the tracked repository. No persistence-specific limit
138
+ applied: an upstream with 120 conflicting tags produced a 47,816-character error, all of it
139
+ written into a file refs reads back on every command. A newly recorded message now keeps 1500
140
+ characters from the start and 500 from the end, with an exact count of what was dropped in
141
+ between — so both the command that failed and git's own closing hint survive. The result of the
142
+ run that produced the failure still carries the message as it arrived.
143
+
144
+ ### Changed
145
+
146
+ - **Dependencies refreshed.** `zod` 4.5.2 → 4.6.2 — the only production dependency, and bundled
147
+ into the published CLI. The development toolchain moved too: `vitest` 4 → 5, `tsdown` 0.22 →
148
+ 0.23 (rolldown 1.2.8), `oxlint`, `oxfmt` and `vite`. No behaviour depends on any of it.
149
+ - **A package's description is never read out of a manifest.** Workspace detection carried each
150
+ package's `description` from its `package.json`, and `refs add <source> --description "…"`
151
+ persisted it into `config.toml` — text written by whoever owns the upstream repository, crossing
152
+ into a file refs reads back as its own configuration and replays on every `refs list` and
153
+ `refs show`. Detection now carries a package's name and path and nothing else, which is what
154
+ `refs sync`'s `unregistered` finding has done since 0.12.0. Every description in a config entry
155
+ is written by someone who read the source.
156
+
157
+ The one-shot `refs add <source> --description "…"` therefore no longer registers workspace
158
+ members: it has one description, about the repository, and none for a package. It still
159
+ finalizes a source with no detected packages, or one whose only detected package is the
160
+ repository root at `.` — and there the text now wins over whatever the root manifest said about
161
+ itself. For anything else it exits `3`, naming every package and printing the two-phase commands
162
+ to run instead, with the source it was given quoted into them, and listing every package the
163
+ proposal will need a description for — the repository root included, which is not the set the
164
+ refusal is about but is the set `refs add --proposal` requires.
165
+
166
+ A refusal registers no ref. The checkout it cloned stays, and is now recorded as a pending add —
167
+ so for the next 24 hours `doctor` reports it as one instead of offering to delete a checkout the
168
+ printed recovery is about to reuse, and the clone mode actually used survives into the finalize.
169
+ That mode cannot be recovered from the checkout afterwards: git records
170
+ `remote.origin.promisor` and `partialclonefilter` from the requested filter, whether or not the
171
+ server honoured it.
172
+
173
+ Existing entries are untouched. A description imported from a manifest before this release stays
174
+ exactly as it is — nothing distinguishes it from one written by hand, so nothing rewrites it. To
175
+ review them: `refs show <ref> --packages --json`, and `refs edit <ref> --package <name>
176
+ description "…"` to replace one.
177
+
178
+ ### Security
179
+
180
+ - `last_error` is documented as what it is — a failure message, usually git's own output, quoting
181
+ ref names the tracked repository chose — and `skills/refs/COMMANDS.md` now tells agents to read
182
+ it as untrusted evidence rather than as something refs vouches for.
183
+ - `SECURITY.md` claimed refs "never reads checkout content as configuration", which was not true of
184
+ manifest descriptions and is still not true of package names, paths, the default branch, or a
185
+ `tag_format` derived from real tags. The claim is now stated as what the code enforces: refs never
186
+ imports a description out of a manifest. The values that do still cross are named, along with the
187
+ fact that being structural does not make them safe to read — a package name is whatever the
188
+ manifest declares.
9
189
 
10
190
  ## [0.12.0] - 2026-09-08
11
191