@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.
- package/.tidyfactor +53 -52
- package/CHANGELOG.md +47 -0
- package/README.ar.md +20 -9
- package/README.md +20 -9
- package/SKILL.md +46 -30
- package/bin/add-skill.js +65 -50
- package/brand.json +67 -67
- package/brand.yaml +55 -0
- package/manifest.json +256 -0
- package/package.json +5 -1
- package/references/commands/audit.md +34 -0
- package/references/commands/brief.md +7 -5
- package/references/memory/20-brain-baas-integration.md +89 -0
- package/references/memory/ad-copy-templates.md +94 -93
- package/references/memory/arabic-writing.md +1 -1
- package/references/memory/campaign-mindmap.md +85 -0
- package/references/memory/decision-points.md +74 -73
- package/references/memory/frameworks.md +87 -86
- package/references/memory/lifecycle-flows.md +107 -106
- package/references/memory/metrics-benchmarks.md +71 -70
- package/references/memory/naming-conventions.md +75 -0
- package/references/memory/philosophy.md +2 -0
- package/references/memory/platform-specs.md +76 -75
- package/references/memory/promotions-math.md +57 -56
- package/references/memory/quality-bar.md +56 -55
- package/references/workflows/brief.md +74 -35
- package/references/workflows/campaign-launch.md +1 -1
- package/references/workflows/content-engine.md +1 -1
- package/references/workflows/email-lifecycle.md +1 -1
- package/references/workflows/paid-acquisition.md +1 -1
- package/references/workflows/promo-conversion.md +1 -1
- package/references/workflows/social-growth.md +1 -1
- package/references/workflows/viral-retention.md +1 -1
- package/scripts/audit_copy.py +112 -0
- package/scripts/calc_promo_math.py +69 -0
- package/scripts/clean_orphaned_assets.py +108 -0
- package/tests/scenarios.md +87 -0
- package/tools/build-skill.js +4 -0
- package/tools/validate_skill.py +265 -112
- package/assets/og-default.png +0 -0
package/tools/validate_skill.py
CHANGED
|
@@ -1,112 +1,265 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
validate_skill.py — TidyFactor Marketing
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
print("
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
print("
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if "
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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()
|
package/assets/og-default.png
DELETED
|
Binary file
|