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.
- package/README.md +1 -1
- package/dist/actions.d.ts +25 -6
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +175 -47
- package/dist/actions.js.map +1 -1
- package/dist/cli.js +46 -9
- package/dist/cli.js.map +1 -1
- package/dist/docindex.d.ts +1 -0
- package/dist/docindex.d.ts.map +1 -1
- package/dist/docindex.js +4 -2
- package/dist/docindex.js.map +1 -1
- package/dist/extract-v2.d.ts +34 -0
- package/dist/extract-v2.d.ts.map +1 -0
- package/dist/extract-v2.js +705 -0
- package/dist/extract-v2.js.map +1 -0
- package/dist/extract.d.ts +8 -2
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +13 -2
- package/dist/extract.js.map +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +8 -2
- package/dist/format.js.map +1 -1
- package/dist/grammar.d.ts +1 -0
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +7 -1
- package/dist/grammar.js.map +1 -1
- package/dist/lockfile.d.ts +1 -1
- package/dist/lockfile.d.ts.map +1 -1
- package/dist/lockfile.js +10 -2
- package/dist/lockfile.js.map +1 -1
- package/dist/markers.d.ts +2 -0
- package/dist/markers.d.ts.map +1 -1
- package/dist/markers.js +85 -28
- package/dist/markers.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/mcp-server.js.map +1 -1
- package/dist/repo.d.ts +12 -2
- package/dist/repo.d.ts.map +1 -1
- package/dist/repo.js +23 -4
- package/dist/repo.js.map +1 -1
- package/dist/tasks.js +4 -4
- package/dist/tasks.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/values.d.ts +1 -1
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +39 -13
- package/dist/values.js.map +1 -1
- 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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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>;
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
15
|
-
export
|
|
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
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
|
83
|
-
deps: fingerprintSet(whole
|
|
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.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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: "
|
|
223
|
-
target:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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`);
|