delimit-cli 4.5.1 → 4.5.3

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +15 -5
  3. package/bin/delimit-cli.js +109 -24
  4. package/gateway/ai/content_engine.py +3 -4
  5. package/gateway/ai/inbox_classifier.py +215 -0
  6. package/gateway/ai/integrations/opensage_wrapper.py +4 -1
  7. package/gateway/ai/ledger_manager.py +218 -38
  8. package/gateway/ai/license.py +26 -0
  9. package/gateway/ai/notify.py +68 -3
  10. package/gateway/ai/reddit_proxy.py +93 -15
  11. package/gateway/ai/reddit_scanner.py +36 -18
  12. package/gateway/ai/remote_resolve.py +422 -0
  13. package/gateway/ai/server.py +301 -117
  14. package/gateway/ai/social_capability/__init__.py +6 -0
  15. package/gateway/ai/social_capability/capability_validator.py +367 -0
  16. package/gateway/ai/social_capability/current_capabilities.yaml +95 -0
  17. package/gateway/ai/social_capability/fit_floor.py +360 -0
  18. package/gateway/ai/social_queue.py +307 -0
  19. package/gateway/ai/supabase_sync.py +14 -2
  20. package/gateway/ai/swarm.py +29 -11
  21. package/gateway/ai/tui.py +6 -2
  22. package/gateway/ai/vendor_news/__init__.py +14 -0
  23. package/gateway/ai/vendor_news/drafter.py +562 -0
  24. package/gateway/ai/vendor_news/sensor.py +509 -0
  25. package/gateway/ai/vendor_news/watchlist.yaml +71 -0
  26. package/gateway/ai/x_ranker.py +417 -0
  27. package/lib/attest-mcp.js +487 -0
  28. package/lib/attest-telemetry.js +48 -0
  29. package/lib/delimit-home.js +35 -0
  30. package/lib/delimit-template.js +14 -0
  31. package/package.json +25 -3
  32. package/scripts/postinstall.js +89 -40
  33. package/adapters/codex-security.js +0 -64
  34. package/adapters/codex-skill.js +0 -78
  35. package/gateway/ai/content_grounding/__init__.py +0 -98
  36. package/gateway/ai/content_grounding/build.py +0 -350
  37. package/gateway/ai/content_grounding/consume.py +0 -280
  38. package/gateway/ai/content_grounding/features.py +0 -218
  39. package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +0 -9
  40. package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +0 -9
  41. package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +0 -17
  42. package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +0 -17
  43. package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +0 -17
  44. package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +0 -18
  45. package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +0 -18
  46. package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +0 -23
  47. package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +0 -16
  48. package/gateway/ai/content_grounding/schemas/claim.schema.json +0 -40
  49. package/gateway/ai/content_grounding/schemas/event.schema.json +0 -23
  50. package/gateway/ai/content_grounding/schemas.py +0 -276
  51. package/gateway/ai/content_grounding/telemetry.py +0 -221
  52. package/gateway/ai/inbox_drafts/__init__.py +0 -61
  53. package/gateway/ai/inbox_drafts/registry.py +0 -412
  54. package/gateway/ai/inbox_drafts/schema.py +0 -374
  55. package/gateway/ai/inbox_executor.py +0 -565
@@ -0,0 +1,367 @@
1
+ """Capability-currency validator (LED-216 Phase 1, tightened LED-1240).
2
+
3
+ Validates social drafts against ``current_capabilities.yaml``. The validator
4
+ hard-fails any draft that:
5
+
6
+ * names a banned surface (literal or regex), OR
7
+ * mentions the Delimit product without anchoring to ground truth (no
8
+ canonical phrase AND no matched_claim from allowed_claims), OR
9
+ * mentions the Delimit product on a long-form platform (reddit, hn, devto,
10
+ etc.) without a delimit.ai URL anchor.
11
+
12
+ The 2026-05-05 tightening (LED-1240) was a response to founder feedback that
13
+ generic-claim drafts were leaking through with only a soft warning. The fix
14
+ reclassifies "mentions product, names no specific claim, links no artifact"
15
+ as a hard-fail — drafts that talk about Delimit must either (a) name a
16
+ mechanism from allowed_claims, or (b) link to a delimit.ai resource (the
17
+ methodology, a worked-example report, the attestation replay UI, etc.).
18
+
19
+ Twitter (and other ≤280-char platforms) gets a deliberate carve-out: a URL
20
+ won't always fit, so for ``platform="twitter"`` the URL requirement is
21
+ relaxed to "draft must contain at least one matched_claim". Reddit, HN,
22
+ devto, and any unspecified platform keep the URL requirement.
23
+
24
+ Wiring: ``ai.social.save_draft`` calls :func:`validate_draft` after the
25
+ existing tone/length checks but BEFORE the file is appended. On hard-fail
26
+ the draft's ``quality`` is overridden to ``"rejected_capability_drift"`` and
27
+ the entry MUST NOT be enqueued for notify. On warn the quality becomes
28
+ ``"ready_with_warnings"``. Both outcomes are logged to
29
+ ``~/.delimit/social_drafts_validation.jsonl`` for audit / replay.
30
+
31
+ Governance: the underlying ``current_capabilities.yaml`` is gated by the
32
+ LED-1037 banned-vocabulary contract — edits require a unanimous
33
+ ``delimit_deliberate`` verdict. This module only consumes the file; it does
34
+ not mutate it.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import logging
41
+ import re
42
+ from datetime import datetime, timezone
43
+ from pathlib import Path
44
+ from typing import Any, Dict, List, Optional
45
+
46
+ try:
47
+ import yaml as _yaml
48
+ except ImportError: # pragma: no cover
49
+ _yaml = None # type: ignore[assignment]
50
+
51
+ logger = logging.getLogger("delimit.ai.social_capability.capability_validator")
52
+
53
+ # Default capabilities path — co-located with this module so the file ships
54
+ # (or doesn't) with the same npm exclusion rule as the rest of
55
+ # ai/social_capability/. The package name is `social_capability` rather
56
+ # than `social` because `ai/social.py` already exists as a top-level
57
+ # module and Python forbids a package with the same name as a sibling
58
+ # module.
59
+ DEFAULT_CAPABILITIES_PATH = Path(__file__).parent / "current_capabilities.yaml"
60
+
61
+ # Audit log for every validator decision (pass / warn / fail). One JSONL
62
+ # line per draft. Used by self-repair to detect over- or under-firing.
63
+ VALIDATION_LOG = Path.home() / ".delimit" / "social_drafts_validation.jsonl"
64
+
65
+ # ── product-mention detection ────────────────────────────────────────
66
+
67
+ # Case-insensitive match for "Delimit" as a standalone word OR the
68
+ # @delimit_ai twitter handle. Avoid false positives on hostnames like
69
+ # "delimit.ai/methodology/..." by NOT requiring word boundaries — the URL
70
+ # itself is a product reference, which is exactly the case where we want
71
+ # to nudge the canonical phrase.
72
+ _PRODUCT_MENTION_RE = re.compile(
73
+ r"(?:\bDelimit\b|@delimit_ai|delimit\.ai)",
74
+ re.IGNORECASE,
75
+ )
76
+
77
+ # Match a delimit.ai URL anchor — either a recognized known path, or a bare
78
+ # delimit.ai reference. Used for the LED-1240 long-form URL-grounding gate.
79
+ # The known-path list is recognised by the validator and any other
80
+ # delimit.ai/<segment> URL also counts as grounding (we only need to know
81
+ # the draft is anchored to a real artifact on the public site).
82
+ _DELIMIT_URL_RE = re.compile(
83
+ r"\bdelimit\.ai(?:/(?:methodology|reports|att|docs|trust|pricing)\b|/\S+|\b)",
84
+ re.IGNORECASE,
85
+ )
86
+
87
+ # Platforms that get the short-form URL relaxation. Anything not in this set
88
+ # is treated as long-form and must carry a delimit.ai URL anchor when the
89
+ # draft mentions Delimit by name.
90
+ _SHORT_FORM_PLATFORMS = {"twitter", "x"}
91
+
92
+
93
+ def _load_capabilities(path: Path) -> Dict[str, Any]:
94
+ """Load and parse the capabilities YAML.
95
+
96
+ Returns an empty config (no banned, no claims) if the file is missing
97
+ or YAML is unavailable, so the validator fails open in degraded
98
+ environments rather than crashing draft generation. The fail-open is
99
+ intentional: a missing capability file should NOT block legitimate
100
+ drafts; the worst outcome is the draft passes through without
101
+ capability-currency enforcement, which we'll catch via the audit log.
102
+ """
103
+ if _yaml is None:
104
+ logger.warning(
105
+ "capability_validator: PyYAML not available; validator is a no-op"
106
+ )
107
+ return {}
108
+ if not path.exists():
109
+ logger.warning(
110
+ "capability_validator: capabilities file missing at %s; "
111
+ "validator is a no-op",
112
+ path,
113
+ )
114
+ return {}
115
+ try:
116
+ with open(path, "r", encoding="utf-8") as f:
117
+ data = _yaml.safe_load(f) or {}
118
+ except Exception as exc: # pragma: no cover — corrupt yaml
119
+ logger.error(
120
+ "capability_validator: failed to load %s: %s", path, exc
121
+ )
122
+ return {}
123
+ if not isinstance(data, dict):
124
+ logger.error(
125
+ "capability_validator: %s did not parse to a mapping", path
126
+ )
127
+ return {}
128
+ return data
129
+
130
+
131
+ def _matched_claims(text: str, claims: List[Dict[str, Any]]) -> List[str]:
132
+ """Return IDs of allowed_claims whose surface_name appears in text."""
133
+ out: List[str] = []
134
+ lower = text.lower()
135
+ for claim in claims:
136
+ if not isinstance(claim, dict):
137
+ continue
138
+ surface = (claim.get("surface_name") or "").strip()
139
+ cid = (claim.get("id") or "").strip()
140
+ if not surface or not cid:
141
+ continue
142
+ if surface.lower() in lower:
143
+ out.append(cid)
144
+ return out
145
+
146
+
147
+ def _matched_banned_literal(
148
+ text: str, banned: List[str]
149
+ ) -> List[str]:
150
+ """Return banned surface literals (case-insensitive) found in text."""
151
+ lower = text.lower()
152
+ return [b for b in banned if isinstance(b, str) and b and b.lower() in lower]
153
+
154
+
155
+ def _matched_banned_pattern(
156
+ text: str, patterns: List[str]
157
+ ) -> List[str]:
158
+ """Return banned regex patterns that match somewhere in text.
159
+
160
+ Compilation errors on individual patterns are logged and the pattern is
161
+ skipped so one bad regex does not break the whole validator.
162
+ """
163
+ out: List[str] = []
164
+ for pat in patterns:
165
+ if not isinstance(pat, str) or not pat:
166
+ continue
167
+ try:
168
+ if re.search(pat, text, flags=re.IGNORECASE):
169
+ out.append(pat)
170
+ except re.error as exc:
171
+ logger.warning(
172
+ "capability_validator: bad banned_surface_pattern %r: %s",
173
+ pat, exc,
174
+ )
175
+ continue
176
+ return out
177
+
178
+
179
+ def _has_canonical_phrase(text: str, phrases: List[str]) -> bool:
180
+ lower = text.lower()
181
+ for p in phrases:
182
+ if isinstance(p, str) and p and p.lower() in lower:
183
+ return True
184
+ return False
185
+
186
+
187
+ def _mentions_product(text: str) -> bool:
188
+ return bool(_PRODUCT_MENTION_RE.search(text or ""))
189
+
190
+
191
+ def _has_delimit_url(text: str) -> bool:
192
+ """True iff the draft contains any delimit.ai URL anchor.
193
+
194
+ Matches both the curated path list (delimit.ai/methodology, /reports,
195
+ /att, /docs, /trust, /pricing) and any other delimit.ai/<path> URL.
196
+ Bare 'delimit.ai' is also accepted — the goal is grounding, not exact
197
+ path validation.
198
+ """
199
+ return bool(_DELIMIT_URL_RE.search(text or ""))
200
+
201
+
202
+ def _append_audit(record: Dict[str, Any]) -> None:
203
+ """Append a validation decision to the audit log. Best-effort."""
204
+ try:
205
+ VALIDATION_LOG.parent.mkdir(parents=True, exist_ok=True)
206
+ with open(VALIDATION_LOG, "a", encoding="utf-8") as f:
207
+ f.write(json.dumps(record) + "\n")
208
+ except Exception as exc: # pragma: no cover — disk full, etc.
209
+ logger.debug("capability_validator: audit write failed: %s", exc)
210
+
211
+
212
+ def validate_draft(
213
+ text: str,
214
+ capabilities_path: Optional[Path] = None,
215
+ *,
216
+ platform: str = "",
217
+ audit_meta: Optional[Dict[str, Any]] = None,
218
+ log: bool = True,
219
+ ) -> Dict[str, Any]:
220
+ """Validate a social draft against ``current_capabilities.yaml``.
221
+
222
+ Args:
223
+ text: The candidate draft text.
224
+ capabilities_path: Override path to the capabilities YAML. Defaults
225
+ to the bundled ``current_capabilities.yaml`` next to this
226
+ module.
227
+ platform: Platform string ("twitter", "reddit", "hn", "devto", ...).
228
+ Twitter / X get a short-form carve-out: drafts that mention the
229
+ product without a delimit.ai URL still pass IF they cite a
230
+ specific allowed_claim. Long-form platforms must carry both a
231
+ specific claim AND a URL anchor when they mention the product.
232
+ Empty string defaults to long-form behavior (strictest gate).
233
+ audit_meta: Optional fields to embed in the audit log entry
234
+ (e.g. ``{"draft_id": ..., "platform": ...}``). Never required
235
+ for validation logic.
236
+ log: When False, skip the audit log write. Used by tests.
237
+
238
+ Returns:
239
+ Dict with:
240
+ - ``ok`` (bool): False iff a banned surface (literal or pattern)
241
+ appeared, OR the draft mentions the product but anchors to no
242
+ ground truth (LED-1240).
243
+ - ``errors`` (list[str]): Hard-fail reasons.
244
+ - ``warnings`` (list[str]): Soft-fail reasons.
245
+ - ``matched_claims`` (list[str]): IDs of allowed_claims found.
246
+ - ``matched_banned`` (list[str]): Banned surfaces / patterns hit.
247
+ - ``mentions_product`` (bool): Whether the draft references
248
+ Delimit by name or handle.
249
+ - ``has_canonical_phrase`` (bool): Whether at least one canonical
250
+ phrase appears.
251
+ - ``has_delimit_url`` (bool): Whether the draft contains a
252
+ delimit.ai URL anchor.
253
+ - ``platform`` (str): The normalized platform string used for the
254
+ short-form carve-out decision.
255
+ """
256
+ text = text or ""
257
+ path = capabilities_path or DEFAULT_CAPABILITIES_PATH
258
+ cfg = _load_capabilities(path)
259
+
260
+ allowed_claims = cfg.get("allowed_claims") or []
261
+ banned_literals = cfg.get("banned_surfaces") or []
262
+ banned_patterns = cfg.get("banned_surface_patterns") or []
263
+ required_phrases = cfg.get("required_canonical_phrases") or []
264
+
265
+ matched_claims = _matched_claims(text, allowed_claims)
266
+ matched_literal = _matched_banned_literal(text, banned_literals)
267
+ matched_patterns = _matched_banned_pattern(text, banned_patterns)
268
+ matched_banned = matched_literal + matched_patterns
269
+
270
+ errors: List[str] = []
271
+ for hit in matched_literal:
272
+ errors.append(
273
+ f"banned surface literal: {hit!r} — see ai/social_capability/current_capabilities.yaml"
274
+ )
275
+ for pat in matched_patterns:
276
+ errors.append(
277
+ f"banned surface pattern matched: {pat!r} (tool-count hero "
278
+ f"language is forbidden in social copy)"
279
+ )
280
+
281
+ mentions_product = _mentions_product(text)
282
+ has_canonical = _has_canonical_phrase(text, required_phrases)
283
+ has_url = _has_delimit_url(text)
284
+ platform_norm = (platform or "").strip().lower()
285
+ is_short_form = platform_norm in _SHORT_FORM_PLATFORMS
286
+
287
+ warnings: List[str] = []
288
+
289
+ # ── LED-1240 grounding gate ──────────────────────────────────────
290
+ # Tightened 2026-05-05: a draft that mentions Delimit but anchors to
291
+ # no ground truth is now a hard-fail, not a soft warning. The two
292
+ # rules are:
293
+ # (a) Product mention + no canonical phrase + no matched_claim
294
+ # ⇒ hard-fail on every platform. The draft is naming the
295
+ # product without grounding to anything in the allow list.
296
+ # (b) Product mention on a long-form platform without a delimit.ai
297
+ # URL anchor ⇒ hard-fail. Twitter / X are exempt because a URL
298
+ # won't always fit in 280 chars; for them, a matched_claim is
299
+ # sufficient grounding.
300
+ # The carve-out preserves the existing twitter draft contract while
301
+ # raising the floor for reddit / HN / devto / etc.
302
+ if mentions_product and required_phrases:
303
+ if not has_canonical and not matched_claims:
304
+ errors.append(
305
+ "draft mentions Delimit but cites no canonical phrase and no "
306
+ "specific allowed_claim. Anchor the claim to a mechanism in "
307
+ "current_capabilities.yaml or rewrite without naming the "
308
+ "product (LED-1240)."
309
+ )
310
+ elif not is_short_form and not has_url:
311
+ errors.append(
312
+ "draft mentions Delimit on a long-form platform "
313
+ f"(platform={platform_norm or 'unspecified'}) without a "
314
+ "delimit.ai URL anchor. Cite a specific artifact "
315
+ "(delimit.ai/methodology, /reports, /att, ...) so the claim "
316
+ "is verifiable (LED-1240)."
317
+ )
318
+ elif not has_canonical:
319
+ # Has a matched_claim but still missing a canonical phrase —
320
+ # downgrade to a warning (existing soft-fail behavior). The
321
+ # claim is grounded; the framing isn't on-message yet.
322
+ warnings.append(
323
+ "draft mentions Delimit and cites a specific claim but does "
324
+ "not include a canonical phrase (merge gate / signed, "
325
+ "replayable attestation / AI-written code / AI-assisted "
326
+ "merge). Founder review recommended."
327
+ )
328
+
329
+ ok = not errors
330
+
331
+ result: Dict[str, Any] = {
332
+ "ok": ok,
333
+ "errors": errors,
334
+ "warnings": warnings,
335
+ "matched_claims": matched_claims,
336
+ "matched_banned": matched_banned,
337
+ "mentions_product": mentions_product,
338
+ "has_canonical_phrase": has_canonical,
339
+ "has_delimit_url": has_url,
340
+ "platform": platform_norm,
341
+ }
342
+
343
+ if log:
344
+ record = {
345
+ "ts": datetime.now(timezone.utc).isoformat(),
346
+ "ok": ok,
347
+ "errors": errors,
348
+ "warnings": warnings,
349
+ "matched_claims": matched_claims,
350
+ "matched_banned": matched_banned,
351
+ "mentions_product": mentions_product,
352
+ "has_canonical_phrase": has_canonical,
353
+ "has_delimit_url": has_url,
354
+ "platform": platform_norm,
355
+ "text_len": len(text),
356
+ "capabilities_path": str(path),
357
+ }
358
+ if audit_meta:
359
+ # Don't let audit_meta clobber computed fields.
360
+ for k, v in audit_meta.items():
361
+ record.setdefault(k, v)
362
+ _append_audit(record)
363
+
364
+ return result
365
+
366
+
367
+ __all__ = ["validate_draft", "DEFAULT_CAPABILITIES_PATH", "VALIDATION_LOG"]
@@ -0,0 +1,95 @@
1
+ # LED-216 Phase 1 — Capability Currency
2
+ #
3
+ # This file is the single source of truth for what Delimit is allowed to
4
+ # claim in any social draft. The capability_validator.py module reads it
5
+ # at draft emit time and hard-fails any draft that:
6
+ #
7
+ # * names a banned surface (literal substring), OR
8
+ # * matches a banned surface regex (e.g. tool-count hero language).
9
+ #
10
+ # Drafts that mention "Delimit" / "@delimit_ai" but reference NO canonical
11
+ # phrase are warned (not failed) so the founder can still review.
12
+ #
13
+ # ──────────────────────────────────────────────────────────────────────
14
+ # GOVERNANCE (mirror of LED-1037 banned-vocabulary gate)
15
+ # ──────────────────────────────────────────────────────────────────────
16
+ # Edits to this file MUST be gated by `delimit_deliberate` unanimous
17
+ # verdict. Same governance contract as the LED-1037 banned-vocabulary
18
+ # list: any change requires a multi-model panel sign-off (Claude +
19
+ # Gemini + Codex + Grok) before it lands. Silent edits are a policy
20
+ # violation and will be reverted.
21
+ #
22
+ # 30-day review cadence: capabilities rot fast. The `last_reviewed`
23
+ # field below MUST be bumped on every panel-approved edit, and the
24
+ # orchestrator should re-deliberate if `now - last_reviewed >
25
+ # review_cadence_days`.
26
+ # ──────────────────────────────────────────────────────────────────────
27
+
28
+ version: 1
29
+ last_reviewed: 2026-05-02
30
+ review_cadence_days: 30
31
+
32
+ allowed_claims:
33
+ # Each entry: {id, surface_name, description, since_version, evidence_link}
34
+ - id: attest_mcp
35
+ surface_name: "delimit attest mcp"
36
+ description: "Local CLI subcommand that runs the 5-check methodology preview."
37
+ since_version: 4.5.0
38
+ evidence_link: https://delimit.ai/methodology/mcp-attestation
39
+ - id: self_repair_v1
40
+ surface_name: "self-repair v1 closed loop"
41
+ description: "Sensor → diagnose → deliberate → apply → verify with founder approval gate."
42
+ since_version: gateway-2026-05-01
43
+ evidence_link: https://delimit.ai/methodology/mcp-attestation
44
+ - id: merge_gate
45
+ surface_name: "merge gate for AI-written code"
46
+ description: "Signed, replayable attestation for every AI-assisted merge."
47
+ since_version: "1.0"
48
+ evidence_link: https://delimit.ai
49
+ - id: multi_model_deliberation
50
+ surface_name: "multi-model deliberation panel"
51
+ description: "delimit_deliberate runs Claude + Gemini + Codex + Grok to consensus."
52
+ since_version: "1.0"
53
+ - id: persistent_context
54
+ surface_name: "persistent context across sessions"
55
+ description: "Memory + ledger survive across sessions and models via delimit_revive / delimit_session_handoff."
56
+ - id: governance_kernel
57
+ surface_name: "fail-closed governance kernel"
58
+ description: "Audit trail + evidence_collect at every gate."
59
+ - id: diff_engine
60
+ surface_name: "27 breaking-change types"
61
+ description: "Deterministic diff engine for OpenAPI spec changes."
62
+ - id: github_action
63
+ surface_name: "delimit-ai/delimit-action GitHub Action"
64
+ description: "On Marketplace, breaking-change detection on PRs."
65
+ - id: corp_dashboard
66
+ surface_name: "delimit_corp_dashboard"
67
+ description: "Single-call corp status synthesis."
68
+ since_version: gateway-2026-05-01
69
+ - id: methodology_v1
70
+ surface_name: "MCP attestation methodology v1"
71
+ description: "Public methodology document at delimit.ai/methodology/mcp-attestation."
72
+
73
+ banned_surfaces:
74
+ # Strings that MUST NOT appear in any social draft. Validator hard-fails.
75
+ - "AI OS"
76
+ - "governance platform"
77
+ - "one workspace for every AI coding assistant"
78
+ - "5 capability domains"
79
+ - "Jamsons OS"
80
+ - "Delimit OS"
81
+ - "186 tools"
82
+
83
+ banned_surface_patterns:
84
+ # Tool-count hero language patterns — match by regex in validator.
85
+ - "\\d+ tools"
86
+ - "\\d+ capability domains"
87
+
88
+ required_canonical_phrases:
89
+ # At least one of these MUST appear in any draft that mentions Delimit's
90
+ # product. If draft mentions Delimit by name and references no canonical
91
+ # phrase, validator warns (does not hard-fail).
92
+ - "merge gate"
93
+ - "signed, replayable attestation"
94
+ - "AI-written code"
95
+ - "AI-assisted merge"