@gobing-ai/knowledge-kit 0.0.29 → 0.0.30

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 (72) 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 +19 -2
  7. package/plugins/generations/dailynews-gen/src/index.ts +7 -0
  8. package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
  9. package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
  10. package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
  11. package/plugins/generations/image-gen/dist/index.js +122 -20
  12. package/plugins/generations/image-gen/src/bytes.ts +106 -18
  13. package/plugins/generations/image-gen/src/index.ts +46 -6
  14. package/plugins/generations/image-gen/src/job.ts +6 -0
  15. package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
  16. package/plugins/generations/image-gen/src/providers/types.ts +12 -1
  17. package/plugins/kk/plugin.json +1 -1
  18. package/plugins/kk/scripts/daily-voice-style.zh.md +5 -5
  19. package/plugins/kk/scripts/itc-stages.ts +21 -3
  20. package/plugins/kk/scripts/kk-workflow-stages.ts +199 -7
  21. package/plugins/kk/scripts/topic-boundary.json +3 -12
  22. package/plugins/kk/skills/content-judge/SKILL.md +30 -0
  23. package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
  24. package/plugins/kk/skills/publish/SKILL.md +1 -1
  25. package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
  26. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +39 -14
  27. package/plugins/kk/workflows/kk-itc.yaml +21 -8
  28. package/plugins/publishings/xhs-pub/dist/index.js +1 -1
  29. package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
  30. package/plugins/publishings/xhs-pub/src/index.ts +1 -1
  31. package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
  32. package/plugins/sp/lib/env.ts +0 -46
  33. package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
  34. package/plugins/sp/scripts/batch-preflight.mjs +0 -346
  35. package/plugins/sp/scripts/batch-preflight.ts +0 -459
  36. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
  37. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
  38. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  39. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
  40. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
  41. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
  42. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
  43. package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
  44. package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
  45. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
  46. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
  47. package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
  48. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
  49. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
  50. package/plugins/sp/scripts/idea-handoff.mjs +0 -22
  51. package/plugins/sp/scripts/idea-handoff.ts +0 -44
  52. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
  53. package/plugins/sp/scripts/inline-run-setup.ts +0 -722
  54. package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
  55. package/plugins/sp/scripts/pr-reviewing.ts +0 -925
  56. package/plugins/sp/scripts/quality-gate.mjs +0 -179
  57. package/plugins/sp/scripts/quality-gate.ts +0 -217
  58. package/plugins/sp/scripts/script-contract-check.ts +0 -319
  59. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  60. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
  61. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
  62. package/plugins/sp/scripts/task-size-precheck.ts +0 -175
  63. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  64. package/plugins/sp/scripts/validate-commands.ts +0 -689
  65. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  66. package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
  68. package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
  71. package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
  72. 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.30",
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.30",
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);
@@ -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 });
@@ -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' });
@@ -22430,7 +22430,8 @@ var DEFAULT_FILTER_CONFIG = {
22430
22430
  minQuality: 2,
22431
22431
  minImportance: 3,
22432
22432
  minUrgency: 2,
22433
- minImpact: 1
22433
+ minImpact: 1,
22434
+ minEpisodeItems: 8
22434
22435
  };
22435
22436
  function parseFilterThreshold(raw, name, fallback) {
22436
22437
  if (raw === undefined || raw.trim() === "")
@@ -22443,6 +22444,17 @@ function parseFilterThreshold(raw, name, fallback) {
22443
22444
  throw new Error(`${name}=${parsed} is outside 0\u20135 (fail loud, no clamping)`);
22444
22445
  return parsed;
22445
22446
  }
22447
+ function parseFloorCount(raw, name, fallback) {
22448
+ if (raw === undefined || raw.trim() === "")
22449
+ return fallback;
22450
+ const value = raw.trim();
22451
+ if (!/^\d+$/.test(value))
22452
+ throw new Error(`${name}='${raw}' is not a positive integer (fail loud, no clamping)`);
22453
+ const parsed = Number.parseInt(value, 10);
22454
+ if (parsed < 1)
22455
+ throw new Error(`${name}=${parsed} must be >= 1 (a 0 floor disables the gate \u2014 drop the env instead)`);
22456
+ return parsed;
22457
+ }
22446
22458
  function parseFilterCategories(raw) {
22447
22459
  if (raw === undefined || raw.trim() === "")
22448
22460
  return [...DEFAULT_FILTER_CONFIG.categories];
@@ -22460,7 +22472,8 @@ function filterConfigFromEnv(env2 = process.env) {
22460
22472
  minQuality: parseFilterThreshold(env2.QC_MIN_QUALITY, "QC_MIN_QUALITY", DEFAULT_FILTER_CONFIG.minQuality),
22461
22473
  minImportance: parseFilterThreshold(env2.QC_MIN_IMPORTANCE, "QC_MIN_IMPORTANCE", DEFAULT_FILTER_CONFIG.minImportance),
22462
22474
  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)
22475
+ minImpact: parseFilterThreshold(env2.QC_MIN_IMPACT, "QC_MIN_IMPACT", DEFAULT_FILTER_CONFIG.minImpact),
22476
+ minEpisodeItems: parseFloorCount(env2.QC_MIN_EPISODE_ITEMS, "QC_MIN_EPISODE_ITEMS", DEFAULT_FILTER_CONFIG.minEpisodeItems)
22464
22477
  };
22465
22478
  }
22466
22479
  function requireAnnotation(doc2) {
@@ -22505,6 +22518,39 @@ function filterAnnotated(docs, config3) {
22505
22518
  }
22506
22519
  return { kept, rejected };
22507
22520
  }
22521
+ function importanceOf(doc2) {
22522
+ const scores = asRecord(asRecord(doc2.metadata).scores);
22523
+ return typeof scores.importance === "number" ? scores.importance : 0;
22524
+ }
22525
+ function planScoreOf(doc2) {
22526
+ const value = asRecord(doc2.metadata).planScore;
22527
+ return typeof value === "number" ? value : 0;
22528
+ }
22529
+ function backfillToFloor(kept, rejected, config3) {
22530
+ if (kept.length >= config3.minEpisodeItems)
22531
+ return { kept, rejected, admitted: [] };
22532
+ const eligible = rejected.map((entry, index) => ({ entry, index })).filter(({ entry }) => entry.reasons.length > 0 && entry.reasons.every((r) => r.startsWith("importance:")));
22533
+ eligible.sort((a, b) => {
22534
+ const byImportance = importanceOf(b.entry.doc) - importanceOf(a.entry.doc);
22535
+ if (byImportance !== 0)
22536
+ return byImportance;
22537
+ const byPlanScore = planScoreOf(b.entry.doc) - planScoreOf(a.entry.doc);
22538
+ if (byPlanScore !== 0)
22539
+ return byPlanScore;
22540
+ return a.index - b.index;
22541
+ });
22542
+ const taken = eligible.slice(0, config3.minEpisodeItems - kept.length);
22543
+ const takenIds = new Set(taken.map(({ entry }) => entry.doc.id));
22544
+ const admitted = taken.map(({ entry }) => ({
22545
+ ...entry.doc,
22546
+ metadata: { ...asRecord(entry.doc.metadata), backfilled: true }
22547
+ }));
22548
+ return {
22549
+ kept: [...kept, ...admitted],
22550
+ rejected: rejected.filter((entry) => !takenIds.has(entry.doc.id)),
22551
+ admitted
22552
+ };
22553
+ }
22508
22554
  function rejectedOutPath(outputPath) {
22509
22555
  return outputPath.endsWith(".json") ? `${outputPath.slice(0, -".json".length)}.rejected.json` : `${outputPath}.rejected.json`;
22510
22556
  }
@@ -22530,9 +22576,13 @@ async function processFilterIO(inputPath, outputPath) {
22530
22576
  const config3 = filterConfigFromEnv();
22531
22577
  const docs = DocListSchema.parse(await readJsonFile(inputPath));
22532
22578
  const { kept, rejected } = filterAnnotated(docs, config3);
22533
- const content = planContent(kept, resolvePlanDate());
22579
+ const filled = backfillToFloor(kept, rejected, config3);
22580
+ if (filled.admitted.length > 0) {
22581
+ echoError(`episode floor backfill: admitted ${filled.admitted.length} importance-only reject(s) to reach ${config3.minEpisodeItems} items (${filled.admitted.map((d) => d.id).join(", ")})`);
22582
+ }
22583
+ const content = planContent(filled.kept, resolvePlanDate());
22534
22584
  await atomicWrite(outputPath, content);
22535
- await atomicWrite(rejectedOutPath(outputPath), { rejected, config: config3, total: docs.length });
22585
+ await atomicWrite(rejectedOutPath(outputPath), { rejected: filled.rejected, config: config3, total: docs.length });
22536
22586
  return content;
22537
22587
  }
22538
22588
  async function main2() {
@@ -22581,5 +22631,6 @@ export {
22581
22631
  docsToContent,
22582
22632
  cleanBody,
22583
22633
  classifyCategory,
22634
+ backfillToFloor,
22584
22635
  DEFAULT_FILTER_CONFIG
22585
22636
  };