agentforge-framework 0.2.0__py3-none-any.whl

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 (89) hide show
  1. agentforge_framework/.claude-plugin/plugin.json +4 -0
  2. agentforge_framework/__init__.py +3 -0
  3. agentforge_framework/agents/__init__.py +92 -0
  4. agentforge_framework/agents/architect.py +146 -0
  5. agentforge_framework/agents/implementer.py +162 -0
  6. agentforge_framework/agents/orchestrator.py +588 -0
  7. agentforge_framework/agents/reviewer.py +335 -0
  8. agentforge_framework/agents/security.py +138 -0
  9. agentforge_framework/agents/tester.py +125 -0
  10. agentforge_framework/cli.py +461 -0
  11. agentforge_framework/context/__init__.py +1 -0
  12. agentforge_framework/context/extractors/__init__.py +76 -0
  13. agentforge_framework/context/extractors/base.py +47 -0
  14. agentforge_framework/context/extractors/python.py +65 -0
  15. agentforge_framework/context/extractors/sql.py +121 -0
  16. agentforge_framework/context/extractors/yaml.py +59 -0
  17. agentforge_framework/context/prompt.py +104 -0
  18. agentforge_framework/context/resolver.py +185 -0
  19. agentforge_framework/core/__init__.py +1 -0
  20. agentforge_framework/core/commands.py +170 -0
  21. agentforge_framework/core/config.py +90 -0
  22. agentforge_framework/core/contracts.py +875 -0
  23. agentforge_framework/core/gates.py +333 -0
  24. agentforge_framework/core/issues.py +697 -0
  25. agentforge_framework/core/plan_format.py +272 -0
  26. agentforge_framework/core/process.py +141 -0
  27. agentforge_framework/core/project.py +262 -0
  28. agentforge_framework/core/registry.py +455 -0
  29. agentforge_framework/core/repo.py +185 -0
  30. agentforge_framework/core/router.py +1 -0
  31. agentforge_framework/core/runtime.py +639 -0
  32. agentforge_framework/core/skills.py +255 -0
  33. agentforge_framework/core/workflow.py +215 -0
  34. agentforge_framework/plugins/__init__.py +35 -0
  35. agentforge_framework/plugins/databricks/__init__.py +86 -0
  36. agentforge_framework/plugins/pyspark/__init__.py +57 -0
  37. agentforge_framework/plugins/python/__init__.py +45 -0
  38. agentforge_framework/plugins/sql/__init__.py +377 -0
  39. agentforge_framework/providers/__init__.py +48 -0
  40. agentforge_framework/providers/base.py +248 -0
  41. agentforge_framework/providers/claude.py +159 -0
  42. agentforge_framework/providers/codex.py +139 -0
  43. agentforge_framework/skills/MANIFEST.yaml +157 -0
  44. agentforge_framework/skills/NOTICE +49 -0
  45. agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
  46. agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  47. agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
  48. agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
  49. agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
  50. agentforge_framework/skills/grilling/SKILL.md +28 -0
  51. agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
  52. agentforge_framework/skills/to-spec/SKILL.md +75 -0
  53. agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
  54. agentforge_framework/skills/to-tickets/SKILL.md +105 -0
  55. agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
  56. agentforge_framework/skills/unslop/SKILL.md +131 -0
  57. agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
  58. agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
  59. agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
  60. agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
  61. agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
  62. agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
  63. agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
  64. agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
  65. agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
  66. agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
  67. agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
  68. agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
  69. agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
  70. agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
  71. agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
  72. agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
  73. agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
  74. agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
  75. agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
  76. agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
  77. agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
  78. agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
  79. agentforge_framework/skills/write-plainly/SKILL.md +94 -0
  80. agentforge_framework/workflows/bugfix.yaml +8 -0
  81. agentforge_framework/workflows/feature.yaml +16 -0
  82. agentforge_framework/workflows/review.yaml +10 -0
  83. agentforge_framework-0.2.0.dist-info/METADATA +321 -0
  84. agentforge_framework-0.2.0.dist-info/RECORD +89 -0
  85. agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
  86. agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
  87. agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
  88. agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
  89. agentforge_framework-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate that all must-preserve constraints survived transformation.
4
+
5
+ Compares original text constraints against transformed text.
6
+ Exit code 0 = all constraints preserved, 1 = missing constraints.
7
+
8
+ Usage:
9
+ python validate_preservation.py original.txt transformed.txt
10
+ python validate_preservation.py original.txt transformed.txt constraints.json
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+ import json
17
+ import re
18
+ from typing import TypedDict
19
+
20
+ # Import constraint extraction
21
+ from extract_constraints import extract_constraints, Constraint
22
+
23
+
24
+ class ValidationResult(TypedDict):
25
+ passed: bool
26
+ total_constraints: int
27
+ preserved: int
28
+ missing: list[Constraint]
29
+ warnings: list[str]
30
+
31
+
32
+ _MAGNITUDES = {
33
+ "k": 1e3, "thousand": 1e3,
34
+ "m": 1e6, "million": 1e6,
35
+ "b": 1e9, "billion": 1e9,
36
+ "trillion": 1e12,
37
+ }
38
+
39
+ _MONTHS = [
40
+ "january", "february", "march", "april", "may", "june", "july",
41
+ "august", "september", "october", "november", "december",
42
+ ]
43
+
44
+
45
+ def normalize_value(value: str) -> str:
46
+ """Normalize a constraint value for comparison."""
47
+ # Remove extra whitespace
48
+ normalized = re.sub(r'\s+', ' ', value.strip())
49
+ # Lowercase for comparison (preserve original for display)
50
+ return normalized.lower()
51
+
52
+
53
+ def parse_money(token: str) -> float | None:
54
+ """Parse a currency token into an absolute amount, honoring magnitude words.
55
+
56
+ '$47.3M' and '$47.3 million' -> 47_300_000; '$47.3 billion' -> 47_300_000_000.
57
+ Magnitude is part of the fact, so the two must not compare equal.
58
+ """
59
+ m = re.search(
60
+ r'([\d,]+\.?\d*)\s*(k|m|b|thousand|million|billion)?',
61
+ token.lower().replace("$", ""),
62
+ )
63
+ if not m or not m.group(1).strip(","):
64
+ return None
65
+ amount = float(m.group(1).replace(",", ""))
66
+ if m.group(2):
67
+ amount *= _MAGNITUDES[m.group(2)]
68
+ return amount
69
+
70
+
71
+ def parse_magnitude_number(token: str) -> float | None:
72
+ m = re.search(
73
+ r'\b([\d,]+\.?\d*)\s*(thousand|million|billion|trillion)?\b',
74
+ token.lower(),
75
+ )
76
+ if not m or not m.group(1).strip(","):
77
+ return None
78
+ amount = float(m.group(1).replace(",", ""))
79
+ if m.group(2):
80
+ amount *= _MAGNITUDES[m.group(2)]
81
+ return amount
82
+
83
+
84
+ def _numbers_match_exactly(value: str, text: str, pattern: str) -> bool:
85
+ """True iff the numeric value appears as a whole quantity in text.
86
+
87
+ Guards against the substring trap where '12' is 'found' inside '120'.
88
+ """
89
+ want = re.findall(r'\d+\.?\d*', value)
90
+ have = set(re.findall(pattern, text.lower()))
91
+ return all(num in have for num in want) and bool(want)
92
+
93
+
94
+ _UNIT_SYNONYMS = {
95
+ "km": {"km", "kilometer", "kilometers", "kilometre", "kilometres"},
96
+ "mi": {"mi", "mile", "miles"},
97
+ "kg": {"kg", "kilogram", "kilograms"},
98
+ "g": {"g", "gram", "grams"},
99
+ "ms": {"ms", "millisecond", "milliseconds"},
100
+ "s": {"s", "sec", "second", "seconds"},
101
+ "m": {"m", "meter", "meters", "metre", "metres"},
102
+ "cm": {"cm", "centimeter", "centimeters", "centimetre", "centimetres"},
103
+ "mm": {"mm", "millimeter", "millimeters", "millimetre", "millimetres"},
104
+ "ft": {"ft", "foot", "feet"},
105
+ "in": {"in", "inch", "inches"},
106
+ "lb": {"lb", "lbs", "pound", "pounds"},
107
+ "oz": {"oz", "ounce", "ounces"},
108
+ "°c": {"°c", "c", "celsius"},
109
+ "°f": {"°f", "f", "fahrenheit"},
110
+ "min": {"min", "mins", "minute", "minutes"},
111
+ "hr": {"hr", "hrs", "hour", "hours"},
112
+ "day": {"day", "days"},
113
+ "week": {"week", "weeks", "wk", "wks"},
114
+ "month": {"month", "months", "mo"},
115
+ "year": {"year", "years", "yr", "yrs"},
116
+ "kb": {"kb", "kilobyte", "kilobytes"},
117
+ "mb": {"mb", "megabyte", "megabytes"},
118
+ "gb": {"gb", "gigabyte", "gigabytes"},
119
+ "tb": {"tb", "terabyte", "terabytes"},
120
+ "pb": {"pb", "petabyte", "petabytes"},
121
+ "px": {"px", "pixel", "pixels"},
122
+ }
123
+
124
+
125
+ def _measurement_parts(value: str) -> tuple[str, set[str]] | None:
126
+ m = re.search(r'(\d+\.?\d*)\s*([^\d\s]+|degrees?(?:\s+\w+)?)', value, re.I)
127
+ if not m:
128
+ return None
129
+ unit = m.group(2).lower().strip()
130
+ unit = re.sub(r'^degrees?\s*', '', unit) or "degree"
131
+ aliases = _UNIT_SYNONYMS.get(unit)
132
+ if aliases is None:
133
+ for family in _UNIT_SYNONYMS.values():
134
+ if unit in family:
135
+ aliases = family
136
+ break
137
+ else:
138
+ aliases = {unit}
139
+ return m.group(1), aliases
140
+
141
+
142
+ def _quote_core(value: str) -> str:
143
+ return value.strip().strip('"“”').lower()
144
+
145
+
146
+ def _time_variants(value: str) -> set[str]:
147
+ m = re.search(r'\b(\d{1,2})(?::(\d{2}))?(?::\d{2})?\s*(am|pm)?\b', value, re.I)
148
+ if not m:
149
+ return set()
150
+ hour = str(int(m.group(1)))
151
+ minute = m.group(2) or "00"
152
+ suffix = (m.group(3) or "").lower()
153
+ spaced = f" {suffix}" if suffix else ""
154
+ compact = suffix
155
+ variants = {f"{hour}:{minute}{spaced}".strip(), f"{hour}:{minute}{compact}".strip()}
156
+ if minute == "00":
157
+ variants.update({f"{hour}{spaced}".strip(), f"{hour}{compact}".strip()})
158
+ return variants
159
+
160
+
161
+ def find_constraint_in_text(constraint: Constraint, text: str) -> bool:
162
+ """Check if a constraint value exists in text."""
163
+ value = constraint["value"]
164
+ ctype = constraint["type"]
165
+ normalized_value = normalize_value(value)
166
+ normalized_text = normalize_value(text)
167
+
168
+ # Direct match
169
+ if normalized_value in normalized_text:
170
+ return True
171
+
172
+ # and/or: the disjunction must survive; "or" (or "or both") is faithful,
173
+ # a bare conjunction is not.
174
+ if ctype == "and_or":
175
+ return bool(re.search(r"\band/or\b|\bor\b", text, re.IGNORECASE))
176
+
177
+ # Currency: compare absolute amounts so a magnitude swap (M -> billion) fails.
178
+ if ctype == "currency":
179
+ target = parse_money(value)
180
+ if target is None:
181
+ return False
182
+ for token in re.findall(
183
+ r'\$?[\d,]+\.?\d*\s*(?:k|m|b|thousand|million|billion)?', text, re.IGNORECASE
184
+ ):
185
+ amount = parse_money(token)
186
+ if amount is not None and abs(amount - target) < 0.01:
187
+ return True
188
+ return False
189
+
190
+ if ctype == "magnitude_number":
191
+ target = parse_magnitude_number(value)
192
+ if target is None:
193
+ return False
194
+ for token in re.findall(
195
+ r'\b[\d,]+\.?\d*\s*(?:thousand|million|billion|trillion)?\b',
196
+ text,
197
+ re.IGNORECASE,
198
+ ):
199
+ amount = parse_magnitude_number(token)
200
+ if amount is not None and abs(amount - target) < 0.01:
201
+ return True
202
+ return False
203
+
204
+ # Percentage: require an exact numeric match (12% != 120%).
205
+ if ctype == "percentage":
206
+ return _numbers_match_exactly(value, text, r'(\d+\.?\d*)\s*(?:%|percent)')
207
+
208
+ if ctype == "measurement":
209
+ parts = _measurement_parts(value)
210
+ if not parts:
211
+ return False
212
+ number, units = parts
213
+ clean_text = text.replace(",", "").lower()
214
+ if not re.search(r'(?<![\d.])' + re.escape(number.replace(",", "")) + r'(?![\d.])', clean_text):
215
+ return False
216
+ return any(re.search(r'(?<!\w)' + re.escape(unit) + r'(?!\w)', clean_text) for unit in units)
217
+
218
+ if ctype == "range":
219
+ return _numbers_match_exactly(value, text, r'(?<![\d.])(\d+\.?\d*)(?![\d.])')
220
+
221
+ if ctype == "time":
222
+ text_variants = _time_variants(text)
223
+ return bool(_time_variants(value) & text_variants)
224
+
225
+ # Other quantities: match digits, comma-insensitive, but as whole tokens.
226
+ if ctype in ("count", "number"):
227
+ for num in re.findall(r'[\d,]+\.?\d*', value):
228
+ clean_num = num.replace(",", "")
229
+ if re.search(r'(?<![\d.])' + re.escape(clean_num) + r'(?![\d.])',
230
+ text.replace(",", "")):
231
+ return True
232
+ return False
233
+
234
+ # Dates: the year alone is not enough — a changed month is a changed fact.
235
+ if ctype == "date_quarter":
236
+ year_match = re.search(r'\d{4}', value)
237
+ if not (year_match and year_match.group() in text):
238
+ return False
239
+ q = re.search(r'q([1-4])', value.lower())
240
+ if not q:
241
+ return False
242
+ ordinal = {"1": "first", "2": "second", "3": "third", "4": "fourth"}[q.group(1)]
243
+ low_text = text.lower()
244
+ return q.group(0) in low_text or re.search(ordinal + r'\s+quarter', low_text) is not None
245
+
246
+ if ctype.startswith("date"):
247
+ year_match = re.search(r'\d{4}', value)
248
+ if not (year_match and year_match.group() in text):
249
+ return False
250
+ low = value.lower()
251
+ month = next((mo for mo in _MONTHS if mo[:3] in low), None)
252
+ if month and month[:3] not in text.lower():
253
+ return False
254
+ quarter = re.search(r'q[1-4]', low)
255
+ if quarter and quarter.group() not in text.lower():
256
+ return False
257
+ return True
258
+
259
+ # For quotes, check if core content is present (without surrounding quotes)
260
+ if constraint["type"] == "quote":
261
+ inner = _quote_core(value)
262
+ comparable_text = normalized_text.replace("“", '"').replace("”", '"')
263
+ if inner in comparable_text:
264
+ return True
265
+
266
+ if ctype == "proper_noun":
267
+ words = re.findall(r'[A-Z][a-z]+', value)
268
+ if words and all(re.search(r'\b' + re.escape(word) + r'\b', text, re.I) for word in words):
269
+ return True
270
+
271
+ return False
272
+
273
+
274
+ _NEGATION_RE = re.compile(
275
+ r"\b(?:not|never|no|cannot|can't|won't|don't|doesn't|didn't|isn't|aren't|"
276
+ r"wasn't|weren't|without|neither|nor|none|fails?\s+to|rather\s+than)\b", re.I)
277
+ _SCOPE_RE = re.compile(
278
+ r"\b(?:most|all|none|every|each|some|few|several|majority|minority|only|"
279
+ r"always|usually|typically|rarely|approximately|roughly|about|nearly)\b", re.I)
280
+ _CONDITIONAL_RE = re.compile(
281
+ r"\b(?:if|unless|provided\s+that|only\s+if|assuming|given\s+that|"
282
+ r"in\s+the\s+event|contingent\s+on|except|excluding|other\s+than|"
283
+ r"aside\s+from|save\s+for)\b", re.I)
284
+ _WEAK_MODAL_RE = re.compile(
285
+ r"\b(?:may|might|could|should|can|likely|probably|appears?|suggests?|seems?)\b", re.I)
286
+ _STRONG_MODAL_RE = re.compile(
287
+ r"\b(?:will|must|always|definitely|certainly|guarantees?|proves?)\b", re.I)
288
+
289
+
290
+ def semantic_drift_warnings(original: str, transformed: str) -> list[str]:
291
+ """Warn when meaning-bearing words present in the original vanish in the rewrite."""
292
+ out: list[str] = []
293
+ o, t = original.lower(), transformed.lower()
294
+
295
+ on, tn = len(_NEGATION_RE.findall(o)), len(_NEGATION_RE.findall(t))
296
+ if on > tn:
297
+ out.append(
298
+ f"Negation count dropped {on}->{tn}. Verify no claim was inverted or "
299
+ "weakened (e.g. 'does not support' -> 'supports').")
300
+
301
+ missing_scope = sorted({w for w in _SCOPE_RE.findall(o)} - {w for w in _SCOPE_RE.findall(t)})
302
+ for w in missing_scope:
303
+ out.append(f"Scope/precision word '{w}' not in output. Verify the claim's scope is unchanged.")
304
+
305
+ oc, tc = len(_CONDITIONAL_RE.findall(o)), len(_CONDITIONAL_RE.findall(t))
306
+ if oc > tc:
307
+ out.append(
308
+ f"Conditional count dropped {oc}->{tc}. Verify a conditional claim "
309
+ "wasn't turned into an unconditional one.")
310
+
311
+ ow, tw = len(_WEAK_MODAL_RE.findall(o)), len(_WEAK_MODAL_RE.findall(t))
312
+ os, ts = len(_STRONG_MODAL_RE.findall(o)), len(_STRONG_MODAL_RE.findall(t))
313
+ if ow > tw and ts > os:
314
+ out.append("Hedged claim may have been strengthened. Verify uncertainty was not turned into certainty.")
315
+
316
+ return out
317
+
318
+
319
+ def validate_preservation(
320
+ original_text: str,
321
+ transformed_text: str,
322
+ constraints: list[Constraint] | None = None
323
+ ) -> ValidationResult:
324
+ """Validate that all constraints are preserved in transformed text."""
325
+
326
+ if constraints is None:
327
+ constraints = extract_constraints(original_text)
328
+
329
+ missing: list[Constraint] = []
330
+ warnings: list[str] = []
331
+
332
+ for constraint in constraints:
333
+ if not find_constraint_in_text(constraint, transformed_text):
334
+ missing.append(constraint)
335
+
336
+ # Generate warnings for near-misses
337
+ for m in missing:
338
+ if m["type"] == "percentage":
339
+ # Check if number exists without %
340
+ num = re.search(r'[\d.]+', m["value"])
341
+ if num and num.group() in transformed_text:
342
+ warnings.append(f"Number {num.group()} found but missing '%' symbol")
343
+
344
+ # Semantic drift warnings. These are NOT numbers/names — they are the
345
+ # meaning-bearing words (negations, scope, conditionals) that the skill's
346
+ # de-hedging rules are most likely to delete and that pure fact-matching
347
+ # cannot see. Surfaced as warnings so the rewrite gets a human re-check;
348
+ # de-slopping legitimately removes some, so they don't hard-fail on their own.
349
+ warnings.extend(semantic_drift_warnings(original_text, transformed_text))
350
+
351
+ preserved = len(constraints) - len(missing)
352
+
353
+ return {
354
+ "passed": len(missing) == 0,
355
+ "total_constraints": len(constraints),
356
+ "preserved": preserved,
357
+ "missing": missing,
358
+ "warnings": warnings
359
+ }
360
+
361
+
362
+ def main() -> None:
363
+ args = sys.argv[1:]
364
+ strict = False
365
+ if args and args[0] == "--strict":
366
+ strict = True
367
+ args = args[1:]
368
+
369
+ if len(args) < 2:
370
+ print("Usage: validate_preservation.py <original.txt> <transformed.txt> [constraints.json]")
371
+ sys.exit(1)
372
+
373
+ # Read inputs
374
+ try:
375
+ with open(args[0], 'r') as f:
376
+ original_text = f.read()
377
+ with open(args[1], 'r') as f:
378
+ transformed_text = f.read()
379
+ except OSError as e:
380
+ print(json.dumps({"error": f"Could not read input: {e}"}))
381
+ sys.exit(2)
382
+
383
+ # Optionally read pre-computed constraints
384
+ constraints = None
385
+ if len(args) > 2:
386
+ with open(args[2], 'r') as f:
387
+ data = json.load(f)
388
+ constraints = data.get("constraints", [])
389
+
390
+ result = validate_preservation(original_text, transformed_text, constraints)
391
+
392
+ # Output result
393
+ output = {
394
+ "passed": result["passed"],
395
+ "total_constraints": result["total_constraints"],
396
+ "preserved": result["preserved"],
397
+ "missing_count": len(result["missing"]),
398
+ "missing": result["missing"],
399
+ "warnings": result["warnings"]
400
+ }
401
+
402
+ print(json.dumps(output, indent=2))
403
+
404
+ # Exit with appropriate code
405
+ sys.exit(0 if result["passed"] and not (strict and result["warnings"]) else 1)
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()