@kaisers-io/refs 0.12.0 → 0.13.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +117 -1
  2. package/dist/refs.mjs +60 -52
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -5,7 +5,123 @@ 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.0] - 2026-09-12
9
+
10
+ ### Upgrading
11
+
12
+ **`refs add <url> --description "…"` now refuses a source with workspace members.** It holds one
13
+ description, about the repository, and none for a package — and a package's description is no
14
+ longer read out of its manifest, so there is nothing for it to fall back on. Such a source exits
15
+ `3`, listing every package the proposal will need a description for and printing the two-phase
16
+ commands to run instead, with the source quoted into them. The shortcut still finalizes a
17
+ repository with no detected packages, and one whose only detected package is the root at `.`.
18
+
19
+ If a script calls the one-shot against a monorepo, it will start failing. The two-phase flow is the
20
+ replacement and always was the documented agent path.
21
+
22
+ **Package descriptions already in `config.toml` are left exactly as they are.** A description
23
+ imported from a manifest before this release stays; nothing distinguishes it from one someone
24
+ wrote, so nothing rewrites it. To review them: `refs show <ref> --packages --json`, and
25
+ `refs edit <ref> --package <name> description "…"` to replace one.
26
+
27
+ **A ref key containing an unpaired surrogate is now rejected.** No configuration written by refs
28
+ can contain one — TOML has no escape for it — so this only affects a hand-built key.
29
+
30
+ ### Fixed
31
+
32
+ - **`refs add` says when it could not fully inspect a repository's declared workspaces.** A
33
+ repository declaring `packages/**` — an ordinary spelling the pattern classifier cannot expand —
34
+ produced a proposal missing every member, with no indication why. That is not self-explanatory:
35
+ a thin `packages` record is also what an ordinary repository produces, and a detected root makes
36
+ even a memberless result look complete. The configuration was then built without those packages,
37
+ and the drift probe returns immediately for a ref that configures none, so nothing downstream
38
+ could recover it either. Both `--dry-run` and the `--description` one-shot now carry a warning
39
+ naming what stopped the scan — an unexpandable pattern, an unreadable manifest, a candidate that
40
+ could not be inspected — alongside any clone warning rather than instead of it.
41
+
42
+ - **A drift probe that could not finish looking says so, instead of reporting `ok`.** The pass that
43
+ finds packages a checkout declares and the configuration does not have stands down whenever
44
+ workspace detection may have missed something — an unreadable manifest, a pattern the classifier
45
+ cannot expand. That conservatism is right: a second declaration of the same name could be behind
46
+ the obstacle, and naming one path from a partial view would prescribe something registration
47
+ might not do. What was wrong is that it then said nothing at all, so `refs doctor` answered
48
+ `config-drift: ok — every configured package path resolves` while an unregistered package sat in
49
+ the checkout. One malformed `package.json` anywhere in a monorepo was enough, permanently.
50
+
51
+ Both discovery passes now report the obstacle. `structure` gains `discovery_incomplete`, naming
52
+ what stopped them (`packages/c: manifest_unreadable`), and its `status` is `unknown` rather than
53
+ `ok`. The findings about configured entries are unaffected — that half never needed the scan to
54
+ begin.
55
+
56
+ - **A ref whose key carries `@`, a space or any non-ASCII character can be locked, and therefore
57
+ synced.** `zRefKey` admits every character but `/`, `\`, `%` and `:`; the lock alphabet is far
58
+ narrower, and the derived lock name was passed through unchanged. Such a ref could be added and
59
+ read but never locked — so `refs sync`, `refs remove`, `refs resolve`'s package verification and
60
+ `refs doctor`'s drift check all failed for it, with a message naming the lock name rather than
61
+ the ref. A name the lock alphabet will not accept now falls back to the same bounded digest form
62
+ a too-long name already used. Keys the alphabet does admit keep their readable name unchanged.
63
+ Reachable through a self-hosted url or a hand-edited `config.toml`; no forge allows these
64
+ characters in a repository path.
65
+ - **A ref key carrying an unpaired surrogate is rejected.** Such a character is a valid JavaScript
66
+ string and has no UTF-8 encoding, so everything that writes the key out — the checkout directory,
67
+ the digest a lock name can fall back to — silently substituted U+FFFD for it. Keys ending
68
+ U+D800, U+D801 and U+FFFD therefore shared one directory and one lock name, and two unrelated
69
+ refs would have serialized against each other. No real config could carry one: TOML has no
70
+ escape for an unpaired surrogate.
71
+ - **A failed sync no longer persists an unbounded amount of remote output into `state.json`.**
72
+ `last_error` holds the failure's message, most often git's own output — which quotes what git was
73
+ working on, ref names among them, chosen by the tracked repository. No persistence-specific limit
74
+ applied: an upstream with 120 conflicting tags produced a 47,816-character error, all of it
75
+ written into a file refs reads back on every command. A newly recorded message now keeps 1500
76
+ characters from the start and 500 from the end, with an exact count of what was dropped in
77
+ between — so both the command that failed and git's own closing hint survive. The result of the
78
+ run that produced the failure still carries the message as it arrived.
79
+
80
+ ### Changed
81
+
82
+ - **Dependencies refreshed.** `zod` 4.5.2 → 4.6.2 — the only production dependency, and bundled
83
+ into the published CLI. The development toolchain moved too: `vitest` 4 → 5, `tsdown` 0.22 →
84
+ 0.23 (rolldown 1.2.8), `oxlint`, `oxfmt` and `vite`. No behaviour depends on any of it.
85
+ - **A package's description is never read out of a manifest.** Workspace detection carried each
86
+ package's `description` from its `package.json`, and `refs add <source> --description "…"`
87
+ persisted it into `config.toml` — text written by whoever owns the upstream repository, crossing
88
+ into a file refs reads back as its own configuration and replays on every `refs list` and
89
+ `refs show`. Detection now carries a package's name and path and nothing else, which is what
90
+ `refs sync`'s `unregistered` finding has done since 0.12.0. Every description in a config entry
91
+ is written by someone who read the source.
92
+
93
+ The one-shot `refs add <source> --description "…"` therefore no longer registers workspace
94
+ members: it has one description, about the repository, and none for a package. It still
95
+ finalizes a source with no detected packages, or one whose only detected package is the
96
+ repository root at `.` — and there the text now wins over whatever the root manifest said about
97
+ itself. For anything else it exits `3`, naming every package and printing the two-phase commands
98
+ to run instead, with the source it was given quoted into them, and listing every package the
99
+ proposal will need a description for — the repository root included, which is not the set the
100
+ refusal is about but is the set `refs add --proposal` requires.
101
+
102
+ A refusal registers no ref. The checkout it cloned stays, and is now recorded as a pending add —
103
+ so for the next 24 hours `doctor` reports it as one instead of offering to delete a checkout the
104
+ printed recovery is about to reuse, and the clone mode actually used survives into the finalize.
105
+ That mode cannot be recovered from the checkout afterwards: git records
106
+ `remote.origin.promisor` and `partialclonefilter` from the requested filter, whether or not the
107
+ server honoured it.
108
+
109
+ Existing entries are untouched. A description imported from a manifest before this release stays
110
+ exactly as it is — nothing distinguishes it from one written by hand, so nothing rewrites it. To
111
+ review them: `refs show <ref> --packages --json`, and `refs edit <ref> --package <name>
112
+ description "…"` to replace one.
113
+
114
+ ### Security
115
+
116
+ - `last_error` is documented as what it is — a failure message, usually git's own output, quoting
117
+ ref names the tracked repository chose — and `skills/refs/COMMANDS.md` now tells agents to read
118
+ it as untrusted evidence rather than as something refs vouches for.
119
+ - `SECURITY.md` claimed refs "never reads checkout content as configuration", which was not true of
120
+ manifest descriptions and is still not true of package names, paths, the default branch, or a
121
+ `tag_format` derived from real tags. The claim is now stated as what the code enforces: refs never
122
+ imports a description out of a manifest. The values that do still cross are named, along with the
123
+ fact that being structural does not make them safe to read — a package name is whatever the
124
+ manifest declares.
9
125
 
10
126
  ## [0.12.0] - 2026-09-08
11
127