lockwire 0.1.29 → 0.1.31

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 +2 -1
  2. package/dist/actions.d.ts +25 -6
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +168 -40
  5. package/dist/actions.js.map +1 -1
  6. package/dist/cli.js +44 -7
  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 +22 -3
  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 +36 -11
  51. package/dist/values.js.map +1 -1
  52. package/package.json +5 -5
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  The mark is literal, not a metaphor: [lockwiring](https://en.wikipedia.org/wiki/Safety_wire) is the aviation-maintenance technique of threading wire through two fasteners so neither can work loose under vibration — the same idea applied to a doc claim and the code it depends on.
14
14
 
15
15
  [![npm](https://img.shields.io/npm/v/lockwire)](https://www.npmjs.com/package/lockwire)
16
+ [![Socket](https://socket.dev/api/badge/npm/package/lockwire)](https://socket.dev/npm/package/lockwire)
16
17
  [![bundle size](https://img.shields.io/bundlephobia/minzip/lockwire)](https://bundlephobia.com/package/lockwire)
17
18
  [![license: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
18
19
  [![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-8A63D2)](https://code.claude.com/docs/en/plugins)
@@ -261,7 +262,7 @@ CLAUDE.md
261
262
  single-hash would flag 1 · lockwire flagged 1 · noise −0%
262
263
  ```
263
264
 
264
- Update the doc, then re-stamp — `--reviewed` is required because this anchor is currently `drifted`:
265
+ Update the doc, then re-stamp — `--reviewed` is required because the code changed since the claim was stamped:
265
266
 
266
267
  ```bash
267
268
  $ 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":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAgB,MAAM,eAAe,CAAC;AAe/E,OAAO,EAEL,KAAK,SAAS,EAIf,MAAM,cAAc,CAAC;AAUtB,OAAO,KAAK,EACV,KAAK,EACL,MAAM,EACN,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,IAAI,EACL,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,6DAA6D;IAC7D,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,4GAA4G;AAC5G,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAKlC;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,gBAAgB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkEjD;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,WAAW,CAAC,MAAM,CAAa,GACrC,MAAM,GAAG,IAAI,CAUf;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/C;AAED,gIAAgI;AAChI,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAAE,CAGjF;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,KAAK,EAAE,SAAS,IAAI,EAAE,GACrB,MAAM,GAAG,IAAI,CAMf;AAUD,oGAAoG;AACpG,wBAAsB,OAAO,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,KAAK,EACZ,IAAI,GAAE;IACJ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iFAAiF;IACjF,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC;IACtC,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;CACV,GACL,OAAO,CAAC,UAAU,CAAC,CAgMrB;AAED,qHAAqH;AACrH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzC,KAAK,EAAE,IAAI,EAAE,EACb,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACzB,YAAY,EAAE,IAAI,EAAE,CAAC;IACrB,qGAAqG;IACrG,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iGAAiG;IACjG,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC3E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QAAE,mBAAmB,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAAC;CACzF;AAED,oGAAoG;AACpG,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,IAE7B,KAAK,MAAM,KAAG,OAAO,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,CAOxD;AAED,+GAA+G;AAC/G,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,QAAQ,GAAG,MAAM,GAAG,cAAc,CAAC,CAAC;AAEpF;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,UAAU,EAAE,GAAG,IAAI,CAAC,GAClE,OAAO,CAAC,cAAc,EAAE,CAAC,CA4B3B;AAKD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY,EACtB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GACpC,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ3B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAAO,GACnD,OAAO,CAAC;IACT,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB,CAAC,CAmBD;AAED,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAW3E;AAwBD,wBAAsB,KAAK,CACzB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,EACtB,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,EAC7B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAO,GAC5C,OAAO,CAAC,WAAW,CAAC,CAsXtB;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE9F;AAED,wBAAsB,GAAG,CACvB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,GAAG,YAAY,GAAG,gBAAgB,EACvD,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CAoDjB;AAED,wBAAsB,KAAK,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG9E;AAED,wBAAsB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAG7F;AAED,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKhF;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;;;GAmB1D"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
package/dist/actions.js CHANGED
@@ -2,29 +2,34 @@ import { existsSync, statSync } from "node:fs";
2
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, walkFiles, } 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,16 +138,28 @@ 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
164
  return candidates.length === 1 ? (candidates[0] ?? null) : null;
109
165
  }
@@ -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
@@ -211,15 +273,27 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
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?.tiers.some((t) => tierFingerprint(resolved.fingerprints, t) !== tierFingerprint(existing.fingerprints, t)) === true;
217
- if ((existing?.status === "drifted" || existing?.status === "orphaned") &&
218
- codeDrifted &&
219
- !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) {
220
294
  result.skipped.push({
221
- reason: "drifted anchor needs --reviewed to re-stamp",
222
- target: marker.target.path,
295
+ reason: "the code changed since this claim was stamped: review it, then link --reviewed",
296
+ target: targetLabel,
223
297
  });
224
298
  continue;
225
299
  }
@@ -245,6 +319,20 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
245
319
  idsSeenInDoc.add(id);
246
320
  if (marker.id)
247
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
+ }
248
336
  const anchor = {
249
337
  id,
250
338
  doc: docPath,
@@ -257,6 +345,7 @@ export async function linkDoc(repoRoot, docPath, config, actor, opts = {}) {
257
345
  target,
258
346
  tiers,
259
347
  fingerprints: resolved.fingerprints,
348
+ fingerprintVersion: FINGERPRINT_VERSION,
260
349
  linked: { at: new Date().toISOString(), commit, by: actor },
261
350
  status: "fresh",
262
351
  waiver: null,
@@ -330,6 +419,7 @@ export async function linkLockfileOnly(repoRoot, docPath, target, tiers, config,
330
419
  target,
331
420
  tiers,
332
421
  fingerprints: resolved.fingerprints,
422
+ fingerprintVersion: FINGERPRINT_VERSION,
333
423
  linked: { at: new Date().toISOString(), commit: null, by: actor },
334
424
  status: "fresh",
335
425
  waiver: null,
@@ -361,7 +451,9 @@ export function markerReader(repoRoot) {
361
451
  * id that belongs to the marker in another doc (a copy-paste), or one used twice in a doc. Until
362
452
  * `lockwire link` runs, such a claim isn't being checked at all.
363
453
  */
364
- 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) {
365
457
  const unlinked = [];
366
458
  for (const doc of [...docs].sort()) {
367
459
  const markers = await readMarkers(doc);
@@ -371,8 +463,18 @@ export async function findUnlinkedMarkers(lockfile, docs, readMarkers) {
371
463
  for (const m of markers) {
372
464
  let reason = null;
373
465
  const anchor = m.id ? lockfile.anchors.find((a) => a.id === m.id) : undefined;
374
- if (!m.id)
375
- 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
+ }
376
478
  else if (!anchor)
377
479
  reason = `id ${m.id} matches no anchor`;
378
480
  else if (seenIds.has(m.id))
@@ -565,7 +667,7 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
565
667
  let resolved;
566
668
  let unresolvedNote = "file not found";
567
669
  try {
568
- resolved = await resolveTarget(repoRoot, anchor.target, config);
670
+ resolved = await resolveTarget(repoRoot, anchor.target, config, versionOf(anchor));
569
671
  }
570
672
  catch (err) {
571
673
  resolved = null;
@@ -596,10 +698,10 @@ export async function check(repoRoot, config, onlyPaths, opts = {}) {
596
698
  const relocatedTo = resolved.symbols
597
699
  ? findRelocationCandidate(resolved.symbols, anchor.fingerprints.sig, anchor.target.symbol.slice(anchor.target.symbol.lastIndexOf(".") + 1), new Set(lockfile.anchors
598
700
  .filter((a) => a.id !== anchor.id && a.target.path === anchor.target.path)
599
- .flatMap((a) => (a.target.symbol ? [a.target.symbol] : []))))
701
+ .flatMap((a) => (a.target.symbol ? [a.target.symbol] : []))), anchor.fingerprints)
600
702
  : null;
601
703
  if (relocatedTo) {
602
- 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));
603
705
  const target = { path: anchor.target.path, symbol: relocatedTo };
604
706
  // A rename must not launder other changes: `sig` is expected to differ (it carries the
605
707
  // name, and the candidate already matched on it), but any other bound tier that moved is
@@ -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`);