@gobing-ai/knowledge-kit 0.1.11 → 0.1.13

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 (137) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2372 -1722
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +15 -7
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-scheduled-run.ts +91 -41
  88. package/plugins/kk/scripts/daily-stages.ts +394 -53
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
  90. package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
  91. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  92. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  93. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  95. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  96. package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
  97. package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
  98. package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
  99. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  100. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  101. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  102. package/plugins/kk/skills/publish/SKILL.md +1 -0
  103. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  104. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  105. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  106. package/plugins/kk/skills/topic/SKILL.md +1 -0
  107. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  108. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  109. package/plugins/kk/workflows/kk-collect.yaml +3 -3
  110. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +21 -19
  111. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +163 -63
  112. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  113. package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
  114. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  115. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  116. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  117. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  118. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  119. package/plugins/publishings/linkedin-pub/package.json +19 -0
  120. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  121. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  122. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  123. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/surfdash-pub/dist/index.js +53 -24
  125. package/plugins/publishings/surfdash-pub/src/index.ts +72 -25
  126. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  127. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  128. package/plugins/publishings/wechatmp-pub/dist/index.js +176 -32
  129. package/plugins/publishings/wechatmp-pub/src/flow.ts +298 -52
  130. package/plugins/publishings/weibo-pub/dist/index.js +100 -7
  131. package/plugins/publishings/weibo-pub/src/index.ts +111 -11
  132. package/plugins/publishings/x-pub/dist/index.js +7 -0
  133. package/plugins/publishings/x-pub/src/index.ts +11 -0
  134. package/plugins/publishings/xhs-pub/dist/index.js +164 -12
  135. package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
  136. package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
  137. package/plugins/publishings/xhs-pub/src/page.ts +26 -0
@@ -0,0 +1,17 @@
1
+ /**
2
+ * LinkedIn profile-session capability (0328 R2).
3
+ *
4
+ * `login` opens the feed in a headed browser and resolves only once LinkedIn's readiness predicate
5
+ * passes; `check` reopens the profile headlessly and reports `verified` or `refused` without
6
+ * posting. Arg parsing, profile precedence (`--profile` > `LINKEDIN_PROFILE_DIR` >
7
+ * `KK_BROWSER_PROFILE_DIR` > `~/.config/kk/browser-profiles/linkedin`, ADR-042) and the exit codes
8
+ * (0 verified, 1 refused, 2 usage error) all live in the shared `runProfileCli`, so the publish
9
+ * preflight (`kk plugin check linkedin-pub` and the publish-matrix probe) and this CLI apply one
10
+ * gate.
11
+ */
12
+ import { runProfileCli } from '@gobing-ai/publish-harness/browser';
13
+ import { LINKEDIN_CONFIG } from './index';
14
+
15
+ if (import.meta.main) {
16
+ process.exit(await runProfileCli(LINKEDIN_CONFIG, Bun.argv.slice(2)));
17
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -12798,11 +12798,14 @@ function resolveSurfdashRoot() {
12798
12798
  return process.env.SURFDASH_ROOT ?? (postsurfingBin.includes("/") ? resolve(dirname3(dirname3(postsurfingBin))) : undefined);
12799
12799
  }
12800
12800
  async function spawnCli(cmd, cwd) {
12801
+ const env = { ...process.env };
12802
+ for (const key of ["SPUR_WORKFLOW_RUN_ACTIVE", "SPUR_EXPECTED_DEFINITION_DIGEST"])
12803
+ delete env[key];
12801
12804
  const proc = Bun.spawn(cmd, {
12802
12805
  cwd,
12803
12806
  stdout: "pipe",
12804
12807
  stderr: "pipe",
12805
- env: process.env
12808
+ env
12806
12809
  });
12807
12810
  const [exitCode, stdout, stderr] = await Promise.all([
12808
12811
  proc.exited,
@@ -12896,7 +12899,30 @@ function extractFailingPostHint(text) {
12896
12899
  return;
12897
12900
  return match[0].replace(/[.,;]+$/, "");
12898
12901
  }
12899
- async function runSiteGate(surfdashRoot) {
12902
+ function extractPostDates(paths) {
12903
+ const dates = new Set;
12904
+ for (const p of paths) {
12905
+ const m = p.replaceAll("\\", "/").match(/(?:^|\/)posts\/articles\/(?:zh|en|ja)\/(\d{8})-/);
12906
+ if (m?.[1])
12907
+ dates.add(m[1]);
12908
+ }
12909
+ return dates;
12910
+ }
12911
+ function isUnrelatedDatePostFailure(output, ownedPaths) {
12912
+ const ownedDates = extractPostDates(ownedPaths);
12913
+ if (ownedDates.size === 0)
12914
+ return false;
12915
+ const failingHints = [...output.matchAll(/(?:content[\\/])?posts[\\/][^\s:)]+/g)].map((m) => m[0].replace(/[.,;]+$/, ""));
12916
+ if (failingHints.length === 0)
12917
+ return false;
12918
+ for (const hint of failingHints) {
12919
+ const m = hint.replaceAll("\\", "/").match(/(?:^|\/)posts\/articles\/(?:zh|en|ja)\/(\d{8})-/);
12920
+ if (!m?.[1] || ownedDates.has(m[1]))
12921
+ return false;
12922
+ }
12923
+ return true;
12924
+ }
12925
+ async function runSiteGate(surfdashRoot, ownedPaths = []) {
12900
12926
  if (process.env.SURFDASH_DIGEST_CHECK === "off") {
12901
12927
  return { ok: true };
12902
12928
  }
@@ -12907,7 +12933,7 @@ async function runSiteGate(surfdashRoot) {
12907
12933
  const postHint = extractFailingPostHint(output);
12908
12934
  const hint = postHint ? ` [failing post: ${postHint}]` : "";
12909
12935
  const error = `site gate failed: ${cmd.join(" ")} (exit ${spawned.exitCode})${hint}: ${output}`;
12910
- if (process.env.SURFDASH_DIGEST_CHECK === "warn") {
12936
+ if (process.env.SURFDASH_DIGEST_CHECK === "warn" || isUnrelatedDatePostFailure(output, ownedPaths)) {
12911
12937
  for (const line of error.split(`
12912
12938
  `)) {
12913
12939
  echoError(`site gate (warn): ${line}`);
@@ -13079,7 +13105,7 @@ function normalizeLocaleFrontmatter(markdown) {
13079
13105
  const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
13080
13106
  if (close < 0)
13081
13107
  return markdown;
13082
- const head = lines.slice(1, close).filter((l) => !/^canonical\s*:/.test(l)).map((l) => /^draft\s*:/.test(l) ? "draft: false" : l);
13108
+ const head = lines.slice(1, close).filter((l) => !/^canonical\s*:.*\/zh\//.test(l)).map((l) => /^draft\s*:/.test(l) ? "draft: false" : l);
13083
13109
  return [lines[0], ...head, ...lines.slice(close)].join(`
13084
13110
  `);
13085
13111
  }
@@ -13172,7 +13198,7 @@ async function patchPost(options) {
13172
13198
  const backfilled = await runListenLinkBackfill(surfdashRoot);
13173
13199
  for (const p of backfilled)
13174
13200
  commitPaths.add(p);
13175
- const gate = await runSiteGate(surfdashRoot);
13201
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, relative(surfdashRoot, postPath.postPath)]);
13176
13202
  if (!gate.ok)
13177
13203
  return fail(gate.error);
13178
13204
  }
@@ -13338,9 +13364,8 @@ async function preparePost(payload) {
13338
13364
  return { ok: false, result: failNotSent(`SURFDASH_LOCALE must be one of zh/en/ja, got '${rawLocale}'`) };
13339
13365
  }
13340
13366
  const locale = rawLocale;
13341
- const runDate = typeof payload.content.metadata?.runDate === "string" ? payload.content.metadata.runDate : "";
13342
- const today = new Date().toISOString().slice(0, 10).replaceAll("-", "");
13343
- const dateStamp = runDate || today;
13367
+ const runDate = typeof payload.content.metadata?.runDate === "string" && payload.content.metadata.runDate !== "" ? payload.content.metadata.runDate : typeof payload.content.metadata?.date === "string" ? payload.content.metadata.date.replaceAll("-", "") : "";
13368
+ const dateStamp = runDate || new Date().toISOString().slice(0, 10).replaceAll("-", "");
13344
13369
  const titleSlug = slugify2(payload.content.title ?? "");
13345
13370
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
13346
13371
  if (runDate !== "" && !/^\d{8}$/.test(runDate)) {
@@ -13367,20 +13392,24 @@ async function preparePost(payload) {
13367
13392
  }
13368
13393
  const filePath = join2(postDir, "index.md");
13369
13394
  try {
13370
- await createNodeFileSystem().ensureDir(postDir);
13371
- let imageFrontmatter;
13372
- if (typeof coverPath === "string") {
13373
- const assetsDir = join2(postDir, "assets");
13374
- await createNodeFileSystem().ensureDir(assetsDir);
13375
- const coverName = `cover${extname(coverPath).toLowerCase()}`;
13376
- await copyFile(coverPath, join2(assetsDir, coverName));
13377
- imageFrontmatter = `./assets/${coverName}`;
13378
- }
13379
- const markdown = mapContentToSurfingMarkdown(payload.content, {
13380
- image: imageFrontmatter,
13381
- publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined
13382
- });
13383
- await createNodeFileSystem().writeFile(filePath, markdown);
13395
+ if (readRefinedSnapshot(filePath) === undefined) {
13396
+ await createNodeFileSystem().ensureDir(postDir);
13397
+ let imageFrontmatter;
13398
+ if (typeof coverPath === "string") {
13399
+ const assetsDir = join2(postDir, "assets");
13400
+ await createNodeFileSystem().ensureDir(assetsDir);
13401
+ const coverName = `cover${extname(coverPath).toLowerCase()}`;
13402
+ await copyFile(coverPath, join2(assetsDir, coverName));
13403
+ imageFrontmatter = `./assets/${coverName}`;
13404
+ }
13405
+ const markdown = mapContentToSurfingMarkdown(payload.content, {
13406
+ image: imageFrontmatter,
13407
+ publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined
13408
+ });
13409
+ await createNodeFileSystem().writeFile(filePath, markdown);
13410
+ } else {
13411
+ echoError(`surfdash-pub: keeping existing refined post at ${filePath} (not re-minting)`);
13412
+ }
13384
13413
  } catch (err) {
13385
13414
  return {
13386
13415
  ok: false,
@@ -13461,10 +13490,10 @@ async function publishPreparedFile(prepared, identity) {
13461
13490
  });
13462
13491
  }
13463
13492
  const backfilled = await runListenLinkBackfill(surfdashRoot);
13464
- const gate = await runSiteGate(surfdashRoot);
13493
+ const commitPaths = relPostDirs ?? [relPostDir];
13494
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, ...backfilled]);
13465
13495
  if (!gate.ok)
13466
13496
  return failUncertain(gate.error);
13467
- const commitPaths = relPostDirs ?? [relPostDir];
13468
13497
  const commitLocales = relPostDirs === undefined ? [locale] : ["zh", "en", "ja"];
13469
13498
  const pushed = await pushPostGit(surfdashRoot, commitPaths, `content: daily AI news ${dateStamp} (${commitLocales.join(", ")})`, backfilled);
13470
13499
  if (!pushed.ok)
@@ -316,11 +316,16 @@ interface CliSpawnResult {
316
316
 
317
317
  /** One spawn helper for every CLI-backed operation (postsurfing publish / sd translate / orchestrator sync). */
318
318
  async function spawnCli(cmd: string[], cwd: string): Promise<CliSpawnResult> {
319
+ // postsurfing runs a nested spur workflow in the surfdash repo; a kk publish3 step shell carries
320
+ // the engine's single-flight guard vars, and the guard would refuse that nested run. Same
321
+ // escape the kk-daily-ai-voice3 safety node uses for its intentional nesting.
322
+ const env = { ...process.env };
323
+ for (const key of ['SPUR_WORKFLOW_RUN_ACTIVE', 'SPUR_EXPECTED_DEFINITION_DIGEST']) delete env[key];
319
324
  const proc = Bun.spawn(cmd, {
320
325
  cwd,
321
326
  stdout: 'pipe',
322
327
  stderr: 'pipe',
323
- env: process.env,
328
+ env,
324
329
  });
325
330
  const [exitCode, stdout, stderr] = await Promise.all([
326
331
  proc.exited,
@@ -471,17 +476,41 @@ export function extractFailingPostHint(text: string): string | undefined {
471
476
  return match[0].replace(/[.,;]+$/, '');
472
477
  }
473
478
 
479
+ function extractPostDates(paths: readonly string[]): Set<string> {
480
+ const dates = new Set<string>();
481
+ for (const p of paths) {
482
+ const m = p.replaceAll('\\', '/').match(/(?:^|\/)posts\/articles\/(?:zh|en|ja)\/(\d{8})-/);
483
+ if (m?.[1]) dates.add(m[1]);
484
+ }
485
+ return dates;
486
+ }
487
+
488
+ function isUnrelatedDatePostFailure(output: string, ownedPaths: readonly string[]): boolean {
489
+ const ownedDates = extractPostDates(ownedPaths);
490
+ if (ownedDates.size === 0) return false;
491
+ const failingHints = [...output.matchAll(/(?:content[\\/])?posts[\\/][^\s:)]+/g)].map((m) =>
492
+ m[0].replace(/[.,;]+$/, ''),
493
+ );
494
+ if (failingHints.length === 0) return false;
495
+ for (const hint of failingHints) {
496
+ const m = hint.replaceAll('\\', '/').match(/(?:^|\/)posts\/articles\/(?:zh|en|ja)\/(\d{8})-/);
497
+ if (!m?.[1] || ownedDates.has(m[1])) return false;
498
+ }
499
+ return true;
500
+ }
501
+
474
502
  /**
475
503
  * Runs the surfdash site gates (`bun run sd check digest` then `bun run check:content`)
476
504
  * with cwd = `surfdashRoot` via `spawnCli`.
477
505
  *
478
506
  * If either command exits non-zero:
479
- * - With `SURFDASH_DIGEST_CHECK=warn`: echoes each line of the failure via `echoError`
480
- * prefixed with `site gate (warn):` and continues.
507
+ * - With `SURFDASH_DIGEST_CHECK=warn` (or when every failing post belongs to an unrelated date
508
+ * outside `ownedPaths`): echoes each line of the failure via `echoError` prefixed with
509
+ * `site gate (warn):` and continues.
481
510
  * - With `SURFDASH_DIGEST_CHECK=off`: skips the gate completely.
482
511
  * - Default (unset or any other value): fails loud with command, trimmed output, and post hint.
483
512
  */
484
- export async function runSiteGate(surfdashRoot: string): Promise<SiteGateOutcome> {
513
+ export async function runSiteGate(surfdashRoot: string, ownedPaths: readonly string[] = []): Promise<SiteGateOutcome> {
485
514
  if (process.env.SURFDASH_DIGEST_CHECK === 'off') {
486
515
  return { ok: true };
487
516
  }
@@ -493,7 +522,7 @@ export async function runSiteGate(surfdashRoot: string): Promise<SiteGateOutcome
493
522
  const postHint = extractFailingPostHint(output);
494
523
  const hint = postHint ? ` [failing post: ${postHint}]` : '';
495
524
  const error = `site gate failed: ${cmd.join(' ')} (exit ${spawned.exitCode})${hint}: ${output}`;
496
- if (process.env.SURFDASH_DIGEST_CHECK === 'warn') {
525
+ if (process.env.SURFDASH_DIGEST_CHECK === 'warn' || isUnrelatedDatePostFailure(output, ownedPaths)) {
497
526
  for (const line of error.split('\n')) {
498
527
  echoError(`site gate (warn): ${line}`);
499
528
  }
@@ -735,7 +764,9 @@ export function normalizeLocaleFrontmatter(markdown: string): string {
735
764
  if (close < 0) return markdown;
736
765
  const head = lines
737
766
  .slice(1, close)
738
- .filter((l) => !/^canonical\s*:/.test(l))
767
+ // Only the zh scaffold's canonical is stale; a sibling's own (refined) canonical must survive,
768
+ // or the orchestrator re-dumps the frontmatter and the byte-exact drift gate fails.
769
+ .filter((l) => !/^canonical\s*:.*\/zh\//.test(l))
739
770
  .map((l) => (/^draft\s*:/.test(l) ? 'draft: false' : l));
740
771
  return [lines[0], ...head, ...lines.slice(close)].join('\n');
741
772
  }
@@ -844,7 +875,7 @@ async function patchPost(options: SurfdashPubOptions): Promise<Result> {
844
875
  if (process.env.SURFDASH_AUTO_PUSH !== '0') {
845
876
  const backfilled = await runListenLinkBackfill(surfdashRoot);
846
877
  for (const p of backfilled) commitPaths.add(p);
847
- const gate = await runSiteGate(surfdashRoot);
878
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, relative(surfdashRoot, postPath.postPath)]);
848
879
  if (!gate.ok) return fail(gate.error);
849
880
  }
850
881
  const commit = await commitPostPaths(
@@ -1078,9 +1109,15 @@ async function preparePost(payload: PublishTransportPayload): Promise<PrepareOut
1078
1109
  return { ok: false, result: failNotSent(`SURFDASH_LOCALE must be one of zh/en/ja, got '${rawLocale}'`) };
1079
1110
  }
1080
1111
  const locale = rawLocale as SupportedLocale;
1081
- const runDate = typeof payload.content.metadata?.runDate === 'string' ? payload.content.metadata.runDate : '';
1082
- const today = new Date().toISOString().slice(0, 10).replaceAll('-', '');
1083
- const dateStamp = runDate || today;
1112
+ // dateStamp source: explicit metadata.runDate, else the payload's own metadata.date —
1113
+ // never the wall clock, which around midnight local time mints `tomorrow` slugs.
1114
+ const runDate =
1115
+ typeof payload.content.metadata?.runDate === 'string' && payload.content.metadata.runDate !== ''
1116
+ ? payload.content.metadata.runDate
1117
+ : typeof payload.content.metadata?.date === 'string'
1118
+ ? payload.content.metadata.date.replaceAll('-', '')
1119
+ : '';
1120
+ const dateStamp = runDate || new Date().toISOString().slice(0, 10).replaceAll('-', '');
1084
1121
  const titleSlug = slugify(payload.content.title ?? '');
1085
1122
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
1086
1123
  if (runDate !== '' && !/^\d{8}$/.test(runDate)) {
@@ -1109,20 +1146,30 @@ async function preparePost(payload: PublishTransportPayload): Promise<PrepareOut
1109
1146
 
1110
1147
  const filePath = join(postDir, 'index.md');
1111
1148
  try {
1112
- await createNodeFileSystem().ensureDir(postDir);
1113
- let imageFrontmatter: string | undefined;
1114
- if (typeof coverPath === 'string') {
1115
- const assetsDir = join(postDir, 'assets');
1116
- await createNodeFileSystem().ensureDir(assetsDir);
1117
- const coverName = `cover${extname(coverPath).toLowerCase()}`;
1118
- await copyFile(coverPath, join(assetsDir, coverName));
1119
- imageFrontmatter = `./assets/${coverName}`;
1149
+ // A post the site's own refine step already marked is not ours to clobber: re-minting it
1150
+ // replaces the refined canonical frontmatter (refine owns `refined`, `seo_title`, …) with
1151
+ // generator defaults and flips an already-published day back to a draft. Keep the file and
1152
+ // reuse it; only genuinely fresh days (no marker) get minted (2026-11-05 tier-0 rerun).
1153
+ if (readRefinedSnapshot(filePath) === undefined) {
1154
+ await createNodeFileSystem().ensureDir(postDir);
1155
+ let imageFrontmatter: string | undefined;
1156
+ if (typeof coverPath === 'string') {
1157
+ const assetsDir = join(postDir, 'assets');
1158
+ await createNodeFileSystem().ensureDir(assetsDir);
1159
+ const coverName = `cover${extname(coverPath).toLowerCase()}`;
1160
+ await copyFile(coverPath, join(assetsDir, coverName));
1161
+ imageFrontmatter = `./assets/${coverName}`;
1162
+ }
1163
+ const markdown = mapContentToSurfingMarkdown(payload.content, {
1164
+ image: imageFrontmatter,
1165
+ publishDate: runDate
1166
+ ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`
1167
+ : undefined,
1168
+ });
1169
+ await createNodeFileSystem().writeFile(filePath, markdown);
1170
+ } else {
1171
+ echoError(`surfdash-pub: keeping existing refined post at ${filePath} (not re-minting)`);
1120
1172
  }
1121
- const markdown = mapContentToSurfingMarkdown(payload.content, {
1122
- image: imageFrontmatter,
1123
- publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined,
1124
- });
1125
- await createNodeFileSystem().writeFile(filePath, markdown);
1126
1173
  } catch (err: unknown) {
1127
1174
  return {
1128
1175
  ok: false,
@@ -1236,9 +1283,9 @@ async function publishPreparedFile(
1236
1283
  });
1237
1284
  }
1238
1285
  const backfilled = await runListenLinkBackfill(surfdashRoot);
1239
- const gate = await runSiteGate(surfdashRoot);
1240
- if (!gate.ok) return failUncertain(gate.error);
1241
1286
  const commitPaths = relPostDirs ?? [relPostDir];
1287
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, ...backfilled]);
1288
+ if (!gate.ok) return failUncertain(gate.error);
1242
1289
  const commitLocales: SupportedLocale[] = relPostDirs === undefined ? [locale] : ['zh', 'en', 'ja'];
1243
1290
  const pushed = await pushPostGit(
1244
1291
  surfdashRoot,
@@ -12391,6 +12391,13 @@ class ThreadsPostApiTransport {
12391
12391
  const unsupported = refuseUnsupportedOptions(payload.options, payload.content);
12392
12392
  if (unsupported !== undefined)
12393
12393
  return fail(unsupported);
12394
+ const requestedStatus = payload.content.metadata?.publishStatus;
12395
+ if (requestedStatus === "draft") {
12396
+ return fail("threads post has no draft: the Threads API publishes immediately \u2014 plan this target dummy/off or launch it live");
12397
+ }
12398
+ if (requestedStatus !== undefined && requestedStatus !== "public") {
12399
+ return fail(`unknown publishStatus '${String(requestedStatus)}' (expected absent, 'draft' or 'public')`);
12400
+ }
12394
12401
  const missing = TOKEN_ENVS.filter((key) => {
12395
12402
  const value = process.env[key];
12396
12403
  return !value || value.trim().length === 0;
@@ -146,6 +146,18 @@ export class ThreadsPostApiTransport implements PublishTransport {
146
146
  const unsupported = refuseUnsupportedOptions(payload.options, payload.content);
147
147
  if (unsupported !== undefined) return fail(unsupported);
148
148
 
149
+ // 0330 R2: the post API publishes immediately — there is no draft. Intercept before the
150
+ // credential lookup so a planned draft never reaches Threads, and fail closed on unknown modes.
151
+ const requestedStatus = payload.content.metadata?.publishStatus;
152
+ if (requestedStatus === 'draft') {
153
+ return fail(
154
+ 'threads post has no draft: the Threads API publishes immediately — plan this target dummy/off or launch it live',
155
+ );
156
+ }
157
+ if (requestedStatus !== undefined && requestedStatus !== 'public') {
158
+ return fail(`unknown publishStatus '${String(requestedStatus)}' (expected absent, 'draft' or 'public')`);
159
+ }
160
+
149
161
  const missing = TOKEN_ENVS.filter((key) => {
150
162
  const value = process.env[key];
151
163
  return !value || value.trim().length === 0;