lockwire 0.1.33 → 0.1.35

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 CHANGED
@@ -173,6 +173,33 @@ sequenceDiagram
173
173
 
174
174
  ## Install
175
175
 
176
+ **Have your AI do it.** Paste this into Claude Code, Codex, Cursor or any coding agent, in the repository you want to protect:
177
+
178
+ ```text
179
+ Set up lockwire (https://github.com/Reality-Ventures/lockwire) in this repository. It binds
180
+ claims in our docs to the code they describe and flags them when the code changes. It's an npm
181
+ package with one runtime dependency and makes no network calls.
182
+
183
+ 1. Install it for the agent you are:
184
+ - Claude Code: add this to .claude/settings.json (merge with what's there), then tell me to
185
+ run /reload-plugins:
186
+ {"extraKnownMarketplaces": {"lockwire": {"source": {"source": "github",
187
+ "repo": "Reality-Ventures/lockwire"}, "autoUpdate": true}},
188
+ "enabledPlugins": {"lockwire@lockwire": true}}
189
+ - Anything else: run `npx skills add Reality-Ventures/lockwire`, and register
190
+ `npx -y lockwire mcp` as a stdio MCP server if you support MCP.
191
+ 2. Run `npx lockwire init` at the repository root.
192
+ 3. In CLAUDE.md, AGENTS.md or README.md, find 3 to 5 sentences that state something specific about
193
+ our code (a function's parameters, a default, a limit, where something lives). Above each, add a
194
+ marker naming the code and what the sentence depends on, e.g.
195
+ <!-- lockwire src/auth/session.ts#createSession sig -->
196
+ (sig: signature and types; body: behaviour; deps: what it calls; path: where it lives).
197
+ 4. Run `npx lockwire link <doc>` for each doc you changed, then `npx lockwire check`, and show me
198
+ the result.
199
+
200
+ Don't commit, push or run `lockwire autofix`; I'll review the changes first.
201
+ ```
202
+
176
203
  **Claude Code** — hook, MCP server, and skill in one plugin:
177
204
 
178
205
  ```bash
@@ -0,0 +1,9 @@
1
+ /**
2
+ * BLAKE3, the unkeyed hash, written out so lockwire needs no hashing dependency (Node's crypto has no
3
+ * BLAKE3). Lockfiles written by earlier versions hold BLAKE3 fingerprints, so the hash has to stay
4
+ * the same one; the tests check this against the reference implementation (`@noble/hashes`, a
5
+ * development dependency) over every input length around the chunk and tree boundaries.
6
+ */
7
+ /** The first `length` bytes (at most 32) of the BLAKE3 hash of `input`, as lowercase hex. */
8
+ export declare function blake3Hex(input: Uint8Array, length?: number): string;
9
+ //# sourceMappingURL=blake3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blake3.d.ts","sourceRoot":"","sources":["../src/blake3.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA8HH,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,SAAK,GAAG,MAAM,CAkBhE"}
package/dist/blake3.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * BLAKE3, the unkeyed hash, written out so lockwire needs no hashing dependency (Node's crypto has no
3
+ * BLAKE3). Lockfiles written by earlier versions hold BLAKE3 fingerprints, so the hash has to stay
4
+ * the same one; the tests check this against the reference implementation (`@noble/hashes`, a
5
+ * development dependency) over every input length around the chunk and tree boundaries.
6
+ */
7
+ const IV = [
8
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
9
+ ];
10
+ const PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
11
+ const CHUNK_START = 1;
12
+ const CHUNK_END = 2;
13
+ const PARENT = 4;
14
+ const ROOT = 8;
15
+ const BLOCK_LEN = 64;
16
+ const CHUNK_LEN = 1024;
17
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
18
+ // Scratch space reused by every compression, so hashing allocates nothing per block.
19
+ const state = new Uint32Array(16);
20
+ const msg = new Uint32Array(16);
21
+ const next = new Uint32Array(16);
22
+ function g(a, b, c, d, mx, my) {
23
+ state[a] = state[a] + state[b] + mx;
24
+ state[d] = rotr(state[d] ^ state[a], 16);
25
+ state[c] = state[c] + state[d];
26
+ state[b] = rotr(state[b] ^ state[c], 12);
27
+ state[a] = state[a] + state[b] + my;
28
+ state[d] = rotr(state[d] ^ state[a], 8);
29
+ state[c] = state[c] + state[d];
30
+ state[b] = rotr(state[b] ^ state[c], 7);
31
+ }
32
+ /**
33
+ * The compression function: an 8-word chaining value and a 16-word block in, 16 words out. The result
34
+ * is `state`, which the next call overwrites: read what you need before compressing again.
35
+ */
36
+ function compress(cv, block, counter, blockLen, flags) {
37
+ for (let i = 0; i < 8; i++)
38
+ state[i] = cv[i];
39
+ state[8] = IV[0];
40
+ state[9] = IV[1];
41
+ state[10] = IV[2];
42
+ state[11] = IV[3];
43
+ state[12] = counter >>> 0;
44
+ state[13] = Math.floor(counter / 0x100000000) >>> 0;
45
+ state[14] = blockLen;
46
+ state[15] = flags;
47
+ for (let i = 0; i < 16; i++)
48
+ msg[i] = block[i];
49
+ for (let round = 0; round < 7; round++) {
50
+ g(0, 4, 8, 12, msg[0], msg[1]);
51
+ g(1, 5, 9, 13, msg[2], msg[3]);
52
+ g(2, 6, 10, 14, msg[4], msg[5]);
53
+ g(3, 7, 11, 15, msg[6], msg[7]);
54
+ g(0, 5, 10, 15, msg[8], msg[9]);
55
+ g(1, 6, 11, 12, msg[10], msg[11]);
56
+ g(2, 7, 8, 13, msg[12], msg[13]);
57
+ g(3, 4, 9, 14, msg[14], msg[15]);
58
+ if (round < 6) {
59
+ for (let i = 0; i < 16; i++)
60
+ next[i] = msg[PERMUTATION[i]];
61
+ msg.set(next);
62
+ }
63
+ }
64
+ for (let i = 0; i < 8; i++) {
65
+ state[i] = state[i] ^ state[i + 8];
66
+ state[i + 8] = state[i + 8] ^ cv[i];
67
+ }
68
+ return state;
69
+ }
70
+ const chainingValue = (o) => Array.from(compress(o.cv, o.block, o.counter, o.blockLen, o.flags).subarray(0, 8));
71
+ const words = (bytes, start, length) => {
72
+ const block = new Uint32Array(16);
73
+ const view = new DataView(bytes.buffer, bytes.byteOffset + start, length);
74
+ for (let i = 0; i < length >> 2; i++)
75
+ block[i] = view.getUint32(i * 4, true);
76
+ // A final partial word is zero-padded, little-endian.
77
+ for (let i = length & ~3; i < length; i++)
78
+ block[i >> 2] = block[i >> 2] | (view.getUint8(i) << (8 * (i & 3)));
79
+ return block;
80
+ };
81
+ /** The output of the chunk starting at `start`, whose last block is left for the caller to finish. */
82
+ function chunkOutput(input, start, index) {
83
+ const length = Math.min(CHUNK_LEN, input.length - start);
84
+ const blocks = Math.max(1, Math.ceil(length / BLOCK_LEN));
85
+ let cv = IV;
86
+ for (let b = 0; b < blocks - 1; b++) {
87
+ const block = words(input, start + b * BLOCK_LEN, BLOCK_LEN);
88
+ cv = Array.from(compress(cv, block, index, BLOCK_LEN, b === 0 ? CHUNK_START : 0).subarray(0, 8));
89
+ }
90
+ const last = (blocks - 1) * BLOCK_LEN;
91
+ const blockLen = length - last;
92
+ return {
93
+ cv,
94
+ block: words(input, start + last, blockLen),
95
+ counter: index,
96
+ blockLen,
97
+ flags: (blocks === 1 ? CHUNK_START : 0) | CHUNK_END,
98
+ };
99
+ }
100
+ const parentOutput = (left, right) => ({
101
+ cv: IV,
102
+ block: Uint32Array.from([...left, ...right]),
103
+ counter: 0,
104
+ blockLen: BLOCK_LEN,
105
+ flags: PARENT,
106
+ });
107
+ /** The first `length` bytes (at most 32) of the BLAKE3 hash of `input`, as lowercase hex. */
108
+ export function blake3Hex(input, length = 32) {
109
+ const chunks = Math.max(1, Math.ceil(input.length / CHUNK_LEN));
110
+ // Completed subtrees, merged as soon as two of equal size exist.
111
+ const stack = [];
112
+ for (let i = 0; i < chunks - 1; i++) {
113
+ let cv = chainingValue(chunkOutput(input, i * CHUNK_LEN, i));
114
+ for (let done = i + 1; (done & 1) === 0; done >>= 1)
115
+ cv = chainingValue(parentOutput(stack.pop(), cv));
116
+ stack.push(cv);
117
+ }
118
+ let out = chunkOutput(input, (chunks - 1) * CHUNK_LEN, chunks - 1);
119
+ while (stack.length > 0)
120
+ out = parentOutput(stack.pop(), chainingValue(out));
121
+ const digest = compress(out.cv, out.block, 0, out.blockLen, out.flags | ROOT);
122
+ let hex = "";
123
+ for (let i = 0; i < Math.ceil(length / 4); i++)
124
+ for (let b = 0; b < 4; b++)
125
+ hex += ((digest[i] >>> (8 * b)) & 0xff).toString(16).padStart(2, "0");
126
+ return hex.slice(0, length * 2);
127
+ }
128
+ //# sourceMappingURL=blake3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blake3.js","sourceRoot":"","sources":["../src/blake3.ts"],"names":[],"mappings":"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"}
@@ -19,15 +19,25 @@
19
19
  * isn't part of the body.
20
20
  * - `deps` names each call by its structure (`client.get().json`), so reformatting a call chain or
21
21
  * renaming a local passed as an argument doesn't move it.
22
+ *
23
+ * Version 3 is version 2 except that what code formatters (Black, Ruff, Prettier) rewrite without
24
+ * changing behaviour reads the same either way, in `sig` and `body` alike: a string's quotes, escaped
25
+ * quotes and prefix case (`'it\'s'`, `"it's"`, `U'a'`); a number's spelling (`0XAB`, `1E5`, and in
26
+ * TS/JS `1.50`, `.5`); Python's optional parentheses (`from m import (a)`, `with (a, b):`,
27
+ * `for (x, y) in`, `return (a, b)`, `-> (int)`); an arrow's single parameter with or without
28
+ * parentheses; and a quoted key that needn't be (`{ 'a': 1 }`). It also stops reading a class's or a
29
+ * whole file's own top-level names (a model's fields, a module's constants) as locals: renaming one
30
+ * is a change (see {@link collectLocals}). Version-2 anchors keep being read the version-2 way (see
31
+ * {@link withVersion}) until a check finds them unchanged and re-stamps them.
22
32
  */
23
33
  import type { Node } from "web-tree-sitter";
24
34
  import type { LangId } from "./grammar.js";
25
35
  import type { FileSymbols } from "./types.js";
26
36
  /** A node as its tokens: comments and layout dropped, a trailing comma before a closer dropped. */
27
- export declare function canon(node: Node): string;
28
- export declare function extractFileSymbolsV2(root: Node, lang: LangId, normalizeLocals: boolean): FileSymbols;
29
- /** Whole-file `body` and `deps` for a file-only anchor, read the version-2 way. */
30
- export declare function computeWholeFileTiersV2(root: Node, lang: LangId, normalizeLocals: boolean): {
37
+ export declare function canon(node: Node, lang?: LangId): string;
38
+ export declare function extractFileSymbolsV2(root: Node, lang: LangId, normalizeLocals: boolean, version?: number): FileSymbols;
39
+ /** Whole-file `body` and `deps` for a file-only anchor, read the version-2 (or 3) way. */
40
+ export declare function computeWholeFileTiersV2(root: Node, lang: LangId, normalizeLocals: boolean, version?: number): {
31
41
  bodyNormalized: string;
32
42
  deps: string[];
33
43
  };
@@ -1 +1 @@
1
- {"version":3,"file":"extract-v2.d.ts","sourceRoot":"","sources":["../src/extract-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,YAAY,CAAC;AAE9D,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAiBxC;AA2bD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,GACvB,WAAW,CAqPb;AAED,mFAAmF;AACnF,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,GACvB;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAO5C"}
1
+ {"version":3,"file":"extract-v2.d.ts","sourceRoot":"","sources":["../src/extract-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,YAAY,CAAC;AA+G9D,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAoCvD;AA2eD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,EACxB,OAAO,SAAI,GACV,WAAW,CAEb;AAyPD,0FAA0F;AAC1F,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,EACxB,OAAO,SAAI,GACV;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAS5C"}
@@ -1,17 +1,149 @@
1
+ /**
2
+ * Whether the extraction under way reads formatting the version-3 way. Set for the length of one
3
+ * (synchronous) extraction by {@link withVersion}, so the helpers below needn't all carry it.
4
+ */
5
+ let formatProof = false;
6
+ function withVersion(version, run) {
7
+ const saved = formatProof;
8
+ formatProof = version >= 3;
9
+ try {
10
+ return run();
11
+ }
12
+ finally {
13
+ formatProof = saved;
14
+ }
15
+ }
16
+ /** `\'` and `\"` as the quote alone; every other escape as written. */
17
+ const unescapeQuotes = (s) => s.replace(/\\([\s\S])/g, (pair, c) => (c === "'" || c === '"' ? c : pair));
18
+ /**
19
+ * A string literal whichever quotes delimit it: its prefix (lower case, sorted, without Python 3's
20
+ * no-op `u`) and its contents with escaped quotes unescaped. `inner` renders an f-string's
21
+ * interpolations. Null for anything but a plain or f-string literal (a template literal keeps its
22
+ * backticks: there's no other way to write one).
23
+ */
24
+ function stringLiteral(node, lang, inner) {
25
+ // Named: TS's `string` keyword is an unnamed node of the same type.
26
+ if (node.type !== "string" || !node.isNamed)
27
+ return null;
28
+ if (lang !== "python")
29
+ return JSON.stringify(unescapeQuotes(node.text.slice(1, -1)));
30
+ let prefix = "";
31
+ let quote = 1;
32
+ let contents = "";
33
+ const text = (t) => {
34
+ contents += prefix.includes("r") ? t : unescapeQuotes(t);
35
+ };
36
+ for (const c of node.children) {
37
+ if (!c || c.isExtra)
38
+ continue;
39
+ if (c.type === "string_start") {
40
+ const delimiter = c.text.replace(/^[^'"]*/, "");
41
+ quote = delimiter.length;
42
+ prefix = [...c.text.slice(0, -quote).toLowerCase().replace("u", "")].sort().join("");
43
+ }
44
+ // An f-string's own braces are written doubled (`{{`), so an interpolation can't be mistaken for text.
45
+ else if (c.type === "interpolation")
46
+ contents += inner(c);
47
+ // The grammar ends `r'\''` with a `string_end` of `\''`: what precedes the closing quote is text.
48
+ else if (c.type === "string_end")
49
+ text(c.text.slice(0, -quote));
50
+ else
51
+ text(c.text);
52
+ }
53
+ return `${prefix}${JSON.stringify(contents)}`;
54
+ }
55
+ /** A number literal however it's spelled: case, `_` separators and, in TS/JS, the value it denotes. */
56
+ function numberLiteral(text, lang) {
57
+ const t = text.toLowerCase().replace(/_/g, "");
58
+ // Python keeps the spelling (`1` and `1.0` are different types); so do BigInts and legacy octals.
59
+ if (lang === "python" || t.endsWith("n") || /^0\d/.test(t))
60
+ return t;
61
+ const value = Number(t);
62
+ return Number.isFinite(value) ? String(value) : t;
63
+ }
64
+ const NUMBER_TYPES = new Set(["number", "integer", "float"]);
65
+ /** The node types version 3 reads differently from version 2 (bar optional parentheses). */
66
+ const FORMATTED_TYPES = new Set(["string", ...NUMBER_TYPES, "formal_parameters"]);
67
+ /** Members whose name may be quoted or not, as long as it's an identifier either way. */
68
+ const NAMED_MEMBERS = new Set([
69
+ "pair",
70
+ "property_signature",
71
+ "method_signature",
72
+ "public_field_definition",
73
+ "field_definition",
74
+ "method_definition",
75
+ "abstract_method_signature",
76
+ ]);
77
+ /** A quoted member name that needn't be (`{ 'a': 1 }`): the bare name. */
78
+ function unquotedKey(node, lang) {
79
+ if (lang === "python" || node.type !== "string" || !node.isNamed)
80
+ return null;
81
+ const parent = node.parent;
82
+ if (!parent || !NAMED_MEMBERS.has(parent.type))
83
+ return null;
84
+ const field = ["key", "name", "property"]
85
+ .map((f) => parent.childForFieldName(f))
86
+ .find((c) => c?.id === node.id);
87
+ if (!field)
88
+ return null;
89
+ const name = unescapeQuotes(node.text.slice(1, -1));
90
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : null;
91
+ }
92
+ /** Python's optional parentheses: around an import list, a `with` clause, or a tuple. */
93
+ const OPTIONAL_PARENS = new Set(["import_from_statement", "with_clause", "tuple", "tuple_pattern"]);
94
+ /** A parenthesized tuple is the same tuple without the parentheses. */
95
+ const TUPLE_AS = new Map([
96
+ ["tuple", "expression_list"],
97
+ ["tuple_pattern", "pattern_list"],
98
+ ]);
99
+ /** An arrow function's one plain parameter, when it's written in parentheses (`(x) => x`). */
100
+ function soleArrowParameter(node) {
101
+ if (node.type !== "formal_parameters" || node.parent?.type !== "arrow_function")
102
+ return null;
103
+ const params = node.namedChildren.filter((c) => c !== null && !c.isExtra);
104
+ if (params.length !== 1)
105
+ return null;
106
+ const p = params[0];
107
+ if (p.type === "identifier")
108
+ return p;
109
+ const named = p.namedChildren.filter((c) => c !== null && !c.isExtra);
110
+ return p.type === "required_parameter" && named.length === 1 && named[0]?.type === "identifier"
111
+ ? named[0]
112
+ : null;
113
+ }
1
114
  /** A node as its tokens: comments and layout dropped, a trailing comma before a closer dropped. */
2
- export function canon(node) {
115
+ export function canon(node, lang) {
3
116
  const tokens = [];
4
117
  const walk = (n) => {
5
118
  if (n.isExtra)
6
119
  return; // comments
120
+ if (formatProof && lang) {
121
+ const key = unquotedKey(n, lang);
122
+ const literal = key ?? stringLiteral(n, lang, (i) => canon(i, lang));
123
+ if (literal !== null) {
124
+ tokens.push(literal);
125
+ return;
126
+ }
127
+ if (n.isNamed && NUMBER_TYPES.has(n.type)) {
128
+ tokens.push(numberLiteral(n.text, lang));
129
+ return;
130
+ }
131
+ if (n.type === "parenthesized_expression" && n.namedChildCount === 1) {
132
+ const inner = n.namedChildren[0];
133
+ if (inner && !inner.isExtra)
134
+ return walk(inner);
135
+ }
136
+ // (An arrow's parameters need nothing here: sigOf writes a bare one in parentheses.)
137
+ }
7
138
  if (n.childCount === 0) {
8
139
  const t = n.text.trim();
9
140
  if (t)
10
141
  tokens.push(t);
11
142
  return;
12
143
  }
144
+ const dropParens = formatProof && lang === "python" && OPTIONAL_PARENS.has(n.type);
13
145
  for (const c of n.children)
14
- if (c)
146
+ if (c && !(dropParens && (c.type === "(" || c.type === ")")))
15
147
  walk(c);
16
148
  };
17
149
  walk(node);
@@ -101,20 +233,20 @@ function isExported(node) {
101
233
  return false;
102
234
  }
103
235
  /** Decorators written before a declaration: Python's `decorated_definition`, TS's on the class or export, or class-body siblings for a member. */
104
- function decoratorsOf(def, member) {
236
+ function decoratorsOf(def, member, lang) {
105
237
  const out = [];
106
238
  const parent = member.parent;
107
239
  if (parent?.type === "decorated_definition" || parent?.type === "export_statement")
108
240
  for (const c of parent.namedChildren)
109
241
  if (c?.type === "decorator")
110
- out.push(canon(c));
242
+ out.push(canon(c, lang));
111
243
  for (const c of def.namedChildren)
112
244
  if (c?.type === "decorator")
113
- out.push(canon(c));
245
+ out.push(canon(c, lang));
114
246
  // A TS class member's decorators are the class body's children right before it.
115
247
  if (parent?.type === "class_body")
116
248
  for (let s = member.previousSibling; s?.type === "decorator"; s = s.previousSibling)
117
- out.push(canon(s));
249
+ out.push(canon(s, lang));
118
250
  return out;
119
251
  }
120
252
  /** Keyword modifiers written before the name: `static`, `async`, `get`, `private`, `readonly`, `*`, `abstract`… */
@@ -165,23 +297,23 @@ function sigOf(def, name, lang) {
165
297
  const nameNode = n.childForFieldName("name");
166
298
  const body = [...n.children]
167
299
  .filter((c) => c !== null && c !== nameNode && !c.isExtra && c.type !== "decorator")
168
- .map(canon)
300
+ .map((c) => canon(c, lang))
169
301
  .join(" ");
170
302
  return `${n.type} ${name} ${body}${tail}`;
171
303
  }
172
304
  const typeParams = n.childForFieldName("type_parameters");
173
- const tp = typeParams ? canon(typeParams) : "";
305
+ const tp = typeParams ? canon(typeParams, lang) : "";
174
306
  if (def.kind === "class") {
175
307
  const heritage = lang === "python"
176
308
  ? n.childForFieldName("superclasses")
177
309
  : (n.namedChildren.find((c) => c?.type === "class_heritage") ?? null);
178
- return `class ${name}${tp}${heritage ? ` ${canon(heritage)}` : ""}${tail}`;
310
+ return `class ${name}${tp}${heritage ? ` ${canon(heritage, lang)}` : ""}${tail}`;
179
311
  }
180
312
  const params = n.childForFieldName("parameters");
181
313
  const single = lang === "python" ? null : n.childForFieldName("parameter");
182
- const ps = params ? canon(params) : single ? `(${canon(single)})` : "()";
314
+ const ps = params ? canon(params, lang) : single ? `(${canon(single, lang)})` : "()";
183
315
  const ret = n.childForFieldName("return_type");
184
- const rs = ret ? canon(ret).replace(/^:\s*/, "") : "";
316
+ const rs = ret ? canon(ret, lang).replace(/^:\s*/, "") : "";
185
317
  return `fn ${name}${tp}${ps}${rs ? `->${rs}` : ""}${tail}`;
186
318
  }
187
319
  // -------------------------------------------------------------------------------------------------
@@ -237,25 +369,52 @@ function paramNames(params, lang, out) {
237
369
  }
238
370
  }
239
371
  }
240
- function collectLocals(subtree, outerParams, lang) {
372
+ /**
373
+ * The names `subtree` binds that are local to it, aliased in `body`. For a class or a whole file
374
+ * (`ownNames`), from version 3 the names bound at its own top level, outside any function in it, aren't
375
+ * locals: they are its fields, attributes, constants and exports, and renaming one changes what other
376
+ * code can use (`montly_fee: float` → `monthly_fee: float` renames a model's field and its JSON key).
377
+ */
378
+ function collectLocals(subtree, outerParams, lang, ownNames = false) {
241
379
  const locals = new Set();
380
+ const own = new Set();
381
+ const keepOwn = ownNames && formatProof;
382
+ /** Records the names `pattern` binds at `site`: the scope's own when no function lies between. */
383
+ const bind = (site, pattern) => {
384
+ if (!site)
385
+ return;
386
+ const names = new Set();
387
+ patternNames(pattern, names);
388
+ let inFunction = false;
389
+ if (keepOwn)
390
+ for (let p = site.parent; p && p.id !== subtree.id; p = p.parent)
391
+ if (FUNCTION_LIKE[lang].has(p.type)) {
392
+ inFunction = true;
393
+ break;
394
+ }
395
+ for (const name of names)
396
+ (keepOwn && !inFunction ? own : locals).add(name);
397
+ };
242
398
  paramNames(outerParams, lang, locals);
243
399
  if (lang === "python") {
244
400
  for (const fn of subtree.descendantsOfType(["function_definition", "lambda"]))
245
401
  paramNames(fn?.childForFieldName("parameters") ?? null, lang, locals);
246
402
  for (const n of subtree.descendantsOfType(["assignment", "augmented_assignment"]))
247
- patternNames(n?.childForFieldName("left") ?? null, locals);
248
- for (const n of subtree.descendantsOfType(["for_statement", "for_in_clause"]))
403
+ bind(n, n?.childForFieldName("left") ?? null);
404
+ for (const n of subtree.descendantsOfType(["for_statement"]))
405
+ bind(n, n?.childForFieldName("left") ?? null);
406
+ // A comprehension is a scope of its own, wherever it is.
407
+ for (const n of subtree.descendantsOfType(["for_in_clause"]))
249
408
  patternNames(n?.childForFieldName("left") ?? null, locals);
250
409
  for (const n of subtree.descendantsOfType(["as_pattern_target"]))
251
410
  for (const c of n?.namedChildren ?? [])
252
- patternNames(c, locals);
411
+ bind(n, c);
253
412
  for (const n of subtree.descendantsOfType(["named_expression"]))
254
- patternNames(n?.childForFieldName("name") ?? null, locals);
413
+ bind(n, n?.childForFieldName("name") ?? null);
255
414
  }
256
415
  else {
257
416
  for (const n of subtree.descendantsOfType(["variable_declarator"]))
258
- patternNames(n?.childForFieldName("name") ?? null, locals);
417
+ bind(n, n?.childForFieldName("name") ?? null);
259
418
  for (const n of subtree.descendantsOfType([...FUNCTION_LIKE[lang]])) {
260
419
  paramNames(n?.childForFieldName("parameters") ?? null, lang, locals);
261
420
  const single = n?.childForFieldName("parameter");
@@ -263,10 +422,13 @@ function collectLocals(subtree, outerParams, lang) {
263
422
  patternNames(single, locals);
264
423
  }
265
424
  for (const n of subtree.descendantsOfType(["for_in_statement"]))
266
- patternNames(n?.childForFieldName("left") ?? null, locals);
425
+ bind(n, n?.childForFieldName("left") ?? null);
267
426
  for (const n of subtree.descendantsOfType(["catch_clause"]))
268
- patternNames(n?.childForFieldName("parameter") ?? null, locals);
427
+ bind(n, n?.childForFieldName("parameter") ?? null);
269
428
  }
429
+ // A name that is also the scope's own is read as written everywhere, so the two never get confused.
430
+ for (const name of own)
431
+ locals.delete(name);
270
432
  return locals;
271
433
  }
272
434
  /** An identifier that names a value, not a property, attribute or keyword argument. */
@@ -287,13 +449,28 @@ function isValueIdentifier(node, lang) {
287
449
  function serializeBody(node, locals, normalizeLocals, lang, aliasOf) {
288
450
  if (node.isExtra)
289
451
  return "";
452
+ // Read once: each read of a node's type is a call into the parser.
453
+ const nodeType = node.type;
290
454
  // Grouping parentheses carry no meaning (the tree's shape already has the precedence), and
291
455
  // formatters add and remove them to wrap lines.
292
- if (node.type === "parenthesized_expression" && node.namedChildCount === 1) {
456
+ if (nodeType === "parenthesized_expression" && node.namedChildCount === 1) {
293
457
  const inner = node.namedChildren[0];
294
458
  if (inner && !inner.isExtra)
295
459
  return serializeBody(inner, locals, normalizeLocals, lang, aliasOf);
296
460
  }
461
+ if (formatProof && FORMATTED_TYPES.has(nodeType)) {
462
+ const key = unquotedKey(node, lang);
463
+ if (key !== null)
464
+ return `property_identifier:${key}`;
465
+ const literal = stringLiteral(node, lang, (i) => serializeBody(i, locals, normalizeLocals, lang, aliasOf));
466
+ if (literal !== null)
467
+ return `string:${literal}`;
468
+ if (node.isNamed && NUMBER_TYPES.has(nodeType))
469
+ return `${nodeType}:${numberLiteral(node.text, lang)}`;
470
+ const sole = soleArrowParameter(node);
471
+ if (sole)
472
+ return serializeBody(sole, locals, normalizeLocals, lang, aliasOf);
473
+ }
297
474
  if (node.childCount === 0) {
298
475
  if (node.isNamed) {
299
476
  if (normalizeLocals && locals.has(node.text) && isValueIdentifier(node, lang)) {
@@ -304,21 +481,23 @@ function serializeBody(node, locals, normalizeLocals, lang, aliasOf) {
304
481
  }
305
482
  return `$${idx}`;
306
483
  }
307
- return `${node.type}:${node.text}`;
484
+ return `${nodeType}:${node.text}`;
308
485
  }
309
- if (node.type === "," || node.type === ";")
486
+ if (nodeType === "," || nodeType === ";")
310
487
  return "";
311
- return node.type;
488
+ return nodeType;
312
489
  }
490
+ const dropParens = formatProof && lang === "python" && OPTIONAL_PARENS.has(nodeType);
313
491
  const parts = [];
314
492
  for (const child of node.children) {
315
- if (!child)
493
+ if (!child || (dropParens && (child.type === "(" || child.type === ")")))
316
494
  continue;
317
495
  const s = serializeBody(child, locals, normalizeLocals, lang, aliasOf);
318
496
  if (s)
319
497
  parts.push(s);
320
498
  }
321
- return `(${node.type} ${parts.join(" ")})`;
499
+ const type = (formatProof && lang === "python" && TUPLE_AS.get(nodeType)) || nodeType;
500
+ return `(${type} ${parts.join(" ")})`;
322
501
  }
323
502
  /** A Python function's docstring is documentation, not behaviour. */
324
503
  function withoutDocstring(body, lang) {
@@ -450,7 +629,10 @@ function commonJsPath(left) {
450
629
  const path = parts.filter((p) => p !== "prototype").join(".");
451
630
  return path ? { path, exported } : null;
452
631
  }
453
- export function extractFileSymbolsV2(root, lang, normalizeLocals) {
632
+ export function extractFileSymbolsV2(root, lang, normalizeLocals, version = 2) {
633
+ return withVersion(version, () => extractSymbols(root, lang, normalizeLocals));
634
+ }
635
+ function extractSymbols(root, lang, normalizeLocals) {
454
636
  const importedNames = collectImportedNames(root, lang);
455
637
  const bySymbolPath = new Map();
456
638
  const fileExports = [];
@@ -467,7 +649,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
467
649
  ? withoutDocstring(body, lang)
468
650
  : [body];
469
651
  const locals = def.kind === "class"
470
- ? collectLocals(body ?? n, null, lang)
652
+ ? collectLocals(body ?? n, null, lang, true)
471
653
  : collectLocals(body ?? n, n.childForFieldName("parameters") ?? n.childForFieldName("parameter"), lang);
472
654
  const aliasOf = new Map();
473
655
  const bodyNormalized = roots
@@ -565,7 +747,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
565
747
  outer: prefix ? cls : outerOf(cls),
566
748
  exported: prefix ? false : isExported(cls),
567
749
  extra: [],
568
- decorators: decoratorsOf(cls, cls),
750
+ decorators: decoratorsOf(cls, cls, lang),
569
751
  });
570
752
  if (isType)
571
753
  return;
@@ -594,7 +776,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
594
776
  outer: member.parent?.type === "decorated_definition" ? member.parent : member,
595
777
  exported: false,
596
778
  extra: [],
597
- decorators: decoratorsOf(member, member),
779
+ decorators: decoratorsOf(member, member, lang),
598
780
  });
599
781
  }
600
782
  else if (member.type === "public_field_definition" || member.type === "field_definition") {
@@ -610,7 +792,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
610
792
  outer: member,
611
793
  exported: false,
612
794
  extra: [...leadingModifiers(member), ...fn.wrappers.map((w) => `wrap:${w}`)],
613
- decorators: decoratorsOf(member, member),
795
+ decorators: decoratorsOf(member, member, lang),
614
796
  });
615
797
  }
616
798
  }
@@ -629,7 +811,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
629
811
  outer: outerOf(node),
630
812
  exported: lang === "python" ? false : isExported(node),
631
813
  extra: [],
632
- decorators: decoratorsOf(node, node),
814
+ decorators: decoratorsOf(node, node, lang),
633
815
  });
634
816
  }
635
817
  else if (CLASS_LIKE.has(node.type)) {
@@ -664,7 +846,7 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
664
846
  outer: outerOf(node),
665
847
  exported: isExported(node),
666
848
  extra: [
667
- ...(type ? [`:${canon(type).replace(/^:\s*/, "")}`] : []),
849
+ ...(type ? [`:${canon(type, lang).replace(/^:\s*/, "")}`] : []),
668
850
  ...fn.wrappers.map((w) => `wrap:${w}`),
669
851
  ],
670
852
  decorators: [],
@@ -693,13 +875,15 @@ export function extractFileSymbolsV2(root, lang, normalizeLocals) {
693
875
  }
694
876
  return { bySymbolPath, fileExports };
695
877
  }
696
- /** Whole-file `body` and `deps` for a file-only anchor, read the version-2 way. */
697
- export function computeWholeFileTiersV2(root, lang, normalizeLocals) {
698
- const importedNames = collectImportedNames(root, lang);
699
- const locals = collectLocals(root, null, lang);
700
- return {
701
- bodyNormalized: serializeBody(root, locals, normalizeLocals, lang, new Map()),
702
- deps: collectDeps([root], lang, importedNames, locals),
703
- };
878
+ /** Whole-file `body` and `deps` for a file-only anchor, read the version-2 (or 3) way. */
879
+ export function computeWholeFileTiersV2(root, lang, normalizeLocals, version = 2) {
880
+ return withVersion(version, () => {
881
+ const importedNames = collectImportedNames(root, lang);
882
+ const locals = collectLocals(root, null, lang, true);
883
+ return {
884
+ bodyNormalized: serializeBody(root, locals, normalizeLocals, lang, new Map()),
885
+ deps: collectDeps([root], lang, importedNames, locals),
886
+ };
887
+ });
704
888
  }
705
889
  //# sourceMappingURL=extract-v2.js.map