@vodmal/vdx-cli 0.31.3 → 0.32.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vodmal/vdx-cli",
3
- "version": "0.31.3",
3
+ "version": "0.32.0",
4
4
  "description": "vdx — unified lifecycle interface (up/down/build/test/check/fix) + versioned maturity audit with drift detection. Pairs with the vdx Claude Code plugin.",
5
5
  "keywords": [
6
6
  "vdx",
package/src/audit.ts CHANGED
@@ -137,7 +137,7 @@ export function audit(
137
137
  });
138
138
  continue;
139
139
  }
140
- const { achieved, not_required, unknown } = evalAxisDetailed(axis, evalCtx);
140
+ const { achieved, not_required, unknown, not_met } = evalAxisDetailed(axis, evalCtx);
141
141
  const ov = overrideByAxis.get(axis.id);
142
142
  const target = (ov?.target as LevelName) ?? axis.default_target;
143
143
  const drift: 'aligned' | 'gap' | 'over' =
@@ -156,6 +156,7 @@ export function audit(
156
156
  if (ov?.target) entry.override_target = ov.target as LevelName;
157
157
  if (not_required) entry.not_required = not_required;
158
158
  if (unknown) entry.unknown = unknown;
159
+ if (not_met) entry.not_met = not_met;
159
160
  if (axis.clone_check) {
160
161
  const c = cloneCheck(axis.clone_check, ctx.projectRoot);
161
162
  if (c && c.status !== 'ok') entry.note = c.remedy ? `${c.message} → ${c.remedy}` : c.message;
package/src/evaluator.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Predicate } from './rubric.ts';
2
2
  import type { Ctx } from './facts.ts';
3
- import { REGISTRY, isUnknown, type Args, type Unknown, type Verdict } from './predicates.ts';
3
+ import { REGISTRY, isFalse, isNo, isUnknown, type Args, type No, type Unknown, type Verdict } from './predicates.ts';
4
4
  import { at, isDict, list } from './narrow.ts';
5
5
 
6
6
  export { isUnknown, type Verdict };
@@ -25,6 +25,11 @@ function firstUnknown(vs: Verdict[]): Unknown | undefined {
25
25
  return vs.find(isUnknown);
26
26
  }
27
27
 
28
+ /** Not met, with the first reason a branch gave, if one did. */
29
+ function no(vs: Verdict[]): false | No {
30
+ return vs.find(isNo) ?? false;
31
+ }
32
+
28
33
  /**
29
34
  * Three-valued: true, false, or unknown with the reason the fact could not be
30
35
  * read (a CI system or a hosting vdx does not inspect). Unknown never turns
@@ -40,13 +45,13 @@ export function evalVerdict(p: Predicate, ctx: Ctx): Verdict {
40
45
 
41
46
  if ('all_of' in obj) {
42
47
  const vs = list(obj['all_of']).map((q) => evalVerdict(q, ctx));
43
- if (vs.some((v) => v === false)) return false;
48
+ if (vs.some(isFalse)) return no(vs);
44
49
  return firstUnknown(vs) ?? true;
45
50
  }
46
51
  if ('any_of' in obj) {
47
52
  const vs = list(obj['any_of']).map((q) => evalVerdict(q, ctx));
48
53
  if (vs.some((v) => v === true)) return true;
49
- return firstUnknown(vs) ?? false;
54
+ return firstUnknown(vs) ?? no(vs);
50
55
  }
51
56
  if ('at_least_n_of' in obj) {
52
57
  const { n, predicates } = obj['at_least_n_of'] as { n: number; predicates: Predicate[] };
@@ -54,12 +59,12 @@ export function evalVerdict(p: Predicate, ctx: Ctx): Verdict {
54
59
  const matched = vs.filter((v) => v === true).length;
55
60
  if (matched >= n) return true;
56
61
  const open = vs.filter(isUnknown).length;
57
- if (matched + open < n) return false;
62
+ if (matched + open < n) return no(vs);
58
63
  return firstUnknown(vs)!;
59
64
  }
60
65
  if ('not' in obj) {
61
66
  const v = evalVerdict(obj['not'], ctx);
62
- return isUnknown(v) ? v : !v;
67
+ return isUnknown(v) ? v : v !== true;
63
68
  }
64
69
  if ('fn' in obj) {
65
70
  const name = String(obj['fn']);
package/src/hosting.ts CHANGED
@@ -115,13 +115,25 @@ function judge(io: HostingIo, repo: string, token: string | null): Verdict {
115
115
  const refusedBranch = refusal(b, repo, token);
116
116
  if (refusedBranch) return refusedBranch;
117
117
  const classic = at(b.body, 'protection', 'required_status_checks');
118
- if (at(classic, 'enforcement_level') === 'everyone' && checkCount(classic) > 0) return true;
118
+ const level = at(classic, 'enforcement_level');
119
+ const named = checkCount(classic) > 0;
120
+ if (level === 'everyone' && named) return true;
121
+ // Why not, for the report (DL #11): what classic protection lacks.
122
+ const classicWhy =
123
+ at(b.body, 'protected') !== true
124
+ ? `${branch} is not protected`
125
+ : level === 'non_admins' && named
126
+ ? `${branch} requires checks of everyone but admins — and an agent pushes as its owner, an admin (DL #3)`
127
+ : level === 'everyone'
128
+ ? `${branch} is protected, but names no check it requires`
129
+ : `${branch} is protected without required checks`;
119
130
 
120
131
  // Rulesets: a rule that requires checks, in a ruleset nobody bypasses.
121
132
  const rules = io.get(`/repos/${repo}/rules/branches/${ref}`, token);
122
133
  const refusedRules = refusal(rules, repo, token);
123
134
  if (refusedRules) return refusedRules;
124
135
  let hidden = false;
136
+ let bypassers = 0;
125
137
  for (const rule of list(rules.body)) {
126
138
  if (at(rule, 'type') !== 'required_status_checks') continue;
127
139
  if (list(at(rule, 'parameters', 'required_status_checks')).length === 0) continue;
@@ -130,13 +142,19 @@ function judge(io: HostingIo, repo: string, token: string | null): Verdict {
130
142
  const bypass = ruleset?.status === 200 ? at(ruleset.body, 'bypass_actors') : undefined;
131
143
  if (!Array.isArray(bypass)) hidden = true;
132
144
  else if (bypass.length === 0) return true;
145
+ else bypassers = Math.max(bypassers, bypass.length);
133
146
  }
134
147
  if (hidden) {
135
148
  return {
136
149
  unknown: `a ruleset requires checks on ${branch}, but GitHub shows who bypasses it only to ${token ? 'a repository admin' : 'a token; gh auth login, or GH_TOKEN'}`,
137
150
  };
138
151
  }
139
- return false;
152
+ return {
153
+ no:
154
+ bypassers > 0
155
+ ? `${classicWhy}; a ruleset requires checks, but ${bypassers} actor(s) may bypass it`
156
+ : `${classicWhy}, and no ruleset requires checks`,
157
+ };
140
158
  }
141
159
 
142
160
  /**
package/src/predicates.ts CHANGED
@@ -24,11 +24,24 @@ export type Args = Record<string, unknown>;
24
24
  export interface Unknown {
25
25
  unknown: string;
26
26
  }
27
- export type Verdict = boolean | Unknown;
27
+ /** Not met, and why — from a predicate that knows (DL #11 of docs/tasks/hosting-extension). */
28
+ export interface No {
29
+ no: string;
30
+ }
31
+ export type Verdict = boolean | Unknown | No;
28
32
  type PredicateFn = (args: Args, ctx: Ctx) => Verdict;
29
33
 
30
34
  export function isUnknown(v: Verdict): v is Unknown {
31
- return typeof v === 'object' && v !== null;
35
+ return typeof v === 'object' && v !== null && 'unknown' in v;
36
+ }
37
+
38
+ export function isNo(v: Verdict): v is No {
39
+ return typeof v === 'object' && v !== null && 'no' in v;
40
+ }
41
+
42
+ /** Not met: `false`, or `false` with its reason. */
43
+ export function isFalse(v: Verdict): v is false | No {
44
+ return v === false || isNo(v);
32
45
  }
33
46
 
34
47
  /**
package/src/report.ts CHANGED
@@ -47,6 +47,14 @@ export function reportMarkdown(r: AuditResult): string {
47
47
  lines.push(`| \`${a.axis_id}\` | ${cls} | ${achievedCell(a)} | ${a.target} | ${drift} |`);
48
48
  }
49
49
 
50
+ const notMet = r.per_axis.filter((a) => a.not_met && !a.suppressed && a.drift_kind !== 'excluded');
51
+ if (notMet.length > 0) {
52
+ lines.push('');
53
+ lines.push(`## Not met`);
54
+ lines.push('');
55
+ for (const a of notMet) lines.push(`- \`${a.axis_id}\` ${a.not_met!.level}: ${a.not_met!.reason}`);
56
+ }
57
+
50
58
  const unknown = r.per_axis.filter((a) => a.unknown);
51
59
  if (unknown.length > 0) {
52
60
  lines.push('');
package/src/scoring.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Axis, LevelName, Rubric } from './rubric.ts';
2
2
  import type { Ctx } from './facts.ts';
3
3
  import { evalPredicate, evalVerdict, isUnknown } from './evaluator.ts';
4
+ import { isNo } from './predicates.ts';
4
5
 
5
6
  const LEVELS: LevelName[] = ['L1', 'L2', 'L3', 'L4'];
6
7
 
@@ -18,6 +19,8 @@ export interface AxisResult {
18
19
  not_required?: LevelName[];
19
20
  /** The level whose requirement could not be checked, and why; the ascent stopped there. */
20
21
  unknown?: { level: LevelName; reason: string };
22
+ /** The level whose requirement is not met, and why — when the predicate knows. */
23
+ not_met?: { level: LevelName; reason: string };
21
24
  /** A clone check printed next to the axis; does not change the level. */
22
25
  note?: string;
23
26
  }
@@ -26,6 +29,7 @@ export interface AxisEvaluation {
26
29
  achieved: LevelName;
27
30
  not_required?: LevelName[];
28
31
  unknown?: { level: LevelName; reason: string };
32
+ not_met?: { level: LevelName; reason: string };
29
33
  }
30
34
 
31
35
  export function evalAxis(axis: Axis, ctx: Ctx): LevelName {
@@ -41,6 +45,7 @@ function evalAxisLevels(axis: Axis, ctx: Ctx): AxisEvaluation {
41
45
  let achieved: LevelName = 'L0';
42
46
  const notRequired: LevelName[] = [];
43
47
  let unknown: AxisEvaluation['unknown'];
48
+ let notMet: AxisEvaluation['not_met'];
44
49
  for (const L of LEVELS) {
45
50
  const lvl = axis.levels?.[L];
46
51
  if (!lvl) break;
@@ -55,6 +60,7 @@ function evalAxisLevels(axis: Axis, ctx: Ctx): AxisEvaluation {
55
60
  } else {
56
61
  // delta-style: discontinuity → stop ascent; unknown is "not met", named
57
62
  if (isUnknown(v)) unknown = { level: L, reason: v.unknown };
63
+ else if (isNo(v)) notMet = { level: L, reason: v.no };
58
64
  break;
59
65
  }
60
66
  }
@@ -62,6 +68,7 @@ function evalAxisLevels(axis: Axis, ctx: Ctx): AxisEvaluation {
62
68
  achieved,
63
69
  ...(notRequired.length > 0 ? { not_required: notRequired } : {}),
64
70
  ...(unknown ? { unknown } : {}),
71
+ ...(notMet ? { not_met: notMet } : {}),
65
72
  };
66
73
  }
67
74