@hublo/sentinel 0.1.0-alpha.8 → 1.0.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.
@@ -12,6 +12,9 @@ function setDefaultRunner(target, runner) {
12
12
  function all() {
13
13
  return adapters;
14
14
  }
15
+ function availableTargets() {
16
+ return [...new Set(adapters.map((a) => a.target))];
17
+ }
15
18
  function resolve(target, flavour, runner) {
16
19
  const forTarget = adapters.filter((a) => a.target === target);
17
20
  if (forTarget.length === 0) {
@@ -77,22 +80,27 @@ function palette(stream) {
77
80
  import { existsSync, readFileSync } from "fs";
78
81
  import { dirname, join } from "path";
79
82
  import { fileURLToPath } from "url";
80
- function readOwnVersion() {
83
+ function readOwnPackage() {
81
84
  let dir = dirname(fileURLToPath(import.meta.url));
82
85
  for (; ; ) {
83
86
  const pkgPath = join(dir, "package.json");
84
87
  if (existsSync(pkgPath)) {
85
88
  try {
86
89
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
87
- if (typeof pkg.version === "string") return pkg.version;
90
+ if (typeof pkg.version === "string") {
91
+ return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version };
92
+ }
88
93
  } catch {
89
94
  }
90
95
  }
91
96
  const parent = dirname(dir);
92
- if (parent === dir) return "0.0.0";
97
+ if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
93
98
  dir = parent;
94
99
  }
95
100
  }
101
+ function readOwnVersion() {
102
+ return readOwnPackage().version;
103
+ }
96
104
  function readProjectPackageJson(dir) {
97
105
  const path = join(dir, "package.json");
98
106
  if (!existsSync(path)) return {};
@@ -249,6 +257,25 @@ function readTsconfigAdoption(cwd) {
249
257
 
250
258
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
251
259
  var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
260
+ var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
261
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
262
+ var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
263
+ function parseDiagnostics(output) {
264
+ const diagnostics = [];
265
+ for (const raw of output.split("\n")) {
266
+ const m = DIAGNOSTIC_RE.exec(raw.trim());
267
+ if (m) {
268
+ diagnostics.push({
269
+ file: m[1],
270
+ line: Number(m[2]),
271
+ col: Number(m[3]),
272
+ code: m[4],
273
+ message: m[5]
274
+ });
275
+ }
276
+ }
277
+ return diagnostics;
278
+ }
252
279
  var PHASED_STRICTNESS_WARNING = `sentinel typescript: phase 1 (non-breaking) \u2014 deferred: ${deferredRuleNames().join(", ")}. Enabled centrally in a later wave; run \`sentinel --inspect --typescript\` for the list.`;
253
280
  function composeExtends(current, preset) {
254
281
  const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
@@ -260,7 +287,7 @@ var TscAdapter = class extends BaseAdapter {
260
287
  /**
261
288
  * The tsc adapter drives type-checking for any flavour: `--run`/`--report`/
262
289
  * `--inspect` just execute tsc against the module's existing config, which is
263
- * meaningful regardless of flavour. `--update` is the exception, it only WRITES a
290
+ * meaningful regardless of flavour. `--init` is the exception, it only WRITES a
264
291
  * preset for flavours that ship one (gated inside `plan`), so svelte is not
265
292
  * clobbered with a non-existent preset.
266
293
  */
@@ -268,10 +295,10 @@ var TscAdapter = class extends BaseAdapter {
268
295
  return true;
269
296
  }
270
297
  /**
271
- * Plan `--update`: make the module extend the sentinel preset with a THIN,
272
- * conformant stub, and route type-checking through the CLI. The engine applies
273
- * the ops; ensuring the `@hublo/sentinel` dependency is an adoption step
274
- * (`pnpm add`), not a file write.
298
+ * Plan `--init`: make the module extend the sentinel preset with a THIN,
299
+ * conformant stub, route type-checking through the CLI, and pin the
300
+ * `@hublo/sentinel` devDependency into the module. The engine applies the ops, so
301
+ * adoption is `--init` then `pnpm install`, with nothing to add by hand.
275
302
  *
276
303
  * Per resolved case:
277
304
  * - extends-base: append the preset to the `extends` chain (keep the base for
@@ -284,23 +311,23 @@ var TscAdapter = class extends BaseAdapter {
284
311
  if (!hasShippedPreset(context.flavour)) {
285
312
  return {
286
313
  operations: [],
287
- notes: [`skipped: no TypeScript preset for flavour "${context.flavour}" yet`]
314
+ blocked: `no TypeScript preset for flavour "${context.flavour}" yet (shipped: ${SHIPPED_FLAVOURS.join(", ")}). Nothing was written; this module cannot adopt the TypeScript preset until that flavour ships.`
288
315
  };
289
316
  }
290
317
  const target = resolveTsconfigTarget(context.cwd);
291
318
  const preset = `@hublo/sentinel/tsconfig/${context.flavour}`;
292
- const addScript = this.typecheckScriptOperation(context.cwd);
319
+ const addScript = this.packageJsonOperation(context.cwd);
293
320
  if (target.reason === "other-chain") {
294
321
  return {
295
322
  operations: [],
296
- notes: [`skipped: ${target.path} extends a non-base config; handled separately`]
323
+ blocked: `${target.path} extends a config chain sentinel does not handle, so nothing was written. Point it at the workspace base (or a plain config) and re-run.`
297
324
  };
298
325
  }
299
326
  if (target.reason === "none") {
300
327
  const contents = JSON.stringify({ extends: preset, include: ["src"] }, null, 2) + "\n";
301
328
  return {
302
329
  operations: [{ kind: "write", path: target.path, contents }, addScript],
303
- notes: [`created ${target.path} (no tsconfig found)`]
330
+ notes: [`created ${target.path} (no tsconfig found)`, INSTALL_NOTE]
304
331
  };
305
332
  }
306
333
  const existing = parseJsonc(
@@ -314,14 +341,17 @@ var TscAdapter = class extends BaseAdapter {
314
341
  ];
315
342
  const notes = [];
316
343
  if (drift.length > 0) {
344
+ const localKeys = Object.keys(existing.compilerOptions ?? {});
345
+ const stripsAll = localKeys.length > 0 && localKeys.every((key) => drift.includes(key));
317
346
  operations.push({
318
347
  kind: "remove-json-keys",
319
348
  path: target.path,
320
- keys: drift.map((key) => ["compilerOptions", key])
349
+ keys: stripsAll ? [["compilerOptions"]] : drift.map((key) => ["compilerOptions", key])
321
350
  });
322
351
  notes.push(`stripped preset-owned compilerOptions: ${drift.join(", ")}`);
323
352
  }
324
353
  operations.push(addScript);
354
+ notes.push(INSTALL_NOTE);
325
355
  return { operations, notes };
326
356
  }
327
357
  /**
@@ -329,18 +359,25 @@ var TscAdapter = class extends BaseAdapter {
329
359
  * `package.json`, merge the script in and leave the rest untouched. If it does
330
360
  * NOT (common for nx apps/services that carry only a `project.json`), scaffold a
331
361
  * minimal, workspace-valid one, its nx name + `private: true`, so pnpm accepts it
332
- * and it can then receive the `@hublo/sentinel` devDep (added via `pnpm add` at
333
- * adoption, never written here, so the lockfile stays authoritative).
362
+ * and it can carry the pinned `@hublo/sentinel` devDep this op also writes. The
363
+ * exact version is written (never a range), so `pnpm install` resolves the same
364
+ * build the stub's `extends` points at.
334
365
  */
335
- typecheckScriptOperation(cwd) {
366
+ packageJsonOperation(cwd) {
367
+ const own = readOwnPackage();
368
+ const devDependencies = { [own.name]: own.version };
336
369
  if (existsSync5(join5(cwd, "package.json"))) {
337
- return { kind: "merge-json", path: "package.json", value: { scripts: TYPECHECK_SCRIPT } };
370
+ return {
371
+ kind: "merge-json",
372
+ path: "package.json",
373
+ value: { scripts: TYPECHECK_SCRIPT, devDependencies }
374
+ };
338
375
  }
339
376
  const name = readNxProjectName(cwd) ?? basename(cwd);
340
377
  return {
341
378
  kind: "merge-json",
342
379
  path: "package.json",
343
- value: { name, private: true, scripts: TYPECHECK_SCRIPT }
380
+ value: { name, private: true, scripts: TYPECHECK_SCRIPT, devDependencies }
344
381
  };
345
382
  }
346
383
  /**
@@ -419,7 +456,7 @@ var TscAdapter = class extends BaseAdapter {
419
456
  async report(ctx) {
420
457
  const config = this.typecheckTarget(ctx.cwd);
421
458
  if (!config) {
422
- return { ok: true, code: 0, metrics: { errors: 0, implicitAny: 0 } };
459
+ return { ok: true, code: 0, metrics: { errors: 0, implicitAny: "deferred", diagnostics: [] } };
423
460
  }
424
461
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
425
462
  const result = spawnSync(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
@@ -432,8 +469,50 @@ var TscAdapter = class extends BaseAdapter {
432
469
  }
433
470
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
434
471
  const errors = (output.match(/error TS\d+/g) ?? []).length;
435
- const implicitAny = (output.match(/error TS70\d\d/g) ?? []).length;
436
- return { ok: errors === 0, code: result.status ?? 0, metrics: { errors, implicitAny } };
472
+ const parsed = parseDiagnostics(output);
473
+ const cap = ctx.maxDiagnostics === 0 ? Infinity : ctx.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS;
474
+ const diagnostics = Number.isFinite(cap) ? parsed.slice(0, cap) : parsed;
475
+ return {
476
+ ok: errors === 0,
477
+ code: result.status ?? 0,
478
+ metrics: {
479
+ errors,
480
+ implicitAny: this.implicitAnyMetric(ctx.cwd, tsc, config, output),
481
+ diagnostics,
482
+ diagnosticsTruncated: diagnostics.length < parsed.length
483
+ }
484
+ };
485
+ }
486
+ /**
487
+ * The `implicitAny` report metric, honestly. Implicit-`any` violations (TS70xx) are
488
+ * only *visible* to tsc when `noImplicitAny` is ON. In phase 1 the rule is DEFERRED
489
+ * (off), so counting TS70xx from the committed-config run is structurally always 0 —
490
+ * a misleading "no implicit-any" when the rule simply was not applied. So: report the
491
+ * real count only when the rule is on, otherwise `'deferred'` (never a fake `0`). The
492
+ * remaining debt while deferred is a separate, opt-in probe (a later `--migration`).
493
+ */
494
+ implicitAnyMetric(cwd, tsc, config, mainOutput) {
495
+ return this.noImplicitAnyEnabled(cwd, tsc, config, mainOutput) ? (mainOutput.match(/error TS70\d\d/g) ?? []).length : "deferred";
496
+ }
497
+ /**
498
+ * Whether `noImplicitAny` is effectively ON in the module's resolved config. Read from
499
+ * `tsc --showConfig` (an explicit value wins; otherwise `strict` implies it). If
500
+ * `--showConfig` is unavailable, fall back to the run's own evidence: implicit-`any`
501
+ * errors in the output mean the rule must be on.
502
+ */
503
+ noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
504
+ const shown = spawnSync(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
505
+ if (shown.status === 0 && shown.stdout) {
506
+ try {
507
+ const co = parseJsonc(
508
+ shown.stdout,
509
+ "tsconfig(--showConfig)"
510
+ ).compilerOptions;
511
+ if (co) return co.noImplicitAny ?? co.strict === true;
512
+ } catch {
513
+ }
514
+ }
515
+ return /error TS70\d\d/.test(mainOutput);
437
516
  }
438
517
  };
439
518
 
@@ -451,15 +530,23 @@ function registerAdapters() {
451
530
  // src/core/detect-framework.ts
452
531
  var FRAMEWORK_SIGNALS = [
453
532
  { flavour: "nest", dependency: "@nestjs/core" },
454
- { flavour: "svelte", dependency: "svelte" },
455
- { flavour: "react", dependency: "react" }
533
+ { flavour: "react", dependency: "react" },
534
+ { flavour: "svelte", dependency: "svelte" }
456
535
  ];
457
- function detectFramework(packageJson) {
536
+ var BACKEND_HINTS = ["sails", "express", "koa", "fastify", "@hapi/hapi"];
537
+ function describeFramework(packageJson) {
458
538
  const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
459
- for (const { flavour, dependency } of FRAMEWORK_SIGNALS) {
460
- if (dependency in dependencies) return flavour;
461
- }
462
- return "node";
539
+ const matched = FRAMEWORK_SIGNALS.filter((s) => s.dependency in dependencies);
540
+ const chosen = matched[0];
541
+ const flavour = chosen?.flavour ?? "node";
542
+ const source = chosen?.dependency ?? "(no framework dependency)";
543
+ const otherFlavourSignals = matched.slice(1).map((s) => s.dependency);
544
+ const backendHints = flavour === "react" || flavour === "svelte" ? BACKEND_HINTS.filter((hint) => hint in dependencies) : [];
545
+ const conflicts = [...otherFlavourSignals, ...backendHints];
546
+ return { flavour, source, ambiguous: conflicts.length > 0, conflicts };
547
+ }
548
+ function detectFramework(packageJson) {
549
+ return describeFramework(packageJson).flavour;
463
550
  }
464
551
 
465
552
  // src/shared/text.ts
@@ -597,14 +684,24 @@ function writeFileAtomic(absolutePath, contents) {
597
684
  renameSync(tempPath, absolutePath);
598
685
  }
599
686
  function applyPlan(cwd, plan) {
600
- const prepared = preparePlan(cwd, plan);
601
- for (const file of prepared) writeFileAtomic(file.absolutePath, file.after);
602
- return prepared.map((file) => file.path);
687
+ const changed = preparePlan(cwd, plan).filter((file) => file.before !== file.after);
688
+ for (const file of changed) writeFileAtomic(file.absolutePath, file.after);
689
+ return changed.map((file) => file.path);
603
690
  }
604
691
 
605
692
  // src/core/dispatch.ts
606
693
  function resolveFlavour(opts) {
607
- return opts.flavour ?? detectFramework(readProjectPackageJson(opts.cwd));
694
+ if (opts.flavour) return opts.flavour;
695
+ const detection = describeFramework(readProjectPackageJson(opts.cwd));
696
+ if (detection.ambiguous) {
697
+ const warn = palette(process.stderr);
698
+ process.stderr.write(
699
+ warn.warn(
700
+ `sentinel: flavour is ambiguous, detected "${detection.flavour}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to write the intended preset.`
701
+ ) + "\n"
702
+ );
703
+ }
704
+ return detection.flavour;
608
705
  }
609
706
  function previewPlan(opts, plan) {
610
707
  const changed = preparePlan(opts.cwd, plan).filter((file) => file.before !== file.after);
@@ -645,70 +742,45 @@ function previewPlan(opts, plan) {
645
742
  return 0;
646
743
  }
647
744
  async function dispatch(opts) {
648
- const adapter = resolve(opts.target, opts.flavour, opts.runner);
745
+ if (opts.verb !== "init") {
746
+ throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
747
+ }
649
748
  const flavour = resolveFlavour(opts);
650
- const ctx = {
651
- module: opts.module,
652
- cwd: opts.cwd,
653
- flavour,
654
- ci: opts.ci,
655
- fix: opts.fix
656
- };
657
- switch (opts.verb) {
658
- case "run": {
659
- const res = await adapter.run(ctx);
660
- return res.code;
661
- }
662
- case "inspect": {
663
- const config = await adapter.inspect(ctx);
664
- process.stdout.write(JSON.stringify(config, null, 2) + "\n");
665
- return 0;
666
- }
667
- case "update": {
668
- const context = { cwd: opts.cwd, flavour };
669
- const plan = await adapter.plan(context);
670
- if (opts.dryRun) {
671
- return previewPlan(opts, plan);
672
- }
673
- const written = applyPlan(opts.cwd, plan);
674
- for (const path of written) process.stderr.write(` wrote ${path}
675
- `);
676
- for (const note of plan.notes ?? []) process.stderr.write(` ${note}
749
+ const adapter = resolve(opts.target, flavour, opts.runner);
750
+ const context = { cwd: opts.cwd, flavour };
751
+ const plan = await adapter.plan(context);
752
+ if (plan.blocked) {
753
+ process.stderr.write(`sentinel (${opts.target}): ${plan.blocked}
677
754
  `);
678
- return 0;
679
- }
680
- case "report": {
681
- const res = await adapter.report(ctx);
682
- if (res.metrics) {
683
- process.stdout.write(JSON.stringify(res.metrics, null, 2) + "\n");
684
- }
685
- return res.code;
686
- }
687
- case "status": {
688
- const report = await adapter.status(ctx);
689
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
690
- return report.adopted && !report.conformant ? 1 : 0;
691
- }
692
- default: {
693
- const unreachable = opts.verb;
694
- throw new Error(`Unknown verb: ${String(unreachable)}`);
695
- }
755
+ return 1;
696
756
  }
757
+ if (opts.dryRun) {
758
+ return previewPlan(opts, plan);
759
+ }
760
+ const written = applyPlan(opts.cwd, plan);
761
+ for (const path of written) process.stderr.write(` wrote ${path}
762
+ `);
763
+ for (const note of plan.notes ?? []) process.stderr.write(` ${note}
764
+ `);
765
+ return 0;
697
766
  }
698
767
 
699
768
  export {
700
769
  register,
701
770
  setDefaultRunner,
702
771
  all,
772
+ availableTargets,
703
773
  resolve,
704
774
  BaseAdapter,
705
775
  palette,
776
+ readOwnPackage,
706
777
  readOwnVersion,
707
778
  readProjectPackageJson,
708
779
  readNxProjectName,
709
780
  resolveBin,
710
781
  registerAdapters,
782
+ describeFramework,
711
783
  detectFramework,
712
784
  dispatch
713
785
  };
714
- //# sourceMappingURL=chunk-UG74KTIU.js.map
786
+ //# sourceMappingURL=chunk-HKGCWPRT.js.map
package/dist/index.d.ts CHANGED
@@ -10,8 +10,13 @@ declare function registerAdapters(): void;
10
10
  * Tunable behaviour (defaults, detection signals, marker filenames) lives in
11
11
  * `settings.ts`, not here.
12
12
  */
13
- /** Verbs: what to do. Each maps to an adapter method in dispatch. */
14
- declare const VERBS: readonly ["run", "inspect", "update", "report", "status"];
13
+ /**
14
+ * Verbs: what to do. `init` writes a module's config + the workspace prep it needs (the
15
+ * one-time setup); the read verbs (run/inspect/report/status) execute or introspect.
16
+ * `migrate` is reserved for changing an already-initialized setup and is not wired yet
17
+ * (the CLI lists it as planned and refuses it).
18
+ */
19
+ declare const VERBS: readonly ["run", "inspect", "init", "migrate", "report", "status"];
15
20
  type Verb = (typeof VERBS)[number];
16
21
  /** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */
17
22
  declare const TARGETS: readonly ["lint", "format", "typescript", "build", "test", "static-analysis", "runtime-analysis", "arch"];
@@ -31,7 +36,7 @@ type Flavour = (typeof FLAVOURS)[number];
31
36
  */
32
37
 
33
38
  /**
34
- * Normalized context for `--update`, built and guaranteed by the engine. The
39
+ * Normalized context for `--init`, built and guaranteed by the engine. The
35
40
  * adapter TRUSTS this shape (no re-validation): `cwd` is the absolute module root,
36
41
  * `flavour` is resolved (explicit `--flavour`, else detected). The adapter may READ
37
42
  * the module through bounded resolvers to plan, but never writes, the engine
@@ -55,9 +60,15 @@ interface RunContext {
55
60
  ci: boolean;
56
61
  /** Apply auto-fixes where the tool supports it. */
57
62
  fix: boolean;
63
+ /**
64
+ * Cap on the number of structured diagnostics a `--report` embeds per module (the
65
+ * full count still lives in `errors`, so nothing is hidden silently). `0` means no
66
+ * cap. Undefined lets the adapter use its default.
67
+ */
68
+ maxDiagnostics?: number;
58
69
  }
59
70
  /**
60
- * A single, DECLARATIVE file operation an `--update` wants to happen. Adapters
71
+ * A single, DECLARATIVE file operation an `--init` wants to happen. Adapters
61
72
  * describe intent in tool terms; they never touch the filesystem. The engine
62
73
  * (which owns IO and the repo layout) executes these against the project root.
63
74
  *
@@ -102,13 +113,21 @@ type FileOperation =
102
113
  keys: string[][];
103
114
  };
104
115
  /**
105
- * The result of planning an `--update`: an ordered list of declarative operations,
116
+ * The result of planning an `--init`: an ordered list of declarative operations,
106
117
  * plus optional human notes (what was created/stripped/skipped) surfaced by the
107
118
  * CLI. Multi-operation on purpose; the drift guard and golden tests diff the plan.
108
119
  */
109
120
  interface UpdatePlan {
110
121
  operations: FileOperation[];
111
122
  notes?: string[];
123
+ /**
124
+ * Set when the adapter CANNOT set this module up at all (no preset for the flavour,
125
+ * a config chain it does not handle, …). The engine reports it as a FAILURE with a
126
+ * non-zero exit, never as a silent success: `--init` that writes nothing must not
127
+ * look like adoption to a person reading a log or to a script reading `$?`. Carries
128
+ * the reason, phrased for the developer who has to act on it.
129
+ */
130
+ blocked?: string;
112
131
  }
113
132
  /** Outcome of `run` / `report`. */
114
133
  interface AdapterResult {
@@ -136,8 +155,8 @@ interface StatusReport {
136
155
  }
137
156
  /**
138
157
  * One tool implementation for one target (and, optionally, a subset of flavours,
139
- * see `appliesTo`). The 4 CLI verbs map onto these: `--run` -> run, `--inspect`
140
- * -> inspect, `--update` -> plan(+write), `--report` -> report.
158
+ * see `appliesTo`). The CLI verbs map onto these: `--run` -> run, `--inspect` ->
159
+ * inspect, `--init` -> plan(+write), `--report` -> report, `--status` -> status.
141
160
  */
142
161
  interface Adapter {
143
162
  /** The target this adapter serves (e.g. 'lint'). */
@@ -151,7 +170,7 @@ interface Adapter {
151
170
  */
152
171
  appliesTo(flavour: Flavour): boolean;
153
172
  /**
154
- * Plan what `--update` should write, as declarative operations (see
173
+ * Plan what `--init` should write, as declarative operations (see
155
174
  * `FileOperation`). Given a normalized `UpdateContext`, the adapter may READ the
156
175
  * module (through bounded resolvers) to decide the ops, but never writes, the
157
176
  * engine applies them and owns all IO. The drift guard diffs against this plan.
@@ -199,7 +218,7 @@ declare abstract class BaseAdapter implements Adapter {
199
218
  * dependency is a plain TypeScript library (`node`). This is not the old silent
200
219
  * guessing (there is no "assume react" fallback); `node` is a real preset.
201
220
  *
202
- * Reusable across tools: every tool's `--update` needs the module's flavour to
221
+ * Reusable across tools: every tool's `--init` needs the module's flavour to
203
222
  * pick its preset, so this lives in core, not in the TypeScript role.
204
223
  */
205
224
 
@@ -212,19 +231,14 @@ interface PackageDependencies {
212
231
  declare function detectFramework(packageJson: PackageDependencies): Flavour;
213
232
 
214
233
  interface DispatchOptions {
234
+ /** Always `init` (dispatch is the write path); other verbs route through `analyse`. */
215
235
  verb: Verb;
216
236
  target: Target;
217
237
  runner?: string;
218
- module: string;
219
238
  cwd: string;
220
- /**
221
- * Declared flavour, when known (`--flavour`). `--run`/`--report` don't need it;
222
- * `--update`/`--inspect` resolve it, falling back to `detectFramework`.
223
- */
239
+ /** Declared flavour (`--flavour`), else resolved by `detectFramework` from deps. */
224
240
  flavour?: Flavour;
225
- ci: boolean;
226
- fix: boolean;
227
- /** `--dry-run`: for `--update`, preview the plan instead of writing anything. */
241
+ /** `--dry-run`: preview the plan instead of writing anything. */
228
242
  dryRun?: boolean;
229
243
  /** `--json`: emit the dry-run preview as machine-readable JSON. */
230
244
  json?: boolean;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-UG74KTIU.js";
10
+ } from "./chunk-HKGCWPRT.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "0.1.0-alpha.8",
3
+ "version": "1.0.0",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/hublo/sentinel.git"
11
11
  },
12
12
  "engines": {
13
- "node": ">=20"
13
+ "node": ">=20.12.0"
14
14
  },
15
15
  "bin": {
16
16
  "sentinel": "./dist/bin/sentinel.js"
@@ -31,14 +31,14 @@
31
31
  }
32
32
  },
33
33
  "files": [
34
- "dist"
34
+ "dist",
35
+ "!dist/**/*.map"
35
36
  ],
36
37
  "dependencies": {
37
38
  "commander": "^13.0.0",
38
39
  "jsonc-parser": "^3.3.1"
39
40
  },
40
41
  "devDependencies": {
41
- "@changesets/cli": "^2.27.0",
42
42
  "@eslint/js": "^10.0.1",
43
43
  "@types/node": "^22.0.0",
44
44
  "eslint": "^10.8.0",
@@ -52,6 +52,7 @@
52
52
  "vitest": "^2.1.0"
53
53
  },
54
54
  "publishConfig": {
55
+ "registry": "https://registry.npmjs.org",
55
56
  "access": "public"
56
57
  },
57
58
  "scripts": {
@@ -66,7 +67,6 @@
66
67
  "test:watch": "vitest",
67
68
  "test:e2e": "vitest run tests/e2e",
68
69
  "cli": "node --import tsx bin/sentinel.ts",
69
- "changeset": "changeset",
70
- "release": "pnpm build && changeset publish"
70
+ "docs:traces": "pnpm build && bash scripts/gen-traces.sh"
71
71
  }
72
72
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../bin/sentinel.ts","../../src/core/context.ts","../../src/core/discover-modules.ts","../../src/core/settings.ts","../../src/core/domain.ts","../../src/core/orchestrate.ts","../../src/core/render.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: a command composes verb × type × location.\n *\n * Exactly one verb (--run / --inspect / --report / --status / --update); a type (--typescript …)\n * or ALL when none is named; and a location resolved by `resolveContext` (a module\n * directory, or the workspace root with --module / --ci / all). The CLI is generic: it\n * resolves each target's adapter (honouring --runner) and dispatches; tools arrive as\n * adapters in later tickets. The flavour is detected from a module's dependencies\n * (deterministic, `node` default) and can be overridden with --flavour (recommended\n * for --update).\n */\nimport { program } from 'commander'\n\nimport { registerAdapters } from '../src/adapters.js'\nimport { resolveContext } from '../src/core/context.js'\nimport { FLAVOURS, TARGETS, VERBS, type Flavour, type Target } from '../src/core/domain.js'\nimport { dispatch } from '../src/core/dispatch.js'\nimport { analyse, generateSummaries } from '../src/core/orchestrate.js'\nimport {\n renderStatusRow,\n renderStatusSummary,\n renderSummary,\n statusCoverage,\n} from '../src/core/render.js'\nimport { palette } from '../src/shared/color.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\nprogram\n .name('sentinel')\n .description('One CLI that guards code health: presets, analysis, and arch checks.')\n .version(readOwnVersion())\n .configureHelp({ sortOptions: false })\n // On any parse error (unknown flag, missing value): suggest the closest flag and\n // point at --help, so a typo like `--lnt` gets \"did you mean --lint?\".\n .showSuggestionAfterError(true)\n .showHelpAfterError('(run \"sentinel --help\" for usage)')\n .addHelpText(\n 'before',\n [\n 'A check composes: verb + type + location.',\n ' verb what to do: --run --inspect --report --status --update',\n ' type which check: --lint --typescript ... (omit = all types; or --all)',\n ' where run from a MODULE dir → that module; from the ROOT → --module <name>,',\n ' --ci (affected), or all modules. --update targets one module only.',\n '',\n ].join('\\n'),\n )\n // verbs (pick one)\n .option('--run', 'execute the target tool')\n .option('--inspect', 'show the resolved configuration')\n .option('--update', 'generate/apply the config stubs')\n .option('--report', 'metrics and health report')\n .option('--status', 'adoption + conformity across modules (coverage + drift)')\n // targets (pick one, or --all)\n .option('--lint', 'linting')\n .option('--format', 'formatting')\n .option('--typescript', 'type checking')\n .option('--build', 'build')\n .option('--test', 'tests')\n .option('--static-analysis', 'cycles, complexity, duplication, centrality')\n .option('--runtime-analysis', 'bundle, Lighthouse, web vitals')\n .option('--arch', 'architecture boundaries')\n .option('--all', 'every target')\n // modifiers\n .option(\n '--module <name>',\n 'from the workspace root: scope to one module (omit = all; inside a module dir, drop this)',\n )\n .option('--flavour <name>', `override the detected stack preset (${FLAVOURS.join(', ')})`)\n .option('--runner <tool>', 'override the default runner (e.g. eslint, biome)')\n .option('--ci', 'CI mode: from the root, only the affected modules; non-zero exit on failure')\n .option('--fix', 'auto-fix where applicable')\n .option('--dry-run', 'preview the changes without writing (--update)')\n .option('--json', 'machine-readable JSON output (report/inspect/--dry-run)')\n .addHelpText(\n 'after',\n [\n '',\n 'Examples:',\n ' sentinel --run --typescript # in a module → that module',\n ' sentinel --report --typescript --module bff-admin # from root → one module',\n ' sentinel --report # from root → all types, all modules',\n ' sentinel --report --ci # from root → affected only',\n ' sentinel --update --typescript --flavour react # write stubs for the current module',\n ].join('\\n'),\n )\n .parse()\n\nconst opts = program.opts()\n\n// commander camelCases hyphenated flags (--static-analysis -> staticAnalysis).\nfunction toCamel(flag: string): string {\n return flag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())\n}\n\n/** Exactly one of `keys` must be flagged; return it, else fail with guidance. */\nfunction pickOne<T extends string>(kind: string, keys: readonly T[]): T {\n const chosen = keys.filter((k) => opts[toCamel(k)])\n if (chosen.length !== 1) {\n const supported = keys.map((k) => `--${k}`).join(', ')\n program.error(\n chosen.length === 0\n ? `Missing a ${kind}. Supported: ${supported}.`\n : `Pick exactly one ${kind}: got ${chosen.map((k) => `--${k}`).join(', ')}.`,\n )\n }\n return chosen[0] as T\n}\n\n/**\n * The explicit `--flavour`, VALIDATED, or undefined when not given. When undefined,\n * the flavour is DETECTED from the module's dependencies (see `detect-framework.ts`);\n * this override is recommended for `--update`, where hoisted deps can make detection\n * fall back to `node`. An unknown value is a hard error (typo caught), not a silent guess.\n */\nfunction parseFlavour(value: unknown): Flavour | undefined {\n if (value === undefined) return undefined\n if (typeof value !== 'string' || !FLAVOURS.includes(value as Flavour)) {\n return program.error(\n `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(', ')}.`,\n )\n }\n return value as Flavour\n}\n\nfunction asMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nconst verb = pickOne('verb', VERBS)\nconst cwd = process.cwd()\nconst flavour = parseFlavour(opts.flavour)\n\n// Targets (the \"type\" axis): a named one, or ALL when none is given. Uniform for every\n// verb — run / inspect / report / update. `resolveContext` owns the \"location\" axis.\nconst namedTargets = TARGETS.filter((t) => opts[toCamel(t)])\nif (opts.all && namedTargets.length > 0) {\n program.error(\n `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(', ')}.`,\n )\n}\nconst targets: Target[] = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets\n\n// --dry-run only previews a write.\nif (opts.dryRun && verb !== 'update') {\n program.error('--dry-run only applies to --update (the read verbs never write).')\n}\n\n/**\n * run / inspect / report: resolve the module context (cwd module, `--module`, all, or\n * `--ci` affected), then execute verb × targets across it and print.\n */\nasync function runVerb(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) })\n const out = palette(process.stdout)\n const err = palette(process.stderr)\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'run' | 'report' | 'inspect' | 'status',\n targets,\n modules,\n runner: opts.runner,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) =>\n process.stderr.write(err.dim(` [${done}/${total}] ${name}\\n`)),\n })\n\n // Shape each outcome into a summary row, then print it colored. `run` rows carry no\n // metrics (bare head line); `status` has its own 3-state rows + a coverage footer;\n // `--json` bypasses the human rendering entirely.\n const summary = generateSummaries(results)\n if (opts.json && verb !== 'run') {\n const payload =\n verb === 'status' ? { ...summary, coverage: statusCoverage(summary.results) } : summary\n process.stdout.write(JSON.stringify(payload, null, 2) + '\\n')\n } else if (verb === 'status') {\n for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + '\\n')\n process.stdout.write(renderStatusSummary(summary.results, out) + '\\n')\n } else {\n for (const item of summary.results) {\n process.stdout.write(renderSummary(item, out) + '\\n')\n }\n }\n process.stderr.write(\n err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms\\n`),\n )\n // `run` always reports failures via its exit code; report/inspect only under --ci.\n return verb === 'run' || opts.ci ? worstCode : 0\n}\n\n/**\n * update: a WRITE. Same context rule, but it must resolve to exactly ONE module (the\n * current one, or `--module`). Adopting every module at once would be a big-bang, so\n * the implicit \"all\" is deliberately refused; adopt gradually.\n */\nasync function runUpdate(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false })\n const [module] = modules\n if (scope === 'all' || scope === 'affected' || !module) {\n program.error(\n '--update writes files: target one module (run from its directory, or pass --module). ' +\n 'Adopting every module at once is intentionally not allowed — adopt gradually.',\n )\n }\n let worst = 0\n for (const type of targets) {\n try {\n const code = await dispatch({\n verb,\n target: type,\n runner: opts.runner,\n module: module.name,\n cwd: module.root,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // With --all, report per target and keep going; a single target fails hard.\n process.stderr.write(`\\nsentinel (${type}): ${asMessage(err)}\\n`)\n if (targets.length === 1) return 1\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\n\nregisterAdapters() // wire in every tool adapter; the CLI itself never lists them\n\n;(verb === 'update' ? runUpdate() : runVerb())\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`\\nsentinel: ${asMessage(err)}\\n`)\n process.exit(1)\n })\n","/**\n * Context resolution: the composable \"location\" axis of a command.\n *\n * One rule for every read verb (run / inspect / report), so a developer works the way\n * they already work with nx, from their module or from the root:\n * - in a MODULE dir (has package.json/project.json, and is NOT the workspace root):\n * the context is THAT module; `--module` is redundant there and is rejected.\n * - at the workspace ROOT: `--module X` scopes to X, `--ci` scopes to the affected\n * set, and otherwise it is every module (the whole-repo status).\n * Update (a write) reuses this but forbids the implicit \"all\" (see the CLI).\n */\nimport { existsSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { readNxProjectName } from '../shared/package-json.js'\nimport { discoverModules, type ModuleRef } from './discover-modules.js'\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\n/** A directory is a module if it carries one of these (and is not the root). */\nconst MODULE_MARKERS = ['package.json', 'project.json'] as const\n\n/** How the context was resolved, for messages and the \"all\"-guard on update. */\nexport type ContextScope = 'cwd-module' | 'named-module' | 'affected' | 'all'\n\nexport interface ResolvedContext {\n modules: ModuleRef[]\n scope: ContextScope\n}\n\nfunction isModuleDir(cwd: string): boolean {\n return MODULE_MARKERS.some((marker) => existsSync(join(cwd, marker)))\n}\n\n/**\n * Resolve which modules a command targets from where it runs + its flags. Throws with\n * an actionable message when the combination is contradictory (e.g. `--module` from\n * inside a module) or the directory is neither a module nor the workspace root.\n */\nexport function resolveContext(\n cwd: string,\n opts: { module?: string; ci?: boolean },\n): ResolvedContext {\n const atRoot = existsSync(join(cwd, WORKSPACE_ROOT_MARKER))\n\n // In a module directory: the module IS the current one.\n if (!atRoot && isModuleDir(cwd)) {\n if (opts.module) {\n throw new Error(\n 'You are in a module directory: drop --module (the context is the current module).',\n )\n }\n if (opts.ci) {\n throw new Error('--ci selects the affected set from the workspace root; run it there.')\n }\n const name = readNxProjectName(cwd) ?? basename(cwd)\n return { modules: [{ name, root: cwd }], scope: 'cwd-module' }\n }\n\n // At the workspace root: --module (one), --ci (affected), or every module.\n if (atRoot) {\n if (opts.module) {\n const found = discoverModules(cwd).find((module) => module.name === opts.module)\n if (!found) throw new Error(`module \"${opts.module}\" not found in the workspace.`)\n return { modules: [found], scope: 'named-module' }\n }\n if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: 'affected' }\n return { modules: discoverModules(cwd), scope: 'all' }\n }\n\n throw new Error(\n `Run sentinel from a module directory or the workspace root ` +\n `(found neither ${MODULE_MARKERS.join('/')} nor ${WORKSPACE_ROOT_MARKER} here).`,\n )\n}\n","/**\n * Module discovery via nx. `--report`/`--inspect` without a `--module` analyse\n * every project; `--ci` narrows to the affected ones. We ask nx (its cached graph)\n * for the authoritative names + roots in a couple of calls, not one per project,\n * so it stays fast on the real monorepo.\n */\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { resolveBin } from '../shared/resolve-bin.js'\n\n/** A discovered module: its nx project name and absolute root. */\nexport interface ModuleRef {\n name: string\n root: string\n}\n\nfunction runNx(cwd: string, args: string[]): string {\n const nx = resolveBin(cwd, 'nx') ?? 'nx'\n try {\n // Disable the daemon for deterministic, CI-friendly one-shot invocations.\n return execFileSync(nx, args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, NX_DAEMON: 'false' },\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,\n { cause: error },\n )\n }\n}\n\n/** Every module (name -> absolute root) from the nx project graph, in one call. */\nfunction readGraph(cwd: string): ModuleRef[] {\n const dir = mkdtempSync(join(tmpdir(), 'sentinel-nx-'))\n const file = join(dir, 'graph.json')\n try {\n runNx(cwd, ['graph', '--file', file])\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as {\n graph?: { nodes?: Record<string, { data: { root: string } }> }\n }\n const nodes = parsed.graph?.nodes\n if (!nodes || typeof nodes !== 'object') {\n throw new Error(\n 'sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible.',\n )\n }\n return Object.entries(nodes).map(([name, node]) => ({\n name,\n root: join(cwd, node.data.root),\n }))\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n\nexport function discoverModules(cwd: string, options: { affected?: boolean } = {}): ModuleRef[] {\n const modules = readGraph(cwd)\n if (!options.affected) return modules\n const affected = new Set(\n JSON.parse(runNx(cwd, ['show', 'projects', '--affected', '--json'])) as string[],\n )\n return modules.filter((module) => affected.has(module.name))\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the flavour is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--flavour` overrides the detection (recommended for `--update`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n","/**\n * The domain vocabulary: the fixed sets of verbs, targets, and flavours, and the\n * types derived from them. This is the model, and the extension point: adding a\n * verb / target / flavour is a one-line edit to a list here, and because the types\n * are DERIVED (`(typeof LIST)[number]`), the compiler forces every switch/handler\n * to cover the new member.\n *\n * Tunable behaviour (defaults, detection signals, marker filenames) lives in\n * `settings.ts`, not here.\n */\n\n/** Verbs: what to do. Each maps to an adapter method in dispatch. */\nexport const VERBS = ['run', 'inspect', 'update', 'report', 'status'] as const\nexport type Verb = (typeof VERBS)[number]\n\n/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */\nexport const TARGETS = [\n 'lint',\n 'format',\n 'typescript',\n 'build',\n 'test',\n 'static-analysis',\n 'runtime-analysis',\n 'arch',\n] as const\nexport type Target = (typeof TARGETS)[number]\n\n/** Flavours: the stack preset a project resolves to (strict by default). */\nexport const FLAVOURS = ['react', 'nest', 'svelte', 'node'] as const\nexport type Flavour = (typeof FLAVOURS)[number]\n","/**\n * Orchestration for `--report`/`--inspect`: analyse a set of modules across a set\n * of targets, then shape the outcomes into one versioned, parseable summary.\n *\n * The engine resolves each module's flavour and adapter and calls the per-module\n * method; a target with no adapter yet is simply skipped. `generateSummaries`\n * builds the aggregate by reusing `generateSummary` for each result (one factory,\n * no duplicated shaping between the single- and multi-module paths).\n */\nimport { palette } from '../shared/color.js'\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, Target } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext } from './types.js'\n\n/** One project × target outcome. */\nexport interface AnalyseResult {\n project: string\n target: Target\n flavour: Flavour\n ok: boolean\n /** `--report`: metrics (errors, implicit-any); `--inspect`: the resolved config. */\n data: unknown\n}\n\nexport interface AnalyseParams {\n verb: 'run' | 'report' | 'inspect' | 'status'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n /** Explicit `--flavour`, when declared; otherwise detected per module from deps. */\n flavour?: Flavour\n ci: boolean\n fix: boolean\n /** Called after each module, for progress display. */\n onProgress?: (done: number, total: number, moduleName: string) => void\n}\n\n/** Run the analyse across modules × targets. Returns the results + worst exit code. */\nexport async function analyse(\n params: AnalyseParams,\n): Promise<{ results: AnalyseResult[]; worstCode: number }> {\n const results: AnalyseResult[] = []\n let worstCode = 0\n let done = 0\n\n for (const module of params.modules) {\n // Explicit --flavour wins; otherwise detect it from the module's deps.\n const flavour = params.flavour ?? detectFramework(readProjectPackageJson(module.root))\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch {\n continue // no adapter for this target yet: skip it (not a failure)\n }\n // Isolate every check: one module/target that throws must not abort the whole\n // sweep. A failure becomes a `ok:false` row (with the error) so `--report --all`\n // still returns a complete picture across a large workspace.\n try {\n if (params.verb === 'run') {\n // Label the run so multi-module output (root/--all) is readable; the tool\n // streams its own output (stdio inherit) between headers.\n if (params.modules.length > 1) {\n const err = palette(process.stderr)\n process.stderr.write(\n `\\n ${err.accent('▶')} ${err.strong(module.name)} ${err.dim(`(${flavour})`)} ${target}\\n`,\n )\n }\n const result = await adapter.run(ctx)\n results.push({ project: module.name, target, flavour, ok: result.ok, data: {} })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'report') {\n const result = await adapter.report(ctx)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n data: result.metrics ?? {},\n })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'inspect') {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n } else {\n // status: adoption + conformity, read from the committed config (no tool run).\n // A drifted adopted module is a conformance failure; a non-adopted one is not.\n const status = await adapter.status(ctx)\n const ok = !status.adopted || status.conformant\n results.push({ project: module.name, target, flavour, ok, data: status })\n if (!ok) worstCode = Math.max(worstCode, 1)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n results.push({ project: module.name, target, flavour, ok: false, data: { error: message } })\n worstCode = Math.max(worstCode, 1)\n }\n }\n params.onProgress?.((done += 1), params.modules.length, module.name)\n }\n\n return { results, worstCode }\n}\n\n/** The shaped summary for ONE result (the factory unit). */\nexport function generateSummary(result: AnalyseResult): Record<string, unknown> {\n const { project, target, flavour, ok, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, ...details }\n}\n\n/** The aggregate, versioned envelope for MANY results, built from `generateSummary`. */\nexport function generateSummaries(results: readonly AnalyseResult[]): {\n schemaVersion: number\n results: Record<string, unknown>[]\n} {\n return { schemaVersion: 1, results: results.map(generateSummary) }\n}\n","/**\n * Human rendering of one summary row (`--run`/`--report`/`--inspect`), colored via a\n * `Palette`. Pure and stream-agnostic: it returns the text, the CLI writes it. `--json`\n * bypasses this entirely (machines get the raw envelope).\n *\n * The goal is legibility: the pass/fail mark and the module name read at a glance, the\n * error counts stand out when non-zero, and the phased `deferred` rules become their own\n * indented block instead of an inline JSON blob.\n */\nimport type { Palette } from '../shared/color.js'\n\n/** Keys carried by the row head (or redundant with it), never repeated in the details. */\nconst HEAD_KEYS = new Set(['project', 'module', 'target', 'flavour', 'ok'])\n\n/** A deferred (phased) rule, as `--inspect` reports it. */\ninterface DeferredRule {\n rule: string\n phase: number\n reason: string\n}\n\nfunction isDeferredRules(value: unknown): value is DeferredRule[] {\n return (\n Array.isArray(value) &&\n value.every((entry) => typeof entry === 'object' && entry !== null && 'rule' in entry)\n )\n}\n\n/** Color a detail value: error counts are the signal, so red when >0, green at 0. */\nfunction renderValue(key: string, value: unknown, p: Palette): string {\n if ((key === 'errors' || key === 'implicitAny') && typeof value === 'number') {\n return value > 0 ? p.fail(String(value)) : p.ok(String(value))\n }\n return typeof value === 'string' ? value : JSON.stringify(value)\n}\n\n/**\n * One summary row → colored lines: a head line (`✓ name (flavour) target — key=value …`)\n * plus, when present, an indented `deferred` block listing the phased rules.\n */\nexport function renderSummary(item: Record<string, unknown>, p: Palette): string {\n const ok = item.ok === true\n const mark = ok ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n // Split details: the phased `deferred` rules get their own block; everything else is\n // rendered inline as dim `key=value` pairs (the head keys are dropped as redundant).\n const entries = Object.entries(item).filter(([key]) => !HEAD_KEYS.has(key))\n const deferred = entries.find(([key]) => key === 'deferred')?.[1]\n const inline = entries\n .filter(([key]) => key !== 'deferred')\n .map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`)\n .join(' ')\n\n const lines = [inline ? `${head} ${p.dim('—')} ${inline}` : head]\n\n if (isDeferredRules(deferred) && deferred.length > 0) {\n lines.push(` ${p.warn('deferred (phase 1, non-breaking):')}`)\n for (const { rule, phase, reason } of deferred) {\n lines.push(` ${p.warn('•')} ${p.strong(rule)} ${p.dim(`— phase ${phase}: ${reason}`)}`)\n }\n }\n return lines.join('\\n')\n}\n\n/** Workspace adoption + conformity totals, for the `--status` footer / JSON. */\nexport interface Coverage {\n total: number\n adopted: number\n conformant: number\n drifted: number\n}\n\nexport function statusCoverage(items: readonly Record<string, unknown>[]): Coverage {\n const adopted = items.filter((i) => i.adopted === true)\n const conformant = adopted.filter((i) => i.conformant === true)\n return {\n total: items.length,\n adopted: adopted.length,\n conformant: conformant.length,\n drifted: adopted.length - conformant.length,\n }\n}\n\n/** Short preset name for display (`@hublo/sentinel/tsconfig/nest` -> `nest`). */\nfunction presetShort(preset: unknown): string {\n return typeof preset === 'string' ? preset.replace('@hublo/sentinel/tsconfig/', '') : '?'\n}\n\n/**\n * One `--status` row, three states: not-adopted (dim `·`), adopted + conformant (green\n * `✓`), adopted + drifted (red `✗`, with the drifted keys named).\n */\nexport function renderStatusRow(item: Record<string, unknown>, p: Palette): string {\n const adopted = item.adopted === true\n const conformant = item.conformant === true\n const mark = !adopted ? p.dim('·') : conformant ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n if (!adopted) return `${head} ${p.dim('— not adopted')}`\n if (conformant) {\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)}) conformant`)}`\n }\n const drift = Array.isArray(item.drift) ? item.drift.join(', ') : ''\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)})`)} ${p.fail(`drift: ${drift}`)}`\n}\n\n/** The `--status` footer: `coverage: X/N adopted · K/X conformant`. */\nexport function renderStatusSummary(items: readonly Record<string, unknown>[], p: Palette): string {\n const c = statusCoverage(items)\n const drift = c.drifted > 0 ? p.fail(` · ${c.drifted} drifted`) : ''\n return ` ${p.strong('coverage:')} ${c.adopted}/${c.total} adopted ${p.dim('·')} ${c.conformant}/${c.adopted} conformant${drift}`\n}\n"],"mappings":"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","names":["join","cwd","cwd","join","opts","flavour"]}