@openclawbrain/cli 0.4.35 → 0.4.36

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.
@@ -0,0 +1,977 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const defaultRepoRoot = path.resolve(__dirname, "..", "..", "..");
12
+ const defaultWorkspaceRoot = path.resolve(defaultRepoRoot, "..");
13
+ const defaultOutputRoot = path.join(defaultWorkspaceRoot, "artifacts", "graphify-lints");
14
+
15
+ export const GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT = {
16
+ deterministicLints: "deterministic-lints.json",
17
+ summary: "summary.md",
18
+ proposalEnvelope: "proposal-envelope.json",
19
+ verdict: "verdict.json",
20
+ };
21
+
22
+ const RELEVANT_RELEASE_SURFACES = new Set([
23
+ "README.md",
24
+ "docs/README.md",
25
+ "docs/END_STATE.md",
26
+ "CHANGELOG.md",
27
+ ]);
28
+
29
+ const SHADOW_ONLY_CLASSES = new Set(["mutation", "forgetting", "correction"]);
30
+ const PROMOTABLE_CLASSES = new Set(["compiler", "lint"]);
31
+
32
+ function stableJson(value) {
33
+ return `${JSON.stringify(value, null, 2)}\n`;
34
+ }
35
+
36
+ function sha256Text(text) {
37
+ return `sha256:${createHash("sha256").update(String(text ?? ""), "utf8").digest("hex")}`;
38
+ }
39
+
40
+ function ensureDir(dirPath) {
41
+ if (!existsSync(dirPath)) {
42
+ mkdirSync(dirPath, { recursive: true });
43
+ }
44
+ }
45
+
46
+ function readTextIfExists(filePath) {
47
+ return existsSync(filePath) ? readFileSync(filePath, "utf8") : null;
48
+ }
49
+
50
+ function readJsonIfExists(filePath) {
51
+ const text = readTextIfExists(filePath);
52
+ return text === null ? null : JSON.parse(text);
53
+ }
54
+
55
+ function writeJson(filePath, value) {
56
+ writeFileSync(filePath, stableJson(value), "utf8");
57
+ }
58
+
59
+ function writeText(filePath, value) {
60
+ writeFileSync(filePath, `${value}\n`, "utf8");
61
+ }
62
+
63
+ function toPosix(value) {
64
+ return String(value).replace(/\\/g, "/");
65
+ }
66
+
67
+ function relativeWorkspacePath(absPath, workspaceRoot) {
68
+ const relative = path.relative(workspaceRoot, absPath);
69
+ return relative.startsWith("..") ? toPosix(absPath) : toPosix(relative);
70
+ }
71
+
72
+ function stripFragment(sourceId) {
73
+ return sourceId.split("#")[0] ?? sourceId;
74
+ }
75
+
76
+ function isLikelyPathRef(value) {
77
+ if (typeof value !== "string") {
78
+ return false;
79
+ }
80
+ const trimmed = value.trim();
81
+ if (trimmed.length === 0) {
82
+ return false;
83
+ }
84
+ if (
85
+ trimmed === "README.md" ||
86
+ trimmed === "CHANGELOG.md" ||
87
+ trimmed === "manifest.json" ||
88
+ trimmed === "pack.manifest.json" ||
89
+ trimmed === "summary.md" ||
90
+ trimmed === "status.json" ||
91
+ trimmed === "surface-map.json" ||
92
+ trimmed === "proposal-report.json" ||
93
+ trimmed === "verdict.json" ||
94
+ trimmed === "docs/README.md" ||
95
+ trimmed === "docs/END_STATE.md"
96
+ ) {
97
+ return true;
98
+ }
99
+ const allowedPrefixes = ["docs/", "artifacts/", "scripts/", "openclawbrain/", "./", "../"];
100
+ if (allowedPrefixes.some((prefix) => trimmed.startsWith(prefix)) && /\.[a-z]+$/i.test(trimmed)) {
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+
106
+ function candidatePathsForRef(ref, bundleRoot, repoRoot, workspaceRoot) {
107
+ const cleaned = stripFragment(ref);
108
+ const candidates = [];
109
+ if (path.isAbsolute(cleaned)) {
110
+ candidates.push(cleaned);
111
+ return candidates;
112
+ }
113
+ if (cleaned.startsWith("openclawbrain/")) {
114
+ candidates.push(path.join(repoRoot, cleaned.slice("openclawbrain/".length)));
115
+ candidates.push(path.join(workspaceRoot, cleaned.slice("openclawbrain/".length)));
116
+ }
117
+ if (cleaned.startsWith("./")) {
118
+ candidates.push(path.join(bundleRoot, cleaned.slice(2)));
119
+ }
120
+ candidates.push(path.join(bundleRoot, cleaned));
121
+ candidates.push(path.join(repoRoot, cleaned));
122
+ candidates.push(path.join(workspaceRoot, cleaned));
123
+ return [...new Set(candidates)];
124
+ }
125
+
126
+ function resolveRef(ref, bundleRoot, repoRoot, workspaceRoot) {
127
+ const candidates = candidatePathsForRef(ref, bundleRoot, repoRoot, workspaceRoot);
128
+ const resolvedPath = candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? null;
129
+ return {
130
+ sourceRef: ref,
131
+ resolvedPath,
132
+ exists: resolvedPath !== null && existsSync(resolvedPath),
133
+ };
134
+ }
135
+
136
+ function hashFile(filePath) {
137
+ return sha256Text(readFileSync(filePath, "utf8"));
138
+ }
139
+
140
+ function collectPathLikeRefs(value, refs = new Set()) {
141
+ if (value === null || value === undefined) {
142
+ return refs;
143
+ }
144
+ if (typeof value === "string") {
145
+ if (isLikelyPathRef(value.trim())) {
146
+ refs.add(value.trim());
147
+ }
148
+ return refs;
149
+ }
150
+ if (Array.isArray(value)) {
151
+ for (const item of value) {
152
+ collectPathLikeRefs(item, refs);
153
+ }
154
+ return refs;
155
+ }
156
+ if (typeof value === "object") {
157
+ for (const nestedValue of Object.values(value)) {
158
+ collectPathLikeRefs(nestedValue, refs);
159
+ }
160
+ }
161
+ return refs;
162
+ }
163
+
164
+ function collectEvidenceRefsFromObjects(objects, bundleRoot, repoRoot, workspaceRoot) {
165
+ const refs = [];
166
+ const seen = new Set();
167
+ const sourceRefs = new Set();
168
+ for (const object of objects) {
169
+ collectPathLikeRefs(object, sourceRefs);
170
+ }
171
+ for (const sourceRef of sourceRefs) {
172
+ const resolved = resolveRef(sourceRef, bundleRoot, repoRoot, workspaceRoot);
173
+ const key = `${sourceRef}::${resolved.resolvedPath ?? "missing"}`;
174
+ if (seen.has(key)) {
175
+ continue;
176
+ }
177
+ seen.add(key);
178
+ refs.push({
179
+ evidenceId: `evi_${sha256Text(sourceRef).slice(7, 19)}`,
180
+ sourceKind: sourceRef.includes("/") || /\.[a-z]+$/i.test(sourceRef) ? "file" : "summary",
181
+ sourceId: sourceRef,
182
+ authority: "raw_source",
183
+ derivation: "teacher_lint",
184
+ sourceHash: resolved.exists && resolved.resolvedPath !== null ? hashFile(resolved.resolvedPath) : null,
185
+ });
186
+ }
187
+ return refs;
188
+ }
189
+
190
+ function detectCurrentReleaseVersion(changelogText) {
191
+ for (const line of changelogText.split(/\r?\n/)) {
192
+ const match = /^##\s+((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\s*$/.exec(line);
193
+ if (match) {
194
+ return match[1];
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function readReleaseState(repoRoot) {
201
+ const changelogPath = path.join(repoRoot, "CHANGELOG.md");
202
+ const readmePath = path.join(repoRoot, "README.md");
203
+ const docsReadmePath = path.join(repoRoot, "docs", "README.md");
204
+ const endStatePath = path.join(repoRoot, "docs", "END_STATE.md");
205
+ const readmeText = readTextIfExists(readmePath);
206
+ const docsReadmeText = readTextIfExists(docsReadmePath);
207
+ const endStateText = readTextIfExists(endStatePath);
208
+ const changelogText = readTextIfExists(changelogPath);
209
+ const currentVersion = changelogText === null ? null : detectCurrentReleaseVersion(changelogText);
210
+ const readmeVersion = readmeText === null ? null : (readmeText.match(/Current version:\s+\*\*((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\*\*/m)?.[1] ?? null);
211
+ const docsIndexMatch = docsReadmeText?.match(/-\s+\[Current release notes \(((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\)\]\(release-notes-((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\.md\)/m);
212
+ const docsIndexVersion = docsIndexMatch?.[1] ?? null;
213
+ const docsIndexTargetVersion = docsIndexMatch?.[2] ?? null;
214
+ const endStateMatches = [...(endStateText?.matchAll(/split packages `@openclawbrain\/openclaw@((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))` and `@openclawbrain\/cli@((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))` are published/g) ?? [])].map((match) => [match[1], match[2]]);
215
+ return {
216
+ currentVersion,
217
+ readmeVersion,
218
+ docsIndexVersion,
219
+ docsIndexTargetVersion,
220
+ endStateVersions: endStateMatches,
221
+ releaseNotesPath: currentVersion === null ? null : path.join(repoRoot, "docs", `release-notes-${currentVersion}.md`),
222
+ };
223
+ }
224
+
225
+ function addFinding(findings, finding) {
226
+ findings.push({
227
+ code: finding.code,
228
+ severity: finding.severity ?? "error",
229
+ summary: finding.summary,
230
+ detail: finding.detail ?? null,
231
+ evidenceRefs: finding.evidenceRefs ?? [],
232
+ paths: finding.paths ?? [],
233
+ truthLayer: finding.truthLayer ?? null,
234
+ });
235
+ }
236
+
237
+ function uniqueByKey(items, keyFn) {
238
+ const seen = new Set();
239
+ const unique = [];
240
+ for (const item of items) {
241
+ const key = keyFn(item);
242
+ if (seen.has(key)) {
243
+ continue;
244
+ }
245
+ seen.add(key);
246
+ unique.push(item);
247
+ }
248
+ return unique;
249
+ }
250
+
251
+ function normalizeBundleObjects(bundleRoot) {
252
+ const summaryPath = path.join(bundleRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.summary);
253
+ const statusPath = path.join(bundleRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.summary.replace("summary.md", "status.json"));
254
+ const surfaceMapPath = path.join(bundleRoot, "surface-map.json");
255
+ const proposalReportPath = path.join(bundleRoot, "proposal-report.json");
256
+ const verdictPath = path.join(bundleRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.verdict);
257
+ const manifestPath = path.join(bundleRoot, "manifest.json");
258
+ const packManifestPath = path.join(bundleRoot, "pack.manifest.json");
259
+
260
+ return {
261
+ summaryText: readTextIfExists(summaryPath),
262
+ status: readJsonIfExists(statusPath),
263
+ surfaceMap: readJsonIfExists(surfaceMapPath),
264
+ proposalReport: readJsonIfExists(proposalReportPath),
265
+ verdict: readJsonIfExists(verdictPath),
266
+ manifest: readJsonIfExists(manifestPath),
267
+ packManifest: readJsonIfExists(packManifestPath),
268
+ summaryPath,
269
+ statusPath,
270
+ surfaceMapPath,
271
+ proposalReportPath,
272
+ verdictPath,
273
+ manifestPath,
274
+ packManifestPath,
275
+ };
276
+ }
277
+
278
+ function extractVersionCandidates(objects) {
279
+ const candidates = new Set();
280
+ for (const object of objects) {
281
+ const strings = [];
282
+ const stack = [object];
283
+ while (stack.length > 0) {
284
+ const current = stack.pop();
285
+ if (current === null || current === undefined) {
286
+ continue;
287
+ }
288
+ if (typeof current === "string") {
289
+ strings.push(current);
290
+ continue;
291
+ }
292
+ if (Array.isArray(current)) {
293
+ for (const item of current) {
294
+ stack.push(item);
295
+ }
296
+ continue;
297
+ }
298
+ if (typeof current === "object") {
299
+ for (const [key, value] of Object.entries(current)) {
300
+ if (typeof value === "string" && (key === "version" || key === "currentVersion" || key === "releaseVersion" || key === "docsVersion")) {
301
+ const semverMatch = /((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))/u.exec(value);
302
+ if (semverMatch) {
303
+ candidates.add(semverMatch[1]);
304
+ }
305
+ }
306
+ stack.push(value);
307
+ }
308
+ }
309
+ }
310
+ for (const text of strings) {
311
+ const releaseNotesMatch = /^docs\/release-notes-((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\.md$/.exec(text.trim());
312
+ if (releaseNotesMatch) {
313
+ candidates.add(releaseNotesMatch[1]);
314
+ }
315
+ const subjectMatch = /^release:((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))$/.exec(text.trim());
316
+ if (subjectMatch) {
317
+ candidates.add(subjectMatch[1]);
318
+ }
319
+ }
320
+ }
321
+ return [...candidates];
322
+ }
323
+
324
+ function buildBundleFindings(bundleRoot, repoRoot, workspaceRoot, bundle) {
325
+ const findings = [];
326
+ const bundleObjects = [bundle.manifest, bundle.packManifest, bundle.status, bundle.surfaceMap, bundle.proposalReport, bundle.verdict].filter(Boolean);
327
+ const evidenceRefs = collectEvidenceRefsFromObjects(bundleObjects, bundleRoot, repoRoot, workspaceRoot);
328
+ const bundleId = bundle.proposalReport?.bundleId ?? bundle.status?.bundleId ?? bundle.surfaceMap?.bundleId ?? bundle.manifest?.bundleId ?? bundle.packManifest?.packId ?? path.basename(bundleRoot);
329
+ const proposal = bundle.proposalReport?.proposal ?? bundle.status?.proposal ?? null;
330
+ const proposalId = proposal?.proposalId ?? bundle.proposalReport?.proposalId ?? bundle.status?.proposalId ?? bundle.verdict?.proposalId ?? bundleId;
331
+ const proposalClass = proposal?.proposalClass ?? bundle.proposalReport?.proposalClass ?? bundle.status?.proposalClass ?? bundle.status?.proposalLane ?? bundle.manifest?.proposalClass ?? "lint";
332
+ const reviewMode = proposal?.reviewMode ?? bundle.proposalReport?.reviewMode ?? bundle.status?.reviewMode ?? bundle.verdict?.reviewMode ?? "reviewable";
333
+ const proposalStatus = proposal?.status ?? bundle.proposalReport?.status ?? bundle.status?.proposalStatus ?? bundle.verdict?.verdict ?? "reviewable";
334
+
335
+ const mandatoryFiles = [
336
+ { label: "summary.md", path: bundle.summaryPath },
337
+ { label: "status.json", path: bundle.statusPath },
338
+ { label: "surface-map.json", path: bundle.surfaceMapPath },
339
+ { label: "proposal-report.json", path: bundle.proposalReportPath },
340
+ { label: "verdict.json", path: bundle.verdictPath },
341
+ ];
342
+ const missingMandatory = mandatoryFiles.filter((item) => !existsSync(item.path));
343
+ if (missingMandatory.length > 0) {
344
+ addFinding(findings, {
345
+ code: "missing_source_files",
346
+ summary: `${missingMandatory.length} required bundle files are missing`,
347
+ detail: missingMandatory.map((item) => item.label).join(", "),
348
+ evidenceRefs: [
349
+ { sourceId: "docs/architecture/teacher-v3-lints.md", evidenceId: "evi_teacher_v3_lints", authority: "raw_source", derivation: "teacher_lint" },
350
+ { sourceId: "docs/architecture/teacher-v3-proof.md", evidenceId: "evi_teacher_v3_proof", authority: "raw_source", derivation: "teacher_lint" },
351
+ ],
352
+ paths: missingMandatory.map((item) => relativeWorkspacePath(item.path, workspaceRoot)),
353
+ truthLayer: "bundle_surface",
354
+ });
355
+ }
356
+
357
+ const sourceRefs = uniqueByKey([...collectPathLikeRefs(bundle.manifest), ...collectPathLikeRefs(bundle.packManifest), ...collectPathLikeRefs(bundle.status), ...collectPathLikeRefs(bundle.surfaceMap), ...collectPathLikeRefs(bundle.proposalReport), ...collectPathLikeRefs(bundle.verdict)], (value) => value);
358
+ const resolvedRefs = sourceRefs.map((sourceRef) => resolveRef(sourceRef, bundleRoot, repoRoot, workspaceRoot));
359
+ const missingRefs = resolvedRefs.filter((ref) => !ref.exists);
360
+ if (missingRefs.length > 0) {
361
+ addFinding(findings, {
362
+ code: "missing_source_files",
363
+ summary: `${missingRefs.length} referenced source files are missing`,
364
+ detail: missingRefs.slice(0, 8).map((ref) => ref.sourceRef).join(", "),
365
+ evidenceRefs: evidenceRefs.slice(0, 4),
366
+ paths: missingRefs.map((ref) => relativeWorkspacePath(ref.resolvedPath ?? ref.sourceRef, workspaceRoot)),
367
+ truthLayer: "bundle_manifest",
368
+ });
369
+ }
370
+
371
+ const hashDrifts = [];
372
+ for (const manifest of [bundle.manifest, bundle.packManifest].filter(Boolean)) {
373
+ const artifactEntries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
374
+ for (const artifact of artifactEntries) {
375
+ const markdownPath = typeof artifact.markdownPath === "string" ? resolveRef(artifact.markdownPath, bundleRoot, repoRoot, workspaceRoot) : null;
376
+ const metaPath = typeof artifact.metaPath === "string" ? resolveRef(artifact.metaPath, bundleRoot, repoRoot, workspaceRoot) : null;
377
+ if (markdownPath?.exists && typeof artifact.contentHash === "string") {
378
+ const actualHash = hashFile(markdownPath.resolvedPath);
379
+ if (actualHash !== artifact.contentHash) {
380
+ hashDrifts.push({
381
+ label: artifact.artifactId ?? artifact.kind ?? artifact.markdownPath,
382
+ path: markdownPath.resolvedPath,
383
+ expected: artifact.contentHash,
384
+ actual: actualHash,
385
+ });
386
+ }
387
+ }
388
+ if (metaPath?.exists) {
389
+ const meta = readJsonIfExists(metaPath.resolvedPath);
390
+ if (meta && typeof meta.contentHash === "string" && markdownPath?.exists) {
391
+ const actualHash = hashFile(markdownPath.resolvedPath);
392
+ if (meta.contentHash !== actualHash) {
393
+ hashDrifts.push({
394
+ label: artifact.artifactId ?? artifact.kind ?? artifact.markdownPath,
395
+ path: markdownPath.resolvedPath,
396
+ expected: meta.contentHash,
397
+ actual: actualHash,
398
+ });
399
+ }
400
+ }
401
+ if (meta && typeof artifact.contentHash === "string" && typeof meta.contentHash === "string" && meta.contentHash !== artifact.contentHash) {
402
+ hashDrifts.push({
403
+ label: artifact.artifactId ?? artifact.kind ?? artifact.markdownPath,
404
+ path: metaPath.resolvedPath,
405
+ expected: artifact.contentHash,
406
+ actual: meta.contentHash,
407
+ });
408
+ }
409
+ }
410
+ }
411
+ if (typeof manifest.contentHash === "string") {
412
+ const manifestPath = manifest === bundle.manifest ? bundle.manifestPath : bundle.packManifestPath;
413
+ if (existsSync(manifestPath)) {
414
+ const actualHash = hashFile(manifestPath);
415
+ if (manifest.contentHash !== actualHash) {
416
+ hashDrifts.push({
417
+ label: path.basename(manifestPath),
418
+ path: manifestPath,
419
+ expected: manifest.contentHash,
420
+ actual: actualHash,
421
+ });
422
+ }
423
+ }
424
+ }
425
+ }
426
+ if (hashDrifts.length > 0) {
427
+ addFinding(findings, {
428
+ code: "manifest_hash_drift",
429
+ summary: `${hashDrifts.length} manifest or artifact hashes drifted from the written content`,
430
+ detail: hashDrifts.slice(0, 5).map((item) => `${relativeWorkspacePath(item.path, workspaceRoot)} expected=${item.expected} actual=${item.actual}`).join("; "),
431
+ evidenceRefs: [
432
+ { sourceId: "docs/architecture/compiled-artifacts.md", evidenceId: "evi_compiled_artifacts", authority: "raw_source", derivation: "teacher_lint" },
433
+ ...evidenceRefs.slice(0, 2),
434
+ ],
435
+ paths: hashDrifts.map((item) => relativeWorkspacePath(item.path, workspaceRoot)),
436
+ truthLayer: "manifest",
437
+ });
438
+ }
439
+
440
+ const promotableOnly = PROMOTABLE_CLASSES.has(proposalClass);
441
+ const shadowOnly = SHADOW_ONLY_CLASSES.has(proposalClass);
442
+ const promotionSignals = [proposalStatus, reviewMode, bundle.status?.proposalStatus, bundle.verdict?.verdict].filter((value) => typeof value === "string");
443
+ if (shadowOnly && promotionSignals.some((value) => ["promotable", "promoted"].includes(value))) {
444
+ addFinding(findings, {
445
+ code: "illegal_trust_class_promotion",
446
+ summary: `shadow-only proposal class ${proposalClass} is surfaced as promotable`,
447
+ detail: `status=${proposalStatus}; reviewMode=${reviewMode}`,
448
+ evidenceRefs: [
449
+ { sourceId: "docs/architecture/teacher-v3.md", evidenceId: "evi_teacher_v3_truth", authority: "raw_source", derivation: "teacher_lint" },
450
+ { sourceId: "docs/architecture/teacher-v3-proposals.md", evidenceId: "evi_teacher_v3_proposals", authority: "raw_source", derivation: "teacher_lint" },
451
+ ],
452
+ truthLayer: "proposal_truth",
453
+ });
454
+ }
455
+ if (!promotableOnly && !shadowOnly && ["promotable", "promoted"].includes(proposalStatus)) {
456
+ addFinding(findings, {
457
+ code: "illegal_trust_class_promotion",
458
+ summary: `unknown proposal class ${proposalClass} is being promoted`,
459
+ detail: `status=${proposalStatus}; reviewMode=${reviewMode}`,
460
+ evidenceRefs: [
461
+ { sourceId: "docs/architecture/teacher-v3.md", evidenceId: "evi_teacher_v3_truth", authority: "raw_source", derivation: "teacher_lint" },
462
+ ],
463
+ truthLayer: "proposal_truth",
464
+ });
465
+ }
466
+
467
+ const evidenceList = Array.isArray(proposal?.evidence) ? proposal.evidence : Array.isArray(bundle.proposalReport?.evidence) ? bundle.proposalReport.evidence : [];
468
+ const counterevidenceList = Array.isArray(proposal?.counterevidence) ? proposal.counterevidence : Array.isArray(bundle.proposalReport?.counterevidence) ? bundle.proposalReport.counterevidence : [];
469
+ const claims = Array.isArray(proposal?.claims) ? proposal.claims : Array.isArray(bundle.proposalReport?.claims) ? bundle.proposalReport.claims : [];
470
+ const allEvidenceIds = new Set(evidenceList.map((item) => item?.evidenceId).filter(Boolean));
471
+ const evidenceProblems = [];
472
+ for (const evidence of evidenceList) {
473
+ if (!evidence || typeof evidence !== "object" || typeof evidence.evidenceId !== "string" || evidence.evidenceId.trim().length === 0 || typeof evidence.sourceId !== "string" || evidence.sourceId.trim().length === 0 || typeof evidence.authority !== "string" || evidence.authority.trim().length === 0) {
474
+ evidenceProblems.push("invalid evidence ref");
475
+ }
476
+ }
477
+ for (const evidence of counterevidenceList) {
478
+ if (!evidence || typeof evidence !== "object" || typeof evidence.evidenceId !== "string" || evidence.evidenceId.trim().length === 0 || typeof evidence.sourceId !== "string" || evidence.sourceId.trim().length === 0 || typeof evidence.authority !== "string" || evidence.authority.trim().length === 0) {
479
+ evidenceProblems.push("invalid counterevidence ref");
480
+ }
481
+ }
482
+ for (const claim of claims) {
483
+ const claimRefs = Array.isArray(claim?.evidenceIds) ? claim.evidenceIds.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
484
+ if (claimRefs.length === 0) {
485
+ evidenceProblems.push(`claim ${claim?.claimId ?? "unknown"} is missing evidence refs`);
486
+ continue;
487
+ }
488
+ for (const claimRef of claimRefs) {
489
+ if (!allEvidenceIds.has(claimRef)) {
490
+ evidenceProblems.push(`claim ${claim?.claimId ?? "unknown"} references missing evidence ${claimRef}`);
491
+ }
492
+ }
493
+ }
494
+ if (evidenceProblems.length > 0) {
495
+ addFinding(findings, {
496
+ code: "missing_evidence_refs",
497
+ summary: `${evidenceProblems.length} evidence coverage issues were found`,
498
+ detail: evidenceProblems.slice(0, 6).join("; "),
499
+ evidenceRefs: [
500
+ { sourceId: "docs/architecture/compiled-artifacts.md", evidenceId: "evi_compiled_artifacts", authority: "raw_source", derivation: "teacher_lint" },
501
+ { sourceId: "docs/architecture/teacher-v3-proof.md", evidenceId: "evi_teacher_v3_proof", authority: "raw_source", derivation: "teacher_lint" },
502
+ ],
503
+ truthLayer: "proposal_truth",
504
+ });
505
+ }
506
+
507
+ const joinProblems = [];
508
+ const bundleIds = [bundle.manifest?.bundleId, bundle.packManifest?.bundleId, bundle.status?.bundleId, bundle.surfaceMap?.bundleId, bundle.proposalReport?.bundleId, bundle.verdict?.bundleId].filter((value) => typeof value === "string");
509
+ if (new Set(bundleIds).size > 1) {
510
+ joinProblems.push(`bundleId mismatch: ${bundleIds.join(", ")}`);
511
+ }
512
+ const proposalIds = [bundle.status?.proposalId, proposal?.proposalId, bundle.proposalReport?.proposalId, bundle.verdict?.proposalId].filter((value) => typeof value === "string");
513
+ if (new Set(proposalIds).size > 1) {
514
+ joinProblems.push(`proposalId mismatch: ${proposalIds.join(", ")}`);
515
+ }
516
+ const proposalClasses = [bundle.status?.proposalClass, bundle.status?.proposalLane, proposal?.proposalClass, bundle.proposalReport?.proposalClass, bundle.verdict?.proposalClass].filter((value) => typeof value === "string");
517
+ if (new Set(proposalClasses).size > 1) {
518
+ joinProblems.push(`proposal class mismatch: ${proposalClasses.join(", ")}`);
519
+ }
520
+ const reviewModes = [bundle.status?.reviewMode, proposal?.reviewMode, bundle.proposalReport?.reviewMode, bundle.verdict?.reviewMode].filter((value) => typeof value === "string");
521
+ if (new Set(reviewModes).size > 1) {
522
+ joinProblems.push(`reviewMode mismatch: ${reviewModes.join(", ")}`);
523
+ }
524
+ const surfaceMapCounts = bundle.surfaceMap?.counts;
525
+ if (surfaceMapCounts && typeof surfaceMapCounts === "object") {
526
+ const observedLength = Array.isArray(bundle.surfaceMap?.observedSurfaces) ? bundle.surfaceMap.observedSurfaces.length : null;
527
+ const bundleArtifactLength = Array.isArray(bundle.surfaceMap?.bundleArtifacts) ? bundle.surfaceMap.bundleArtifacts.length : null;
528
+ const totalLength = (observedLength ?? 0) + (bundleArtifactLength ?? 0);
529
+ if (
530
+ surfaceMapCounts.observedSurfaceCount !== observedLength ||
531
+ surfaceMapCounts.totalSurfaceCount !== totalLength ||
532
+ surfaceMapCounts.targetSurfaceCount !== bundleArtifactLength
533
+ ) {
534
+ joinProblems.push(
535
+ `surface counts mismatch: observed=${surfaceMapCounts.observedSurfaceCount}/${observedLength} target=${surfaceMapCounts.targetSurfaceCount}/${bundleArtifactLength} total=${surfaceMapCounts.totalSurfaceCount}/${totalLength}`,
536
+ );
537
+ }
538
+ }
539
+ const publicationSafeArtifacts = Array.isArray(bundle.proposalReport?.publicationSafeArtifacts) ? bundle.proposalReport.publicationSafeArtifacts : [];
540
+ const publicationSafeProblems = publicationSafeArtifacts.filter((artifact) => {
541
+ if (!artifact || typeof artifact.path !== "string") {
542
+ return true;
543
+ }
544
+ const resolved = resolveRef(artifact.path, bundleRoot, repoRoot, workspaceRoot);
545
+ return !resolved.exists;
546
+ });
547
+ if (publicationSafeProblems.length > 0) {
548
+ joinProblems.push(`publication-safe artifacts are missing: ${publicationSafeProblems.slice(0, 5).map((item) => item?.path ?? "unknown").join(", ")}`);
549
+ }
550
+ const releaseCandidates = extractVersionCandidates(bundleObjects);
551
+ const referencesPublicReleaseSurfaces = [...sourceRefs].some((ref) => {
552
+ const cleaned = stripFragment(ref);
553
+ if (RELEVANT_RELEASE_SURFACES.has(cleaned)) {
554
+ return true;
555
+ }
556
+ return /^docs\/release-notes-((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\.md$/.test(cleaned);
557
+ });
558
+ const releaseState = readReleaseState(repoRoot);
559
+ const repoReleaseIssues = [];
560
+ if (releaseState.currentVersion === null) {
561
+ repoReleaseIssues.push("CHANGELOG.md does not expose a current semver heading");
562
+ }
563
+ if (releaseState.currentVersion !== null) {
564
+ if (releaseState.readmeVersion === null) {
565
+ repoReleaseIssues.push("README.md does not advertise the current version banner");
566
+ } else if (releaseState.readmeVersion !== releaseState.currentVersion) {
567
+ repoReleaseIssues.push(`README.md advertises ${releaseState.readmeVersion} instead of ${releaseState.currentVersion}`);
568
+ }
569
+ if (releaseState.docsIndexVersion === null || releaseState.docsIndexTargetVersion === null) {
570
+ repoReleaseIssues.push("docs/README.md does not point at the current release notes link");
571
+ } else if (releaseState.docsIndexVersion !== releaseState.currentVersion || releaseState.docsIndexTargetVersion !== releaseState.currentVersion) {
572
+ repoReleaseIssues.push(`docs/README.md points at ${releaseState.docsIndexVersion}/${releaseState.docsIndexTargetVersion} instead of ${releaseState.currentVersion}`);
573
+ }
574
+ if (releaseState.endStateVersions.length > 0 && releaseState.endStateVersions.some(([openclawVersion, cliVersion]) => openclawVersion !== releaseState.currentVersion || cliVersion !== releaseState.currentVersion)) {
575
+ repoReleaseIssues.push(`docs/END_STATE.md still mixes split-package versions instead of ${releaseState.currentVersion}`);
576
+ }
577
+ if (releaseState.releaseNotesPath !== null && !existsSync(releaseState.releaseNotesPath)) {
578
+ repoReleaseIssues.push(`docs/release-notes-${releaseState.currentVersion}.md is missing`);
579
+ }
580
+ }
581
+ if (repoReleaseIssues.length > 0 && referencesPublicReleaseSurfaces) {
582
+ addFinding(findings, {
583
+ code: "release_docs_version_drift",
584
+ summary: `${repoReleaseIssues.length} public release-truth checks failed in the repository`,
585
+ detail: repoReleaseIssues.join("; "),
586
+ evidenceRefs: [
587
+ { sourceId: "scripts/verify-release-docs-drift.mjs", evidenceId: "evi_release_drift_script", authority: "raw_source", derivation: "teacher_lint" },
588
+ { sourceId: "docs/architecture/teacher-v3-lints.md", evidenceId: "evi_teacher_v3_lints", authority: "raw_source", derivation: "teacher_lint" },
589
+ ],
590
+ truthLayer: "docs_truth",
591
+ });
592
+ }
593
+ if (releaseCandidates.length > 0 && releaseState.currentVersion !== null && releaseCandidates.some((version) => version !== releaseState.currentVersion)) {
594
+ addFinding(findings, {
595
+ code: "release_docs_version_drift",
596
+ summary: `bundle release references point at ${releaseCandidates.join(", ")} instead of ${releaseState.currentVersion}`,
597
+ detail: `bundle versions=${releaseCandidates.join(", ")}; currentVersion=${releaseState.currentVersion}`,
598
+ evidenceRefs: [
599
+ { sourceId: "scripts/verify-release-docs-drift.mjs", evidenceId: "evi_release_drift_script", authority: "raw_source", derivation: "teacher_lint" },
600
+ ...evidenceRefs.slice(0, 2),
601
+ ],
602
+ truthLayer: "docs_truth",
603
+ });
604
+ }
605
+
606
+ if (bundle.status?.proposalStatus !== undefined && bundle.verdict?.verdict !== undefined && bundle.status.proposalStatus !== bundle.verdict.verdict) {
607
+ joinProblems.push(`verdict/status mismatch: status=${bundle.status.proposalStatus} verdict=${bundle.verdict.verdict}`);
608
+ }
609
+ if (joinProblems.length > 0) {
610
+ addFinding(findings, {
611
+ code: "broken_bundle_joins",
612
+ summary: `${joinProblems.length} bundle join problems were found`,
613
+ detail: joinProblems.slice(0, 6).join("; "),
614
+ evidenceRefs: evidenceRefs.slice(0, 4),
615
+ truthLayer: "bundle_join",
616
+ });
617
+ }
618
+
619
+ return {
620
+ ok: findings.length === 0,
621
+ bundleId,
622
+ proposalId,
623
+ proposalClass,
624
+ reviewMode,
625
+ proposalStatus,
626
+ evidenceRefs: evidenceRefs.slice(0, 12),
627
+ sourceRefs: resolvedRefs.map((ref) => ({
628
+ sourceRef: ref.sourceRef,
629
+ resolvedPath: ref.resolvedPath === null ? null : relativeWorkspacePath(ref.resolvedPath, workspaceRoot),
630
+ exists: ref.exists,
631
+ hash: ref.exists && ref.resolvedPath !== null ? hashFile(ref.resolvedPath) : null,
632
+ })),
633
+ releaseState: referencesPublicReleaseSurfaces ? releaseState : null,
634
+ findings,
635
+ };
636
+ }
637
+
638
+ function chooseSeverity(findings) {
639
+ if (findings.some((finding) => finding.severity === "error")) {
640
+ return "error";
641
+ }
642
+ if (findings.some((finding) => finding.severity === "warn")) {
643
+ return "warn";
644
+ }
645
+ return "info";
646
+ }
647
+
648
+ function buildSummaryMarkdown(report) {
649
+ const findingLines = report.findings.length > 0
650
+ ? report.findings.map((finding) => `- **${finding.code}** — ${finding.summary}${finding.detail ? ` (${finding.detail})` : ""}`)
651
+ : ["- none"];
652
+ return [
653
+ "# Graphify deterministic pre-lint",
654
+ "",
655
+ `- bundle: \`${report.bundleId}\``,
656
+ `- proposal: \`${report.proposalId}\` (${report.proposalClass}, ${report.reviewMode})`,
657
+ `- verdict: **${report.verdict}**`,
658
+ `- severity: **${report.severity}**`,
659
+ `- blockers: ${report.blockerCount}`,
660
+ `- warnings: ${report.warningCount}`,
661
+ `- checked bundle files: summary.md, status.json, surface-map.json, proposal-report.json, verdict.json`,
662
+ `- source refs checked: ${report.sourceRefCount}`,
663
+ `- evidence refs attached: ${report.evidenceRefCount}`,
664
+ "",
665
+ "## Findings",
666
+ ...findingLines,
667
+ "",
668
+ "## Guardrail",
669
+ "This bundle is review/proposal-only. It does not mutate the graph or serve path.",
670
+ ].join("\n") + "\n";
671
+ }
672
+
673
+ function buildProposalEnvelope(report, bundlePaths) {
674
+ return {
675
+ contract: "graphify_deterministic_lint_proposal_envelope.v1",
676
+ bundleId: report.bundleId,
677
+ proposalId: report.proposalId,
678
+ lane: "lint",
679
+ status: report.ok ? "reviewable" : "rejected",
680
+ reviewMode: "deterministic",
681
+ proposalClass: report.proposalClass,
682
+ trustClass: report.proposalClass,
683
+ bundleRoot: report.bundleRoot,
684
+ repoRoot: report.repoRoot,
685
+ lineage: {
686
+ producer: "graphify-deterministic-lint",
687
+ producerVersion: "0.1.0",
688
+ scope: "bundle-prelint",
689
+ idempotencyKey: sha256Text([report.bundleRoot, report.bundleId, report.findings.map((finding) => finding.code).join("|")].join("::")),
690
+ sourceBundleId: report.bundleId,
691
+ sourceManifestHash: report.bundleManifestHash ?? null,
692
+ },
693
+ subjectIds: uniqueByKey([report.bundleId, report.proposalId, ...(report.releaseState?.currentVersion ? [`release:${report.releaseState.currentVersion}`] : [])], (value) => value),
694
+ evidence: report.evidenceRefs,
695
+ counterevidence: [],
696
+ findings: report.findings,
697
+ recommendations: report.ok
698
+ ? ["Proceed to semantic-lint or proposal review only after the deterministic bundle stays clean."]
699
+ : ["Fix the deterministic blockers before any semantic lint or graph mutation work.", "Keep the bundle review-only until hash, join, evidence, and truth surfaces align."],
700
+ bundlePaths,
701
+ counts: {
702
+ findings: report.findings.length,
703
+ blockers: report.blockerCount,
704
+ warnings: report.warningCount,
705
+ },
706
+ };
707
+ }
708
+
709
+ function buildVerdict(report, bundlePaths) {
710
+ return {
711
+ contract: "graphify_deterministic_lint_verdict.v1",
712
+ bundleId: report.bundleId,
713
+ proposalId: report.proposalId,
714
+ verdict: report.ok ? "reviewable" : "rejected",
715
+ severity: report.severity,
716
+ reviewMode: "deterministic",
717
+ why: report.ok
718
+ ? "deterministic pre-lint found no blockers"
719
+ : `${report.blockerCount} blocker(s) and ${report.warningCount} warning(s) were found`,
720
+ blockerCount: report.blockerCount,
721
+ warningCount: report.warningCount,
722
+ findingCount: report.findings.length,
723
+ bundlePaths,
724
+ outputRoot: report.outputRoot,
725
+ };
726
+ }
727
+
728
+ function buildNormalizedReport(bundleRoot, repoRoot, workspaceRoot) {
729
+ const bundle = normalizeBundleObjects(bundleRoot);
730
+ const bundleId = bundle.proposalReport?.bundleId ?? bundle.status?.bundleId ?? bundle.surfaceMap?.bundleId ?? bundle.manifest?.bundleId ?? bundle.packManifest?.packId ?? path.basename(bundleRoot);
731
+ const proposal = bundle.proposalReport?.proposal ?? bundle.status?.proposal ?? null;
732
+ const proposalId = proposal?.proposalId ?? bundle.proposalReport?.proposalId ?? bundle.status?.proposalId ?? bundle.verdict?.proposalId ?? bundleId;
733
+ const bundleAnalysis = buildBundleFindings(bundleRoot, repoRoot, workspaceRoot, bundle);
734
+ const findings = bundleAnalysis.findings;
735
+ const sourceRefObjects = bundleAnalysis.sourceRefs;
736
+ const releaseState = bundleAnalysis.releaseState;
737
+ const evidenceRefs = bundleAnalysis.evidenceRefs;
738
+ const severity = chooseSeverity(findings);
739
+ const blockerCount = findings.filter((finding) => finding.severity === "error").length;
740
+ const warningCount = findings.filter((finding) => finding.severity === "warn").length;
741
+ return {
742
+ contract: "graphify_deterministic_lint_bundle.v1",
743
+ bundleId,
744
+ proposalId,
745
+ proposalClass: proposal?.proposalClass ?? bundle.proposalReport?.proposalClass ?? bundle.status?.proposalClass ?? bundle.status?.proposalLane ?? "lint",
746
+ reviewMode: proposal?.reviewMode ?? bundle.proposalReport?.reviewMode ?? bundle.status?.reviewMode ?? "reviewable",
747
+ proposalStatus: proposal?.status ?? bundle.proposalReport?.status ?? bundle.status?.proposalStatus ?? bundle.verdict?.verdict ?? "reviewable",
748
+ bundleRoot: relativeWorkspacePath(bundleRoot, workspaceRoot),
749
+ repoRoot: relativeWorkspacePath(repoRoot, workspaceRoot),
750
+ outputRoot: null,
751
+ inspectedAt: new Date().toISOString(),
752
+ bundlePaths: {
753
+ summary: relativeWorkspacePath(bundle.summaryPath, workspaceRoot),
754
+ status: relativeWorkspacePath(bundle.statusPath, workspaceRoot),
755
+ surfaceMap: relativeWorkspacePath(bundle.surfaceMapPath, workspaceRoot),
756
+ proposalReport: relativeWorkspacePath(bundle.proposalReportPath, workspaceRoot),
757
+ verdict: relativeWorkspacePath(bundle.verdictPath, workspaceRoot),
758
+ manifest: existsSync(bundle.manifestPath) ? relativeWorkspacePath(bundle.manifestPath, workspaceRoot) : null,
759
+ packManifest: existsSync(bundle.packManifestPath) ? relativeWorkspacePath(bundle.packManifestPath, workspaceRoot) : null,
760
+ },
761
+ bundleManifestHash: existsSync(bundle.packManifestPath) ? hashFile(bundle.packManifestPath) : (existsSync(bundle.manifestPath) ? hashFile(bundle.manifestPath) : null),
762
+ findings,
763
+ severity,
764
+ verdict: findings.length === 0 ? "reviewable" : "rejected",
765
+ ok: findings.length === 0,
766
+ blockerCount,
767
+ warningCount,
768
+ sourceRefCount: sourceRefObjects.length,
769
+ evidenceRefCount: evidenceRefs.length,
770
+ sourceRefs: sourceRefObjects,
771
+ evidenceRefs,
772
+ releaseState,
773
+ };
774
+ }
775
+
776
+ export function buildGraphifyDeterministicLintBundle(options = {}) {
777
+ const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot);
778
+ const workspaceRoot = path.resolve(options.workspaceRoot ?? path.resolve(repoRoot, ".."));
779
+ const bundleRoot = path.resolve(options.bundleRoot ?? options.inputBundleRoot ?? options.bundleDir ?? options.bundlePath ?? "");
780
+ if (!existsSync(bundleRoot)) {
781
+ throw new Error(`bundle root does not exist: ${bundleRoot}`);
782
+ }
783
+ const report = buildNormalizedReport(bundleRoot, repoRoot, workspaceRoot);
784
+ const runId = options.runId ?? `graphify-lint-${Date.now()}`;
785
+ const outputRoot = path.resolve(options.outputRoot ?? path.join(defaultOutputRoot, runId));
786
+ ensureDir(outputRoot);
787
+ const bundlePaths = {
788
+ deterministicLints: path.join(outputRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.deterministicLints),
789
+ summary: path.join(outputRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.summary),
790
+ proposalEnvelope: path.join(outputRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.proposalEnvelope),
791
+ verdict: path.join(outputRoot, GRAPHIFY_DETERMINISTIC_LINT_BUNDLE_LAYOUT.verdict),
792
+ };
793
+ report.outputRoot = relativeWorkspacePath(outputRoot, workspaceRoot);
794
+ const proposalEnvelope = buildProposalEnvelope(report, Object.fromEntries(Object.entries(bundlePaths).map(([key, value]) => [key, relativeWorkspacePath(value, workspaceRoot)])));
795
+ const verdict = buildVerdict(report, Object.fromEntries(Object.entries(bundlePaths).map(([key, value]) => [key, relativeWorkspacePath(value, workspaceRoot)])));
796
+ const summary = buildSummaryMarkdown(report);
797
+ const deterministicLints = {
798
+ ...report,
799
+ outputRoot: relativeWorkspacePath(outputRoot, workspaceRoot),
800
+ bundlePaths: Object.fromEntries(Object.entries(bundlePaths).map(([key, value]) => [key, relativeWorkspacePath(value, workspaceRoot)])),
801
+ };
802
+ writeJson(bundlePaths.deterministicLints, deterministicLints);
803
+ writeText(bundlePaths.summary, summary);
804
+ writeJson(bundlePaths.proposalEnvelope, proposalEnvelope);
805
+ writeJson(bundlePaths.verdict, verdict);
806
+ return {
807
+ ok: report.ok,
808
+ runId,
809
+ outputRoot,
810
+ bundleRoot,
811
+ repoRoot,
812
+ workspaceRoot,
813
+ report: deterministicLints,
814
+ proposalEnvelope,
815
+ verdict,
816
+ paths: bundlePaths,
817
+ summary,
818
+ };
819
+ }
820
+
821
+ export function parseGraphifyDeterministicLintCliArgs(argv) {
822
+ let bundleRoot = null;
823
+ let repoRoot = defaultRepoRoot;
824
+ let workspaceRoot = defaultWorkspaceRoot;
825
+ let outputRoot = null;
826
+ let runId = null;
827
+ let json = false;
828
+ let help = false;
829
+ for (let index = 0; index < argv.length; index += 1) {
830
+ const arg = argv[index];
831
+ if (arg === "--help" || arg === "-h") {
832
+ help = true;
833
+ continue;
834
+ }
835
+ if (arg === "--json") {
836
+ json = true;
837
+ continue;
838
+ }
839
+ if (arg === "--bundle-root") {
840
+ const next = argv[index + 1];
841
+ if (next === undefined) {
842
+ throw new Error("--bundle-root requires a value");
843
+ }
844
+ bundleRoot = next;
845
+ index += 1;
846
+ continue;
847
+ }
848
+ if (arg === "--repo-root") {
849
+ const next = argv[index + 1];
850
+ if (next === undefined) {
851
+ throw new Error("--repo-root requires a value");
852
+ }
853
+ repoRoot = next;
854
+ index += 1;
855
+ continue;
856
+ }
857
+ if (arg === "--workspace-root") {
858
+ const next = argv[index + 1];
859
+ if (next === undefined) {
860
+ throw new Error("--workspace-root requires a value");
861
+ }
862
+ workspaceRoot = next;
863
+ index += 1;
864
+ continue;
865
+ }
866
+ if (arg === "--output-root") {
867
+ const next = argv[index + 1];
868
+ if (next === undefined) {
869
+ throw new Error("--output-root requires a value");
870
+ }
871
+ outputRoot = next;
872
+ index += 1;
873
+ continue;
874
+ }
875
+ if (arg === "--run-id") {
876
+ const next = argv[index + 1];
877
+ if (next === undefined) {
878
+ throw new Error("--run-id requires a value");
879
+ }
880
+ runId = next;
881
+ index += 1;
882
+ continue;
883
+ }
884
+ throw new Error(`unknown argument for graphify-lints: ${arg}`);
885
+ }
886
+ return {
887
+ command: "graphify-lints",
888
+ bundleRoot: bundleRoot === null ? null : path.resolve(bundleRoot),
889
+ repoRoot: path.resolve(repoRoot),
890
+ workspaceRoot: path.resolve(workspaceRoot),
891
+ outputRoot: outputRoot === null ? null : path.resolve(outputRoot),
892
+ runId,
893
+ json,
894
+ help,
895
+ };
896
+ }
897
+
898
+ export function formatGraphifyDeterministicLintSummary(result) {
899
+ const lines = [
900
+ "GRAPHIFY deterministic pre-lint",
901
+ `bundle: ${result.report.bundleId}`,
902
+ `proposal: ${result.report.proposalId} (${result.report.proposalClass}, ${result.report.reviewMode})`,
903
+ `verdict: ${result.verdict.verdict} (${result.verdict.severity})`,
904
+ `findings: ${result.report.findings.length} (blockers=${result.report.blockerCount}, warnings=${result.report.warningCount})`,
905
+ `output: ${result.paths.summary}`,
906
+ `proposal-envelope: ${result.paths.proposalEnvelope}`,
907
+ `deterministic-lints: ${result.paths.deterministicLints}`,
908
+ `verdict-json: ${result.paths.verdict}`,
909
+ ];
910
+ if (result.report.findings.length > 0) {
911
+ lines.push("findings:");
912
+ for (const finding of result.report.findings.slice(0, 8)) {
913
+ lines.push(` - ${finding.code}: ${finding.summary}`);
914
+ }
915
+ }
916
+ return `${lines.join("\n")}\n`;
917
+ }
918
+
919
+ export function runGraphifyDeterministicLints(argvOrOptions = {}) {
920
+ const parsed = Array.isArray(argvOrOptions)
921
+ ? parseGraphifyDeterministicLintCliArgs(argvOrOptions)
922
+ : { command: "graphify-lints", json: false, help: false, ...argvOrOptions };
923
+ if (parsed.help) {
924
+ return {
925
+ ok: true,
926
+ help: true,
927
+ summary: "",
928
+ report: null,
929
+ verdict: null,
930
+ proposalEnvelope: null,
931
+ paths: null,
932
+ outputRoot: null,
933
+ bundleRoot: null,
934
+ repoRoot: null,
935
+ workspaceRoot: null,
936
+ runId: null,
937
+ };
938
+ }
939
+ const bundleRoot = parsed.bundleRoot ?? parsed.inputBundleRoot ?? parsed.bundlePath ?? parsed.bundleDir;
940
+ if (bundleRoot === null || bundleRoot === undefined) {
941
+ throw new Error("graphify-lints requires --bundle-root <path>");
942
+ }
943
+ const result = buildGraphifyDeterministicLintBundle({
944
+ bundleRoot,
945
+ repoRoot: parsed.repoRoot,
946
+ workspaceRoot: parsed.workspaceRoot,
947
+ outputRoot: parsed.outputRoot,
948
+ runId: parsed.runId,
949
+ });
950
+ return {
951
+ ...result,
952
+ json: Boolean(parsed.json),
953
+ summary: formatGraphifyDeterministicLintSummary(result),
954
+ };
955
+ }
956
+
957
+ function main() {
958
+ const result = runGraphifyDeterministicLints(process.argv.slice(2));
959
+ if (result.help) {
960
+ process.stdout.write([
961
+ "Usage:",
962
+ " node scripts/graphify-lints.mjs --bundle-root <path> [--repo-root <path>] [--workspace-root <path>] [--output-root <path>] [--run-id <id>] [--json]",
963
+ "",
964
+ "This deterministic pre-lint writes bounded review/proposal surfaces only.",
965
+ ].join("\n") + "\n");
966
+ return;
967
+ }
968
+ if (result.json) {
969
+ process.stdout.write(`${stableJson({ ok: result.ok, report: result.report, verdict: result.verdict, proposalEnvelope: result.proposalEnvelope, paths: result.paths })}`);
970
+ return;
971
+ }
972
+ process.stdout.write(result.summary);
973
+ }
974
+
975
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
976
+ main();
977
+ }