@gobing-ai/knowledge-kit 0.1.12 → 0.1.14

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 (25) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +16 -4
  3. package/package.json +1 -1
  4. package/plugins/kk/plugin.json +1 -1
  5. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +3 -3
  6. package/plugins/kk/publish-presets/daily-ai-voice.json +4 -4
  7. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +2 -2
  8. package/plugins/kk/scripts/daily-scheduled-run.ts +138 -44
  9. package/plugins/kk/scripts/daily-stages.ts +52 -9
  10. package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
  11. package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
  12. package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
  13. package/plugins/kk/workflows/kk-collect.yaml +3 -3
  14. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +12 -12
  15. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +2 -2
  16. package/plugins/publishings/surfdash-pub/dist/index.js +52 -23
  17. package/plugins/publishings/surfdash-pub/src/index.ts +69 -24
  18. package/plugins/publishings/wechatmp-pub/dist/index.js +171 -30
  19. package/plugins/publishings/wechatmp-pub/src/flow.ts +287 -49
  20. package/plugins/publishings/weibo-pub/dist/index.js +114 -8
  21. package/plugins/publishings/weibo-pub/src/index.ts +140 -16
  22. package/plugins/publishings/xhs-pub/dist/index.js +164 -12
  23. package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
  24. package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
  25. package/plugins/publishings/xhs-pub/src/page.ts +26 -0
@@ -48,10 +48,10 @@ states:
48
48
  onEnter:
49
49
  - kind: shell
50
50
  options:
51
- # Reporting only: a ledger failure must never abort a tick before the lock is even taken.
52
- onError: continue
53
51
  command: >-
54
52
  kk ledger sync --workflow kk-collect
53
+ # Reporting only: a ledger failure must never abort a tick before the lock is even taken.
54
+ onError: continue
55
55
  - kind: shell
56
56
  options:
57
57
  # stamp: the pinned var, else the UTC minute (R4). work_dir: `.works/kk-collect/<stamp>` (R4),
@@ -92,13 +92,13 @@ states:
92
92
  # revocation warns (captured:false) and only disables Board previews.
93
93
  # onError: continue keeps the lock-releasing failure path intact instead of a hard abort.
94
94
  options:
95
- onError: continue
96
95
  command: >-
97
96
  kk board capture --workflow kk-collect --run "${vars.__runId}"
98
97
  --work-dir "${vars.work_dir}" --stamp "${vars.stamp}"
99
98
  --definition-digest "${vars.__definitionDigest}"
100
99
  --setting "limit=${vars.limit}" --setting "horizon_hours=${vars.horizon_hours}"
101
100
  > "${vars.work_dir}/board-capture.json"
101
+ onError: continue
102
102
  - kind: shell
103
103
  # onError: continue on both ingestion actions, with the artifact guard below as the positive
104
104
  # success contract: a state-machine aborts the run on a failed action without taking a
@@ -221,10 +221,10 @@ nodes:
221
221
  action:
222
222
  kind: shell
223
223
  options:
224
- onError: continue
225
224
  command: >-
226
225
  kk plugin check wechatmp-pub xhs-pub --timeout 60000
227
226
  || echo "session-preflight: a publish channel's saved session was refused — fix it before this run's publish step with: bun plugins/publishings/<channel>-pub/src/profile-check.ts login (wechatmp or xhs)"
227
+ onError: continue
228
228
 
229
229
  - id: extract
230
230
  description: >-
@@ -689,7 +689,7 @@ nodes:
689
689
  options:
690
690
  command: >-
691
691
  kk stage daily-voice-audit "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
692
- onError: continue
692
+ onError: continue
693
693
 
694
694
  - id: duration
695
695
  description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates, exit 0 — 0266); above episode_max_minutes it still fails loud. Duration is authoritative."
@@ -839,15 +839,6 @@ nodes:
839
839
  action:
840
840
  kind: agent.run
841
841
  options:
842
- # 20261012 hardening: this is the LAST agent step and a translation failure must not abort
843
- # the overall report/podcast close, yet the step carried no tolerance and no retry sibling — a stochastic
844
- # executor failure here aborted the DAG *after* surfdash and podcast had published, so
845
- # publish-classify, mark-published, run-report and ledger never ran for an otherwise complete
846
- # day. Tolerating the failure is safe because a scaffold is an unpublished draft: the prompt
847
- # below is what flips `draft: true` → `draft: false`, so an untranslated locale stays off the
848
- # site, and `daily-publish-completeness` now reads that flag and reports the locale as failed
849
- # instead of calling a scaffold `done`.
850
- onError: continue
851
842
  role: scribe
852
843
  agent: ${vars.agent}
853
844
  timeoutMs: 600000
@@ -863,6 +854,15 @@ nodes:
863
854
  Series names are fixed product identity, never invented or freely translated: the en series name is "Daily AI Stew" (show "Salty Talk"); the ja series name is "デイリーAIシチュー" (show "咸話咸説"). Never emit 毎日AIブレンド, ごった煮, "Daily AI Medley", "hodgepodge", "Daily AI Briefing", or the zh name 每日AI乱炖 in en/ja prose or link text (a URL keeps whatever it contains). Openers: en "Your Daily AI Stew from Robin, on Salty Talk."; ja "咸話咸説のデイリーAIシチュー、Robin がお届けします。" Listen links: en "[listen to Daily AI Stew · <date>](<url>)", ja "[デイリーAIシチュー · <date> を聴く](<url>)". In each key_points item keep the "Label — sentence" shape: the label before " — " stays a short phrase of at most 14 characters (ja) / 28 (en), never truncated with "…". The local translate-validate stage rewrites known retired names deterministically and fails the publish gate on leftovers.
864
855
  Keep frontmatter valid YAML: double-quote any value containing ': '. Read the surfdash translator glossary if present at the surfdash checkout root under .agents/skills/surfdash-translator/glossary.md and apply its terminology.
865
856
  Do not wait for operator confirmation. Each written file must end fully in its target language.
857
+ # 20261012 hardening: this is the LAST agent step and a translation failure must not abort
858
+ # the overall report/podcast close, yet the step carried no tolerance and no retry sibling — a stochastic
859
+ # executor failure here aborted the DAG *after* surfdash and podcast had published, so
860
+ # publish-classify, mark-published, run-report and ledger never ran for an otherwise complete
861
+ # day. Tolerating the failure is safe because a scaffold is an unpublished draft: the prompt
862
+ # above is what flips `draft: true` → `draft: false`, so an untranslated locale stays off the
863
+ # site, and `daily-publish-completeness` now reads that flag and reports the locale as failed
864
+ # instead of calling a scaffold `done`.
865
+ onError: continue
866
866
 
867
867
  # 2026-10-24: the locale translation carried `onError: continue` with NO retry sibling, so a
868
868
  # failed executor left the en/ja drafts untranslated and the old sync stage could not distinguish
@@ -880,11 +880,11 @@ nodes:
880
880
  action:
881
881
  kind: agent.run
882
882
  options:
883
- onError: continue
884
883
  role: reviewer
885
884
  agent: ${vars.agent_retry}
886
885
  timeoutMs: 600000
887
886
  input: *translate-input
887
+ onError: continue
888
888
 
889
889
  - id: translate-refine
890
890
  description: "Run the Surfdash-owned refine pass over zh/en/ja locally before the locale fingerprint is admitted; no CMS, git, or remote side effect"
@@ -194,10 +194,10 @@ nodes:
194
194
  action:
195
195
  kind: shell
196
196
  options:
197
- onError: continue
198
197
  command: >-
199
198
  kk stage run publish-matrix-check "${vars.work_dir}" --all-windows
200
199
  || echo "session-preflight: a publish channel's saved session was refused — fix it with: kk bot login <channel>"
200
+ onError: continue
201
201
 
202
202
  - id: extract
203
203
  description: >-
@@ -695,7 +695,7 @@ nodes:
695
695
  options:
696
696
  command: >-
697
697
  kk stage daily-voice-audit "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
698
- onError: continue
698
+ onError: continue
699
699
 
700
700
  - id: duration
701
701
  description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates + publish-path invalidation, exit 0 — 0266/0329); above episode_max_minutes it still fails loud. Duration is authoritative."
@@ -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}`);
@@ -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
  }
@@ -846,7 +875,7 @@ async function patchPost(options: SurfdashPubOptions): Promise<Result> {
846
875
  if (process.env.SURFDASH_AUTO_PUSH !== '0') {
847
876
  const backfilled = await runListenLinkBackfill(surfdashRoot);
848
877
  for (const p of backfilled) commitPaths.add(p);
849
- const gate = await runSiteGate(surfdashRoot);
878
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, relative(surfdashRoot, postPath.postPath)]);
850
879
  if (!gate.ok) return fail(gate.error);
851
880
  }
852
881
  const commit = await commitPostPaths(
@@ -1080,9 +1109,15 @@ async function preparePost(payload: PublishTransportPayload): Promise<PrepareOut
1080
1109
  return { ok: false, result: failNotSent(`SURFDASH_LOCALE must be one of zh/en/ja, got '${rawLocale}'`) };
1081
1110
  }
1082
1111
  const locale = rawLocale as SupportedLocale;
1083
- const runDate = typeof payload.content.metadata?.runDate === 'string' ? payload.content.metadata.runDate : '';
1084
- const today = new Date().toISOString().slice(0, 10).replaceAll('-', '');
1085
- 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('-', '');
1086
1121
  const titleSlug = slugify(payload.content.title ?? '');
1087
1122
  const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
1088
1123
  if (runDate !== '' && !/^\d{8}$/.test(runDate)) {
@@ -1111,20 +1146,30 @@ async function preparePost(payload: PublishTransportPayload): Promise<PrepareOut
1111
1146
 
1112
1147
  const filePath = join(postDir, 'index.md');
1113
1148
  try {
1114
- await createNodeFileSystem().ensureDir(postDir);
1115
- let imageFrontmatter: string | undefined;
1116
- if (typeof coverPath === 'string') {
1117
- const assetsDir = join(postDir, 'assets');
1118
- await createNodeFileSystem().ensureDir(assetsDir);
1119
- const coverName = `cover${extname(coverPath).toLowerCase()}`;
1120
- await copyFile(coverPath, join(assetsDir, coverName));
1121
- 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)`);
1122
1172
  }
1123
- const markdown = mapContentToSurfingMarkdown(payload.content, {
1124
- image: imageFrontmatter,
1125
- publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined,
1126
- });
1127
- await createNodeFileSystem().writeFile(filePath, markdown);
1128
1173
  } catch (err: unknown) {
1129
1174
  return {
1130
1175
  ok: false,
@@ -1238,9 +1283,9 @@ async function publishPreparedFile(
1238
1283
  });
1239
1284
  }
1240
1285
  const backfilled = await runListenLinkBackfill(surfdashRoot);
1241
- const gate = await runSiteGate(surfdashRoot);
1242
- if (!gate.ok) return failUncertain(gate.error);
1243
1286
  const commitPaths = relPostDirs ?? [relPostDir];
1287
+ const gate = await runSiteGate(surfdashRoot, [...commitPaths, ...backfilled]);
1288
+ if (!gate.ok) return failUncertain(gate.error);
1244
1289
  const commitLocales: SupportedLocale[] = relPostDirs === undefined ? [locale] : ['zh', 'en', 'ja'];
1245
1290
  const pushed = await pushPostGit(
1246
1291
  surfdashRoot,