@hublo/sentinel 1.1.0-alpha.6 → 1.1.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.
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-U2NAKQXR.js";
19
+ } from "../chunk-FA6N4XCP.js";
20
20
 
21
21
  // bin/sentinel.ts
22
22
  import { program } from "commander";
@@ -223,6 +223,14 @@ async function analyse(params) {
223
223
  toolArgs: params.toolArgs
224
224
  };
225
225
  try {
226
+ let adoption;
227
+ if (params.verb === "run" || params.verb === "report") {
228
+ try {
229
+ adoption = await adapter.status(ctx);
230
+ } catch {
231
+ }
232
+ }
233
+ const adoptionFields = adoption ? { adopted: adoption.adopted, preset: adoption.preset, conformant: adoption.conformant } : {};
226
234
  if (params.verb === "run") {
227
235
  if (params.modules.length > 1) {
228
236
  const err = palette(process.stderr);
@@ -239,7 +247,7 @@ async function analyse(params) {
239
247
  flavour: effectiveFlavour,
240
248
  ok: result.ok,
241
249
  status: result.ok ? "ok" : "failed",
242
- data: {}
250
+ data: { ...adoptionFields }
243
251
  });
244
252
  worstCode = Math.max(worstCode, result.code);
245
253
  } else if (params.verb === "report") {
@@ -250,12 +258,15 @@ async function analyse(params) {
250
258
  flavour: effectiveFlavour,
251
259
  ok: result.ok,
252
260
  status: result.ok ? "ok" : "failed",
253
- data: result.metrics ?? {}
261
+ // Engine fields LAST, so they WIN. Spreading them first let a role's own metrics
262
+ // override them, which is the disagreement this was written to remove: the point
263
+ // is that adoption has exactly one source, not that it has a default.
264
+ data: { ...result.metrics ?? {}, ...adoptionFields }
254
265
  });
255
266
  worstCode = Math.max(worstCode, result.code);
256
267
  } else if (params.verb === "inspect") {
257
- const [config, adoption] = await Promise.all([adapter.inspect(ctx), adapter.status(ctx)]);
258
- const ok = !adoption.unreadable && (!adoption.adopted || adoption.conformant);
268
+ const [config, adoption2] = await Promise.all([adapter.inspect(ctx), adapter.status(ctx)]);
269
+ const ok = !adoption2.unreadable && (!adoption2.adopted || adoption2.conformant);
259
270
  results.push({
260
271
  project: module.name,
261
272
  target,
@@ -265,7 +276,7 @@ async function analyse(params) {
265
276
  // `inspect` is typed as `unknown` (each role shapes its own detail), so the
266
277
  // merge is guarded rather than assumed: a role returning a non-object still
267
278
  // gets its adoption fields instead of throwing here.
268
- data: config && typeof config === "object" ? { ...config, ...adoption } : { ...adoption }
279
+ data: config && typeof config === "object" ? { ...config, ...adoption2 } : { ...adoption2 }
269
280
  });
270
281
  if (!ok) worstCode = Math.max(worstCode, 1);
271
282
  } else {
@@ -1057,7 +1057,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1057
1057
  */
1058
1058
  async report(ctx) {
1059
1059
  const adoption = readFormatAdoption(ctx.cwd);
1060
- const base = { adopted: adoption.adopted, preset: adoption.preset };
1060
+ const base = {};
1061
1061
  if (!adoption.adopted) return { ok: true, code: 0, metrics: { ...base, unformatted: 0 } };
1062
1062
  const oxfmt = resolveOxfmt(ctx.cwd);
1063
1063
  if (!oxfmt) {
@@ -3023,9 +3023,15 @@ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3023
3023
  import { join as join13 } from "path";
3024
3024
 
3025
3025
  // src/core/config/owned-keys.ts
3026
- function presetOwnedKeys(config, permitted) {
3026
+ function presetOwnedKeys(config, permitted, presetSets) {
3027
3027
  if (!config) return [];
3028
- return Object.keys(config).filter((key) => !permitted.includes(key));
3028
+ return Object.keys(config).filter(
3029
+ (key) => !permitted.includes(key) && (presetSets === void 0 || presetSets.includes(key))
3030
+ );
3031
+ }
3032
+ function localOnlyKeys(config, permitted, presetSets) {
3033
+ if (!config) return [];
3034
+ return Object.keys(config).filter((key) => !permitted.includes(key) && !presetSets.includes(key));
3029
3035
  }
3030
3036
 
3031
3037
  // src/core/config/resolve-config-target.ts
@@ -3095,7 +3101,7 @@ function readAdoption(cwd, options) {
3095
3101
  const preset = normaliseExtends(parsed.extends).find((entry) => options.presetPattern.test(entry)) ?? null;
3096
3102
  if (preset === null) return NOT_ADOPTED2(target.path);
3097
3103
  const settings = options.settingsKey === null ? parsed : parsed[options.settingsKey];
3098
- const drift = presetOwnedKeys(settings, options.permitted);
3104
+ const drift = presetOwnedKeys(settings, options.permitted, options.presetOwns?.(preset));
3099
3105
  return {
3100
3106
  configFile: target.path,
3101
3107
  preset,
@@ -3740,10 +3746,9 @@ var OxlintAdapter = class extends BaseAdapter {
3740
3746
  /** Lint health for `--report`. Deepened in a later step; today it is the run outcome. */
3741
3747
  async report(ctx) {
3742
3748
  const adoption = readLintAdoption(ctx.cwd);
3743
- if (!adoption.adopted) return { ok: true, code: 0, metrics: { adopted: false } };
3749
+ if (!adoption.adopted) return { ok: true, code: 0, metrics: {} };
3744
3750
  const oxlint = resolveOxlint(ctx.cwd);
3745
- if (!oxlint)
3746
- return { ok: false, code: 1, metrics: { adopted: true, error: "oxlint not found" } };
3751
+ if (!oxlint) return { ok: false, code: 1, metrics: { error: "oxlint not found" } };
3747
3752
  const typeAware = canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [];
3748
3753
  const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
3749
3754
  cwd: ctx.cwd,
@@ -3751,7 +3756,7 @@ var OxlintAdapter = class extends BaseAdapter {
3751
3756
  env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) }
3752
3757
  });
3753
3758
  if (result.error) {
3754
- return { ok: false, code: 1, metrics: { adopted: true, error: result.error.message } };
3759
+ return { ok: false, code: 1, metrics: { error: result.error.message } };
3755
3760
  }
3756
3761
  const diagnostics = parseOxlintDiagnostics(result.stdout ?? "");
3757
3762
  const errors = diagnostics.filter((entry) => entry.severity === "error").length;
@@ -3769,7 +3774,6 @@ var OxlintAdapter = class extends BaseAdapter {
3769
3774
  ok: errors === 0,
3770
3775
  code: errors === 0 ? 0 : 1,
3771
3776
  metrics: {
3772
- adopted: true,
3773
3777
  errors,
3774
3778
  warnings,
3775
3779
  rules,
@@ -3952,6 +3956,14 @@ validate2();
3952
3956
  function hasShippedPreset(preset) {
3953
3957
  return preset in LAYERS;
3954
3958
  }
3959
+ function tsPresetFor(preset) {
3960
+ const layer = LAYERS[preset];
3961
+ if (!layer) throw new Error(`no TypeScript preset ships for preset "${preset}"`);
3962
+ return {
3963
+ ...BASE.$schema === void 0 ? {} : { $schema: BASE.$schema },
3964
+ compilerOptions: { ...BASE.compilerOptions, ...layer.compilerOptions }
3965
+ };
3966
+ }
3955
3967
 
3956
3968
  // src/roles/typescript/phased-rules.ts
3957
3969
  function deferredRuleNames2() {
@@ -3966,8 +3978,17 @@ var PERMITTED_COMPILER_OPTIONS = [
3966
3978
  "outDir",
3967
3979
  "tsBuildInfoFile"
3968
3980
  ];
3969
- function presetOwnedKeys2(compilerOptions) {
3970
- return presetOwnedKeys(compilerOptions, PERMITTED_COMPILER_OPTIONS);
3981
+ function presetCompilerOptionKeys(preset) {
3982
+ if (preset === void 0 || !hasShippedPreset(preset)) return void 0;
3983
+ return Object.keys(tsPresetFor(preset).compilerOptions);
3984
+ }
3985
+ function presetOwnedKeys2(compilerOptions, preset) {
3986
+ return presetOwnedKeys(compilerOptions, PERMITTED_COMPILER_OPTIONS, presetCompilerOptionKeys(preset));
3987
+ }
3988
+ function localOnlyCompilerOptions(compilerOptions, preset) {
3989
+ const sets = presetCompilerOptionKeys(preset);
3990
+ if (sets === void 0) return [];
3991
+ return localOnlyKeys(compilerOptions, PERMITTED_COMPILER_OPTIONS, sets);
3971
3992
  }
3972
3993
 
3973
3994
  // src/roles/typescript/resolve-tsconfig-target.ts
@@ -3991,7 +4012,11 @@ function readTsconfigAdoption(cwd) {
3991
4012
  fallback: TSCONFIG_FALLBACK,
3992
4013
  presetPattern: SENTINEL_PRESET,
3993
4014
  settingsKey: "compilerOptions",
3994
- permitted: PERMITTED_COMPILER_OPTIONS
4015
+ permitted: PERMITTED_COMPILER_OPTIONS,
4016
+ // Drift is measured against what the preset SETS, not against the allowlist alone: see
4017
+ // `presetOwnedKeys`. Without this a module overriding the workspace base reads as
4018
+ // non-conformant forever, since re-running `--init` would not (and should not) fix it.
4019
+ presetOwns: (path) => presetCompilerOptionKeys(path.slice(path.lastIndexOf("/") + 1)) ?? []
3995
4020
  });
3996
4021
  }
3997
4022
 
@@ -4086,7 +4111,8 @@ function planAdoption(context) {
4086
4111
  target.path
4087
4112
  );
4088
4113
  const extendsChain = composeExtends(existing.extends, preset);
4089
- const drift = presetOwnedKeys2(existing.compilerOptions);
4114
+ const drift = presetOwnedKeys2(existing.compilerOptions, context.preset);
4115
+ const keptLocal = localOnlyCompilerOptions(existing.compilerOptions, context.preset);
4090
4116
  const operations = [
4091
4117
  { kind: "merge-json", path: target.path, value: { extends: extendsChain } }
4092
4118
  ];
@@ -4101,6 +4127,11 @@ function planAdoption(context) {
4101
4127
  });
4102
4128
  notes.push(`stripped preset-owned compilerOptions: ${drift.join(", ")}`);
4103
4129
  }
4130
+ if (keptLocal.length > 0) {
4131
+ notes.push(
4132
+ `KEPT this module's own compilerOptions (${keptLocal.join(", ")}): the preset does not set them, so they are yours rather than drift. Removing them would not hand the decision to the preset, it would hand it to the workspace tsconfig this module extends`
4133
+ );
4134
+ }
4104
4135
  operations.push(addScript, ...nxTargets);
4105
4136
  const existingCheck = moduleScripts(context.cwd)[TYPECHECK_SCRIPT_NAME];
4106
4137
  if (keepsForeignChecker(existingCheck)) {
@@ -4133,7 +4164,7 @@ function parseDiagnostics(output) {
4133
4164
  return diagnostics;
4134
4165
  }
4135
4166
  var PHASED_STRICTNESS_WARNING = `sentinel typescript: phase 1 (non-breaking) \u2014 deferred: ${deferredRuleNames2().join(", ")}. Enabled centrally in a later wave; run \`sentinel --inspect --typescript\` for the list, or \`sentinel --run --typescript -- --noImplicitAny\` to measure what one would cost today.`;
4136
- var TscAdapter = class extends BaseAdapter {
4167
+ var TscAdapter = class _TscAdapter extends BaseAdapter {
4137
4168
  target = "typescript";
4138
4169
  runner = "tsc";
4139
4170
  /**
@@ -4241,6 +4272,13 @@ var TscAdapter = class extends BaseAdapter {
4241
4272
  );
4242
4273
  return { ok: true, code: 0 };
4243
4274
  }
4275
+ if (!readTsconfigAdoption(ctx.cwd).adopted) {
4276
+ process.stderr.write(
4277
+ `sentinel typescript(tsc): no sentinel preset in this module's tsconfig; run \`sentinel --init --typescript\` to adopt.
4278
+ `
4279
+ );
4280
+ return { ok: true, code: 0 };
4281
+ }
4244
4282
  const missing = this.missingPresetPackage(ctx.cwd);
4245
4283
  if (missing) {
4246
4284
  process.stderr.write(
@@ -4366,10 +4404,23 @@ var TscAdapter = class extends BaseAdapter {
4366
4404
  * 7006/7031/7053/… ), the signal that drives the noImplicitAny migration. No
4367
4405
  * tsconfig is a clean, empty report.
4368
4406
  */
4407
+ /**
4408
+ * The metrics for a module this role has nothing to say about. Every KEY the full path
4409
+ * emits, so a consumer never sees a row whose shape depends on whether the module adopted.
4410
+ */
4411
+ static NOTHING_TO_REPORT = {
4412
+ errors: 0,
4413
+ implicitAny: "deferred",
4414
+ diagnostics: [],
4415
+ diagnosticsTruncated: false
4416
+ };
4369
4417
  async report(ctx) {
4370
4418
  const config = this.typecheckTarget(ctx.cwd);
4371
4419
  if (!config) {
4372
- return { ok: true, code: 0, metrics: { errors: 0, implicitAny: "deferred", diagnostics: [] } };
4420
+ return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
4421
+ }
4422
+ if (!readTsconfigAdoption(ctx.cwd).adopted) {
4423
+ return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
4373
4424
  }
4374
4425
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
4375
4426
  const result = spawnSync3(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
@@ -4861,4 +4912,4 @@ export {
4861
4912
  detectFramework,
4862
4913
  dispatch
4863
4914
  };
4864
- //# sourceMappingURL=chunk-U2NAKQXR.js.map
4915
+ //# sourceMappingURL=chunk-FA6N4XCP.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-U2NAKQXR.js";
10
+ } from "./chunk-FA6N4XCP.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": "1.1.0-alpha.6",
3
+ "version": "1.1.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",