forked from elizaOS/eliza
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassert-comment-only-diff.mjs
More file actions
235 lines (215 loc) · 7.4 KB
/
Copy pathassert-comment-only-diff.mjs
File metadata and controls
235 lines (215 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/env node
/**
* Machine proof that a comment-cleanup change touches comments and whitespace
* only, never code.
*
* For every file changed against a base ref (default `origin/develop`), the
* base blob and the working-tree copy are parsed with the TypeScript parser and
* their ASTs are walked to the leaf-token level; comments (including JSDoc
* doc-comments, which the parser keeps as nodes rather than trivia) are
* excluded, and the two code-token streams are asserted identical. Any token, or
* any added / deleted / renamed / non-source changed file, fails the check and
* is reported with the first offending token. A comment edit cannot change the
* parsed code tokens, so an identical token stream is a sound proof that only
* comments moved.
*
* This script is the single non-comment change in the repo-wide comment cleanup
* (parent elizaOS/eliza#12181, Work Item 2): it lets each batch PR prove "zero
* functional diff" mechanically instead of by reviewer trust. Wired as the root
* `check:comment-only` npm script; run per batch PR alongside `bun run verify`.
*
* Usage:
* node scripts/assert-comment-only-diff.mjs [base-ref] # default origin/develop
* node scripts/assert-comment-only-diff.mjs --self-test # planted-diff self-check
*/
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { extname } from "node:path";
import ts from "typescript";
const SOURCE_EXT = new Set([
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
]);
function git(args) {
return execFileSync("git", args, { encoding: "utf8", maxBuffer: 1 << 30 });
}
function isSource(file) {
return SOURCE_EXT.has(extname(file));
}
function scriptKind(fileName) {
if (fileName.endsWith(".tsx")) return ts.ScriptKind.TSX;
if (fileName.endsWith(".jsx")) return ts.ScriptKind.JSX;
if (
fileName.endsWith(".js") ||
fileName.endsWith(".mjs") ||
fileName.endsWith(".cjs")
)
return ts.ScriptKind.JS;
return ts.ScriptKind.TS;
}
/**
* Non-trivia token stream: `<kind>:<raw source text>` per leaf token, in order.
*
* The file is fully parsed (`createSourceFile`) and its AST is walked to its
* leaf tokens — the raw scanner is deliberately NOT used, because a scanner has
* no parse context and cannot tell a regex literal (`/…/`) from division or a
* template literal from a backtick inside a regex; a regex such as
* `` /=`([^`]+)`/ `` would make the scanner mis-open a template literal that
* swallows following comments, producing false divergences on comment-only
* edits. The parser resolves all of that. Comments are trivia and are not
* nodes, so they are excluded; string/template/regex literals and JSX text are
* single leaf tokens whose text is compared, so any code or literal edit
* surfaces as a divergence.
*/
function tokenStream(text, fileName) {
const sf = ts.createSourceFile(
fileName,
text,
ts.ScriptTarget.Latest,
/* setParentNodes */ false,
scriptKind(fileName),
);
const tokens = [];
const visit = (node) => {
// `/** … */` doc-comments are parsed into JSDoc nodes (not trivia); they are
// comments and are editable, so skip the whole JSDoc subtree.
if (
node.kind >= ts.SyntaxKind.FirstJSDocNode &&
node.kind <= ts.SyntaxKind.LastJSDocNode
) {
return;
}
const children = node.getChildren(sf);
if (children.length === 0) {
if (node.kind === ts.SyntaxKind.EndOfFileToken) return;
tokens.push(`${node.kind}:${node.getText(sf)}`);
return;
}
for (const child of children) visit(child);
};
visit(sf);
return tokens;
}
/** First divergent index, or -1 if the two streams are identical. */
function firstDivergence(a, b) {
const n = Math.min(a.length, b.length);
for (let i = 0; i < n; i++) {
if (a[i] !== b[i]) return i;
}
return a.length === b.length ? -1 : n;
}
function selfTest() {
const before = `const answer = 40 + 2; // old note\nexport default answer;\n`;
const commentsOnly = `/** Header prose. */\nconst answer = 40 + 2; // new note, rewritten\nexport default answer;\n`;
const oneToken = `const answer = 40 + 3; // old note\nexport default answer;\n`;
const failures = [];
if (
firstDivergence(
tokenStream(before, "x.ts"),
tokenStream(commentsOnly, "x.ts"),
) !== -1
) {
failures.push(
"comments-only plant was flagged as a code change (false positive)",
);
}
if (
firstDivergence(
tokenStream(before, "x.ts"),
tokenStream(oneToken, "x.ts"),
) === -1
) {
failures.push(
"one-token code plant (40+2 -> 40+3) slipped through (false negative)",
);
}
if (failures.length) {
for (const f of failures)
console.error(`[assert-comment-only-diff] self-test FAIL: ${f}`);
process.exit(1);
}
console.log(
"[assert-comment-only-diff] self-test PASS: comments-only accepted, one-token change rejected.",
);
process.exit(0);
}
function main() {
const argv = process.argv.slice(2);
if (argv.includes("--self-test")) return selfTest();
const base = argv[0] || "origin/develop";
let mergeBase;
try {
mergeBase = git(["merge-base", base, "HEAD"]).trim();
} catch {
console.error(
`[assert-comment-only-diff] cannot resolve merge-base of ${base} and HEAD.`,
);
process.exit(2);
}
// Diff merge-base against the working tree: catches committed and uncommitted
// changes on this branch without flagging what develop moved on its own.
const raw = git(["diff", "--name-status", "-z", mergeBase]);
const parts = raw.split("\0").filter(Boolean);
const violations = [];
let checked = 0;
for (let i = 0; i < parts.length; ) {
const status = parts[i++];
const code = status[0];
const file = parts[i++];
// Renames/copies carry a second path field.
const dest = code === "R" || code === "C" ? parts[i++] : file;
if (code !== "M") {
violations.push(
`${dest}: ${code} — comment cleanup must modify existing files only, not add/delete/rename`,
);
continue;
}
if (!isSource(dest)) {
violations.push(
`${dest}: changed non-source file — comment cleanup touches source files only`,
);
continue;
}
let baseText;
try {
baseText = git(["show", `${mergeBase}:${file}`]);
} catch {
violations.push(`${dest}: cannot read base blob at ${mergeBase}`);
continue;
}
const headText = readFileSync(dest, "utf8");
const baseTokens = tokenStream(baseText, dest);
const headTokens = tokenStream(headText, dest);
const idx = firstDivergence(baseTokens, headTokens);
checked++;
if (idx !== -1) {
const b =
baseTokens[idx]?.split(":").slice(1).join(":") ?? "<end of file>";
const h =
headTokens[idx]?.split(":").slice(1).join(":") ?? "<end of file>";
violations.push(
`${dest}: code token #${idx} diverges — base \`${b}\` vs head \`${h}\``,
);
}
}
if (violations.length) {
console.error(
`[assert-comment-only-diff] FAIL — ${violations.length} file(s) changed code, not just comments:\n`,
);
for (const v of violations) console.error(` ✗ ${v}`);
console.error(
`\nComment cleanup requires zero functional diff. Fix the files above.`,
);
process.exit(1);
}
console.log(
`[assert-comment-only-diff] OK — ${checked} source file(s) changed; every code token identical to ${base}. Comments only.`,
);
}
main();