weavatrix 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -63
- package/SECURITY.md +25 -8
- package/package.json +2 -2
- package/skill/SKILL.md +108 -39
- package/src/analysis/architecture-contract.js +343 -0
- package/src/analysis/audit-debt.js +94 -0
- package/src/analysis/change-classification.js +509 -0
- package/src/analysis/cycle-route.js +14 -0
- package/src/analysis/dead-check.js +13 -6
- package/src/analysis/dead-code-review.js +245 -0
- package/src/analysis/dep-check-ecosystems.js +163 -0
- package/src/analysis/dep-check.js +69 -147
- package/src/analysis/dep-rules.js +47 -31
- package/src/analysis/duplicates.compute.js +47 -7
- package/src/analysis/endpoints-java.js +186 -0
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +18 -5
- package/src/analysis/findings.js +8 -1
- package/src/analysis/git-history.js +549 -0
- package/src/analysis/git-ref-graph.js +74 -0
- package/src/analysis/graph-analysis.aggregate.js +29 -28
- package/src/analysis/graph-analysis.edges.js +24 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/http-contracts.js +581 -0
- package/src/analysis/internal-audit.collect.js +43 -2
- package/src/analysis/internal-audit.reach.js +52 -1
- package/src/analysis/internal-audit.run.js +66 -13
- package/src/analysis/java-source.js +36 -0
- package/src/analysis/package-reachability.js +39 -0
- package/src/analysis/static-test-reachability.js +133 -0
- package/src/build-graph.js +72 -13
- package/src/graph/build-worker.js +3 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-js.js +71 -12
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +27 -0
- package/src/graph/file-lock.js +69 -0
- package/src/graph/freshness-probe.js +141 -0
- package/src/graph/graph-filter.js +28 -11
- package/src/graph/incremental-refresh.js +232 -0
- package/src/graph/internal-builder.barrels.js +169 -0
- package/src/graph/internal-builder.build.js +111 -16
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.langs.js +2 -1
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/layout.js +21 -5
- package/src/graph/relations.js +4 -0
- package/src/graph/repo-registry.js +124 -0
- package/src/mcp/catalog.mjs +64 -21
- package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
- package/src/mcp/evidence-snapshot.common.mjs +160 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
- package/src/mcp/evidence-snapshot.health.mjs +95 -0
- package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
- package/src/mcp/evidence-snapshot.mjs +50 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
- package/src/mcp/evidence-snapshot.structure.mjs +81 -0
- package/src/mcp/graph-context.mjs +106 -181
- package/src/mcp/graph-diff.mjs +217 -0
- package/src/mcp/staleness-notice.mjs +20 -0
- package/src/mcp/sync-evidence.mjs +418 -0
- package/src/mcp/sync-payload.mjs +194 -8
- package/src/mcp/tool-result.mjs +51 -0
- package/src/mcp/tools-actions.mjs +114 -33
- package/src/mcp/tools-architecture.mjs +144 -0
- package/src/mcp/tools-company.mjs +273 -0
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +36 -68
- package/src/mcp/tools-health.mjs +354 -20
- package/src/mcp/tools-history.mjs +22 -0
- package/src/mcp/tools-impact-change.mjs +261 -0
- package/src/mcp/tools-impact.mjs +35 -11
- package/src/mcp-server.mjs +100 -17
- package/src/mcp-source-tools.mjs +11 -3
- package/src/path-classification.js +201 -0
- package/src/path-ignore.js +69 -0
- package/src/security/typosquat.js +1 -1
|
@@ -1,29 +1,192 @@
|
|
|
1
|
-
// Java extractor.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
const
|
|
1
|
+
// Java extractor. Keeps file-level imports/calls while modelling the Java ownership and type system:
|
|
2
|
+
// classes/interfaces/enums/records own their methods, heritage distinguishes extends from implements,
|
|
3
|
+
// and project-local type references resolve to the declaration node (never to synthetic type-name nodes).
|
|
4
|
+
const TYPE_CORE = `
|
|
5
5
|
(class_declaration name: (identifier) @class)
|
|
6
|
-
(interface_declaration name: (identifier) @
|
|
7
|
-
(enum_declaration name: (identifier) @
|
|
6
|
+
(interface_declaration name: (identifier) @interface)
|
|
7
|
+
(enum_declaration name: (identifier) @enum)`;
|
|
8
|
+
// Keep grammar-version-dependent declarations separate: one unknown node type invalidates a whole query.
|
|
9
|
+
const TYPE_OPTIONAL = [
|
|
10
|
+
`(record_declaration name: (identifier) @record)`,
|
|
11
|
+
`(annotation_type_declaration name: (identifier) @annotation)`,
|
|
12
|
+
];
|
|
13
|
+
const MEMBERS = `
|
|
8
14
|
(method_declaration name: (identifier) @method)
|
|
9
|
-
(constructor_declaration name: (identifier) @
|
|
15
|
+
(constructor_declaration name: (identifier) @constructor)
|
|
10
16
|
(field_declaration declarator: (variable_declarator name: (identifier) @field))`;
|
|
11
17
|
|
|
18
|
+
const TYPE_DECLARATIONS = new Set([
|
|
19
|
+
"class_declaration", "interface_declaration", "enum_declaration",
|
|
20
|
+
"record_declaration", "annotation_type_declaration",
|
|
21
|
+
]);
|
|
22
|
+
const FIELD_DECLARATIONS = new Set(["field_declaration"]);
|
|
23
|
+
|
|
24
|
+
const ancestor = (node, accepted) => {
|
|
25
|
+
let current = node?.parent;
|
|
26
|
+
for (let hops = 0; current && hops < 12; hops++, current = current.parent) {
|
|
27
|
+
if (accepted.has(current.type)) return current;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const visibilityOf = (declaration, owner) => {
|
|
33
|
+
const modifiers = declaration?.namedChildren?.find((node) => node.type === "modifiers")?.text || "";
|
|
34
|
+
if (/\bprivate\b/.test(modifiers)) return "private";
|
|
35
|
+
if (/\bprotected\b/.test(modifiers)) return "protected";
|
|
36
|
+
if (/\bpublic\b/.test(modifiers)) return "public";
|
|
37
|
+
if (["interface_declaration", "annotation_type_declaration"].includes(owner?.type)) return "public";
|
|
38
|
+
return "package";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const lineOf = (node) => node.startPosition.row + 1;
|
|
42
|
+
const symbolBaseId = (fileRel, nameNode) => `${fileRel}#${nameNode.text}@${lineOf(nameNode)}`;
|
|
43
|
+
const declarationKey = (node) => `${node?.startIndex ?? -1}:${node?.endIndex ?? -1}`;
|
|
44
|
+
const exactJavaTarget = (resolveJavaImport, parts) => {
|
|
45
|
+
const target = resolveJavaImport(parts);
|
|
46
|
+
const suffix = parts.join("/") + ".java";
|
|
47
|
+
return target && (target === suffix || target.endsWith("/" + suffix)) ? target : null;
|
|
48
|
+
};
|
|
49
|
+
|
|
12
50
|
export default {
|
|
13
51
|
family: "java",
|
|
14
52
|
grammars: ["java"],
|
|
15
53
|
exts: { ".java": "java" },
|
|
16
54
|
isWeb: false,
|
|
17
55
|
calls: `(method_invocation name: (identifier) @callee)`,
|
|
18
|
-
|
|
56
|
+
// Capturing the base type_identifier (rather than the whole generic/scoped type) gives the shared
|
|
57
|
+
// resolver the imported/local declaration name. Each query is deliberately non-overlapping.
|
|
58
|
+
heritage: [
|
|
59
|
+
{ relation: "inherits", query: `(superclass (type_identifier) @super)` },
|
|
60
|
+
{ relation: "inherits", query: `(superclass (generic_type (type_identifier) @super))` },
|
|
61
|
+
{ relation: "inherits", query: `(superclass (scoped_type_identifier name: (type_identifier) @super))` },
|
|
62
|
+
{ relation: "inherits", query: `(superclass (generic_type (scoped_type_identifier name: (type_identifier) @super)))` },
|
|
63
|
+
{ relation: "inherits", query: `(extends_interfaces (type_list (type_identifier) @super))` },
|
|
64
|
+
{ relation: "inherits", query: `(extends_interfaces (type_list (generic_type (type_identifier) @super)))` },
|
|
65
|
+
{ relation: "inherits", query: `(extends_interfaces (type_list (scoped_type_identifier name: (type_identifier) @super)))` },
|
|
66
|
+
{ relation: "inherits", query: `(extends_interfaces (type_list (generic_type (scoped_type_identifier name: (type_identifier) @super))))` },
|
|
67
|
+
{ relation: "implements", query: `(super_interfaces (type_list (type_identifier) @super))` },
|
|
68
|
+
{ relation: "implements", query: `(super_interfaces (type_list (generic_type (type_identifier) @super)))` },
|
|
69
|
+
{ relation: "implements", query: `(super_interfaces (type_list (scoped_type_identifier name: (type_identifier) @super)))` },
|
|
70
|
+
{ relation: "implements", query: `(super_interfaces (type_list (generic_type (scoped_type_identifier name: (type_identifier) @super))))` },
|
|
71
|
+
],
|
|
19
72
|
|
|
20
73
|
pass1(ctx) {
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
74
|
+
const {
|
|
75
|
+
grammar, tree, fileRel, caps, field, addSym, addImportEdge, imports,
|
|
76
|
+
resolveJavaImport, fileSet, links, nodeIds,
|
|
77
|
+
} = ctx;
|
|
78
|
+
const ownerIds = new Map();
|
|
79
|
+
const addJavaSym = (nameNode, callable, extra) => {
|
|
80
|
+
const base = symbolBaseId(fileRel, nameNode);
|
|
81
|
+
// Compact/generated Java can put an owner, constructor and overloads with the same name on one
|
|
82
|
+
// line. Preserve historical IDs normally; disambiguate only an actual collision by source column.
|
|
83
|
+
const id = nodeIds.has(base) ? `${base}:c${nameNode.startPosition.column + 1}` : base;
|
|
84
|
+
addSym(nameNode.text, lineOf(nameNode), callable, {
|
|
85
|
+
...extra,
|
|
86
|
+
...(id === base ? {} : { idSuffix: id.slice(base.length) }),
|
|
87
|
+
});
|
|
88
|
+
return nodeIds.has(id) ? id : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Add owners first even though tree-sitter captures are source-ordered. This makes ownership edges
|
|
92
|
+
// deterministic for nested types and methods declared before/after other nested declarations.
|
|
93
|
+
for (const src of [TYPE_CORE, ...TYPE_OPTIONAL]) {
|
|
94
|
+
for (const cap of caps(grammar, src, tree.rootNode)) {
|
|
95
|
+
const declaration = cap.node.parent;
|
|
96
|
+
const id = addJavaSym(cap.node, false, {
|
|
97
|
+
sourceNode: cap.node.parent,
|
|
98
|
+
symbolKind: cap.name,
|
|
99
|
+
});
|
|
100
|
+
if (id) ownerIds.set(declarationKey(declaration), id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const cap of caps(grammar, MEMBERS, tree.rootNode)) {
|
|
105
|
+
const declaration = cap.name === "field" ? ancestor(cap.node, FIELD_DECLARATIONS) : cap.node.parent;
|
|
106
|
+
const owner = ancestor(cap.node, TYPE_DECLARATIONS);
|
|
107
|
+
const ownerNameNode = owner && field(owner, "name");
|
|
108
|
+
const memberKind = cap.name === "constructor" ? "constructor" : cap.name;
|
|
109
|
+
const memberId = addJavaSym(cap.node, cap.name !== "field", {
|
|
110
|
+
sourceNode: declaration,
|
|
111
|
+
symbolKind: memberKind,
|
|
112
|
+
...(ownerNameNode ? { memberOf: ownerNameNode.text } : {}),
|
|
113
|
+
visibility: visibilityOf(declaration, owner),
|
|
114
|
+
});
|
|
115
|
+
if (!ownerNameNode || cap.name === "field") continue;
|
|
116
|
+
const ownerId = ownerIds.get(declarationKey(owner));
|
|
117
|
+
if (ownerId && memberId && ownerId !== memberId) {
|
|
118
|
+
links.push({ source: ownerId, target: memberId, relation: "method", confidence: "EXTRACTED" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const wildcardPackages = [];
|
|
123
|
+
const staticWildcardTargets = [];
|
|
124
|
+
const importStatements = caps(grammar, `(import_declaration) @imp`, tree.rootNode);
|
|
125
|
+
for (const cap of importStatements) {
|
|
126
|
+
const match = cap.node.text.match(/^\s*import\s+(static\s+)?([\w.]+?)(\.\*)?\s*;?\s*$/);
|
|
127
|
+
if (!match) continue;
|
|
128
|
+
const isStatic = !!match[1];
|
|
129
|
+
const parts = match[2].split(".").filter(Boolean);
|
|
130
|
+
const wildcard = !!match[3];
|
|
131
|
+
const line = lineOf(cap.node);
|
|
132
|
+
if (!isStatic && wildcard) {
|
|
133
|
+
wildcardPackages.push(parts);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!isStatic) {
|
|
137
|
+
const target = exactJavaTarget(resolveJavaImport, parts);
|
|
138
|
+
if (!target) continue;
|
|
139
|
+
const local = parts[parts.length - 1];
|
|
140
|
+
addImportEdge(target, { line, specifier: parts.join("."), compileOnly: true });
|
|
141
|
+
imports.set(local, { imported: local, targetFile: target });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Static imports end in a member name. Walk prefixes until the declaring project class resolves.
|
|
146
|
+
let target = null; let classParts = null;
|
|
147
|
+
const max = wildcard ? parts.length : parts.length - 1;
|
|
148
|
+
for (let take = max; take > 0 && !target; take--) {
|
|
149
|
+
const candidate = parts.slice(0, take);
|
|
150
|
+
target = exactJavaTarget(resolveJavaImport, candidate);
|
|
151
|
+
if (target) classParts = candidate;
|
|
152
|
+
}
|
|
153
|
+
if (!target) continue;
|
|
154
|
+
addImportEdge(target, { line, specifier: `${isStatic ? "static " : ""}${parts.join(".")}${wildcard ? ".*" : ""}`, compileOnly: true });
|
|
155
|
+
if (wildcard) staticWildcardTargets.push(target);
|
|
156
|
+
else {
|
|
157
|
+
const member = parts[classParts.length];
|
|
158
|
+
if (member) imports.set(member, { imported: member, targetFile: target });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Seed the shared pass-2 resolver with Java's implicit same-package types and wildcard imports.
|
|
163
|
+
// Exact-path validation is important: the general basename fallback is useful for explicit imports,
|
|
164
|
+
// but must not bind an unimported Foo to an unrelated package's Foo.java.
|
|
165
|
+
const slash = fileRel.lastIndexOf("/");
|
|
166
|
+
const ownDir = slash < 0 ? "" : fileRel.slice(0, slash);
|
|
167
|
+
const bindType = (name) => {
|
|
168
|
+
if (!name || imports.has(name)) return;
|
|
169
|
+
const samePackage = `${ownDir ? ownDir + "/" : ""}${name}.java`;
|
|
170
|
+
if (fileSet.has(samePackage)) {
|
|
171
|
+
imports.set(name, { imported: name, targetFile: samePackage });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const pkg of wildcardPackages) {
|
|
175
|
+
const target = exactJavaTarget(resolveJavaImport, [...pkg, name]);
|
|
176
|
+
if (target) { imports.set(name, { imported: name, targetFile: target }); return; }
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
for (const cap of caps(grammar, `(type_identifier) @type`, tree.rootNode)) bindType(cap.node.text);
|
|
180
|
+
for (const cap of caps(grammar, `(scoped_type_identifier) @type`, tree.rootNode)) {
|
|
181
|
+
const parts = cap.node.text.split(".").filter(Boolean);
|
|
182
|
+
const target = exactJavaTarget(resolveJavaImport, parts);
|
|
183
|
+
const name = parts[parts.length - 1];
|
|
184
|
+
if (target && name && !imports.has(name)) imports.set(name, { imported: name, targetFile: target });
|
|
185
|
+
}
|
|
186
|
+
if (staticWildcardTargets.length === 1) {
|
|
187
|
+
for (const cap of caps(grammar, `(method_invocation name: (identifier) @callee)`, tree.rootNode)) {
|
|
188
|
+
if (!imports.has(cap.node.text)) imports.set(cap.node.text, { imported: cap.node.text, targetFile: staticWildcardTargets[0] });
|
|
189
|
+
}
|
|
27
190
|
}
|
|
28
191
|
},
|
|
29
192
|
};
|
|
@@ -16,6 +16,16 @@ const TOPVARS = `
|
|
|
16
16
|
(program (export_statement (variable_declaration (variable_declarator) @decl)))`;
|
|
17
17
|
const REQUIRE = `(variable_declarator name: (_) @lhs value: (call_expression function: (identifier) @req arguments: (arguments (string (string_fragment) @src))))`;
|
|
18
18
|
|
|
19
|
+
function parseExportSpecifiers(raw) {
|
|
20
|
+
return String(raw || "").split(",").map((part) => {
|
|
21
|
+
const text = part.trim();
|
|
22
|
+
const typeOnly = /^type\s+/.test(text);
|
|
23
|
+
const clean = text.replace(/^type\s+/, "").trim();
|
|
24
|
+
const match = clean.match(/^([A-Za-z_$][\w$]*|default)(?:\s+as\s+([A-Za-z_$][\w$]*|default))?$/);
|
|
25
|
+
return match ? { imported: match[1], exported: match[2] || match[1], typeOnly } : null;
|
|
26
|
+
}).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export default {
|
|
20
30
|
family: "js",
|
|
21
31
|
grammars: ["javascript", "typescript", "tsx"],
|
|
@@ -26,6 +36,7 @@ export default {
|
|
|
26
36
|
|
|
27
37
|
pass1(ctx) {
|
|
28
38
|
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, markExported, imports, resolveJsImport, resolveAlias } = ctx;
|
|
39
|
+
const recordJsExport = typeof ctx.recordJsExport === "function" ? ctx.recordJsExport : () => {};
|
|
29
40
|
// exported-ness of a declaration = an export_statement ancestor (export function/class/const …).
|
|
30
41
|
// BOUNDED climb: web-tree-sitter's node.parent is O(depth) (re-walks a cursor from the root each call),
|
|
31
42
|
// so an UNBOUNDED walk to `program` for every symbol was O(depth^3) and HUNG the build for minutes/hours
|
|
@@ -34,15 +45,16 @@ export default {
|
|
|
34
45
|
// not a module-level export → bail immediately. See [[graph-builder-internalization]].
|
|
35
46
|
// Early-out for nested scopes keeps this O(1); class members are intentionally never module exports even
|
|
36
47
|
// when their owner class is exported. The hop cap remains a backstop for malformed/deep syntax trees.
|
|
37
|
-
const
|
|
48
|
+
const exportStatementOf = (node) => {
|
|
38
49
|
let p = node.parent;
|
|
39
50
|
for (let hops = 0; p && hops < 6; hops++) {
|
|
40
|
-
if (p.type === "export_statement") return
|
|
41
|
-
if (p.type === "program" || p.type === "statement_block" || p.type === "class_body") return
|
|
51
|
+
if (p.type === "export_statement") return p;
|
|
52
|
+
if (p.type === "program" || p.type === "statement_block" || p.type === "class_body") return null;
|
|
42
53
|
p = p.parent;
|
|
43
54
|
}
|
|
44
|
-
return
|
|
55
|
+
return null;
|
|
45
56
|
};
|
|
57
|
+
const isExportedDecl = (node) => !!exportStatementOf(node);
|
|
46
58
|
const isModuleDeclaration = (node) => {
|
|
47
59
|
let p = node.parent;
|
|
48
60
|
for (let hops = 0; p && hops < 8; hops++) {
|
|
@@ -78,21 +90,32 @@ export default {
|
|
|
78
90
|
};
|
|
79
91
|
for (const cap of caps(grammar, FUNCS, tree.rootNode)) {
|
|
80
92
|
const isMethod = cap.name === "method";
|
|
93
|
+
const exportStatement = !isMethod && exportStatementOf(cap.node);
|
|
81
94
|
addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name !== "class", {
|
|
82
95
|
sourceNode: cap.node.parent,
|
|
83
|
-
...(
|
|
96
|
+
...(exportStatement ? { exported: true } : {}),
|
|
84
97
|
...(isMethod ? methodMetadata(cap.node) : { symbolKind: cap.name === "class" ? "class" : "function", moduleDeclaration: isModuleDeclaration(cap.node) })
|
|
85
98
|
});
|
|
99
|
+
if (exportStatement) {
|
|
100
|
+
recordJsExport({
|
|
101
|
+
kind: "local",
|
|
102
|
+
exported: /^\s*export\s+default\b/.test(exportStatement.text) ? "default" : cap.node.text,
|
|
103
|
+
local: cap.node.text,
|
|
104
|
+
typeOnly: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
86
107
|
}
|
|
87
108
|
for (const cap of caps(grammar, TOPVARS, tree.rootNode)) {
|
|
88
109
|
const nameNode = field(cap.node, "name"); if (!nameNode || nameNode.type !== "identifier") continue;
|
|
89
110
|
const val = field(cap.node, "value");
|
|
111
|
+
const exported = isExportedDecl(cap.node);
|
|
90
112
|
addSym(nameNode.text, nameNode.startPosition.row + 1, !!(val && CALLABLE.test(val.type)), {
|
|
91
113
|
sourceNode: val || cap.node,
|
|
92
|
-
...(
|
|
114
|
+
...(exported ? { exported: true } : {}),
|
|
93
115
|
symbolKind: "variable",
|
|
94
116
|
moduleDeclaration: true
|
|
95
117
|
});
|
|
118
|
+
if (exported) recordJsExport({ kind: "local", exported: nameNode.text, local: nameNode.text, typeOnly: false });
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
const importTypeOnly = (node) => {
|
|
@@ -127,9 +150,9 @@ export default {
|
|
|
127
150
|
addImportEdge(tgt, { typeOnly, line, specifier: rawSpec });
|
|
128
151
|
const clause = node.namedChildren.find((c) => c.type === "import_clause"); if (!clause) continue;
|
|
129
152
|
for (const c of clause.namedChildren) {
|
|
130
|
-
if (c.type === "identifier") imports.set(c.text, { imported: "default", targetFile: tgt, typeOnly });
|
|
131
|
-
else if (c.type === "namespace_import") { const id = c.namedChildren.find((x) => x.type === "identifier"); if (id) imports.set(id.text, { imported: "*", targetFile: tgt, typeOnly }); }
|
|
132
|
-
else if (c.type === "named_imports") for (const s of c.namedChildren) { if (s.type !== "import_specifier") continue; const nm = field(s, "name"), al = field(s, "alias"); if (nm) imports.set((al || nm).text, { imported: nm.text, targetFile: tgt, typeOnly: typeOnly || /^\s*type\b/.test(s.text) }); }
|
|
153
|
+
if (c.type === "identifier") imports.set(c.text, { imported: "default", targetFile: tgt, typeOnly, line, specifier: rawSpec });
|
|
154
|
+
else if (c.type === "namespace_import") { const id = c.namedChildren.find((x) => x.type === "identifier"); if (id) imports.set(id.text, { imported: "*", targetFile: tgt, typeOnly, line, specifier: rawSpec }); }
|
|
155
|
+
else if (c.type === "named_imports") for (const s of c.namedChildren) { if (s.type !== "import_specifier") continue; const nm = field(s, "name"), al = field(s, "alias"); if (nm) imports.set((al || nm).text, { imported: nm.text, targetFile: tgt, typeOnly: typeOnly || /^\s*type\b/.test(s.text), line, specifier: rawSpec }); }
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
158
|
|
|
@@ -140,8 +163,8 @@ export default {
|
|
|
140
163
|
const tgt = resolveJsImport(fileRel, cap.node.text); if (!tgt) continue;
|
|
141
164
|
addImportEdge(tgt, { typeOnly: false, line: cap.node.startPosition.row + 1, specifier: cap.node.text });
|
|
142
165
|
const lhs = dv && field(dv, "name");
|
|
143
|
-
if (lhs && lhs.type === "identifier") imports.set(lhs.text, { imported: "*", targetFile: tgt });
|
|
144
|
-
else if (lhs && lhs.type === "object_pattern") for (const p of lhs.namedChildren) { const key = field(p, "key") || (p.type === "shorthand_property_identifier_pattern" ? p : null); const val = field(p, "value"); const local = (val && val.type === "identifier") ? val : key; if (key && local) imports.set(local.text, { imported: key.text, targetFile: tgt }); }
|
|
166
|
+
if (lhs && lhs.type === "identifier") imports.set(lhs.text, { imported: "*", targetFile: tgt, line: cap.node.startPosition.row + 1, specifier: cap.node.text });
|
|
167
|
+
else if (lhs && lhs.type === "object_pattern") for (const p of lhs.namedChildren) { const key = field(p, "key") || (p.type === "shorthand_property_identifier_pattern" ? p : null); const val = field(p, "value"); const local = (val && val.type === "identifier") ? val : key; if (key && local) imports.set(local.text, { imported: key.text, targetFile: tgt, line: cap.node.startPosition.row + 1, specifier: cap.node.text }); }
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
// ---- CJS require, ALL forms (side-effect require("dotenv/config") included): bare-pkg records +
|
|
@@ -186,11 +209,47 @@ export default {
|
|
|
186
209
|
const line = node.startPosition.row + 1;
|
|
187
210
|
const typeOnly = reexportTypeOnly(node);
|
|
188
211
|
const tgt = resolveJsImport(fileRel, rawSpec);
|
|
189
|
-
if (tgt)
|
|
212
|
+
if (tgt) {
|
|
213
|
+
addImportEdge(tgt, { relation: "re_exports", typeOnly, line, specifier: rawSpec });
|
|
214
|
+
const text = node.text.trim();
|
|
215
|
+
const star = text.match(/^export\s+(type\s+)?\*\s+from\b/);
|
|
216
|
+
const namespace = text.match(/^export\s+(type\s+)?\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\b/);
|
|
217
|
+
const clause = text.match(/^export\s+(type\s+)?\{([\s\S]*?)\}\s+from\b/);
|
|
218
|
+
if (namespace) {
|
|
219
|
+
recordJsExport({ kind: "namespace", exported: namespace[2], targetFile: tgt, typeOnly: !!namespace[1] });
|
|
220
|
+
} else if (star) {
|
|
221
|
+
recordJsExport({ kind: "star", targetFile: tgt, typeOnly: !!star[1] });
|
|
222
|
+
} else if (clause) {
|
|
223
|
+
for (const spec of parseExportSpecifiers(clause[2])) {
|
|
224
|
+
recordJsExport({ kind: "named", exported: spec.exported, imported: spec.imported, targetFile: tgt, typeOnly: !!clause[1] || spec.typeOnly });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
190
228
|
else if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "reexport", line, typeOnly });
|
|
191
229
|
else if (rawSpec) recordUnresolved(rawSpec, "reexport", line, typeOnly);
|
|
192
230
|
}
|
|
193
231
|
|
|
232
|
+
// Local aliases (`export { internal as publicName }`) and default identifiers are part of the
|
|
233
|
+
// same export table used by the post-pass barrel resolver. Sourced clauses were recorded above.
|
|
234
|
+
for (const cap of caps(grammar, `(export_statement) @exp`, tree.rootNode)) {
|
|
235
|
+
const node = cap.node;
|
|
236
|
+
if (field(node, "source")) continue;
|
|
237
|
+
const text = node.text.trim();
|
|
238
|
+
const clause = text.match(/^export\s+(type\s+)?\{([\s\S]*?)\}/);
|
|
239
|
+
if (clause) for (const spec of parseExportSpecifiers(clause[2])) {
|
|
240
|
+
recordJsExport({ kind: "local", exported: spec.exported, local: spec.imported, typeOnly: !!clause[1] || spec.typeOnly });
|
|
241
|
+
}
|
|
242
|
+
const identifierDefault = text.match(/^export\s+default\s+([A-Za-z_$][\w$]*)\s*;?$/);
|
|
243
|
+
if (identifierDefault) recordJsExport({ kind: "local", exported: "default", local: identifierDefault[1], typeOnly: false });
|
|
244
|
+
const typedDeclaration = text.match(/^export\s+(?:declare\s+)?(type|interface|enum)\s+([A-Za-z_$][\w$]*)\b/);
|
|
245
|
+
if (typedDeclaration) recordJsExport({
|
|
246
|
+
kind: "local",
|
|
247
|
+
exported: typedDeclaration[2],
|
|
248
|
+
local: typedDeclaration[2],
|
|
249
|
+
typeOnly: typedDeclaration[1] !== "enum",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
194
253
|
// ---- export markers beyond declarations: `export { a, b }`, `export default X`, CJS module.exports ----
|
|
195
254
|
for (const cap of caps(grammar, `(export_statement (export_clause (export_specifier name: (identifier) @n)))`, tree.rootNode)) {
|
|
196
255
|
let st = cap.node.parent; while (st && st.type !== "export_statement") st = st.parent;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// Rust extractor. Symbols: fn (incl. impl methods) + struct/enum/trait/type/mod + const/static
|
|
2
2
|
// (+ macro_rules!/union via optional queries). Calls: `foo()` / `x.foo()` / `path::foo()` (name only).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// no import edges in v1; cross-file connectivity comes from the name-based calls pass plus the
|
|
7
|
-
// same-folder scope (mod.rs convention).
|
|
3
|
+
// File dependencies follow Rust's module tree: outlined `mod`, `use` trees, public re-exports, and anchored
|
|
4
|
+
// `crate/self/super` paths resolve to repo-local .rs or */mod.rs files. External crates deliberately stay out
|
|
5
|
+
// of this adapter; Cargo dependency analysis owns them.
|
|
8
6
|
const SYMS_CORE = `
|
|
9
7
|
(function_item name: (identifier) @method)
|
|
10
8
|
(struct_item name: (type_identifier) @class)
|
|
@@ -20,6 +18,66 @@ const SYMS_OPTIONAL = [
|
|
|
20
18
|
`(union_item name: (type_identifier) @class)`,
|
|
21
19
|
];
|
|
22
20
|
|
|
21
|
+
const cleanSegment = (part) => String(part || "").trim().replace(/^r#/, "");
|
|
22
|
+
const pathParts = (node) => String(node?.text || "").split("::").map(cleanSegment).filter(Boolean);
|
|
23
|
+
const under = (node, type) => { for (let p = node?.parent; p; p = p.parent) if (p.type === type) return true; return false; };
|
|
24
|
+
|
|
25
|
+
function pathAttribute(modNode) {
|
|
26
|
+
for (let prev = modNode?.previousNamedSibling; prev?.type === "attribute_item"; prev = prev.previousNamedSibling) {
|
|
27
|
+
const hashedRaw = prev.text.match(/\bpath\s*=\s*r(#+)"([^"]+)"\1/);
|
|
28
|
+
if (hashedRaw) return hashedRaw[2] || "";
|
|
29
|
+
const plain = prev.text.match(/\bpath\s*=\s*(?:r)?"([^"]+)"/);
|
|
30
|
+
if (plain) return plain[1] || "";
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inlineAncestors(node) {
|
|
36
|
+
const result = [];
|
|
37
|
+
for (let p = node?.parent; p; p = p.parent) {
|
|
38
|
+
if (p.type !== "declaration_list" || p.parent?.type !== "mod_item") continue;
|
|
39
|
+
const mod = p.parent;
|
|
40
|
+
const name = mod.namedChildren.find((child) => child.type === "identifier")?.text;
|
|
41
|
+
if (name) result.unshift({ name: cleanSegment(name), path: pathAttribute(mod) });
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Expand a Rust use tree into its leaf paths while retaining aliases. Examples:
|
|
47
|
+
// `crate::api::{self, Client as C, types::*}` -> api, api::Client, api::types.
|
|
48
|
+
function useLeaves(node, prefix = []) {
|
|
49
|
+
if (!node) return [];
|
|
50
|
+
if (node.type === "use_declaration") {
|
|
51
|
+
const body = node.namedChildren.find((child) => child.type !== "visibility_modifier");
|
|
52
|
+
return useLeaves(body, prefix);
|
|
53
|
+
}
|
|
54
|
+
if (node.type === "use_list") return node.namedChildren.flatMap((child) => useLeaves(child, prefix));
|
|
55
|
+
if (node.type === "scoped_use_list") {
|
|
56
|
+
const list = node.namedChildren.find((child) => child.type === "use_list");
|
|
57
|
+
const head = node.namedChildren.find((child) => child !== list);
|
|
58
|
+
return useLeaves(list, [...prefix, ...pathParts(head)]);
|
|
59
|
+
}
|
|
60
|
+
if (node.type === "use_as_clause") {
|
|
61
|
+
const named = node.namedChildren;
|
|
62
|
+
const alias = cleanSegment(named[named.length - 1]?.text);
|
|
63
|
+
return useLeaves(named[0], prefix).map((leaf) => ({ ...leaf, local: alias }));
|
|
64
|
+
}
|
|
65
|
+
if (node.type === "use_wildcard") {
|
|
66
|
+
return [{ segments: [...prefix, ...pathParts(node.namedChildren[0])], wildcard: true, local: null }];
|
|
67
|
+
}
|
|
68
|
+
if (node.type === "self") {
|
|
69
|
+
const local = [...prefix].reverse().find((part) => !["crate", "self", "super"].includes(part)) || null;
|
|
70
|
+
return [{ segments: prefix.length ? [...prefix] : ["self"], moduleSelf: true, local }];
|
|
71
|
+
}
|
|
72
|
+
if (["identifier", "scoped_identifier", "scoped_type_identifier", "crate", "super"].includes(node.type)) {
|
|
73
|
+
const own = pathParts(node);
|
|
74
|
+
const segments = [...prefix, ...own];
|
|
75
|
+
const local = [...own].reverse().find((part) => !["crate", "self", "super"].includes(part)) || null;
|
|
76
|
+
return [{ segments, local }];
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
23
81
|
export default {
|
|
24
82
|
family: "rust",
|
|
25
83
|
grammars: ["rust"],
|
|
@@ -35,11 +93,76 @@ export default {
|
|
|
35
93
|
],
|
|
36
94
|
|
|
37
95
|
pass1(ctx) {
|
|
38
|
-
const { grammar, tree, caps, addSym } = ctx;
|
|
96
|
+
const { grammar, tree, fileRel, caps, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath } = ctx;
|
|
39
97
|
for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
|
|
40
98
|
for (const cap of caps(grammar, src, tree.rootNode)) {
|
|
41
99
|
addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
|
|
42
100
|
}
|
|
43
101
|
}
|
|
102
|
+
|
|
103
|
+
// File dependency edges are intentionally unique per source/target/relation. A qualified path can be
|
|
104
|
+
// nested in another qualified path and often repeats an existing `mod`/`use`; counting occurrences would
|
|
105
|
+
// inflate module coupling without adding structure.
|
|
106
|
+
const emitted = new Set();
|
|
107
|
+
const emit = (target, meta = {}) => {
|
|
108
|
+
if (!target || target === fileRel) return;
|
|
109
|
+
const relation = meta.relation || "imports";
|
|
110
|
+
const key = relation + ">" + target;
|
|
111
|
+
if (emitted.has(key)) return;
|
|
112
|
+
emitted.add(key);
|
|
113
|
+
// Rust `mod`/`use`/`pub use` relationships are resolved by the compiler. They describe real
|
|
114
|
+
// architectural coupling, but they are not runtime module loading and therefore must not create
|
|
115
|
+
// JavaScript-style initialization cycles or runtime boundary/blast-radius findings.
|
|
116
|
+
addImportEdge(target, { ...meta, relation, compileOnly: true });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// `mod foo;` loads foo.rs or foo/mod.rs. Inline `mod foo { ... }` stays in the current file, but any
|
|
120
|
+
// outlined modules declared inside it are captured separately with their inline ancestor path.
|
|
121
|
+
for (const cap of caps(grammar, `(mod_item) @mod`, tree.rootNode)) {
|
|
122
|
+
const mod = cap.node;
|
|
123
|
+
if (mod.namedChildren.some((child) => child.type === "declaration_list")) continue;
|
|
124
|
+
const name = mod.namedChildren.find((child) => child.type === "identifier")?.text;
|
|
125
|
+
if (!name) continue;
|
|
126
|
+
const target = resolveRustMod(fileRel, cleanSegment(name), {
|
|
127
|
+
inlineModules: inlineAncestors(mod),
|
|
128
|
+
explicitPath: pathAttribute(mod),
|
|
129
|
+
});
|
|
130
|
+
emit(target, { line: mod.startPosition.row + 1, specifier: `mod ${name}` });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Imports/re-exports may be nested trees and aliases. Besides the file edge, retain direct item aliases
|
|
134
|
+
// so the existing pass-2 call resolver can connect `use crate::worker::run; run()` across folders.
|
|
135
|
+
for (const cap of caps(grammar, `(use_declaration) @use`, tree.rootNode)) {
|
|
136
|
+
const use = cap.node;
|
|
137
|
+
const relation = use.namedChildren.some((child) => child.type === "visibility_modifier") ? "re_exports" : "imports";
|
|
138
|
+
const ancestors = inlineAncestors(use);
|
|
139
|
+
for (const leaf of useLeaves(use)) {
|
|
140
|
+
const resolved = resolveRustPath(fileRel, leaf.segments, { inlineModules: ancestors, unqualified: true });
|
|
141
|
+
if (!resolved) continue;
|
|
142
|
+
emit(resolved.targetFile, { relation, line: use.startPosition.row + 1, specifier: use.text.replace(/;\s*$/, "") });
|
|
143
|
+
if (!leaf.wildcard && leaf.local && leaf.local !== "_") {
|
|
144
|
+
const imported = resolved.remaining.length ? cleanSegment(resolved.remaining.at(-1)) : "*";
|
|
145
|
+
imports.set(cleanSegment(leaf.local), { imported, targetFile: resolved.targetFile, rustModule: imported === "*" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fully-qualified paths need no `use` declaration but still prove a dependency. Restrict this pass to
|
|
151
|
+
// explicit crate/self/super anchors so associated paths such as `Type::method` cannot be mistaken for a
|
|
152
|
+
// same-named module file. Inner prefixes are skipped; only the maximal path emits an edge.
|
|
153
|
+
for (const cap of caps(grammar, `[(scoped_identifier) (scoped_type_identifier)] @path`, tree.rootNode)) {
|
|
154
|
+
const node = cap.node;
|
|
155
|
+
if (under(node, "use_declaration")) continue;
|
|
156
|
+
if (["scoped_identifier", "scoped_type_identifier"].includes(node.parent?.type)) continue;
|
|
157
|
+
const segments = pathParts(node);
|
|
158
|
+
if (!["crate", "self", "super"].includes(segments[0])) continue;
|
|
159
|
+
const resolved = resolveRustPath(fileRel, segments, { inlineModules: inlineAncestors(node), unqualified: false });
|
|
160
|
+
if (!resolved) continue;
|
|
161
|
+
emit(resolved.targetFile, { line: node.startPosition.row + 1, specifier: node.text });
|
|
162
|
+
const finalName = cleanSegment(segments.at(-1));
|
|
163
|
+
if (resolved.remaining.length && finalName && !imports.has(finalName)) {
|
|
164
|
+
imports.set(finalName, { imported: finalName, targetFile: resolved.targetFile, rustQualified: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
44
167
|
},
|
|
45
168
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Deterministic community territories. Most languages keep the historical top-two-folder bucket. Java
|
|
2
|
+
// Maven/Gradle trees need package depth: otherwise every class under application/src becomes one giant blob.
|
|
3
|
+
export function communityTerritoryOf(file) {
|
|
4
|
+
const normalized = String(file || "").replace(/\\/g, "/");
|
|
5
|
+
if (/\.java$/i.test(normalized)) {
|
|
6
|
+
const marker = /(?:^|\/)src\/(?:main|test)\/java\//.exec(normalized);
|
|
7
|
+
if (marker) {
|
|
8
|
+
const end = marker.index + marker[0].length;
|
|
9
|
+
const prefix = normalized.slice(0, end).replace(/\/$/, "");
|
|
10
|
+
const packageDirs = normalized.slice(end).split("/").filter(Boolean).slice(0, -1);
|
|
11
|
+
if (packageDirs.length) return `${prefix}/${packageDirs.slice(0, 5).join("/")}`;
|
|
12
|
+
return prefix;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const dirs = normalized.split("/").filter(Boolean).slice(0, -1);
|
|
16
|
+
return dirs.length ? dirs.slice(0, 2).join("/") : "(root)";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Community ids are serialized into graph.json and therefore must not depend on which subset of
|
|
20
|
+
// files happened to be reparsed. Assign ids from the complete, sorted territory universe so a
|
|
21
|
+
// scoped incremental merge produces the same ids as a full build.
|
|
22
|
+
export function assignDeterministicCommunities(nodes = []) {
|
|
23
|
+
const territories = [...new Set(nodes.map((node) => communityTerritoryOf(node?.source_file)))].sort();
|
|
24
|
+
const ids = new Map(territories.map((territory, index) => [territory, index]));
|
|
25
|
+
for (const node of nodes) node.community = ids.get(communityTerritoryOf(node?.source_file));
|
|
26
|
+
return nodes;
|
|
27
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Small cross-process lock + atomic file replacement helpers for derived graph state. Multiple MCP
|
|
2
|
+
// clients commonly run at once (Claude, Codex, desktop); a plain read -> write sequence can lose a
|
|
3
|
+
// registry record or expose a half-written multi-megabyte graph to another reader.
|
|
4
|
+
import {mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync} from 'node:fs'
|
|
5
|
+
import {dirname} from 'node:path'
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
|
|
8
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
+
const waitSync = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
10
|
+
|
|
11
|
+
function clearStaleLock(lockDir, staleMs) {
|
|
12
|
+
try {
|
|
13
|
+
if (Date.now() - statSync(lockDir).mtimeMs <= staleMs) return false
|
|
14
|
+
try {
|
|
15
|
+
const ownerPid = Number(readFileSync(`${lockDir}/owner`, 'utf8').split(/\s+/)[0])
|
|
16
|
+
if (Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
17
|
+
try { process.kill(ownerPid, 0); return false }
|
|
18
|
+
catch (error) { if (error?.code === 'EPERM') return false }
|
|
19
|
+
}
|
|
20
|
+
} catch { /* missing owner: stale age remains the authority */ }
|
|
21
|
+
rmSync(lockDir, {recursive: true, force: true})
|
|
22
|
+
return true
|
|
23
|
+
} catch { return false }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function acquired(lockDir) {
|
|
27
|
+
mkdirSync(dirname(lockDir), {recursive: true})
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(lockDir)
|
|
30
|
+
writeFileSync(`${lockDir}/owner`, `${process.pid}\n${new Date().toISOString()}\n`, 'utf8')
|
|
31
|
+
return true
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error?.code === 'EEXIST') return false
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function withFileLock(lockDir, fn, {timeoutMs = 10_000, staleMs = 60_000, pollMs = 25} = {}) {
|
|
39
|
+
const started = Date.now()
|
|
40
|
+
while (!acquired(lockDir)) {
|
|
41
|
+
clearStaleLock(lockDir, staleMs)
|
|
42
|
+
if (Date.now() - started >= timeoutMs) throw new Error(`timed out waiting for derived-state lock: ${lockDir}`)
|
|
43
|
+
await wait(pollMs)
|
|
44
|
+
}
|
|
45
|
+
try { return await fn() }
|
|
46
|
+
finally { rmSync(lockDir, {recursive: true, force: true}) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function withFileLockSync(lockDir, fn, {timeoutMs = 10_000, staleMs = 60_000, pollMs = 20} = {}) {
|
|
50
|
+
const started = Date.now()
|
|
51
|
+
while (!acquired(lockDir)) {
|
|
52
|
+
clearStaleLock(lockDir, staleMs)
|
|
53
|
+
if (Date.now() - started >= timeoutMs) throw new Error(`timed out waiting for derived-state lock: ${lockDir}`)
|
|
54
|
+
waitSync(pollMs)
|
|
55
|
+
}
|
|
56
|
+
try { return fn() }
|
|
57
|
+
finally { rmSync(lockDir, {recursive: true, force: true}) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function atomicWriteFileSync(path, data, encoding = undefined) {
|
|
61
|
+
mkdirSync(dirname(path), {recursive: true})
|
|
62
|
+
const temp = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(temp, data, encoding)
|
|
65
|
+
renameSync(temp, path)
|
|
66
|
+
} finally {
|
|
67
|
+
try { rmSync(temp, {force: true}) } catch { /* already renamed */ }
|
|
68
|
+
}
|
|
69
|
+
}
|