@tidyfactor/marketing 1.3.0 → 1.6.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.
Files changed (40) hide show
  1. package/.tidyfactor +53 -52
  2. package/CHANGELOG.md +47 -0
  3. package/README.ar.md +20 -9
  4. package/README.md +20 -9
  5. package/SKILL.md +46 -30
  6. package/bin/add-skill.js +65 -50
  7. package/brand.json +67 -67
  8. package/brand.yaml +55 -0
  9. package/manifest.json +256 -0
  10. package/package.json +5 -1
  11. package/references/commands/audit.md +34 -0
  12. package/references/commands/brief.md +7 -5
  13. package/references/memory/20-brain-baas-integration.md +89 -0
  14. package/references/memory/ad-copy-templates.md +94 -93
  15. package/references/memory/arabic-writing.md +1 -1
  16. package/references/memory/campaign-mindmap.md +85 -0
  17. package/references/memory/decision-points.md +74 -73
  18. package/references/memory/frameworks.md +87 -86
  19. package/references/memory/lifecycle-flows.md +107 -106
  20. package/references/memory/metrics-benchmarks.md +71 -70
  21. package/references/memory/naming-conventions.md +75 -0
  22. package/references/memory/philosophy.md +2 -0
  23. package/references/memory/platform-specs.md +76 -75
  24. package/references/memory/promotions-math.md +57 -56
  25. package/references/memory/quality-bar.md +56 -55
  26. package/references/workflows/brief.md +74 -35
  27. package/references/workflows/campaign-launch.md +1 -1
  28. package/references/workflows/content-engine.md +1 -1
  29. package/references/workflows/email-lifecycle.md +1 -1
  30. package/references/workflows/paid-acquisition.md +1 -1
  31. package/references/workflows/promo-conversion.md +1 -1
  32. package/references/workflows/social-growth.md +1 -1
  33. package/references/workflows/viral-retention.md +1 -1
  34. package/scripts/audit_copy.py +112 -0
  35. package/scripts/calc_promo_math.py +69 -0
  36. package/scripts/clean_orphaned_assets.py +108 -0
  37. package/tests/scenarios.md +87 -0
  38. package/tools/build-skill.js +4 -0
  39. package/tools/validate_skill.py +265 -112
  40. package/assets/og-default.png +0 -0
@@ -1,112 +1,265 @@
1
- #!/usr/bin/env python3
2
- """
3
- validate_skill.py — TidyFactor Marketing integrity and release validation script.
4
- Verifies:
5
- 1. Version synchronization across .tidyfactor, package.json, brand.json, and CHANGELOG.md.
6
- 2. License consistency across files.
7
- 3. Link integrity between SKILL.md and referenced command/workflow/memory files.
8
- 4. Absence of hardcoded absolute machine paths in markdown files.
9
- """
10
-
11
- import sys
12
- import os
13
- import re
14
- import json
15
- from pathlib import Path
16
-
17
- ROOT = Path(__file__).resolve().parent.parent
18
-
19
- # Set stdout/stderr encoding to UTF-8 on Windows
20
- if sys.platform == "win32":
21
- sys.stdout.reconfigure(encoding="utf-8")
22
- sys.stderr.reconfigure(encoding="utf-8")
23
-
24
- def validate():
25
- failures = []
26
- print("=" * 60)
27
- print(" RUNNING TIDYFACTOR MARKETING RELEASE VALIDATION")
28
- print("=" * 60)
29
-
30
- # 1. Version Sync Check
31
- print("\n[1] Checking SemVer synchronization across metadata...")
32
- with open(ROOT / "package.json", "r", encoding="utf-8") as f:
33
- pkg_ver = json.load(f).get("version")
34
- with open(ROOT / ".tidyfactor", "r", encoding="utf-8") as f:
35
- tf_ver = json.load(f).get("version")
36
- with open(ROOT / "brand.json", "r", encoding="utf-8") as f:
37
- brand_ver = json.load(f).get("version")
38
-
39
- with open(ROOT / "CHANGELOG.md", "r", encoding="utf-8") as f:
40
- changelog_content = f.read()
41
-
42
- print(f" package.json : {pkg_ver}")
43
- print(f" .tidyfactor : {tf_ver}")
44
- print(f" brand.json : {brand_ver}")
45
-
46
- if not (pkg_ver == tf_ver == brand_ver):
47
- failures.append(f"Version mismatch: package.json({pkg_ver}) vs .tidyfactor({tf_ver}) vs brand.json({brand_ver})")
48
- else:
49
- print(f" [OK] Version {pkg_ver} synchronized across all JSON metadata.")
50
-
51
- if f"## [{pkg_ver}]" not in changelog_content:
52
- failures.append(f"CHANGELOG.md is missing release entry for version [{pkg_ver}].")
53
- else:
54
- print(f" [OK] CHANGELOG.md contains release entry for [{pkg_ver}].")
55
-
56
- # 2. License Consistency Check
57
- print("\n[2] Checking license consistency...")
58
- with open(ROOT / "package.json", "r", encoding="utf-8") as f:
59
- pkg_lic = json.load(f).get("license")
60
- with open(ROOT / "README.md", "r", encoding="utf-8") as f:
61
- readme_en = f.read()
62
- with open(ROOT / "README.ar.md", "r", encoding="utf-8") as f:
63
- readme_ar = f.read()
64
-
65
- if "License-MIT" in readme_en or "License-MIT" in readme_ar:
66
- failures.append("README contains MIT badge while package is licensed under Apache-2.0.")
67
- else:
68
- print(" [OK] License badges match Apache-2.0.")
69
-
70
- # 3. Path reference and link checks from SKILL.md
71
- print("\n[3] Checking SKILL.md referenced files exist on disk...")
72
- with open(ROOT / "SKILL.md", "r", encoding="utf-8") as f:
73
- skill_content = f.read()
74
-
75
- # Find all references like references/commands/*.md, workflows/*.md, memory/*.md
76
- refs = re.findall(r'(?:references/)?((?:commands|workflows|memory)/[a-zA-Z0-9_\-\.\*]+)', skill_content)
77
- for ref in refs:
78
- if "*" in ref:
79
- continue
80
- full_ref_path = ROOT / "references" / ref
81
- if not full_ref_path.exists():
82
- failures.append(f"Broken link in SKILL.md: references/{ref} does not exist on disk.")
83
- else:
84
- print(f" [OK] Found references/{ref}")
85
-
86
- # 4. Check for personal/machine-specific absolute paths
87
- print("\n[4] Auditing for leaked machine-specific absolute paths...")
88
- leaked_path_pattern = re.compile(r'[a-zA-Z]:\\[wW]amp64\\', re.IGNORECASE)
89
- for ext in ["*.md", "*.json", "*.js", "*.py"]:
90
- for file_path in ROOT.rglob(ext):
91
- if "dist" in file_path.parts or "__pycache__" in file_path.parts:
92
- continue
93
- with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
94
- content = f.read()
95
- if leaked_path_pattern.search(content):
96
- rel = file_path.relative_to(ROOT)
97
- failures.append(f"Leaked machine-specific absolute path found in {rel}")
98
-
99
- print("\n" + "=" * 60)
100
- if failures:
101
- print(f"[FAIL] {len(failures)} validation error(s) found:")
102
- for err in failures:
103
- print(f" - {err}")
104
- print("=" * 60)
105
- sys.exit(1)
106
- else:
107
- print("[SUCCESS] ALL SKILL INTEGRITY CHECKS PASSED!")
108
- print("=" * 60)
109
- sys.exit(0)
110
-
111
- if __name__ == "__main__":
112
- validate()
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_skill.py — TidyFactor Marketing Release & Integrity Validator.
4
+ Comprehensive 13-point governance audit across all skill artifacts.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+
13
+ # Ensure UTF-8 output on Windows terminal
14
+ if sys.platform == "win32":
15
+ sys.stdout.reconfigure(encoding="utf-8")
16
+ sys.stderr.reconfigure(encoding="utf-8")
17
+
18
+ def main():
19
+ root = Path(__file__).resolve().parent.parent
20
+ errors = []
21
+ warnings = []
22
+
23
+ print("=" * 60)
24
+ print(" RUNNING TIDYFACTOR MARKETING RELEASE VALIDATION")
25
+ print("=" * 60)
26
+
27
+ # 1. SemVer Synchronization Check
28
+ print("\n[1] Checking SemVer synchronization across metadata...")
29
+ pkg_file = root / "package.json"
30
+ tf_file = root / ".tidyfactor"
31
+ brand_yaml_file = root / "brand.yaml"
32
+ brand_json_file = root / "brand.json"
33
+ cl_file = root / "CHANGELOG.md"
34
+
35
+ pkg_ver = json.loads(pkg_file.read_text(encoding="utf-8")).get("version") if pkg_file.exists() else None
36
+ tf_ver = json.loads(tf_file.read_text(encoding="utf-8")).get("version") if tf_file.exists() else None
37
+
38
+ brand_ver = None
39
+ brand_source = None
40
+ if brand_yaml_file.exists():
41
+ brand_source = "brand.yaml"
42
+ try:
43
+ import yaml
44
+ brand_data = yaml.safe_load(brand_yaml_file.read_text(encoding="utf-8")) or {}
45
+ brand_ver = brand_data.get("version")
46
+ except Exception:
47
+ for line in brand_yaml_file.read_text(encoding="utf-8").splitlines():
48
+ if line.strip().startswith("version:"):
49
+ brand_ver = line.split(":", 1)[1].strip().strip('"').strip("'")
50
+ break
51
+ elif brand_json_file.exists():
52
+ brand_source = "brand.json"
53
+ brand_ver = json.loads(brand_json_file.read_text(encoding="utf-8")).get("version")
54
+
55
+ print(f" package.json : {pkg_ver}")
56
+ print(f" .tidyfactor : {tf_ver}")
57
+ print(f" {brand_source or 'brand.yaml/json'}: {brand_ver}")
58
+
59
+ if not (pkg_ver and tf_ver and brand_ver and pkg_ver == tf_ver == brand_ver):
60
+ errors.append(f"SemVer mismatch: package.json ({pkg_ver}), .tidyfactor ({tf_ver}), {brand_source} ({brand_ver})")
61
+ else:
62
+ print(f" [OK] Version {pkg_ver} synchronized across metadata ({brand_source}).")
63
+
64
+ if cl_file.exists():
65
+ cl_text = cl_file.read_text(encoding="utf-8")
66
+ if f"[{pkg_ver}]" not in cl_text:
67
+ errors.append(f"CHANGELOG.md missing release section for version [{pkg_ver}]")
68
+ else:
69
+ print(f" [OK] CHANGELOG.md contains release entry for [{pkg_ver}].")
70
+ else:
71
+ errors.append("CHANGELOG.md missing from repository root.")
72
+
73
+ # 2. License Consistency Check
74
+ print("\n[2] Checking license consistency...")
75
+ lic_file = root / "LICENSE"
76
+ if lic_file.exists():
77
+ lic_text = lic_file.read_text(encoding="utf-8")
78
+ if "Apache License" in lic_text or "Version 2.0" in lic_text:
79
+ print(" [OK] LICENSE file exists (Apache-2.0).")
80
+ else:
81
+ warnings.append("LICENSE file exists but may not be Apache-2.0.")
82
+ else:
83
+ errors.append("LICENSE file missing from repository root.")
84
+
85
+ # 3. Path References & File Existence from SKILL.md
86
+ print("\n[3] Checking SKILL.md referenced files exist on disk...")
87
+ skill_file = root / "SKILL.md"
88
+ if skill_file.exists():
89
+ skill_text = skill_file.read_text(encoding="utf-8")
90
+ raw_refs = re.findall(r'`(references/(?:commands|workflows|memory)/[a-zA-Z0-9_\-\.]+?\.md)`', skill_text)
91
+ raw_refs += re.findall(r'`(scripts/[a-zA-Z0-9_\-\.]+?\.py)`', skill_text)
92
+ refs = sorted(list(set(raw_refs)))
93
+ for ref in refs:
94
+ full_path = root / ref
95
+ if not full_path.exists():
96
+ errors.append(f"Referenced file does not exist: {ref}")
97
+ else:
98
+ print(f" [OK] Found {ref}")
99
+ else:
100
+ errors.append("SKILL.md missing from repository root.")
101
+
102
+ # 4. Workflow Single Outcome & Validation Checklists
103
+ print("\n[4] Checking workflow checklists...")
104
+ workflows_dir = root / "references" / "workflows"
105
+ if workflows_dir.exists():
106
+ for wf in sorted(workflows_dir.glob("*.md")):
107
+ wf_text = wf.read_text(encoding="utf-8")
108
+ if "## Validation checklist" not in wf_text:
109
+ errors.append(f"Workflow missing mandatory '## Validation checklist': {wf.name}")
110
+ else:
111
+ print(f" [OK] {wf.name} has Validation checklist.")
112
+ else:
113
+ errors.append("Workflows directory missing: references/workflows/")
114
+
115
+ # 5. Leaked Machine-Specific Absolute Paths
116
+ print("\n[5] Auditing for leaked machine-specific absolute paths...")
117
+ leaked_pattern = re.compile(r'([A-Za-z]:\\[wW]amp64\\|/Users/[a-zA-Z0-9_-]+/|/home/[a-zA-Z0-9_-]+/)', re.IGNORECASE)
118
+ scanned_exts = {".md", ".json", ".yaml", ".yml", ".js", ".py"}
119
+ for ext in scanned_exts:
120
+ for p in root.rglob(f"*{ext}"):
121
+ if any(ignored in p.parts for ignored in ["dist", ".git", "__pycache__", "node_modules"]):
122
+ continue
123
+ try:
124
+ content = p.read_text(encoding="utf-8", errors="ignore")
125
+ matches = leaked_pattern.findall(content)
126
+ if matches:
127
+ rel_p = p.relative_to(root)
128
+ errors.append(f"Machine-specific path leak in {rel_p}: {matches[0]}")
129
+ except Exception as e:
130
+ warnings.append(f"Could not read {p.name}: {e}")
131
+
132
+ # 6. SKILL.md Token Budget
133
+ print("\n[6] Checking SKILL.md token budget...")
134
+ if skill_file.exists():
135
+ st = skill_file.read_text(encoding="utf-8")
136
+ est_tokens = len(st.split()) * 1.25
137
+ print(f" SKILL.md size: {len(st)} chars / {len(st.split())} words ≈ {int(est_tokens)} tokens")
138
+ if est_tokens > 700:
139
+ errors.append(f"SKILL.md exceeds dispatcher token budget: ~{int(est_tokens)} tokens (max 700)")
140
+ else:
141
+ print(f" [OK] Within token budget ({int(est_tokens)} tokens, target ~350, max 700).")
142
+
143
+ # 7. Memory Freshness Markers
144
+ print("\n[7] Checking memory freshness markers (Rule 11)...")
145
+ from datetime import datetime, timezone
146
+ today = datetime.now(timezone.utc)
147
+ mem_dir = root / "references" / "memory"
148
+ if mem_dir.exists():
149
+ for mf in sorted(mem_dir.glob("*.md")):
150
+ mt = mf.read_text(encoding="utf-8")
151
+ m = re.search(r'<!--\s*last-verified:\s*(\d{4}-\d{2}-\d{2})\s*-->', mt)
152
+ if not m:
153
+ warnings.append(f"Missing freshness stamp <!-- last-verified: YYYY-MM-DD --> in {mf.name}")
154
+ else:
155
+ d_str = m.group(1)
156
+ try:
157
+ vf_date = datetime.strptime(d_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
158
+ age_days = (today - vf_date).days
159
+ if age_days > 180:
160
+ warnings.append(f"Memory file {mf.name} freshness marker is stale ({age_days} days old > 180)")
161
+ else:
162
+ print(f" [OK] {mf.name}: verified {d_str} ({age_days} days ago)")
163
+ except Exception:
164
+ warnings.append(f"Malformed date in freshness stamp for {mf.name}: {d_str}")
165
+
166
+ # 8. SKILL.md Frontmatter YAML Syntax
167
+ print("\n[8] Checking SKILL.md frontmatter YAML syntax & constraints (Rule 9)...")
168
+ if skill_file.exists():
169
+ st = skill_file.read_text(encoding="utf-8")
170
+ fm_match = re.match(r'^---\r?\n(.*?)\r?\n---', st, re.DOTALL)
171
+ if not fm_match:
172
+ errors.append("SKILL.md missing valid YAML frontmatter delimiter (--- ... ---)")
173
+ else:
174
+ fm_raw = fm_match.group(1)
175
+ try:
176
+ import yaml
177
+ fm_data = yaml.safe_load(fm_raw)
178
+ if not isinstance(fm_data, dict):
179
+ errors.append("SKILL.md frontmatter must parse into a mapping/dict.")
180
+ else:
181
+ name_val = fm_data.get("name")
182
+ desc_val = fm_data.get("description", "")
183
+ if not name_val or not re.match(r'^[a-z0-9-]+$', str(name_val)):
184
+ errors.append(f"Invalid name in frontmatter: '{name_val}'")
185
+ else:
186
+ print(f" [OK] Name: '{name_val}' (valid).")
187
+ if len(desc_val) > 1024:
188
+ errors.append(f"SKILL.md description too long ({len(desc_val)} > 1024 chars)")
189
+ else:
190
+ print(f" [OK] Description: {len(desc_val)}/1024 chars (YAML syntax valid).")
191
+ except Exception as e:
192
+ errors.append(f"SKILL.md YAML frontmatter parsing failed: {e}")
193
+
194
+ # 9. Runtime Tooling Manifest
195
+ print("\n[9] Checking Runtime Tooling Manifest & Scope declaration (Rule 10)...")
196
+ manifest_file = root / "manifest.json"
197
+ if manifest_file.exists():
198
+ try:
199
+ m_data = json.loads(manifest_file.read_text(encoding="utf-8"))
200
+ tools = m_data.get("tools", [])
201
+ print(f" [OK] Tooling Scope declared with {len(tools)} tool(s).")
202
+ except Exception as e:
203
+ errors.append(f"manifest.json parsing error: {e}")
204
+ else:
205
+ errors.append("manifest.json missing from repository root.")
206
+
207
+ # 10. Test Scenarios
208
+ print("\n[10] Checking Test Scenarios (tests/scenarios.md)...")
209
+ scenarios_file = root / "tests" / "scenarios.md"
210
+ if scenarios_file.exists():
211
+ s_text = scenarios_file.read_text(encoding="utf-8")
212
+ sc_count = len(re.findall(r'##\s+Scenario\s+\d+:', s_text))
213
+ if sc_count < 3:
214
+ warnings.append(f"tests/scenarios.md has only {sc_count} scenario(s), minimum 3 recommended.")
215
+ else:
216
+ print(f" [OK] tests/scenarios.md found with {sc_count} test scenarios (min 3).")
217
+ else:
218
+ warnings.append("Missing tests/scenarios.md test evaluation file.")
219
+
220
+ # 11. MCP Boundary Documentation
221
+ print("\n[11] Checking MCP boundary documentation (Rule 12)...")
222
+ if skill_file.exists() and "Skill vs MCP Boundary" in skill_file.read_text(encoding="utf-8"):
223
+ print(" [OK] SKILL.md contains Skill vs MCP Boundary section.")
224
+ else:
225
+ warnings.append("SKILL.md missing explicit 'Skill vs MCP Boundary' section.")
226
+
227
+ # 12. Contextual Decision Layer & Decision Gates
228
+ print("\n[12] Checking Contextual Decision Layer & Decision Gates (Rule 14 / CDL v1.1.0)...")
229
+ if manifest_file.exists():
230
+ try:
231
+ m_data = json.loads(manifest_file.read_text(encoding="utf-8"))
232
+ gates = m_data.get("decision_gates", [])
233
+ if not gates:
234
+ warnings.append("manifest.json missing 'decision_gates' declaration (CDL v1.1.0).")
235
+ else:
236
+ print(f" [OK] manifest.json contains {len(gates)} valid decision gate(s) (CDL v1.1.0 compliant).")
237
+ except Exception:
238
+ pass
239
+
240
+ # 13. Token Efficiency & YAML Primacy
241
+ print("\n[13] Checking Token Efficiency & YAML Primacy (Rule 15)...")
242
+ if brand_yaml_file.exists():
243
+ print(" [OK] brand.yaml present (cognitive layer prioritized for token efficiency).")
244
+ else:
245
+ warnings.append("Missing brand.yaml. Adopting brand.yaml is recommended under Rule 15 for 35-50% token efficiency.")
246
+
247
+ print("\n" + "=" * 60)
248
+ if warnings:
249
+ print(f"[WARN] {len(warnings)} non-blocking warning(s):")
250
+ for w in warnings:
251
+ print(f" ⚠️ {w}")
252
+
253
+ if errors:
254
+ print(f"[FAIL] {len(errors)} validation error(s) found:")
255
+ for err in errors:
256
+ print(f" ❌ {err}")
257
+ print("=" * 60)
258
+ sys.exit(1)
259
+ else:
260
+ print("[SUCCESS] ALL SKILL INTEGRITY CHECKS PASSED!")
261
+ print("=" * 60)
262
+ sys.exit(0)
263
+
264
+ if __name__ == "__main__":
265
+ main()
Binary file