@gobing-ai/knowledge-kit 0.0.29 → 0.0.31

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +114 -38
  3. package/package.json +1 -1
  4. package/plugins/generations/daily-article-gen/dist/index.js +24 -7
  5. package/plugins/generations/daily-article-gen/src/index.ts +36 -12
  6. package/plugins/generations/dailynews-gen/dist/index.js +20 -3
  7. package/plugins/generations/dailynews-gen/src/index.ts +7 -0
  8. package/plugins/generations/dailynews-gen/src/script-builder.ts +4 -2
  9. package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
  10. package/plugins/generations/episode-plan-gen/dist/index.js +66 -5
  11. package/plugins/generations/episode-plan-gen/src/index.ts +103 -4
  12. package/plugins/generations/image-gen/dist/index.js +122 -20
  13. package/plugins/generations/image-gen/src/bytes.ts +106 -18
  14. package/plugins/generations/image-gen/src/index.ts +46 -6
  15. package/plugins/generations/image-gen/src/job.ts +6 -0
  16. package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
  17. package/plugins/generations/image-gen/src/providers/types.ts +12 -1
  18. package/plugins/kk/plugin.json +1 -1
  19. package/plugins/kk/scripts/daily-voice-style.zh.md +5 -5
  20. package/plugins/kk/scripts/itc-stages.ts +21 -3
  21. package/plugins/kk/scripts/kk-workflow-stages.ts +199 -7
  22. package/plugins/kk/scripts/topic-boundary.json +3 -12
  23. package/plugins/kk/skills/content-judge/SKILL.md +32 -0
  24. package/plugins/kk/skills/content-judge/references/workflow-integration.md +10 -2
  25. package/plugins/kk/skills/publish/SKILL.md +1 -1
  26. package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
  27. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +39 -14
  28. package/plugins/kk/workflows/kk-itc.yaml +21 -8
  29. package/plugins/publishings/xhs-pub/dist/index.js +1 -1
  30. package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
  31. package/plugins/publishings/xhs-pub/src/index.ts +1 -1
  32. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
  33. package/plugins/sp/lib/env.ts +0 -46
  34. package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
  35. package/plugins/sp/scripts/batch-preflight.mjs +0 -346
  36. package/plugins/sp/scripts/batch-preflight.ts +0 -459
  37. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
  38. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
  39. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  40. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
  41. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
  42. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
  43. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
  44. package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
  45. package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
  46. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
  47. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
  48. package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
  49. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
  50. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
  51. package/plugins/sp/scripts/idea-handoff.mjs +0 -22
  52. package/plugins/sp/scripts/idea-handoff.ts +0 -44
  53. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
  54. package/plugins/sp/scripts/inline-run-setup.ts +0 -722
  55. package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
  56. package/plugins/sp/scripts/pr-reviewing.ts +0 -925
  57. package/plugins/sp/scripts/quality-gate.mjs +0 -179
  58. package/plugins/sp/scripts/quality-gate.ts +0 -217
  59. package/plugins/sp/scripts/script-contract-check.ts +0 -319
  60. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  61. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
  62. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
  63. package/plugins/sp/scripts/task-size-precheck.ts +0 -175
  64. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  65. package/plugins/sp/scripts/validate-commands.ts +0 -689
  66. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  67. package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
  68. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
  69. package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
  70. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  71. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
  72. package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
  73. package/plugins/sp/scripts/wrapup-steps.ts +0 -552
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.0.29",
9
+ "version": "0.0.31",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
package/dist/index.js CHANGED
@@ -26530,6 +26530,11 @@ function killSubtree(pid) {
26530
26530
  } catch {}
26531
26531
  }
26532
26532
  async function killProcessTree(pid) {
26533
+ if (process.platform !== "win32") {
26534
+ try {
26535
+ process.kill(-pid, "SIGKILL");
26536
+ } catch {}
26537
+ }
26533
26538
  const sweep = () => {
26534
26539
  for (const child of listChildren(pid)) {
26535
26540
  killSubtree(child);
@@ -26549,7 +26554,8 @@ async function spawnPluginEntryAsync(opts) {
26549
26554
  cwd: opts.cwd,
26550
26555
  env: pluginEntryEnv(opts),
26551
26556
  stdout: "pipe",
26552
- stderr: "pipe"
26557
+ stderr: "pipe",
26558
+ detached: process.platform !== "win32"
26553
26559
  });
26554
26560
  if (opts.timeoutMs !== undefined) {
26555
26561
  timer = setTimeout(() => {
@@ -27021,41 +27027,70 @@ function aggregate(entries) {
27021
27027
  }
27022
27028
  return { ok: summary.failed === 0, summary, targets: entries };
27023
27029
  }
27024
- async function fanout(content, targets, dispatch) {
27025
- const entries = [];
27026
- for (const target of targets) {
27027
- const started = Date.now();
27028
- let entry;
27029
- try {
27030
- const result = await dispatch(target);
27031
- const durationMs = Date.now() - started;
27032
- entry = result.ok ? {
27033
- target,
27034
- ok: true,
27035
- status: "published",
27036
- ...result.url !== undefined ? { url: result.url } : {},
27037
- ...result.id !== undefined ? { id: result.id } : {},
27038
- ...result.metadata !== undefined ? { metadata: result.metadata } : {},
27039
- durationMs
27040
- } : {
27041
- target,
27042
- ok: false,
27043
- status: "failed",
27044
- error: result.error ?? result.message ?? "publish failed",
27045
- durationMs
27046
- };
27047
- } catch (err) {
27048
- entry = {
27049
- target,
27050
- ok: false,
27051
- status: "failed",
27052
- error: err instanceof Error ? err.message : String(err),
27053
- durationMs: Date.now() - started
27054
- };
27055
- }
27056
- entries.push(entry);
27030
+ async function dispatchOne(target, dispatch, timeoutMs) {
27031
+ const started = Date.now();
27032
+ try {
27033
+ const result = timeoutMs === undefined ? await dispatch(target) : await withTimeout(dispatch(target), timeoutMs, target);
27034
+ const durationMs = Date.now() - started;
27035
+ return result.ok ? {
27036
+ target,
27037
+ ok: true,
27038
+ status: "published",
27039
+ ...result.url !== undefined ? { url: result.url } : {},
27040
+ ...result.id !== undefined ? { id: result.id } : {},
27041
+ ...result.metadata !== undefined ? { metadata: result.metadata } : {},
27042
+ durationMs
27043
+ } : {
27044
+ target,
27045
+ ok: false,
27046
+ status: "failed",
27047
+ error: result.error ?? result.message ?? "publish failed",
27048
+ durationMs
27049
+ };
27050
+ } catch (err) {
27051
+ return {
27052
+ target,
27053
+ ok: false,
27054
+ status: "failed",
27055
+ error: err instanceof Error ? err.message : String(err),
27056
+ durationMs: Date.now() - started
27057
+ };
27057
27058
  }
27058
- return aggregate(entries);
27059
+ }
27060
+ function withTimeout(promise2, timeoutMs, target) {
27061
+ let timer;
27062
+ const budget = new Promise((_resolve, reject) => {
27063
+ timer = setTimeout(() => reject(new Error(`target '${target}' exceeded its ${timeoutMs}ms budget`)), timeoutMs);
27064
+ });
27065
+ return Promise.race([promise2, budget]).finally(() => clearTimeout(timer));
27066
+ }
27067
+ async function fanout(content, targets, dispatch, options = {}) {
27068
+ const outcomes = new Map;
27069
+ const runOne = async (target) => {
27070
+ outcomes.set(target, await dispatchOne(target, dispatch, options.timeoutMs));
27071
+ };
27072
+ if (options.mode === "serial") {
27073
+ for (const target of targets)
27074
+ await runOne(target);
27075
+ } else {
27076
+ const serial = new Set(options.serialTargets ?? []);
27077
+ const parallel = targets.filter((target) => !serial.has(target));
27078
+ const solo = targets.filter((target) => serial.has(target));
27079
+ const jobs = options.jobs !== undefined && options.jobs > 0 ? options.jobs : parallel.length;
27080
+ let cursor2 = 0;
27081
+ const worker = async () => {
27082
+ while (cursor2 < parallel.length) {
27083
+ const target = parallel[cursor2++];
27084
+ if (target === undefined)
27085
+ return;
27086
+ await runOne(target);
27087
+ }
27088
+ };
27089
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(jobs, parallel.length)) }, worker));
27090
+ for (const target of solo)
27091
+ await runOne(target);
27092
+ }
27093
+ return aggregate(targets.map((target) => outcomes.get(target)).filter((entry) => entry !== undefined));
27059
27094
  }
27060
27095
  async function readContent(fs2, path3) {
27061
27096
  if (!await fs2.exists(path3)) {
@@ -27139,10 +27174,14 @@ async function readAggregateResult(fs2, path3) {
27139
27174
  function parseArgs2(argv) {
27140
27175
  const targets = [];
27141
27176
  const pluginsPath = [];
27177
+ const serialTargets = [];
27142
27178
  let contentPath;
27143
27179
  let outPath;
27144
27180
  let retryFromPath;
27145
27181
  let pluginsRoot;
27182
+ let serial = false;
27183
+ let jobs;
27184
+ let targetTimeoutMs;
27146
27185
  for (let i = 0;i < argv.length; i++) {
27147
27186
  const arg = argv[i];
27148
27187
  if (arg === "--content") {
@@ -27193,6 +27232,34 @@ function parseArgs2(argv) {
27193
27232
  throw new Error("--plugins-path requires a directory path");
27194
27233
  }
27195
27234
  pluginsPath.push(value);
27235
+ } else if (arg === "--serial") {
27236
+ serial = true;
27237
+ } else if (arg === "--jobs") {
27238
+ const value = argv[++i];
27239
+ if (value === undefined) {
27240
+ throw new Error("--jobs requires a positive integer");
27241
+ }
27242
+ const parsed = Number(value);
27243
+ if (!Number.isInteger(parsed) || parsed < 1) {
27244
+ throw new Error(`--jobs requires a positive integer, got '${value}'`);
27245
+ }
27246
+ jobs = parsed;
27247
+ } else if (arg === "--serial-target") {
27248
+ const value = argv[++i];
27249
+ if (value === undefined) {
27250
+ throw new Error("--serial-target requires a plugin name");
27251
+ }
27252
+ serialTargets.push(value);
27253
+ } else if (arg === "--target-timeout") {
27254
+ const value = argv[++i];
27255
+ if (value === undefined) {
27256
+ throw new Error("--target-timeout requires milliseconds");
27257
+ }
27258
+ const parsed = Number(value);
27259
+ if (!Number.isInteger(parsed) || parsed < 1) {
27260
+ throw new Error(`--target-timeout requires a positive integer in milliseconds, got '${value}'`);
27261
+ }
27262
+ targetTimeoutMs = parsed;
27196
27263
  } else {
27197
27264
  throw new Error(`Unknown argument: ${arg}`);
27198
27265
  }
@@ -27212,7 +27279,11 @@ function parseArgs2(argv) {
27212
27279
  outPath,
27213
27280
  ...retryFromPath !== undefined ? { retryFromPath } : {},
27214
27281
  ...pluginsRoot !== undefined ? { pluginsRoot } : {},
27215
- ...pluginsPath.length > 0 ? { pluginsPath } : {}
27282
+ ...pluginsPath.length > 0 ? { pluginsPath } : {},
27283
+ ...serial ? { serial } : {},
27284
+ ...jobs !== undefined ? { jobs } : {},
27285
+ ...serialTargets.length > 0 ? { serialTargets } : {},
27286
+ ...targetTimeoutMs !== undefined ? { targetTimeoutMs } : {}
27216
27287
  };
27217
27288
  }
27218
27289
  async function pluginDispatch(target, contentPath, discovery) {
@@ -27315,7 +27386,12 @@ async function runFanout(argv, deps = {}) {
27315
27386
  if (deps.dispatch === undefined && !validateTargets(targets, discovery)) {
27316
27387
  return EXIT_INPUT2;
27317
27388
  }
27318
- const fresh = await fanout(content, targets, dispatch);
27389
+ const fresh = await fanout(content, targets, dispatch, {
27390
+ ...opts.serial === true ? { mode: "serial" } : {},
27391
+ ...opts.jobs !== undefined ? { jobs: opts.jobs } : {},
27392
+ ...opts.serialTargets !== undefined ? { serialTargets: opts.serialTargets } : {},
27393
+ ...opts.targetTimeoutMs !== undefined ? { timeoutMs: opts.targetTimeoutMs } : {}
27394
+ });
27319
27395
  const result = prior !== undefined ? aggregate([...skipped, ...retainedFailed, ...fresh.targets]) : fresh;
27320
27396
  try {
27321
27397
  await fs2.writeFile(resolve4(opts.outPath), `${JSON.stringify(result, null, 2)}
@@ -27399,7 +27475,7 @@ function withFaninOptions(cmd) {
27399
27475
  return cmd.option("--in <path>", "Input Doc[] JSON file (repeatable)", collectRepeatable, []).option("--source <plugin>:<file>", "Ingestion source to spawn concurrently: plugin name and its input file (repeatable)", collectRepeatable, []).option("--source-timeout <ms>", "Per-source timeout in milliseconds (default 1800000)").option("--source-timeout-override <plugin>:<ms>", "Per-source timeout override for one named --source plugin, e.g. horizon-ingest:2700000 (repeatable; wins over --source-timeout/default)", collectRepeatable, []).requiredOption("--out <path>", "Output merged Doc[] JSON file path").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
27400
27476
  }
27401
27477
  function withFanoutOptions(cmd) {
27402
- return cmd.requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectRepeatable, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
27478
+ return cmd.requiredOption("--content <path>", "Source ContentSchema JSON file path").option("--target <plugin>", "Target publish plugin name (repeatable)", collectRepeatable, []).requiredOption("--out <path>", "Aggregate Result JSON file path").option("--retry-from <path>", "Prior Result JSON file to retry only previously failed targets").option("--serial", "Dispatch the parallel batch one target at a time (pre-2026-10-03 behaviour)").option("--jobs <n>", "Concurrent-start bound for the parallel batch (default: every target at once)").option("--serial-target <plugin>", "Target kept out of the parallel batch and run alone afterwards, e.g. a headed browser channel or a shared checkout (repeatable)", collectRepeatable, []).option("--target-timeout <ms>", "Per-target wall-clock budget; an overrun is recorded failed and siblings continue").option("--plugins-root <dir>", "Exclusive root containing plugins (isolation/tests only)").option("--plugins-path <dir>", "Extra plugin root to union with defaults (repeatable)", collectRepeatable, []);
27403
27479
  }
27404
27480
  function registerExecutorCommand(program2) {
27405
27481
  const executor = program2.command("executor").description("Run knowledge-kit plugin steps (invoke, fan-in, fan-out)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22035,17 +22035,15 @@ ${metaLine ? `${metaLine}
22035
22035
 
22036
22036
  ` : ""}${stripScaffold(doc2.body ?? "").trim()}`;
22037
22037
  }
22038
- function episodeTitle(docs, date6) {
22039
- const lead = docs[0]?.title?.trim() ?? "";
22040
- const theme = lead !== "" ? lead : "\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5";
22041
- return `AI \u4E71\u7096\uFF5C${theme} \xB7 ${date6}`;
22038
+ function episodeTitle(date6) {
22039
+ return `AI \u4E71\u7096 \xB7 ${date6}`;
22042
22040
  }
22043
22041
  function renderDailyArticle(docs, date6) {
22044
22042
  const items = docs.map(renderDoc).join(`
22045
22043
 
22046
22044
  `);
22047
22045
  const intro = docs.length > 0 ? `\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u5171 ${docs.length} \u6761\u5185\u5BB9\uFF0C\u5747\u9644\u53EF\u70B9\u51FB\u6765\u6E90\u94FE\u63A5\u3002` : "\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002";
22048
- return `# ${episodeTitle(docs, date6)}
22046
+ return `# ${episodeTitle(date6)}
22049
22047
 
22050
22048
  ${intro}
22051
22049
 
@@ -22061,8 +22059,24 @@ function tagsFor(docs) {
22061
22059
  return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
22062
22060
  }
22063
22061
  var LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
22062
+ var NOTE_EXCLUDED_RE = /\u672A\u5199\u5165[\uFF1A:](.*)/g;
22063
+ var HEX_ID_RE = /(?:[a-zA-Z0-9_-]+:)?[0-9a-f]{16}/g;
22064
+ function excludedIdsFromNote(body) {
22065
+ const ids = new Set;
22066
+ if (!body)
22067
+ return ids;
22068
+ for (const note of body.matchAll(NOTE_EXCLUDED_RE)) {
22069
+ for (const id of (note[1] ?? "").matchAll(HEX_ID_RE)) {
22070
+ ids.add(id[0]);
22071
+ const colon = id[0].indexOf(":");
22072
+ if (colon !== -1)
22073
+ ids.add(id[0].slice(colon + 1));
22074
+ }
22075
+ }
22076
+ return ids;
22077
+ }
22064
22078
  function docsToContent(docs, date6, bodyOverride) {
22065
- const title = episodeTitle(docs, date6);
22079
+ const title = episodeTitle(date6);
22066
22080
  let body = renderDailyArticle(docs, date6);
22067
22081
  if (bodyOverride) {
22068
22082
  const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), "");
@@ -22096,7 +22110,9 @@ async function processGeneratorIO(inputPath, outputPath) {
22096
22110
  if (bodyOverride.trim() === "")
22097
22111
  bodyOverride = undefined;
22098
22112
  }
22099
- const content = docsToContent(docs, date6, bodyOverride);
22113
+ const excluded = excludedIdsFromNote(bodyOverride);
22114
+ const kept = excluded.size === 0 ? docs : docs.filter((d) => !excluded.has(d.id) && !(d.id.includes(":") && excluded.has(d.id.slice(d.id.indexOf(":") + 1))));
22115
+ const content = docsToContent(kept, date6, bodyOverride);
22100
22116
  const fs = createNodeFileSystem();
22101
22117
  const outDir = dirname2(outputPath);
22102
22118
  if (outDir && outDir !== ".")
@@ -22132,6 +22148,7 @@ export {
22132
22148
  renderDailyArticle,
22133
22149
  processGeneratorIO,
22134
22150
  main2 as main,
22151
+ excludedIdsFromNote,
22135
22152
  episodeTitle,
22136
22153
  docsToContent
22137
22154
  };
@@ -103,16 +103,14 @@ function renderDoc(doc: Doc, index: number): string {
103
103
  }
104
104
 
105
105
  /**
106
- * Episode title for the 「AI 乱炖」 column (task 0189 R1/R4): `AI 乱炖|<本期实际技术主题> · YYYY-MM-DD`.
107
- * The deterministic proxy for 本期实际技术主题 is the lead plan item's title — the same doc the
108
- * cover brief and XHS cards already treat as the episode's lead story. Pure function so the
109
- * agent-written and template article paths, ID3, RSS and show notes all derive one identical
110
- * title; never re-introduces the retired news-bulletin positioning (task 0189 R4).
106
+ * Episode title for the 「AI 乱炖」 column: fixed `AI 乱炖 · YYYY-MM-DD` (2026-10-02 operator
107
+ * simplification, superseding the 0189 R1/R4 `AI 乱炖|<lead item> · date` pattern). The date
108
+ * alone identifies the episode; the day's stories live in the show notes, cards and post body.
109
+ * Pure function so the agent-written and template article paths, ID3, RSS and show notes all
110
+ * derive one identical title.
111
111
  */
112
- export function episodeTitle(docs: Doc[], date: string): string {
113
- const lead = docs[0]?.title?.trim() ?? '';
114
- const theme = lead !== '' ? lead : '工具、论文与实践';
115
- return `AI 乱炖|${theme} · ${date}`;
112
+ export function episodeTitle(date: string): string {
113
+ return `AI 乱炖 · ${date}`;
116
114
  }
117
115
 
118
116
  /** Render the article markdown for a merged Doc[] (pure — exported for tests). */
@@ -122,7 +120,7 @@ export function renderDailyArticle(docs: Doc[], date: string): string {
122
120
  docs.length > 0
123
121
  ? `「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期共 ${docs.length} 条内容,均附可点击来源链接。`
124
122
  : '「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期没有足够扎实的可聊内容。';
125
- return `# ${episodeTitle(docs, date)}\n\n${intro}\n\n${items}\n`;
123
+ return `# ${episodeTitle(date)}\n\n${intro}\n\n${items}\n`;
126
124
  }
127
125
 
128
126
  /** Single-source daily tag: stamped on every Content this generator emits (task 0119). */
@@ -140,9 +138,27 @@ function tagsFor(docs: Doc[]): string[] {
140
138
  /** A leading H1 line — agent bodies used to carry their own title; the canonical title is computed (task 0189 R4). */
141
139
  const LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
142
140
 
141
+ /** ids the article-write note marks as dropped on the topic boundary, e.g. 「…未写入:<id>(原因)、<id>(原因)」. */
142
+ const NOTE_EXCLUDED_RE = /未写入[::](.*)/g;
143
+ const HEX_ID_RE = /(?:[a-zA-Z0-9_-]+:)?[0-9a-f]{16}/g;
144
+
145
+ /** Parse the boundary-note ids out of an article body so references/metadata never cite excluded docs (judge finding 2026-10-02). */
146
+ export function excludedIdsFromNote(body: string | undefined): Set<string> {
147
+ const ids = new Set<string>();
148
+ if (!body) return ids;
149
+ for (const note of body.matchAll(NOTE_EXCLUDED_RE)) {
150
+ for (const id of (note[1] ?? '').matchAll(HEX_ID_RE)) {
151
+ ids.add(id[0]);
152
+ const colon = id[0].indexOf(':');
153
+ if (colon !== -1) ids.add(id[0].slice(colon + 1));
154
+ }
155
+ }
156
+ return ids;
157
+ }
158
+
143
159
  /** Build the article Content from a merged Doc[]; `bodyOverride` replaces the mechanical render (agent-written prose), still scaffold-stripped as a safety net. */
144
160
  export function docsToContent(docs: Doc[], date: string, bodyOverride?: string): Content {
145
- const title = episodeTitle(docs, date);
161
+ const title = episodeTitle(date);
146
162
  let body = renderDailyArticle(docs, date);
147
163
  if (bodyOverride) {
148
164
  // The canonical episode title is computed from the lead item; a leading H1 the agent may
@@ -174,7 +190,15 @@ export async function processGeneratorIO(inputPath: string, outputPath: string):
174
190
  bodyOverride = await Bun.file(bodyFile).text();
175
191
  if (bodyOverride.trim() === '') bodyOverride = undefined;
176
192
  }
177
- const content = docsToContent(docs, date, bodyOverride);
193
+ const excluded = excludedIdsFromNote(bodyOverride);
194
+ const kept =
195
+ excluded.size === 0
196
+ ? docs
197
+ : docs.filter(
198
+ (d) =>
199
+ !excluded.has(d.id) && !(d.id.includes(':') && excluded.has(d.id.slice(d.id.indexOf(':') + 1))),
200
+ );
201
+ const content = docsToContent(kept, date, bodyOverride);
178
202
  const fs = createNodeFileSystem();
179
203
  const outDir = dirname(outputPath);
180
204
  if (outDir && outDir !== '.') await fs.ensureDir(outDir);
@@ -21994,7 +21994,7 @@ function normalizeBroadcastText(raw, isZh) {
21994
21994
  text = text.replace(/\s+/g, " ");
21995
21995
  if (isZh) {
21996
21996
  text = text.replace(/[''\u2018\u2019]/g, "");
21997
- text = text.replace(/[:\uFF1A]/g, "\uFF0C");
21997
+ text = text.replace(/[:\uFF1A](?!\/\/)/g, "\uFF0C");
21998
21998
  text = text.replace(/[;\uFF1B]/g, "\u3002");
21999
21999
  text = text.replace(/--+/g, "\uFF0C");
22000
22000
  text = text.replace(/\.{2,}/g, "\u3002");
@@ -22344,11 +22344,16 @@ var ScriptPlanItemSchema = exports_external.object({
22344
22344
  lead: exports_external.string().min(1),
22345
22345
  insight: exports_external.string().min(1)
22346
22346
  });
22347
+ var ScriptPlanExclusionSchema = exports_external.object({
22348
+ id: exports_external.string().min(1),
22349
+ reason: exports_external.string().min(1)
22350
+ });
22347
22351
  var ScriptPlanSchema = exports_external.object({
22348
22352
  version: exports_external.literal(1),
22349
22353
  intro: exports_external.string().min(1),
22350
22354
  outro: exports_external.string().min(1),
22351
- items: exports_external.array(ScriptPlanItemSchema)
22355
+ items: exports_external.array(ScriptPlanItemSchema),
22356
+ excluded: exports_external.array(ScriptPlanExclusionSchema).optional()
22352
22357
  });
22353
22358
  var StyleContractSchema = exports_external.object({
22354
22359
  caps: exports_external.object({
@@ -22386,8 +22391,15 @@ function validateScriptPlan(plan, docs, style) {
22386
22391
  errors5.push({ itemId: item.id, rule: "unknown-id" });
22387
22392
  }
22388
22393
  const plannedIds = new Set(valid.items.map((item) => item.id));
22394
+ const excludedIds = new Set((valid.excluded ?? []).map((ex) => ex.id));
22395
+ for (const ex of valid.excluded ?? []) {
22396
+ if (!docIds.has(ex.id))
22397
+ errors5.push({ itemId: ex.id, rule: "unknown-id" });
22398
+ if (plannedIds.has(ex.id))
22399
+ errors5.push({ itemId: ex.id, rule: "conflict-id" });
22400
+ }
22389
22401
  for (const doc2 of docs) {
22390
- if (!plannedIds.has(doc2.id))
22402
+ if (!plannedIds.has(doc2.id) && !excludedIds.has(doc2.id))
22391
22403
  errors5.push({ itemId: doc2.id, rule: "missing-item" });
22392
22404
  }
22393
22405
  if (valid.intro.length > style.caps.intro)
@@ -22493,6 +22505,11 @@ async function processGeneratorIO(inputPath, outputPath, options) {
22493
22505
  }
22494
22506
  const isZh = (options?.language || "zh").toLowerCase() === "zh";
22495
22507
  const planOutcome = isZh ? await resolveScriptPlan(fs, docs) : { scriptSource: "template", scriptPlanErrors: [] };
22508
+ const excludedIds = new Set(planOutcome.scriptPlan?.excluded?.map((ex) => ex.id) ?? []);
22509
+ if (excludedIds.size > 0) {
22510
+ echoError(`dailynews-gen: topic boundary excludes ${excludedIds.size} doc(s): ${[...excludedIds].join(", ")}`);
22511
+ docs = docs.filter((doc2) => !excludedIds.has(doc2.id));
22512
+ }
22496
22513
  const date6 = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
22497
22514
  const script = buildNewsVoiceScript(docs, { ...options, date: date6, scriptPlan: planOutcome.scriptPlan });
22498
22515
  const yamlBody = formatVoiceScriptToYaml(script);
@@ -127,6 +127,13 @@ export async function processGeneratorIO(
127
127
  const planOutcome = isZh
128
128
  ? await resolveScriptPlan(fs, docs)
129
129
  : { scriptSource: 'template' as const, scriptPlanErrors: [] as ScriptPlanError[] };
130
+ // Topic boundary (task 0189): docs the plan declares excluded are not spoken, not counted,
131
+ // and not referenced — title, storyCount, and references must match what is actually said.
132
+ const excludedIds = new Set(planOutcome.scriptPlan?.excluded?.map((ex) => ex.id) ?? []);
133
+ if (excludedIds.size > 0) {
134
+ echoError(`dailynews-gen: topic boundary excludes ${excludedIds.size} doc(s): ${[...excludedIds].join(', ')}`);
135
+ docs = docs.filter((doc) => !excludedIds.has(doc.id));
136
+ }
130
137
  // Explicit option wins; else DAILYNEWS_DATE (workflow backfill/retry); else the builder's now.
131
138
  const date = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
132
139
  const script = buildNewsVoiceScript(docs, { ...options, date, scriptPlan: planOutcome.scriptPlan });
@@ -119,8 +119,10 @@ export function normalizeBroadcastText(raw: string, isZh: boolean): string {
119
119
 
120
120
  if (isZh) {
121
121
  text = text.replace(/[''‘’]/g, '');
122
- // Replace English punctuation with Chinese pauses
123
- text = text.replace(/[::]/g, ',');
122
+ // Replace English punctuation with Chinese pauses. A URL scheme's colon (`https://`,
123
+ // `wss://`) is the exception: spoken as `,//` the URL is mangled, and the content gate
124
+ // (`validate-voicescript.ts` CONTENT_URL_MANGLE_RE) rejects the whole run over it.
125
+ text = text.replace(/[::](?!\/\/)/g, ',');
124
126
  text = text.replace(/[;;]/g, '。');
125
127
  text = text.replace(/--+/g, ',');
126
128
  text = text.replace(/\.{2,}/g, '。');
@@ -12,6 +12,12 @@ export interface ScriptPlan {
12
12
  intro: string;
13
13
  outro: string;
14
14
  items: { id: string; lead: string; insight: string }[];
15
+ /**
16
+ * Docs deliberately not spoken (topic-boundary exclusions, task 0189). An id here is
17
+ * exempt from `missing-item`; listing an id in both `items` and `excluded` is a
18
+ * `conflict-id` error, and an unknown id is `unknown-id` like any plan item.
19
+ */
20
+ excluded?: { id: string; reason: string }[];
15
21
  }
16
22
 
17
23
  /**
@@ -41,11 +47,17 @@ const ScriptPlanItemSchema = z.object({
41
47
  insight: z.string().min(1),
42
48
  });
43
49
 
50
+ const ScriptPlanExclusionSchema = z.object({
51
+ id: z.string().min(1),
52
+ reason: z.string().min(1),
53
+ });
54
+
44
55
  const ScriptPlanSchema = z.object({
45
56
  version: z.literal(1),
46
57
  intro: z.string().min(1),
47
58
  outro: z.string().min(1),
48
59
  items: z.array(ScriptPlanItemSchema),
60
+ excluded: z.array(ScriptPlanExclusionSchema).optional(),
49
61
  });
50
62
 
51
63
  const StyleContractSchema = z.object({
@@ -86,8 +98,9 @@ export function parseStyleContract(markdown: string): StyleContract {
86
98
  * one warn line names every violation, so a rejected plan is diagnosable in one read:
87
99
  *
88
100
  * - `schema`: wrong shape or version (`itemId: episode`)
89
- * - `unknown-id`: a plan item id that is not a doc
90
- * - `missing-item`: a doc with no plan item
101
+ * - `unknown-id`: a plan item (or exclusion) id that is not a doc
102
+ * - `missing-item`: a doc with no plan item and no exclusion
103
+ * - `conflict-id`: an id listed in both `items` and `excluded`
91
104
  * - `cap:<field>`: a field over its cap (`intro`/`outro` report `itemId: episode`)
92
105
  * - `banned:<phrase>`: a field containing a banned phrase
93
106
  * - `duplicate-insight`: the same insight text on two items in one episode
@@ -107,8 +120,13 @@ export function validateScriptPlan(plan: unknown, docs: Doc[], style: StyleContr
107
120
  if (!docIds.has(item.id)) errors.push({ itemId: item.id, rule: 'unknown-id' });
108
121
  }
109
122
  const plannedIds = new Set(valid.items.map((item) => item.id));
123
+ const excludedIds = new Set((valid.excluded ?? []).map((ex) => ex.id));
124
+ for (const ex of valid.excluded ?? []) {
125
+ if (!docIds.has(ex.id)) errors.push({ itemId: ex.id, rule: 'unknown-id' });
126
+ if (plannedIds.has(ex.id)) errors.push({ itemId: ex.id, rule: 'conflict-id' });
127
+ }
110
128
  for (const doc of docs) {
111
- if (!plannedIds.has(doc.id)) errors.push({ itemId: doc.id, rule: 'missing-item' });
129
+ if (!plannedIds.has(doc.id) && !excludedIds.has(doc.id)) errors.push({ itemId: doc.id, rule: 'missing-item' });
112
130
  }
113
131
 
114
132
  if (valid.intro.length > style.caps.intro) errors.push({ itemId: 'episode', rule: 'cap:intro' });
@@ -22118,8 +22118,18 @@ function passesGates(doc2, planDate) {
22118
22118
  return true;
22119
22119
  return published >= Date.parse(planDate) - FRESHNESS_DAYS * 86400000;
22120
22120
  }
22121
+ var SCAFFOLD_HEADING_RE = /^\s*\*\*\s*[\u300C\u300E][^\u300D\u300F\n]{1,80}[\u300D\u300F]\s*\*\*\s*[\uFF1A:]?(?:\s+|$)/;
22121
22122
  function cleanBody(body) {
22122
- return body.replace(/\u2B50\uFE0F\s*[\d.]+\s*\/\s*10/g, "").replace(/^#{1,6}\s*\[[^\]]*\]\([^)]*\)\s*$/gm, "").replace(/\n{3,}/g, `
22123
+ const kept = [];
22124
+ for (const line of body.split(`
22125
+ `)) {
22126
+ const stripped = line.replace(SCAFFOLD_HEADING_RE, "");
22127
+ if (stripped.trim() === "" && line.trim() !== "")
22128
+ continue;
22129
+ kept.push(stripped);
22130
+ }
22131
+ return kept.join(`
22132
+ `).replace(/\u2B50\uFE0F\s*[\d.]+\s*\/\s*10/g, "").replace(/^#{1,6}\s*\[[^\]]*\]\([^)]*\)\s*$/gm, "").replace(/\n{3,}/g, `
22123
22133
 
22124
22134
  `).trim();
22125
22135
  }
@@ -22430,7 +22440,8 @@ var DEFAULT_FILTER_CONFIG = {
22430
22440
  minQuality: 2,
22431
22441
  minImportance: 3,
22432
22442
  minUrgency: 2,
22433
- minImpact: 1
22443
+ minImpact: 1,
22444
+ minEpisodeItems: 8
22434
22445
  };
22435
22446
  function parseFilterThreshold(raw, name, fallback) {
22436
22447
  if (raw === undefined || raw.trim() === "")
@@ -22443,6 +22454,17 @@ function parseFilterThreshold(raw, name, fallback) {
22443
22454
  throw new Error(`${name}=${parsed} is outside 0\u20135 (fail loud, no clamping)`);
22444
22455
  return parsed;
22445
22456
  }
22457
+ function parseFloorCount(raw, name, fallback) {
22458
+ if (raw === undefined || raw.trim() === "")
22459
+ return fallback;
22460
+ const value = raw.trim();
22461
+ if (!/^\d+$/.test(value))
22462
+ throw new Error(`${name}='${raw}' is not a positive integer (fail loud, no clamping)`);
22463
+ const parsed = Number.parseInt(value, 10);
22464
+ if (parsed < 1)
22465
+ throw new Error(`${name}=${parsed} must be >= 1 (a 0 floor disables the gate \u2014 drop the env instead)`);
22466
+ return parsed;
22467
+ }
22446
22468
  function parseFilterCategories(raw) {
22447
22469
  if (raw === undefined || raw.trim() === "")
22448
22470
  return [...DEFAULT_FILTER_CONFIG.categories];
@@ -22460,7 +22482,8 @@ function filterConfigFromEnv(env2 = process.env) {
22460
22482
  minQuality: parseFilterThreshold(env2.QC_MIN_QUALITY, "QC_MIN_QUALITY", DEFAULT_FILTER_CONFIG.minQuality),
22461
22483
  minImportance: parseFilterThreshold(env2.QC_MIN_IMPORTANCE, "QC_MIN_IMPORTANCE", DEFAULT_FILTER_CONFIG.minImportance),
22462
22484
  minUrgency: parseFilterThreshold(env2.QC_MIN_URGENCY, "QC_MIN_URGENCY", DEFAULT_FILTER_CONFIG.minUrgency),
22463
- minImpact: parseFilterThreshold(env2.QC_MIN_IMPACT, "QC_MIN_IMPACT", DEFAULT_FILTER_CONFIG.minImpact)
22485
+ minImpact: parseFilterThreshold(env2.QC_MIN_IMPACT, "QC_MIN_IMPACT", DEFAULT_FILTER_CONFIG.minImpact),
22486
+ minEpisodeItems: parseFloorCount(env2.QC_MIN_EPISODE_ITEMS, "QC_MIN_EPISODE_ITEMS", DEFAULT_FILTER_CONFIG.minEpisodeItems)
22464
22487
  };
22465
22488
  }
22466
22489
  function requireAnnotation(doc2) {
@@ -22505,6 +22528,39 @@ function filterAnnotated(docs, config3) {
22505
22528
  }
22506
22529
  return { kept, rejected };
22507
22530
  }
22531
+ function importanceOf(doc2) {
22532
+ const scores = asRecord(asRecord(doc2.metadata).scores);
22533
+ return typeof scores.importance === "number" ? scores.importance : 0;
22534
+ }
22535
+ function planScoreOf(doc2) {
22536
+ const value = asRecord(doc2.metadata).planScore;
22537
+ return typeof value === "number" ? value : 0;
22538
+ }
22539
+ function backfillToFloor(kept, rejected, config3) {
22540
+ if (kept.length >= config3.minEpisodeItems)
22541
+ return { kept, rejected, admitted: [] };
22542
+ const eligible = rejected.map((entry, index) => ({ entry, index })).filter(({ entry }) => entry.reasons.length > 0 && entry.reasons.every((r) => r.startsWith("importance:")));
22543
+ eligible.sort((a, b) => {
22544
+ const byImportance = importanceOf(b.entry.doc) - importanceOf(a.entry.doc);
22545
+ if (byImportance !== 0)
22546
+ return byImportance;
22547
+ const byPlanScore = planScoreOf(b.entry.doc) - planScoreOf(a.entry.doc);
22548
+ if (byPlanScore !== 0)
22549
+ return byPlanScore;
22550
+ return a.index - b.index;
22551
+ });
22552
+ const taken = eligible.slice(0, config3.minEpisodeItems - kept.length);
22553
+ const takenIds = new Set(taken.map(({ entry }) => entry.doc.id));
22554
+ const admitted = taken.map(({ entry }) => ({
22555
+ ...entry.doc,
22556
+ metadata: { ...asRecord(entry.doc.metadata), backfilled: true }
22557
+ }));
22558
+ return {
22559
+ kept: [...kept, ...admitted],
22560
+ rejected: rejected.filter((entry) => !takenIds.has(entry.doc.id)),
22561
+ admitted
22562
+ };
22563
+ }
22508
22564
  function rejectedOutPath(outputPath) {
22509
22565
  return outputPath.endsWith(".json") ? `${outputPath.slice(0, -".json".length)}.rejected.json` : `${outputPath}.rejected.json`;
22510
22566
  }
@@ -22530,9 +22586,13 @@ async function processFilterIO(inputPath, outputPath) {
22530
22586
  const config3 = filterConfigFromEnv();
22531
22587
  const docs = DocListSchema.parse(await readJsonFile(inputPath));
22532
22588
  const { kept, rejected } = filterAnnotated(docs, config3);
22533
- const content = planContent(kept, resolvePlanDate());
22589
+ const filled = backfillToFloor(kept, rejected, config3);
22590
+ if (filled.admitted.length > 0) {
22591
+ echoError(`episode floor backfill: admitted ${filled.admitted.length} importance-only reject(s) to reach ${config3.minEpisodeItems} items (${filled.admitted.map((d) => d.id).join(", ")})`);
22592
+ }
22593
+ const content = planContent(filled.kept, resolvePlanDate());
22534
22594
  await atomicWrite(outputPath, content);
22535
- await atomicWrite(rejectedOutPath(outputPath), { rejected, config: config3, total: docs.length });
22595
+ await atomicWrite(rejectedOutPath(outputPath), { rejected: filled.rejected, config: config3, total: docs.length });
22536
22596
  return content;
22537
22597
  }
22538
22598
  async function main2() {
@@ -22581,5 +22641,6 @@ export {
22581
22641
  docsToContent,
22582
22642
  cleanBody,
22583
22643
  classifyCategory,
22644
+ backfillToFloor,
22584
22645
  DEFAULT_FILTER_CONFIG
22585
22646
  };