delimit-cli 4.6.1 → 4.6.2

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.
@@ -26,17 +26,42 @@ class SemverBump(Enum):
26
26
 
27
27
  # ── Change-type buckets ──────────────────────────────────────────────
28
28
 
29
+ # LED-1600: this bucket MUST stay aligned with diff_engine_v2.Change.is_breaking.
30
+ # Previously it listed only 10 of the engine's breaking types, so a
31
+ # PARAM_REQUIRED_CHANGED, RESPONSE_TYPE_CHANGED, SECURITY_REMOVED,
32
+ # SECURITY_SCOPE_REMOVED, MAX_LENGTH_DECREASED, MIN_LENGTH_INCREASED or
33
+ # PARAM_TYPE_CHANGED — all of which the engine flags `is_breaking=True` — was
34
+ # silently classified MINOR instead of MAJOR. That is the exact "silent semver
35
+ # minor->major leak" this LED closes: a breaking change slipping through as
36
+ # non-breaking is the worst failure mode for a merge gate. The context-
37
+ # sensitive field types (FIELD_REMOVED / REQUIRED_FIELD_ADDED /
38
+ # FIELD_REQUIREMENT_RELAXED) are NOT listed here as unconditional — their
39
+ # breaking-ness depends on request/response direction and is read per-Change
40
+ # via `is_breaking` in classify() below.
29
41
  BREAKING_TYPES = frozenset({
30
42
  ChangeType.ENDPOINT_REMOVED,
31
43
  ChangeType.METHOD_REMOVED,
32
44
  ChangeType.REQUIRED_PARAM_ADDED,
33
45
  ChangeType.PARAM_REMOVED,
34
46
  ChangeType.RESPONSE_REMOVED,
35
- ChangeType.REQUIRED_FIELD_ADDED,
36
- ChangeType.FIELD_REMOVED,
37
47
  ChangeType.TYPE_CHANGED,
38
48
  ChangeType.FORMAT_CHANGED,
39
49
  ChangeType.ENUM_VALUE_REMOVED,
50
+ ChangeType.PARAM_TYPE_CHANGED,
51
+ ChangeType.PARAM_REQUIRED_CHANGED,
52
+ ChangeType.RESPONSE_TYPE_CHANGED,
53
+ ChangeType.SECURITY_REMOVED,
54
+ ChangeType.SECURITY_SCOPE_REMOVED,
55
+ ChangeType.MAX_LENGTH_DECREASED,
56
+ ChangeType.MIN_LENGTH_INCREASED,
57
+ })
58
+
59
+ # Context-sensitive types whose breaking-ness is decided per-Change by the
60
+ # engine's direction-aware is_breaking, not by membership in BREAKING_TYPES.
61
+ CONTEXT_SENSITIVE_TYPES = frozenset({
62
+ ChangeType.FIELD_REMOVED,
63
+ ChangeType.REQUIRED_FIELD_ADDED,
64
+ ChangeType.FIELD_REQUIREMENT_RELAXED,
40
65
  })
41
66
 
42
67
  ADDITIVE_TYPES = frozenset({
@@ -65,7 +90,14 @@ def classify(changes: List[Change]) -> SemverBump:
65
90
  has_additive = False
66
91
 
67
92
  for change in changes:
68
- if change.type in BREAKING_TYPES:
93
+ # LED-1600: the engine's is_breaking is the single source of truth.
94
+ # For context-sensitive types it already encodes request/response
95
+ # direction; for everything else it matches BREAKING_TYPES membership.
96
+ # Using it here closes the gap where a breaking change (e.g. an
97
+ # optional->required param, or a response field removal) was bucketed
98
+ # MINOR. `getattr` keeps the function working for duck-typed Change-
99
+ # likes that may not carry the property.
100
+ if _is_breaking(change):
69
101
  has_breaking = True
70
102
  break # short-circuit — can't go higher than MAJOR
71
103
  if change.type in ADDITIVE_TYPES:
@@ -78,6 +110,20 @@ def classify(changes: List[Change]) -> SemverBump:
78
110
  return SemverBump.PATCH
79
111
 
80
112
 
113
+ def _is_breaking(change) -> bool:
114
+ """Authoritative breaking check for a Change.
115
+
116
+ Prefers the engine's direction-aware ``is_breaking`` property. Falls back
117
+ to BREAKING_TYPES membership for objects that don't expose it (e.g. test
118
+ doubles). Context-sensitive types are only breaking when ``is_breaking``
119
+ says so; never inferred from the type alone.
120
+ """
121
+ val = getattr(change, "is_breaking", None)
122
+ if isinstance(val, bool):
123
+ return val
124
+ return getattr(change, "type", None) in BREAKING_TYPES
125
+
126
+
81
127
  def classify_detailed(changes: List[Change]) -> Dict[str, Any]:
82
128
  """Return a detailed classification with per-category breakdowns.
83
129
 
@@ -85,9 +131,9 @@ def classify_detailed(changes: List[Change]) -> Dict[str, Any]:
85
131
  """
86
132
  bump = classify(changes)
87
133
 
88
- breaking = [c for c in changes if c.type in BREAKING_TYPES]
89
- additive = [c for c in changes if c.type in ADDITIVE_TYPES]
90
- patch = [c for c in changes if c.type in PATCH_TYPES]
134
+ breaking = [c for c in changes if _is_breaking(c)]
135
+ additive = [c for c in changes if not _is_breaking(c) and c.type in ADDITIVE_TYPES]
136
+ patch = [c for c in changes if not _is_breaking(c) and c.type in PATCH_TYPES]
91
137
 
92
138
  return {
93
139
  "bump": bump.value,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "4.6.1",
4
+ "version": "4.6.2",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [