@vodmal/vdx-cli 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/narrow.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What a parsed file (JSON, YAML, TOML) or a caught error holds is `unknown` until checked.
3
+ * These helpers check it on the way in, so the code past them works with real types.
4
+ */
5
+
6
+ export type Dict = Record<string, unknown>;
7
+
8
+ /** A plain object — not null, not an array. */
9
+ export function isDict(v: unknown): v is Dict {
10
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
11
+ }
12
+
13
+ /** `v` when it is a plain object, else undefined. */
14
+ export function dict(v: unknown): Dict | undefined {
15
+ return isDict(v) ? v : undefined;
16
+ }
17
+
18
+ /**
19
+ * The value under `keys`, one level per key — an object's property or an array's index ("0");
20
+ * undefined as soon as a level is not an object.
21
+ */
22
+ export function at(v: unknown, ...keys: string[]): unknown {
23
+ let cur = v;
24
+ for (const k of keys) {
25
+ if (typeof cur !== 'object' || cur === null) return undefined;
26
+ cur = (cur as Dict)[k];
27
+ }
28
+ return cur;
29
+ }
30
+
31
+ /** `v` when it is a string, else undefined. */
32
+ export function str(v: unknown): string | undefined {
33
+ return typeof v === 'string' ? v : undefined;
34
+ }
35
+
36
+ /** `v` when it is an array, else an empty one. */
37
+ export function list(v: unknown): unknown[] {
38
+ return Array.isArray(v) ? (v as unknown[]) : [];
39
+ }
40
+
41
+ export function isStringList(v: unknown): v is string[] {
42
+ return Array.isArray(v) && v.every((x) => typeof x === 'string');
43
+ }
44
+
45
+ /** The message of a caught value, whatever was thrown. */
46
+ export function errorMessage(e: unknown): string {
47
+ return e instanceof Error ? e.message : String(e);
48
+ }
49
+
50
+ /** What a failed child process said: its stderr, else the error's message. */
51
+ export function stderrOf(e: unknown): string {
52
+ const err = at(e, 'stderr');
53
+ if (typeof err === 'string' && err) return err;
54
+ if (Buffer.isBuffer(err) && err.length > 0) return err.toString();
55
+ return errorMessage(e);
56
+ }
@@ -92,17 +92,25 @@ function stateOf(h: PersonalHook, root: string | null, deps: PersonalHookDeps):
92
92
  let drift: string | null = null;
93
93
  if (have.length === 0 && command === null) drift = 'not in ~/.gitconfig';
94
94
  else if (command !== h.command) drift = `its command in ~/.gitconfig differs from the profile`;
95
- else if (events(h).some((e) => !have.includes(e))) drift = `not bound to ${events(h).filter((e) => !have.includes(e)).join(', ')}`;
95
+ else if (events(h).some((e) => !have.includes(e)))
96
+ drift = `not bound to ${events(h)
97
+ .filter((e) => !have.includes(e))
98
+ .join(', ')}`;
96
99
 
97
100
  let local: string | null = null;
98
101
  if (root !== null) {
99
- if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.enabled`]) === 'false') local = 'off in this repository';
100
- else if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.command`]) !== null) local = 'own command in this repository';
102
+ if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.enabled`]) === 'false')
103
+ local = 'off in this repository';
104
+ else if (deps.git(['-C', root, 'config', '--local', '--get', `${key}.command`]) !== null)
105
+ local = 'own command in this repository';
101
106
  }
102
107
  return { hook: h, drift, local };
103
108
  }
104
109
 
105
- export function checkPersonalHooks(projectRoot: string | null, deps: PersonalHookDeps = defaultPersonalHookDeps()): CheckResult | null {
110
+ export function checkPersonalHooks(
111
+ projectRoot: string | null,
112
+ deps: PersonalHookDeps = defaultPersonalHookDeps(),
113
+ ): CheckResult | null {
106
114
  const hooks = applicableHooks(deps);
107
115
  if (!hooks) return null;
108
116
  const id = 'personal-hooks';
@@ -132,7 +140,10 @@ export function checkPersonalHooks(projectRoot: string | null, deps: PersonalHoo
132
140
  };
133
141
  }
134
142
 
135
- const missing = missingMachinePaths(hooks.map((h) => h.command), deps.home);
143
+ const missing = missingMachinePaths(
144
+ hooks.map((h) => h.command),
145
+ deps.home,
146
+ );
136
147
  if (missing.length > 0) {
137
148
  return {
138
149
  id,
package/src/predicates.ts CHANGED
@@ -15,8 +15,9 @@ import {
15
15
  import { checkingJobs, ciSystems, ciTaskCalls, mainPushSteps, readWorkflows, shippingJobs, upstream } from './gha.ts';
16
16
  import { detectHookFrameworks } from './hooks.ts';
17
17
  import { covers, expandTasks, isVocabularyTask, projectTaskCalls } from './vocabulary.ts';
18
+ import { at, isStringList, str } from './narrow.ts';
18
19
 
19
- type Args = Record<string, any>;
20
+ export type Args = Record<string, unknown>;
20
21
 
21
22
  /** A fact vdx could not read here — a CI system or a hosting it does not inspect. */
22
23
  export interface Unknown {
@@ -42,7 +43,7 @@ const DEFAULT_TEST_PATTERN =
42
43
 
43
44
  function testPattern(args: Args): RegExp {
44
45
  try {
45
- return new RegExp(String(args.pattern ?? DEFAULT_TEST_PATTERN));
46
+ return new RegExp(str(args.pattern) ?? DEFAULT_TEST_PATTERN);
46
47
  } catch {
47
48
  return new RegExp(DEFAULT_TEST_PATTERN);
48
49
  }
@@ -102,7 +103,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
102
103
  has_file: (args, ctx) => fileExists(ctx, String(args.path)),
103
104
 
104
105
  file_contains: (args, ctx) => {
105
- const paths: string[] = Array.isArray(args.paths) ? args.paths : [String(args.path)];
106
+ const paths = isStringList(args.paths) ? args.paths : [String(args.path)];
106
107
  const expanded = expandPaths(paths, ctx);
107
108
  let re: RegExp;
108
109
  try {
@@ -119,7 +120,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
119
120
 
120
121
  package_present: (args, ctx) => {
121
122
  if (typeof args === 'string') return isPackagePresent(args, 'any', ctx);
122
- return isPackagePresent(String(args.name), String(args.ecosystem ?? 'any'), ctx);
123
+ return isPackagePresent(String(args.name), str(args.ecosystem) ?? 'any', ctx);
123
124
  },
124
125
 
125
126
  tsc_flag: (args, ctx) => {
@@ -132,9 +133,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
132
133
 
133
134
  phpstan_level_at_least: (args, ctx) => {
134
135
  const text =
135
- readText(ctx, 'phpstan.neon') ||
136
- readText(ctx, 'phpstan.dist.neon') ||
137
- readText(ctx, 'phpstan.neon.dist');
136
+ readText(ctx, 'phpstan.neon') || readText(ctx, 'phpstan.dist.neon') || readText(ctx, 'phpstan.neon.dist');
138
137
  if (!text) return false;
139
138
  const m = text.match(/^\s*level:\s*(\d+|max)/m);
140
139
  if (!m || !m[1]) return false;
@@ -145,7 +144,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
145
144
  gh_workflow_blocks_pr: (args, ctx) => {
146
145
  const dir = path.join(ctx.projectRoot, '.github', 'workflows');
147
146
  if (!fs.existsSync(dir)) return false;
148
- const wanted = args.check_name ? String(args.check_name) : null;
147
+ const wanted = str(args.check_name) || null;
149
148
  try {
150
149
  for (const f of fs.readdirSync(dir)) {
151
150
  if (!/\.ya?ml$/.test(f)) continue;
@@ -168,21 +167,19 @@ export const REGISTRY: Record<string, PredicateFn> = {
168
167
  const text = readText(ctx, f);
169
168
  if (text && new RegExp(`^${hook}:`, 'm').test(text)) return true;
170
169
  }
171
- const composer = readJson(ctx, 'composer.json');
172
- if (composer?.extra?.hooks?.[hook]) return true;
173
- return false;
170
+ return Boolean(at(readJson(ctx, 'composer.json'), 'extra', 'hooks', hook));
174
171
  },
175
172
 
176
173
  config_value: (args, ctx) => {
177
- const filePath = String(args.path ?? '');
178
- const jsonpath = String(args.jsonpath ?? '');
174
+ const filePath = str(args.path) ?? '';
175
+ const jsonpath = str(args.jsonpath) ?? '';
179
176
  if (!filePath || !jsonpath) return false;
180
177
  const data = readStructured(ctx, filePath);
181
178
  if (data === null || data === undefined) return false;
182
179
  const value = resolveConfigPath(data, jsonpath);
183
180
 
184
181
  // Operator resolution: explicit `op`, otherwise infer from arg shape.
185
- let op = args.op ? String(args.op) : null;
182
+ let op = str(args.op) || null;
186
183
  if (!op) {
187
184
  if (args.equals !== undefined) op = 'equals';
188
185
  else if (args.gte !== undefined) op = 'gte';
@@ -236,8 +233,7 @@ export const REGISTRY: Record<string, PredicateFn> = {
236
233
  }),
237
234
 
238
235
  /** L4 (library): the testing job runs across a matrix. */
239
- gha_test_matrix: (args, ctx) =>
240
- ciVerdict(ctx, () => [...checkingJobs(ctx, testPattern(args))].some((j) => j.matrix)),
236
+ gha_test_matrix: (args, ctx) => ciVerdict(ctx, () => [...checkingJobs(ctx, testPattern(args))].some((j) => j.matrix)),
241
237
 
242
238
  /** L4 (service): whatever CI ships on a push to main waits, via `needs:`, for a checking job. */
243
239
  gha_ship_needs_checks: (args, ctx) =>
package/src/publish.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import type { AuditResult } from './audit.ts';
5
5
  import { stackForDir, type Ctx } from './facts.ts';
6
+ import { at, dict, errorMessage, stderrOf, str } from './narrow.ts';
6
7
 
7
8
  export type BumpKind = 'patch' | 'minor' | 'major';
8
9
 
@@ -72,7 +73,10 @@ export function bumpSemver(current: string, kind: BumpKind): string {
72
73
 
73
74
  export function compareSemver(a: string, b: string): number {
74
75
  const norm = (s: string) =>
75
- s.replace(/^v/, '').split(/[.\-+]/).map((p) => parseInt(p, 10) || 0);
76
+ s
77
+ .replace(/^v/, '')
78
+ .split(/[.\-+]/)
79
+ .map((p) => parseInt(p, 10) || 0);
76
80
  const pa = norm(a);
77
81
  const pb = norm(b);
78
82
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
@@ -112,7 +116,7 @@ const runGit: GitRunner = (args, cwd) =>
112
116
  * since the last fetch; nothing is fetched.
113
117
  */
114
118
  export function planPush(projectRoot: string, enabled: boolean, git: GitRunner = runGit): PushPlan {
115
- let upstream: string | null = null;
119
+ let upstream: string | null;
116
120
  try {
117
121
  upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], projectRoot).trim() || null;
118
122
  } catch {
@@ -141,10 +145,10 @@ function fetchPublishedVersion(packageName: string, npm: NpmRunner): string | nu
141
145
  try {
142
146
  const out = npm(['view', packageName, 'version']);
143
147
  return out.trim() || null;
144
- } catch (e: any) {
145
- const stderr = String(e?.stderr ?? e?.message ?? '');
148
+ } catch (e) {
149
+ const stderr = stderrOf(e);
146
150
  if (stderr.includes('E404') || stderr.includes('404 Not Found')) return null;
147
- throw new Error(`npm view failed: ${stderr.split('\n')[0] ?? stderr}`);
151
+ throw new Error(`npm view failed: ${stderr.split('\n')[0] ?? stderr}`, { cause: e });
148
152
  }
149
153
  }
150
154
 
@@ -152,12 +156,10 @@ function fetchPublishedVersion(packageName: string, npm: NpmRunner): string | nu
152
156
  * The registry `npm publish` sends this package to, when it is not npm's
153
157
  * default: `publishConfig.registry`, else the scope's registry from npm config.
154
158
  */
155
- export function publishRegistry(
156
- pkg: { name?: string; publishConfig?: { registry?: string } },
157
- npm: NpmRunner,
158
- ): string | null {
159
- if (pkg.publishConfig?.registry) return pkg.publishConfig.registry;
160
- const scope = pkg.name?.match(/^(@[^/]+)\//)?.[1];
159
+ export function publishRegistry(pkg: unknown, npm: NpmRunner): string | null {
160
+ const registry = str(at(pkg, 'publishConfig', 'registry'));
161
+ if (registry) return registry;
162
+ const scope = str(at(pkg, 'name'))?.match(/^(@[^/]+)\//)?.[1];
161
163
  if (!scope) return null;
162
164
  try {
163
165
  const value = npm(['config', 'get', `${scope}:registry`]).trim();
@@ -181,8 +183,8 @@ export function checkNpmAuth(registry: string | null, npm: NpmRunner): Preflight
181
183
  const who = npm(registry ? ['whoami', '--registry', registry] : ['whoami']).trim();
182
184
  if (who) return { name: 'npm-auth', ok: true, message: `OK (logged in to ${where} as ${who})` };
183
185
  return { name: 'npm-auth', ok: false, message: `npm whoami answered nothing for ${where}. Run: ${login}`, fix };
184
- } catch (e: any) {
185
- const stderr = String(e?.stderr ?? e?.message ?? '');
186
+ } catch (e) {
187
+ const stderr = stderrOf(e);
186
188
  if (/E401|ENEEDAUTH/.test(stderr)) {
187
189
  return {
188
190
  name: 'npm-auth',
@@ -249,9 +251,7 @@ export function planPublish(
249
251
  // Resolve effective stack: prefer subpackage stack if primary_subpackage is set
250
252
  // (meta-projects publishing a nested lib, e.g. vdx itself with primary_subpackage=cli).
251
253
  const subpkg = audit.primary_subpackage;
252
- const effectiveStack = subpkg
253
- ? stackForDir(path.join(opts.projectRoot, subpkg))
254
- : ctx.stack;
254
+ const effectiveStack = subpkg ? stackForDir(path.join(opts.projectRoot, subpkg)) : ctx.stack;
255
255
 
256
256
  if (effectiveStack !== 'node') {
257
257
  throw new Error(
@@ -267,9 +267,9 @@ export function planPublish(
267
267
  throw new Error(`vdx publish: package.json not found at ${pkgJsonPath}`);
268
268
  }
269
269
 
270
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
271
- const packageName: string = pkg.name ?? '(unnamed)';
272
- const currentVersion: string = pkg.version ?? '0.0.0';
270
+ const pkg: unknown = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
271
+ const packageName = str(at(pkg, 'name')) ?? '(unnamed)';
272
+ const currentVersion = str(at(pkg, 'version')) ?? '0.0.0';
273
273
  const newVersion = bumpSemver(currentVersion, opts.bump);
274
274
 
275
275
  const preflight: PreflightCheck[] = [];
@@ -311,8 +311,8 @@ export function planPublish(
311
311
  let registryError: string | null = null;
312
312
  try {
313
313
  published = fetchPublishedVersion(packageName, npm);
314
- } catch (e: any) {
315
- registryError = e?.message ?? String(e);
314
+ } catch (e) {
315
+ registryError = errorMessage(e);
316
316
  }
317
317
  if (registryError) {
318
318
  preflight.push({
@@ -415,9 +415,10 @@ export function defaultPublishDeps(): PublishDeps {
415
415
 
416
416
  /** Set `version` in package-lock.json — the root and its own entry under `packages`. */
417
417
  export function bumpLockText(text: string, version: string): string {
418
- const lock = JSON.parse(text);
419
- lock.version = version;
420
- if (lock.packages?.['']) lock.packages[''].version = version;
418
+ const lock = dict(JSON.parse(text)) ?? {};
419
+ lock['version'] = version;
420
+ const self = dict(at(lock, 'packages', ''));
421
+ if (self) self['version'] = version;
421
422
  return JSON.stringify(lock, null, 2) + '\n';
422
423
  }
423
424
 
@@ -501,8 +502,8 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
501
502
  fs.writeFileSync(file, next(text), 'utf8');
502
503
  };
503
504
  rewrite(plan.packageJsonPath, (text) => {
504
- const pkg = JSON.parse(text);
505
- pkg.version = plan.newVersion;
505
+ const pkg = dict(JSON.parse(text)) ?? {};
506
+ pkg['version'] = plan.newVersion;
506
507
  return JSON.stringify(pkg, null, 2) + '\n';
507
508
  });
508
509
  if (plan.lockPath) rewrite(plan.lockPath, (text) => bumpLockText(text, plan.newVersion));
@@ -520,11 +521,13 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
520
521
  for (const s of signals) process.on(s, holdOut);
521
522
  try {
522
523
  deps.run('npm', ['publish'], path.dirname(plan.packageJsonPath));
523
- } catch (e: any) {
524
+ } catch (e) {
524
525
  for (const [file, text] of originals) fs.writeFileSync(file, text, 'utf8');
525
- const what = e?.signal ? `interrupted (${e.signal})` : `failed (exit ${e?.status ?? '?'})`;
526
+ const signal = str(at(e, 'signal'));
527
+ const status = at(e, 'status');
528
+ const what = signal ? `interrupted (${signal})` : `failed (exit ${typeof status === 'number' ? status : '?'})`;
526
529
  deps.log(`✗ npm publish ${what}; reverted ${bumped.map((f) => path.basename(f)).join(' and ')}`);
527
- throw new Error(`npm publish ${e?.signal ? 'interrupted' : 'failed'}`);
530
+ throw new Error(`npm publish ${signal ? 'interrupted' : 'failed'}`, { cause: e });
528
531
  } finally {
529
532
  for (const s of signals) process.off(s, holdOut);
530
533
  }
@@ -543,7 +546,9 @@ export function executePublish(plan: PublishPlan, deps: PublishDeps = defaultPub
543
546
  try {
544
547
  deps.run('git', ['push', '--follow-tags'], plan.projectRoot);
545
548
  } catch {
546
- deps.log(`✗ git push failed — ${plan.packageName}@${plan.newVersion} is published and tagged here; push it: git push --follow-tags`);
549
+ deps.log(
550
+ `✗ git push failed — ${plan.packageName}@${plan.newVersion} is published and tagged here; push it: git push --follow-tags`,
551
+ );
547
552
  throw new Error('git push failed');
548
553
  }
549
554
  deps.log(`✓ pushed to ${push.upstream} with tag v${plan.newVersion}`);
@@ -599,7 +604,9 @@ export function renderPublishPlan(plan: PublishPlan): string {
599
604
  lines.push('## Pipeline (would execute on `vdx publish`)');
600
605
  lines.push('');
601
606
  if (fixable) {
602
- const fixes = plan.preflight.filter((c) => !c.ok && c.fix).map((c) => `\`${c.fix!.cmd} ${c.fix!.args.join(' ')}\``);
607
+ const fixes = plan.preflight
608
+ .filter((c) => !c.ok && c.fix)
609
+ .map((c) => `\`${c.fix!.cmd} ${c.fix!.args.join(' ')}\``);
603
610
  lines.push(`0. ${fixes.join(', ')} — at a terminal, then the pre-flight again`);
604
611
  }
605
612
  lines.push(`1. Set \`version\` in ${files} → \`${plan.newVersion}\``);
package/src/report.ts CHANGED
@@ -116,9 +116,7 @@ export function reportDoctorMarkdown(r: DoctorReport): string {
116
116
  const lines: string[] = [];
117
117
  lines.push('# vdx doctor report');
118
118
  lines.push('');
119
- lines.push(
120
- `**OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`,
121
- );
119
+ lines.push(`**OK**: ${r.ok} **Warning**: ${r.warning} **Missing**: ${r.missing}`);
122
120
  lines.push('');
123
121
  lines.push('| Check | Status | Detail | Remedy |');
124
122
  lines.push('|-------|:------:|--------|--------|');
package/src/rubric.ts CHANGED
@@ -124,8 +124,8 @@ export function parseRubric(text: string, source: string): Rubric {
124
124
  }
125
125
  // нормализуем default_target (если кто-то напишет "L4" со spaces)
126
126
  for (const ax of data.axes) {
127
- ax.default_target = (ax.default_target ?? 'L4') as LevelName;
128
- ax.storage = (ax.storage ?? 'level') as Storage;
127
+ ax.default_target = ax.default_target ?? 'L4';
128
+ ax.storage = ax.storage ?? 'level';
129
129
  }
130
130
  if (compareSchema(schema, '0.3')! >= 0) {
131
131
  const bad = invalidPredicates(data);
package/src/run.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as TOML from 'smol-toml';
4
+ import { errorMessage } from './narrow.ts';
4
5
 
5
6
  export const LIFECYCLE_VERBS = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
6
7
  export type LifecycleVerb = (typeof LIFECYCLE_VERBS)[number];
@@ -13,10 +14,7 @@ export type VerbResolveResult =
13
14
  detail?: string;
14
15
  };
15
16
 
16
- export function resolveLifecycleVerb(
17
- projectRoot: string,
18
- verb: string,
19
- ): VerbResolveResult {
17
+ export function resolveLifecycleVerb(projectRoot: string, verb: string): VerbResolveResult {
20
18
  const miseTomlPath = path.join(projectRoot, 'mise.toml');
21
19
  if (!fs.existsSync(miseTomlPath)) {
22
20
  return { ok: false, reason: 'no-mise-toml' };
@@ -24,8 +22,8 @@ export function resolveLifecycleVerb(
24
22
  let parsed: unknown;
25
23
  try {
26
24
  parsed = TOML.parse(fs.readFileSync(miseTomlPath, 'utf8'));
27
- } catch (e: any) {
28
- return { ok: false, reason: 'parse-error', detail: e?.message ?? String(e) };
25
+ } catch (e) {
26
+ return { ok: false, reason: 'parse-error', detail: errorMessage(e) };
29
27
  }
30
28
  const tasks = (parsed as { tasks?: Record<string, unknown> })?.tasks;
31
29
  if (!tasks || typeof tasks !== 'object' || !(verb in tasks)) {
@@ -42,8 +40,7 @@ export function renderResolveError(
42
40
  switch (res.reason) {
43
41
  case 'no-mise-toml':
44
42
  return (
45
- `vdx: no mise.toml in ${projectRoot}\n` +
46
- `hint: run \`vdx init\` to generate one, or define it manually.\n`
43
+ `vdx: no mise.toml in ${projectRoot}\n` + `hint: run \`vdx init\` to generate one, or define it manually.\n`
47
44
  );
48
45
  case 'parse-error':
49
46
  return `vdx: failed to parse mise.toml: ${res.detail ?? 'unknown error'}\n`;
package/src/scoring.ts CHANGED
@@ -95,15 +95,9 @@ function evalAxisFlags(axis: Axis, ctx: Ctx): LevelName {
95
95
  return achieved;
96
96
  }
97
97
 
98
- export function projectLevel(
99
- results: AxisResult[],
100
- rubric: Rubric,
101
- suppressed: Set<string>,
102
- ): LevelName {
98
+ export function projectLevel(results: AxisResult[], rubric: Rubric, suppressed: Set<string>): LevelName {
103
99
  const supportingThreshold = rubric.scoring.supporting_threshold ?? 0.8;
104
- const visible = results.filter(
105
- (r) => !suppressed.has(r.axis_id) && r.drift_kind !== 'excluded',
106
- );
100
+ const visible = results.filter((r) => !suppressed.has(r.axis_id) && r.drift_kind !== 'excluded');
107
101
  const critical = visible.filter((r) => r.class === 'critical');
108
102
  const supporting = visible.filter((r) => r.class === 'supporting');
109
103
 
package/src/vocabulary.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * predicates (what a hook runs), so both read "the project's task" the same way.
6
6
  */
7
7
  import { listAllTasks, readJson, readToml, type Ctx } from './facts.ts';
8
+ import { at, dict, list } from './narrow.ts';
8
9
 
9
10
  export const LIFECYCLE_VOCABULARY = ['up', 'down', 'build', 'test', 'check', 'fix'] as const;
10
11
 
@@ -39,7 +40,11 @@ export function runnerCalls(command: string): string[] {
39
40
  const toks = tokenize(raw.trim());
40
41
  let i = 0;
41
42
  // `FOO=1 npm test`, `exec npm test`, `npx --no -- vdx test`
42
- while (i < toks.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i]!) || ['exec', 'time', 'command'].includes(toks[i]!))) i++;
43
+ while (
44
+ i < toks.length &&
45
+ (/^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i]!) || ['exec', 'time', 'command'].includes(toks[i]!))
46
+ )
47
+ i++;
43
48
  const head = toks[i];
44
49
  if (!head) continue;
45
50
  const runner = head.split('/').pop()!;
@@ -95,11 +100,9 @@ export function isVocabularyTask(name: string, words: readonly string[] = ['test
95
100
  /** Tasks a task's definition calls: composer `@refs`, `npm run X` inside a script, mise `depends`/`run`. */
96
101
  export function taskChildren(ctx: Ctx, name: string): string[] {
97
102
  const out: string[] = [];
98
- const composer = readJson(ctx, 'composer.json');
99
- const entry = composer?.scripts?.[name];
103
+ const entry = at(readJson(ctx, 'composer.json'), 'scripts', name);
100
104
  if (entry !== undefined) {
101
- const list = Array.isArray(entry) ? entry : [entry];
102
- for (const e of list) {
105
+ for (const e of Array.isArray(entry) ? (entry as unknown[]) : [entry]) {
103
106
  if (typeof e !== 'string') continue;
104
107
  if (e.startsWith('@')) {
105
108
  const [ref, ...rest] = e.slice(1).trim().split(/\s+/);
@@ -110,14 +113,12 @@ export function taskChildren(ctx: Ctx, name: string): string[] {
110
113
  }
111
114
  }
112
115
  }
113
- const pkg = readJson(ctx, 'package.json');
114
- const script = pkg?.scripts?.[name];
116
+ const script = at(readJson(ctx, 'package.json'), 'scripts', name);
115
117
  if (typeof script === 'string') out.push(...runnerCalls(script));
116
- const mise = readToml(ctx, 'mise.toml');
117
- const task = mise?.tasks?.[name];
118
- if (task && typeof task === 'object') {
119
- if (Array.isArray(task.depends)) out.push(...task.depends.filter((d: unknown): d is string => typeof d === 'string'));
120
- const runs = Array.isArray(task.run) ? task.run : [task.run];
118
+ const task = dict(at(readToml(ctx, 'mise.toml'), 'tasks', name));
119
+ if (task) {
120
+ out.push(...list(task['depends']).filter((d): d is string => typeof d === 'string'));
121
+ const runs = Array.isArray(task['run']) ? (task['run'] as unknown[]) : [task['run']];
121
122
  for (const r of runs) if (typeof r === 'string') out.push(...runnerCalls(r));
122
123
  }
123
124
  const tasks = listAllTasks(ctx);