@yawlabs/ctxlint 0.18.7 → 0.19.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/dist/index.js CHANGED
@@ -13770,8 +13770,8 @@ function extractPathReferences(lines, sections) {
13770
13770
  if (inCodeBlock && codeBlockLang === "" && /^\s/.test(line) && /^[\w@.-]+\/$/.test(cleanValue)) {
13771
13771
  continue;
13772
13772
  }
13773
- const lowerProse = /^([a-z][\w-]*)\/[a-z][\w-]*$/.exec(cleanValue);
13774
- if (lowerProse && !PATH_FIRST_SEGMENTS.has(lowerProse[1])) {
13773
+ const twoSegmentProse = /^([A-Za-z][\w-]*)\/[A-Za-z][\w-]*$/.exec(cleanValue);
13774
+ if (twoSegmentProse && !PATH_FIRST_SEGMENTS.has(twoSegmentProse[1].toLowerCase())) {
13775
13775
  continue;
13776
13776
  }
13777
13777
  if (/^[a-z][\w-]*(?:\/[a-z][\w-]*){2,}$/.test(cleanValue)) continue;
@@ -19966,8 +19966,8 @@ async function findRenames(projectRoot, filePath) {
19966
19966
  async function findRenamesBatch(projectRoot, relPaths) {
19967
19967
  const result = /* @__PURE__ */ new Map();
19968
19968
  if (relPaths.length === 0) return result;
19969
- let cache = renameCache.get(projectRoot);
19970
- if (!cache) {
19969
+ let cache2 = renameCache.get(projectRoot);
19970
+ if (!cache2) {
19971
19971
  try {
19972
19972
  const git = getGit(projectRoot);
19973
19973
  const rawOutput = await git.raw([
@@ -19985,14 +19985,14 @@ async function findRenamesBatch(projectRoot, relPaths) {
19985
19985
  } catch {
19986
19986
  }
19987
19987
  const { allRenames: allRenames2, basenameBuckets: basenameBuckets2 } = parseRenameLogAll(rawOutput);
19988
- cache = { allRenames: allRenames2, basenameBuckets: basenameBuckets2, prefix: prefix2 };
19989
- renameCache.set(projectRoot, cache);
19988
+ cache2 = { allRenames: allRenames2, basenameBuckets: basenameBuckets2, prefix: prefix2 };
19989
+ renameCache.set(projectRoot, cache2);
19990
19990
  } catch {
19991
19991
  for (const p2 of relPaths) result.set(p2, null);
19992
19992
  return result;
19993
19993
  }
19994
19994
  }
19995
- const { allRenames, basenameBuckets, prefix } = cache;
19995
+ const { allRenames, basenameBuckets, prefix } = cache2;
19996
19996
  for (const relPath of relPaths) {
19997
19997
  const rel = relPath.replace(/\\/g, "/");
19998
19998
  const target = prefix + rel;
@@ -22130,6 +22130,50 @@ var init_staleness = __esm({
22130
22130
  }
22131
22131
  });
22132
22132
 
22133
+ // src/core/suppressions.ts
22134
+ function collectSuppressions(content) {
22135
+ const suppressions = /* @__PURE__ */ new Map();
22136
+ const lines = content.split("\n");
22137
+ const add = (lineNumber, checks) => {
22138
+ const existing = suppressions.get(lineNumber);
22139
+ if (existing === null) return;
22140
+ if (checks.length === 0) {
22141
+ suppressions.set(lineNumber, null);
22142
+ return;
22143
+ }
22144
+ const merged = existing ?? /* @__PURE__ */ new Set();
22145
+ for (const check2 of checks) merged.add(check2);
22146
+ suppressions.set(lineNumber, merged);
22147
+ };
22148
+ for (let i2 = 0; i2 < lines.length; i2++) {
22149
+ const line = lines[i2];
22150
+ if (!line.includes("ctxlint-ignore-")) continue;
22151
+ DIRECTIVE_RE.lastIndex = 0;
22152
+ let match;
22153
+ while ((match = DIRECTIVE_RE.exec(line)) !== null) {
22154
+ const kind = match[1];
22155
+ const checks = (match[2] ?? "").trim().split(/\s+/).filter(Boolean);
22156
+ add(kind === "next-line" ? i2 + 2 : i2 + 1, checks);
22157
+ }
22158
+ }
22159
+ return suppressions;
22160
+ }
22161
+ function isSuppressed(suppressions, issue2) {
22162
+ if (suppressions.size === 0) return false;
22163
+ if (!suppressions.has(issue2.line)) return false;
22164
+ const checks = suppressions.get(issue2.line);
22165
+ if (checks === null) return true;
22166
+ return checks.has(issue2.check);
22167
+ }
22168
+ var DIRECTIVE_RE;
22169
+ var init_suppressions = __esm({
22170
+ "src/core/suppressions.ts"() {
22171
+ "use strict";
22172
+ init_define_WEB_FIRST_SEGMENTS();
22173
+ DIRECTIVE_RE = /<!--\s*ctxlint-ignore-(next-line|line)((?:\s+[\w-]+)*)\s*-->/g;
22174
+ }
22175
+ });
22176
+
22133
22177
  // src/core/checks/tokens.ts
22134
22178
  function resolveTokenThresholds(overrides) {
22135
22179
  if (!overrides) return DEFAULT_TOKEN_THRESHOLDS;
@@ -24081,6 +24125,11 @@ function loadWebFirstSegments() {
24081
24125
  function encodeProjectDir(fsPath) {
24082
24126
  return fsPath.replace(/[:\\/\.]/g, "-");
24083
24127
  }
24128
+ function projectDirCandidates(fsPath) {
24129
+ const encoded = encodeProjectDir(fsPath);
24130
+ const folded = encoded.replace(/_/g, "-");
24131
+ return folded === encoded ? [encoded] : [encoded, folded];
24132
+ }
24084
24133
  function projectDirMatchesPath(encodedDir, fsPath) {
24085
24134
  const normalized = fsPath.replace(/\\/g, "/");
24086
24135
  return encodedDir === encodeProjectDir(normalized);
@@ -24967,13 +25016,19 @@ import { join as join8 } from "node:path";
24967
25016
  async function checkMemoryIndexOverflow(ctx) {
24968
25017
  const home2 = process.env.HOME || process.env.USERPROFILE || homedir5();
24969
25018
  if (!home2) return [];
24970
- const encoded = encodeProjectDir(ctx.currentProject);
24971
- const memoryFile = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
24972
- let content;
24973
- try {
24974
- content = stripBom(await readFile3(memoryFile, "utf-8"));
24975
- } catch {
24976
- return [];
25019
+ let content = "";
25020
+ let memoryFile = "";
25021
+ for (const encoded of projectDirCandidates(ctx.currentProject)) {
25022
+ const candidate = join8(home2, ".claude", "projects", encoded, "memory", "MEMORY.md");
25023
+ try {
25024
+ content = stripBom(await readFile3(candidate, "utf-8"));
25025
+ } catch {
25026
+ continue;
25027
+ }
25028
+ if (content) {
25029
+ memoryFile = candidate;
25030
+ break;
25031
+ }
24977
25032
  }
24978
25033
  if (!content) return [];
24979
25034
  const lines = content.split("\n");
@@ -25018,16 +25073,429 @@ var init_memory_index_overflow = __esm({
25018
25073
  }
25019
25074
  });
25020
25075
 
25021
- // src/core/checks/ci-coverage.ts
25022
- import { readdir as readdir3, readFile as readFile4 } from "node:fs/promises";
25076
+ // src/core/transcript.ts
25077
+ import { createReadStream as createReadStream2, existsSync as existsSync5, statSync as statSync4 } from "node:fs";
25078
+ import { readdir as readdir3 } from "node:fs/promises";
25079
+ import { homedir as homedir6 } from "node:os";
25023
25080
  import { join as join9 } from "node:path";
25024
- import { existsSync as existsSync5 } from "node:fs";
25081
+ import { createInterface as createInterface2 } from "node:readline";
25082
+ function asString(v2) {
25083
+ return typeof v2 === "string" ? v2 : "";
25084
+ }
25085
+ function resultText(content) {
25086
+ if (typeof content === "string") return content;
25087
+ if (!Array.isArray(content)) return "";
25088
+ return content.map((b2) => b2 && typeof b2 === "object" ? asString(b2.text) : "").join("");
25089
+ }
25090
+ function resolveHome() {
25091
+ return process.env.HOME || process.env.USERPROFILE || homedir6();
25092
+ }
25093
+ function readProjectTranscript(project, home2 = resolveHome()) {
25094
+ const key = `${home2} ${project}`;
25095
+ let hit = cache.get(key);
25096
+ if (!hit) {
25097
+ hit = readUncached(project, home2);
25098
+ cache.set(key, hit);
25099
+ }
25100
+ return hit;
25101
+ }
25102
+ function candidateDirs(project, home2) {
25103
+ const root = join9(home2, ".claude", "projects");
25104
+ return projectDirCandidates(project).map((n7) => join9(root, n7)).filter((d) => existsSync5(d));
25105
+ }
25106
+ async function readUncached(project, home2) {
25107
+ if (!home2 || !project) return EMPTY;
25108
+ const dirs = candidateDirs(project, home2);
25109
+ if (dirs.length === 0) return EMPTY;
25110
+ const names = [];
25111
+ for (const dir of dirs) {
25112
+ for (const name of await readdir3(dir).catch(() => [])) {
25113
+ names.push({ dir, name });
25114
+ }
25115
+ }
25116
+ const files = names.filter(({ name }) => name.endsWith(".jsonl")).map(({ dir, name }) => {
25117
+ const p2 = join9(dir, name);
25118
+ try {
25119
+ return { p: p2, mtime: statSync4(p2).mtimeMs };
25120
+ } catch {
25121
+ return null;
25122
+ }
25123
+ }).filter((f) => f !== null).sort((a, b2) => b2.mtime - a.mtime);
25124
+ let truncated = files.length > MAX_TRANSCRIPTS;
25125
+ const selected = files.slice(0, MAX_TRANSCRIPTS);
25126
+ const events = [];
25127
+ const pending = /* @__PURE__ */ new Map();
25128
+ let lines = 0;
25129
+ for (const { p: p2 } of selected) {
25130
+ if (lines >= MAX_LINES2) {
25131
+ truncated = true;
25132
+ break;
25133
+ }
25134
+ const rl = createInterface2({
25135
+ input: createReadStream2(p2, { encoding: "utf-8" }),
25136
+ crlfDelay: Infinity
25137
+ });
25138
+ try {
25139
+ for await (const raw of rl) {
25140
+ if (++lines >= MAX_LINES2) {
25141
+ truncated = true;
25142
+ break;
25143
+ }
25144
+ const line = raw.trim();
25145
+ if (!line) continue;
25146
+ let rec;
25147
+ try {
25148
+ rec = JSON.parse(line);
25149
+ } catch {
25150
+ continue;
25151
+ }
25152
+ collect(rec, events, pending);
25153
+ }
25154
+ } catch {
25155
+ } finally {
25156
+ rl.close();
25157
+ }
25158
+ }
25159
+ return { events, filesRead: selected.length, truncated };
25160
+ }
25161
+ function collect(rec, events, pending) {
25162
+ const message = rec.message;
25163
+ const content = message?.content;
25164
+ if (!Array.isArray(content)) return;
25165
+ const timestamp = Date.parse(asString(rec.timestamp)) || 0;
25166
+ const sessionId = asString(rec.sessionId) || asString(rec.session_id);
25167
+ const gitBranch = asString(rec.gitBranch) || void 0;
25168
+ for (const raw of content) {
25169
+ if (!raw || typeof raw !== "object") continue;
25170
+ const block = raw;
25171
+ const type = asString(block.type);
25172
+ if (type === "text" && rec.type === "assistant") {
25173
+ const text = asString(block.text);
25174
+ if (text) {
25175
+ events.push({ kind: "assistant-text", text, tool: "", gitBranch, timestamp, sessionId });
25176
+ }
25177
+ continue;
25178
+ }
25179
+ if (type === "tool_use") {
25180
+ const tool = asString(block.name);
25181
+ const input = block.input ?? {};
25182
+ const cmdField = COMMAND_TOOLS[tool];
25183
+ const writeField = WRITE_TOOLS[tool];
25184
+ let ev = null;
25185
+ if (cmdField) {
25186
+ const text = asString(input[cmdField]);
25187
+ if (text) ev = { kind: "command", text, tool, gitBranch, timestamp, sessionId };
25188
+ } else if (writeField) {
25189
+ const text = asString(input[writeField]);
25190
+ if (text) ev = { kind: "file-write", text, tool, gitBranch, timestamp, sessionId };
25191
+ }
25192
+ if (ev) {
25193
+ events.push(ev);
25194
+ const id = asString(block.id);
25195
+ if (id) pending.set(id, ev);
25196
+ }
25197
+ continue;
25198
+ }
25199
+ if (type === "tool_result") {
25200
+ const id = asString(block.tool_use_id);
25201
+ const ev = id ? pending.get(id) : void 0;
25202
+ if (!ev) continue;
25203
+ pending.delete(id);
25204
+ ev.isError = block.is_error === true;
25205
+ ev.emptyOutput = resultText(block.content).trim().length === 0;
25206
+ }
25207
+ }
25208
+ }
25209
+ var WRITE_TOOLS, COMMAND_TOOLS, MAX_TRANSCRIPTS, MAX_LINES2, EMPTY, cache;
25210
+ var init_transcript = __esm({
25211
+ "src/core/transcript.ts"() {
25212
+ "use strict";
25213
+ init_define_WEB_FIRST_SEGMENTS();
25214
+ init_session_parser();
25215
+ WRITE_TOOLS = {
25216
+ Write: "file_path",
25217
+ Edit: "file_path",
25218
+ NotebookEdit: "notebook_path"
25219
+ };
25220
+ COMMAND_TOOLS = {
25221
+ Bash: "command",
25222
+ PowerShell: "command"
25223
+ };
25224
+ MAX_TRANSCRIPTS = 5;
25225
+ MAX_LINES2 = 2e5;
25226
+ EMPTY = { events: [], filesRead: 0, truncated: false };
25227
+ cache = /* @__PURE__ */ new Map();
25228
+ }
25229
+ });
25230
+
25231
+ // src/core/checks/session/shared-temp-path.ts
25232
+ function unquote(raw) {
25233
+ let s = raw;
25234
+ let prev;
25235
+ do {
25236
+ prev = s;
25237
+ s = s.replace(/^['"`]|['"`]$/g, "");
25238
+ } while (s !== prev);
25239
+ return s;
25240
+ }
25241
+ function normalize3(raw) {
25242
+ return unquote(raw).replace(/\\/g, "/").toLowerCase();
25243
+ }
25244
+ function isFixedTempPath(candidate) {
25245
+ const p2 = normalize3(candidate);
25246
+ const root = TEMP_ROOTS.map((r2) => normalize3(r2)).find((r2) => p2.startsWith(r2));
25247
+ if (!root) return false;
25248
+ if (p2.length <= root.length) return false;
25249
+ return !SCOPED_MARKERS.some((m) => p2.includes(m.toLowerCase()));
25250
+ }
25251
+ function extract(line, patterns) {
25252
+ const found = [];
25253
+ for (const re2 of patterns) {
25254
+ for (const m of line.matchAll(new RegExp(re2, "g"))) {
25255
+ if (m[1]) found.push(m[1]);
25256
+ }
25257
+ }
25258
+ return found;
25259
+ }
25260
+ async function checkSharedTempPath(ctx) {
25261
+ const issues = [];
25262
+ const written = /* @__PURE__ */ new Map();
25263
+ const reported = /* @__PURE__ */ new Set();
25264
+ const transcript = await readProjectTranscript(ctx.currentProject);
25265
+ const scanned = [
25266
+ ...transcript.events.filter((e) => e.kind === "command").map((e) => ({ display: e.text, timestamp: e.timestamp })),
25267
+ ...ctx.history.map((h2) => ({ display: h2.display, timestamp: h2.timestamp }))
25268
+ ];
25269
+ const ordered = scanned.sort((a, b2) => a.timestamp - b2.timestamp);
25270
+ for (const entry of ordered) {
25271
+ const line = entry.display;
25272
+ const usesMktemp = /\bmktemp\b/.test(line);
25273
+ for (const candidate of extract(line, READ_PATTERNS)) {
25274
+ if (usesMktemp || !isFixedTempPath(candidate)) continue;
25275
+ const key = normalize3(candidate);
25276
+ const origin = written.get(key);
25277
+ if (!origin || reported.has(key)) continue;
25278
+ reported.add(key);
25279
+ issues.push({
25280
+ severity: "error",
25281
+ check: "session-shared-temp-path",
25282
+ ruleId: "session-shared-temp-path/shared-temp-path",
25283
+ line: 0,
25284
+ message: `Fixed temp path "${unquote(candidate)}" is written and later read back`,
25285
+ detail: `Written by: ${origin.display.trim().slice(0, 120)}
25286
+ Read by: ${line.trim().slice(0, 120)}`,
25287
+ suggestion: "Use a per-run path instead: `T=$(mktemp)` in shell, or a session-scoped scratch directory. A literal path under a shared temp root can be overwritten by any concurrent session between the write and the read, and a restore then puts the wrong bytes into your workspace."
25288
+ });
25289
+ }
25290
+ for (const candidate of extract(line, WRITE_PATTERNS)) {
25291
+ if (usesMktemp || !isFixedTempPath(candidate)) continue;
25292
+ written.set(normalize3(candidate), entry);
25293
+ }
25294
+ }
25295
+ return issues;
25296
+ }
25297
+ var TEMP_ROOTS, SCOPED_MARKERS, WRITE_PATTERNS, READ_PATTERNS;
25298
+ var init_shared_temp_path = __esm({
25299
+ "src/core/checks/session/shared-temp-path.ts"() {
25300
+ "use strict";
25301
+ init_define_WEB_FIRST_SEGMENTS();
25302
+ init_transcript();
25303
+ TEMP_ROOTS = [
25304
+ "/tmp/",
25305
+ "/var/tmp/",
25306
+ "$TMPDIR/",
25307
+ "${TMPDIR}/",
25308
+ "%TEMP%\\",
25309
+ "%TMP%\\"
25310
+ ];
25311
+ SCOPED_MARKERS = [
25312
+ "$$",
25313
+ "$pid",
25314
+ "${pid}",
25315
+ "mktemp",
25316
+ "$random",
25317
+ "${random}",
25318
+ "$session",
25319
+ "${session}",
25320
+ "$uuid",
25321
+ "${uuid}"
25322
+ ];
25323
+ WRITE_PATTERNS = [
25324
+ />\s*(\S+)/,
25325
+ // shell redirect
25326
+ /\bcp\s+\S+\s+(\S+)/,
25327
+ // cp src dest
25328
+ /\bmv\s+\S+\s+(\S+)/,
25329
+ /\bcopy\s+\S+\s+(\S+)/,
25330
+ /\bmove\s+\S+\s+(\S+)/,
25331
+ /\btee\s+(\S+)/,
25332
+ /writeFileSync\(\s*['"`]([^'"`]+)/,
25333
+ /\bcurl\b[^|]*-o\s+(\S+)/
25334
+ ];
25335
+ READ_PATTERNS = [
25336
+ /\bcp\s+(\S+)\s+\S+/,
25337
+ /\bcopy\s+(\S+)\s+\S+/,
25338
+ /\bcat\s+(\S+)/,
25339
+ /\btype\s+(\S+)/,
25340
+ /\bsource\s+(\S+)/,
25341
+ /\breadFileSync\(\s*['"`]([^'"`]+)/,
25342
+ // Input redirect. `(?!<)` excludes heredocs (`<<'EOF'`), and the leading
25343
+ // boundary plus the restricted character class keep it from matching a `<`
25344
+ // that is part of a comparison, an HTML fragment, or another operator --
25345
+ // a bare /<\s*(\S+)/ matched unrelated text in real transcripts.
25346
+ /(?:^|\s)<(?!<)\s*([^\s<>|&;]+)/
25347
+ ];
25348
+ }
25349
+ });
25350
+
25351
+ // src/core/checks/session/unverified-gate-claimed-clean.ts
25352
+ function gateOf(cmd) {
25353
+ for (const { re: re2, gate } of GATE_PATTERNS) {
25354
+ if (re2.test(cmd)) return gate;
25355
+ }
25356
+ return null;
25357
+ }
25358
+ function isUnverified(ev) {
25359
+ return ev.isError === true || ev.emptyOutput === true;
25360
+ }
25361
+ async function checkUnverifiedGateClaimedClean(ctx) {
25362
+ const { events } = await readProjectTranscript(ctx.currentProject);
25363
+ if (events.length === 0) return [];
25364
+ const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
25365
+ const issues = [];
25366
+ const reported = /* @__PURE__ */ new Set();
25367
+ for (let i2 = 0; i2 < ordered.length; i2++) {
25368
+ const ev = ordered[i2];
25369
+ if (ev.kind !== "command") continue;
25370
+ const gate = gateOf(ev.text);
25371
+ if (!gate || !isUnverified(ev)) continue;
25372
+ for (let j3 = i2 + 1; j3 < ordered.length && j3 <= i2 + ADJACENCY; j3++) {
25373
+ const next = ordered[j3];
25374
+ if (next.kind === "command" && gateOf(next.text) === gate) break;
25375
+ if (next.kind !== "assistant-text") continue;
25376
+ if (HONEST_MARKERS.some((re2) => re2.test(next.text))) break;
25377
+ if (!CLEAN_CLAIMS.some((re2) => re2.test(next.text))) continue;
25378
+ if (reported.has(gate)) break;
25379
+ reported.add(gate);
25380
+ const why = ev.isError ? "the invocation reported an error" : "the invocation produced no output";
25381
+ issues.push({
25382
+ severity: "warning",
25383
+ check: "session-unverified-gate-claimed-clean",
25384
+ ruleId: "session-unverified-gate-claimed-clean/unverified-gate-claimed-clean",
25385
+ line: 0,
25386
+ message: `'${gate}' asserted as passing, but ${why}`,
25387
+ detail: `Gate command: ${ev.text.trim().slice(0, 120)}
25388
+ Claim: ${next.text.trim().slice(0, 120)}`,
25389
+ suggestion: 'An empty or failed gate carries no information about cleanliness -- a runner that dies before emitting diagnostics looks exactly like one that found none. Either re-run the gate until it produces a real result, or report the state honestly ("lint UNVERIFIED -- runner crashed").'
25390
+ });
25391
+ break;
25392
+ }
25393
+ }
25394
+ return issues;
25395
+ }
25396
+ var GATE_PATTERNS, CLEAN_CLAIMS, HONEST_MARKERS, ADJACENCY;
25397
+ var init_unverified_gate_claimed_clean = __esm({
25398
+ "src/core/checks/session/unverified-gate-claimed-clean.ts"() {
25399
+ "use strict";
25400
+ init_define_WEB_FIRST_SEGMENTS();
25401
+ init_transcript();
25402
+ GATE_PATTERNS = [
25403
+ { re: /\b(biome|eslint|ruff|clippy)\b/, gate: "lint" },
25404
+ { re: /\blint(:fix)?\b/, gate: "lint" },
25405
+ { re: /\btsc\b|\btypecheck\b|\btype-check\b/, gate: "typecheck" },
25406
+ { re: /\b(vitest|jest|pytest|mocha)\b/, gate: "test" },
25407
+ { re: /\btest\b/, gate: "test" },
25408
+ { re: /\bbuild\b/, gate: "build" }
25409
+ ];
25410
+ CLEAN_CLAIMS = [
25411
+ /\ball green\b/i,
25412
+ /\bno (lint )?(violations|errors|issues|diagnostics)\b/i,
25413
+ /\b0 errors\b/i,
25414
+ /\bzero (errors|violations|diagnostics)\b/i,
25415
+ /\b(lint|typecheck|type-check|tests?|build)\b[^.\n]{0,40}\b(clean|passe[sd]|passing|green|succeeded)\b/i,
25416
+ /\b(clean|passing|green)\b[^.\n]{0,25}\b(lint|typecheck|type-check|tests?|build)\b/i
25417
+ ];
25418
+ HONEST_MARKERS = [
25419
+ /\bunverified\b/i,
25420
+ /\bcould not verify\b/i,
25421
+ /\bcannot verify\b/i,
25422
+ /\bunable to verify\b/i,
25423
+ /\bcrashed?\b/i,
25424
+ /\bsegfault/i,
25425
+ /\bblocked\b/i,
25426
+ /\bnot trustworthy\b/i,
25427
+ /\bno signal\b/i,
25428
+ /\bdid not run\b/i,
25429
+ /\binconclusive\b/i
25430
+ ];
25431
+ ADJACENCY = 12;
25432
+ }
25433
+ });
25434
+
25435
+ // src/core/checks/session/default-branch-accumulation.ts
25436
+ function isCommit(cmd) {
25437
+ return /\bgit\s+commit\b/.test(cmd) && !/--dry-run\b/.test(cmd);
25438
+ }
25439
+ function isBranchAway(cmd) {
25440
+ return /\bgit\s+(checkout\s+-b|switch\s+-c|worktree\s+add)\b/.test(cmd);
25441
+ }
25442
+ async function checkDefaultBranchAccumulation(ctx) {
25443
+ const { events } = await readProjectTranscript(ctx.currentProject);
25444
+ if (events.length === 0) return [];
25445
+ const ordered = [...events].sort((a, b2) => a.timestamp - b2.timestamp);
25446
+ const pending = /* @__PURE__ */ new Set();
25447
+ let branch = "";
25448
+ let firstWrite = "";
25449
+ for (const ev of ordered) {
25450
+ if (ev.gitBranch) branch = ev.gitBranch;
25451
+ if (ev.kind === "command") {
25452
+ if (isCommit(ev.text) || isBranchAway(ev.text)) {
25453
+ pending.clear();
25454
+ firstWrite = "";
25455
+ }
25456
+ continue;
25457
+ }
25458
+ if (ev.kind !== "file-write") continue;
25459
+ if (!DEFAULT_BRANCHES.has(branch)) continue;
25460
+ if (pending.size === 0) firstWrite = ev.text;
25461
+ pending.add(ev.text);
25462
+ }
25463
+ if (pending.size < THRESHOLD) return [];
25464
+ const sample = [...pending].slice(0, 5);
25465
+ return [
25466
+ {
25467
+ severity: "warning",
25468
+ check: "session-default-branch-accumulation",
25469
+ ruleId: "session-default-branch-accumulation/default-branch-accumulation",
25470
+ line: 0,
25471
+ message: `${pending.size} files edited on '${branch}' with no intervening commit`,
25472
+ detail: `First uncommitted write: ${firstWrite}
25473
+ Sample: ${sample.join(", ")}${pending.size > sample.length ? ", ..." : ""}`,
25474
+ suggestion: "Commit as you go, or move the work to a topic branch (`git checkout -b`). A large uncommitted delta on a shared default branch is hard to review and, when other agent sessions share the checkout, one `git stash` or `git checkout --` away from being lost."
25475
+ }
25476
+ ];
25477
+ }
25478
+ var DEFAULT_BRANCHES, THRESHOLD;
25479
+ var init_default_branch_accumulation = __esm({
25480
+ "src/core/checks/session/default-branch-accumulation.ts"() {
25481
+ "use strict";
25482
+ init_define_WEB_FIRST_SEGMENTS();
25483
+ init_transcript();
25484
+ DEFAULT_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
25485
+ THRESHOLD = 10;
25486
+ }
25487
+ });
25488
+
25489
+ // src/core/checks/ci-coverage.ts
25490
+ import { readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
25491
+ import { join as join10 } from "node:path";
25492
+ import { existsSync as existsSync6 } from "node:fs";
25025
25493
  async function findReleaseWorkflows(projectRoot) {
25026
- const workflowDir = join9(projectRoot, ".github", "workflows");
25027
- if (!existsSync5(workflowDir)) return [];
25494
+ const workflowDir = join10(projectRoot, ".github", "workflows");
25495
+ if (!existsSync6(workflowDir)) return [];
25028
25496
  let files;
25029
25497
  try {
25030
- files = await readdir3(workflowDir);
25498
+ files = await readdir4(workflowDir);
25031
25499
  } catch {
25032
25500
  return [];
25033
25501
  }
@@ -25039,7 +25507,7 @@ async function findReleaseWorkflows(projectRoot) {
25039
25507
  continue;
25040
25508
  }
25041
25509
  try {
25042
- const content = stripBom(await readFile4(join9(workflowDir, f), "utf-8"));
25510
+ const content = stripBom(await readFile4(join10(workflowDir, f), "utf-8"));
25043
25511
  const nameMatch = content.match(/^\s*name:\s*(.+?)\s*$/m);
25044
25512
  if (nameMatch) {
25045
25513
  const name = nameMatch[1].replace(/^(['"])(.*)\1$/, "$2");
@@ -25098,15 +25566,15 @@ var init_ci_coverage = __esm({
25098
25566
  });
25099
25567
 
25100
25568
  // src/core/checks/ci-secrets.ts
25101
- import { readdir as readdir4, readFile as readFile5 } from "node:fs/promises";
25102
- import { join as join10 } from "node:path";
25103
- import { existsSync as existsSync6 } from "node:fs";
25569
+ import { readdir as readdir5, readFile as readFile5 } from "node:fs/promises";
25570
+ import { join as join11 } from "node:path";
25571
+ import { existsSync as existsSync7 } from "node:fs";
25104
25572
  async function findSecretUsages(projectRoot) {
25105
- const workflowDir = join10(projectRoot, ".github", "workflows");
25106
- if (!existsSync6(workflowDir)) return [];
25573
+ const workflowDir = join11(projectRoot, ".github", "workflows");
25574
+ if (!existsSync7(workflowDir)) return [];
25107
25575
  let files;
25108
25576
  try {
25109
- files = await readdir4(workflowDir);
25577
+ files = await readdir5(workflowDir);
25110
25578
  } catch {
25111
25579
  return [];
25112
25580
  }
@@ -25115,7 +25583,7 @@ async function findSecretUsages(projectRoot) {
25115
25583
  if (!(f.endsWith(".yml") || f.endsWith(".yaml"))) continue;
25116
25584
  let content;
25117
25585
  try {
25118
- content = stripBom(await readFile5(join10(workflowDir, f), "utf-8"));
25586
+ content = stripBom(await readFile5(join11(workflowDir, f), "utf-8"));
25119
25587
  } catch {
25120
25588
  continue;
25121
25589
  }
@@ -25504,9 +25972,24 @@ function isMsysFlag(token) {
25504
25972
  }
25505
25973
  function translateMsysDrivePath(s, platform) {
25506
25974
  if (platform !== "win32") return s;
25507
- const m = /^\/([A-Za-z])\/(.*)$/.exec(s);
25975
+ const m = /^\/{1,2}([A-Za-z])\/(.*)$/.exec(s);
25508
25976
  return m ? `${m[1]}:/${m[2]}` : s;
25509
25977
  }
25978
+ function findExistingVariant(raw, projectRoot, homeDir, platform) {
25979
+ const token = stripMatcherWildcard(raw);
25980
+ const variants = /* @__PURE__ */ new Set();
25981
+ if (/^\/{2}[A-Za-z]\//.test(token)) variants.add(token.slice(1));
25982
+ if (/^\/[A-Za-z]\//.test(token)) variants.add(`/${token}`);
25983
+ if (token.startsWith("/") && !/^\/{1,2}[A-Za-z]\//.test(token)) {
25984
+ variants.add(token.replace(/^\/+/, ""));
25985
+ }
25986
+ if (token.includes("\\")) variants.add(token.replace(/\\/g, "/"));
25987
+ for (const v2 of variants) {
25988
+ const resolved = expandPath(v2, projectRoot, homeDir, platform);
25989
+ if (resolved !== null && fileExists(resolved)) return resolved;
25990
+ }
25991
+ return null;
25992
+ }
25510
25993
  function extractCommandPaths(command, projectRoot, homeDir, platform) {
25511
25994
  const tokens = command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
25512
25995
  const out = [];
@@ -25539,6 +26022,18 @@ function checkSource(source, projectRoot, homeDir, platform) {
25539
26022
  if (cand.resolved === null) return;
25540
26023
  if (fileExists(cand.resolved)) return;
25541
26024
  const where = source.isUserGlobal ? ` (in ${source.displayPath})` : "";
26025
+ const variant = findExistingVariant(cand.raw, projectRoot, homeDir, platform);
26026
+ if (variant !== null) {
26027
+ issues.push({
26028
+ severity: "warning",
26029
+ check: "hook-coverage",
26030
+ ruleId: "hook-coverage/dead-hook",
26031
+ line,
26032
+ message: `${origin} references "${cand.raw}" which does not resolve${where}, but "${variant}" exists \u2014 the path form is wrong, so the gate silently no-ops`,
26033
+ suggestion: `Rewrite the entry to use "${variant}". Do NOT delete it \u2014 the target exists; only the path form is unresolvable.`
26034
+ });
26035
+ return;
26036
+ }
25542
26037
  issues.push({
25543
26038
  severity: "warning",
25544
26039
  check: "hook-coverage",
@@ -27839,7 +28334,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
27839
28334
  import { resolve as resolve14, dirname as dirname6 } from "node:path";
27840
28335
  import { fileURLToPath as fileURLToPath2 } from "node:url";
27841
28336
  function loadVersion() {
27842
- if (true) return "0.18.6";
28337
+ if (true) return "0.18.7";
27843
28338
  try {
27844
28339
  const __dir = dirname6(fileURLToPath2(import.meta.url));
27845
28340
  const pkgPath = resolve14(__dir, "../package.json");
@@ -27928,7 +28423,8 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
27928
28423
  if (activeChecks.includes("content-secrets"))
27929
28424
  checkPromises.push(checkContentSecrets(parseResult, projectRoot));
27930
28425
  const results = await Promise.all(checkPromises);
27931
- const singleFileIssues = results.flat();
28426
+ const suppressions = collectSuppressions(parseResult.content);
28427
+ const singleFileIssues = results.flat().filter((issue2) => !isSuppressed(suppressions, issue2));
27932
28428
  if (stat3 !== null) {
27933
28429
  setCacheEntry(absPath, {
27934
28430
  mtime: stat3.mtimeMs,
@@ -28077,6 +28573,12 @@ async function runAudit(projectRoot, activeChecks, options = {}) {
28077
28573
  sessionPromises.push(checkLoopDetection(sessionCtx));
28078
28574
  if (sessionChecksToRun.includes("session-memory-index-overflow"))
28079
28575
  sessionPromises.push(checkMemoryIndexOverflow(sessionCtx));
28576
+ if (sessionChecksToRun.includes("session-shared-temp-path"))
28577
+ sessionPromises.push(checkSharedTempPath(sessionCtx));
28578
+ if (sessionChecksToRun.includes("session-unverified-gate-claimed-clean"))
28579
+ sessionPromises.push(checkUnverifiedGateClaimedClean(sessionCtx));
28580
+ if (sessionChecksToRun.includes("session-default-branch-accumulation"))
28581
+ sessionPromises.push(checkDefaultBranchAccumulation(sessionCtx));
28080
28582
  const sessionResults = await Promise.all(sessionPromises);
28081
28583
  const sessionIssues = sessionResults.flat();
28082
28584
  fileResults.push({
@@ -28255,6 +28757,7 @@ var init_audit = __esm({
28255
28757
  init_paths();
28256
28758
  init_commands();
28257
28759
  init_staleness();
28760
+ init_suppressions();
28258
28761
  init_tokens2();
28259
28762
  init_tier_tokens();
28260
28763
  init_redundancy();
@@ -28276,6 +28779,9 @@ var init_audit = __esm({
28276
28779
  init_duplicate_memory();
28277
28780
  init_loop_detection();
28278
28781
  init_memory_index_overflow();
28782
+ init_shared_temp_path();
28783
+ init_unverified_gate_claimed_clean();
28784
+ init_default_branch_accumulation();
28279
28785
  init_ci_coverage();
28280
28786
  init_ci_secrets();
28281
28787
  init_content_secrets();
@@ -28317,7 +28823,10 @@ var init_audit = __esm({
28317
28823
  "session-stale-memory",
28318
28824
  "session-duplicate-memory",
28319
28825
  "session-loop-detection",
28320
- "session-memory-index-overflow"
28826
+ "session-memory-index-overflow",
28827
+ "session-shared-temp-path",
28828
+ "session-unverified-gate-claimed-clean",
28829
+ "session-default-branch-accumulation"
28321
28830
  ];
28322
28831
  ALL_SKILL_CHECKS = [
28323
28832
  "skill-frontmatter",
@@ -54160,8 +54669,8 @@ var require_resolve = __commonJS({
54160
54669
  }
54161
54670
  return count;
54162
54671
  }
54163
- function getFullPath(resolver, id = "", normalize3) {
54164
- if (normalize3 !== false)
54672
+ function getFullPath(resolver, id = "", normalize4) {
54673
+ if (normalize4 !== false)
54165
54674
  id = normalizeId(id);
54166
54675
  const p2 = resolver.parse(id);
54167
54676
  return _getFullPath(resolver, p2);
@@ -55564,7 +56073,7 @@ var require_fast_uri = __commonJS({
55564
56073
  init_define_WEB_FIRST_SEGMENTS();
55565
56074
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils2();
55566
56075
  var { SCHEMES, getSchemeHandler } = require_schemes();
55567
- function normalize3(uri, options) {
56076
+ function normalize4(uri, options) {
55568
56077
  if (typeof uri === "string") {
55569
56078
  uri = /** @type {T} */
55570
56079
  normalizeString(uri, options);
@@ -55831,7 +56340,7 @@ var require_fast_uri = __commonJS({
55831
56340
  }
55832
56341
  var fastUri = {
55833
56342
  SCHEMES,
55834
- normalize: normalize3,
56343
+ normalize: normalize4,
55835
56344
  resolve: resolve18,
55836
56345
  resolveComponent,
55837
56346
  equal,
@@ -68384,6 +68893,8 @@ var init_ora = __esm({
68384
68893
  });
68385
68894
 
68386
68895
  // src/core/reporter.ts
68896
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "node:fs";
68897
+ import { join as join16 } from "node:path";
68387
68898
  function classifyFile(f) {
68388
68899
  if (f.path === "(project)") return "context";
68389
68900
  if (f.path === "(mcp)") return "mcp";
@@ -68405,10 +68916,32 @@ function classifyFile(f) {
68405
68916
  function isSyntheticBucket(p2) {
68406
68917
  return p2 === "(project)" || p2 === "(mcp)" || p2.includes(SESSION_AUDIT_PATH_MARKER) || p2.includes(SKILL_AUDIT_PATH_MARKER);
68407
68918
  }
68919
+ function detectVersionSkew(projectRoot, runningVersion) {
68920
+ try {
68921
+ const pkgPath = join16(projectRoot, "package.json");
68922
+ if (!existsSync8(pkgPath)) return null;
68923
+ const raw = readFileSync10(pkgPath, "utf8").replace(/^\uFEFF/, "");
68924
+ const pkg = JSON.parse(raw);
68925
+ const name = pkg.name ?? "";
68926
+ if (name !== "ctxlint" && name !== "@yawlabs/ctxlint") return null;
68927
+ if (!pkg.version || pkg.version === runningVersion) return null;
68928
+ return pkg.version;
68929
+ } catch {
68930
+ return null;
68931
+ }
68932
+ }
68408
68933
  function formatText(result, verbose = false) {
68409
68934
  const lines = [];
68410
68935
  lines.push("");
68411
68936
  lines.push(source_default.bold(`ctxlint v${result.version}`));
68937
+ const skew = detectVersionSkew(result.projectRoot, result.version);
68938
+ if (skew !== null) {
68939
+ lines.push(
68940
+ source_default.yellow(
68941
+ ` note: this checkout is v${skew} -- you are running v${result.version}. Rebuild or reinstall before trusting a result against it.`
68942
+ )
68943
+ );
68944
+ }
68412
68945
  lines.push("");
68413
68946
  lines.push(`Scanning ${result.projectRoot}...`);
68414
68947
  lines.push("");
@@ -68782,6 +69315,27 @@ function buildRuleDescriptors() {
68782
69315
  },
68783
69316
  helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
68784
69317
  },
69318
+ {
69319
+ id: "ctxlint/session-shared-temp-path",
69320
+ shortDescription: {
69321
+ text: "Fixed temp path written then read back -- clobberable by a concurrent session"
69322
+ },
69323
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69324
+ },
69325
+ {
69326
+ id: "ctxlint/session-unverified-gate-claimed-clean",
69327
+ shortDescription: {
69328
+ text: "A quality gate asserted as passing while its invocation failed or emitted nothing"
69329
+ },
69330
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69331
+ },
69332
+ {
69333
+ id: "ctxlint/session-default-branch-accumulation",
69334
+ shortDescription: {
69335
+ text: "Many files edited on the default branch with no intervening commit"
69336
+ },
69337
+ helpUri: "https://github.com/yawlabs/ctxlint#what-it-checks"
69338
+ },
68785
69339
  {
68786
69340
  id: "ctxlint/skill-frontmatter",
68787
69341
  shortDescription: { text: "Skill/agent definition missing required frontmatter" },
@@ -69178,8 +69732,8 @@ npx @yawlabs/ctxlint@${VERSION} --strict
69178
69732
  program2.parse();
69179
69733
  }
69180
69734
  async function promptYesNo(question) {
69181
- const { createInterface: createInterface2 } = await import("node:readline/promises");
69182
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
69735
+ const { createInterface: createInterface3 } = await import("node:readline/promises");
69736
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
69183
69737
  try {
69184
69738
  const answer = await rl.question(question);
69185
69739
  return /^\s*y(es)?\s*$/i.test(answer);
@@ -69310,4 +69864,3 @@ if (args.includes("--lsp")) {
69310
69864
  const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
69311
69865
  await runCli2();
69312
69866
  }
69313
- //# sourceMappingURL=index.js.map