@gobing-ai/knowledge-kit 0.1.8 → 0.1.10

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 (78) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.js +725 -1101
  3. package/dist/board/daily-voice/index.js +722 -101
  4. package/dist/board/itc/index.js +13 -0
  5. package/dist/index.js +1796 -102
  6. package/package.json +1 -1
  7. package/plugins/generations/content-gen/dist/index.js +4 -0
  8. package/plugins/generations/core-facts-gen/dist/index.js +4 -0
  9. package/plugins/generations/daily-article-gen/dist/index.js +4 -0
  10. package/plugins/generations/dailynews-gen/dist/index.js +4 -0
  11. package/plugins/generations/episode-plan-gen/dist/index.js +102 -1
  12. package/plugins/generations/episode-plan-gen/src/index.ts +2 -4
  13. package/plugins/generations/gemini-tts/dist/index.js +4 -0
  14. package/plugins/generations/image-gen/dist/index.js +4 -0
  15. package/plugins/generations/news-report-gen/dist/index.js +4 -0
  16. package/plugins/generations/voice-gen/dist/index.js +4 -0
  17. package/plugins/generations/voxcpm-tts/dist/index.js +4 -0
  18. package/plugins/ingestions/aihot-ingest/dist/index.js +4 -0
  19. package/plugins/ingestions/horizon-ingest/dist/index.js +28 -1
  20. package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
  21. package/plugins/ingestions/last30days-ingest/dist/index.js +11 -3
  22. package/plugins/ingestions/last30days-ingest/src/index.ts +14 -3
  23. package/plugins/ingestions/web-search/dist/index.js +4 -0
  24. package/plugins/kk/config.example.yaml +8 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
  27. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
  28. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
  29. package/plugins/kk/scripts/daily-stages.ts +665 -78
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
  31. package/plugins/kk/scripts/sentences.ts +134 -0
  32. package/plugins/kk/scripts/series-names.ts +124 -0
  33. package/plugins/kk/scripts/topic-boundary.json +18 -83
  34. package/plugins/kk/workflows/kk-collect.yaml +2 -0
  35. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +112 -9
  36. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
  37. package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +50 -5
  39. package/plugins/publishings/bilibili-pub/dist/index.js +4 -0
  40. package/plugins/publishings/cnblogs-pub/dist/index.js +4 -0
  41. package/plugins/publishings/csdn-pub/dist/index.js +4 -0
  42. package/plugins/publishings/csdn-pub/dist/profile-check.js +4 -0
  43. package/plugins/publishings/emdash-pub/dist/index.js +4 -0
  44. package/plugins/publishings/infoq-pub/dist/index.js +4 -0
  45. package/plugins/publishings/infoq-pub/dist/profile-check.js +4 -0
  46. package/plugins/publishings/jike-pub/dist/index.js +4 -0
  47. package/plugins/publishings/jike-pub/dist/profile-check.js +4 -0
  48. package/plugins/publishings/juejin-pub/dist/index.js +4 -0
  49. package/plugins/publishings/juejin-pub/dist/profile-check.js +4 -0
  50. package/plugins/publishings/medium-pub/dist/index.js +4 -0
  51. package/plugins/publishings/podcast-pub/README.md +65 -0
  52. package/plugins/publishings/podcast-pub/dist/index.js +1906 -374
  53. package/plugins/publishings/podcast-pub/plugin.json +1 -1
  54. package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
  55. package/plugins/publishings/podcast-pub/src/index.ts +211 -34
  56. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
  57. package/plugins/publishings/podcast-pub/src/seo.ts +1079 -0
  58. package/plugins/publishings/podcast-pub/src/show-notes.ts +74 -14
  59. package/plugins/publishings/qiita-pub/dist/index.js +4 -0
  60. package/plugins/publishings/shipinhao-pub/dist/index.js +4 -0
  61. package/plugins/publishings/sspai-pub/dist/index.js +4 -0
  62. package/plugins/publishings/substack-pub/dist/index.js +4 -0
  63. package/plugins/publishings/substack-pub/dist/profile-check.js +4 -0
  64. package/plugins/publishings/surfdash-pub/dist/index.js +313 -32
  65. package/plugins/publishings/surfdash-pub/src/digest-check.ts +218 -0
  66. package/plugins/publishings/surfdash-pub/src/index.ts +170 -39
  67. package/plugins/publishings/surfdash-pub/src/patch.ts +44 -7
  68. package/plugins/publishings/threads-pub/dist/index.js +4 -0
  69. package/plugins/publishings/wechatmp-pub/dist/index.js +4 -0
  70. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +4 -0
  71. package/plugins/publishings/weibo-pub/dist/index.js +4 -0
  72. package/plugins/publishings/weibo-pub/dist/profile-check.js +4 -0
  73. package/plugins/publishings/x-pub/dist/index.js +4 -0
  74. package/plugins/publishings/x-pub/dist/profile-check.js +4 -0
  75. package/plugins/publishings/xhs-pub/dist/index.js +4 -0
  76. package/plugins/publishings/xhs-pub/dist/profile-check.js +4 -0
  77. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +4 -0
  78. package/plugins/publishings/zenn-pub/dist/index.js +4 -0
package/dist/index.js CHANGED
@@ -42385,6 +42385,10 @@ function getByName(result, name) {
42385
42385
  }
42386
42386
  // ../../packages/kk-core/src/render-md.ts
42387
42387
  if (false) {}
42388
+ // ../../packages/kk-core/src/sentences.ts
42389
+ var CJK_TERMINATORS = new Set(["\u3002", "\uFF01", "\uFF1F"]);
42390
+ var LATIN_TERMINATORS = new Set([".", "!", "?"]);
42391
+ var ATTACHED_CLOSERS = new Set(["\u201D", "\u2019", "\u300D", "\u300F", "\uFF09", ")", '"', "'"]);
42388
42392
  // src/invoke.ts
42389
42393
  init_dist();
42390
42394
 
@@ -42732,6 +42736,9 @@ async function runInvoke(opts) {
42732
42736
  echoError(`Plugin entry '${record.name}' failed (exit code ${proc.exitCode}): ${errMsg}`);
42733
42737
  return proc.exitCode;
42734
42738
  }
42739
+ if (proc.stderr !== "") {
42740
+ echoError(`[plugin:${record.name}] ${proc.stderr}`);
42741
+ }
42735
42742
  if (!existsSync3(absOutPath)) {
42736
42743
  echoError(`Plugin entry succeeded but did not write output file: ${absOutPath}`);
42737
42744
  return 1;
@@ -42977,6 +42984,90 @@ var DAILY_VOICE_ARTIFACTS = [
42977
42984
  kind: "text",
42978
42985
  pattern: "{stamp}_19_run-report_report.md",
42979
42986
  producerNode: "run-report"
42987
+ },
42988
+ {
42989
+ id: "episode-pool",
42990
+ label: "Episode pool",
42991
+ kind: "json",
42992
+ pattern: "10-collect/pool.json",
42993
+ producerNode: "extract"
42994
+ },
42995
+ {
42996
+ id: "episode-annotated",
42997
+ label: "QC annotation input",
42998
+ kind: "json",
42999
+ pattern: "21-plan/{stamp}_04_quality-control-content_annotated.json",
43000
+ producerNode: "qc-content"
43001
+ },
43002
+ {
43003
+ id: "episode-qc-rejections",
43004
+ label: "QC rejections",
43005
+ kind: "json",
43006
+ pattern: "21-plan/{stamp}_04_quality-control-content_candidates.rejected.json",
43007
+ producerNode: "qc-content"
43008
+ },
43009
+ {
43010
+ id: "episode-exclusions",
43011
+ label: "Topic exclusions",
43012
+ kind: "json",
43013
+ pattern: "21-plan/{stamp}_04_quality-control-content_exclusions.json",
43014
+ producerNode: "qc-content"
43015
+ },
43016
+ {
43017
+ id: "episode-candidates",
43018
+ label: "Post-QC candidates",
43019
+ kind: "json",
43020
+ pattern: "21-plan/{stamp}_04_quality-control-content_candidates.json",
43021
+ producerNode: "qc-content"
43022
+ },
43023
+ {
43024
+ id: "episode-plan",
43025
+ label: "Episode plan",
43026
+ kind: "json",
43027
+ pattern: "21-plan/{stamp}_03_plan_plan.json",
43028
+ producerNode: "plan"
43029
+ },
43030
+ {
43031
+ id: "episode-floor-refusal",
43032
+ label: "QC floor refusal",
43033
+ kind: "json",
43034
+ pattern: "21-plan/{stamp}_04_quality-control-content_floor-refusal.json",
43035
+ producerNode: "qc-content"
43036
+ },
43037
+ {
43038
+ id: "episode-duration",
43039
+ label: "Duration verdict",
43040
+ kind: "json",
43041
+ pattern: "30-audio/{stamp}_11_generate_content_duration.json",
43042
+ producerNode: "duration"
43043
+ },
43044
+ {
43045
+ id: "episode-duration-refusal",
43046
+ label: "Duration refusal",
43047
+ kind: "json",
43048
+ pattern: "30-audio/{stamp}_11_generate_content_duration-refusal.json",
43049
+ producerNode: "duration"
43050
+ },
43051
+ {
43052
+ id: "episode-script-refusal",
43053
+ label: "Script refusal",
43054
+ kind: "json",
43055
+ pattern: "30-audio/{stamp}_11_generate_content_duration-refusal.json",
43056
+ producerNode: "generate"
43057
+ },
43058
+ {
43059
+ id: "episode-script-refusal-fallback",
43060
+ label: "Script refusal (fallback)",
43061
+ kind: "json",
43062
+ pattern: "30-audio/{stamp}_11_generate_content_duration-refusal.json",
43063
+ producerNode: "generate-fallback"
43064
+ },
43065
+ {
43066
+ id: "episode-script-refusal-fallback2",
43067
+ label: "Script refusal (second fallback)",
43068
+ kind: "json",
43069
+ pattern: "30-audio/{stamp}_11_generate_content_duration-refusal.json",
43070
+ producerNode: "generate-fallback2"
42980
43071
  }
42981
43072
  ];
42982
43073
  var ITC_ARTIFACTS = [
@@ -44621,6 +44712,23 @@ function statusOf(rank) {
44621
44712
  return "dropped";
44622
44713
  return "none";
44623
44714
  }
44715
+ function parseSourceCounts(raw) {
44716
+ let parsed;
44717
+ try {
44718
+ parsed = JSON.parse(raw);
44719
+ } catch {
44720
+ return null;
44721
+ }
44722
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
44723
+ return null;
44724
+ const counts = {};
44725
+ for (const [key, value] of Object.entries(parsed)) {
44726
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
44727
+ return null;
44728
+ counts[key] = value;
44729
+ }
44730
+ return counts;
44731
+ }
44624
44732
 
44625
44733
  class CollectViewDao extends BaseDao {
44626
44734
  constructor(adapter) {
@@ -44721,6 +44829,66 @@ class CollectViewDao extends BaseDao {
44721
44829
  const rows = await this.adapter.queryAll("SELECT DISTINCT topic AS topic FROM selections WHERE topic <> '' ORDER BY topic");
44722
44830
  return rows.map((row) => row.topic);
44723
44831
  }
44832
+ async channelYield(sinceMs, untilMs) {
44833
+ const rows = untilMs === undefined ? await this.adapter.queryAll(`SELECT date(first_seen_at / 1000, 'unixepoch', 'localtime') AS day,
44834
+ source AS source,
44835
+ COUNT(*) AS items
44836
+ FROM items WHERE first_seen_at >= ?
44837
+ GROUP BY day, source
44838
+ ORDER BY day DESC, items DESC, source ASC`, sinceMs) : await this.adapter.queryAll(`SELECT date(first_seen_at / 1000, 'unixepoch', 'localtime') AS day,
44839
+ source AS source,
44840
+ COUNT(*) AS items
44841
+ FROM items WHERE first_seen_at >= ? AND first_seen_at < ?
44842
+ GROUP BY day, source
44843
+ ORDER BY day DESC, items DESC, source ASC`, sinceMs, untilMs);
44844
+ return rows.map((row) => ({
44845
+ day: row.day,
44846
+ source: row.source === "" ? "(none)" : row.source,
44847
+ items: row.items
44848
+ }));
44849
+ }
44850
+ async channelRunYield(fromMs, untilMs) {
44851
+ const rows = await this.adapter.queryAll(`SELECT date(started_at / 1000, 'unixepoch', 'localtime') AS day,
44852
+ started_at AS startedAt,
44853
+ source_counts_json AS sourceCountsJson
44854
+ FROM collect_runs WHERE started_at >= ? AND started_at < ?
44855
+ ORDER BY started_at ASC`, fromMs, untilMs);
44856
+ const byDay = new Map;
44857
+ for (const row of rows) {
44858
+ const entry = byDay.get(row.day) ?? { runs: 0, plugins: {} };
44859
+ entry.runs += 1;
44860
+ const counts = parseSourceCounts(row.sourceCountsJson);
44861
+ if (counts === null) {
44862
+ entry.plugins = null;
44863
+ } else if (entry.plugins !== null) {
44864
+ for (const [plugin, count] of Object.entries(counts)) {
44865
+ entry.plugins[plugin] = (entry.plugins[plugin] ?? 0) + count;
44866
+ }
44867
+ }
44868
+ byDay.set(row.day, entry);
44869
+ }
44870
+ const history = await this.adapter.queryAll(`SELECT started_at AS startedAt, source_counts_json AS sourceCountsJson
44871
+ FROM collect_runs WHERE started_at < ?
44872
+ ORDER BY started_at ASC`, untilMs);
44873
+ const lastPositiveAt = {};
44874
+ for (const row of history) {
44875
+ const counts = parseSourceCounts(row.sourceCountsJson);
44876
+ if (counts === null)
44877
+ continue;
44878
+ for (const [plugin, count] of Object.entries(counts)) {
44879
+ if (count > 0)
44880
+ lastPositiveAt[plugin] = row.startedAt;
44881
+ }
44882
+ }
44883
+ return {
44884
+ days: [...byDay.entries()].map(([day, entry]) => ({
44885
+ day,
44886
+ runs: entry.runs,
44887
+ plugins: entry.plugins
44888
+ })),
44889
+ lastPositiveAt
44890
+ };
44891
+ }
44724
44892
  async itemsPage(runId, filter, page, policy) {
44725
44893
  const params = [];
44726
44894
  const conditions = [];
@@ -45737,6 +45905,12 @@ var SOURCE_COUNTS = {
45737
45905
  code: "board.store.row-invalid",
45738
45906
  message: "A stored run row carries source counts that are not valid JSON."
45739
45907
  };
45908
+ var NAMED_PLUGINS = ["aihot-ingest", "horizon-ingest", "last30days-ingest"];
45909
+ var CHANNELS_PAGE_SIZE = 30;
45910
+ var CHANNEL_INVALID = {
45911
+ code: "board.store.channel-invalid",
45912
+ message: "A stored run row carries plugin counts that are not nonnegative integers."
45913
+ };
45740
45914
  async function sessionFor(deps) {
45741
45915
  const fs = deps.fs ?? createNodeFileSystem();
45742
45916
  const dbPath = join5(deps.projectPath, DEFAULT_STORE_DB_PATH);
@@ -46043,6 +46217,103 @@ function emptyItems(runId, query, sources) {
46043
46217
  diagnostics: []
46044
46218
  };
46045
46219
  }
46220
+ function hostTimeZone() {
46221
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
46222
+ }
46223
+ function emptyChannels(query, diagnostics) {
46224
+ return {
46225
+ schemaVersion: 1,
46226
+ availability: "unavailable",
46227
+ fromMs: query.fromMs,
46228
+ untilMs: query.untilMs,
46229
+ timeZone: hostTimeZone(),
46230
+ page: query.page,
46231
+ pageSize: CHANNELS_PAGE_SIZE,
46232
+ totalDays: 0,
46233
+ sources: [],
46234
+ plugins: [],
46235
+ byDay: [],
46236
+ bySource: {},
46237
+ runsByDay: [],
46238
+ lastPositiveAt: {},
46239
+ diagnostics
46240
+ };
46241
+ }
46242
+ async function storeChannels(deps, query) {
46243
+ const session = await sessionFor(deps);
46244
+ if (!session.ok)
46245
+ return emptyChannels(query, session.diagnostics);
46246
+ const { store, diagnostics } = session.session;
46247
+ try {
46248
+ const [itemDays, runYield] = await Promise.all([
46249
+ store.collectView.channelYield(query.fromMs, query.untilMs),
46250
+ store.collectView.channelRunYield(query.fromMs, query.untilMs)
46251
+ ]);
46252
+ const sources = [...new Set(itemDays.map((row) => row.source))].sort();
46253
+ const bySource = {};
46254
+ const itemByDay = new Map;
46255
+ for (const row of itemDays) {
46256
+ const entry = itemByDay.get(row.day) ?? { total: 0, sources: {} };
46257
+ entry.total += row.items;
46258
+ entry.sources[row.source] = (entry.sources[row.source] ?? 0) + row.items;
46259
+ itemByDay.set(row.day, entry);
46260
+ bySource[row.source] = (bySource[row.source] ?? 0) + row.items;
46261
+ }
46262
+ const runByDay = new Map(runYield.days.map((day) => [day.day, day]));
46263
+ const observedPlugins = new Set;
46264
+ for (const day of runYield.days) {
46265
+ if (day.plugins !== null)
46266
+ for (const plugin of Object.keys(day.plugins))
46267
+ observedPlugins.add(plugin);
46268
+ }
46269
+ for (const plugin of Object.keys(runYield.lastPositiveAt))
46270
+ observedPlugins.add(plugin);
46271
+ const plugins = [...new Set([...NAMED_PLUGINS, ...observedPlugins])].sort();
46272
+ const days = [...new Set([...itemByDay.keys(), ...runByDay.keys()])].sort().reverse();
46273
+ const totalDays = days.length;
46274
+ const offset = (Math.max(1, query.page) - 1) * CHANNELS_PAGE_SIZE;
46275
+ const pageDays = days.slice(offset, offset + CHANNELS_PAGE_SIZE);
46276
+ const byDay = pageDays.map((day) => {
46277
+ const item = itemByDay.get(day);
46278
+ const sourcesOfDay = {};
46279
+ for (const source of sources)
46280
+ sourcesOfDay[source] = item?.sources[source] ?? 0;
46281
+ return { day, total: item?.total ?? 0, sources: sourcesOfDay };
46282
+ });
46283
+ const runsByDay = pageDays.map((day) => {
46284
+ const run = runByDay.get(day);
46285
+ const pluginsOfDay = run?.plugins === null ? null : Object.fromEntries(plugins.map((plugin) => [plugin, run?.plugins?.[plugin] ?? 0]));
46286
+ return { day, runs: run?.runs ?? 0, plugins: pluginsOfDay };
46287
+ });
46288
+ const lastPositiveAt = {};
46289
+ for (const plugin of plugins)
46290
+ lastPositiveAt[plugin] = runYield.lastPositiveAt[plugin] ?? null;
46291
+ const channelDiagnostics = [...diagnostics];
46292
+ if (runYield.days.some((day) => day.plugins === null))
46293
+ channelDiagnostics.push(CHANNEL_INVALID);
46294
+ return {
46295
+ schemaVersion: 1,
46296
+ availability: "available",
46297
+ fromMs: query.fromMs,
46298
+ untilMs: query.untilMs,
46299
+ timeZone: hostTimeZone(),
46300
+ page: query.page,
46301
+ pageSize: CHANNELS_PAGE_SIZE,
46302
+ totalDays,
46303
+ sources,
46304
+ plugins,
46305
+ byDay,
46306
+ bySource,
46307
+ runsByDay,
46308
+ lastPositiveAt,
46309
+ diagnostics: channelDiagnostics
46310
+ };
46311
+ } catch {
46312
+ return emptyChannels(query, [...diagnostics, UNREADABLE]);
46313
+ } finally {
46314
+ await store.close();
46315
+ }
46316
+ }
46046
46317
 
46047
46318
  // src/board/daily-voice.ts
46048
46319
  var isTrue = (settings, key) => settings[key] === "true";
@@ -46258,6 +46529,280 @@ function channelOutcomes(settings, availability, textOf) {
46258
46529
  function stringArray(value) {
46259
46530
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
46260
46531
  }
46532
+ var FUNNEL_EXCLUSION_ROWS = 100;
46533
+ var FUNNEL_TEXT_CHARS = 200;
46534
+ function parseAnyJson(raw) {
46535
+ if (raw === undefined)
46536
+ return null;
46537
+ try {
46538
+ return JSON.parse(raw);
46539
+ } catch {
46540
+ return null;
46541
+ }
46542
+ }
46543
+ function nonNegativeNumber(value) {
46544
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
46545
+ }
46546
+ function arrayCount(value) {
46547
+ return Array.isArray(value) ? value.length : null;
46548
+ }
46549
+ function minutesFromSeconds(seconds) {
46550
+ return Math.round(seconds / 60 * 10) / 10;
46551
+ }
46552
+ function exclusionRow(entry) {
46553
+ const record = asRecord3(entry);
46554
+ if (record === null || typeof record.rule !== "string")
46555
+ return null;
46556
+ return {
46557
+ rule: record.rule.slice(0, FUNNEL_TEXT_CHARS),
46558
+ matched: typeof record.matched === "string" ? record.matched.slice(0, FUNNEL_TEXT_CHARS) : ""
46559
+ };
46560
+ }
46561
+ function measuredRefusalOf(record) {
46562
+ if (record === null || record.stage === "script")
46563
+ return null;
46564
+ const measuredMinutes = nonNegativeNumber(record.measuredMinutes);
46565
+ const minMinutes = nonNegativeNumber(record.minMinutes);
46566
+ if (measuredMinutes === null || minMinutes === null)
46567
+ return null;
46568
+ return {
46569
+ measuredMinutes,
46570
+ estimatedMinutes: nonNegativeNumber(record.estimatedMinutes),
46571
+ minMinutes,
46572
+ keptCount: nonNegativeNumber(record.keptCount),
46573
+ id: "episode-duration-refusal"
46574
+ };
46575
+ }
46576
+ function scriptRefusalOf(availability, jsonOf) {
46577
+ for (const id of [
46578
+ "episode-script-refusal",
46579
+ "episode-script-refusal-fallback",
46580
+ "episode-script-refusal-fallback2"
46581
+ ]) {
46582
+ if (availability.get(id) !== "available")
46583
+ continue;
46584
+ const record = jsonOf(id);
46585
+ if (record === null || record.stage !== "script")
46586
+ continue;
46587
+ return {
46588
+ estimatedMinutes: nonNegativeNumber(record.estimatedMinutes),
46589
+ minMinutes: nonNegativeNumber(record.minMinutes),
46590
+ keptCount: nonNegativeNumber(record.keptCount),
46591
+ id
46592
+ };
46593
+ }
46594
+ return null;
46595
+ }
46596
+ function floorRefusalOf(record) {
46597
+ if (record === null)
46598
+ return null;
46599
+ return {
46600
+ estimatedMinutes: nonNegativeNumber(record.estimatedMinutes),
46601
+ minMinutes: nonNegativeNumber(record.minMinutes),
46602
+ id: "episode-floor-refusal"
46603
+ };
46604
+ }
46605
+ function projectFunnel(settings, availability, textOf, outcome) {
46606
+ const diagnostics = [];
46607
+ const evidence = {};
46608
+ const rawOf = (id) => parseAnyJson(textOf(id));
46609
+ const poolRecord = rawOf("episode-pool");
46610
+ const poolCount = arrayCount(poolRecord);
46611
+ if (poolCount !== null)
46612
+ evidence.poolCount = "episode-pool";
46613
+ const annotatedCount = arrayCount(rawOf("episode-annotated"));
46614
+ if (annotatedCount !== null)
46615
+ evidence.annotatedCount = "episode-annotated";
46616
+ const qcRejectedCount = arrayCount(asRecord3(rawOf("episode-qc-rejections"))?.rejected);
46617
+ if (qcRejectedCount !== null)
46618
+ evidence.qcRejectedCount = "episode-qc-rejections";
46619
+ const exclusionsRecord = asRecord3(rawOf("episode-exclusions"));
46620
+ const topicKeptCount = arrayCount(exclusionsRecord?.kept);
46621
+ if (topicKeptCount !== null)
46622
+ evidence.topicKeptCount = "episode-exclusions";
46623
+ const plannedCount = arrayCount(rawOf("episode-plan"));
46624
+ if (plannedCount !== null)
46625
+ evidence.plannedCount = "episode-plan";
46626
+ const exclusions = [];
46627
+ const excludedRaw = Array.isArray(exclusionsRecord?.excluded) ? exclusionsRecord.excluded : [];
46628
+ const truncated = excludedRaw.length > FUNNEL_EXCLUSION_ROWS || excludedRaw.some((entry) => typeof entry?.rule === "string" && entry.rule.length > FUNNEL_TEXT_CHARS);
46629
+ for (const entry of excludedRaw.slice(0, FUNNEL_EXCLUSION_ROWS)) {
46630
+ const row = exclusionRow(entry);
46631
+ if (row !== null)
46632
+ exclusions.push(row);
46633
+ }
46634
+ if (truncated) {
46635
+ diagnostics.push({
46636
+ code: "board.daily.funnel.truncated",
46637
+ message: `Exclusion evidence was bounded to ${FUNNEL_EXCLUSION_ROWS} rows of ${FUNNEL_TEXT_CHARS} characters.`
46638
+ });
46639
+ }
46640
+ const measured = measuredRefusalOf(asRecord3(rawOf("episode-duration-refusal")));
46641
+ const script = scriptRefusalOf(availability, (id) => asRecord3(rawOf(id)) ?? null);
46642
+ const floor = floorRefusalOf(asRecord3(rawOf("episode-floor-refusal")));
46643
+ const refusalPresent = measured !== null || script !== null || floor !== null;
46644
+ const candidatesRecord = asRecord3(rawOf("episode-candidates"));
46645
+ const candidateDocs = arrayCount(metadataOf(candidatesRecord ?? {})?.docs);
46646
+ const dedupKeptCount = refusalPresent ? null : candidateDocs;
46647
+ if (dedupKeptCount !== null)
46648
+ evidence.dedupKeptCount = "episode-candidates";
46649
+ const verdict = asRecord3(rawOf("episode-duration"));
46650
+ const verdictMeasured = nonNegativeNumber(verdict?.measuredMinutes);
46651
+ const verdictMin = nonNegativeNumber(verdict?.minMinutes);
46652
+ const verdictMax = nonNegativeNumber(verdict?.maxMinutes);
46653
+ const audioSeconds = nonNegativeNumber(asRecord3(asRecord3(rawOf("audio-record"))?.metadata)?.duration);
46654
+ let measuredMinutes = null;
46655
+ if (measured !== null) {
46656
+ measuredMinutes = measured.measuredMinutes;
46657
+ evidence.measuredMinutes = measured.id;
46658
+ } else if (verdictMeasured !== null) {
46659
+ measuredMinutes = verdictMeasured;
46660
+ evidence.measuredMinutes = "episode-duration";
46661
+ } else if (audioSeconds !== null) {
46662
+ measuredMinutes = minutesFromSeconds(audioSeconds);
46663
+ evidence.measuredMinutes = "audio-record";
46664
+ }
46665
+ let estimatedMinutes = null;
46666
+ if (measured?.estimatedMinutes != null) {
46667
+ estimatedMinutes = measured.estimatedMinutes;
46668
+ evidence.estimatedMinutes = measured.id;
46669
+ } else if (script?.estimatedMinutes != null) {
46670
+ estimatedMinutes = script.estimatedMinutes;
46671
+ evidence.estimatedMinutes = script.id;
46672
+ } else if (floor?.estimatedMinutes != null) {
46673
+ estimatedMinutes = floor.estimatedMinutes;
46674
+ evidence.estimatedMinutes = floor.id;
46675
+ }
46676
+ const settingsNumber = (key) => {
46677
+ const value = Number(settings[key]);
46678
+ return settings[key] !== undefined && Number.isFinite(value) && value >= 0 ? value : null;
46679
+ };
46680
+ let minMinutes = measured?.minMinutes ?? script?.minMinutes ?? floor?.minMinutes ?? verdictMin;
46681
+ if (minMinutes !== null)
46682
+ evidence.minMinutes = measured?.id ?? script?.id ?? floor?.id ?? "episode-duration";
46683
+ else if (settingsNumber("episode_min_minutes") !== null) {
46684
+ minMinutes = settingsNumber("episode_min_minutes");
46685
+ evidence.minMinutes = "captured-settings";
46686
+ }
46687
+ let maxMinutes = verdictMax;
46688
+ if (maxMinutes !== null)
46689
+ evidence.maxMinutes = "episode-duration";
46690
+ else if (settingsNumber("episode_max_minutes") !== null) {
46691
+ maxMinutes = settingsNumber("episode_max_minutes");
46692
+ evidence.maxMinutes = "captured-settings";
46693
+ }
46694
+ if (poolCount !== null && annotatedCount !== null && poolCount !== annotatedCount) {
46695
+ diagnostics.push({
46696
+ code: "board.daily.funnel.mismatch",
46697
+ message: `Stage evidence disagrees: the pool records ${poolCount} item(s) while the annotation input carries ${annotatedCount}; both are shown as recorded.`
46698
+ });
46699
+ }
46700
+ let gate = null;
46701
+ if (outcome === "full" || outcome === "partial") {
46702
+ if (refusalPresent) {
46703
+ diagnostics.push({
46704
+ code: "board.daily.funnel.contradictory",
46705
+ message: "A refusal artifact is present beside a recorded content outcome; the outcome stands and the gate stays unset."
46706
+ });
46707
+ }
46708
+ } else {
46709
+ if (measured !== null)
46710
+ gate = "episode-duration";
46711
+ else if (script !== null)
46712
+ gate = "script";
46713
+ else if (floor !== null)
46714
+ gate = "qc-floor";
46715
+ else if (poolCount === 0)
46716
+ gate = "empty-pool";
46717
+ else if (poolCount !== null && poolCount > 0 && candidateDocs === 0)
46718
+ gate = "qc-empty";
46719
+ else
46720
+ gate = "unknown";
46721
+ if (gate === "empty-pool")
46722
+ evidence.gate = "episode-pool";
46723
+ else if (gate === "qc-empty")
46724
+ evidence.gate = "episode-candidates";
46725
+ else if (gate !== "unknown")
46726
+ evidence.gate = measured?.id ?? script?.id ?? floor?.id ?? "";
46727
+ }
46728
+ return {
46729
+ poolCount,
46730
+ annotatedCount,
46731
+ qcRejectedCount,
46732
+ topicKeptCount,
46733
+ dedupKeptCount,
46734
+ plannedCount,
46735
+ estimatedMinutes,
46736
+ measuredMinutes,
46737
+ minMinutes,
46738
+ maxMinutes,
46739
+ gate,
46740
+ exclusions,
46741
+ evidence,
46742
+ diagnostics
46743
+ };
46744
+ }
46745
+ function boundedText(value, limit = FUNNEL_TEXT_CHARS) {
46746
+ return typeof value === "string" && value !== "" ? value.slice(0, limit) : null;
46747
+ }
46748
+ function countProjection(value, key = "count") {
46749
+ return Array.isArray(value) ? { [key]: value.length } : null;
46750
+ }
46751
+ function sanitizeFunnelArtifact(workflow, artifactId, raw) {
46752
+ if (workflow !== "kk-daily-ai-voice2")
46753
+ return null;
46754
+ const parsed = parseAnyJson(raw);
46755
+ if (parsed === null && raw !== undefined)
46756
+ return { note: "malformed" };
46757
+ const record = asRecord3(parsed);
46758
+ switch (artifactId) {
46759
+ case "episode-pool":
46760
+ case "episode-annotated":
46761
+ case "episode-plan":
46762
+ return countProjection(parsed);
46763
+ case "episode-qc-rejections":
46764
+ return countProjection(record?.rejected, "rejectedCount");
46765
+ case "episode-candidates":
46766
+ return countProjection(metadataOf(record ?? {})?.docs, "docCount");
46767
+ case "episode-exclusions": {
46768
+ if (record === null)
46769
+ return null;
46770
+ const excluded = Array.isArray(record.excluded) ? record.excluded : [];
46771
+ return {
46772
+ keptCount: arrayCount(record.kept),
46773
+ excluded: excluded.slice(0, FUNNEL_EXCLUSION_ROWS).map((entry) => exclusionRow(entry)).filter((row) => row !== null)
46774
+ };
46775
+ }
46776
+ case "episode-floor-refusal":
46777
+ return record === null ? null : {
46778
+ estimatedMinutes: nonNegativeNumber(record.estimatedMinutes),
46779
+ minMinutes: nonNegativeNumber(record.minMinutes),
46780
+ candidateChars: nonNegativeNumber(record.candidateChars),
46781
+ reason: boundedText(record.reason)
46782
+ };
46783
+ case "episode-duration":
46784
+ return record === null ? null : {
46785
+ measuredMinutes: nonNegativeNumber(record.measuredMinutes),
46786
+ minMinutes: nonNegativeNumber(record.minMinutes),
46787
+ maxMinutes: nonNegativeNumber(record.maxMinutes),
46788
+ ok: typeof record.ok === "boolean" ? record.ok : null
46789
+ };
46790
+ case "episode-duration-refusal":
46791
+ case "episode-script-refusal":
46792
+ case "episode-script-refusal-fallback":
46793
+ case "episode-script-refusal-fallback2":
46794
+ return record === null ? null : {
46795
+ stage: record.stage === "script" ? "script" : null,
46796
+ measuredMinutes: nonNegativeNumber(record.measuredMinutes),
46797
+ estimatedMinutes: nonNegativeNumber(record.estimatedMinutes),
46798
+ minMinutes: nonNegativeNumber(record.minMinutes),
46799
+ keptCount: nonNegativeNumber(record.keptCount),
46800
+ reason: boundedText(record.reason)
46801
+ };
46802
+ default:
46803
+ return null;
46804
+ }
46805
+ }
46261
46806
  function projectDailyResults(input) {
46262
46807
  const { record, detail, marker } = input;
46263
46808
  const terminal = isTerminalStatus(detail.status);
@@ -46300,6 +46845,7 @@ function projectDailyResults(input) {
46300
46845
  channels,
46301
46846
  quality,
46302
46847
  safety,
46848
+ funnel: projectFunnel(record.settings, availability, textOf, outcome),
46303
46849
  progress: projectProgress(input.actions, record.topology),
46304
46850
  counts: {
46305
46851
  artifactsAvailable: availableCount,
@@ -46988,6 +47534,13 @@ function createBoardReader(options) {
46988
47534
  }
46989
47535
  };
46990
47536
  }
47537
+ const sanitized = sanitizeFunnelArtifact(workflow, artifactId, observed.text);
47538
+ if (sanitized !== null) {
47539
+ return {
47540
+ status: 200,
47541
+ body: { schemaVersion: 1, artifact: artifactId, projection: sanitized }
47542
+ };
47543
+ }
46991
47544
  return {
46992
47545
  status: range === null ? 200 : 206,
46993
47546
  body: {
@@ -47018,6 +47571,9 @@ function createBoardReader(options) {
47018
47571
  async function storeSelectionsFor(query) {
47019
47572
  return { status: 200, body: await storeSelections({ projectPath, fs }, query) };
47020
47573
  }
47574
+ async function storeChannelsFor(query) {
47575
+ return { status: 200, body: await storeChannels({ projectPath, fs }, query) };
47576
+ }
47021
47577
  const server = Bun.serve({
47022
47578
  hostname: "127.0.0.1",
47023
47579
  port,
@@ -47049,11 +47605,12 @@ function createBoardReader(options) {
47049
47605
  }
47050
47606
  const overviewMatch = /^\/api\/workflows\/([^/]+)\/store\/overview$/.exec(path);
47051
47607
  const selectionsMatch = /^\/api\/workflows\/([^/]+)\/store\/selections$/.exec(path);
47608
+ const channelsMatch = /^\/api\/workflows\/([^/]+)\/store\/channels$/.exec(path);
47052
47609
  const storeItemsMatch = /^\/api\/workflows\/([^/]+)\/store\/items$/.exec(path);
47053
47610
  const runStoreSelectionsMatch = /^\/api\/workflows\/([^/]+)\/runs\/([^/]+)\/store\/selections$/.exec(path);
47054
47611
  const runStoreMatch = /^\/api\/workflows\/([^/]+)\/runs\/([^/]+)\/store$/.exec(path);
47055
47612
  const runStoreItemsMatch = /^\/api\/workflows\/([^/]+)\/runs\/([^/]+)\/store\/items$/.exec(path);
47056
- const storeMatch = overviewMatch ?? selectionsMatch ?? storeItemsMatch ?? runStoreSelectionsMatch ?? runStoreMatch ?? runStoreItemsMatch;
47613
+ const storeMatch = overviewMatch ?? selectionsMatch ?? channelsMatch ?? storeItemsMatch ?? runStoreSelectionsMatch ?? runStoreMatch ?? runStoreItemsMatch;
47057
47614
  if (storeMatch !== null) {
47058
47615
  const storeRaw = storeMatch[1] ?? "";
47059
47616
  if (decodeURIComponent(storeRaw) !== STORE_WORKFLOW) {
@@ -47073,6 +47630,13 @@ function createBoardReader(options) {
47073
47630
  const response = await storeSelectionsFor(query.query);
47074
47631
  return jsonResponse(response.body, response.status, cors);
47075
47632
  }
47633
+ if (channelsMatch !== null) {
47634
+ const query = parseChannelsQuery(url.searchParams);
47635
+ if ("response" in query)
47636
+ return jsonResponse(query.response.body, query.response.status, cors);
47637
+ const response = await storeChannelsFor(query.query);
47638
+ return jsonResponse(response.body, response.status, cors);
47639
+ }
47076
47640
  if (storeItemsMatch !== null) {
47077
47641
  const query = parseItemsQuery(url.searchParams);
47078
47642
  if ("response" in query)
@@ -47145,6 +47709,7 @@ var LABEL_VALUE_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
47145
47709
  var SELECTION_FILTERS = ["selected", "published", "dropped", "none"];
47146
47710
  var ITEMS_QUERY_KEYS = new Set(["page", "source", "seen", "decision", "value", "selection", "search"]);
47147
47711
  var SELECTIONS_QUERY_KEYS = new Set(["page", "runId", "topic", "status", "search"]);
47712
+ var CHANNELS_QUERY_KEYS = new Set(["from", "until", "page"]);
47148
47713
  function invalidQuery(message) {
47149
47714
  return { status: 400, body: { code: "board.query.invalid", message } };
47150
47715
  }
@@ -47245,6 +47810,40 @@ function parseSelectionsQuery(params) {
47245
47810
  }
47246
47811
  };
47247
47812
  }
47813
+ function parseChannelsQuery(params) {
47814
+ for (const key of params.keys()) {
47815
+ if (!CHANNELS_QUERY_KEYS.has(key))
47816
+ return { response: invalidQuery("Unknown query parameter.") };
47817
+ }
47818
+ for (const key of CHANNELS_QUERY_KEYS) {
47819
+ if (params.getAll(key).length > 1)
47820
+ return { response: invalidQuery("Duplicate query parameter.") };
47821
+ }
47822
+ const rawFrom = params.get("from");
47823
+ const rawUntil = params.get("until");
47824
+ if (rawFrom === null || rawUntil === null) {
47825
+ return { response: invalidQuery("`from` and `until` are required.") };
47826
+ }
47827
+ const fromMs = parseEpochMs(rawFrom);
47828
+ const untilMs = parseEpochMs(rawUntil);
47829
+ if (fromMs === null || untilMs === null) {
47830
+ return { response: invalidQuery("`from` and `until` must be nonnegative integer epoch milliseconds.") };
47831
+ }
47832
+ if (fromMs >= untilMs)
47833
+ return { response: invalidQuery("`from` must be less than `until`.") };
47834
+ const rawPage = params.get("page");
47835
+ const page = rawPage === null ? 1 : Number(rawPage);
47836
+ if (rawPage !== null && (!/^\d+$/.test(rawPage) || !Number.isSafeInteger(page) || page < 1)) {
47837
+ return { response: invalidQuery("`page` must be an integer of at least 1.") };
47838
+ }
47839
+ return { query: { fromMs, untilMs, page } };
47840
+ }
47841
+ function parseEpochMs(raw) {
47842
+ if (!/^\d+$/.test(raw))
47843
+ return null;
47844
+ const value = Number(raw);
47845
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
47846
+ }
47248
47847
  function parseQuery(params) {
47249
47848
  if (params.size === 0)
47250
47849
  return null;
@@ -47685,6 +48284,9 @@ function noContentReason(workDir, runDate, poolCount) {
47685
48284
  if (measured !== null && measuredEstimate !== null && durationFloor !== null && keptCount !== null) {
47686
48285
  return `no-content: the episode ran ${measured} min (pre-spend estimate ${measuredEstimate} min), below the ${durationFloor} min floor \u2014 duration refusal after synthesis (pool ${poolCount ?? 0}, kept ${keptCount})`;
47687
48286
  }
48287
+ if (duration?.stage === "script" && measuredEstimate !== null && durationFloor !== null && keptCount !== null) {
48288
+ return `no-content: the authored script estimates ${measuredEstimate} min of speech, below the ${durationFloor} min floor \u2014 script gate refusal before the TTS spend (pool ${poolCount ?? 0}, kept ${keptCount})`;
48289
+ }
47688
48290
  const floor = readRefusal(join7(workDir, "21-plan", `${runDate}_04_quality-control-content_floor-refusal.json`));
47689
48291
  const estimated = num(floor?.estimatedMinutes);
47690
48292
  const minMinutes = num(floor?.minMinutes);
@@ -48332,6 +48934,9 @@ async function collectSourceDocs(discovery, source, timeoutMs) {
48332
48934
  const why = proc.timedOut === true ? `timed out after ${timeoutMs}ms` : `exited with code ${proc.exitCode}`;
48333
48935
  throw new Error(`Plugin '${source.plugin}' ${why}: ${proc.stderr || proc.stdout || "no output"}`);
48334
48936
  }
48937
+ if (proc.stderr !== "") {
48938
+ echoError(`[source:${source.plugin}] ${proc.stderr.trim()}`);
48939
+ }
48335
48940
  if (!await fs.exists(outTmp)) {
48336
48941
  throw new Error(`Plugin '${source.plugin}' succeeded but did not write an output file`);
48337
48942
  }
@@ -49201,7 +49806,9 @@ var SCHEDULES_REL = "plugins/kk/schedules";
49201
49806
  var CHECKOUT_ROOT2 = resolve17(import.meta.dir, "../../../..");
49202
49807
  var SCHEDULE_TEMPLATES = {
49203
49808
  "kk-collect": { launchd: "launchd.plist.tmpl", cron: "cron.tmpl" },
49204
- "kk-serve": { launchd: "kk-serve.launchd.plist.tmpl" }
49809
+ "kk-serve": { launchd: "kk-serve.launchd.plist.tmpl" },
49810
+ "kk-daily-publish3-cn-peak": { cron: "kk-daily-publish3-cn-peak.cron.tmpl" },
49811
+ "kk-daily-publish3-overseas": { cron: "kk-daily-publish3-overseas.cron.tmpl" }
49205
49812
  };
49206
49813
  var REPO_PLACEHOLDER = "{{repo}}";
49207
49814
  var HOME_PLACEHOLDER = "{{home}}";
@@ -49915,9 +50522,18 @@ var ItcPlanTargetRequestSchema = object2({
49915
50522
  plugin: string2().min(1),
49916
50523
  profile: string2().min(1).optional()
49917
50524
  }).strict();
50525
+ var PublishWindowSchema = _enum2(["after-generate", "cn-peak", "overseas-rss"]);
49918
50526
  var PublishPresetTargetSchema = object2({
49919
50527
  plugin: string2().min(1),
49920
- profile: string2().min(1).optional()
50528
+ profile: string2().min(1).optional(),
50529
+ id: string2().min(1).optional(),
50530
+ operation: string2().min(1).optional(),
50531
+ variant: string2().min(1).optional(),
50532
+ tier: number2().int().min(0).max(3).optional(),
50533
+ window: PublishWindowSchema.optional(),
50534
+ mode: _enum2(["off", "dummy", "draft"]).optional(),
50535
+ required: boolean2().optional(),
50536
+ options: record(string2(), unknown()).optional()
49921
50537
  }).strict();
49922
50538
  var PublishPresetSchema = object2({
49923
50539
  schemaVersion: literal(1).default(1),
@@ -50092,6 +50708,11 @@ async function resolveItcTargets(input, fs = createNodeFileSystem()) {
50092
50708
  origin: "none"
50093
50709
  };
50094
50710
  }
50711
+ async function loadPublishPreset(slug, roots = {}, fs = createNodeFileSystem()) {
50712
+ const projectRoot = roots.projectRoot ? resolve19(roots.projectRoot) : process.cwd();
50713
+ const packageRoot = roots.packageRoot ? resolve19(roots.packageRoot) : await resolveDefaultPackageRoot(fs);
50714
+ return resolvePresetFromRoots(fs, slug.trim(), projectRoot, packageRoot);
50715
+ }
50095
50716
  async function hasCoverImage(fs, workDir) {
50096
50717
  const dir = join17(workDir, "4-illustrations");
50097
50718
  if (!await fs.exists(dir))
@@ -50609,7 +51230,13 @@ var ItcReceiptSchema = object2({
50609
51230
  aggregatePath: string2().optional(),
50610
51231
  publicUrlVerified: boolean2().optional(),
50611
51232
  url: string2().optional(),
50612
- durationMs: number2().optional()
51233
+ durationMs: number2().optional(),
51234
+ target: string2().min(1).optional(),
51235
+ window: string2().min(1).optional(),
51236
+ dummy: boolean2().optional(),
51237
+ error: string2().optional(),
51238
+ startedAt: string2().optional(),
51239
+ finishedAt: string2().optional()
50613
51240
  }).strict();
50614
51241
  async function findCover(fs, workDir) {
50615
51242
  const dir = join19(workDir, "4-illustrations");
@@ -50824,6 +51451,32 @@ async function prepareTargetPayloads(fs, workDir, plan, plugins) {
50824
51451
  await fs.writeFile(join19(publishDir, "publish-plan.json"), `${JSON.stringify(plan, null, 2)}
50825
51452
  `);
50826
51453
  }
51454
+ async function probePlugin(plugin, executor = new NodeProcessExecutor) {
51455
+ if (!plugin.check)
51456
+ return { ok: true };
51457
+ const probe = await executor.run({
51458
+ command: process.execPath,
51459
+ args: [plugin.check, "check"],
51460
+ env: pluginProcessEnv(plugin.kind, plugin.name),
51461
+ timeout: 60000
51462
+ });
51463
+ if (probe.exitCode === 0)
51464
+ return { ok: true };
51465
+ let detail = "";
51466
+ try {
51467
+ const last = probe.stdout.trim().split(`
51468
+ `).filter(Boolean).at(-1) ?? "";
51469
+ const parsed = JSON.parse(last);
51470
+ if (parsed.detail) {
51471
+ detail = `${parsed.status ?? "refused"}: ${parsed.detail}`;
51472
+ for (const value of Object.values(pluginEnvFromConfig(plugin.kind, plugin.name))) {
51473
+ if (value)
51474
+ detail = detail.split(value).join("<redacted>");
51475
+ }
51476
+ }
51477
+ } catch {}
51478
+ return { ok: false, ...detail ? { detail } : {} };
51479
+ }
50827
51480
  async function itcCheckTargets(args, fs = createNodeFileSystem(), executor = new NodeProcessExecutor) {
50828
51481
  const [workDir = ""] = args;
50829
51482
  if (!workDir) {
@@ -50840,69 +51493,33 @@ async function itcCheckTargets(args, fs = createNodeFileSystem(), executor = new
50840
51493
  const byName = new Map(plugins.map((p) => [p.name, p]));
50841
51494
  for (const target of plan.targets) {
50842
51495
  const plugin = byName.get(target.plugin);
50843
- if (plugin?.check) {
50844
- const probe = await executor.run({
50845
- command: process.execPath,
50846
- args: [plugin.check, "check"],
50847
- env: pluginProcessEnv(plugin.kind, plugin.name),
50848
- timeout: 60000
50849
- });
50850
- if (probe.exitCode !== 0) {
50851
- let detail = "";
50852
- try {
50853
- const last = probe.stdout.trim().split(`
50854
- `).filter(Boolean).at(-1) ?? "";
50855
- const parsed = JSON.parse(last);
50856
- if (parsed.detail) {
50857
- detail = `${parsed.status ?? "refused"}: ${parsed.detail}`;
50858
- for (const value of Object.values(pluginEnvFromConfig(plugin.kind, plugin.name))) {
50859
- if (value)
50860
- detail = detail.split(value).join("<redacted>");
50861
- }
50862
- }
50863
- } catch {}
50864
- const botName = plugin.browserSession?.profileName ?? plugin.name;
50865
- echoError(detail ? `channel probe for '${target.plugin}' refused: ${detail}` : `channel probe for '${target.plugin}' refused: run \`kk bot open/login ${botName}\` to authenticate`);
50866
- return 1;
50867
- }
51496
+ if (!plugin?.check)
51497
+ continue;
51498
+ const probe = await probePlugin(plugin, executor);
51499
+ if (!probe.ok) {
51500
+ const botName = plugin.browserSession?.profileName ?? plugin.name;
51501
+ echoError(probe.detail ? `channel probe for '${target.plugin}' refused: ${probe.detail}` : `channel probe for '${target.plugin}' refused: run \`kk bot open/login ${botName}\` to authenticate`);
51502
+ return 1;
50868
51503
  }
50869
51504
  }
50870
51505
  return 0;
50871
51506
  }
50872
- async function itcSendTargets(args, fs = createNodeFileSystem(), dispatch = runFanout) {
50873
- const [workDir = ""] = args;
50874
- const isRetry = args.includes("--retry");
50875
- if (!workDir) {
50876
- echoError("usage: itc-send-targets <work_dir> [--retry]");
50877
- return 1;
50878
- }
50879
- const publishDir = join19(workDir, "4-publish");
50880
- const planPath = join19(publishDir, "publish-plan.json");
50881
- if (!await fs.exists(planPath))
50882
- return 0;
50883
- const plan = JSON.parse(await fs.readFile(planPath));
50884
- if (!plan.targets || plan.targets.length === 0)
50885
- return 0;
50886
- const parentRunId = (await readOptionsSnapshot(fs, workDir))?.parentRunId;
50887
- if (!parentRunId) {
50888
- echoError(`itc-send-targets: ${join19(workDir, "itc-options.json")} has no parentRunId`);
50889
- return 1;
50890
- }
50891
- const receiptsDir = join19(publishDir, "receipts");
50892
- await fs.ensureDir(receiptsDir);
50893
- const planRaw = await fs.readFile(planPath);
50894
- const planDigest = createHash6("sha256").update(planRaw).digest("hex");
50895
- for (const target of plan.targets) {
50896
- const targetReceiptPath = join19(receiptsDir, `${target.plugin}.json`);
51507
+ async function sendRows(options) {
51508
+ const fs = options.fs ?? createNodeFileSystem();
51509
+ const dispatch = options.dispatch ?? runFanout;
51510
+ const { rows, publishDir, receiptsDir, parentRunId, planDigest, retry, label } = options;
51511
+ const result = { sent: [], skipped: [], receipts: [] };
51512
+ for (const row of rows) {
51513
+ const startedAt = new Date().toISOString();
51514
+ const targetReceiptPath = join19(receiptsDir, `${row.key}.json`);
50897
51515
  let reattempt = false;
50898
- if (isRetry) {
51516
+ if (retry) {
50899
51517
  if (!await fs.exists(targetReceiptPath))
50900
51518
  continue;
50901
51519
  try {
50902
51520
  const priorReceipt = JSON.parse(await fs.readFile(targetReceiptPath));
50903
- if (priorReceipt.status === "succeeded" || priorReceipt.effect !== "none") {
51521
+ if (priorReceipt.status === "succeeded" || priorReceipt.effect !== "none")
50904
51522
  continue;
50905
- }
50906
51523
  } catch {
50907
51524
  continue;
50908
51525
  }
@@ -50910,31 +51527,80 @@ async function itcSendTargets(args, fs = createNodeFileSystem(), dispatch = runF
50910
51527
  } else if (await fs.exists(targetReceiptPath)) {
50911
51528
  try {
50912
51529
  const prior = JSON.parse(await fs.readFile(targetReceiptPath));
50913
- if (prior.status === "succeeded")
51530
+ const dummyOnly = prior.dummy === true && row.mode !== "dummy";
51531
+ if (prior.status === "succeeded" && !dummyOnly)
50914
51532
  continue;
50915
- const mayAlreadyBeLive = (prior.status === "uncertain" || prior.effect === "unknown") && (target.mode === "live" || prior.mode === "live");
51533
+ const mayAlreadyBeLive = (prior.status === "uncertain" || prior.effect === "unknown") && (row.mode === "live" || prior.mode === "live");
50916
51534
  if (mayAlreadyBeLive) {
50917
- echoError(`itc-send-targets: ${target.plugin} has an unknown-effect live receipt (may already be published) \u2014 resolve it before resending; skipped`);
51535
+ echoError(`${label}: ${row.key} has an unknown-effect live receipt (may already be published) \u2014 resolve it before resending; skipped`);
51536
+ result.skipped.push(row.key);
50918
51537
  continue;
50919
51538
  }
50920
51539
  } catch {}
50921
51540
  reattempt = true;
50922
51541
  }
50923
- const payloadRel = target.payloadPath ?? `4-publish/content.${target.plugin}.json`;
50924
- const payloadPath = join19(workDir, payloadRel);
51542
+ let payloadPath = row.payloadPath;
51543
+ let preparedError;
51544
+ if (options.preparePayload) {
51545
+ const prepared = await options.preparePayload(row);
51546
+ if ("error" in prepared) {
51547
+ preparedError = prepared.error;
51548
+ } else {
51549
+ payloadPath = prepared.payloadPath;
51550
+ }
51551
+ }
50925
51552
  const payloadRaw = await fs.exists(payloadPath) ? await fs.readFile(payloadPath) : "{}";
50926
51553
  const payloadDigest = createHash6("sha256").update(payloadRaw).digest("hex");
50927
- const outPath = join19(publishDir, `fanout-result.${target.plugin}${isRetry ? ".retry" : ""}.json`);
51554
+ if (preparedError !== undefined) {
51555
+ const receipt = {
51556
+ ...row.receiptExtras ?? {},
51557
+ schemaVersion: 1,
51558
+ parentRunId,
51559
+ planDigest,
51560
+ payloadDigest,
51561
+ plugin: row.plugin,
51562
+ profile: row.profile,
51563
+ mode: row.mode === "live" ? "live" : "draft",
51564
+ attemptId: retry ? "retry-1" : "attempt-1",
51565
+ status: "failed",
51566
+ effect: "none",
51567
+ publicUrlVerified: false,
51568
+ error: preparedError
51569
+ };
51570
+ await fs.writeFile(targetReceiptPath, `${JSON.stringify(receipt, null, 2)}
51571
+ `);
51572
+ result.receipts.push(receipt);
51573
+ if (options.onReceipt)
51574
+ await options.onReceipt(receipt);
51575
+ continue;
51576
+ }
51577
+ if (row.mode === "dummy") {
51578
+ if (options.dummyReceipt === undefined) {
51579
+ echoError(`${label}: ${row.key} is dummy but no dummy receipt writer is configured; skipped`);
51580
+ result.skipped.push(row.key);
51581
+ continue;
51582
+ }
51583
+ const receipt = await options.dummyReceipt(row, { payloadPath, payloadDigest, startedAt });
51584
+ await fs.writeFile(targetReceiptPath, `${JSON.stringify(receipt, null, 2)}
51585
+ `);
51586
+ result.receipts.push(receipt);
51587
+ if (receipt.status === "succeeded")
51588
+ result.sent.push(row.key);
51589
+ if (options.onReceipt)
51590
+ await options.onReceipt(receipt);
51591
+ continue;
51592
+ }
51593
+ const outPath = join19(publishDir, `fanout-result.${row.key}${retry ? ".retry" : ""}.json`);
50928
51594
  if (reattempt && await fs.exists(outPath))
50929
51595
  await fs.deleteFile(outPath);
50930
51596
  const fanoutArgs = [
50931
51597
  "--content",
50932
51598
  payloadPath,
50933
51599
  "--target",
50934
- target.plugin,
51600
+ row.plugin,
50935
51601
  "--out",
50936
51602
  outPath,
50937
- ...target.transport === "browser" ? ["--serial-target", target.plugin] : []
51603
+ ...row.transport === "browser" ? ["--serial-target", row.plugin] : []
50938
51604
  ];
50939
51605
  let fanoutExit = 0;
50940
51606
  if (!await fs.exists(outPath)) {
@@ -50948,7 +51614,7 @@ async function itcSendTargets(args, fs = createNodeFileSystem(), dispatch = runF
50948
51614
  if (await fs.exists(outPath)) {
50949
51615
  try {
50950
51616
  const agg = JSON.parse(await fs.readFile(outPath));
50951
- const found = (agg.targets ?? []).find((t) => t.target === target.plugin);
51617
+ const found = (agg.targets ?? []).find((t) => t.target === row.plugin);
50952
51618
  if (found)
50953
51619
  outcome = found;
50954
51620
  } catch {
@@ -50961,37 +51627,45 @@ async function itcSendTargets(args, fs = createNodeFileSystem(), dispatch = runF
50961
51627
  let publicUrlVerified = false;
50962
51628
  if (isSuccess) {
50963
51629
  effect = "confirmed";
50964
- if (target.mode === "live" && outcome.url) {
51630
+ if (row.mode === "live" && outcome.url) {
50965
51631
  publicUrlVerified = itcDelivery?.publicUrlVerified === true;
50966
51632
  }
50967
51633
  } else if (itcDelivery?.effect === "none") {
50968
51634
  effect = "none";
50969
51635
  }
50970
51636
  const receipt = {
51637
+ ...row.receiptExtras ?? {},
50971
51638
  schemaVersion: 1,
50972
51639
  parentRunId,
50973
51640
  planDigest,
50974
51641
  payloadDigest,
50975
- plugin: target.plugin,
50976
- profile: target.profile,
50977
- mode: target.mode,
50978
- attemptId: isRetry ? "retry-1" : "attempt-1",
51642
+ plugin: row.plugin,
51643
+ profile: row.profile,
51644
+ mode: row.mode === "live" ? "live" : "draft",
51645
+ attemptId: retry ? "retry-1" : "attempt-1",
50979
51646
  status: isSuccess ? "succeeded" : effect === "unknown" ? "uncertain" : "failed",
50980
51647
  effect,
50981
51648
  publicUrlVerified,
50982
51649
  ...outcome.url ? { url: outcome.url } : {},
51650
+ ...outcome.error ? { error: outcome.error } : {},
50983
51651
  ...outcome.durationMs !== undefined ? { durationMs: outcome.durationMs } : {},
50984
51652
  aggregatePath: outPath
50985
51653
  };
50986
51654
  await fs.writeFile(targetReceiptPath, `${JSON.stringify(receipt, null, 2)}
50987
51655
  `);
51656
+ result.receipts.push(receipt);
51657
+ if (receipt.status === "succeeded")
51658
+ result.sent.push(row.key);
51659
+ if (options.onReceipt)
51660
+ await options.onReceipt(receipt);
50988
51661
  }
50989
- return 0;
51662
+ return result;
50990
51663
  }
50991
- async function itcReconcileReceipts(args, fs = createNodeFileSystem()) {
51664
+ async function itcSendTargets(args, fs = createNodeFileSystem(), dispatch = runFanout) {
50992
51665
  const [workDir = ""] = args;
51666
+ const isRetry = args.includes("--retry");
50993
51667
  if (!workDir) {
50994
- echoError("usage: itc-reconcile-receipts <work_dir>");
51668
+ echoError("usage: itc-send-targets <work_dir> [--retry]");
50995
51669
  return 1;
50996
51670
  }
50997
51671
  const publishDir = join19(workDir, "4-publish");
@@ -50999,14 +51673,57 @@ async function itcReconcileReceipts(args, fs = createNodeFileSystem()) {
50999
51673
  if (!await fs.exists(planPath))
51000
51674
  return 0;
51001
51675
  const plan = JSON.parse(await fs.readFile(planPath));
51002
- const chosenPath = join19(workDir, "chosen-source.json");
51003
- const chosen = await fs.exists(chosenPath) ? JSON.parse(await fs.readFile(chosenPath)) : undefined;
51004
- const settle = async (outcome) => {
51005
- if (chosen === undefined)
51006
- return;
51007
- await itcSelectionSettle({
51008
- schemaVersion: 1,
51009
- intakeDir: workDir,
51676
+ if (!plan.targets || plan.targets.length === 0)
51677
+ return 0;
51678
+ const parentRunId = (await readOptionsSnapshot(fs, workDir))?.parentRunId;
51679
+ if (!parentRunId) {
51680
+ echoError(`itc-send-targets: ${join19(workDir, "itc-options.json")} has no parentRunId`);
51681
+ return 1;
51682
+ }
51683
+ const receiptsDir = join19(publishDir, "receipts");
51684
+ await fs.ensureDir(receiptsDir);
51685
+ const planRaw = await fs.readFile(planPath);
51686
+ const planDigest = createHash6("sha256").update(planRaw).digest("hex");
51687
+ const rows = plan.targets.map((target) => ({
51688
+ key: target.plugin,
51689
+ plugin: target.plugin,
51690
+ profile: target.profile,
51691
+ mode: target.mode,
51692
+ payloadPath: join19(workDir, target.payloadPath ?? `4-publish/content.${target.plugin}.json`),
51693
+ transport: target.transport
51694
+ }));
51695
+ await sendRows({
51696
+ rows,
51697
+ publishDir,
51698
+ receiptsDir,
51699
+ parentRunId,
51700
+ planDigest,
51701
+ retry: isRetry,
51702
+ label: "itc-send-targets",
51703
+ fs,
51704
+ dispatch
51705
+ });
51706
+ return 0;
51707
+ }
51708
+ async function itcReconcileReceipts(args, fs = createNodeFileSystem()) {
51709
+ const [workDir = ""] = args;
51710
+ if (!workDir) {
51711
+ echoError("usage: itc-reconcile-receipts <work_dir>");
51712
+ return 1;
51713
+ }
51714
+ const publishDir = join19(workDir, "4-publish");
51715
+ const planPath = join19(publishDir, "publish-plan.json");
51716
+ if (!await fs.exists(planPath))
51717
+ return 0;
51718
+ const plan = JSON.parse(await fs.readFile(planPath));
51719
+ const chosenPath = join19(workDir, "chosen-source.json");
51720
+ const chosen = await fs.exists(chosenPath) ? JSON.parse(await fs.readFile(chosenPath)) : undefined;
51721
+ const settle = async (outcome) => {
51722
+ if (chosen === undefined)
51723
+ return;
51724
+ await itcSelectionSettle({
51725
+ schemaVersion: 1,
51726
+ intakeDir: workDir,
51010
51727
  parentRunId: chosen.parentRunId,
51011
51728
  topicProfile: chosen.topicProfile,
51012
51729
  outcome
@@ -51562,6 +52279,922 @@ async function runItcPrepare(args, fs) {
51562
52279
  return 0;
51563
52280
  }
51564
52281
 
52282
+ // src/publish-matrix-stage.ts
52283
+ init_dist3();
52284
+
52285
+ // src/publish-matrix.ts
52286
+ import { createHash as createHash8 } from "crypto";
52287
+ import { dirname as dirname12, join as join21, resolve as resolve21 } from "path";
52288
+ init_dist3();
52289
+ init_dist();
52290
+ init_zod();
52291
+ var BundleVariantSchema = object2({
52292
+ path: string2().min(1),
52293
+ sha256: string2().min(1)
52294
+ }).strict();
52295
+ var BundleSchema = object2({
52296
+ schemaVersion: literal(1).default(1),
52297
+ runId: string2().min(1),
52298
+ runDate: string2().min(1),
52299
+ parentRunId: string2().min(1).optional(),
52300
+ runs: array(string2()).default([]),
52301
+ episode: object2({ title: string2().min(1), guid: string2().optional() }).strict(),
52302
+ variants: record(string2(), BundleVariantSchema)
52303
+ }).strict();
52304
+ var PublishPlanRowSchema = object2({
52305
+ id: string2().min(1),
52306
+ plugin: string2().min(1),
52307
+ operation: string2().min(1).optional(),
52308
+ variant: string2().min(1),
52309
+ tier: number2().int().min(0).max(3),
52310
+ window: PublishWindowSchema,
52311
+ mode: _enum2(["dummy", "draft", "live"]),
52312
+ required: boolean2(),
52313
+ approval: _enum2(["none", "required"]),
52314
+ status: literal("planned"),
52315
+ profile: string2().min(1).optional(),
52316
+ payloadPath: string2().min(1),
52317
+ payloadDigest: string2().min(1)
52318
+ }).strict();
52319
+ var PublishPlanSchema = object2({
52320
+ schemaVersion: literal(1).default(1),
52321
+ window: PublishWindowSchema,
52322
+ bundleRunId: string2().min(1),
52323
+ parentRunId: string2().min(1),
52324
+ presetName: string2().min(1),
52325
+ presetDigest: string2().min(1),
52326
+ rows: array(PublishPlanRowSchema)
52327
+ }).strict();
52328
+ var PublishOutcomeRowSchema = object2({
52329
+ id: string2().min(1),
52330
+ tier: number2().int().min(0).max(3),
52331
+ required: boolean2(),
52332
+ status: _enum2(["succeeded", "failed", "uncertain", "missing"]),
52333
+ effect: _enum2(["none", "confirmed", "unknown"]).optional(),
52334
+ dummy: boolean2()
52335
+ }).strict();
52336
+ var PublishOutcomeSchema = object2({
52337
+ schemaVersion: literal(1).default(1),
52338
+ window: PublishWindowSchema,
52339
+ runDate: string2().min(1),
52340
+ status: _enum2(["full", "partial", "none", "dummy"]),
52341
+ published: number2().int().min(0),
52342
+ targets: number2().int().min(0),
52343
+ rows: array(PublishOutcomeRowSchema)
52344
+ }).strict();
52345
+ function classifyPublishMatrix(plan, receipts, runDate) {
52346
+ const byTarget = new Map;
52347
+ for (const receipt of receipts) {
52348
+ const key = receipt.target ?? receipt.plugin;
52349
+ byTarget.set(key, receipt);
52350
+ }
52351
+ const rows = plan.rows.map((row) => {
52352
+ const receipt = byTarget.get(row.id);
52353
+ return {
52354
+ id: row.id,
52355
+ tier: row.tier,
52356
+ required: row.required,
52357
+ status: receipt?.status ?? "missing",
52358
+ ...receipt?.effect !== undefined ? { effect: receipt.effect } : {},
52359
+ dummy: receipt?.dummy === true
52360
+ };
52361
+ });
52362
+ const real = rows.filter((row) => !row.dummy);
52363
+ const succeeded = real.filter((row) => row.status === "succeeded");
52364
+ const status = real.length === 0 ? "dummy" : succeeded.length === real.length ? "full" : succeeded.length > 0 ? "partial" : "none";
52365
+ return {
52366
+ schemaVersion: 1,
52367
+ window: plan.window,
52368
+ runDate,
52369
+ status,
52370
+ published: succeeded.length,
52371
+ targets: real.length,
52372
+ rows
52373
+ };
52374
+ }
52375
+ var LEGACY_RESULT_FILES = [
52376
+ { plugin: "surfdash-pub", file: "_14_publish-surfdash_result.json" },
52377
+ { plugin: "podcast-pub", file: "_16_publish-podcast_result.json" }
52378
+ ];
52379
+ function slugify2(value) {
52380
+ return value.trim().replace(/[\s/]+/g, "-");
52381
+ }
52382
+ function targetId(target) {
52383
+ if (target.id)
52384
+ return target.id;
52385
+ if (target.operation)
52386
+ return `${target.plugin}.${slugify2(target.operation)}`;
52387
+ return target.plugin;
52388
+ }
52389
+ function flagValue(args, flag) {
52390
+ const idx = args.indexOf(flag);
52391
+ if (idx === -1)
52392
+ return;
52393
+ const value = args[idx + 1];
52394
+ return value !== undefined && !value.startsWith("--") ? value : undefined;
52395
+ }
52396
+ function listValue(args, flag) {
52397
+ const raw = flagValue(args, flag);
52398
+ if (raw === undefined)
52399
+ return [];
52400
+ return raw.split(",").map((token) => token.trim()).filter(Boolean);
52401
+ }
52402
+ async function appendEvent(fs, publishDir, event) {
52403
+ await fs.ensureDir(publishDir);
52404
+ const path = join21(publishDir, "publish-events.jsonl");
52405
+ const prior = await fs.exists(path) ? await fs.readFile(path) : "";
52406
+ await fs.writeFile(path, `${prior}${JSON.stringify({ at: new Date().toISOString(), ...event })}
52407
+ `);
52408
+ }
52409
+ var LINK_SOURCES = [
52410
+ { placeholder: "{{article_url}}", plugin: "surfdash-pub", receipt: "article" },
52411
+ { placeholder: "{{podcast_url}}", plugin: "podcast-pub", receipt: "podcast" }
52412
+ ];
52413
+ async function readPlan(fs, workDir, window) {
52414
+ const planPath = join21(workDir, "4-publish", `plan.${window}.json`);
52415
+ if (!await fs.exists(planPath)) {
52416
+ throw new Error(`no plan at ${planPath}; run publish-matrix-plan first`);
52417
+ }
52418
+ const parsed = PublishPlanSchema.safeParse(JSON.parse(await fs.readFile(planPath)));
52419
+ if (!parsed.success) {
52420
+ throw new Error(`malformed plan at ${planPath}: ${parsed.error.message}`);
52421
+ }
52422
+ return parsed.data;
52423
+ }
52424
+ async function composePlan(fs, input) {
52425
+ const bundleAbs = resolve21(input.bundlePath);
52426
+ if (!await fs.exists(bundleAbs))
52427
+ throw new Error(`bundle not found: ${bundleAbs}`);
52428
+ const bundleParsed = BundleSchema.safeParse(JSON.parse(await fs.readFile(bundleAbs)));
52429
+ if (!bundleParsed.success) {
52430
+ throw new Error(`malformed bundle at ${bundleAbs}: ${bundleParsed.error.message}`);
52431
+ }
52432
+ const bundle = bundleParsed.data;
52433
+ const bundleDir = dirname12(bundleAbs);
52434
+ const { preset, digest } = await loadPublishPreset(input.preset, { ...input.projectRoot ? { projectRoot: input.projectRoot } : {} }, fs);
52435
+ const inWindow = preset.targets.filter((target) => (target.window ?? "after-generate") === input.window && (target.mode ?? "draft") !== "off");
52436
+ const matches = (target, token) => target.plugin === token || target.id === token;
52437
+ for (const token of [...input.channels, ...input.liveChannels]) {
52438
+ if (!preset.targets.some((target) => matches(target, token))) {
52439
+ throw new Error(`channel '${token}' is not listed in preset '${preset.name}'`);
52440
+ }
52441
+ }
52442
+ for (const token of [...input.channels, ...input.liveChannels]) {
52443
+ if (!inWindow.some((target) => matches(target, token))) {
52444
+ throw new Error(`channel '${token}' has no target in window '${input.window}'`);
52445
+ }
52446
+ }
52447
+ const selected = input.channels.length === 0 ? inWindow : inWindow.filter((target) => input.channels.some((token) => matches(target, token)));
52448
+ const rows = [];
52449
+ const seen = new Set;
52450
+ for (const target of selected) {
52451
+ const id = targetId(target);
52452
+ if (seen.has(id))
52453
+ throw new Error(`duplicate target id '${id}' in preset '${preset.name}'`);
52454
+ seen.add(id);
52455
+ const liveToken = input.liveChannels.find((token) => matches(target, token));
52456
+ if (liveToken !== undefined && target.mode === "dummy") {
52457
+ throw new Error(`channel '${liveToken}' is dummy-only and cannot be promoted to live`);
52458
+ }
52459
+ const mode = input.dummy ? "dummy" : liveToken !== undefined ? "live" : target.mode === "dummy" ? "dummy" : "draft";
52460
+ if (target.variant === undefined) {
52461
+ throw new Error(`target '${id}' has no variant; a publish-matrix target must name one`);
52462
+ }
52463
+ const entry = bundle.variants[`${target.variant}@${target.plugin}`] ?? bundle.variants[target.variant];
52464
+ if (entry === undefined) {
52465
+ throw new Error(`bundle has no variant '${target.variant}' for target '${id}'`);
52466
+ }
52467
+ const variantPath = resolve21(bundleDir, entry.path);
52468
+ if (!await fs.exists(variantPath))
52469
+ throw new Error(`variant file not found: ${variantPath}`);
52470
+ const variantRaw = await fs.readFile(variantPath);
52471
+ const actual = createHash8("sha256").update(variantRaw).digest("hex");
52472
+ if (actual !== entry.sha256) {
52473
+ throw new Error(`variant digest mismatch for '${target.variant}': expected ${entry.sha256}, got ${actual}`);
52474
+ }
52475
+ const parsedContent = ContentSchema.safeParse(JSON.parse(variantRaw));
52476
+ if (!parsedContent.success) {
52477
+ throw new Error(`variant '${target.variant}' is not valid Content: ${parsedContent.error.message}`);
52478
+ }
52479
+ const payload = {
52480
+ ...parsedContent.data,
52481
+ metadata: {
52482
+ ...parsedContent.data.metadata ?? {},
52483
+ ...target.options ?? {},
52484
+ ...target.operation !== undefined ? { operation: target.operation } : {},
52485
+ publishStatus: mode === "live" ? "public" : "draft"
52486
+ }
52487
+ };
52488
+ const payloadRelative = `4-publish/payloads/content.${id}.json`;
52489
+ const payloadText = `${JSON.stringify(payload, null, 2)}
52490
+ `;
52491
+ rows.push({
52492
+ id,
52493
+ plugin: target.plugin,
52494
+ ...target.operation !== undefined ? { operation: target.operation } : {},
52495
+ variant: target.variant,
52496
+ tier: target.tier ?? 3,
52497
+ window: input.window,
52498
+ mode,
52499
+ required: target.required ?? false,
52500
+ approval: mode === "live" ? "required" : "none",
52501
+ status: "planned",
52502
+ ...target.profile !== undefined ? { profile: target.profile } : {},
52503
+ payloadPath: payloadRelative,
52504
+ payloadDigest: createHash8("sha256").update(payloadText).digest("hex")
52505
+ });
52506
+ await fs.ensureDir(join21(input.workDir, "4-publish", "payloads"));
52507
+ await fs.writeFile(join21(input.workDir, payloadRelative), payloadText);
52508
+ }
52509
+ return {
52510
+ schemaVersion: 1,
52511
+ window: input.window,
52512
+ bundleRunId: bundle.runId,
52513
+ parentRunId: bundle.parentRunId ?? bundle.runId,
52514
+ presetName: preset.name,
52515
+ presetDigest: digest,
52516
+ rows
52517
+ };
52518
+ }
52519
+ async function planPublishMatrix(args, deps = {}) {
52520
+ const fs = deps.fs ?? createNodeFileSystem();
52521
+ const [workDir = ""] = args;
52522
+ const bundlePath = flagValue(args, "--bundle");
52523
+ const presetFlag = flagValue(args, "--preset");
52524
+ const windowArg = flagValue(args, "--window");
52525
+ const channels = listValue(args, "--channels");
52526
+ const liveChannels = listValue(args, "--live-channels");
52527
+ const dummy = args.includes("--dummy");
52528
+ if (!workDir || bundlePath === undefined || windowArg === undefined) {
52529
+ echoError("usage: publish-matrix-plan <work_dir> --bundle <bundle.json> [--preset <slug>] --window <after-generate|cn-peak|overseas-rss> [--channels a,b] [--live-channels a,b] [--dummy]");
52530
+ return 1;
52531
+ }
52532
+ const parsedWindow = PublishWindowSchema.safeParse(windowArg);
52533
+ if (!parsedWindow.success) {
52534
+ echoError(`publish-matrix-plan: unknown window '${windowArg}'`);
52535
+ return 1;
52536
+ }
52537
+ if (dummy && liveChannels.length > 0) {
52538
+ echoError("publish-matrix-plan: --dummy is a rehearsal and cannot be combined with --live-channels");
52539
+ return 1;
52540
+ }
52541
+ try {
52542
+ const preset = presetFlag ?? await readPublishPresetSelector(process.env, fs);
52543
+ const plan = await composePlan(fs, {
52544
+ workDir,
52545
+ bundlePath,
52546
+ preset,
52547
+ window: parsedWindow.data,
52548
+ channels,
52549
+ liveChannels,
52550
+ dummy,
52551
+ ...deps.projectRoot ? { projectRoot: deps.projectRoot } : {}
52552
+ });
52553
+ const publishDir = join21(workDir, "4-publish");
52554
+ await fs.ensureDir(publishDir);
52555
+ await fs.writeFile(join21(publishDir, `plan.${plan.window}.json`), `${JSON.stringify(plan, null, 2)}
52556
+ `);
52557
+ for (const row of plan.rows) {
52558
+ await appendEvent(fs, publishDir, {
52559
+ event: "planned",
52560
+ window: plan.window,
52561
+ id: row.id,
52562
+ plugin: row.plugin,
52563
+ mode: row.mode,
52564
+ tier: row.tier
52565
+ });
52566
+ }
52567
+ echo(`${plan.rows.length} target(s) planned for window ${plan.window}`);
52568
+ return 0;
52569
+ } catch (err) {
52570
+ echoError(`publish-matrix-plan: ${err instanceof Error ? err.message : String(err)}`);
52571
+ return 1;
52572
+ }
52573
+ }
52574
+ async function checkPublishMatrix(args, deps = {}) {
52575
+ const fs = deps.fs ?? createNodeFileSystem();
52576
+ const [workDir = ""] = args;
52577
+ const windowArg = flagValue(args, "--window");
52578
+ const presetFlag = flagValue(args, "--preset");
52579
+ const allWindows = args.includes("--all-windows");
52580
+ const presetMode = allWindows;
52581
+ if (!workDir || windowArg === undefined && !presetMode) {
52582
+ echoError("usage: publish-matrix-check <work_dir> --window <after-generate|cn-peak|overseas-rss> | [--preset <slug>] --all-windows");
52583
+ return 1;
52584
+ }
52585
+ const plugins = deps.plugins ?? discoverAllPlugins().plugins;
52586
+ const byName = new Map(plugins.map((plugin) => [plugin.name, plugin]));
52587
+ const executor = deps.executor ?? new NodeProcessExecutor;
52588
+ const probes = [];
52589
+ try {
52590
+ let names;
52591
+ let outFile;
52592
+ let label;
52593
+ if (presetMode) {
52594
+ const { preset } = await loadPublishPreset(presetFlag ?? await readPublishPresetSelector(process.env, fs), { ...deps.projectRoot ? { projectRoot: deps.projectRoot } : {} }, fs);
52595
+ const unknown = preset.targets.filter((target) => !byName.has(target.plugin));
52596
+ if (unknown.length > 0) {
52597
+ echoError(`publish-matrix-check: preset '${preset.name}' names uninstalled plugin(s): ${unknown.map((target) => target.plugin).join(", ")}`);
52598
+ return 1;
52599
+ }
52600
+ names = [
52601
+ ...new Set(preset.targets.filter((target) => (target.mode ?? "draft") !== "off" && target.mode !== "dummy").map((target) => target.plugin))
52602
+ ];
52603
+ label = "preflight";
52604
+ outFile = "check.preflight.json";
52605
+ } else {
52606
+ const parsedWindow = PublishWindowSchema.safeParse(windowArg);
52607
+ if (!parsedWindow.success) {
52608
+ echoError(`publish-matrix-check: unknown window '${windowArg}'`);
52609
+ return 1;
52610
+ }
52611
+ const plan = await readPlan(fs, workDir, parsedWindow.data);
52612
+ names = [...new Set(plan.rows.filter((row) => row.mode !== "dummy").map((row) => row.plugin))];
52613
+ label = plan.window;
52614
+ outFile = `check.${plan.window}.json`;
52615
+ }
52616
+ for (const name of names) {
52617
+ const plugin = byName.get(name);
52618
+ if (plugin === undefined) {
52619
+ probes.push({ plugin: name, ok: false, detail: "plugin not installed" });
52620
+ echoError(`publish-matrix-check: plugin '${name}' is not installed`);
52621
+ continue;
52622
+ }
52623
+ const probe = await probePlugin(plugin, executor);
52624
+ probes.push({ plugin: name, ok: probe.ok, ...probe.detail ? { detail: probe.detail } : {} });
52625
+ if (!probe.ok) {
52626
+ echoError(`publish-matrix-check: channel probe for '${name}' refused: ${probe.detail ?? "unknown"}`);
52627
+ }
52628
+ }
52629
+ const publishDir = join21(workDir, "4-publish");
52630
+ await fs.ensureDir(publishDir);
52631
+ await fs.writeFile(join21(publishDir, outFile), `${JSON.stringify({ schemaVersion: 1, window: label, plugins: probes }, null, 2)}
52632
+ `);
52633
+ echo(names.length === 0 ? "no probes" : `${probes.length} probe(s) recorded for ${label}`);
52634
+ return 0;
52635
+ } catch (err) {
52636
+ echoError(`publish-matrix-check: ${err instanceof Error ? err.message : String(err)}`);
52637
+ return 1;
52638
+ }
52639
+ }
52640
+ async function runPublishMatrixClassify(args, deps = {}) {
52641
+ const fs = deps.fs ?? createNodeFileSystem();
52642
+ const [workDir = ""] = args;
52643
+ const windowArg = flagValue(args, "--window");
52644
+ const runDate = flagValue(args, "--run-date");
52645
+ if (!workDir || windowArg === undefined || runDate === undefined) {
52646
+ echoError("usage: publish-matrix-classify <work_dir> --window <w> --run-date <stamp>");
52647
+ return 1;
52648
+ }
52649
+ const parsedWindow = PublishWindowSchema.safeParse(windowArg);
52650
+ if (!parsedWindow.success) {
52651
+ echoError(`publish-matrix-classify: unknown window '${windowArg}'`);
52652
+ return 1;
52653
+ }
52654
+ const publishDir = join21(workDir, "4-publish");
52655
+ let plan;
52656
+ try {
52657
+ plan = await readPlan(fs, workDir, parsedWindow.data);
52658
+ } catch (err) {
52659
+ echoError(`publish-matrix-classify: ${err instanceof Error ? err.message : String(err)}`);
52660
+ await fs.ensureDir(publishDir);
52661
+ const none = {
52662
+ schemaVersion: 1,
52663
+ window: parsedWindow.data,
52664
+ runDate,
52665
+ status: "none",
52666
+ published: 0,
52667
+ targets: 0,
52668
+ rows: []
52669
+ };
52670
+ await fs.writeFile(join21(publishDir, `outcome.${parsedWindow.data}.json`), `${JSON.stringify(none, null, 2)}
52671
+ `);
52672
+ await fs.writeFile(join21(publishDir, `publish-status.${parsedWindow.data}.txt`), `none
52673
+ `);
52674
+ await fs.writeFile(join21(workDir, "publish-classify.summary.txt"), `${JSON.stringify({ status: "none", published: 0, targets: 0 })}
52675
+ `);
52676
+ return 0;
52677
+ }
52678
+ const receiptsDir = join21(publishDir, "receipts");
52679
+ const receipts = [];
52680
+ for (const row of plan.rows) {
52681
+ const path = join21(receiptsDir, `${row.id}.json`);
52682
+ if (!await fs.exists(path))
52683
+ continue;
52684
+ try {
52685
+ receipts.push(JSON.parse(await fs.readFile(path)));
52686
+ } catch {}
52687
+ }
52688
+ const outcome = classifyPublishMatrix(plan, receipts, runDate);
52689
+ await fs.ensureDir(publishDir);
52690
+ await fs.writeFile(join21(publishDir, `outcome.${outcome.window}.json`), `${JSON.stringify(outcome, null, 2)}
52691
+ `);
52692
+ await fs.writeFile(join21(publishDir, `publish-status.${outcome.window}.txt`), `${outcome.status}
52693
+ `);
52694
+ await fs.writeFile(join21(workDir, "publish-classify.summary.txt"), `${JSON.stringify({ status: outcome.status, published: outcome.published, targets: outcome.targets })}
52695
+ `);
52696
+ if (outcome.window === "after-generate") {
52697
+ const legacyDir = join21(workDir, "31-publish");
52698
+ for (const source of LEGACY_RESULT_FILES) {
52699
+ const row = plan.rows.find((candidate) => candidate.plugin === source.plugin && candidate.tier === 0);
52700
+ if (row === undefined || row.mode === "dummy")
52701
+ continue;
52702
+ const receipt = receipts.find((candidate) => (candidate.target ?? candidate.plugin) === row.id);
52703
+ if (receipt === undefined || receipt.dummy === true || receipt.aggregatePath === undefined)
52704
+ continue;
52705
+ const aggregate = receipt.aggregatePath;
52706
+ if (!await fs.exists(aggregate))
52707
+ continue;
52708
+ await fs.ensureDir(legacyDir);
52709
+ await fs.writeFile(join21(legacyDir, `${runDate}${source.file}`), await fs.readFile(aggregate));
52710
+ }
52711
+ }
52712
+ echo(outcome.status);
52713
+ return 0;
52714
+ }
52715
+ async function writeDummyReceipt(fs, row, context) {
52716
+ const base = {
52717
+ ...row.receiptExtras ?? {},
52718
+ schemaVersion: 1,
52719
+ parentRunId: context.parentRunId,
52720
+ planDigest: context.planDigest,
52721
+ payloadDigest: context.payloadDigest,
52722
+ plugin: row.plugin,
52723
+ profile: row.profile,
52724
+ mode: "draft",
52725
+ attemptId: "attempt-1",
52726
+ status: "succeeded",
52727
+ effect: "none",
52728
+ publicUrlVerified: false,
52729
+ dummy: true,
52730
+ url: `dummy://${row.key}`,
52731
+ startedAt: context.startedAt,
52732
+ finishedAt: new Date().toISOString()
52733
+ };
52734
+ let invalid;
52735
+ try {
52736
+ const raw = await fs.readFile(context.payloadPath);
52737
+ const parsed = ContentSchema.safeParse(JSON.parse(raw));
52738
+ if (!parsed.success)
52739
+ invalid = parsed.error.message;
52740
+ } catch (err) {
52741
+ invalid = err instanceof Error ? err.message : String(err);
52742
+ }
52743
+ if (invalid === undefined)
52744
+ return base;
52745
+ return {
52746
+ ...base,
52747
+ status: "failed",
52748
+ effect: "none",
52749
+ error: "invalid-payload",
52750
+ url: undefined
52751
+ };
52752
+ }
52753
+ async function readSucceeded(fs, receiptsDir, id) {
52754
+ const path = join21(receiptsDir, `${id}.json`);
52755
+ if (!await fs.exists(path))
52756
+ return false;
52757
+ try {
52758
+ return JSON.parse(await fs.readFile(path)).status === "succeeded";
52759
+ } catch {
52760
+ return false;
52761
+ }
52762
+ }
52763
+ async function readLinkMap(fs, receiptsDir, rows) {
52764
+ const map = {};
52765
+ for (const source of LINK_SOURCES) {
52766
+ const row = rows.find((candidate) => candidate.plugin === source.plugin && candidate.tier === 0);
52767
+ if (row === undefined)
52768
+ continue;
52769
+ const path = join21(receiptsDir, `${row.id}.json`);
52770
+ if (!await fs.exists(path))
52771
+ continue;
52772
+ try {
52773
+ const receipt = JSON.parse(await fs.readFile(path));
52774
+ if (receipt.status === "succeeded" && receipt.url)
52775
+ map[source.receipt] = receipt.url;
52776
+ } catch {}
52777
+ }
52778
+ return map;
52779
+ }
52780
+ async function sendPublishMatrix(args, deps = {}) {
52781
+ const fs = deps.fs ?? createNodeFileSystem();
52782
+ const [workDir = ""] = args;
52783
+ const windowArg = flagValue(args, "--window");
52784
+ const tierArg = flagValue(args, "--tier");
52785
+ const retry = args.includes("--retry");
52786
+ if (!workDir || windowArg === undefined) {
52787
+ echoError("usage: publish-matrix-send <work_dir> --window <w> [--retry] [--tier <n>]");
52788
+ return 1;
52789
+ }
52790
+ const parsedWindow = PublishWindowSchema.safeParse(windowArg);
52791
+ if (!parsedWindow.success) {
52792
+ echoError(`publish-matrix-send: unknown window '${windowArg}'`);
52793
+ return 1;
52794
+ }
52795
+ let onlyTier;
52796
+ if (tierArg !== undefined) {
52797
+ onlyTier = Number(tierArg);
52798
+ if (!Number.isInteger(onlyTier) || onlyTier < 0 || onlyTier > 3) {
52799
+ echoError(`publish-matrix-send: --tier must be 0-3, got '${tierArg}'`);
52800
+ return 1;
52801
+ }
52802
+ }
52803
+ try {
52804
+ const plan = await readPlan(fs, workDir, parsedWindow.data);
52805
+ const publishDir = join21(workDir, "4-publish");
52806
+ const receiptsDir = join21(publishDir, "receipts");
52807
+ await fs.ensureDir(receiptsDir);
52808
+ const planDigest = createHash8("sha256").update(await fs.readFile(join21(publishDir, `plan.${plan.window}.json`))).digest("hex");
52809
+ const succeededAlready = (id) => readSucceeded(fs, receiptsDir, id);
52810
+ const tiers = [...new Set(plan.rows.map((row) => row.tier))].sort((a, b) => a - b);
52811
+ const pending = onlyTier === undefined ? plan.rows : plan.rows.filter((row) => row.tier === onlyTier);
52812
+ let tierZeroBroken = false;
52813
+ for (const tier of tiers) {
52814
+ const rows = pending.filter((row) => row.tier === tier);
52815
+ if (rows.length === 0)
52816
+ continue;
52817
+ if (tierZeroBroken) {
52818
+ for (const row of rows) {
52819
+ await appendEvent(fs, publishDir, {
52820
+ event: "skipped",
52821
+ window: plan.window,
52822
+ id: row.id,
52823
+ plugin: row.plugin,
52824
+ reason: "tier-0-required-failed"
52825
+ });
52826
+ }
52827
+ continue;
52828
+ }
52829
+ const links = tier === 0 ? {} : await readLinkMap(fs, receiptsDir, plan.rows);
52830
+ const sendRowsInput = rows.map((row) => ({
52831
+ key: row.id,
52832
+ plugin: row.plugin,
52833
+ profile: row.profile ?? "article",
52834
+ mode: row.mode,
52835
+ payloadPath: join21(workDir, row.payloadPath),
52836
+ transport: "fanout",
52837
+ receiptExtras: { target: row.id, window: plan.window }
52838
+ }));
52839
+ await sendRows({
52840
+ rows: sendRowsInput,
52841
+ publishDir,
52842
+ receiptsDir,
52843
+ parentRunId: plan.parentRunId,
52844
+ planDigest,
52845
+ retry,
52846
+ label: "publish-matrix-send",
52847
+ fs,
52848
+ ...deps.dispatch ? { dispatch: deps.dispatch } : {},
52849
+ dummyReceipt: (row, context) => writeDummyReceipt(fs, row, { ...context, parentRunId: plan.parentRunId, planDigest }),
52850
+ preparePayload: async (row) => {
52851
+ if (tier === 0)
52852
+ return { payloadPath: row.payloadPath };
52853
+ const raw = await fs.exists(row.payloadPath) ? await fs.readFile(row.payloadPath) : "";
52854
+ let resolved = raw;
52855
+ for (const source of LINK_SOURCES) {
52856
+ const url = links[source.receipt];
52857
+ if (url === undefined)
52858
+ continue;
52859
+ resolved = resolved.split(source.placeholder).join(url);
52860
+ }
52861
+ if (LINK_SOURCES.some((source) => resolved.includes(source.placeholder))) {
52862
+ return { error: "unresolved-link" };
52863
+ }
52864
+ const resolvedPath = join21(workDir, "4-publish", "payloads", `content.${row.key}.resolved.json`);
52865
+ await fs.writeFile(resolvedPath, resolved);
52866
+ return { payloadPath: resolvedPath };
52867
+ },
52868
+ onReceipt: async (receipt) => {
52869
+ await appendEvent(fs, publishDir, {
52870
+ event: receipt.status === "succeeded" ? "sent" : "failed",
52871
+ window: plan.window,
52872
+ id: receipt.target ?? receipt.plugin,
52873
+ plugin: receipt.plugin,
52874
+ mode: receipt.mode,
52875
+ effect: receipt.effect,
52876
+ ...receipt.error ? { error: receipt.error } : {}
52877
+ });
52878
+ }
52879
+ });
52880
+ if (tier === 0) {
52881
+ tierZeroBroken = false;
52882
+ for (const row of rows) {
52883
+ if (!row.required)
52884
+ continue;
52885
+ if (await succeededAlready(row.id))
52886
+ continue;
52887
+ tierZeroBroken = true;
52888
+ break;
52889
+ }
52890
+ }
52891
+ }
52892
+ echo(`window ${plan.window}: ${pending.length} target(s) dispatched`);
52893
+ return 0;
52894
+ } catch (err) {
52895
+ echoError(`publish-matrix-send: ${err instanceof Error ? err.message : String(err)}`);
52896
+ return 1;
52897
+ }
52898
+ }
52899
+ var RssGateResultSchema = object2({
52900
+ schemaVersion: literal(1).default(1),
52901
+ date: string2().min(1),
52902
+ feedUrl: string2().min(1),
52903
+ hit: boolean2(),
52904
+ reason: _enum2(["guid", "title", "missing", "fetch-failed"]),
52905
+ detail: string2().optional()
52906
+ }).strict();
52907
+ function feedItems(xml) {
52908
+ const items = [];
52909
+ for (const block of xml.matchAll(/<item>([\s\S]*?)<\/item>/g)) {
52910
+ const body = block[1] ?? "";
52911
+ const title = /<title>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/title>/.exec(body)?.[1]?.trim() ?? "";
52912
+ const guid = /<guid[^>]*>([\s\S]*?)<\/guid>/.exec(body)?.[1]?.trim() ?? "";
52913
+ items.push({ title, guid });
52914
+ }
52915
+ return items;
52916
+ }
52917
+ function matchRssGate(items, date, guid) {
52918
+ if (guid !== "" && items.some((item) => item.guid === guid))
52919
+ return "guid";
52920
+ if (items.some((item) => item.title.includes(date)))
52921
+ return "title";
52922
+ return "missing";
52923
+ }
52924
+ async function runPublishMatrixRssGate(args, deps = {}) {
52925
+ const fs = deps.fs ?? createNodeFileSystem();
52926
+ const [workDir = ""] = args;
52927
+ const date = flagValue(args, "--date");
52928
+ const feedUrl = flagValue(args, "--feed") ?? "https://podcast.salty.vip/rss/";
52929
+ if (!workDir || date === undefined) {
52930
+ echoError("usage: publish-matrix-rss-gate <work_dir> --date <YYYY-MM-DD> [--feed <url>]");
52931
+ return 1;
52932
+ }
52933
+ const publishDir = join21(workDir, "4-publish");
52934
+ let guid = "";
52935
+ const receiptsDir = join21(publishDir, "receipts");
52936
+ if (await fs.exists(receiptsDir)) {
52937
+ for (const planWindow of ["after-generate", "cn-peak"]) {
52938
+ const planPath = join21(publishDir, `plan.${planWindow}.json`);
52939
+ if (!await fs.exists(planPath))
52940
+ continue;
52941
+ const plan = JSON.parse(await fs.readFile(planPath));
52942
+ for (const row of plan.rows) {
52943
+ if (row.plugin !== "podcast-pub")
52944
+ continue;
52945
+ const receiptPath = join21(receiptsDir, `${row.id}.json`);
52946
+ if (!await fs.exists(receiptPath))
52947
+ continue;
52948
+ const receipt = JSON.parse(await fs.readFile(receiptPath));
52949
+ if (typeof receipt.guid === "string")
52950
+ guid = receipt.guid;
52951
+ }
52952
+ }
52953
+ }
52954
+ const write = async (result) => {
52955
+ await fs.ensureDir(publishDir);
52956
+ await fs.writeFile(join21(publishDir, "rss-gate.json"), `${JSON.stringify(result, null, 2)}
52957
+ `);
52958
+ await appendEvent(fs, publishDir, {
52959
+ event: result.hit ? "gate-hit" : "gate-miss",
52960
+ window: "overseas-rss",
52961
+ reason: result.reason,
52962
+ date: result.date
52963
+ });
52964
+ if (!result.hit) {
52965
+ const incident = {
52966
+ schemaVersion: 1,
52967
+ date: result.date,
52968
+ class: "rss-lag",
52969
+ reason: `feed ${result.feedUrl} has no episode for ${result.date} (${result.reason})`,
52970
+ evidence: ["4-publish/rss-gate.json", ...result.detail === undefined ? [] : [result.detail]]
52971
+ };
52972
+ await fs.writeFile(join21(publishDir, "incident.json"), `${JSON.stringify(incident, null, 2)}
52973
+ `);
52974
+ await fs.writeFile(join21(publishDir, "incident.md"), [
52975
+ `# Overseas window missed \u2014 ${result.date}`,
52976
+ "",
52977
+ `The podcast feed \`${result.feedUrl}\` does not carry the ${result.date} episode (${result.reason}).`,
52978
+ "Class `rss-lag`: the episode is not in the feed yet.",
52979
+ result.detail === undefined ? "" : `Detail: ${result.detail}`,
52980
+ "",
52981
+ "Next: confirm the generation run published the podcast tier-0 target, then re-run the",
52982
+ "overseas window for this date. An automatic repair only resends `effect: none` tier-0 rows."
52983
+ ].filter((line) => line !== "").join(`
52984
+ `));
52985
+ echoError(`publish-matrix-rss-gate: miss (${result.reason}) for ${result.date}`);
52986
+ }
52987
+ return result.hit ? 0 : 3;
52988
+ };
52989
+ let xml;
52990
+ try {
52991
+ const read = deps.readFeed ?? (async (url) => (await fetch(url)).text());
52992
+ xml = await read(feedUrl);
52993
+ } catch (err) {
52994
+ return write({
52995
+ schemaVersion: 1,
52996
+ date,
52997
+ feedUrl,
52998
+ hit: false,
52999
+ reason: "fetch-failed",
53000
+ detail: err instanceof Error ? err.message : String(err)
53001
+ });
53002
+ }
53003
+ const reason = matchRssGate(feedItems(xml), date, guid);
53004
+ return write({ schemaVersion: 1, date, feedUrl, hit: reason !== "missing", reason });
53005
+ }
53006
+ async function runPublishMatrixWaitUntil(args, deps = {}) {
53007
+ const [target = "", zone = ""] = args;
53008
+ const match = /^(\d{1,2}):(\d{2})$/.exec(target);
53009
+ if (!match || zone === "") {
53010
+ echoError("usage: publish-matrix-wait-until <HH:MM> <Area/City>");
53011
+ return 1;
53012
+ }
53013
+ const hours = Number(match[1]);
53014
+ const minutes = Number(match[2]);
53015
+ if (hours > 23 || minutes > 59) {
53016
+ echoError(`publish-matrix-wait-until: '${target}' is not a valid wall-clock time`);
53017
+ return 1;
53018
+ }
53019
+ const now = deps.now ?? Date.now;
53020
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
53021
+ const offsetAt = (at) => {
53022
+ const parts = new Intl.DateTimeFormat("en-US", {
53023
+ timeZone: zone,
53024
+ hour12: false,
53025
+ year: "numeric",
53026
+ month: "2-digit",
53027
+ day: "2-digit",
53028
+ hour: "2-digit",
53029
+ minute: "2-digit",
53030
+ second: "2-digit"
53031
+ }).formatToParts(new Date(at));
53032
+ const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
53033
+ const asUtc = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
53034
+ return Math.round((asUtc - at) / 60000);
53035
+ };
53036
+ const wall = new Intl.DateTimeFormat("en-US", {
53037
+ timeZone: zone,
53038
+ hour12: false,
53039
+ hour: "2-digit",
53040
+ minute: "2-digit"
53041
+ }).formatToParts(new Date(now()));
53042
+ const part = (type) => Number(wall.find((entry) => entry.type === type)?.value ?? "0");
53043
+ const localHour = part("hour") % 24;
53044
+ const localMinute = part("minute");
53045
+ const already = localHour * 60 + localMinute;
53046
+ if (already >= hours * 60 + minutes) {
53047
+ echo(`publish-matrix-wait-until: ${zone} is already past ${target}`);
53048
+ return 0;
53049
+ }
53050
+ let wait = (hours * 60 + minutes - already) * 60000;
53051
+ const delta = offsetAt(now() + wait) - offsetAt(now());
53052
+ wait -= delta * 60000;
53053
+ if (wait <= 0) {
53054
+ echo(`publish-matrix-wait-until: ${zone} is already past ${target}`);
53055
+ return 0;
53056
+ }
53057
+ echo(`publish-matrix-wait-until: sleeping ${Math.round(wait / 60000)} min to ${target} ${zone}`);
53058
+ await sleep(wait);
53059
+ return 0;
53060
+ }
53061
+ var DEFAULT_PUBLISH_PRESET = "daily-ai-voice";
53062
+ async function readPublishPresetSelector(env = process.env, fs = createNodeFileSystem()) {
53063
+ const configPath = resolveItcConfigPath(env, env.HOME ?? "");
53064
+ if (!await fs.exists(configPath))
53065
+ return DEFAULT_PUBLISH_PRESET;
53066
+ let parsed;
53067
+ try {
53068
+ parsed = Bun.YAML.parse(await fs.readFile(configPath));
53069
+ } catch {
53070
+ throw new Error(`config file is not valid YAML: ${configPath}`);
53071
+ }
53072
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
53073
+ return DEFAULT_PUBLISH_PRESET;
53074
+ const workflows = parsed.workflows;
53075
+ if (workflows === null || typeof workflows !== "object" || Array.isArray(workflows))
53076
+ return DEFAULT_PUBLISH_PRESET;
53077
+ const block = workflows["kk-daily-ai-voice3"];
53078
+ if (block === undefined)
53079
+ return DEFAULT_PUBLISH_PRESET;
53080
+ if (block === null || typeof block !== "object" || Array.isArray(block)) {
53081
+ throw new Error("config key 'workflows.kk-daily-ai-voice3' must be a mapping");
53082
+ }
53083
+ const settings = block;
53084
+ for (const key of Object.keys(settings)) {
53085
+ if (key !== "publish_preset") {
53086
+ throw new Error(`config key 'workflows.kk-daily-ai-voice3.${key}' is not a declared setting (allowed: publish_preset)`);
53087
+ }
53088
+ }
53089
+ const value = settings.publish_preset;
53090
+ if (value === undefined)
53091
+ return DEFAULT_PUBLISH_PRESET;
53092
+ if (typeof value !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
53093
+ throw new Error("config key 'workflows.kk-daily-ai-voice3.publish_preset' must be a lowercase hyphenated slug");
53094
+ }
53095
+ return value;
53096
+ }
53097
+ async function runPublishMatrixBundle(args, deps = {}) {
53098
+ const fs = deps.fs ?? createNodeFileSystem();
53099
+ const [workDir = ""] = args;
53100
+ const runDate = flagValue(args, "--run-date");
53101
+ const parentRunId = flagValue(args, "--run-id");
53102
+ if (!workDir || runDate === undefined || parentRunId === undefined) {
53103
+ echoError("usage: publish-matrix-bundle <work_dir> --run-date <YYYY-MM-DD> --run-id <voice3 run id>");
53104
+ return 1;
53105
+ }
53106
+ const variantsDir = join21(workDir, "30-variants");
53107
+ if (!await fs.exists(variantsDir)) {
53108
+ echoError(`publish-matrix-bundle: no variants at ${variantsDir}; run daily-bundle-variants first`);
53109
+ return 1;
53110
+ }
53111
+ try {
53112
+ const entries = (await fs.readDir(variantsDir)).filter((file) => file.endsWith(".content.json")).sort();
53113
+ if (entries.length === 0)
53114
+ throw new Error(`no *.content.json under ${variantsDir}`);
53115
+ const variants = {};
53116
+ for (const file of entries) {
53117
+ const key = file.replace(/\.content\.json$/, "").replaceAll("_", ":");
53118
+ const raw = await fs.readFile(join21(variantsDir, file));
53119
+ const parsed = ContentSchema.safeParse(JSON.parse(raw));
53120
+ if (!parsed.success) {
53121
+ throw new Error(`variant '${key}' is not valid Content: ${parsed.error.message}`);
53122
+ }
53123
+ variants[key] = {
53124
+ path: `30-variants/${file}`,
53125
+ sha256: createHash8("sha256").update(raw).digest("hex")
53126
+ };
53127
+ }
53128
+ const hasOwnedVariant = Object.keys(variants).some((key) => key === "article" || key === "episode");
53129
+ if (!hasOwnedVariant) {
53130
+ echoError("publish-matrix-bundle: no article/episode variant \u2014 the publication contract needs both");
53131
+ return 1;
53132
+ }
53133
+ const articleTitle = await readVariantTitle(fs, workDir, variants);
53134
+ const bundle = {
53135
+ schemaVersion: 1,
53136
+ runId: `${runDate}-${parentRunId}`,
53137
+ runDate,
53138
+ parentRunId,
53139
+ runs: [],
53140
+ episode: { title: articleTitle === "" ? `\u6BCF\u65E5AI\u4E71\u7096 \xB7 ${runDate}` : articleTitle },
53141
+ variants
53142
+ };
53143
+ const validated = BundleSchema.parse(bundle);
53144
+ const bundlePath = join21(workDir, "bundle.json");
53145
+ if (await fs.exists(bundlePath)) {
53146
+ try {
53147
+ const prior = BundleSchema.parse(JSON.parse(await fs.readFile(bundlePath)));
53148
+ if (prior.runs.length > 0)
53149
+ validated.runs = prior.runs;
53150
+ } catch {}
53151
+ }
53152
+ await fs.writeFile(bundlePath, `${JSON.stringify(validated, null, 2)}
53153
+ `);
53154
+ echo(`bundle frozen: ${Object.keys(variants).length} variant(s) -> ${bundlePath}`);
53155
+ return 0;
53156
+ } catch (err) {
53157
+ echoError(`publish-matrix-bundle: ${err instanceof Error ? err.message : String(err)}`);
53158
+ return 1;
53159
+ }
53160
+ }
53161
+ async function readVariantTitle(fs, workDir, variants) {
53162
+ for (const key of ["episode", "episode@podcast-pub", "article"]) {
53163
+ const entry = variants[key];
53164
+ if (entry === undefined)
53165
+ continue;
53166
+ try {
53167
+ const parsed = ContentSchema.parse(JSON.parse(await fs.readFile(join21(workDir, entry.path))));
53168
+ if (parsed.title !== undefined && parsed.title.trim() !== "")
53169
+ return parsed.title.trim();
53170
+ } catch {}
53171
+ }
53172
+ return "";
53173
+ }
53174
+
53175
+ // src/publish-matrix-stage.ts
53176
+ async function runPublishMatrixStage(args, fs = createNodeFileSystem()) {
53177
+ const [stageName, ...rest] = args;
53178
+ switch (stageName) {
53179
+ case "publish-matrix-plan":
53180
+ return planPublishMatrix(rest, { fs });
53181
+ case "publish-matrix-check":
53182
+ return checkPublishMatrix(rest, { fs });
53183
+ case "publish-matrix-send":
53184
+ return sendPublishMatrix(rest, { fs });
53185
+ case "publish-matrix-classify":
53186
+ return runPublishMatrixClassify(rest, { fs });
53187
+ case "publish-matrix-bundle":
53188
+ return runPublishMatrixBundle(rest, { fs });
53189
+ case "publish-matrix-rss-gate":
53190
+ return runPublishMatrixRssGate(rest, { fs });
53191
+ case "publish-matrix-wait-until":
53192
+ return runPublishMatrixWaitUntil(rest);
53193
+ default:
53194
+ return;
53195
+ }
53196
+ }
53197
+
51565
53198
  // src/commands/stage.ts
51566
53199
  var ROOT_GLOBALS = new Set(["-v", "--verbose"]);
51567
53200
  function normalizeStageArgs(argv) {
@@ -51577,10 +53210,11 @@ function normalizeStageArgs(argv) {
51577
53210
  }
51578
53211
  function registerStageCommand(program) {
51579
53212
  program.command("stage").description("Run a kk-workflow-stages.ts stage (workflow orchestration steps)").command("run").description("Run one stage by name with positional pass-through").argument("<args...>", "Stage name followed by its positional arguments").allowUnknownOption(true).action((args) => {
51580
- if (args[0]?.startsWith("itc-")) {
53213
+ const stageName = args[0] ?? "";
53214
+ if (stageName.startsWith("itc-") || stageName.startsWith("publish-matrix-")) {
51581
53215
  return (async () => {
51582
- const itcCode = await runItcStage(args);
51583
- const code = itcCode !== undefined ? itcCode : await runStageCancellable(args);
53216
+ const nativeCode = stageName.startsWith("itc-") ? await runItcStage(args) : await runPublishMatrixStage(args);
53217
+ const code = nativeCode !== undefined ? nativeCode : await runStageCancellable(args);
51584
53218
  if (code !== 0) {
51585
53219
  throw new CommanderError(code, "kk.stage.failed", `stage failed with exit code ${code}`);
51586
53220
  }
@@ -51596,7 +53230,7 @@ function registerStageCommand(program) {
51596
53230
  }
51597
53231
 
51598
53232
  // src/commands/store.ts
51599
- import { dirname as dirname12, join as join22, resolve as resolve22 } from "path";
53233
+ import { dirname as dirname13, join as join23, resolve as resolve23 } from "path";
51600
53234
  init_dist3();
51601
53235
  init_commander();
51602
53236
 
@@ -51604,16 +53238,16 @@ init_commander();
51604
53238
  init_dist3();
51605
53239
  import { randomUUID as randomUUID2 } from "crypto";
51606
53240
  import { tmpdir as tmpdir3 } from "os";
51607
- import { join as join21, resolve as resolve21 } from "path";
53241
+ import { join as join22, resolve as resolve22 } from "path";
51608
53242
  var STORE_CATALOG_PATH_REL = "plugins/kk/decisions/kk-items.yaml";
51609
53243
  var SPUR_CONFIG_PATH_REL = ".spur/config.yaml";
51610
53244
  var EVIDENCE_MAX_CHARS = 2000;
51611
53245
  var SUBPROCESS_FALLBACK_AFTER = 2;
51612
53246
  async function resolveCatalogPath(projectRoot, fs = createNodeFileSystem()) {
51613
53247
  const candidates = [
51614
- resolve21(projectRoot, STORE_CATALOG_PATH_REL),
51615
- resolve21(import.meta.dir, "../../..", STORE_CATALOG_PATH_REL),
51616
- resolve21(import.meta.dir, "..", STORE_CATALOG_PATH_REL)
53248
+ resolve22(projectRoot, STORE_CATALOG_PATH_REL),
53249
+ resolve22(import.meta.dir, "../../..", STORE_CATALOG_PATH_REL),
53250
+ resolve22(import.meta.dir, "..", STORE_CATALOG_PATH_REL)
51617
53251
  ];
51618
53252
  for (const candidate of candidates) {
51619
53253
  if (await fs.exists(candidate))
@@ -51633,7 +53267,7 @@ async function loadInjectedHub() {
51633
53267
  }
51634
53268
  async function readDecisionRouting(projectRoot, fs = createNodeFileSystem()) {
51635
53269
  const catalogPath = await resolveCatalogPath(projectRoot, fs);
51636
- const configPath = join21(projectRoot, SPUR_CONFIG_PATH_REL);
53270
+ const configPath = join22(projectRoot, SPUR_CONFIG_PATH_REL);
51637
53271
  if (!await fs.exists(configPath))
51638
53272
  return { catalogPath, routing: { makers: {} } };
51639
53273
  let parsed;
@@ -51717,8 +53351,8 @@ function createBufferedDecisionFetch(inner = fetch) {
51717
53351
  var bufferedDecisionFetch = createBufferedDecisionFetch();
51718
53352
  function runDecisionSubprocess(decisionId, input, maker, fs = createNodeFileSystem()) {
51719
53353
  const command = process.env.KK_SPUR_BIN ?? "spur";
51720
- const scratch = join21(tmpdir3(), `kk-decision-${randomUUID2()}`);
51721
- const evidencePath = join21(scratch, "evidence.md");
53354
+ const scratch = join22(tmpdir3(), `kk-decision-${randomUUID2()}`);
53355
+ const evidencePath = join22(scratch, "evidence.md");
51722
53356
  try {
51723
53357
  fs.writeFile(evidencePath, input.body);
51724
53358
  const args = [
@@ -51752,7 +53386,7 @@ var HEURISTIC_MAKER = "heuristic";
51752
53386
  var LABEL_PLUGIN = "episode-plan-gen";
51753
53387
  var LABEL_SCRATCH_DIR = "label";
51754
53388
  var PROFILE_DIR_REL = "plugins/kk/decisions/extract";
51755
- var CHECKOUT_ROOT3 = resolve22(import.meta.dir, "../../../..");
53389
+ var CHECKOUT_ROOT3 = resolve23(import.meta.dir, "../../../..");
51756
53390
  var CATALOG_PATH_REL = "plugins/kk/decisions/kk-items.yaml";
51757
53391
  var TOPIC_SLUG = /^[a-z0-9][a-z0-9-]*$/;
51758
53392
  var WINDOW_UNITS2 = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
@@ -51833,7 +53467,7 @@ function sourceCountsOf3(docs) {
51833
53467
  return counts;
51834
53468
  }
51835
53469
  async function writeNewIds(fs, path, ids) {
51836
- await fs.ensureDir(dirname12(path));
53470
+ await fs.ensureDir(dirname13(path));
51837
53471
  await fs.writeFile(path, `${JSON.stringify([...new Set(ids)], null, 2)}
51838
53472
  `);
51839
53473
  }
@@ -51954,8 +53588,8 @@ async function readAnnotations(fs, outputPath) {
51954
53588
  async function heuristicAnnotations(fs, scratch, runId, items) {
51955
53589
  if (items.length === 0)
51956
53590
  return new Map;
51957
- const inputPath = join22(scratch, "label-input.json");
51958
- const outputPath = join22(scratch, "label-out.json");
53591
+ const inputPath = join23(scratch, "label-input.json");
53592
+ const outputPath = join23(scratch, "label-out.json");
51959
53593
  await fs.ensureDir(scratch);
51960
53594
  const docs = items.map((item) => ({
51961
53595
  id: item.id,
@@ -52174,7 +53808,7 @@ function parseDecisions(raw) {
52174
53808
  return ids;
52175
53809
  }
52176
53810
  async function readCatalogVersion(fs) {
52177
- const candidates = [resolve22(process.cwd(), CATALOG_PATH_REL), resolve22(CHECKOUT_ROOT3, CATALOG_PATH_REL)];
53811
+ const candidates = [resolve23(process.cwd(), CATALOG_PATH_REL), resolve23(CHECKOUT_ROOT3, CATALOG_PATH_REL)];
52178
53812
  for (const path of candidates) {
52179
53813
  try {
52180
53814
  if (!await fs.exists(path))
@@ -52306,7 +53940,7 @@ async function labelAction(options) {
52306
53940
  const mode = parseLabelMode(options.mode);
52307
53941
  const configPath = resolveKkConfigPath();
52308
53942
  const dbPath = await resolveStorePath(configPath, options.db);
52309
- const scratch = join22(dirname12(plan.kind === "new-ids" ? plan.newIds : dbPath), LABEL_SCRATCH_DIR);
53943
+ const scratch = join23(dirname13(plan.kind === "new-ids" ? plan.newIds : dbPath), LABEL_SCRATCH_DIR);
52310
53944
  const store = await openStore({ path: dbPath });
52311
53945
  const catalogVersion = await readCatalogVersion(fs);
52312
53946
  const labels = (await readStoreConfig(configPath)).labels;
@@ -52351,8 +53985,8 @@ async function resolveProfilePath(fs, topic) {
52351
53985
  throw new Error(`--topic must be a lowercase slug (e.g. ai-news), got '${topic}'`);
52352
53986
  }
52353
53987
  const candidates = [
52354
- resolve22(process.cwd(), PROFILE_DIR_REL, `${topic}.yaml`),
52355
- resolve22(CHECKOUT_ROOT3, PROFILE_DIR_REL, `${topic}.yaml`)
53988
+ resolve23(process.cwd(), PROFILE_DIR_REL, `${topic}.yaml`),
53989
+ resolve23(CHECKOUT_ROOT3, PROFILE_DIR_REL, `${topic}.yaml`)
52356
53990
  ];
52357
53991
  for (const path of candidates) {
52358
53992
  if (await fs.exists(path))
@@ -52389,7 +54023,7 @@ async function extractAction(options) {
52389
54023
  try {
52390
54024
  const policy = (await readStoreConfig(resolveKkConfigPath())).labels;
52391
54025
  const docs = (await store.items.selectByProfile(query, policy)).map(toPoolDoc2);
52392
- await fs.ensureDir(dirname12(options.out));
54026
+ await fs.ensureDir(dirname13(options.out));
52393
54027
  await fs.writeFile(options.out, `${JSON.stringify(docs, null, 2)}
52394
54028
  `);
52395
54029
  consoleOutput.write(JSON.stringify({ count: docs.length }));
@@ -52424,6 +54058,65 @@ async function countPoolItems(fs, path) {
52424
54058
  return 0;
52425
54059
  }
52426
54060
  }
54061
+ var DEFAULT_STATS_DAYS = 14;
54062
+ async function statsAction(options) {
54063
+ try {
54064
+ const days = Number(options.days === "" ? DEFAULT_STATS_DAYS : options.days);
54065
+ if (!Number.isInteger(days) || days <= 0) {
54066
+ throw new Error(`--days must be a positive integer, got '${options.days}'`);
54067
+ }
54068
+ const sinceMs = Date.now() - days * 86400000;
54069
+ const dbPath = await resolveStorePath(resolveKkConfigPath(), options.db);
54070
+ const store = await openStore({ path: dbPath });
54071
+ try {
54072
+ const rows = await store.collectView.channelYield(sinceMs);
54073
+ const sources = [...new Set(rows.map((row) => row.source))].sort();
54074
+ const byDay = new Map;
54075
+ for (const row of rows) {
54076
+ const entry = byDay.get(row.day) ?? new Map;
54077
+ entry.set(row.source, (entry.get(row.source) ?? 0) + row.items);
54078
+ byDay.set(row.day, entry);
54079
+ }
54080
+ const bySource = {};
54081
+ for (const row of rows) {
54082
+ bySource[row.source] = (bySource[row.source] ?? 0) + row.items;
54083
+ }
54084
+ const window = [...byDay.entries()].map(([day, counts]) => ({
54085
+ day,
54086
+ total: [...counts.values()].reduce((sum, n) => sum + n, 0),
54087
+ sources: Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b)))
54088
+ }));
54089
+ if (options.json === true) {
54090
+ consoleOutput.write(JSON.stringify({ days, byDay: window, bySource }));
54091
+ return;
54092
+ }
54093
+ consoleOutput.write(renderChannelTable(window, sources, bySource));
54094
+ } finally {
54095
+ await store.close();
54096
+ }
54097
+ } catch (error) {
54098
+ failLoud5("kk.store.stats.failed", error);
54099
+ }
54100
+ }
54101
+ function renderChannelTable(window, sources, bySource) {
54102
+ if (window.length === 0)
54103
+ return "no items in the window";
54104
+ const headers = ["day", ...sources, "total"];
54105
+ const body = window.map((entry) => [
54106
+ entry.day,
54107
+ ...sources.map((source) => String(entry.sources[source] ?? 0)),
54108
+ String(entry.total)
54109
+ ]);
54110
+ const footer = [
54111
+ "TOTAL",
54112
+ ...sources.map((source) => String(bySource[source] ?? 0)),
54113
+ String(window.reduce((sum, entry) => sum + entry.total, 0))
54114
+ ];
54115
+ const widths = headers.map((header, i) => Math.max(header.length, ...body.map((row) => (row[i] ?? "").length), (footer[i] ?? "").length));
54116
+ const line = (row) => row.map((cell, i) => i === 0 ? cell.padEnd(widths[i] ?? 0) : cell.padStart(widths[i] ?? 0)).join(" ");
54117
+ return [line(headers), ...body.map(line), line(footer)].join(`
54118
+ `);
54119
+ }
52427
54120
  async function poolCountAction(options) {
52428
54121
  try {
52429
54122
  const min = Number(options.min);
@@ -52543,6 +54236,7 @@ function registerStoreCommand(program) {
52543
54236
  store.command("label").description("Label items as raw observations, heuristic-first; prints the {verdict, confidence, labeled, model, fallback, consults, consultMs} line, or re-label a historical pool with --replay").option("--run <id>", "Collect run id the labels belong to (required with --new-ids)").option("--new-ids <path>", "Input path listing the item ids this pass must decide").option("--replay <selector>", "Re-label a historical pool (mutually exclusive with --new-ids): 'topic:<profile>', 'run:<collect-run-id>' or 'since:<n><s|m|h|d>'; requires --maker").option("--decisions <ids>", `Comma-separated decision ids a --replay routes (default: ${CATALOG_DECISION_IDS.join(",")})`).option("--maker <name>", "Decision maker override for the catalog calls (defaults to .spur/config.yaml decisions.maker; required with --replay)").option("--mode <mode>", "Labeling mode: 'heuristic' (default) asks the model only where the keyword pass is unsure; 'decision' asks it for every item", "heuristic").option("--window <duration>", "Only items first seen within this window ('<n>s|m|h|d') for a 'topic:' replay selector", DEFAULT_EXTRACT_WINDOW).option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(labelAction);
52544
54237
  store.command("extract").description("Select a topic pool from the stored labels via a profile; writes Doc[] and prints {count}").requiredOption("--topic <topic>", `Topic profile name (reads ${PROFILE_DIR_REL}/<topic>.yaml)`).option("--window <duration>", "Only items first seen within this window ('<n>s|m|h|d')", DEFAULT_EXTRACT_WINDOW).option("--limit <n>", "Pool cap (default: the profile limit, else 20)").option("--include-selected", "Include items whose stories are already selected or published for --topic").requiredOption("--out <path>", "Output Doc[] JSON path (the fan-in blended.json shape)").option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(extractAction);
52545
54238
  store.command("pool-count").description("Guard predicate: exit 0 when the pool file holds at least --min items (prints {count, min})").argument("<file>", "Pool artifact: a Doc[] JSON file or an object carrying metadata.docs").requiredOption("--min <n>", "Minimum item count for exit 0").action((file, options) => poolCountAction({ file, min: options.min }));
54239
+ store.command("stats").description("New-item yield per local day per source channel (read-only channel-width report)").option("--days <n>", "Trailing window in days", String(DEFAULT_STATS_DAYS)).option("--json", "Print the {days, byDay, bySource} JSON instead of the table").option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(statsAction);
52546
54240
  store.command("select").description("Record a run\u2019s planned set as selected stories for --topic (idempotent); prints {selected}").requiredOption("--topic <topic>", "Topic profile the selection belongs to (e.g. ai-news)").requiredOption("--workflow <name>", "Workflow that made the selection (provenance)").requiredOption("--run <id>", "Run id the selection belongs to").requiredOption("--in <path>", "Input file: a Doc[] or an object carrying metadata.docs").option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(selectAction);
52547
54241
  store.command("mark-published").description("Promote a run\u2019s selected rows to published; prints {published} (0 is exit 0)").requiredOption("--topic <topic>", "Topic profile the selection belongs to (e.g. ai-news)").requiredOption("--run <id>", "Run id whose selected rows are promoted").option("--in <path>", "Optional file listing the ids to promote (same shapes as select)").option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(markPublishedAction);
52548
54242
  store.command("release").description("Drop a run\u2019s selected rows so its items re-enter the pool; prints {released}").requiredOption("--run <id>", "Run id whose selected rows are dropped").option("--topic <topic>", "Narrow the release to one topic (default: every topic the run selected for)").option("--db <path>", "Store database path (default: kk config store.db_path, else .works/kk.db)").action(releaseAction);