taketomarket 0.1.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 (123) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +419 -0
  4. package/agents/ttm-producer.md +53 -0
  5. package/bin/lib/campaign.cjs +553 -0
  6. package/bin/lib/commit.cjs +105 -0
  7. package/bin/lib/core.cjs +172 -0
  8. package/bin/lib/deviation.cjs +149 -0
  9. package/bin/lib/drift-log.cjs +219 -0
  10. package/bin/lib/health.cjs +438 -0
  11. package/bin/lib/slug.cjs +59 -0
  12. package/bin/lib/state.cjs +96 -0
  13. package/bin/ttm-tools.cjs +157 -0
  14. package/gates/base-gates.md +266 -0
  15. package/gates/discipline/.gitkeep +0 -0
  16. package/gates/gate-evaluation.md +341 -0
  17. package/gates/meta-gates.md +19 -0
  18. package/install.js +307 -0
  19. package/package.json +53 -0
  20. package/playbooks/.gitkeep +0 -0
  21. package/playbooks/aeo.md +223 -0
  22. package/playbooks/affiliate.md +272 -0
  23. package/playbooks/base.md +110 -0
  24. package/playbooks/email.md +306 -0
  25. package/playbooks/events.md +320 -0
  26. package/playbooks/linkedin.md +263 -0
  27. package/playbooks/paid-ads.md +318 -0
  28. package/playbooks/pr-media.md +296 -0
  29. package/playbooks/seo.md +284 -0
  30. package/playbooks/social.md +305 -0
  31. package/playbooks/youtube.md +325 -0
  32. package/references/context-loading.md +107 -0
  33. package/references/learnings-extraction.md +94 -0
  34. package/references/measurement-template.md +48 -0
  35. package/references/meta-gate-evaluation.md +169 -0
  36. package/references/positioning-check-report.md +197 -0
  37. package/references/review-checklist.md +78 -0
  38. package/references/ship-checklist-items.md +94 -0
  39. package/settings.json +4 -0
  40. package/skills/ttm-aeo-check/SKILL.md +20 -0
  41. package/skills/ttm-affiliate-kit/SKILL.md +19 -0
  42. package/skills/ttm-archive/SKILL.md +13 -0
  43. package/skills/ttm-brand-refresh/SKILL.md +19 -0
  44. package/skills/ttm-brief/SKILL.md +14 -0
  45. package/skills/ttm-competitor-scan/SKILL.md +19 -0
  46. package/skills/ttm-email-preflight/SKILL.md +19 -0
  47. package/skills/ttm-fix/SKILL.md +13 -0
  48. package/skills/ttm-health/SKILL.md +12 -0
  49. package/skills/ttm-icp-refresh/SKILL.md +19 -0
  50. package/skills/ttm-init/SKILL.md +12 -0
  51. package/skills/ttm-keyword-map/SKILL.md +19 -0
  52. package/skills/ttm-learn/SKILL.md +14 -0
  53. package/skills/ttm-measure/SKILL.md +14 -0
  54. package/skills/ttm-new-campaign/SKILL.md +13 -0
  55. package/skills/ttm-next/SKILL.md +12 -0
  56. package/skills/ttm-positioning-check/SKILL.md +19 -0
  57. package/skills/ttm-positioning-shift/SKILL.md +19 -0
  58. package/skills/ttm-produce/SKILL.md +14 -0
  59. package/skills/ttm-repurpose/SKILL.md +20 -0
  60. package/skills/ttm-research/SKILL.md +13 -0
  61. package/skills/ttm-resume/SKILL.md +13 -0
  62. package/skills/ttm-review/SKILL.md +13 -0
  63. package/skills/ttm-seo-audit/SKILL.md +20 -0
  64. package/skills/ttm-ship/SKILL.md +13 -0
  65. package/skills/ttm-state/SKILL.md +13 -0
  66. package/skills/ttm-verify/SKILL.md +14 -0
  67. package/templates/agents-md.md +65 -0
  68. package/templates/campaign-brief.md +74 -0
  69. package/templates/campaign-research.md +39 -0
  70. package/templates/campaign-state.md +40 -0
  71. package/templates/claude-md.md +65 -0
  72. package/templates/deviation-log.md +12 -0
  73. package/templates/drift-log.md +17 -0
  74. package/templates/fix-brief.md +59 -0
  75. package/templates/fix-log.md +22 -0
  76. package/templates/measurement-report.md +75 -0
  77. package/templates/migration-plan.md +24 -0
  78. package/templates/production-manifest.json +20 -0
  79. package/templates/reference-files/brand.md +45 -0
  80. package/templates/reference-files/calendar.md +30 -0
  81. package/templates/reference-files/channels.md +40 -0
  82. package/templates/reference-files/competitors.md +40 -0
  83. package/templates/reference-files/icp.md +50 -0
  84. package/templates/reference-files/learnings.md +40 -0
  85. package/templates/reference-files/metrics.md +42 -0
  86. package/templates/reference-files/positioning.md +38 -0
  87. package/templates/reference-files/state.md +27 -0
  88. package/templates/verification-report.md +59 -0
  89. package/workflows/discipline/.gitkeep +0 -0
  90. package/workflows/discipline/aeo-check.md +180 -0
  91. package/workflows/discipline/affiliate-kit.md +147 -0
  92. package/workflows/discipline/email-preflight.md +150 -0
  93. package/workflows/discipline/keyword-map.md +125 -0
  94. package/workflows/discipline/repurpose.md +329 -0
  95. package/workflows/discipline/seo-audit.md +169 -0
  96. package/workflows/lifecycle/.gitkeep +0 -0
  97. package/workflows/lifecycle/brief-positioning-check.md +90 -0
  98. package/workflows/lifecycle/brief.md +355 -0
  99. package/workflows/lifecycle/fix.md +495 -0
  100. package/workflows/lifecycle/learn.md +405 -0
  101. package/workflows/lifecycle/measure.md +379 -0
  102. package/workflows/lifecycle/produce.md +383 -0
  103. package/workflows/lifecycle/research.md +264 -0
  104. package/workflows/lifecycle/review.md +432 -0
  105. package/workflows/lifecycle/ship.md +521 -0
  106. package/workflows/lifecycle/verify.md +507 -0
  107. package/workflows/reference-mgmt/.gitkeep +0 -0
  108. package/workflows/reference-mgmt/brand-refresh.md +193 -0
  109. package/workflows/reference-mgmt/competitor-scan.md +228 -0
  110. package/workflows/reference-mgmt/icp-refresh.md +200 -0
  111. package/workflows/reference-mgmt/positioning-check.md +339 -0
  112. package/workflows/reference-mgmt/positioning-shift.md +368 -0
  113. package/workflows/setup/.gitkeep +0 -0
  114. package/workflows/setup/init-questions.md +225 -0
  115. package/workflows/setup/init-validation.md +155 -0
  116. package/workflows/setup/init.md +449 -0
  117. package/workflows/setup/new-campaign.md +134 -0
  118. package/workflows/utility/.gitkeep +0 -0
  119. package/workflows/utility/archive.md +334 -0
  120. package/workflows/utility/health.md +166 -0
  121. package/workflows/utility/next.md +187 -0
  122. package/workflows/utility/resume.md +249 -0
  123. package/workflows/utility/state.md +207 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Core -- Shared output helpers and utility functions for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins: fs, path.
5
+ *
6
+ * Exports: output, error, parseNamedArgs, safeReadFile, safeWriteFile,
7
+ * parseFrontmatter, serializeFrontmatter
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // ── Output helpers ───────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * Write result to stdout. JSON by default, raw string if --raw flag is set.
19
+ * @param {object} result - Structured result object
20
+ * @param {boolean} raw - Whether --raw flag was passed
21
+ * @param {*} rawValue - Plain text value for raw mode
22
+ */
23
+ function output(result, raw, rawValue) {
24
+ if (raw && rawValue !== undefined) {
25
+ process.stdout.write(String(rawValue) + '\n');
26
+ } else {
27
+ const json = JSON.stringify(result, null, 2);
28
+ process.stdout.write(json + '\n');
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Write error message to stderr and exit with code 1.
34
+ * @param {string} message - Error description
35
+ */
36
+ function error(message) {
37
+ process.stderr.write('Error: ' + message + '\n');
38
+ process.exit(1);
39
+ }
40
+
41
+ // ── Argument parsing ─────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Parse --key value pairs from an args array.
45
+ * Skips --raw (handled globally by router).
46
+ * @param {string[]} args - Array of CLI arguments
47
+ * @returns {{ positional: string[], named: object }}
48
+ */
49
+ function parseNamedArgs(args) {
50
+ const positional = [];
51
+ const named = {};
52
+ let i = 0;
53
+ while (i < args.length) {
54
+ const arg = args[i];
55
+ if (arg === '--raw') {
56
+ i++;
57
+ continue;
58
+ }
59
+ if (arg.startsWith('--') && i + 1 < args.length) {
60
+ const key = arg.slice(2);
61
+ named[key] = args[i + 1];
62
+ i += 2;
63
+ } else {
64
+ positional.push(arg);
65
+ i++;
66
+ }
67
+ }
68
+ return { positional, named };
69
+ }
70
+
71
+ // ── File helpers ─────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Read a file safely. Returns null if file does not exist.
75
+ * @param {string} filePath - Absolute or relative path
76
+ * @returns {string|null}
77
+ */
78
+ function safeReadFile(filePath) {
79
+ try {
80
+ return fs.readFileSync(filePath, 'utf-8');
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Write a file safely. Creates parent directories if needed.
88
+ * @param {string} filePath - Absolute or relative path
89
+ * @param {string} content - File content
90
+ */
91
+ function safeWriteFile(filePath, content) {
92
+ const dir = path.dirname(filePath);
93
+ fs.mkdirSync(dir, { recursive: true });
94
+ fs.writeFileSync(filePath, content, 'utf-8');
95
+ }
96
+
97
+ // ── Frontmatter helpers ──────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Parse YAML frontmatter from markdown content.
101
+ * Handles simple key: value pairs (no nested objects needed for STATE.md).
102
+ * Returns empty frontmatter object if no frontmatter found.
103
+ * @param {string} content - Markdown content with optional frontmatter
104
+ * @returns {{ frontmatter: object, body: string }}
105
+ */
106
+ function parseFrontmatter(content) {
107
+ if (!content || !content.startsWith('---')) {
108
+ return { frontmatter: {}, body: content || '' };
109
+ }
110
+ // Normalize line endings before parsing (handles Windows \r\n)
111
+ const normalized = content.replace(/\r\n/g, '\n');
112
+ // Match the closing --- only when it occupies a full line (followed by \n or end-of-string)
113
+ const endMatch = normalized.substring(3).search(/\n---(\n|$)/);
114
+ if (endMatch === -1) {
115
+ return { frontmatter: {}, body: content };
116
+ }
117
+ const endIndex = endMatch + 3; // adjust for the substring(3) offset
118
+ const fmBlock = normalized.substring(4, endIndex).trim();
119
+ const body = normalized.substring(endIndex + 4).trimStart();
120
+ const frontmatter = {};
121
+ for (const line of fmBlock.split('\n')) {
122
+ const colonIndex = line.indexOf(':');
123
+ if (colonIndex === -1) continue;
124
+ const key = line.substring(0, colonIndex).trim();
125
+ let value = line.substring(colonIndex + 1).trim();
126
+ // Strip surrounding quotes and unescape escaped quotes for round-trip safety
127
+ if (value.startsWith('"') && value.endsWith('"')) {
128
+ value = value.slice(1, -1).replace(/\\"/g, '"');
129
+ } else if (value.startsWith("'") && value.endsWith("'")) {
130
+ value = value.slice(1, -1);
131
+ }
132
+ frontmatter[key] = value;
133
+ }
134
+ return { frontmatter, body };
135
+ }
136
+
137
+ /**
138
+ * Serialize frontmatter object and body back to markdown string.
139
+ * @param {object} data - Frontmatter key-value pairs
140
+ * @param {string} body - Markdown body
141
+ * @returns {string}
142
+ */
143
+ function serializeFrontmatter(data, body) {
144
+ const lines = ['---'];
145
+ for (const [key, value] of Object.entries(data)) {
146
+ const strVal = String(value);
147
+ // Quote values that contain special YAML characters
148
+ if (strVal.includes(':') || strVal.includes('#') || strVal.includes('"') ||
149
+ strVal.includes('\n') || strVal.includes('\r') ||
150
+ strVal.startsWith(' ') || strVal.endsWith(' ')) {
151
+ lines.push(`${key}: "${strVal.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`);
152
+ } else {
153
+ lines.push(`${key}: ${strVal}`);
154
+ }
155
+ }
156
+ lines.push('---');
157
+ lines.push('');
158
+ if (body) {
159
+ lines.push(body);
160
+ }
161
+ return lines.join('\n');
162
+ }
163
+
164
+ module.exports = {
165
+ output,
166
+ error,
167
+ parseNamedArgs,
168
+ safeReadFile,
169
+ safeWriteFile,
170
+ parseFrontmatter,
171
+ serializeFrontmatter,
172
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Deviation -- Append-only deviation log operations for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins: path, fs.
5
+ * Depends on: ./core.cjs for output, error, safeReadFile, safeWriteFile
6
+ *
7
+ * Exports: cmdDeviationAppend
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const {
15
+ output,
16
+ error,
17
+ safeReadFile,
18
+ safeWriteFile,
19
+ } = require('./core.cjs');
20
+
21
+ // Allowlists for validation
22
+ const ALLOWED_GATES = new Set([
23
+ 'positioning_drift', 'claim_accuracy', 'voice_drift',
24
+ 'outcome_alignment', 'funnel_integrity', 'utm_hygiene',
25
+ 'compliance', 'competitor_collision', 'icp_fit',
26
+ 'format_correctness',
27
+ ]);
28
+
29
+ const ALLOWED_RESULTS = new Set(['accepted', 'correct', 'escalated']);
30
+
31
+ /**
32
+ * Sanitize justification text to prevent injection.
33
+ * Strips backticks, all $ signs, newlines, pipe chars, and redirects.
34
+ * @param {string} text - Raw justification
35
+ * @returns {string} Sanitized text (max 100 chars)
36
+ */
37
+ function sanitizeJustification(text) {
38
+ if (!text) return '';
39
+ return text
40
+ .replace(/`/g, "'")
41
+ .replace(/\$/g, '')
42
+ .replace(/\n/g, ' ')
43
+ .replace(/\r/g, '')
44
+ .replace(/\|/g, '-')
45
+ .replace(/[<>]/g, '')
46
+ .substring(0, 100)
47
+ .trim();
48
+ }
49
+
50
+ /**
51
+ * Append a deviation entry to a campaign's DEVIATIONS.md.
52
+ *
53
+ * @param {string} slug - Campaign slug
54
+ * @param {string} gate - Gate name (must be in ALLOWED_GATES)
55
+ * @param {string} result - Deviation result (accepted, correct, escalated)
56
+ * @param {string} justification - User justification text
57
+ * @param {string} asset - Asset filename
58
+ * @param {boolean} raw - Whether to output raw string
59
+ * @param {object} [extra] - Additional named fields: gate_id, tier, finding, action, run
60
+ */
61
+ function cmdDeviationAppend(slug, gate, result, justification, asset, raw, extra) {
62
+ if (!slug || !slug.trim()) error('slug required for deviation append');
63
+ if (!gate || !gate.trim()) error('gate required for deviation append');
64
+ if (!result || !result.trim()) error('result required for deviation append');
65
+ if (!asset || !asset.trim()) error('asset required for deviation append');
66
+
67
+ // Validate slug via path.resolve to prevent traversal
68
+ const safe = slug.toLowerCase().replace(/[^a-z0-9-]/g, '');
69
+ const projectRoot = path.resolve(process.cwd());
70
+ const campaignDir = path.resolve(projectRoot, '.marketing', 'CAMPAIGNS', safe);
71
+ if (!campaignDir.startsWith(projectRoot)) {
72
+ error('campaign path escapes project directory');
73
+ }
74
+
75
+ // Validate gate name against allowlist
76
+ const gateLower = gate.toLowerCase().trim();
77
+ if (!ALLOWED_GATES.has(gateLower)) {
78
+ error(`Unknown gate: ${gate}. Allowed: ${[...ALLOWED_GATES].join(', ')}`);
79
+ }
80
+
81
+ // Validate result against allowlist
82
+ const resultLower = result.toLowerCase().trim();
83
+ if (!ALLOWED_RESULTS.has(resultLower)) {
84
+ error(`Unknown result: ${result}. Allowed: ${[...ALLOWED_RESULTS].join(', ')}`);
85
+ }
86
+
87
+ const deviationsPath = path.resolve(campaignDir, 'DEVIATIONS.md');
88
+ const safeAsset = asset.replace(/\|/g, '-').replace(/\n/g, '').substring(0, 80);
89
+ const safeJustification = sanitizeJustification(justification || '');
90
+ const timestamp = new Date().toISOString();
91
+
92
+ // Atomically create DEVIATIONS.md if it does not exist (prevents TOCTOU race)
93
+ const templatePath = path.resolve(__dirname, '..', '..', 'templates', 'deviation-log.md');
94
+ const template = safeReadFile(templatePath);
95
+ const initialContent = template
96
+ ? template.replace(/\[SLUG\]/g, safe).replace(/\[ISO_TIMESTAMP\]/g, timestamp)
97
+ : [
98
+ '# Deviation Log',
99
+ '',
100
+ `**Campaign:** ${safe}`,
101
+ `**Created:** ${timestamp}`,
102
+ '',
103
+ '| Timestamp | Gate | Tier | Result | Asset | Finding | Action | Justification | Verify Run |',
104
+ '|-----------|------|------|--------|-------|---------|--------|---------------|------------|',
105
+ '<!-- NEW ENTRIES BELOW THIS LINE -->',
106
+ ].join('\n');
107
+ try {
108
+ fs.writeFileSync(deviationsPath, initialContent, { flag: 'wx', encoding: 'utf-8' });
109
+ } catch (e) {
110
+ if (e.code !== 'EEXIST') throw e;
111
+ // File already exists -- proceed to append
112
+ }
113
+
114
+ // Read current content and append new entry
115
+ const content = safeReadFile(deviationsPath);
116
+ if (content === null) {
117
+ error(`Failed to read DEVIATIONS.md for campaign: ${safe}`);
118
+ }
119
+
120
+ // Build table row entry
121
+ const opts = extra || {};
122
+ const tierMap = {
123
+ positioning_drift: 'T1', claim_accuracy: 'T1', voice_drift: 'T2',
124
+ outcome_alignment: 'T1', funnel_integrity: 'T2', utm_hygiene: 'T2',
125
+ compliance: 'T2', competitor_collision: 'T2', icp_fit: 'T2',
126
+ format_correctness: 'T2',
127
+ };
128
+ const tier = (opts.tier || tierMap[gateLower] || 'T2').toString().substring(0, 10);
129
+ const actionMap = { accepted: 'Accept+log', correct: 'Correct', escalated: 'Escalate' };
130
+ const action = sanitizeJustification(opts.action || '') || actionMap[resultLower] || resultLower;
131
+ const finding = sanitizeJustification(opts.finding || '') || '--';
132
+ const run = (opts.run || '--').toString().replace(/\|/g, '-').substring(0, 20);
133
+
134
+ const newRow = `| ${timestamp} | ${gateLower} | ${tier} | ${resultLower} | ${safeAsset} | ${finding} | ${action} | ${safeJustification} | ${run} |`;
135
+
136
+ // Append after the last line of content
137
+ const updated = content.trimEnd() + '\n' + newRow + '\n';
138
+ safeWriteFile(deviationsPath, updated);
139
+
140
+ output(
141
+ { appended: true, gate: gateLower, result: resultLower, asset: safeAsset, timestamp },
142
+ raw,
143
+ `appended ${gateLower}=${resultLower}`
144
+ );
145
+ }
146
+
147
+ module.exports = {
148
+ cmdDeviationAppend,
149
+ };
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Drift Log -- Append-only DRIFT-LOG.md operations for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins: path, fs.
5
+ * Depends on: ./core.cjs for output, error, safeReadFile, safeWriteFile
6
+ *
7
+ * Exports: cmdDriftLogAppend, cmdDriftLogDeprecation
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const {
15
+ output,
16
+ error,
17
+ safeReadFile,
18
+ safeWriteFile,
19
+ } = require('./core.cjs');
20
+
21
+ // Allowlist for event types
22
+ const ALLOWED_EVENT_TYPES = new Set(['shift', 'audit', 'deviation']);
23
+
24
+ /**
25
+ * Sanitize details text to prevent injection.
26
+ * Strips backticks, all $ signs, newlines, pipe chars, and redirects.
27
+ * @param {string} text - Raw details text
28
+ * @returns {string} Sanitized text (max 200 chars)
29
+ */
30
+ function sanitizeDetails(text) {
31
+ if (!text) return '';
32
+ return text
33
+ .replace(/`/g, "'")
34
+ .replace(/\$/g, '')
35
+ .replace(/\n/g, ' ')
36
+ .replace(/\r/g, '')
37
+ .replace(/\|/g, '-')
38
+ .replace(/[<>]/g, '')
39
+ .substring(0, 200)
40
+ .trim();
41
+ }
42
+
43
+ /**
44
+ * Resolve the DRIFT-LOG.md path and ensure it is inside the project root.
45
+ * @returns {{ driftLogPath: string, projectRoot: string }}
46
+ */
47
+ function resolveDriftLogPath() {
48
+ const projectRoot = path.resolve(process.cwd());
49
+ const driftLogPath = path.resolve(process.cwd(), '.marketing', 'DRIFT-LOG.md');
50
+ if (!driftLogPath.startsWith(projectRoot)) {
51
+ error('DRIFT-LOG.md path escapes project directory');
52
+ }
53
+ return { driftLogPath, projectRoot };
54
+ }
55
+
56
+ /**
57
+ * Ensure DRIFT-LOG.md exists using TOCTOU-safe atomic creation.
58
+ * @param {string} driftLogPath - Absolute path to DRIFT-LOG.md
59
+ */
60
+ function ensureDriftLog(driftLogPath) {
61
+ const timestamp = new Date().toISOString();
62
+ const templatePath = path.resolve(__dirname, '..', '..', 'templates', 'drift-log.md');
63
+ const template = safeReadFile(templatePath);
64
+ const initialContent = template
65
+ ? template.replace(/\[ISO_TIMESTAMP\]/g, timestamp)
66
+ : [
67
+ '# Positioning Drift Log',
68
+ '',
69
+ `**Created:** ${timestamp}`,
70
+ '',
71
+ 'This file is **append-only**.',
72
+ '',
73
+ '## Audit Trail',
74
+ '',
75
+ '| Date | Event | Source | Details | Assets Affected |',
76
+ '|------|-------|--------|---------|-----------------|',
77
+ '<!-- NEW ENTRIES BELOW THIS LINE -->',
78
+ '',
79
+ '## Deprecation Backlog',
80
+ '',
81
+ '| Asset | Campaign | Old Positioning Element | Required Update | Deadline | Status |',
82
+ '|-------|----------|------------------------|-----------------|----------|--------|',
83
+ '<!-- DEPRECATION ENTRIES BELOW THIS LINE -->',
84
+ ].join('\n');
85
+
86
+ // Ensure .marketing directory exists
87
+ const dir = path.dirname(driftLogPath);
88
+ fs.mkdirSync(dir, { recursive: true });
89
+
90
+ try {
91
+ fs.writeFileSync(driftLogPath, initialContent, { flag: 'wx', encoding: 'utf-8' });
92
+ } catch (e) {
93
+ if (e.code !== 'EEXIST') throw e;
94
+ // File already exists -- proceed to append
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Append a drift event entry to .marketing/DRIFT-LOG.md.
100
+ *
101
+ * @param {string} eventType - Event type (shift, audit, deviation)
102
+ * @param {string} source - Source command or campaign that triggered the event
103
+ * @param {string} details - Details/summary text
104
+ * @param {string|number} affectedCount - Number of assets affected
105
+ * @param {boolean} raw - Whether to output raw string
106
+ */
107
+ function cmdDriftLogAppend(eventType, source, details, affectedCount, raw) {
108
+ if (!eventType || !eventType.trim()) error('event-type required for drift-log append');
109
+ if (!source || !source.trim()) error('source required for drift-log append');
110
+
111
+ // Validate event type against allowlist
112
+ const eventLower = eventType.toLowerCase().trim();
113
+ if (!ALLOWED_EVENT_TYPES.has(eventLower)) {
114
+ error(`Unknown event type: ${eventType}. Allowed: ${[...ALLOWED_EVENT_TYPES].join(', ')}`);
115
+ }
116
+
117
+ // Sanitize inputs
118
+ const safeSource = source.replace(/\|/g, '-').replace(/\n/g, '').replace(/[<>]/g, '').substring(0, 80);
119
+ const safeDetails = sanitizeDetails(details || '');
120
+ const safeAffected = parseInt(affectedCount, 10) || 0;
121
+
122
+ const { driftLogPath } = resolveDriftLogPath();
123
+
124
+ // TOCTOU defense: create if not exists
125
+ ensureDriftLog(driftLogPath);
126
+
127
+ // Read current content and append new entry
128
+ const content = safeReadFile(driftLogPath);
129
+ if (content === null) {
130
+ error('Failed to read DRIFT-LOG.md');
131
+ }
132
+
133
+ const timestamp = new Date().toISOString();
134
+ const newRow = `| ${timestamp} | ${eventLower} | ${safeSource} | ${safeDetails} | ${safeAffected} |`;
135
+
136
+ // Find the Audit Trail marker and append after it
137
+ const marker = '<!-- NEW ENTRIES BELOW THIS LINE -->';
138
+ let updated;
139
+ const markerCount = content.split(marker).length - 1;
140
+ if (markerCount > 1) {
141
+ error(`DRIFT-LOG.md has ${markerCount} occurrences of the Audit Trail marker. File may be corrupted.`);
142
+ }
143
+ if (markerCount === 1) {
144
+ updated = content.replace(marker, marker + '\n' + newRow);
145
+ } else {
146
+ // Fallback: append at end of content
147
+ updated = content.trimEnd() + '\n' + newRow + '\n';
148
+ }
149
+
150
+ safeWriteFile(driftLogPath, updated);
151
+
152
+ output(
153
+ { appended: true, event_type: eventLower, source: safeSource, timestamp },
154
+ raw,
155
+ 'appended ' + eventLower + '=' + safeSource
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Append a deprecation entry to .marketing/DRIFT-LOG.md Deprecation Backlog.
161
+ *
162
+ * @param {string} asset - Asset identifier
163
+ * @param {string} campaign - Campaign slug
164
+ * @param {string} oldElement - Old positioning element being deprecated
165
+ * @param {string} requiredUpdate - Description of required update
166
+ * @param {string} deadline - Deadline for the update (ISO date or description)
167
+ * @param {boolean} raw - Whether to output raw string
168
+ */
169
+ function cmdDriftLogDeprecation(asset, campaign, oldElement, requiredUpdate, deadline, raw) {
170
+ if (!asset || !asset.trim()) error('asset required for drift-log deprecation');
171
+ if (!campaign || !campaign.trim()) error('campaign required for drift-log deprecation');
172
+
173
+ // Sanitize all inputs
174
+ const safeAsset = (asset || '').replace(/\|/g, '-').replace(/\n/g, '').replace(/[<>]/g, '').substring(0, 80);
175
+ const safeCampaign = (campaign || '').replace(/\|/g, '-').replace(/\n/g, '').replace(/[<>]/g, '').substring(0, 80);
176
+ const safeOldElement = sanitizeDetails(oldElement || '');
177
+ const safeUpdate = sanitizeDetails(requiredUpdate || '');
178
+ const safeDeadline = (deadline || '').replace(/\|/g, '-').replace(/\n/g, '').replace(/[<>]/g, '').substring(0, 30);
179
+
180
+ const { driftLogPath } = resolveDriftLogPath();
181
+
182
+ // TOCTOU defense: create if not exists
183
+ ensureDriftLog(driftLogPath);
184
+
185
+ // Read current content and append deprecation entry
186
+ const content = safeReadFile(driftLogPath);
187
+ if (content === null) {
188
+ error('Failed to read DRIFT-LOG.md');
189
+ }
190
+
191
+ const newRow = `| ${safeAsset} | ${safeCampaign} | ${safeOldElement} | ${safeUpdate} | ${safeDeadline} | pending |`;
192
+
193
+ // Find the Deprecation Backlog marker and append after it
194
+ const marker = '<!-- DEPRECATION ENTRIES BELOW THIS LINE -->';
195
+ let updated;
196
+ const markerCount = content.split(marker).length - 1;
197
+ if (markerCount > 1) {
198
+ error(`DRIFT-LOG.md has ${markerCount} occurrences of the Deprecation marker. File may be corrupted.`);
199
+ }
200
+ if (markerCount === 1) {
201
+ updated = content.replace(marker, marker + '\n' + newRow);
202
+ } else {
203
+ // Fallback: append at end of content
204
+ updated = content.trimEnd() + '\n' + newRow + '\n';
205
+ }
206
+
207
+ safeWriteFile(driftLogPath, updated);
208
+
209
+ output(
210
+ { appended: true, asset: safeAsset, deadline: safeDeadline },
211
+ raw,
212
+ 'deprecation added'
213
+ );
214
+ }
215
+
216
+ module.exports = {
217
+ cmdDriftLogAppend,
218
+ cmdDriftLogDeprecation,
219
+ };