basalt-ui 1.21.0 → 1.22.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.
@@ -44,6 +44,13 @@ beforeEach(() => {
44
44
  },
45
45
  }),
46
46
  )
47
+ // `shadow-basalt-export` and `ai-sdk-major` both scope through `isBasaltScopedFile`, so a fixture
48
+ // repo with NO manifest is out of scope for both and every other assertion here would pass
49
+ // vacuously. Written per-test by the `ai-sdk-major` block, which needs its own `ai` pin.
50
+ writeFileSync(
51
+ resolve(dir, 'package.json'),
52
+ JSON.stringify({ name: 'fixture', version: '0.0.0', dependencies: { 'basalt-ui': '^1.21.0' } }),
53
+ )
47
54
  })
48
55
 
49
56
  afterEach(() => {
@@ -918,6 +925,115 @@ describe('basalt/shadow-basalt-export', () => {
918
925
  expect(code).toBe(0)
919
926
  expect(rules).not.toContain('shadow-basalt-export')
920
927
  })
928
+
929
+ // The scoping fix, mirroring `ai-sdk-major`'s. obsidian's `packages/obsidian-vault-core` is
930
+ // React-free, carries no basalt-ui dependency, and sits outside the root's declared
931
+ // `basalt.roots` — so it CANNOT import the export it was told it had forked. The rule reported
932
+ // `SlugTracker` there anyway, because it was the one rule in this file that never asked.
933
+ it('does NOT flag a package that does not depend on basalt-ui at all', () => {
934
+ writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ name: 'standalone' }))
935
+ const { code, rules } = run(`export function EmptyState() { return null }\n`)
936
+ expect(code).toBe(0)
937
+ expect(rules).not.toContain('shadow-basalt-export')
938
+ })
939
+
940
+ it('does NOT flag a file outside a declared basalt.roots', () => {
941
+ writeFileSync(
942
+ resolve(dir, 'package.json'),
943
+ JSON.stringify({
944
+ basalt: { roots: ['packages/ui/src'] },
945
+ devDependencies: { 'basalt-ui': '^1.21.0' },
946
+ }),
947
+ )
948
+ expect(
949
+ run(`export function EmptyState() { return null }\n`, 'packages/core/src/a.tsx').rules,
950
+ ).not.toContain('shadow-basalt-export')
951
+ // …and the declared root is still policed, so the narrowing costs no real coverage.
952
+ expect(
953
+ run(`export function EmptyState() { return null }\n`, 'packages/ui/src/a.tsx').rules,
954
+ ).toContain('shadow-basalt-export')
955
+ })
956
+
957
+ // The second half: a PascalCase name is not a component. The nine-barrel widening pulled every
958
+ // PascalCase value export into range, and a bare-PascalCase test then matched a plain data class
959
+ // (obsidian's `SlugTracker` — the same NAME basalt's `./content` exports, and nothing else).
960
+ it('does NOT flag a plain class that merely shares a name', () => {
961
+ const { code, rules } = run(`export class SlugTracker {}\n`, 'slug.ts')
962
+ expect(code).toBe(0)
963
+ expect(rules).not.toContain('shadow-basalt-export')
964
+ })
965
+
966
+ it('DOES still flag a legacy class component (one that extends)', () => {
967
+ const { rules } = run(`export class EmptyState extends Component {}\n`)
968
+ expect(rules).toContain('shadow-basalt-export')
969
+ })
970
+
971
+ it('does NOT flag a non-function binding that shares a name', () => {
972
+ const { code, rules } = run(`export const EmptyState = { label: 'none' }\n`, 'consts.ts')
973
+ expect(code).toBe(0)
974
+ expect(rules).not.toContain('shadow-basalt-export')
975
+ })
976
+
977
+ it('DOES still flag a memo/forwardRef-wrapped component', () => {
978
+ expect(run(`export const EmptyState = memo(() => null)\n`).rules).toContain(
979
+ 'shadow-basalt-export',
980
+ )
981
+ expect(run(`export const EmptyState = React.forwardRef(() => null)\n`).rules).toContain(
982
+ 'shadow-basalt-export',
983
+ )
984
+ })
985
+ })
986
+
987
+ // ── theme-allow comment shapes (the plugin half of the annotation contract) ───
988
+
989
+ // The prefix rule has had three holes found in three rounds, so every comment shape a consumer
990
+ // actually writes is pinned here — and the guard's copy is pinned identically in
991
+ // `src/guard/check-source.test.ts`. The two must agree on what an annotation IS.
992
+ describe('theme-allow comment shapes', () => {
993
+ const flags = (source: string): boolean => run(source).rules.has('no-raw-font-size')
994
+
995
+ it.each([
996
+ [
997
+ 'own line //',
998
+ `// theme-allow no-raw-font-size — deliberate\nexport const C = <Text fz={10} />\n`,
999
+ ],
1000
+ [
1001
+ 'trailing //',
1002
+ `export const C = <Text fz={10} /> // theme-allow no-raw-font-size — deliberate\n`,
1003
+ ],
1004
+ [
1005
+ 'trailing block',
1006
+ `export const C = <Text fz={10} /* theme-allow no-raw-font-size — deliberate */ />\n`,
1007
+ ],
1008
+ [
1009
+ 'JSX expression comment',
1010
+ `export const C = () => (\n <div>\n {/* theme-allow no-raw-font-size — deliberate */}\n <Text fz={10} />\n </div>\n)\n`,
1011
+ ],
1012
+ [
1013
+ 'JSX expression comment, token on its own wrapped line',
1014
+ `export const C = () => (\n <div>\n {/*\n theme-allow no-raw-font-size — deliberate */}\n <Text fz={10} />\n </div>\n)\n`,
1015
+ ],
1016
+ [
1017
+ 'block gutter',
1018
+ `/**\n * theme-allow no-raw-font-size — deliberate\n */\nexport const C = <Text fz={10} />\n`,
1019
+ ],
1020
+ [
1021
+ 'docblock opener',
1022
+ `/** theme-allow no-raw-font-size — deliberate */\nexport const C = <Text fz={10} />\n`,
1023
+ ],
1024
+ ])('%s waives', (_name, source) => {
1025
+ expect(flags(source as string)).toBe(false)
1026
+ })
1027
+
1028
+ // The false NEGATIVE the prefix rule exists to close: prose that MENTIONS the token is not an
1029
+ // annotation. linewatch documented its own waivers in a docblock and disarmed the file.
1030
+ it('prose that merely mentions the token does not waive', () => {
1031
+ expect(
1032
+ flags(
1033
+ `// each chart here needs a theme-allow no-raw-font-size eventually\nexport const C = <Text fz={10} />\n`,
1034
+ ),
1035
+ ).toBe(true)
1036
+ })
921
1037
  })
922
1038
 
923
1039
  // ── hand-rolled-shell ────────────────────────────────────────────────────────
@@ -358,8 +358,14 @@ type SyncOptions = {
358
358
  * - `--check` makes NO writes; exits non-zero if any managed file is out-of-date or locally drifted
359
359
  * (a CI freshness gate), exit 0 when all current.
360
360
  * - `--force` overwrites locally-drifted files instead of skipping them.
361
+ *
362
+ * Runs against `resolveProjectDir(cwd)`, exactly as `check-theme` and `doctor` do — and then
363
+ * REFUSES rather than scaffolding when the resolved project has no manifest. See
364
+ * {@link findManifestAbove} for the failure that bought this: `sync` is the drift refresh and
365
+ * `init` is the scaffold, so placing twenty files on a cwd that was never a consumer is a decision
366
+ * `sync` does not get to make silently.
361
367
  */
362
- export declare function sync(opts?: SyncOptions, cwd?: string): number;
368
+ export declare function sync(opts?: SyncOptions, invocationCwd?: string): number;
363
369
  /**
364
370
  * Assert the 8 SURFACES invariants against the live SURFACES + GUARD_RULES + plugin.json.
365
371
  * Returns 0 when all pass; 1 when any fail (console.error each failure).
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import {
3
4
  existsSync,
@@ -14,9 +15,11 @@ import {
14
15
  checkSource,
15
16
  DEFAULT_GUARD_CONFIG,
16
17
  GENERATED_HEADER_LINE,
18
+ findAllowAnnotations,
17
19
  guardKindRemedy,
18
20
  GUARD_RULES,
19
21
  guardWaiverHint,
22
+ neutralizeAllowAnnotation,
20
23
  TOKENS_ONLY_DISABLED_KINDS,
21
24
  unmatchedExemptPatterns
22
25
  } from "../guard/index.js";
@@ -219,6 +222,13 @@ function appShellFiles(rootAbs) {
219
222
  out.push(...walkSourceFiles(resolve(appDir, "public")));
220
223
  return out;
221
224
  }
225
+ const PLAIN_JSON_HINT_PATH = "manifest.json";
226
+ function waiverHintFor(relPath, profile) {
227
+ if (profile !== "tokens-only" || !relPath.endsWith(".webmanifest")) {
228
+ return guardWaiverHint(relPath);
229
+ }
230
+ return `${guardWaiverHint(PLAIN_JSON_HINT_PATH)} (basaltAppPlugin would emit the file instead, but it needs basalt's Vite/React layer, which a tokens-only consumer has opted out of.)`;
231
+ }
222
232
  function checkTheme(invocationCwd = process.cwd(), flags = []) {
223
233
  if (conflictingProfileFlags(flags)) {
224
234
  console.error(
@@ -329,7 +339,9 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
329
339
  `);
330
340
  }
331
341
  const presentKinds = [...new Set(findings.map((f) => f.kind))].toSorted();
332
- const waiverHints = [...new Set(findings.map((f) => guardWaiverHint(f.relPath)))].toSorted();
342
+ const waiverHints = [
343
+ ...new Set(findings.map((f) => waiverHintFor(f.relPath, profile)))
344
+ ].toSorted();
333
345
  console.error(["Fix:", ...presentKinds.map(guardKindRemedy), ...waiverHints].join(" "));
334
346
  if (findings.some((f) => SERIES_MODULE_HINT_RE.test(f.relPath))) {
335
347
  console.error(
@@ -338,20 +350,89 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
338
350
  }
339
351
  return errors.length > 0 ? 1 : 0;
340
352
  }
341
- const AUDIT_NEUTRALIZED_TOKEN = "basalt-audit-neutralized";
342
353
  function findingKey(f) {
343
354
  return `${f.relPath}:${f.line}:${f.kind}`;
344
355
  }
345
- const AUDIT_ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
346
- function annotationRuleIds(rest) {
347
- const head = (rest.replace(/^-file\b/, "").split(/—|–|:|\s-\s/)[0] ?? "").trim();
348
- return head.split(",").map((word) => word.trim().replace(/^basalt\//, "")).filter((word) => /^[a-z][a-z0-9-]*$/.test(word));
349
- }
350
- function isAllowAnnotationLine(line, token) {
351
- const at = line.indexOf(token);
352
- if (at === -1) return false;
353
- if (line.includes(`"basalt:${token}`)) return true;
354
- return AUDIT_ANNOTATION_PREFIX.test(line.slice(0, at));
356
+ function resolveToolBin(dir, name) {
357
+ let current = dir;
358
+ for (; ; ) {
359
+ const candidate = resolve(current, "node_modules/.bin", name);
360
+ if (existsSync(candidate)) return candidate;
361
+ const parent = resolve(current, "..");
362
+ if (parent === current) break;
363
+ current = parent;
364
+ }
365
+ const probe = spawnSync(name, ["--version"], { stdio: "ignore", timeout: 1e4 });
366
+ return probe.error === void 0 && probe.status === 0 ? name : null;
367
+ }
368
+ function diagnosticCounts(diagnostics, filename) {
369
+ const counts = /* @__PURE__ */ new Map();
370
+ for (const d of diagnostics) {
371
+ if (d.filename !== filename) continue;
372
+ const code = typeof d.code === "string" ? d.code : "";
373
+ const rule = /^basalt\((.+)\)$/.exec(code)?.[1];
374
+ if (rule === void 0) continue;
375
+ const line = d.labels?.[0]?.span?.line;
376
+ const key = `${typeof line === "number" ? line : 0}:${rule}`;
377
+ counts.set(key, (counts.get(key) ?? 0) + 1);
378
+ }
379
+ return counts;
380
+ }
381
+ function probePluginRules(cwd, guardCfg, probes, sources) {
382
+ if (probes.length === 0) return /* @__PURE__ */ new Map();
383
+ const bin = resolveToolBin(cwd, "oxlint");
384
+ if (bin === null) return null;
385
+ const written = [];
386
+ const probePaths = /* @__PURE__ */ new Map();
387
+ try {
388
+ for (const probe of probes) {
389
+ const probed = neutralizeAllowAnnotation(sources.get(probe.rel) ?? "", probe.line, guardCfg);
390
+ const dot = probe.rel.lastIndexOf(".");
391
+ const rel = dot === -1 ? `${probe.rel}.basalt-audit-${probe.slot}` : `${probe.rel.slice(0, dot)}.basalt-audit-${probe.slot}${probe.rel.slice(dot)}`;
392
+ const abs = resolve(cwd, rel);
393
+ if (existsSync(abs)) continue;
394
+ writeFileSync(abs, probed, "utf8");
395
+ written.push(abs);
396
+ probePaths.set(probe.slot, rel);
397
+ }
398
+ if (probePaths.size === 0) return null;
399
+ const targets = [...new Set(probes.map((p) => p.rel)), ...probePaths.values()];
400
+ const run2 = spawnSync(bin, ["--format=json", "--no-error-on-unmatched-pattern", ...targets], {
401
+ cwd,
402
+ encoding: "utf8",
403
+ timeout: 12e4,
404
+ stdio: ["ignore", "pipe", "ignore"]
405
+ });
406
+ if (run2.error !== void 0 || typeof run2.stdout !== "string") return null;
407
+ let diagnostics;
408
+ try {
409
+ diagnostics = JSON.parse(run2.stdout).diagnostics ?? [];
410
+ } catch {
411
+ return null;
412
+ }
413
+ const verdicts = /* @__PURE__ */ new Map();
414
+ for (const probe of probes) {
415
+ const probeRel = probePaths.get(probe.slot);
416
+ if (probeRel === void 0) continue;
417
+ const before = diagnosticCounts(diagnostics, probe.rel);
418
+ const after = diagnosticCounts(diagnostics, probeRel);
419
+ const revealed = [];
420
+ for (const [key, count] of after) {
421
+ if (count <= (before.get(key) ?? 0)) continue;
422
+ const [line = "", rule = ""] = key.split(":");
423
+ revealed.push(`${rule}@${line}`);
424
+ }
425
+ verdicts.set(probe.slot, revealed.toSorted());
426
+ }
427
+ return verdicts;
428
+ } finally {
429
+ for (const abs of written) {
430
+ try {
431
+ unlinkSync(abs);
432
+ } catch {
433
+ }
434
+ }
435
+ }
355
436
  }
356
437
  function auditAllows(cwd, cfg, guardCfg, scanned) {
357
438
  const token = guardCfg.allowComment;
@@ -363,6 +444,7 @@ basalt-ui check-theme --audit-allows \u2014 ${cwd}
363
444
  let outOfReach = 0;
364
445
  let unaccountable = 0;
365
446
  const waiverLines = [];
447
+ const pluginProbes = [];
366
448
  const sources = /* @__PURE__ */ new Map();
367
449
  for (const rel of scanned) sources.set(rel, readFileSync(resolve(cwd, rel), "utf8"));
368
450
  for (const rel of scanned) {
@@ -375,30 +457,59 @@ basalt-ui check-theme --audit-allows \u2014 ${cwd}
375
457
  const unscopedAt = new Map(
376
458
  baseline.filter((f) => f.kind === "theme-allow-unscoped").map((f) => [f.line, f.token])
377
459
  );
378
- const fileLines = text.split("\n");
379
- for (const [index, line] of fileLines.entries()) {
380
- if (!isAllowAnnotationLine(line ?? "", token)) continue;
381
- const probe = [...fileLines];
382
- probe[index] = (line ?? "").replaceAll(token, AUDIT_NEUTRALIZED_TOKEN);
383
- const revealed = checkSource(probe.join("\n"), rel, guardCfg).filter((f) => f.kind !== "theme-allow-unscoped" && !baselineKeys.has(findingKey(f))).map((f) => `${f.kind}@${f.line}`);
384
- const note = unscopedAt.get(index + 1);
460
+ for (const site of findAllowAnnotations(text, rel, guardCfg)) {
461
+ const revealed = checkSource(
462
+ neutralizeAllowAnnotation(text, site.line, guardCfg),
463
+ rel,
464
+ guardCfg
465
+ ).filter((f) => f.kind !== "theme-allow-unscoped" && !baselineKeys.has(findingKey(f))).map((f) => `${f.kind}@${f.line}`);
466
+ const note = unscopedAt.get(site.line);
385
467
  if (note !== void 0) unaccountable++;
386
- const site = ` ${rel}:${index + 1}`.padEnd(52);
468
+ const label = ` ${rel}:${site.line}`.padEnd(52);
387
469
  const suffix = note === void 0 ? "" : ` [${note}]`;
388
- const ids = annotationRuleIds((line ?? "").slice((line ?? "").indexOf(token) + token.length));
389
470
  if (revealed.length > 0) {
390
471
  live++;
391
- waiverLines.push(`${site} suppresses ${[...new Set(revealed)].join(", ")}${suffix}`);
392
- } else if (ids.length > 0 && !ids.some((id) => Object.hasOwn(GUARD_RULES, id))) {
472
+ waiverLines.push(`${label} suppresses ${[...new Set(revealed)].join(", ")}${suffix}`);
473
+ continue;
474
+ }
475
+ if (site.pluginRules.length > 0 && site.guardKinds.length === 0) {
476
+ pluginProbes.push({
477
+ slot: waiverLines.length,
478
+ rel,
479
+ line: site.line,
480
+ ids: site.pluginRules,
481
+ site: label,
482
+ suffix
483
+ });
484
+ waiverLines.push("");
485
+ continue;
486
+ }
487
+ if (site.guardKinds.length === 0 && site.unknownRules.length > 0) {
393
488
  outOfReach++;
394
489
  waiverLines.push(
395
- `${site} scoped to ${ids.join(", ")} \u2014 not a check-theme kind, so this audit cannot judge it (an oxlint plugin rule, or a typo theme-allow-unscoped would report)${suffix}`
490
+ `${label} scoped to ${site.unknownRules.join(", ")} \u2014 names no guard kind and no plugin rule, so this audit cannot judge it (most likely a typo, which theme-allow-unscoped reports)${suffix}`
396
491
  );
397
- } else {
398
- dead++;
399
- waiverLines.push(`${site} SUPPRESSES NOTHING \u2014 dead, delete it${suffix}`);
492
+ continue;
400
493
  }
494
+ dead++;
495
+ waiverLines.push(`${label} SUPPRESSES NOTHING \u2014 dead, delete it${suffix}`);
496
+ }
497
+ }
498
+ const pluginVerdicts = probePluginRules(cwd, guardCfg, pluginProbes, sources);
499
+ for (const probe of pluginProbes) {
500
+ const revealed = pluginVerdicts?.get(probe.slot);
501
+ if (revealed === void 0) {
502
+ outOfReach++;
503
+ waiverLines[probe.slot] = `${probe.site} scoped to ${probe.ids.join(", ")} \u2014 an oxlint plugin rule, and oxlint could not be run here, so this audit cannot judge it${probe.suffix}`;
504
+ continue;
505
+ }
506
+ if (revealed.length > 0) {
507
+ live++;
508
+ waiverLines[probe.slot] = `${probe.site} suppresses ${revealed.join(", ")} (oxlint)${probe.suffix}`;
509
+ continue;
401
510
  }
511
+ dead++;
512
+ waiverLines[probe.slot] = `${probe.site} SUPPRESSES NOTHING \u2014 dead, delete it (proved by re-running oxlint over the file with this annotation neutralized)${probe.suffix}`;
402
513
  }
403
514
  lines.push(`${token} annotations (${waiverLines.length}):`);
404
515
  lines.push(...waiverLines.length === 0 ? [" (none)"] : waiverLines);
@@ -452,7 +563,10 @@ basalt.exemptRules entries (${exemptCount}):`);
452
563
  lines.push(...exemptLines.length === 0 ? [" (none)"] : exemptLines);
453
564
  lines.push(
454
565
  `
455
- ${live} live, ${dead} dead, ${outOfReach} outside check-theme's reach, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
566
+ ${live} live, ${dead} dead, ${outOfReach} unjudgeable, ${unaccountable} unaccountable (reported as theme-allow-unscoped by a normal run).`
567
+ );
568
+ lines.push(
569
+ `Scope: the ${scanned.length} file(s) check-theme scans under basalt.roots (${resolveRoots(cfg).join(", ")}). A waiver outside them is not audited \u2014 widen roots to cover it, or accept that nothing polices that file.`
456
570
  );
457
571
  if (dead > 0) {
458
572
  lines.push(
@@ -969,6 +1083,38 @@ function inspectLefthookSeam(repoRoot) {
969
1083
  if (entry === void 0) return { kind: "unwired", file };
970
1084
  return existsSync(resolve(repoRoot, entry)) ? { kind: "ok", file, entry } : { kind: "broken", file, entry };
971
1085
  }
1086
+ const LEFTHOOK_GUARD_RUN = /^\s*run:\s*.*\bcheck-theme\b/m;
1087
+ function lefthookDump(repoRoot) {
1088
+ const localBin = resolve(repoRoot, "node_modules/.bin/lefthook");
1089
+ const bin = existsSync(localBin) ? localBin : "lefthook";
1090
+ try {
1091
+ const result = spawnSync(bin, ["dump"], {
1092
+ cwd: repoRoot,
1093
+ encoding: "utf8",
1094
+ timeout: 1e4,
1095
+ stdio: ["ignore", "pipe", "ignore"]
1096
+ });
1097
+ if (result.error !== void 0 || result.status !== 0) return null;
1098
+ return result.stdout;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ function inspectLefthookGate(repoRoot) {
1104
+ const seam = inspectLefthookSeam(repoRoot);
1105
+ if (seam.kind === "no-file") return { kind: "no-file" };
1106
+ if (seam.kind === "ok") return { kind: "extends-ok", file: seam.file, entry: seam.entry };
1107
+ if (seam.kind === "broken") {
1108
+ return { kind: "extends-broken", file: seam.file, entry: seam.entry };
1109
+ }
1110
+ const dump = lefthookDump(repoRoot);
1111
+ if (dump !== null) {
1112
+ return LEFTHOOK_GUARD_RUN.test(dump) ? { kind: "wired", file: seam.file, proof: "dump" } : { kind: "absent", file: seam.file };
1113
+ }
1114
+ const raw = readIfExists(resolve(repoRoot, seam.file)) ?? "";
1115
+ if (LEFTHOOK_GUARD_RUN.test(raw)) return { kind: "wired", file: seam.file, proof: "config" };
1116
+ return { kind: "advisory", file: seam.file };
1117
+ }
972
1118
  function mergeOxlintExtends(cwd, presetPath) {
973
1119
  const abs = resolve(cwd, ".oxlintrc.json");
974
1120
  const raw = readIfExists(abs);
@@ -1131,7 +1277,37 @@ function readOxlintPresetPlugins(pkgRoot) {
1131
1277
  return [];
1132
1278
  }
1133
1279
  }
1134
- function sync(opts = {}, cwd = process.cwd()) {
1280
+ function findManifestAbove(dir) {
1281
+ let current = resolve(dir, "..");
1282
+ for (; ; ) {
1283
+ if (existsSync(resolve(current, MANIFEST_PATH))) return current;
1284
+ const parent = resolve(current, "..");
1285
+ if (parent === current) return null;
1286
+ current = parent;
1287
+ }
1288
+ }
1289
+ function sync(opts = {}, invocationCwd = process.cwd()) {
1290
+ const project = resolveProjectDir(invocationCwd);
1291
+ if (project.ambiguous !== null) {
1292
+ console.error(
1293
+ `\u2716 basalt-ui sync: no basalt config at ${invocationCwd}, and ${project.ambiguous.length} workspace packages carry one (${project.ambiguous.map((d) => relativePosix(invocationCwd, d)).join(", ")}) \u2014 run it from one of them, or set BASALT_CWD to pick.`
1294
+ );
1295
+ return 1;
1296
+ }
1297
+ const cwd = project.dir;
1298
+ if (project.relocatedFrom !== null) {
1299
+ console.log(
1300
+ `basalt-ui sync: no basalt config at ${project.relocatedFrom} \u2014 running in ${relativePosix(project.relocatedFrom, cwd)}, where it lives.`
1301
+ );
1302
+ }
1303
+ if (!existsSync(resolve(cwd, MANIFEST_PATH))) {
1304
+ const elsewhere = findManifestAbove(cwd);
1305
+ const wouldWrite = managedFiles(resolvePeerFlags(cwd, {}), resolvePlacement(cwd)).length;
1306
+ console.error(
1307
+ `\u2716 basalt-ui sync: no ${MANIFEST_PATH} at ${cwd} \u2014 refusing to scaffold. \`sync\` refreshes an EXISTING basalt install; creating one is \`basalt-ui init\`'s decision, and continuing here would write ${wouldWrite} managed file(s) into this directory.` + (elsewhere === null ? " No manifest exists at any ancestor either \u2014 run `basalt-ui init` here if this package really is meant to be a consumer." : ` This repo's install is at ${relativePosix(cwd, elsewhere)} (${elsewhere}) \u2014 run sync there, or set BASALT_CWD to it.`)
1308
+ );
1309
+ return 1;
1310
+ }
1135
1311
  const rootsState = reconcileRoots(cwd, { write: opts.check !== true });
1136
1312
  const ctx = renderContext(cwd);
1137
1313
  const manifest = readManifest(cwd);
@@ -1140,6 +1316,7 @@ function sync(opts = {}, cwd = process.cwd()) {
1140
1316
  const placement = resolvePlacement(cwd);
1141
1317
  const files = managedFiles(peers, placement);
1142
1318
  let updated = 0;
1319
+ let created = 0;
1143
1320
  let recreated = 0;
1144
1321
  let skippedDrift = 0;
1145
1322
  let staleForCheck = 0;
@@ -1151,10 +1328,12 @@ function sync(opts = {}, cwd = process.cwd()) {
1151
1328
  missingSources.push(file.source);
1152
1329
  continue;
1153
1330
  }
1331
+ const tracked = manifest.files[file.dest] !== void 0;
1154
1332
  if (file.mode === "seed") {
1155
1333
  if (state.current === null && !opts.check) {
1156
1334
  manifest.files[file.dest] = writeUnit(file, cwd, state.desired);
1157
- recreated++;
1335
+ if (tracked) recreated++;
1336
+ else created++;
1158
1337
  }
1159
1338
  continue;
1160
1339
  }
@@ -1172,8 +1351,9 @@ function sync(opts = {}, cwd = process.cwd()) {
1172
1351
  staleForCheck++;
1173
1352
  if (opts.check) continue;
1174
1353
  manifest.files[file.dest] = writeUnit(file, cwd, state.desired);
1175
- if (kind === "missing") recreated++;
1176
- else updated++;
1354
+ if (kind !== "missing") updated++;
1355
+ else if (tracked) recreated++;
1356
+ else created++;
1177
1357
  }
1178
1358
  if (!placement.isPackageRepoRoot) {
1179
1359
  console.log(
@@ -1222,7 +1402,7 @@ function sync(opts = {}, cwd = process.cwd()) {
1222
1402
  writeFileEnsuringDir(resolve(cwd, MANIFEST_PATH), `${JSON.stringify(manifest, null, 2)}
1223
1403
  `);
1224
1404
  console.log(
1225
- `basalt-ui sync: ${updated} updated, ${recreated} recreated, ${skippedDrift} skipped (drift).`
1405
+ `basalt-ui sync: ${updated} updated, ${created} created, ${recreated} recreated, ${skippedDrift} skipped (drift).`
1226
1406
  );
1227
1407
  if (driftLines.length > 0) {
1228
1408
  console.log("Locally-edited files were skipped (run with --force to overwrite):");
@@ -1630,22 +1810,30 @@ basalt-ui doctor \u2014 ${cwd}
1630
1810
  }
1631
1811
  }
1632
1812
  const repoRoot = findRepoRoot(cwd);
1633
- const lefthookSeam = profile === "tokens-only" ? { kind: "n/a" } : inspectLefthookSeam(repoRoot);
1813
+ const lefthookGate = profile === "tokens-only" ? { kind: "n/a" } : inspectLefthookGate(repoRoot);
1634
1814
  const lefthookCorrect = shippedAssetPath(install, repoRoot, "configs/lefthook.yml");
1635
- if (lefthookSeam.kind === "n/a") {
1815
+ if (lefthookGate.kind === "n/a") {
1636
1816
  pass("lefthook-preset: n/a \u2014 a tokens-only consumer wires its own hooks");
1637
- } else if (lefthookSeam.kind === "no-file") {
1817
+ } else if (lefthookGate.kind === "no-file") {
1638
1818
  pass(`lefthook-preset: n/a \u2014 no lefthook config at ${repoRoot}`);
1639
- } else if (lefthookSeam.kind === "unwired") {
1640
- warn(
1641
- `${lefthookSeam.file} does not extend the shipped lefthook preset \u2014 the oxlint / oxfmt / check-theme pre-commit jobs are not wired. Add "extends: [${lefthookCorrect}]" to it.`
1819
+ } else if (lefthookGate.kind === "extends-ok") {
1820
+ pass(`${lefthookGate.file} extends the shipped lefthook preset (${lefthookGate.entry})`);
1821
+ } else if (lefthookGate.kind === "wired") {
1822
+ pass(
1823
+ `lefthook-preset: ${lefthookGate.file} does not extend the shipped preset, but a pre-commit command runs check-theme directly \u2014 the gate exists (read from ` + (lefthookGate.proof === "dump" ? "`lefthook dump`, so extends/include/root: are all resolved)." : "the config text; `lefthook dump` was not runnable here, so an `include:` or a remote config was not consulted).")
1642
1824
  );
1643
- } else if (lefthookSeam.kind === "broken") {
1825
+ } else if (lefthookGate.kind === "extends-broken") {
1644
1826
  fail(
1645
- `${lefthookSeam.file} extends "${lefthookSeam.entry}", which does not exist (${resolve(repoRoot, lefthookSeam.entry)}) \u2014 lefthook merges a missing extends target into ZERO commands and still exits 0, so this repo has no pre-commit gate and \`lefthook dump\` looks clean. Repoint it at "${lefthookCorrect}".`
1827
+ `${lefthookGate.file} extends "${lefthookGate.entry}", which does not exist (${resolve(repoRoot, lefthookGate.entry)}) \u2014 lefthook merges a missing extends target into ZERO commands and still exits 0, so this repo has no pre-commit gate and \`lefthook dump\` looks clean. Repoint it at "${lefthookCorrect}".`
1828
+ );
1829
+ } else if (lefthookGate.kind === "absent") {
1830
+ warn(
1831
+ `${lefthookGate.file}: \`lefthook dump\` resolves the merged config and NO pre-commit command runs check-theme \u2014 the theme guard is not gating commits. Either add "extends: [${lefthookCorrect}]", or add your own command running \`bunx basalt-ui check-theme\` (with \`root:\` if it must run in a subdirectory).`
1646
1832
  );
1647
1833
  } else {
1648
- pass(`${lefthookSeam.file} extends the shipped lefthook preset (${lefthookSeam.entry})`);
1834
+ warn(
1835
+ `${lefthookGate.file} names no basalt gate and \`lefthook dump\` could not be run here, so this check cannot tell a missing gate from one wired through an \`include:\` or a remote config it never read \u2014 treat it as advisory. If the guard really is unwired, add "extends: [${lefthookCorrect}]" or your own command running \`bunx basalt-ui check-theme\`.`
1836
+ );
1649
1837
  }
1650
1838
  const publicDir = resolve(cwd, "public");
1651
1839
  if (profile === "framework" && existsSync(publicDir)) {
@@ -28,6 +28,17 @@ export type UnmatchedExemptPattern = {
28
28
  * typo'd kind name is an exemption for a rule that does not exist.
29
29
  */
30
30
  export declare function unmatchedExemptPatterns(cfg: GuardConfig, scannedRelPaths: readonly string[]): UnmatchedExemptPattern[];
31
+ /**
32
+ * The oxlint plugin's rule ids. Not guard kinds — they can never suppress a `checkSource` finding —
33
+ * but they ARE valid ids in a `theme-allow`, so an annotation scoped to one of them must parse as
34
+ * accountable rather than be reported as unscoped. Kept as a literal list because the plugin is a
35
+ * standalone `.js` file this module must not import (see the plugin's own header).
36
+ *
37
+ * Exported for `check-theme --audit-allows`: an annotation naming one of these is outside the
38
+ * guard's reach, and judging it needs a `oxlint` run rather than a `checkSource` one. Keep in step
39
+ * with the plugin's own `KNOWN_RULE_IDS`; `configs/oxlint-plugin.test.ts` asserts the two agree.
40
+ */
41
+ export declare const PLUGIN_RULE_IDS: ReadonlySet<string>;
31
42
  /**
32
43
  * The exact first line of every stylesheet the CLI emits — the contract between `basalt-ui
33
44
  * tokens:css` / `fonts:css` and this scanner, and the single source of truth for it (`src/cli`
@@ -262,4 +273,60 @@ export declare function guardWaiverHint(relPath: string): string;
262
273
  * if (findings.some((f) => f.kind === 'raw-hex')) { ... }
263
274
  */
264
275
  export declare function checkSource(text: string, relPath: string, cfg: GuardConfig): Finding[];
276
+ /**
277
+ * One `theme-allow` annotation as WRITTEN, classified by whose reach its ids fall in.
278
+ *
279
+ * `guardKinds` is what `checkSource` can judge by re-running itself with the annotation
280
+ * neutralized. `pluginRules` is what only an `oxlint` run can judge. `unknownRules` names no rule
281
+ * at all. All three empty with `bare: true` is the legacy blanket form.
282
+ */
283
+ export type AllowAnnotationSite = {
284
+ /** 1-based line the annotation is written on. */
285
+ readonly line: number;
286
+ /** `'file'` for the `theme-allow-file` declaration form, `'line'` otherwise. */
287
+ readonly scope: 'line' | 'file';
288
+ /** Every id the annotation names that resolves to a real rule, `basalt/` prefix stripped. */
289
+ readonly rules: readonly string[];
290
+ /** The subset of `rules` `checkSource` owns. */
291
+ readonly guardKinds: readonly GuardKind[];
292
+ /** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
293
+ readonly pluginRules: readonly string[];
294
+ /** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
295
+ readonly unknownRules: readonly string[];
296
+ /** A written reason follows the ids. */
297
+ readonly hasReason: boolean;
298
+ /** The legacy blanket form — no ids at all, known or unknown. Only covers anything at line scope. */
299
+ readonly bare: boolean;
300
+ /** The source line, verbatim. */
301
+ readonly text: string;
302
+ };
303
+ /**
304
+ * Every allow annotation in one file, with the ids it names classified by reach.
305
+ *
306
+ * Pure, and it shares `collectAllowAnnotations` with {@link checkSource} — so a caller enumerating
307
+ * annotations sees exactly the set the scan honours, rather than a second regex that can drift from
308
+ * it. `--audit-allows` carried such a mirror and it was already one alternation behind.
309
+ *
310
+ * A file-scoped declaration appears once, at the line it is written on.
311
+ *
312
+ * @example
313
+ * for (const site of findAllowAnnotations(text, rel, cfg)) {
314
+ * if (site.pluginRules.length > 0) probeWithOxlint(neutralizeAllowAnnotation(text, site.line, cfg))
315
+ * }
316
+ */
317
+ export declare function findAllowAnnotations(text: string, relPath: string, cfg: GuardConfig): AllowAnnotationSite[];
318
+ /** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
319
+ export declare const NEUTRALIZED_ALLOW_TOKEN = "basalt-audit-neutralized";
320
+ /**
321
+ * `text` with the allow token on ONE (1-based) line rewritten to {@link NEUTRALIZED_ALLOW_TOKEN},
322
+ * leaving every other annotation intact.
323
+ *
324
+ * The probe half of an audit: re-scan the result and the findings that appear are exactly what that
325
+ * one annotation suppresses. Shipped rather than left to each caller so the guard probe and the
326
+ * oxlint probe neutralize identically — a `--audit-allows` verdict that differs between the two
327
+ * halves by a substitution detail is worse than no verdict.
328
+ *
329
+ * A line with no token comes back unchanged.
330
+ */
331
+ export declare function neutralizeAllowAnnotation(text: string, line: number, cfg: GuardConfig): string;
265
332
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;AAyb9D,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,WAoBlC,CAAA;AAmJD,sGAAsG;AACtG,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,UAAU,CAAA;CAC7C,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,WAAW,EAChB,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,sBAAsB,EAAE,CAc1B;AA8WD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,gGACwD,CAAA;AA8F1F;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,SAAS,CAqB5D,CAAA;AA2NF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;gCA0BL,WAAW;;;;;;;gCAcX,WAAW;;;;;gCAMX,WAAW;;;;;;;gCAaX,WAAW;;;;;;;gCAQX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCAaX,WAAW;;;;;;gCAMX,WAAW;;;;;;;gCAaX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;;;;;;gCAgBX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;;;;CAWmB,CAAA;AAEjD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEvD;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgBvD;AAUD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,EAAE,CA0XtF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}