@gobing-ai/knowledge-kit 0.0.37 → 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 (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +654 -16
  3. package/dist/board/collect/index.js +191 -64
  4. package/dist/board/daily-voice/index.css +679 -12
  5. package/dist/board/daily-voice/index.js +185 -66
  6. package/dist/board/itc/index.css +679 -13
  7. package/dist/board/itc/index.js +188 -66
  8. package/dist/index.js +2864 -139
  9. package/package.json +3 -2
  10. package/plugins/generations/content-gen/dist/index.js +158 -12
  11. package/plugins/generations/core-facts-gen/dist/index.js +158 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +158 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +158 -12
  14. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  15. package/plugins/generations/gemini-tts/README.md +4 -3
  16. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  17. package/plugins/generations/image-gen/dist/index.js +158 -12
  18. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  19. package/plugins/generations/omni-voice-gen/README.md +15 -4
  20. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  21. package/plugins/generations/voice-gen/dist/index.js +158 -12
  22. package/plugins/generations/voxcpm-tts/README.md +21 -1
  23. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  24. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  25. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  26. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  27. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/web-search/dist/index.js +158 -12
  30. package/plugins/kk/commands/workflow-run.md +56 -18
  31. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  32. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  33. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  34. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  35. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  36. package/plugins/kk/plugin.json +1 -1
  37. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  38. package/plugins/kk/publish-presets/technical-article.json +14 -0
  39. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  40. package/plugins/kk/scripts/daily-stages.ts +42 -2
  41. package/plugins/kk/scripts/itc-stages.ts +25 -118
  42. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  43. package/plugins/kk/scripts/seo-decisions.ts +477 -0
  44. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  45. package/plugins/kk/scripts/seo-facts.ts +610 -0
  46. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  47. package/plugins/kk/scripts/seo-report.ts +460 -0
  48. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  49. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  50. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  51. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  52. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  53. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  54. package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
  55. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  56. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  57. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
  58. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  59. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  60. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  61. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  62. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  63. package/plugins/publishings/bilibili-pub/package.json +20 -0
  64. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  65. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  66. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  67. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  68. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  69. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  70. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  71. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  72. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  73. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  74. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  75. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  76. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  77. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  78. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  79. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  80. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  81. package/plugins/publishings/csdn-pub/package.json +19 -0
  82. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  83. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  84. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  85. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  86. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  87. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  88. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  89. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  90. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  91. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  92. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  93. package/plugins/publishings/jike-pub/package.json +19 -0
  94. package/plugins/publishings/jike-pub/plugin.json +12 -0
  95. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  96. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  97. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  99. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  100. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  101. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  102. package/plugins/publishings/medium-pub/plugin.json +22 -1
  103. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  104. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  105. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  106. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  107. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  108. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  109. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  110. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  111. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  112. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  113. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  114. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  115. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  116. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  117. package/plugins/publishings/sspai-pub/package.json +19 -0
  118. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  119. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  120. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  121. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  122. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  123. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  125. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  126. package/plugins/publishings/substack-pub/plugin.json +21 -0
  127. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  128. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  129. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  130. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  131. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  132. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  133. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  134. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  136. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  137. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  138. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  139. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  140. package/plugins/publishings/x-pub/dist/index.js +160 -12
  141. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  142. package/plugins/publishings/x-pub/plugin.json +40 -0
  143. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  144. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  145. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  146. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  147. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  155. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  156. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  157. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -0,0 +1,85 @@
1
+ # Smart SEO Attribution & Upstream Review Notice
2
+
3
+ ## Upstream Project and License
4
+
5
+ Knowledge Kit Smart SEO (`kk-smart-seo`) adapts and grounds the methodology and rules from the upstream `jev-seo` project.
6
+
7
+ - **Upstream Project:** `jev-seo` (version 0.1.1)
8
+ - **Upstream Commit Snapshot:** `55a184a3b0d09565a4c84268f725a47784e62528`
9
+ - **Upstream Author:** Agrici Daniel
10
+ - **License:** MIT License
11
+
12
+ ```text
13
+ MIT License
14
+
15
+ Copyright (c) 2026 Agrici Daniel
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
34
+ ```
35
+
36
+ ## Architectural & SECUA Audit Linkage
37
+
38
+ The upstream vendor snapshot was fully reviewed in `docs/plans/2026-10-14-smart-seo-vendor-review.md`.
39
+
40
+ Key architectural and security adaptations include:
41
+ 1. **Network Trust Boundary (V01, V02):** Replaced unpinned ambient requests with connection-pinned public HTTP(S) transport, DNS resolution validation against private/reserved/multicast ranges, peer address verification, and scoped redirect validation. No browser or Chromium runtime path is imported.
42
+ 2. **Resource Boundaries (V03, V04):** Streamed response limit of 2 MiB per response, 40 MiB crawl total, shared request quotas (default 200, max 500) and shared deadline (default 600s, max 1800s).
43
+ 3. **Robots & Sitemap Integrity (V04, V05, V06):** Robots.txt fetched first and respects full `Crawl-delay` or marks budget exhaustion; distinguishes absent (404/410), blocked (401/403), and unavailable (429/5xx). Strictly bounded non-expanding XML subset reader rejecting DOCTYPE/entities/compression.
44
+ 4. **Distinct URL & Identity Preservation (V06):** Preserves pathname, query parameters, and trailing slashes (`/a` and `/a/` remain distinct resources); fragments stripped.
45
+ 5. **Clean Runtime Seam:** Knowledge Kit Smart SEO is implemented as native TypeScript stages on Bun 1.4.3 (`plugins/kk/scripts/seo-*.ts`), importing no vendor Python files or bundled font assets.
46
+
47
+ ## Rule Dispositions (52 Rules)
48
+
49
+ All 52 deterministic rules from the vendor snapshot have explicit dispositions:
50
+ - **Native (24 rules):** `redirect_chains`, `links_to_redirects`, `host_temporary_redirect`, `deep_pages`, `title_missing`, `title_duplicate`, `title_length`, `multiple_titles`, `meta_missing`, `meta_duplicate`, `h1_missing`, `h1_multiple`, `heading_skips`, `lang_missing`, `viewport_missing`, `thin_content`, `duplicate_content`, `images_alt`, `images_dimensions`, `jsonld_errors`, `og_missing`, `mixed_content`, `heavy_html`, `generic_anchors`.
51
+ - **Adapted (15 rules):** `robots_blocks_site`, `sitemap_errors`, `sitemap_bad_urls`, `http_errors`, `broken_internal_links`, `noindex`, `canonical_elsewhere`, `canonical_broken`, `soft_404`, `host_variant`, `no_https`, `orphan_pages`, `faq_rich_result_limited`, `hreflang_issues`, `slow_ttfb`.
52
+ - **Informational (11 rules):** `robots_missing`, `sitemap_missing`, `not_in_sitemap`, `canonical_missing`, `js_dependent`, `no_structured_data`, `ai_bots_blocked`, `llms_txt_missing`, `hsts_missing`, `security_headers`, `favicon_missing`.
53
+ - **Deferred (2 rules):** `schema_required`, `broken_external_links`.
54
+
55
+ ## Adapted Rubric & Source Attribution (task 0286)
56
+
57
+ The `smart-seo` skill and its references restate **KK-authored** adaptations of the upstream
58
+ methodology. They are not copies of vendor prose, and the following must stay explicit whenever the
59
+ rubric is described or reused:
60
+
61
+ 1. **Decision catalogs** (`plugins/kk/decisions/kk-seo-{site,page,pair,keyword}.yaml`, 22 decisions)
62
+ adapt the vendor's decision *descriptions and vocabularies* (business model, page type, search
63
+ intent, page action, 0–3 editorial scores, noul questions). KK additions: an `unclear` choice in
64
+ every choice vocabulary, `keep-or-improve|rewrite|review-consolidation|unclear` instead of
65
+ automatic removal, `defaults.minConfidence: 0` so a served low-confidence observation is recorded
66
+ rather than silently replaced, and 0–3 score levels with an explicit `uncertain → review` band in
67
+ the consumer. The vendor's weighted overall grade and relative 100-point impact scale are
68
+ **rejected** — this method publishes per-category pass ratios with their coverage denominators and
69
+ no blended SEO/GEO grade.
70
+ 2. **Rule identities** (`plugins/kk/scripts/seo-rules.ts` `VENDOR_RULES`) retain the 52 upstream
71
+ rule ids for traceability, each carrying `disposition` (`native|adapted|informational|deferred`),
72
+ KK-chosen `severity`, an `effort` band 1–4, a `heuristic` flag and the primary source with the date
73
+ it was checked. Neither severity nor effort is the vendor's.
74
+ 3. **Source registry**: rule rows cite the primary source itself — RFC 9309 for robots matching,
75
+ Google Search Central (canonicalization, robots meta, sitemaps, AI optimization, common crawlers),
76
+ OpenAI's crawler documentation, web.dev Web Vitals. The dated registry, the per-source limits and
77
+ the refresh procedure are `plugins/kk/skills/smart-seo/references/seo-geo-sources.md`; the rubric,
78
+ scoring and action-priority interpretation is
79
+ `plugins/kk/skills/smart-seo/references/method.md`; the strict report-narrative contract is
80
+ `plugins/kk/skills/smart-seo/references/narrative.md`.
81
+ 4. **Narrative rendering** uses KK-owned fixed templates over the audit artifact. Agents select and
82
+ order recorded references and cannot introduce prose, measurements, diagnoses or predictions.
83
+ 5. **Vendor code, Python paths, browser/rendering stacks and bundled font assets are not shipped**;
84
+ the audit is native Bun/TypeScript over the existing stage runner (ADR-018) and `vendors/` stays
85
+ excluded from runtime and install resolution (ADR-036).
@@ -467,6 +467,38 @@ export function dailyCoverNormalize(args: string[]): void {
467
467
  * no-content with the measured vs estimated minutes recorded. Over-max still fails loud — trimming
468
468
  * is a different problem.
469
469
  */
470
+ /**
471
+ * Advisory speaker-consistency audit of a finished episode.
472
+ *
473
+ * Exists because an unanchored voice profile (voxcpm's `instruct` mode pins no speaker) shipped 59
474
+ * different speakers in the 20261015 episode and nothing but a human listening caught it. This runs
475
+ * the plugin's F0 audit and reports; it NEVER fails the run — a regression, a missing venv or a
476
+ * broken script must not stop publishing. A non-zero audit exit prints one WARNING line instead.
477
+ */
478
+ export function dailyVoiceAudit(args: string[]): void {
479
+ const [workDir = '', runDate = '', pluginsPath = ''] = args;
480
+ if (!workDir) fail('missing work_dir');
481
+ if (!runDate) fail('missing run_date');
482
+ const contentPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
483
+ if (!isFile(contentPath)) {
484
+ console.log(`voice-audit: no episode content at ${contentPath}; skipping`);
485
+ return;
486
+ }
487
+ const plugin = join(pluginsPath || 'plugins', 'generations', 'voxcpm-tts');
488
+ const python = join(plugin, '.venv', 'bin', 'python');
489
+ const script = join(plugin, 'scripts', 'audit-speaker-consistency.py');
490
+ if (!isFile(python) || !isFile(script)) {
491
+ console.log(`voice-audit: skipping (needs ${python} and ${script})`);
492
+ return;
493
+ }
494
+ const audit = spawnSync(python, [script, contentPath], { encoding: 'utf-8' });
495
+ const out = `${audit.stdout ?? ''}${audit.stderr ?? ''}`.trim();
496
+ if (out) console.log(out);
497
+ if (audit.status !== 0) {
498
+ console.log('voice-audit: WARNING — the speaker-consistency audit flagged this episode (advisory)');
499
+ }
500
+ }
501
+
470
502
  export function dailyEpisodeDuration(args: string[]): void {
471
503
  const [workDir = '', runDate = '', minArg = '', maxArg = '', vdriver = ''] = args;
472
504
  if (!workDir) fail('missing work_dir');
@@ -1989,9 +2021,17 @@ function voicescriptToBlockYaml(script: Record<string, unknown>): string {
1989
2021
  return lines.join('\n');
1990
2022
  }
1991
2023
 
1992
- /** Measured driver rates from dailyGenerate. Unknown drivers (incl. voxcpm, provisionally) use the voicebox rate. */
2024
+ /** Measured driver rates from dailyGenerate. Unknown drivers reuse the voicebox rate. */
1993
2025
  function spokenCharRate(vdriver: string): number {
1994
- return vdriver === 'gemini' ? 5.4 : 5.6;
2026
+ // Recalibrated 2026-10-15: episode measured 7,395 spoken chars / 1,490.9 s across 12 sections
2027
+ // (4.96 字/秒) — the effective gemini rate dropped ~8% below the 5.33-5.62 band calibrated
2028
+ // 2026-10-04/07, which consumed the fit's 2-min margin and tripped the 25-min gate at 25.2 min.
2029
+ if (vdriver === 'gemini') return 4.95;
2030
+ // voxcpm measured the same day on a 12-section zh+en episode: 6,772 chars / 1,297.7 s speech
2031
+ // (5.19 字/秒). The provisional 5.6 borrowed from voicebox let the fit budget reach 7,683 chars
2032
+ // — 24.6 min of audio, 18 s inside the 25-min gate. 5.2 puts a full-budget script at ~22.9 min.
2033
+ if (vdriver === 'voxcpm') return 5.2;
2034
+ return 5.6;
1995
2035
  }
1996
2036
 
1997
2037
  /**
@@ -232,69 +232,9 @@ export function itcReport(args: string[]): void {
232
232
  }
233
233
 
234
234
  // ---------------------------------------------------------------------------
235
- // 0150: publish-tail seams — TARGET_PROFILES, itc-materialize, itc-publish-plan
235
+ // 0150 / 0298: publish-tail seams — itc-materialize, itc-publish-plan
236
236
  // ---------------------------------------------------------------------------
237
237
 
238
- /** One row of the docs/design/kk-itc-end-to-end.md §5 target profile table. */
239
- export interface TargetProfile {
240
- transport: 'fanout' | 'browser';
241
- /** `source` = the draft's own source_locale; anything else is fixed. */
242
- locale: 'en' | 'zh' | 'ja' | 'source';
243
- format: string;
244
- cover: boolean;
245
- /**
246
- * V3 draft/live switch itc-publish-fanout writes into the variant Content metadata at send
247
- * time. Returning null means the target has no switch for this run's mode and is rejected by
248
- * name: x-pub's `operation: post` has no draft mode; wechatmp-pub refuses every live operation.
249
- */
250
- switch: (live: boolean) => Record<string, unknown> | null;
251
- }
252
-
253
- // Draft/live key per row, verified against each plugin's src/ (0150 R3):
254
- // medium-pub metadata.publishStatus 'draft'|'public' (plugins/publishings/medium-pub/src/index.ts:49)
255
- // emdash-pub metadata.published (plugins/publishings/emdash-pub/src/index.ts:254)
256
- // surfdash-pub metadata.published (plugins/publishings/surfdash-pub/src/index.ts:93)
257
- // zenn-pub metadata.published — defaults to LIVE when absent, so never omit (plugins/publishings/zenn-pub/src/index.ts:92)
258
- // qiita-pub metadata.published, private is its inverse (plugins/publishings/qiita-pub/src/index.ts:70)
259
- // wechatmp-pub metadata.operation, draft default; live refused (plugins/publishings/wechatmp-pub/src/index.ts:162,171)
260
- // x-pub metadata.operation 'post' — no draft mode exists (plugins/publishings/x-pub/src/index.ts:64)
261
- // substack/juejin/infoq/xhs always return a draft Result (plugins/publishings/substack-pub/src/index.ts:26);
262
- // their live path is 0151's `kk:publish --live` agent flag, so the switch is documentation-only.
263
- const publishedSwitch = (live: boolean) => ({ published: live });
264
- const browserSwitch = (live: boolean): Record<string, unknown> => (live ? {} : { publishStatus: 'draft' });
265
-
266
- export const TARGET_PROFILES: Record<string, TargetProfile> = {
267
- 'medium-pub': {
268
- transport: 'fanout',
269
- locale: 'en',
270
- format: 'article',
271
- cover: false,
272
- switch: (live) => ({ publishStatus: live ? 'public' : 'draft' }),
273
- },
274
- 'substack-pub': { transport: 'browser', locale: 'en', format: 'article', cover: false, switch: browserSwitch },
275
- 'emdash-pub': { transport: 'fanout', locale: 'source', format: 'article', cover: false, switch: publishedSwitch },
276
- 'surfdash-pub': { transport: 'fanout', locale: 'source', format: 'article', cover: false, switch: publishedSwitch },
277
- 'zenn-pub': { transport: 'fanout', locale: 'ja', format: 'article', cover: false, switch: publishedSwitch },
278
- 'qiita-pub': { transport: 'fanout', locale: 'ja', format: 'article', cover: false, switch: publishedSwitch },
279
- 'wechatmp-pub': {
280
- transport: 'fanout',
281
- locale: 'zh',
282
- format: 'wechat-article',
283
- cover: true,
284
- switch: (live) => (live ? null : { operation: 'draft' }),
285
- },
286
- 'juejin-pub': { transport: 'browser', locale: 'zh', format: 'article', cover: false, switch: browserSwitch },
287
- 'infoq-pub': { transport: 'browser', locale: 'zh', format: 'article', cover: false, switch: browserSwitch },
288
- 'xhs-pub': { transport: 'browser', locale: 'zh', format: 'short-note', cover: true, switch: browserSwitch },
289
- 'x-pub': {
290
- transport: 'fanout',
291
- locale: 'en',
292
- format: 'short-post',
293
- cover: false,
294
- switch: (live) => (live ? { operation: 'post' } : null),
295
- },
296
- };
297
-
298
238
  const DRAFT_FILE = '3-draft-draft-article.md';
299
239
 
300
240
  /** First `cover.*` in 4-illustrations as a workspace-relative path, or undefined. */
@@ -383,65 +323,32 @@ export interface PublishPlan {
383
323
  }
384
324
 
385
325
  export function itcPublishPlan(args: string[]): void {
386
- const [workDir = '', targetsArg = '', sourceLocale = '', liveArg = ''] = args;
387
- if (!workDir || !targetsArg || !sourceLocale) {
388
- fail('usage: itc-publish-plan <work_dir> <targets> <source_locale> <publish_live>');
326
+ if (process.env._KK_ITC_STAGE_NATIVE === '1') {
327
+ return;
389
328
  }
390
- const live = liveArg === 'true';
391
- const targets = [
392
- ...new Set(
393
- targetsArg
394
- .split(',')
395
- .map((t) => t.trim())
396
- .filter(Boolean),
397
- ),
398
- ];
399
- const hasCover = findCover(workDir) !== undefined;
400
- const variants: PublishPlan['variants'] = {};
401
- const fanout = new Map<string, string[]>();
402
- const browser: PublishPlan['browser'] = [];
403
- for (const target of targets) {
404
- const profile = TARGET_PROFILES[target];
405
- if (!profile) fail(`unsupported publish target: ${target} (unknown or not an article target)`);
406
- const switchValue = profile.switch(live);
407
- if (!switchValue) {
408
- fail(
409
- live
410
- ? `target ${target} refuses a live publish`
411
- : `target ${target} has no draft mode; it needs publish_live=true`,
412
- );
413
- }
414
- if (profile.cover && !hasCover) {
415
- fail(`target ${target} needs a cover image at 4-illustrations/cover.* in ${workDir}`);
416
- }
417
- const variant = `${profile.locale === 'source' ? sourceLocale : profile.locale}-${profile.format}`;
418
- const sharesSourceArticle = variant === `${sourceLocale}-article`;
419
- const entry = variants[variant] ?? {
420
- file: sharesSourceArticle ? 'content.json' : `content.${variant}.json`,
421
- adapt: !sharesSourceArticle,
422
- limits: sharesSourceArticle ? undefined : {},
423
- switches: {} as Record<string, unknown>,
424
- };
425
- Object.assign(entry.switches, switchValue);
426
- variants[variant] = entry;
427
- if (profile.transport === 'browser') browser.push({ target, variant });
428
- else {
429
- const group = fanout.get(variant);
430
- if (group) group.push(target);
431
- else fanout.set(variant, [target]);
432
- }
329
+ const cliEntry = join(__dirname, '../../../apps/cli/src/index.ts');
330
+ let res: ReturnType<typeof spawnSync>;
331
+ if (existsSync(cliEntry)) {
332
+ res = spawnSync(process.execPath, [cliEntry, 'stage', 'run', 'itc-publish-plan', ...args], {
333
+ stdio: ['inherit', 'pipe', 'pipe'],
334
+ env: { ...process.env, _KK_ITC_STAGE_NATIVE: '1' },
335
+ });
336
+ } else {
337
+ res = spawnSync('kk', ['stage', 'run', 'itc-publish-plan', ...args], {
338
+ stdio: ['inherit', 'pipe', 'pipe'],
339
+ env: { ...process.env, _KK_ITC_STAGE_NATIVE: '1' },
340
+ });
341
+ }
342
+ if (res.stdout && res.stdout.length > 0) {
343
+ process.stdout.write(res.stdout);
344
+ }
345
+ if (res.stderr && res.stderr.length > 0) {
346
+ const errText = res.stderr.toString('utf-8');
347
+ console.error(errText.endsWith('\n') ? errText.slice(0, -1) : errText);
348
+ }
349
+ if (res.error || (res.status !== null && res.status !== 0)) {
350
+ process.exit(res.status ?? 1);
433
351
  }
434
- const plan: PublishPlan = {
435
- live,
436
- sourceLocale,
437
- variants,
438
- // canonical group order: the plan file must not depend on target argument order
439
- fanout: [...fanout]
440
- .sort(([a], [b]) => (a < b ? -1 : 1))
441
- .map(([variant, groupTargets]) => ({ variant, targets: groupTargets })),
442
- browser,
443
- };
444
- writeJson(join(workDir, '4-publish', 'publish-plan.json'), plan);
445
352
  }
446
353
 
447
354
  /** R3 — the adapt state's follow-up check: every adapt:true variant file exists with title/body. */
@@ -128,6 +128,21 @@ async function loadItcStages(): Promise<typeof import('./itc-stages.ts')> {
128
128
  }
129
129
  }
130
130
 
131
+ /**
132
+ * Load the seo-* helper sibling lazily (task 0281): this runner is resolved — and copied
133
+ * alone — from installs where the sibling may be absent, so the seo-* stages fail loud with a
134
+ * remediation path instead of breaking every stage at load.
135
+ */
136
+ async function loadSeoStages(): Promise<typeof import('./seo-stages.ts')> {
137
+ try {
138
+ return await import('./seo-stages.ts');
139
+ } catch {
140
+ return fail(
141
+ 'seo-stages.ts not found next to kk-workflow-stages.ts — install or refresh the kk package (bun link in a checkout)',
142
+ );
143
+ }
144
+ }
145
+
131
146
  /** coreutils-style reason for the errno codes the staged file operations can hit. */
132
147
 
133
148
  /**
@@ -1639,6 +1654,11 @@ if (import.meta.main)
1639
1654
  dailyIntro(rest);
1640
1655
  break;
1641
1656
  }
1657
+ case 'daily-voice-audit': {
1658
+ const { dailyVoiceAudit } = await loadDailyStages();
1659
+ dailyVoiceAudit(rest);
1660
+ break;
1661
+ }
1642
1662
  case 'daily-episode-duration': {
1643
1663
  const { dailyEpisodeDuration } = await loadDailyStages();
1644
1664
  dailyEpisodeDuration(rest);
@@ -1739,8 +1759,25 @@ if (import.meta.main)
1739
1759
  itcAdaptVerify(rest);
1740
1760
  break;
1741
1761
  }
1762
+ case 'seo-preflight':
1763
+ case 'seo-collect':
1764
+ case 'seo-check':
1765
+ case 'seo-enrich':
1766
+ case 'seo-judge':
1767
+ case 'seo-score':
1768
+ case 'seo-report': {
1769
+ // The seo helper owns its dispatch: cancellation installation, the cancellation marker
1770
+ // and the conventional signal exit status must apply however the stage was reached.
1771
+ const { runSeoStage } = await loadSeoStages();
1772
+ try {
1773
+ await runSeoStage(stage, rest);
1774
+ } catch (err: unknown) {
1775
+ fail(err instanceof Error ? err.message : String(err));
1776
+ }
1777
+ break;
1778
+ }
1742
1779
  default:
1743
1780
  fail(
1744
- `unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|itc-*|daily-*)`,
1781
+ `unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|itc-*|daily-*|seo-*)`,
1745
1782
  );
1746
1783
  }