aads-cli 1.0.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +159 -0
  3. package/README.md +159 -0
  4. package/data/campaign-layer-policy.json +68 -0
  5. package/dist/analysis/anomaly-detection.d.ts +16 -0
  6. package/dist/analysis/anomaly-detection.d.ts.map +1 -0
  7. package/dist/analysis/anomaly-detection.js +55 -0
  8. package/dist/analysis/anomaly-detection.js.map +1 -0
  9. package/dist/analysis/auto-to-manual.d.ts +11 -0
  10. package/dist/analysis/auto-to-manual.d.ts.map +1 -0
  11. package/dist/analysis/auto-to-manual.js +57 -0
  12. package/dist/analysis/auto-to-manual.js.map +1 -0
  13. package/dist/analysis/campaign-layer-classifier.d.ts +10 -0
  14. package/dist/analysis/campaign-layer-classifier.d.ts.map +1 -0
  15. package/dist/analysis/campaign-layer-classifier.js +50 -0
  16. package/dist/analysis/campaign-layer-classifier.js.map +1 -0
  17. package/dist/analysis/campaign-structure.d.ts +3 -0
  18. package/dist/analysis/campaign-structure.d.ts.map +1 -0
  19. package/dist/analysis/campaign-structure.js +51 -0
  20. package/dist/analysis/campaign-structure.js.map +1 -0
  21. package/dist/analysis/cpc-optimizer.d.ts +11 -0
  22. package/dist/analysis/cpc-optimizer.d.ts.map +1 -0
  23. package/dist/analysis/cpc-optimizer.js +46 -0
  24. package/dist/analysis/cpc-optimizer.js.map +1 -0
  25. package/dist/analysis/performance-analyzer.d.ts +3 -0
  26. package/dist/analysis/performance-analyzer.d.ts.map +1 -0
  27. package/dist/analysis/performance-analyzer.js +58 -0
  28. package/dist/analysis/performance-analyzer.js.map +1 -0
  29. package/dist/analysis/sku-classifier.d.ts +3 -0
  30. package/dist/analysis/sku-classifier.d.ts.map +1 -0
  31. package/dist/analysis/sku-classifier.js +64 -0
  32. package/dist/analysis/sku-classifier.js.map +1 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +351 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config/campaign-layer-policy.d.ts +28 -0
  38. package/dist/config/campaign-layer-policy.d.ts.map +1 -0
  39. package/dist/config/campaign-layer-policy.js +56 -0
  40. package/dist/config/campaign-layer-policy.js.map +1 -0
  41. package/dist/config/constants.d.ts +74 -0
  42. package/dist/config/constants.d.ts.map +1 -0
  43. package/dist/config/constants.js +61 -0
  44. package/dist/config/constants.js.map +1 -0
  45. package/dist/config/optimisation-config.d.ts +24 -0
  46. package/dist/config/optimisation-config.d.ts.map +1 -0
  47. package/dist/config/optimisation-config.js +41 -0
  48. package/dist/config/optimisation-config.js.map +1 -0
  49. package/dist/core/header-mapper.d.ts +6 -0
  50. package/dist/core/header-mapper.d.ts.map +1 -0
  51. package/dist/core/header-mapper.js +34 -0
  52. package/dist/core/header-mapper.js.map +1 -0
  53. package/dist/core/normalizer.d.ts +9 -0
  54. package/dist/core/normalizer.d.ts.map +1 -0
  55. package/dist/core/normalizer.js +76 -0
  56. package/dist/core/normalizer.js.map +1 -0
  57. package/dist/io/csv-reader.d.ts +3 -0
  58. package/dist/io/csv-reader.d.ts.map +1 -0
  59. package/dist/io/csv-reader.js +52 -0
  60. package/dist/io/csv-reader.js.map +1 -0
  61. package/dist/io/excel-reader.d.ts +4 -0
  62. package/dist/io/excel-reader.d.ts.map +1 -0
  63. package/dist/io/excel-reader.js +83 -0
  64. package/dist/io/excel-reader.js.map +1 -0
  65. package/dist/io/excel-writer.d.ts +7 -0
  66. package/dist/io/excel-writer.d.ts.map +1 -0
  67. package/dist/io/excel-writer.js +42 -0
  68. package/dist/io/excel-writer.js.map +1 -0
  69. package/dist/pipeline/analyze-pipeline.d.ts +8 -0
  70. package/dist/pipeline/analyze-pipeline.d.ts.map +1 -0
  71. package/dist/pipeline/analyze-pipeline.js +145 -0
  72. package/dist/pipeline/analyze-pipeline.js.map +1 -0
  73. package/dist/pipeline/types.d.ts +142 -0
  74. package/dist/pipeline/types.d.ts.map +1 -0
  75. package/dist/pipeline/types.js +2 -0
  76. package/dist/pipeline/types.js.map +1 -0
  77. package/dist/ranking/keyword-matcher.d.ts +10 -0
  78. package/dist/ranking/keyword-matcher.d.ts.map +1 -0
  79. package/dist/ranking/keyword-matcher.js +62 -0
  80. package/dist/ranking/keyword-matcher.js.map +1 -0
  81. package/dist/ranking/ranking-db.d.ts +14 -0
  82. package/dist/ranking/ranking-db.d.ts.map +1 -0
  83. package/dist/ranking/ranking-db.js +97 -0
  84. package/dist/ranking/ranking-db.js.map +1 -0
  85. package/dist/ranking/seo-factor.d.ts +5 -0
  86. package/dist/ranking/seo-factor.d.ts.map +1 -0
  87. package/dist/ranking/seo-factor.js +57 -0
  88. package/dist/ranking/seo-factor.js.map +1 -0
  89. package/dist/ranking/types.d.ts +25 -0
  90. package/dist/ranking/types.d.ts.map +1 -0
  91. package/dist/ranking/types.js +2 -0
  92. package/dist/ranking/types.js.map +1 -0
  93. package/dist/schemas/types.d.ts +32 -0
  94. package/dist/schemas/types.d.ts.map +1 -0
  95. package/dist/schemas/types.js +2 -0
  96. package/dist/schemas/types.js.map +1 -0
  97. package/dist/utils/date-utils.d.ts +17 -0
  98. package/dist/utils/date-utils.d.ts.map +1 -0
  99. package/dist/utils/date-utils.js +56 -0
  100. package/dist/utils/date-utils.js.map +1 -0
  101. package/dist/utils/logger.d.ts +12 -0
  102. package/dist/utils/logger.d.ts.map +1 -0
  103. package/dist/utils/logger.js +45 -0
  104. package/dist/utils/logger.js.map +1 -0
  105. package/package.json +65 -0
@@ -0,0 +1,51 @@
1
+ import { safeDivide } from "../core/normalizer.js";
2
+ export const buildCampaignStructure = (records) => {
3
+ const campaigns = new Map();
4
+ for (const record of records) {
5
+ const campaignKey = record.campaignId || record.campaignName;
6
+ if (!campaignKey)
7
+ continue;
8
+ const campaign = campaigns.get(campaignKey) ??
9
+ {
10
+ campaignId: record.campaignId,
11
+ campaignName: record.campaignName,
12
+ adGroups: new Map(),
13
+ };
14
+ const adGroupKey = record.adGroupId || record.adGroupName || "__unknown__";
15
+ const adGroup = campaign.adGroups.get(adGroupKey) ??
16
+ {
17
+ adGroupId: record.adGroupId,
18
+ adGroupName: record.adGroupName,
19
+ keywordCount: 0,
20
+ productTargetCount: 0,
21
+ spend: 0,
22
+ sales: 0,
23
+ };
24
+ if (record.keywordText)
25
+ adGroup.keywordCount += 1;
26
+ if (record.productTargetingExpression)
27
+ adGroup.productTargetCount += 1;
28
+ adGroup.spend += record.spend;
29
+ adGroup.sales += record.sales;
30
+ campaign.adGroups.set(adGroupKey, adGroup);
31
+ campaigns.set(campaignKey, campaign);
32
+ }
33
+ return [...campaigns.values()]
34
+ .map((campaign) => ({
35
+ campaignId: campaign.campaignId,
36
+ campaignName: campaign.campaignName,
37
+ adGroups: [...campaign.adGroups.values()]
38
+ .map((adGroup) => ({
39
+ adGroupId: adGroup.adGroupId,
40
+ adGroupName: adGroup.adGroupName,
41
+ keywordCount: adGroup.keywordCount,
42
+ productTargetCount: adGroup.productTargetCount,
43
+ spend: adGroup.spend,
44
+ sales: adGroup.sales,
45
+ acos: safeDivide(adGroup.spend, adGroup.sales),
46
+ }))
47
+ .sort((a, b) => b.spend - a.spend),
48
+ }))
49
+ .sort((a, b) => a.campaignName.localeCompare(b.campaignName));
50
+ };
51
+ //# sourceMappingURL=campaign-structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-structure.js","sourceRoot":"","sources":["../../src/analysis/campaign-structure.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAiBnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAA2B,EAA2B,EAAE;IAC7F,MAAM,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;IAEzD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,YAAY,CAAC;QAC7D,IAAI,CAAC,WAAW;YAAE,SAAS;QAE3B,MAAM,QAAQ,GACZ,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC;YACzB;gBACC,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,QAAQ,EAAE,IAAI,GAAG,EAA8B;aACjB,CAAC;QAEnC,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,WAAW,IAAI,aAAa,CAAC;QAC3E,MAAM,OAAO,GACX,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;YAChC;gBACC,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,YAAY,EAAE,CAAC;gBACf,kBAAkB,EAAE,CAAC;gBACrB,KAAK,EAAE,CAAC;gBACR,KAAK,EAAE,CAAC;aACqB,CAAC;QAElC,IAAI,MAAM,CAAC,WAAW;YAAE,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC;QAClD,IAAI,MAAM,CAAC,0BAA0B;YAAE,OAAO,CAAC,kBAAkB,IAAI,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;QAC9B,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;QAE9B,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3C,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;SAC3B,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,YAAY,EAAE,QAAQ,CAAC,YAAY;QACnC,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aACtC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACjB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,kBAAkB,EAAE,OAAO,CAAC,kBAAkB;YAC9C,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC;SAC/C,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;KACrC,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;AAClE,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { CpcRecommendation, NormalizedRecord, SkuClassification } from "../pipeline/types.js";
2
+ import type { SeoRankingData } from "../ranking/types.js";
3
+ import type { OptimisationConfig } from "../config/optimisation-config.js";
4
+ export interface CpcOptimizerOptions {
5
+ minClicks: number;
6
+ targetAcos: number;
7
+ seoRankingData?: SeoRankingData;
8
+ seoConfig?: OptimisationConfig["seo"];
9
+ }
10
+ export declare const generateCpcRecommendations: (records: NormalizedRecord[], skuClassifications: SkuClassification[], options: CpcOptimizerOptions) => CpcRecommendation[];
11
+ //# sourceMappingURL=cpc-optimizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpc-optimizer.d.ts","sourceRoot":"","sources":["../../src/analysis/cpc-optimizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAI3E,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,0BAA0B,GACrC,SAAS,gBAAgB,EAAE,EAC3B,oBAAoB,iBAAiB,EAAE,EACvC,SAAS,mBAAmB,KAC3B,iBAAiB,EA8CnB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { normalizeMatchType, safeDivide } from "../core/normalizer.js";
2
+ import { applySeoAdjustment } from "../ranking/seo-factor.js";
3
+ export const generateCpcRecommendations = (records, skuClassifications, options) => {
4
+ const skuAdjust = new Map();
5
+ skuClassifications.forEach((item) => skuAdjust.set(item.sku, item.bidAdjust));
6
+ const result = [];
7
+ for (const record of records) {
8
+ if (!record.keywordText && !record.productTargetingExpression)
9
+ continue;
10
+ if (record.clicks < options.minClicks)
11
+ continue;
12
+ const avgCpc = safeDivide(record.spend, record.clicks);
13
+ const acos = safeDivide(record.spend, record.sales);
14
+ const acosFactor = acos > 0 ? options.targetAcos / acos : 1;
15
+ const skuFactor = skuAdjust.get(record.sku) ?? 1;
16
+ const currentBid = record.bid === "" ? avgCpc : Number(record.bid);
17
+ const recommendedBid = Math.max(1, Math.round(currentBid * acosFactor * skuFactor));
18
+ result.push({
19
+ campaignId: record.campaignId,
20
+ campaignName: record.campaignName,
21
+ adGroupId: record.adGroupId,
22
+ adGroupName: record.adGroupName,
23
+ keywordId: record.keywordId,
24
+ keywordText: record.keywordText || record.productTargetingExpression,
25
+ matchType: normalizeMatchType(record.matchType),
26
+ sku: record.sku,
27
+ clicks: record.clicks,
28
+ avgCpc,
29
+ currentBid,
30
+ recommendedBid,
31
+ bidAdjust: skuFactor,
32
+ reason: `targetAcos=${options.targetAcos.toFixed(2)} skuFactor=${skuFactor.toFixed(2)}`,
33
+ });
34
+ }
35
+ const sorted = result.sort((a, b) => b.clicks - a.clicks);
36
+ // Apply SEO adjustment if ranking data is available
37
+ if (options.seoRankingData && options.seoConfig?.enabled) {
38
+ return applySeoAdjustment(sorted, options.seoRankingData, {
39
+ enabled: options.seoConfig.enabled,
40
+ seoFactors: options.seoConfig.factors,
41
+ cpcCeiling: options.seoConfig.cpcCeiling,
42
+ });
43
+ }
44
+ return sorted;
45
+ };
46
+ //# sourceMappingURL=cpc-optimizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpc-optimizer.js","sourceRoot":"","sources":["../../src/analysis/cpc-optimizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CampaignMetrics, NormalizedRecord } from "../pipeline/types.js";
2
+ export declare const analyzePerformance: (records: NormalizedRecord[]) => CampaignMetrics[];
3
+ //# sourceMappingURL=performance-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-analyzer.d.ts","sourceRoot":"","sources":["../../src/analysis/performance-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAiB9E,eAAO,MAAM,kBAAkB,GAAI,SAAS,gBAAgB,EAAE,KAAG,eAAe,EA8D/E,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { safeDivide } from "../core/normalizer.js";
2
+ export const analyzePerformance = (records) => {
3
+ const campaigns = new Map();
4
+ for (const record of records) {
5
+ const key = record.campaignId || record.campaignName;
6
+ if (!key)
7
+ continue;
8
+ const current = campaigns.get(key) ??
9
+ {
10
+ campaignId: record.campaignId,
11
+ campaignName: record.campaignName,
12
+ targetingType: record.targetingType,
13
+ state: record.state,
14
+ clicks: 0,
15
+ impressions: 0,
16
+ spend: 0,
17
+ sales: 0,
18
+ orders: 0,
19
+ budget: 0,
20
+ budgetCount: 0,
21
+ };
22
+ current.clicks += record.clicks;
23
+ current.impressions += record.impressions;
24
+ current.spend += record.spend;
25
+ current.sales += record.sales;
26
+ current.orders += record.orders;
27
+ if (record.dailyBudget !== "") {
28
+ current.budget += record.dailyBudget;
29
+ current.budgetCount += 1;
30
+ }
31
+ campaigns.set(key, current);
32
+ }
33
+ return [...campaigns.values()]
34
+ .map((item) => {
35
+ const ctr = safeDivide(item.clicks, item.impressions);
36
+ const cvr = safeDivide(item.orders, item.clicks);
37
+ const acos = safeDivide(item.spend, item.sales);
38
+ const roas = safeDivide(item.sales, item.spend);
39
+ return {
40
+ campaignId: item.campaignId,
41
+ campaignName: item.campaignName,
42
+ targetingType: item.targetingType,
43
+ state: item.state,
44
+ clicks: item.clicks,
45
+ impressions: item.impressions,
46
+ spend: item.spend,
47
+ sales: item.sales,
48
+ orders: item.orders,
49
+ ctr,
50
+ cvr,
51
+ acos,
52
+ roas,
53
+ dailyBudget: item.budgetCount > 0 ? item.budget / item.budgetCount : "",
54
+ };
55
+ })
56
+ .sort((a, b) => b.sales - a.sales);
57
+ };
58
+ //# sourceMappingURL=performance-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-analyzer.js","sourceRoot":"","sources":["../../src/analysis/performance-analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { NormalizedRecord, SkuClassification } from "../pipeline/types.js";
2
+ export declare const classifySkus: (records: NormalizedRecord[]) => SkuClassification[];
3
+ //# sourceMappingURL=sku-classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sku-classifier.d.ts","sourceRoot":"","sources":["../../src/analysis/sku-classifier.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA+DhF,eAAO,MAAM,YAAY,GAAI,SAAS,gBAAgB,EAAE,KAAG,iBAAiB,EAa3E,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { SKU_LABEL_RULES } from "../config/constants.js";
2
+ import { safeDivide } from "../core/normalizer.js";
3
+ const classifyOne = (sku, agg) => {
4
+ const cvr = safeDivide(agg.orders, agg.clicks);
5
+ const acos = safeDivide(agg.spend, agg.sales);
6
+ if (agg.clicks < 5) {
7
+ return {
8
+ sku,
9
+ label: "improve",
10
+ bidAdjust: SKU_LABEL_RULES.improve.bidAdjust,
11
+ budgetAdjust: SKU_LABEL_RULES.improve.budgetAdjust,
12
+ reason: "data-insufficient(clicks<5)",
13
+ };
14
+ }
15
+ if (acos <= SKU_LABEL_RULES.focus.maxAcos && cvr >= SKU_LABEL_RULES.focus.minCvr) {
16
+ return {
17
+ sku,
18
+ label: "focus",
19
+ bidAdjust: SKU_LABEL_RULES.focus.bidAdjust,
20
+ budgetAdjust: SKU_LABEL_RULES.focus.budgetAdjust,
21
+ reason: "high-performance",
22
+ };
23
+ }
24
+ if (acos <= SKU_LABEL_RULES.nurture.maxAcos && cvr >= SKU_LABEL_RULES.nurture.minCvr) {
25
+ return {
26
+ sku,
27
+ label: "nurture",
28
+ bidAdjust: SKU_LABEL_RULES.nurture.bidAdjust,
29
+ budgetAdjust: SKU_LABEL_RULES.nurture.budgetAdjust,
30
+ reason: "stable-growth",
31
+ };
32
+ }
33
+ if (acos > SKU_LABEL_RULES.prune.minAcos || cvr < SKU_LABEL_RULES.prune.maxCvr) {
34
+ return {
35
+ sku,
36
+ label: "prune",
37
+ bidAdjust: SKU_LABEL_RULES.prune.bidAdjust,
38
+ budgetAdjust: SKU_LABEL_RULES.prune.budgetAdjust,
39
+ reason: "low-efficiency",
40
+ };
41
+ }
42
+ return {
43
+ sku,
44
+ label: "improve",
45
+ bidAdjust: SKU_LABEL_RULES.improve.bidAdjust,
46
+ budgetAdjust: SKU_LABEL_RULES.improve.budgetAdjust,
47
+ reason: "default",
48
+ };
49
+ };
50
+ export const classifySkus = (records) => {
51
+ const bySku = new Map();
52
+ for (const record of records) {
53
+ if (!record.sku)
54
+ continue;
55
+ const prev = bySku.get(record.sku) ?? { clicks: 0, spend: 0, sales: 0, orders: 0 };
56
+ prev.clicks += record.clicks;
57
+ prev.spend += record.spend;
58
+ prev.sales += record.sales;
59
+ prev.orders += record.orders;
60
+ bySku.set(record.sku, prev);
61
+ }
62
+ return [...bySku.entries()].map(([sku, agg]) => classifyOne(sku, agg)).sort((a, b) => a.sku.localeCompare(b.sku));
63
+ };
64
+ //# sourceMappingURL=sku-classifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sku-classifier.js","sourceRoot":"","sources":["../../src/analysis/sku-classifier.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC"}
package/dist/cli.js ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { Command } from "commander";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { loadCampaignLayerPolicy } from "./config/campaign-layer-policy.js";
7
+ import { loadOptimisationConfig } from "./config/optimisation-config.js";
8
+ import { Logger } from "./utils/logger.js";
9
+ import { runAnalyzePipeline } from "./pipeline/analyze-pipeline.js";
10
+ import { writeXlsx } from "./io/excel-writer.js";
11
+ import { timestampForFilename } from "./utils/date-utils.js";
12
+ const logger = new Logger(process.env.LOG_LEVEL === "debug" ? "debug" : "info");
13
+ const toPct = (value) => `${(value * 100).toFixed(2)}%`;
14
+ const printLayerSummary = async (result, layerPolicyPath) => {
15
+ if (!result.layerClassification || result.layerClassification.length === 0)
16
+ return;
17
+ const policy = await loadCampaignLayerPolicy(layerPolicyPath);
18
+ if (!policy)
19
+ return;
20
+ const LAYER_ORDER = ["L0", "L1", "L2", "L3", "L4"];
21
+ const metricsMap = new Map(result.campaignMetrics.map((m) => [m.campaignName, m]));
22
+ const layerAgg = LAYER_ORDER.map((layerId) => {
23
+ const layer = policy.layers[layerId];
24
+ const classified = result.layerClassification.filter((c) => c.layer === layerId);
25
+ let totalSpend = 0;
26
+ let totalSales = 0;
27
+ let allPaused = true;
28
+ for (const c of classified) {
29
+ const m = metricsMap.get(c.campaignName);
30
+ if (m) {
31
+ totalSpend += m.spend;
32
+ totalSales += m.sales;
33
+ if (m.state !== "paused")
34
+ allPaused = false;
35
+ }
36
+ }
37
+ const acos = totalSales > 0 ? totalSpend / totalSales : 0;
38
+ return {
39
+ layer: layerId,
40
+ name: layer.name,
41
+ campaigns: classified.length,
42
+ spend: totalSpend,
43
+ sales: totalSales,
44
+ acos,
45
+ paused: classified.length > 0 && allPaused,
46
+ };
47
+ });
48
+ const totalSpend = layerAgg.reduce((sum, l) => sum + l.spend, 0);
49
+ console.log("\nLayer Summary");
50
+ console.table(layerAgg.map((row) => ({
51
+ Layer: row.layer,
52
+ Name: row.name,
53
+ Campaigns: row.campaigns,
54
+ Spend: `¥${Math.round(row.spend).toLocaleString()}`,
55
+ Sales: `¥${Math.round(row.sales).toLocaleString()}`,
56
+ ACOS: row.sales > 0 ? toPct(row.acos) : "-",
57
+ "Budget%": totalSpend > 0 ? toPct(row.spend / totalSpend) : "-",
58
+ Note: row.paused ? "(paused)" : "",
59
+ })));
60
+ const lowConfidence = result.layerClassification.filter((c) => c.confidence === "low");
61
+ if (lowConfidence.length > 0) {
62
+ console.log(`\n⚠ ${lowConfidence.length} campaign(s) classified with low confidence (fallback):`);
63
+ for (const c of lowConfidence) {
64
+ console.log(` - ${c.campaignName} → ${c.layer}`);
65
+ }
66
+ }
67
+ };
68
+ const toOutputFormat = (value, fallback = "console") => {
69
+ const raw = String(value ?? fallback)
70
+ .trim()
71
+ .toLowerCase();
72
+ if (raw === "console" || raw === "json" || raw === "markdown" || raw === "xlsx") {
73
+ return raw;
74
+ }
75
+ return fallback;
76
+ };
77
+ const writeTextFile = async (filePath, content) => {
78
+ const resolved = path.resolve(filePath);
79
+ await mkdir(path.dirname(resolved), { recursive: true });
80
+ await writeFile(resolved, content, "utf8");
81
+ };
82
+ const program = new Command();
83
+ program
84
+ .name("aads")
85
+ .description("CLI tool for analyzing Amazon Ads Sponsored Products campaign performance")
86
+ .version("1.0.0");
87
+ program
88
+ .command("analyze")
89
+ .description("Analyze bulk sheet KPIs (CTR, CVR, ACOS, ROAS)")
90
+ .requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
91
+ .option("--layer-policy <file>", "Campaign layer policy JSON path")
92
+ .action(async (options) => {
93
+ const config = loadOptimisationConfig();
94
+ const result = await runAnalyzePipeline(options.input, config, {
95
+ layerPolicyPath: options.layerPolicy,
96
+ });
97
+ const totals = result.records.reduce((acc, row) => {
98
+ acc.clicks += row.clicks;
99
+ acc.impressions += row.impressions;
100
+ acc.spend += row.spend;
101
+ acc.sales += row.sales;
102
+ acc.orders += row.orders;
103
+ return acc;
104
+ }, { clicks: 0, impressions: 0, spend: 0, sales: 0, orders: 0 });
105
+ logger.info("Analyze completed", {
106
+ files: result.input.length,
107
+ rows: result.records.length,
108
+ period: result.dateRange
109
+ ? `${result.dateRange.startDate} ~ ${result.dateRange.endDate} (${result.dateRange.days}d)`
110
+ : "unknown",
111
+ campaigns: result.campaignMetrics.length,
112
+ clicks: totals.clicks,
113
+ impressions: totals.impressions,
114
+ spend: totals.spend,
115
+ sales: totals.sales,
116
+ orders: totals.orders,
117
+ ctr: toPct(totals.impressions > 0 ? totals.clicks / totals.impressions : 0),
118
+ cvr: toPct(totals.clicks > 0 ? totals.orders / totals.clicks : 0),
119
+ acos: toPct(totals.sales > 0 ? totals.spend / totals.sales : 0),
120
+ roas: totals.spend > 0 ? (totals.sales / totals.spend).toFixed(2) : "0.00",
121
+ });
122
+ });
123
+ program
124
+ .command("summary")
125
+ .description("Campaign structure summary with layer-level aggregation")
126
+ .requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
127
+ .option("--layer-policy <file>", "Campaign layer policy JSON path")
128
+ .action(async (options) => {
129
+ const config = loadOptimisationConfig();
130
+ const result = await runAnalyzePipeline(options.input, config, {
131
+ layerPolicyPath: options.layerPolicy,
132
+ });
133
+ console.log("\nCampaign KPI Summary");
134
+ console.table(result.campaignMetrics.slice(0, 20).map((item) => ({
135
+ campaign: item.campaignName,
136
+ clicks: item.clicks,
137
+ spend: Math.round(item.spend),
138
+ sales: Math.round(item.sales),
139
+ acos: toPct(item.acos),
140
+ roas: item.roas.toFixed(2),
141
+ })));
142
+ console.log("\nStructure Summary");
143
+ console.table(result.structure.slice(0, 20).map((campaign) => ({
144
+ campaign: campaign.campaignName,
145
+ adGroups: campaign.adGroups.length,
146
+ keywords: campaign.adGroups.reduce((sum, g) => sum + g.keywordCount, 0),
147
+ productTargets: campaign.adGroups.reduce((sum, g) => sum + g.productTargetCount, 0),
148
+ })));
149
+ await printLayerSummary(result, options.layerPolicy);
150
+ });
151
+ program
152
+ .command("cpc-report")
153
+ .description("CPC bid optimization report with SEO ranking integration")
154
+ .requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
155
+ .requiredOption("--output <file>", "Output xlsx path")
156
+ .option("--ranking-db <path>", "A_rank SQLite DB path for SEO-based CPC adjustment")
157
+ .action(async (options) => {
158
+ const config = loadOptimisationConfig();
159
+ const analyzed = await runAnalyzePipeline(options.input, config, {
160
+ rankingDbPath: options.rankingDb,
161
+ });
162
+ const header = [
163
+ "Campaign",
164
+ "Ad Group",
165
+ "Keyword",
166
+ "Clicks(14d)",
167
+ "AvgCPC(14d)",
168
+ "CurrentBid",
169
+ "RecommendedBid",
170
+ "BidAdjust",
171
+ "OrganicPos",
172
+ "SeoFactor",
173
+ "Reason",
174
+ ];
175
+ const rows = analyzed.cpcRecommendations.map((item) => [
176
+ item.campaignName,
177
+ item.adGroupName,
178
+ item.keywordText,
179
+ item.clicks,
180
+ Math.round(item.avgCpc),
181
+ Math.round(item.currentBid),
182
+ item.recommendedBid,
183
+ item.bidAdjust.toFixed(2),
184
+ item.organicPosition != null ? `#${item.organicPosition}` : "-",
185
+ item.seoFactor != null ? item.seoFactor.toFixed(2) : "-",
186
+ item.reason,
187
+ ]);
188
+ await writeXlsx(options.output, [{ name: "CPC_Optimisation_Report", header, rows }]);
189
+ logger.info("CPC report completed", {
190
+ output: options.output,
191
+ rows: rows.length,
192
+ seoEnabled: Boolean(analyzed.seoRankingData),
193
+ });
194
+ });
195
+ program
196
+ .command("promotion-report")
197
+ .description("Auto-to-Manual promotion candidates and negative keyword suggestions")
198
+ .requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
199
+ .requiredOption("--output <file>", "Output xlsx path")
200
+ .action(async (options) => {
201
+ const config = loadOptimisationConfig();
202
+ const analyzed = await runAnalyzePipeline(options.input, config);
203
+ const promotionHeader = [
204
+ "Auto Campaign",
205
+ "Ad Group",
206
+ "Search Term",
207
+ "Clicks",
208
+ "Spend",
209
+ "CVR(%)",
210
+ "Suggested Match Type",
211
+ "Suggested Bid",
212
+ "Suggested Ad Group",
213
+ ];
214
+ const promotionRows = analyzed.promotionCandidates.map((item) => [
215
+ item.campaignName,
216
+ item.adGroupName,
217
+ item.keywordText,
218
+ item.clicks,
219
+ Math.round(item.spend),
220
+ (item.cvr * 100).toFixed(2),
221
+ item.matchType,
222
+ item.recommendedBid,
223
+ item.recommendedAdGroupName,
224
+ ]);
225
+ const negativeHeader = ["Campaign", "Ad Group", "Term", "MatchType", "Reason"];
226
+ const negativeRows = analyzed.negativeCandidates.map((item) => [
227
+ item.campaignName,
228
+ item.adGroupName,
229
+ item.keywordText,
230
+ item.matchType,
231
+ item.reason,
232
+ ]);
233
+ await writeXlsx(options.output, [
234
+ { name: "AutoToManual_Report", header: promotionHeader, rows: promotionRows },
235
+ { name: "Negative_Keyword_Optimisation", header: negativeHeader, rows: negativeRows },
236
+ ]);
237
+ logger.info("Promotion report completed", {
238
+ output: options.output,
239
+ promotionRows: promotionRows.length,
240
+ negativeRows: negativeRows.length,
241
+ });
242
+ });
243
+ program
244
+ .command("seo-report")
245
+ .description("SEO ranking vs ad keyword integrated report")
246
+ .requiredOption("--input <pattern>", "Input Excel/CSV path or wildcard pattern")
247
+ .requiredOption("--ranking-db <path>", "A_rank SQLite DB path")
248
+ .option("--output <file>", "Output xlsx path")
249
+ .option("--format <type>", "console | json | xlsx", "console")
250
+ .action(async (options) => {
251
+ const config = loadOptimisationConfig();
252
+ const analyzed = await runAnalyzePipeline(options.input, config, {
253
+ rankingDbPath: options.rankingDb,
254
+ });
255
+ if (!analyzed.seoRankingData) {
256
+ logger.error("No SEO ranking data available. Check --ranking-db path and SEO_ENABLED setting.");
257
+ process.exitCode = 1;
258
+ return;
259
+ }
260
+ const seoItems = analyzed.cpcRecommendations.map((rec) => ({
261
+ campaign: rec.campaignName,
262
+ adGroup: rec.adGroupName,
263
+ keyword: rec.keywordText,
264
+ clicks: rec.clicks,
265
+ avgCpc: Math.round(rec.avgCpc),
266
+ currentBid: Math.round(rec.currentBid),
267
+ recommendedBid: rec.recommendedBid,
268
+ organicPos: rec.organicPosition != null ? rec.organicPosition : null,
269
+ seoFactor: rec.seoFactor ?? 1.0,
270
+ reason: rec.reason,
271
+ }));
272
+ const format = toOutputFormat(options.format, "console");
273
+ if (format === "json") {
274
+ const content = JSON.stringify({
275
+ dbPath: analyzed.seoRankingData.dbPath,
276
+ snapshotDate: analyzed.seoRankingData.snapshotDate,
277
+ matchedKeywords: analyzed.seoRankingData.rankings.size,
278
+ items: seoItems,
279
+ }, null, 2);
280
+ if (options.output) {
281
+ await writeTextFile(options.output, content);
282
+ logger.info("SEO report JSON saved", { output: options.output });
283
+ }
284
+ else {
285
+ console.log(content);
286
+ }
287
+ return;
288
+ }
289
+ if (format === "xlsx" || (options.output && options.output.toLowerCase().endsWith(".xlsx"))) {
290
+ const outputPath = options.output ?? path.resolve("output", `seo-report-${timestampForFilename()}.xlsx`);
291
+ const header = [
292
+ "Campaign",
293
+ "Ad Group",
294
+ "Keyword",
295
+ "Clicks(14d)",
296
+ "AvgCPC",
297
+ "CurrentBid",
298
+ "RecommendedBid",
299
+ "OrganicPos",
300
+ "SeoFactor",
301
+ "Reason",
302
+ ];
303
+ const rows = seoItems.map((item) => [
304
+ item.campaign,
305
+ item.adGroup,
306
+ item.keyword,
307
+ item.clicks,
308
+ item.avgCpc,
309
+ item.currentBid,
310
+ item.recommendedBid,
311
+ item.organicPos != null ? item.organicPos : "-",
312
+ item.seoFactor.toFixed(2),
313
+ item.reason,
314
+ ]);
315
+ await writeXlsx(outputPath, [{ name: "SEO_Report", header, rows }]);
316
+ logger.info("SEO report generated", {
317
+ output: outputPath,
318
+ rows: rows.length,
319
+ matchedKeywords: analyzed.seoRankingData.rankings.size,
320
+ });
321
+ return;
322
+ }
323
+ // Console output
324
+ console.log("\nSEO Ranking Report");
325
+ console.log(`DB: ${analyzed.seoRankingData.dbPath}`);
326
+ console.log(`Snapshot: ${analyzed.seoRankingData.snapshotDate}`);
327
+ console.log(`Matched keywords: ${analyzed.seoRankingData.rankings.size}`);
328
+ console.log("");
329
+ const seoAdjusted = seoItems.filter((item) => item.seoFactor < 1.0);
330
+ if (seoAdjusted.length > 0) {
331
+ console.log("SEO-adjusted keywords:");
332
+ console.table(seoAdjusted.map((item) => ({
333
+ keyword: item.keyword,
334
+ organicPos: item.organicPos != null ? `#${item.organicPos}` : "-",
335
+ seoFactor: item.seoFactor.toFixed(2),
336
+ currentBid: item.currentBid,
337
+ recommendedBid: item.recommendedBid,
338
+ })));
339
+ }
340
+ else {
341
+ console.log("No keywords with SEO adjustment (all factors = 1.0)");
342
+ }
343
+ console.log(`\nTotal CPC recommendations: ${seoItems.length}`);
344
+ console.log(`SEO-adjusted: ${seoAdjusted.length}`);
345
+ });
346
+ program.parseAsync(process.argv).catch((error) => {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ logger.error("Command failed", { error: message });
349
+ process.exitCode = 1;
350
+ });
351
+ //# sourceMappingURL=cli.js.map