lockwire 0.1.28 → 0.1.30

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 (52) hide show
  1. package/README.md +1 -1
  2. package/dist/actions.d.ts +25 -6
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +175 -47
  5. package/dist/actions.js.map +1 -1
  6. package/dist/cli.js +46 -9
  7. package/dist/cli.js.map +1 -1
  8. package/dist/docindex.d.ts +1 -0
  9. package/dist/docindex.d.ts.map +1 -1
  10. package/dist/docindex.js +4 -2
  11. package/dist/docindex.js.map +1 -1
  12. package/dist/extract-v2.d.ts +34 -0
  13. package/dist/extract-v2.d.ts.map +1 -0
  14. package/dist/extract-v2.js +705 -0
  15. package/dist/extract-v2.js.map +1 -0
  16. package/dist/extract.d.ts +8 -2
  17. package/dist/extract.d.ts.map +1 -1
  18. package/dist/extract.js +13 -2
  19. package/dist/extract.js.map +1 -1
  20. package/dist/format.d.ts.map +1 -1
  21. package/dist/format.js +8 -2
  22. package/dist/format.js.map +1 -1
  23. package/dist/grammar.d.ts +1 -0
  24. package/dist/grammar.d.ts.map +1 -1
  25. package/dist/grammar.js +7 -1
  26. package/dist/grammar.js.map +1 -1
  27. package/dist/lockfile.d.ts +1 -1
  28. package/dist/lockfile.d.ts.map +1 -1
  29. package/dist/lockfile.js +10 -2
  30. package/dist/lockfile.js.map +1 -1
  31. package/dist/markers.d.ts +2 -0
  32. package/dist/markers.d.ts.map +1 -1
  33. package/dist/markers.js +85 -28
  34. package/dist/markers.js.map +1 -1
  35. package/dist/mcp-server.d.ts.map +1 -1
  36. package/dist/mcp-server.js +2 -4
  37. package/dist/mcp-server.js.map +1 -1
  38. package/dist/repo.d.ts +12 -2
  39. package/dist/repo.d.ts.map +1 -1
  40. package/dist/repo.js +23 -4
  41. package/dist/repo.js.map +1 -1
  42. package/dist/tasks.js +4 -4
  43. package/dist/tasks.js.map +1 -1
  44. package/dist/types.d.ts +7 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +1 -1
  47. package/dist/types.js.map +1 -1
  48. package/dist/values.d.ts +1 -1
  49. package/dist/values.d.ts.map +1 -1
  50. package/dist/values.js +39 -13
  51. package/dist/values.js.map +1 -1
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -261,7 +261,7 @@ CLAUDE.md
261
261
  single-hash would flag 1 · lockwire flagged 1 · noise −0%
262
262
  ```
263
263
 
264
- Update the doc, then re-stamp — `--reviewed` is required because this anchor is currently `drifted`:
264
+ Update the doc, then re-stamp — `--reviewed` is required because the code changed since the claim was stamped:
265
265
 
266
266
  ```bash
267
267
  $ lockwire link CLAUDE.md --reviewed
package/dist/actions.d.ts CHANGED
@@ -13,11 +13,16 @@ export interface TargetResolution {
13
13
  /** When not found and there's more to say (an ambiguous CSV row, a missing column). */
14
14
  reason?: string;
15
15
  }
16
- /** Recomputes all four tier fingerprints for a target, or reports it unresolved (file/symbol not found). */
16
+ /** The fingerprint version an anchor was stamped with (anchors from before versions are version 1). */
17
+ export declare const versionOf: (anchor: Pick<Anchor, "fingerprintVersion">) => number;
18
+ /**
19
+ * Recomputes all four tier fingerprints for a target, or reports it unresolved (file/symbol not found).
20
+ * `version` is how to compute them: the current way to stamp, an anchor's own to compare with it.
21
+ */
17
22
  export declare function resolveTarget(repoRoot: string, target: {
18
23
  path: string;
19
24
  symbol?: string;
20
- }, config: LockwireConfig): Promise<TargetResolution | null>;
25
+ }, config: LockwireConfig, version?: number): Promise<TargetResolution | null>;
21
26
  /**
22
27
  * {@link resolveTarget} on given file contents rather than the file on disk — used to fingerprint a
23
28
  * target as it was at an earlier commit. `source` is the bound symbol's declaration as written.
@@ -25,7 +30,7 @@ export declare function resolveTarget(repoRoot: string, target: {
25
30
  export declare function resolveTargetInText(target: {
26
31
  path: string;
27
32
  symbol?: string;
28
- }, content: string, config: LockwireConfig): Promise<TargetResolution & {
33
+ }, content: string, config: LockwireConfig, version?: number): Promise<TargetResolution & {
29
34
  source?: string;
30
35
  }>;
31
36
  /**
@@ -36,10 +41,18 @@ export declare function resolveTargetInText(target: {
36
41
  * unrelated neighbour with the same shape isn't mistaken for the rename target. Cross-file
37
42
  * relocation (the symbol moved to a different file) is not attempted in P0 — see docs/concepts.md limitations.
38
43
  */
39
- export declare function findRelocationCandidate(symbols: FileSymbols, missingSigFp: string, oldName: string, taken?: ReadonlySet<string>): string | null;
44
+ export declare function findRelocationCandidate(symbols: FileSymbols, missingSigFp: string, oldName: string, taken?: ReadonlySet<string>,
45
+ /**
46
+ * The vanished symbol's stamped body and deps. When given, a candidate must also match one of them:
47
+ * a rename keeps the implementation, while a deleted function's look-alike neighbour (same
48
+ * parameters, different code) is not where its claim went.
49
+ */
50
+ stamped?: Pick<Fingerprints, "body" | "deps">): string | null;
40
51
  export interface LinkResult {
41
52
  created: number;
42
53
  refreshed: number;
54
+ /** Markers whose anchor was already fresh and current: nothing was written for them. */
55
+ unchanged: number;
43
56
  skipped: {
44
57
  reason: string;
45
58
  target: string;
@@ -123,13 +136,15 @@ export interface CheckResult {
123
136
  /** A cached reader of a doc's markers, or null when the doc doesn't exist. */
124
137
  export declare function markerReader(repoRoot: string): (doc: string) => Promise<DocMarker[] | null>;
125
138
  /** The part of a marker needed to classify it and show its claim; DocMarker and the doc index both have it. */
126
- export type MarkerView = Pick<DocMarker, "id" | "target" | "line" | "claimExcerpt">;
139
+ export type MarkerView = Pick<DocMarker, "id" | "target" | "line" | "claimExcerpt" | "problem">;
127
140
  /**
128
141
  * Markers in `docs` that no anchor in `lockfile` backs: no `id=` yet, an id that matches no anchor, an
129
142
  * id that belongs to the marker in another doc (a copy-paste), or one used twice in a doc. Until
130
143
  * `lockwire link` runs, such a claim isn't being checked at all.
131
144
  */
132
- export declare function findUnlinkedMarkers(lockfile: Lockfile, docs: readonly string[], readMarkers: (doc: string) => Promise<readonly MarkerView[] | null>): Promise<UnlinkedMarker[]>;
145
+ export declare function findUnlinkedMarkers(lockfile: Lockfile, docs: readonly string[], readMarkers: (doc: string) => Promise<readonly MarkerView[] | null>,
146
+ /** When given, a marker whose target file is missing says so instead of "not linked yet". */
147
+ repoRoot?: string): Promise<UnlinkedMarker[]>;
133
148
  /**
134
149
  * A holder a long-lived caller keeps between calls so the doc index lives in memory and each call
135
150
  * only re-validates it.
@@ -163,6 +178,10 @@ export declare function check(repoRoot: string, config: LockwireConfig, onlyPath
163
178
  write?: boolean;
164
179
  actor?: Actor;
165
180
  }): Promise<CheckResult>;
181
+ /**
182
+ * Docs the config selects. A symlinked doc is skipped: it's another name for a file the walk reaches
183
+ * anyway (`README.md -> packages/zod/README.md`), and linking both would bind every claim twice.
184
+ */
166
185
  export declare function discoverDocs(repoRoot: string, config: LockwireConfig): Promise<string[]>;
167
186
  export declare function ack(repoRoot: string, anchorId: string, resolution: "updated" | "superseded" | "false-positive", note: string | undefined, actor: Actor, config: LockwireConfig): Promise<Anchor>;
168
187
  export declare function waive(repoRoot: string, anchorId: string, reason: string, expires: string, actor: Actor): Promise<Anchor>;
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
package/dist/actions.js CHANGED
@@ -1,30 +1,35 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
3
  import { headCommit } from "./changed.js";
4
4
  import { loadDocIndex } from "./docindex.js";
5
- import { computeWholeFileTiers, extractFileSymbols, fileExportsFingerprint } from "./extract.js";
5
+ import { computeWholeFileTiers, extractFileSymbols, FINGERPRINT_VERSION, fileExportsFingerprint, } from "./extract.js";
6
6
  import { langForPath, parserFor } from "./grammar.js";
7
7
  import { fingerprint, fingerprintSet } from "./hash.js";
8
8
  import { appendEvent, historyFor, readLedger } from "./ledger.js";
9
9
  import { anchorsForPath, lockfilePath, newAnchorId, readLockfile, removeAnchor, upsertAnchor, writeFileAtomic, writeLockfile, } from "./lockfile.js";
10
10
  import { claimUnchanged, retargetMarkerLine, scanMarkers, stampMarkerLine, } from "./markers.js";
11
- import { globToRegExp, isScannedDoc, matchesAny, outsideRepoMessage, resolveInsideRepo, stripBom, toRepoRelative, walkFiles, walkFilesWithin, } from "./repo.js";
11
+ import { canonicalDocPath, globToRegExp, isScannedDoc, matchesAny, outsideRepoMessage, resolveInsideRepo, stripBom, walkFiles, } from "./repo.js";
12
12
  import { ALL_TIERS, tierFingerprint } from "./types.js";
13
13
  import { checkClaimAgainstValue, dataFormatOf, resolveValue, valueFingerprintInput, wholeFileContent, } from "./values.js";
14
- /** Recomputes all four tier fingerprints for a target, or reports it unresolved (file/symbol not found). */
15
- export async function resolveTarget(repoRoot, target, config) {
14
+ /** The fingerprint version an anchor was stamped with (anchors from before versions are version 1). */
15
+ export const versionOf = (anchor) => anchor.fingerprintVersion ?? 1;
16
+ /**
17
+ * Recomputes all four tier fingerprints for a target, or reports it unresolved (file/symbol not found).
18
+ * `version` is how to compute them: the current way to stamp, an anchor's own to compare with it.
19
+ */
20
+ export async function resolveTarget(repoRoot, target, config, version = FINGERPRINT_VERSION) {
16
21
  const absPath = resolveInsideRepo(repoRoot, target.path);
17
22
  if (!absPath)
18
23
  throw new Error(outsideRepoMessage(target.path));
19
24
  if (!existsSync(absPath))
20
25
  return null;
21
- return resolveTargetInText(target, await readFile(absPath, "utf8"), config);
26
+ return resolveTargetInText(target, await readFile(absPath, "utf8"), config, version);
22
27
  }
23
28
  /**
24
29
  * {@link resolveTarget} on given file contents rather than the file on disk — used to fingerprint a
25
30
  * target as it was at an earlier commit. `source` is the bound symbol's declaration as written.
26
31
  */
27
- export async function resolveTargetInText(target, content, config) {
32
+ export async function resolveTargetInText(target, content, config, version = FINGERPRINT_VERSION) {
28
33
  // A data file (JSON, LaTeX macros): the target is a value in it, not a symbol.
29
34
  const dataFormat = dataFormatOf(target.path);
30
35
  if (dataFormat) {
@@ -49,13 +54,10 @@ export async function resolveTargetInText(target, content, config) {
49
54
  }
50
55
  const lang = langForPath(target.path);
51
56
  if (!lang)
52
- throw new Error(`lockwire supports TypeScript, TSX, JavaScript, and Python source, and values in JSON and LaTeX (.tex) files — "${target.path}" is none of these`);
53
- const parser = await parserFor(lang);
54
- const tree = parser.parse(content);
55
- if (!tree)
56
- throw new Error(`failed to parse ${target.path}`);
57
- const root = tree.rootNode;
58
- const symbols = extractFileSymbols(root, lang, config.normalizeLocals);
57
+ throw new Error(`lockwire supports TypeScript, TSX, JavaScript, and Python source, and values in JSON, CSV, TSV and LaTeX (.tex) files — "${target.path}" is none of these`);
58
+ // A symbol anchor never needs the whole file serialized, which is the costly part in a big file.
59
+ const parsed = await parseSource(target.path, content, lang, config, version, !target.symbol);
60
+ const { symbols } = parsed;
59
61
  const pathFp = fingerprint(target.path);
60
62
  if (target.symbol) {
61
63
  const symbol = symbols.bySymbolPath.get(target.symbol);
@@ -73,19 +75,61 @@ export async function resolveTargetInText(target, content, config) {
73
75
  source: symbol.sourceText,
74
76
  };
75
77
  }
76
- const whole = computeWholeFileTiers(root, lang, config.normalizeLocals);
77
78
  return {
78
79
  found: true,
79
80
  fingerprints: {
80
81
  path: pathFp,
81
82
  sig: fileExportsFingerprint(symbols),
82
- body: fingerprint(whole.bodyNormalized),
83
- deps: fingerprintSet(whole.deps),
83
+ body: fingerprint(parsed.whole?.bodyNormalized ?? ""),
84
+ deps: fingerprintSet(parsed.whole?.deps ?? []),
84
85
  },
85
86
  symbols,
86
87
  source: content,
87
88
  };
88
89
  }
90
+ /**
91
+ * The last parse of each file, reused while its content is the same: a check with many anchors on one
92
+ * file parses it once, not once per anchor. Keyed on the content itself rather than mtime, so an edit
93
+ * within the same millisecond can't be served stale.
94
+ */
95
+ const parseCache = new Map();
96
+ const PARSE_CACHE_SIZE = 256;
97
+ async function parseSource(path, content, lang, config, version, needWhole) {
98
+ const key = `${version}|${config.normalizeLocals}|${lang}|${path}`;
99
+ const hit = parseCache.get(key);
100
+ if (hit && hit.content === content && (hit.whole || !needWhole)) {
101
+ parseCache.delete(key); // most recently used last
102
+ parseCache.set(key, hit);
103
+ return hit;
104
+ }
105
+ const tree = (await parserFor(lang)).parse(content);
106
+ if (!tree)
107
+ throw new Error(`failed to parse ${path}`);
108
+ // Everything kept is read out as strings, so the tree (wasm memory) is freed straight away.
109
+ let parsed;
110
+ try {
111
+ parsed = {
112
+ content,
113
+ symbols: hit?.content === content
114
+ ? hit.symbols
115
+ : extractFileSymbols(tree.rootNode, lang, config.normalizeLocals, version),
116
+ ...(needWhole
117
+ ? { whole: computeWholeFileTiers(tree.rootNode, lang, config.normalizeLocals, version) }
118
+ : {}),
119
+ };
120
+ }
121
+ finally {
122
+ tree.delete();
123
+ }
124
+ parseCache.delete(key);
125
+ parseCache.set(key, parsed);
126
+ if (parseCache.size > PARSE_CACHE_SIZE) {
127
+ const oldest = parseCache.keys().next().value;
128
+ if (oldest !== undefined)
129
+ parseCache.delete(oldest);
130
+ }
131
+ return parsed;
132
+ }
89
133
  /**
90
134
  * Same-file rename detection: if a symbol vanished but exactly one other symbol in the file has the
91
135
  * same signature apart from its name, relink to it. `sig` fingerprints include the symbol's name,
@@ -94,18 +138,30 @@ export async function resolveTargetInText(target, content, config) {
94
138
  * unrelated neighbour with the same shape isn't mistaken for the rename target. Cross-file
95
139
  * relocation (the symbol moved to a different file) is not attempted in P0 — see docs/concepts.md limitations.
96
140
  */
97
- export function findRelocationCandidate(symbols, missingSigFp, oldName, taken = new Set()) {
141
+ export function findRelocationCandidate(symbols, missingSigFp, oldName, taken = new Set(),
142
+ /**
143
+ * The vanished symbol's stamped body and deps. When given, a candidate must also match one of them:
144
+ * a rename keeps the implementation, while a deleted function's look-alike neighbour (same
145
+ * parameters, different code) is not where its claim went.
146
+ */
147
+ stamped) {
98
148
  const candidates = [];
99
149
  for (const [path, symbol] of symbols.bySymbolPath) {
100
150
  if (taken.has(path))
101
151
  continue;
102
- const sig = symbol.sigTokens
103
- .replace(`fn ${symbol.name}(`, `fn ${oldName}(`)
104
- .replace(`class ${symbol.name} [`, `class ${oldName} [`);
105
- if (fingerprint(sig) === missingSigFp)
106
- candidates.push(path);
152
+ const sig = symbol.sigFor?.(oldName) ??
153
+ symbol.sigTokens
154
+ .replace(`fn ${symbol.name}(`, `fn ${oldName}(`)
155
+ .replace(`class ${symbol.name} [`, `class ${oldName} [`);
156
+ if (fingerprint(sig) !== missingSigFp)
157
+ continue;
158
+ if (stamped &&
159
+ fingerprint(symbol.bodyNormalized) !== stamped.body &&
160
+ fingerprintSet(symbol.deps) !== stamped.deps)
161
+ continue;
162
+ candidates.push(path);
107
163
  }
108
- return candidates.length === 1 ? candidates[0] : null;
164
+ return candidates.length === 1 ? (candidates[0] ?? null) : null;
109
165
  }
110
166
  /** Tiers a marker binds when it doesn't say: `value` for a value in a data file, `sig` for a symbol, `path,body` for a file. */
111
167
  /** The tiers a target can actually produce: a tier it can't would be empty forever, so never drift. */
@@ -133,10 +189,12 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
133
189
  const absDoc = resolveInsideRepo(repoRoot, docPath);
134
190
  if (!absDoc)
135
191
  throw new Error(outsideRepoMessage(docPath));
192
+ // A symlinked doc is bound under the file it points to, so naming either one gives the same anchors.
193
+ docPath = canonicalDocPath(repoRoot, docPath, absDoc);
136
194
  const text = await readFile(absDoc, "utf8");
137
195
  const markers = scanMarkers(text);
138
196
  const lockfile = await readLockfile(repoRoot);
139
- const result = { created: 0, refreshed: 0, skipped: [] };
197
+ const result = { created: 0, refreshed: 0, unchanged: 0, skipped: [] };
140
198
  // Keep each line's own terminator: stamping a marker must change that one line, not rewrite every
141
199
  // CRLF in the file to LF. `parts` alternates line, terminator, line, terminator, ...
142
200
  const parts = text.split(/(\r?\n)/);
@@ -152,6 +210,10 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
152
210
  continue;
153
211
  }
154
212
  const targetLabel = `${marker.target.path}${marker.target.symbol ? `#${marker.target.symbol}` : ""}`;
213
+ if (marker.problem) {
214
+ result.skipped.push({ reason: marker.problem, target: targetLabel });
215
+ continue;
216
+ }
155
217
  let existing = marker.id ? current.anchors.find((a) => a.id === marker.id) : undefined;
156
218
  // A marker copy-pasted into another doc carries the original's id. If the original doc still
157
219
  // holds that marker, this is a second claim, not the same one moved -- binding it to the
@@ -188,7 +250,7 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
188
250
  // does that, and only updates the lockfile). Follow the anchor and fix the marker to match.
189
251
  let target = marker.target;
190
252
  let retargeted = false;
191
- if ((!resolved || !resolved.found) && existing) {
253
+ if (!resolved?.found && existing) {
192
254
  const moved = existing.target;
193
255
  if (moved.path !== marker.target.path || moved.symbol !== marker.target.symbol) {
194
256
  try {
@@ -204,23 +266,34 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
204
266
  }
205
267
  }
206
268
  }
207
- if (!resolved || !resolved.found) {
269
+ if (!resolved?.found) {
208
270
  result.skipped.push({
209
271
  reason: resolved?.reason ? `target not found: ${resolved.reason}` : "target not found",
210
272
  target: targetLabel,
211
273
  });
212
274
  continue;
213
275
  }
214
- // Re-stamping a drifted (or orphaned-then-reappeared) anchor accepts code nobody has looked at, so
215
- // it needs a human's say-so -- unless the code still matches and only the claim sentence moved.
216
- const codeDrifted = existing !== undefined &&
217
- existing.tiers.some((t) => tierFingerprint(resolved.fingerprints, t) !== tierFingerprint(existing.fingerprints, t));
218
- if ((existing?.status === "drifted" || existing?.status === "orphaned") &&
219
- codeDrifted &&
220
- !opts.reviewed) {
276
+ // Compare with the anchor as it was computed: an anchor stamped by an older fingerprint version is
277
+ // compared under that version, so upgrading lockwire never reads as changed code.
278
+ const compared = existing && versionOf(existing) !== FINGERPRINT_VERSION
279
+ ? await resolveTarget(repoRoot, target, config, versionOf(existing)).catch(() => null)
280
+ : resolved;
281
+ const then = compared?.found ? compared : resolved;
282
+ // Re-stamping an anchor whose bound code moved since it was stamped accepts code nobody has looked
283
+ // at, so it needs a human's say-so -- whether or not a `check` has recorded the drift yet. A moved
284
+ // value the sentence already states is the fix itself, not unreviewed code.
285
+ const codeDrifted = existing?.tiers.some((t) => {
286
+ if (tierFingerprint(then.fingerprints, t) === tierFingerprint(existing.fingerprints, t))
287
+ return false;
288
+ if (t !== "value" || !resolved.value)
289
+ return true;
290
+ const verdict = checkClaimAgainstValue(marker.claimText, resolved.value.raw);
291
+ return !(verdict.checkable && verdict.matches);
292
+ }) === true;
293
+ if (codeDrifted && !opts.reviewed) {
221
294
  result.skipped.push({
222
- reason: "drifted anchor needs --reviewed to re-stamp",
223
- target: marker.target.path,
295
+ reason: "the code changed since this claim was stamped: review it, then link --reviewed",
296
+ target: targetLabel,
224
297
  });
225
298
  continue;
226
299
  }
@@ -246,6 +319,20 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
246
319
  idsSeenInDoc.add(id);
247
320
  if (marker.id)
248
321
  idsSeenInDoc.add(marker.id);
322
+ // Nothing to re-stamp: same claim, same place, same code. Leave the anchor and the ledger alone.
323
+ if (existing &&
324
+ !isNew &&
325
+ !retargeted &&
326
+ existing.status === "fresh" &&
327
+ versionOf(existing) === FINGERPRINT_VERSION &&
328
+ existing.doc === docPath &&
329
+ existing.claim?.hash === marker.claimHash &&
330
+ existing.claim?.line === marker.claimLine &&
331
+ existing.tiers.join() === tiers.join() &&
332
+ ALL_TIERS.every((t) => tierFingerprint(existing.fingerprints, t) === tierFingerprint(resolved.fingerprints, t))) {
333
+ result.unchanged++;
334
+ continue;
335
+ }
249
336
  const anchor = {
250
337
  id,
251
338
  doc: docPath,
@@ -258,6 +345,7 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
258
345
  target,
259
346
  tiers,
260
347
  fingerprints: resolved.fingerprints,
348
+ fingerprintVersion: FINGERPRINT_VERSION,
261
349
  linked: { at: new Date().toISOString(), commit, by: actor },
262
350
  status: "fresh",
263
351
  waiver: null,
@@ -321,7 +409,7 @@ export async function linkLockfileOnly(repoRoot, docPath, target, tiers, config,
321
409
  if (tierProblem)
322
410
  throw new Error(tierProblem);
323
411
  const resolved = await resolveTarget(repoRoot, target, config);
324
- if (!resolved || !resolved.found)
412
+ if (!resolved?.found)
325
413
  throw new Error(`target not found: ${target.path}${target.symbol ? `#${target.symbol}` : ""}`);
326
414
  const lockfile = await readLockfile(repoRoot);
327
415
  const anchor = {
@@ -331,6 +419,7 @@ export async function linkLockfileOnly(repoRoot, docPath, target, tiers, config,
331
419
  target,
332
420
  tiers,
333
421
  fingerprints: resolved.fingerprints,
422
+ fingerprintVersion: FINGERPRINT_VERSION,
334
423
  linked: { at: new Date().toISOString(), commit: null, by: actor },
335
424
  status: "fresh",
336
425
  waiver: null,
@@ -362,7 +451,9 @@ export function markerReader(repoRoot) {
362
451
  * id that belongs to the marker in another doc (a copy-paste), or one used twice in a doc. Until
363
452
  * `lockwire link` runs, such a claim isn't being checked at all.
364
453
  */
365
- export async function findUnlinkedMarkers(lockfile, docs, readMarkers) {
454
+ export async function findUnlinkedMarkers(lockfile, docs, readMarkers,
455
+ /** When given, a marker whose target file is missing says so instead of "not linked yet". */
456
+ repoRoot) {
366
457
  const unlinked = [];
367
458
  for (const doc of [...docs].sort()) {
368
459
  const markers = await readMarkers(doc);
@@ -372,8 +463,18 @@ export async function findUnlinkedMarkers(lockfile, docs, readMarkers) {
372
463
  for (const m of markers) {
373
464
  let reason = null;
374
465
  const anchor = m.id ? lockfile.anchors.find((a) => a.id === m.id) : undefined;
375
- if (!m.id)
376
- reason = "not linked yet";
466
+ if (!m.id && m.problem)
467
+ reason = `not linked: ${m.problem}`;
468
+ else if (!m.id) {
469
+ const abs = repoRoot ? resolveInsideRepo(repoRoot, m.target.path) : undefined;
470
+ // `link` can't fix a marker that points nowhere: say what's wrong with it instead.
471
+ reason =
472
+ abs === null
473
+ ? `${m.target.path} is outside the repository`
474
+ : abs !== undefined && !existsSync(abs)
475
+ ? `not linked: ${m.target.path} doesn't exist`
476
+ : "not linked yet";
477
+ }
377
478
  else if (!anchor)
378
479
  reason = `id ${m.id} matches no anchor`;
379
480
  else if (seenIds.has(m.id))
@@ -566,7 +667,7 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
566
667
  let resolved;
567
668
  let unresolvedNote = "file not found";
568
669
  try {
569
- resolved = await resolveTarget(repoRoot, anchor.target, config);
670
+ resolved = await resolveTarget(repoRoot, anchor.target, config, versionOf(anchor));
570
671
  }
571
672
  catch (err) {
572
673
  resolved = null;
@@ -597,10 +698,10 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
597
698
  const relocatedTo = resolved.symbols
598
699
  ? findRelocationCandidate(resolved.symbols, anchor.fingerprints.sig, anchor.target.symbol.slice(anchor.target.symbol.lastIndexOf(".") + 1), new Set(lockfile.anchors
599
700
  .filter((a) => a.id !== anchor.id && a.target.path === anchor.target.path)
600
- .flatMap((a) => (a.target.symbol ? [a.target.symbol] : []))))
701
+ .flatMap((a) => (a.target.symbol ? [a.target.symbol] : []))), anchor.fingerprints)
601
702
  : null;
602
703
  if (relocatedTo) {
603
- const reResolved = await resolveTarget(repoRoot, { path: anchor.target.path, symbol: relocatedTo }, config);
704
+ const reResolved = await resolveTarget(repoRoot, { path: anchor.target.path, symbol: relocatedTo }, config, versionOf(anchor));
604
705
  const target = { path: anchor.target.path, symbol: relocatedTo };
605
706
  // A rename must not launder other changes: `sig` is expected to differ (it carries the
606
707
  // name, and the candidate already matched on it), but any other bound tier that moved is
@@ -674,7 +775,8 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
674
775
  }
675
776
  continue;
676
777
  }
677
- const moved = (t) => tierFingerprint(resolved.fingerprints, t) !== tierFingerprint(anchor.fingerprints, t);
778
+ const nowFingerprints = resolved.fingerprints;
779
+ const moved = (t) => tierFingerprint(nowFingerprints, t) !== tierFingerprint(anchor.fingerprints, t);
678
780
  let driftedTiers = anchor.tiers.filter(moved);
679
781
  const anyTierChanged = ALL_TIERS.filter(moved);
680
782
  // A value anchor can be checked for truth: does the sentence still state the value? If it does
@@ -750,6 +852,21 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
750
852
  });
751
853
  }
752
854
  else {
855
+ // Stamped by an older fingerprint version and nothing has moved since: re-stamp it the current way
856
+ // so later checks see what the current version sees (types in `sig`, every overload, ...). Nothing
857
+ // about the claim changed, so this records no event. If the target doesn't resolve the current way
858
+ // (a version-1 path a newer version spells differently), it keeps its old fingerprints.
859
+ if (versionOf(anchor) !== FINGERPRINT_VERSION && anyTierChanged.length === 0) {
860
+ const upgraded = await resolveTarget(repoRoot, anchor.target, config).catch(() => null);
861
+ if (upgraded?.found) {
862
+ anchor = {
863
+ ...anchor,
864
+ fingerprints: upgraded.fingerprints,
865
+ fingerprintVersion: FINGERPRINT_VERSION,
866
+ };
867
+ current = upsertAnchor(current, anchor);
868
+ }
869
+ }
753
870
  if (anchor.status === "drifted" && unchangedForThisClaim) {
754
871
  const resolvedAnchor = { ...anchor, status: "fresh" };
755
872
  current = upsertAnchor(current, resolvedAnchor);
@@ -789,7 +906,7 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
789
906
  const docsToScan = onlyPaths
790
907
  ? await docsInScope(repoRoot, config, onlyPaths)
791
908
  : await discoverDocs(repoRoot, config);
792
- const unlinked = await findUnlinkedMarkers(lockfile, docsToScan, markersIn);
909
+ const unlinked = await findUnlinkedMarkers(lockfile, docsToScan, markersIn, repoRoot);
793
910
  const singleHashWouldFlag = examined.filter((r) => r.singleHashWouldFlag).length;
794
911
  // The noise comparison is about code fingerprints; a claim-only flag has no single-hash counterpart.
795
912
  const tieredFlagged = examined.filter((r) => (r.status === "drifted" || r.status === "orphaned") &&
@@ -810,8 +927,12 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
810
927
  };
811
928
  return { summary, results, unlinked };
812
929
  }
930
+ /**
931
+ * Docs the config selects. A symlinked doc is skipped: it's another name for a file the walk reaches
932
+ * anyway (`README.md -> packages/zod/README.md`), and linking both would bind every claim twice.
933
+ */
813
934
  export async function discoverDocs(repoRoot, config) {
814
- return walkFiles(repoRoot, config.exclude).filter((p) => matchesAny(p, config.docs));
935
+ return walkFiles(repoRoot, config.exclude, { skipSymlinks: true }).filter((p) => matchesAny(p, config.docs));
815
936
  }
816
937
  export async function ack(repoRoot, anchorId, resolution, note, actor, config) {
817
938
  const lockfile = await readLockfile(repoRoot);
@@ -849,6 +970,7 @@ export async function ack(repoRoot, anchorId, resolution, note, actor, config) {
849
970
  ...anchor,
850
971
  status: "fresh",
851
972
  fingerprints: resolved.fingerprints,
973
+ fingerprintVersion: FINGERPRINT_VERSION,
852
974
  claim,
853
975
  ...(resolved.value ? { value: resolved.value.display } : {}),
854
976
  };
@@ -866,7 +988,13 @@ export async function ack(repoRoot, anchorId, resolution, note, actor, config) {
866
988
  }
867
989
  export async function waive(repoRoot, anchorId, reason, expires, actor) {
868
990
  const expiresAt = Date.parse(expires);
869
- if (!/^\d{4}-\d{2}-\d{2}/.test(expires) || Number.isNaN(expiresAt))
991
+ // Date.parse rolls an impossible day over (2027-02-31 is March 3rd); a waiver must say what it means.
992
+ const [y, m, d] = (/^(\d{4})-(\d{2})-(\d{2})/.exec(expires) ?? []).slice(1).map(Number);
993
+ const realDay = y !== undefined &&
994
+ m !== undefined &&
995
+ d !== undefined &&
996
+ new Date(Date.UTC(y, m - 1, d)).toISOString().startsWith(expires.slice(0, 10));
997
+ if (!realDay || Number.isNaN(expiresAt))
870
998
  throw new Error(`invalid --expires "${expires}": use an ISO date such as 2026-12-31 (waivers must expire)`);
871
999
  if (expiresAt <= Date.now())
872
1000
  throw new Error(`--expires "${expires}" is already in the past`);