@gobing-ai/knowledge-kit 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2372 -1722
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +15 -7
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-scheduled-run.ts +91 -41
  88. package/plugins/kk/scripts/daily-stages.ts +394 -53
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
  90. package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
  91. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  92. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  93. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  95. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  96. package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
  97. package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
  98. package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
  99. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  100. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  101. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  102. package/plugins/kk/skills/publish/SKILL.md +1 -0
  103. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  104. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  105. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  106. package/plugins/kk/skills/topic/SKILL.md +1 -0
  107. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  108. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  109. package/plugins/kk/workflows/kk-collect.yaml +3 -3
  110. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +21 -19
  111. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +163 -63
  112. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  113. package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
  114. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  115. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  116. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  117. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  118. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  119. package/plugins/publishings/linkedin-pub/package.json +19 -0
  120. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  121. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  122. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  123. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/surfdash-pub/dist/index.js +53 -24
  125. package/plugins/publishings/surfdash-pub/src/index.ts +72 -25
  126. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  127. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  128. package/plugins/publishings/wechatmp-pub/dist/index.js +176 -32
  129. package/plugins/publishings/wechatmp-pub/src/flow.ts +298 -52
  130. package/plugins/publishings/weibo-pub/dist/index.js +100 -7
  131. package/plugins/publishings/weibo-pub/src/index.ts +111 -11
  132. package/plugins/publishings/x-pub/dist/index.js +7 -0
  133. package/plugins/publishings/x-pub/src/index.ts +11 -0
  134. package/plugins/publishings/xhs-pub/dist/index.js +164 -12
  135. package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
  136. package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
  137. package/plugins/publishings/xhs-pub/src/page.ts +26 -0
@@ -1442,6 +1442,180 @@ function dailyPublishPartial(args: string[]): void {
1442
1442
  * `KK_SURFDASH_PATCH_OFFLINE=1` keeps fixture tests hermetic — the plugin would otherwise write to,
1443
1443
  * commit and push a real checkout.
1444
1444
  */
1445
+ /**
1446
+ * `publish-matrix-partial` (0333): a partial/none window leaves an explicit recovery marker.
1447
+ *
1448
+ * Reads the window's receipt-derived outcome (written by `publish-matrix-classify`) — the legacy
1449
+ * `_17_publish_result.json` aggregate is never an input — names the exact failed target ids, and
1450
+ * points at the matrix rerun contract (`kk stage run publish-matrix-send <work_dir> --window <w>
1451
+ * --retry`). The rerun is receipt-scoped: `--retry` resends only effect-none failures, never a
1452
+ * succeeded or live-uncertain row, so there is no `--retry-from`/`_17` style file replay. A full
1453
+ * window deletes any stale marker from a previous attempt, so the report never re-flags recovered
1454
+ * targets.
1455
+ */
1456
+ function publishMatrixPartial(args: string[]): void {
1457
+ const flag = (name: string): string | undefined => {
1458
+ const i = args.indexOf(name);
1459
+ return i >= 0 ? args[i + 1] : undefined;
1460
+ };
1461
+ const workDir = args[0] ?? '';
1462
+ const window = flag('--window');
1463
+ if (workDir === '' || window === undefined || window === '') {
1464
+ failWith(1, 'usage: publish-matrix-partial <work_dir> --window <w>');
1465
+ }
1466
+ const outcomePath = join(workDir, '4-publish', `outcome.${window}.json`);
1467
+ const outcome = requireJson(outcomePath, 'publish-matrix-partial') as {
1468
+ status?: string;
1469
+ rows?: Array<{ id?: string; status?: string; dummy?: boolean }>;
1470
+ };
1471
+ const marker = join(workDir, '4-publish', `partial-marker.${window}.txt`);
1472
+ timed(workDir, 'publish-matrix-partial', () => {
1473
+ if (outcome.status === 'full') {
1474
+ if (isFile(marker)) sted('rm', marker, () => unlinkSync(marker));
1475
+ return;
1476
+ }
1477
+ const failed = (outcome.rows ?? [])
1478
+ .filter((row) => row.dummy !== true && row.status !== 'succeeded')
1479
+ .map((row) => row.id ?? '(unknown)')
1480
+ .join(',');
1481
+ sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
1482
+ sted('write', marker, () =>
1483
+ writeRaw(
1484
+ marker,
1485
+ `window ${window} ended ${outcome.status ?? 'unknown'}: failed targets: ${failed === '' ? '(unparsed)' : failed}\n` +
1486
+ `recovery: kk stage run publish-matrix-send ${workDir} --window ${window} --retry (resends effect-none failures only; receipts scope the retry)\n`,
1487
+ ),
1488
+ );
1489
+ console.error(`WARNING: partial publish — failed targets: ${failed === '' ? '(unparsed)' : failed}`);
1490
+ });
1491
+ }
1492
+
1493
+ /**
1494
+ * `publish-matrix-patch-surfdash` (0333): patch podcast links into a real surfdash post.
1495
+ *
1496
+ * The patch payload is derived from the window's own receipts, not from legacy aggregate files and
1497
+ * not from a `KK_SURFDASH_ROOT` argument — the plugin resolves its checkout itself. Evidence rules:
1498
+ * the surfdash row must have a real (non-dummy) succeeded receipt whose aggregate metadata carries
1499
+ * an absolute `postPath`; without that there is nothing to patch and the stage records an explicit
1500
+ * skip result instead of fabricating one. Podcast links ride along only when the podcast row is
1501
+ * real too — a dummy or missing episode yields explicit empty `coverUrl`/`showNotesUrl`, never a
1502
+ * fabricated link. `KK_SURFDASH_PATCH_OFFLINE=1` keeps rehearsal runs hermetic the same way the
1503
+ * legacy voice2 stage does.
1504
+ */
1505
+ function publishMatrixPatchSurfdash(args: string[]): void {
1506
+ const flag = (name: string): string | undefined => {
1507
+ const i = args.indexOf(name);
1508
+ return i >= 0 ? args[i + 1] : undefined;
1509
+ };
1510
+ const workDir = args[0] ?? '';
1511
+ const window = flag('--window');
1512
+ if (workDir === '' || window === undefined || window === '') {
1513
+ failWith(1, 'usage: publish-matrix-patch-surfdash <work_dir> --window <w>');
1514
+ }
1515
+ const publishDir = join(workDir, '4-publish');
1516
+ const resultPath = join(publishDir, 'patch-surfdash.result.json');
1517
+ const writeResult = (value: Record<string, unknown>): void => {
1518
+ sted('write', resultPath, () => writeJqText(resultPath, value));
1519
+ };
1520
+ const receipt = (id: string): Record<string, unknown> | null =>
1521
+ readJson(join(publishDir, 'receipts', `${id}.json`));
1522
+ const aggregateTarget = (receiptPath: string | undefined): Record<string, unknown> => {
1523
+ const aggregate = receiptPath === undefined ? null : readJson(receiptPath);
1524
+ const targets = aggregate?.targets;
1525
+ if (!Array.isArray(targets) || targets.length === 0 || targets[0] === null || typeof targets[0] !== 'object') {
1526
+ return {};
1527
+ }
1528
+ return targets[0] as Record<string, unknown>;
1529
+ };
1530
+ timed(workDir, 'publish-matrix-patch-surfdash', () => {
1531
+ const outcome = readJson(join(publishDir, `outcome.${window}.json`)) as {
1532
+ rows?: Array<{ id?: string }>;
1533
+ } | null;
1534
+ const rowId = (prefix: string): string | undefined =>
1535
+ (outcome?.rows ?? []).map((row) => row.id ?? '').find((id) => id.startsWith(prefix));
1536
+ const sdId = rowId('surfdash-pub');
1537
+ const podId = rowId('podcast-pub');
1538
+ if (sdId === undefined) {
1539
+ const error = 'skipped: no surfdash target in this window — nothing to patch';
1540
+ writeResult({ ok: false, target: 'surfdash-pub', error });
1541
+ console.error(`patch-surfdash: ${error}`);
1542
+ return;
1543
+ }
1544
+ const sdReceipt = receipt(sdId);
1545
+ const sdReal = sdReceipt !== null && sdReceipt?.dummy !== true && sdReceipt?.status === 'succeeded';
1546
+ const sdTarget = aggregateTarget(
1547
+ typeof sdReceipt?.aggregatePath === 'string' ? sdReceipt.aggregatePath : undefined,
1548
+ );
1549
+ const postPath = (sdTarget.metadata as Record<string, unknown> | undefined)?.postPath;
1550
+ if (!sdReal || typeof postPath !== 'string' || postPath === '') {
1551
+ const error =
1552
+ sdReceipt === null
1553
+ ? 'skipped: no surfdash publish receipt — nothing to patch'
1554
+ : typeof postPath !== 'string' || postPath === ''
1555
+ ? 'skipped: surfdash receipt carries no real postPath — nothing to patch'
1556
+ : 'skipped: surfdash receipt is dummy — nothing to patch';
1557
+ writeResult({ ok: false, target: 'surfdash-pub', error });
1558
+ console.error(`patch-surfdash: ${error}`);
1559
+ return;
1560
+ }
1561
+ // Links ride along only from real podcast evidence; dummy/missing episodes patch as
1562
+ // explicit empty strings — the patch must never fabricate a link.
1563
+ const podReceipt = podId === undefined ? null : receipt(podId);
1564
+ const podReal = podReceipt !== null && podReceipt?.dummy !== true && podReceipt?.status === 'succeeded';
1565
+ const podTarget = aggregateTarget(
1566
+ typeof podReceipt?.aggregatePath === 'string' ? podReceipt.aggregatePath : undefined,
1567
+ );
1568
+ const podMeta = (podTarget.metadata ?? {}) as Record<string, unknown>;
1569
+ const runDate = String(readJson(join(publishDir, `outcome.${window}.json`))?.runDate ?? '');
1570
+ const payload = join(publishDir, 'patch-surfdash.payload.json');
1571
+ sted('write', payload, () =>
1572
+ writeJqText(payload, {
1573
+ body: '',
1574
+ options: {
1575
+ operation: 'patch',
1576
+ postPath,
1577
+ coverUrl: podReal && typeof podMeta.imageUrl === 'string' ? podMeta.imageUrl : '',
1578
+ showNotesUrl: podReal && typeof podTarget.url === 'string' ? podTarget.url : '',
1579
+ ...(runDate.length === 8
1580
+ ? { dateIso: `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` }
1581
+ : runDate.length === 10
1582
+ ? { dateIso: runDate }
1583
+ : {}),
1584
+ },
1585
+ }),
1586
+ );
1587
+ if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
1588
+ // Offline rehearsals still derive the payload (evidence), but never invoke the plugin.
1589
+ writeResult({ ok: false, target: 'surfdash-pub', error: 'skipped: offline (KK_SURFDASH_PATCH_OFFLINE=1)' });
1590
+ console.error(
1591
+ 'patch-surfdash: offline — payload derived, plugin patch skipped (KK_SURFDASH_PATCH_OFFLINE=1)',
1592
+ );
1593
+ return;
1594
+ }
1595
+ const status = kk(['executor', 'run', 'surfdash-pub', '--in', payload, '--out', resultPath], {
1596
+ unset: NESTED_SPUR_UNSET,
1597
+ soft: true,
1598
+ });
1599
+ const result = readJson(resultPath);
1600
+ if (result?.ok === true) {
1601
+ const meta = (result.metadata ?? {}) as Record<string, unknown>;
1602
+ console.error(
1603
+ `patch-surfdash: ${String(result.message ?? 'patched')} — cms-resync ${String(meta.cmsResync ?? 'ok')}`,
1604
+ );
1605
+ return;
1606
+ }
1607
+ const meta = (result?.metadata ?? {}) as Record<string, unknown>;
1608
+ if (meta.cmsResync === 'failed') {
1609
+ // The posts are patched and committed; only the CMS re-sync (a soft channel) failed.
1610
+ console.error(
1611
+ `patch-surfdash: WARNING: cms re-sync failed — ${String(result?.error ?? `exit ${status}`)}; fix: kk executor run surfdash-pub --in ${payload} --out ${resultPath}`,
1612
+ );
1613
+ return;
1614
+ }
1615
+ failWith(1, `patch-surfdash: ${String(result?.error ?? `surfdash-pub patch exited ${status}`)}`);
1616
+ });
1617
+ }
1618
+
1445
1619
  function dailyPatchSurfdash(args: string[]): void {
1446
1620
  const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
1447
1621
  if (!surfdashRoot || !existsSync(surfdashRoot)) {
@@ -1718,6 +1892,11 @@ if (import.meta.main)
1718
1892
  dailyPreflight(rest);
1719
1893
  break;
1720
1894
  }
1895
+ case 'daily-safety-manifest': {
1896
+ const { dailySafetyManifest } = await loadDailyStages();
1897
+ dailySafetyManifest(rest);
1898
+ break;
1899
+ }
1721
1900
  case 'daily-safety-prep': {
1722
1901
  const { dailySafetyPrep } = await loadDailyStages();
1723
1902
  dailySafetyPrep(rest);
@@ -1768,6 +1947,11 @@ if (import.meta.main)
1768
1947
  dailyQualityReport(rest);
1769
1948
  break;
1770
1949
  }
1950
+ case 'daily-quality-verdict': {
1951
+ const { dailyQualityVerdict } = await loadDailyStages();
1952
+ dailyQualityVerdict(rest);
1953
+ break;
1954
+ }
1771
1955
  case 'daily-publish-prep': {
1772
1956
  const { dailyPublishPrep } = await loadDailyStages();
1773
1957
  dailyPublishPrep(rest);
@@ -1820,6 +2004,12 @@ if (import.meta.main)
1820
2004
  case 'daily-publish-partial':
1821
2005
  dailyPublishPartial(rest);
1822
2006
  break;
2007
+ case 'publish-matrix-partial':
2008
+ publishMatrixPartial(rest);
2009
+ break;
2010
+ case 'publish-matrix-patch-surfdash':
2011
+ publishMatrixPatchSurfdash(rest);
2012
+ break;
1823
2013
  case 'daily-run-report':
1824
2014
  dailyRunReport(rest);
1825
2015
  break;
@@ -41,8 +41,10 @@ function readJson(path: string): JsonObject | null {
41
41
  }
42
42
  }
43
43
 
44
- function safetyPath(workDir: string, runDate: string, name: string): string {
45
- return join(workDir, '24-script', `${runDate}_09_${name}`);
44
+ /** 0331: scope-aware artifact root — `24-script` (or empty) keeps the drafts paths byte-identical. */
45
+ function safetyPath(workDir: string, scopeDir: string, runDate: string, name: string): string {
46
+ const dir = scopeDir.trim() === '' || scopeDir.trim() === '24-script' ? '24-script' : scopeDir.trim();
47
+ return join(workDir, dir, `${runDate}_09_${name}`);
46
48
  }
47
49
 
48
50
  function nonEmptyString(value: unknown): value is string {
@@ -59,9 +61,9 @@ function sortedUnique(values: string[]): string[] | null {
59
61
  return unique.length === values.length ? unique.sort() : null;
60
62
  }
61
63
 
62
- function reportValid(workDir: string, runDate: string): string | null {
63
- const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
64
- const report = readJson(safetyPath(workDir, runDate, 'safety-review_report.json'));
64
+ function reportValid(workDir: string, scopeDir: string, runDate: string): string | null {
65
+ const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
66
+ const report = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_report.json'));
65
67
  if (request === null || report === null) return 'review request or report is missing/invalid JSON';
66
68
 
67
69
  const requested = stringList(request.platforms);
@@ -100,8 +102,8 @@ function reportValid(workDir: string, runDate: string): string | null {
100
102
  return null;
101
103
  }
102
104
 
103
- function judgeValid(workDir: string, runDate: string): string | null {
104
- const verdict = readJson(safetyPath(workDir, runDate, 'safety-judge_verdict.json'));
105
+ function judgeValid(workDir: string, scopeDir: string, runDate: string): string | null {
106
+ const verdict = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
105
107
  if (verdict === null) return 'judge verdict is missing/invalid JSON';
106
108
  if (!VERDICTS.has(String(verdict.verdict))) return 'judge verdict value is invalid';
107
109
  if (!CONFIDENCE.has(String(verdict.confidence))) return 'judge confidence is missing/invalid';
@@ -149,10 +151,10 @@ function judgeValid(workDir: string, runDate: string): string | null {
149
151
  * Only the gate's persisted status represents a real content-safety escalation, so unattended mode
150
152
  * rejects that status while attended mode leaves it for the caller's pause node.
151
153
  */
152
- function publishValid(workDir: string, runDate: string, unattendedArg: string): string | null {
154
+ function publishValid(workDir: string, scopeDir: string, runDate: string, unattendedArg: string): string | null {
153
155
  if (unattendedArg !== 'true' && unattendedArg !== 'false') return 'unattended must be true or false';
154
- const verdictToken = safetyPath(workDir, runDate, 'safety-verdict.status');
155
- const gateStatusPath = safetyPath(workDir, runDate, 'safety-judge_status.txt');
156
+ const verdictToken = safetyPath(workDir, scopeDir, runDate, 'safety-verdict.status');
157
+ const gateStatusPath = safetyPath(workDir, scopeDir, runDate, 'safety-judge_status.txt');
156
158
  let token = '';
157
159
  let gateStatus = '';
158
160
  try {
@@ -171,27 +173,62 @@ function publishValid(workDir: string, runDate: string, unattendedArg: string):
171
173
  return null;
172
174
  }
173
175
 
174
- function draftDigest(workDir: string, runDate: string): string | null {
175
- const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
176
- const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
176
+ function draftDigest(workDir: string, scopeDir: string, runDate: string): string | null {
177
+ const hash = createHash('sha256');
178
+ if (scopeDir.trim() === '' || scopeDir.trim() === '24-script') {
179
+ const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
180
+ const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
181
+ try {
182
+ hash.update(readFileSync(article)).update('\0');
183
+ hash.update(readFileSync(script)).update('\0');
184
+ return hash.digest('hex');
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ // Variants scope: digest over the manifest's sorted keys + current bytes (mirrors the
190
+ // daily-stages scopeDigest algorithm, so request/guard/verdict all bind the same bytes).
177
191
  try {
178
- const hash = createHash('sha256');
179
- hash.update(readFileSync(article)).update('\0');
180
- hash.update(readFileSync(script)).update('\0');
192
+ const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
193
+ variants?: Record<string, { path?: unknown }>;
194
+ } | null;
195
+ if (manifest === null || !isObject(manifest.variants)) return null;
196
+ for (const key of Object.keys(manifest.variants).sort()) {
197
+ const path = join(workDir, String(manifest.variants[key].path));
198
+ hash.update(key).update('\0').update(readFileSync(path)).update('\0');
199
+ }
181
200
  return hash.digest('hex');
182
201
  } catch {
183
202
  return null;
184
203
  }
185
204
  }
186
205
 
187
- function revisionValid(workDir: string, runDate: string): string | null {
188
- const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
206
+ function revisionValid(workDir: string, scopeDir: string, runDate: string): string | null {
207
+ const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
208
+ if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
209
+ const isVariants = scopeDir.trim() !== '' && scopeDir.trim() !== '24-script';
210
+ const digest = draftDigest(workDir, scopeDir, runDate);
211
+ if (digest === null) {
212
+ return isVariants
213
+ ? 'a manifest-listed variant is missing/unreadable'
214
+ : 'revised article or VoiceScript is missing/unreadable';
215
+ }
216
+ if (digest === request.digest) return isVariants ? 'variant digest is unchanged' : 'draft digest is unchanged';
217
+ if (isVariants) {
218
+ const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
219
+ variants?: Record<string, { path?: unknown }>;
220
+ } | null;
221
+ if (manifest === null || !isObject(manifest.variants)) return 'variants manifest is missing/malformed';
222
+ for (const entry of Object.values(manifest.variants)) {
223
+ const value = readJson(join(workDir, String(entry.path)));
224
+ if (value === null || !nonEmptyString(value.title) || !nonEmptyString(value.body)) {
225
+ return 'revised variant title/body is missing';
226
+ }
227
+ }
228
+ return null;
229
+ }
189
230
  const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
190
231
  const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
191
- if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
192
- if (draftDigest(workDir, runDate) === null) return 'revised article or VoiceScript is missing/unreadable';
193
- if (draftDigest(workDir, runDate) === request.digest) return 'draft digest is unchanged';
194
-
195
232
  const article = readJson(articlePath);
196
233
  if (article === null || !nonEmptyString(article.title) || !nonEmptyString(article.body)) {
197
234
  return 'revised article title/body is missing';
@@ -207,22 +244,27 @@ function revisionValid(workDir: string, runDate: string): string | null {
207
244
  }
208
245
 
209
246
  function main(): number {
210
- const [kind = '', workDir = '', runDate = '', unattended = 'false'] = process.argv.slice(2);
247
+ // 0331: an optional trailing scope dir re-roots every artifact path (variants review reads
248
+ // 4-safety-variants); publish-valid keeps `unattended` in the 4th slot it has always had.
249
+ const [kind = '', workDir = '', runDate = '', fourth = '', fifth = ''] = process.argv.slice(2);
250
+ const isPublish = kind === 'publish-valid';
251
+ const unattended = isPublish ? fourth : 'false';
252
+ const scopeDir = isPublish ? fifth : fourth;
211
253
  if (!kind || !workDir || !runDate) {
212
254
  console.error(
213
- 'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended]',
255
+ 'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended] [scope_dir]',
214
256
  );
215
257
  return 2;
216
258
  }
217
259
  const reason =
218
260
  kind === 'report-valid'
219
- ? reportValid(workDir, runDate)
261
+ ? reportValid(workDir, scopeDir, runDate)
220
262
  : kind === 'judge-valid'
221
- ? judgeValid(workDir, runDate)
263
+ ? judgeValid(workDir, scopeDir, runDate)
222
264
  : kind === 'revision-valid'
223
- ? revisionValid(workDir, runDate)
265
+ ? revisionValid(workDir, scopeDir, runDate)
224
266
  : kind === 'publish-valid'
225
- ? publishValid(workDir, runDate, unattended)
267
+ ? publishValid(workDir, scopeDir, runDate, unattended)
226
268
  : 'unknown predicate';
227
269
  if (reason !== null) {
228
270
  console.error(`safety-artifact-check: ${kind}: ${reason}`);
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * voice3-publish-window — cron-safe launcher for the voice3 shadow publish window (task 0334).
4
+ *
5
+ * WHY a launcher (audit F08/F09/F10, docs/design/kk-daily-ai-voice3.md):
6
+ * - cron ends a command at the first UNESCAPED % and feeds the rest to stdin, so any inline
7
+ * `$(date +%Y%m%d)` shell in a crontab entry silently truncates the command. Date math and
8
+ * flag assembly live here, in plain TypeScript, behind zero percent signs.
9
+ * - the publish window must read the GENERATION bundle (`<works>/kk-daily-ai-voice2/<date>/
10
+ * bundle.json` — voice3 prepares under the voice2 namespace until the operator's cutover
11
+ * changes it), never the publish3 output directory that earlier entries pointed at.
12
+ * - the CN consent token list is validated against the pinned preset's WINDOW rows before any
13
+ * spur process starts; the old baked `wechatmp-pub,xhs-pub,…` list (tokens that have no
14
+ * cn-peak row) is refused up front.
15
+ *
16
+ * Contract: everything before `spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml`
17
+ * is preparation and refusal; the only side effects are refusal messages and, on success, the
18
+ * single spur exec. `KK_SPUR_BIN` pins the spur binary (same convention as board/capture.ts and
19
+ * decision-hub.ts); `--dry-run` prints the forwarded vars and stops (rehearsal aid).
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { homedir } from 'node:os';
24
+ import { dirname, join, resolve } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const USAGE =
28
+ 'usage: voice3-publish-window.ts --window <after-generate|cn-peak|overseas-rss> [--run-date <YYYY-MM-DD|YYYYMMDD>] ' +
29
+ '[--preset <slug>] [--live-channels a,b] [--work-dir <dir>] [--dummy] [--dry-run]';
30
+
31
+ const WINDOWS = new Set(['after-generate', 'cn-peak', 'overseas-rss']);
32
+
33
+ function failWith(code: number, message: string): never {
34
+ console.error(message);
35
+ process.exit(code);
36
+ }
37
+
38
+ function expandHome(raw: string): string {
39
+ return raw === '~' || raw.startsWith('~/') ? join(homedir(), raw.slice(1)) : raw;
40
+ }
41
+
42
+ /** KK_WORKS_DIR env > config works_dir > <cwd>/.works — the kk-workflow-stages precedence. */
43
+ function resolveWorksDir(): string {
44
+ const fromEnv = process.env.KK_WORKS_DIR;
45
+ if (fromEnv) return expandHome(fromEnv);
46
+ const configPath = process.env.KK_CONFIG || join(homedir(), '.config/kk/config.yaml');
47
+ if (existsSync(configPath)) {
48
+ try {
49
+ const parsed = Bun.YAML.parse(readFileSync(configPath, 'utf-8')) as Record<string, unknown> | null;
50
+ const worksDir = parsed !== null && typeof parsed === 'object' ? parsed.works_dir : undefined;
51
+ if (typeof worksDir === 'string' && worksDir !== '') return expandHome(worksDir);
52
+ } catch {
53
+ // fall through to the compiled default
54
+ }
55
+ }
56
+ return resolve(process.cwd(), '.works');
57
+ }
58
+
59
+ /** A real calendar day as `YYYY-MM-DD`, or undefined (mirrors publish-matrix normalizeRunDate). */
60
+ function normalizeRunDate(raw: string): string | undefined {
61
+ const match = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(raw.trim());
62
+ if (match === null) return undefined;
63
+ const iso = `${match[1]}-${match[2]}-${match[3]}`;
64
+ const parsed = new Date(`${iso}T00:00:00Z`);
65
+ if (
66
+ Number.isNaN(parsed.getTime()) ||
67
+ parsed.getUTCFullYear() !== Number(match[1]) ||
68
+ parsed.getUTCMonth() + 1 !== Number(match[2]) ||
69
+ parsed.getUTCDate() !== Number(match[3])
70
+ ) {
71
+ return undefined;
72
+ }
73
+ return iso;
74
+ }
75
+
76
+ /** Today (offset 0) or yesterday (offset -1) in America/Los_Angeles — the episodes' own day. */
77
+ function losAngelesDate(offsetDays: number): string {
78
+ const instant = new Date(Date.now() + offsetDays * 24 * 60 * 60 * 1000);
79
+ // en-CA renders YYYY-MM-DD; Intl does the zone math cron cannot do inline.
80
+ return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Los_Angeles' }).format(instant);
81
+ }
82
+
83
+ /** `--flag value`, or undefined. */
84
+ function flagValue(args: string[], flag: string): string | undefined {
85
+ const index = args.indexOf(flag);
86
+ return index >= 0 ? args[index + 1] : undefined;
87
+ }
88
+
89
+ function main(): void {
90
+ const args = process.argv.slice(2);
91
+ if (args.includes('--help') || args.includes('-h')) {
92
+ console.log(USAGE);
93
+ process.exit(0);
94
+ }
95
+ const window = flagValue(args, '--window');
96
+ if (window === undefined || !WINDOWS.has(window))
97
+ failWith(1, `${USAGE}\nvoice3-publish-window: --window must be one of ${[...WINDOWS].join('|')}`);
98
+ const dummy = args.includes('--dummy');
99
+ const dryRun = args.includes('--dry-run');
100
+ const preset = flagValue(args, '--preset');
101
+ const liveChannelsRaw = flagValue(args, '--live-channels');
102
+ const workDirOverride = flagValue(args, '--work-dir');
103
+
104
+ // R1: run date = explicit flag, else window-derived LA day (overseas reads the episode that
105
+ // landed after the 07:00 tick, i.e. yesterday).
106
+ const runDateRaw = flagValue(args, '--run-date');
107
+ let runDate: string | undefined;
108
+ if (runDateRaw !== undefined && runDateRaw.trim() !== '') {
109
+ runDate = normalizeRunDate(runDateRaw);
110
+ if (runDate === undefined)
111
+ failWith(1, `voice3-publish-window: '${runDateRaw}' is not a valid calendar day (YYYY-MM-DD or YYYYMMDD)`);
112
+ } else {
113
+ runDate = losAngelesDate(window === 'overseas-rss' ? -1 : 0);
114
+ }
115
+ const digits = runDate.replaceAll('-', '');
116
+
117
+ if (dummy && liveChannelsRaw !== undefined) {
118
+ failWith(1, 'voice3-publish-window: --dummy and --live-channels are mutually exclusive');
119
+ }
120
+
121
+ // R3/F10: live tokens validate against the pinned preset's WINDOW rows before any spawn.
122
+ const liveChannels =
123
+ liveChannelsRaw !== undefined && liveChannelsRaw.trim() !== ''
124
+ ? liveChannelsRaw
125
+ .split(',')
126
+ .map((token) => token.trim())
127
+ .filter((token) => token !== '')
128
+ : [];
129
+ if (liveChannels.length > 0 && preset === undefined) {
130
+ failWith(1, 'voice3-publish-window: --live-channels requires an explicit --preset to validate the window');
131
+ }
132
+ if (liveChannels.length > 0) {
133
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
134
+ const presetPath = join(dirname(scriptDir), 'publish-presets', `${preset}.json`);
135
+ if (!existsSync(presetPath))
136
+ failWith(1, `voice3-publish-window: preset '${preset}' not found at ${presetPath}`);
137
+ const parsed = JSON.parse(readFileSync(presetPath, 'utf-8')) as {
138
+ targets?: Array<{ plugin: string; id?: string; mode?: string; window?: string }>;
139
+ };
140
+ const targets = parsed.targets ?? [];
141
+ const inWindow = targets.filter((target) => (target.window ?? 'after-generate') === window);
142
+ const refused = liveChannels.filter((token) => {
143
+ const row = inWindow.find((target) => target.plugin === token || target.id === token);
144
+ return row === undefined || (row.mode ?? 'draft') === 'off';
145
+ });
146
+ if (refused.length > 0) {
147
+ failWith(
148
+ 1,
149
+ `voice3-publish-window: live token(s) ${refused.join(', ')} have no active row in window '${window}' of preset '${preset}'` +
150
+ ` (send-confirm resumes are consent-scoped per window; see docs/design/kk-daily-ai-voice3.md)`,
151
+ );
152
+ }
153
+ }
154
+
155
+ // R1/F08: the bundle is the GENERATION artifact under the voice2 namespace, never a publish3
156
+ // output dir. Mismatched/missing bundles refuse before spur starts.
157
+ const genDir =
158
+ workDirOverride !== undefined && workDirOverride.trim() !== ''
159
+ ? resolve(workDirOverride)
160
+ : join(resolveWorksDir(), 'kk-daily-ai-voice2', digits);
161
+ const bundlePath = join(genDir, 'bundle.json');
162
+ if (!existsSync(bundlePath)) failWith(1, `voice3-publish-window: no generation bundle at ${bundlePath}`);
163
+ let bundleRunDate: string | undefined;
164
+ try {
165
+ const bundle = JSON.parse(readFileSync(bundlePath, 'utf-8')) as { runDate?: string };
166
+ bundleRunDate = typeof bundle.runDate === 'string' ? normalizeRunDate(bundle.runDate) : undefined;
167
+ } catch {
168
+ failWith(1, `voice3-publish-window: unreadable generation bundle at ${bundlePath}`);
169
+ }
170
+ if (bundleRunDate !== runDate) {
171
+ failWith(
172
+ 1,
173
+ `voice3-publish-window: bundle at ${bundlePath} is dated ${bundleRunDate ?? 'unknown'}, not the requested ${runDate}`,
174
+ );
175
+ }
176
+
177
+ const vars: Record<string, string> = {
178
+ work_dir: genDir,
179
+ run_date: digits,
180
+ window,
181
+ };
182
+ if (preset !== undefined && preset.trim() !== '') vars.preset = preset;
183
+ if (liveChannels.length > 0) vars.live_channels = liveChannels.join(',');
184
+ if (dummy) vars.dummy = 'true';
185
+
186
+ if (dryRun) {
187
+ console.log(JSON.stringify(vars, null, 2));
188
+ process.exit(0);
189
+ }
190
+
191
+ const proc = spawnSync(
192
+ process.env.KK_SPUR_BIN || 'spur',
193
+ ['workflow', 'run', 'plugins/kk/workflows/kk-daily-publish3.yaml', '--vars', JSON.stringify(vars)],
194
+ {
195
+ stdio: 'inherit',
196
+ },
197
+ );
198
+ // node spawnSync surfaces the child's code as `status` (exitCode is the async-API field).
199
+ process.exit(proc.status ?? 1);
200
+ }
201
+
202
+ main();
@@ -3,6 +3,7 @@ name: article-adapt
3
3
  # Description rules: front-load the leading identity phrase; one trigger per genuine
4
4
  # branch (collapse synonym triggers into one); never restate the body's identity line.
5
5
  description: This skill should be used when a kk-itc publish plan contains variants with adapt=true and per-variant Content files must be written — "adapt the article", "translate for the target profile", "write content.zh-article.json", or when a spur workflow agent.run step invokes kk:article-adapt. Translates title and body to the variant locale, applies the plan limits, and preserves code blocks, URLs and references; it holds no platform table.
6
+ requires: [cli:kk, cli:spur, local-fs]
6
7
  ---
7
8
 
8
9
  # article-adapt
@@ -6,6 +6,7 @@ description: >-
6
6
  "outline options for audio", "author a VoiceScript", "revise the script".
7
7
  Runs briefing → outline → script with explicit operator gates; solo mode
8
8
  ships first, dialogue/interview/narration are named extension points.
9
+ requires: [local-fs]
9
10
  ---
10
11
 
11
12
  # audio-authoring — spoken-audio authoring (create | briefing | outline | script)
@@ -2,6 +2,7 @@
2
2
  name: content-judge
3
3
  description: This skill should be used when an LLM evaluation gate is needed in a knowledge-kit pipeline — "judge this content", "evaluate against rubric", "quality gate before publish", "check technical accuracy", "review brand tone", "compliance check", or when a spur workflow step of type `agent.run` targets the content-judge skill to emit a PASS/FAIL/NEEDS_REVISION verdict. Centralizes evaluation rubrics, prompt templates, and structured verdict emission for generated Content.
4
4
  version: 0.1.0
5
+ requires: [cli:kk, cli:spur, local-fs]
5
6
  ---
6
7
 
7
8
  # content-judge — LLM-as-judge evaluation skill
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: content-safety
3
3
  description: This skill should be used when a knowledge-kit pipeline or operator needs a content-safety / platform-admission review before publishing — "check this before publishing", "小宇宙/公众号/网易云/苹果播客 审核风险", "desensitize personal info", "verify disclosure and sourcing before publish". Detects admission risk per distribution platform, proposes honest revisions or exclusion, and escalates what it cannot verify. Quality scoring is content-judge's job.
4
+ requires: [local-fs]
4
5
  ---
5
6
 
6
7
  # content-safety — pre-publication content-safety review
@@ -0,0 +1,41 @@
1
+ # Salty Podcast (podcast-salty) — platform reference
2
+
3
+ `podcast.salty.vip` 是本项目自有的播客站点(Robin Min 自有域名,microfeed 实例,节目「每日AI乱炖」,RSS `/rss/`)。
4
+ 这是**自有渠道**,不是第三方分发平台:没有平台审核队列、字级词表或收听量干预可引用。
5
+ 本文件存在的意义是把「自有播客宿主没有平台规则」这件事写清楚,避免审核流程把「无三方平台规则」
6
+ 误判成证据缺口并默认 BLOCK。
7
+
8
+ | Field | Value |
9
+ |-------|-------|
10
+ | Official source | 自有站点,无第三方协议页;本文件由站点维护者(Robin Min)本人核定 |
11
+ | Scope | `podcast.salty.vip` 节目标题、音频、show notes、封面(RSS 输出随 item 状态变化) |
12
+ | Effective date | 本文件创建日即生效(自有平台规则由维护者裁定) |
13
+ | Last verified | 2026-11-05(创建) |
14
+ | Evidence status | 维护者本人口径,非三方引用;无字级禁用词表 |
15
+
16
+ ## Evidence distinctions
17
+
18
+ - **维护者裁定(本文件的权威来源)**:播客节目是自有域名下的小型日更节目,采用与本项目其他
19
+ 渠一致的通用内容纪律;没有平台方提供的可引用条款。
20
+ - **与 apple-podcasts 的边界(AP-06 不在此生效)**:Apple Podcasts 目录收录是第三方目录的准入
21
+ 问题,由维护者人工决定,不由本 pipeline 控制;本 host 的 RSS 只是自有分发的出口。对未核实
22
+ 的估值/数字,节目文案应保持项目既有的 hedging 与出处注明(这是事实纪律,不是平台准入规则),
23
+ 但**不要**因为「聚合页转述未读原文」而 block——那属于 sourcing note,由维护者自行复核。
24
+ - **通用纪律(沿用技能默认)**:AI 声明、来源可靠性、隐私/PII、医疗/投资类内容红线全部沿用
25
+ 技能 default;本文件不新增平台规则。
26
+
27
+ ## Review items
28
+
29
+ | ID | Item | Grade |
30
+ |----|------|-------|
31
+ | PS-01 | 来源诚实性:show notes 中的对外引用(媒体/论文/Blog)须给出真实出处;转述链保持既有 hedging;未读原文的聚合转述记 note,不 push block。 | note |
32
+ | PS-02 | 数字与事实核对:与源链接一致的数字/日期/机构名冲突时按技能通用规则处理(correctable → revise;冲突无解 → block)。 | note→judge |
33
+ | PS-03 | 可识别个人信息(听众或第三方的私人信息) | desensitize→block if unfixable |
34
+ | PS-04 | 医疗/投资劝诱内容技能默认红线不变(未核实估值→保持 hedging 并注明出处,而非删除条目)。 | default |
35
+
36
+ ## Open questions / escalation
37
+
38
+ - 自有播客无第三方审核 → 本文件不应产生 `evidenceGap: true` 类 BLOCK;任何「podcast-salty 无
39
+ reference 文件」的发现都指向本文件缺失,而不是内容风险。
40
+ - 若未来节目上线商业化、赞助或接入第三方目录(Apple/小宇宙实际分配),本文件需要为相应渠道
41
+ 追加准入条款。