@gobing-ai/knowledge-kit 0.0.28 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +114 -38
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +45 -5
- package/plugins/generations/daily-article-gen/src/index.ts +59 -4
- package/plugins/generations/dailynews-gen/dist/index.js +46 -9
- package/plugins/generations/dailynews-gen/src/index.ts +36 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +15 -12
- package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
- package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
- package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/OFL.txt +92 -0
- package/plugins/generations/image-gen/assets/fonts/SOURCE.md +15 -0
- package/plugins/generations/image-gen/dist/index.js +700 -22
- package/plugins/generations/image-gen/package.json +1 -0
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +2 -2
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +19 -11
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -1
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +135 -0
- package/plugins/generations/image-gen/src/bytes.ts +116 -0
- package/plugins/generations/image-gen/src/compose/card.ts +294 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +147 -0
- package/plugins/generations/image-gen/src/compose/cmap.ts +177 -0
- package/plugins/generations/image-gen/src/index.ts +211 -13
- package/plugins/generations/image-gen/src/job.ts +12 -0
- package/plugins/generations/image-gen/src/presets.ts +53 -13
- package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
- package/plugins/generations/image-gen/src/providers/types.ts +12 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +27 -14
- package/plugins/kk/scripts/itc-stages.ts +21 -3
- package/plugins/kk/scripts/kk-workflow-stages.ts +461 -10
- package/plugins/kk/scripts/topic-boundary.json +96 -0
- package/plugins/kk/scripts/validate-voicescript.ts +15 -0
- package/plugins/kk/skills/content-judge/SKILL.md +30 -0
- package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
- package/plugins/kk/skills/content-safety/SKILL.md +163 -0
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/netease-music.md +31 -0
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +32 -0
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +35 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -1
- package/plugins/kk/skills/xhs-images/SKILL.md +11 -7
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +4 -4
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +139 -24
- package/plugins/kk/workflows/kk-itc.yaml +21 -8
- package/plugins/publishings/podcast-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/src/index.ts +2 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -0
- package/plugins/publishings/xhs-pub/dist/index.js +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
- package/plugins/publishings/xhs-pub/src/index.ts +1 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
- package/plugins/sp/lib/env.ts +0 -46
- package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
- package/plugins/sp/scripts/batch-preflight.mjs +0 -346
- package/plugins/sp/scripts/batch-preflight.ts +0 -459
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
- package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
- package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
- package/plugins/sp/scripts/idea-handoff.mjs +0 -22
- package/plugins/sp/scripts/idea-handoff.ts +0 -44
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
- package/plugins/sp/scripts/inline-run-setup.ts +0 -722
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
- package/plugins/sp/scripts/pr-reviewing.ts +0 -925
- package/plugins/sp/scripts/quality-gate.mjs +0 -179
- package/plugins/sp/scripts/quality-gate.ts +0 -217
- package/plugins/sp/scripts/script-contract-check.ts +0 -319
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
- package/plugins/sp/scripts/task-size-precheck.ts +0 -175
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
- package/plugins/sp/scripts/wrapup-steps.ts +0 -552
|
@@ -313,11 +313,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
313
313
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
314
314
|
|
|
315
315
|
if (docs.length === 0) {
|
|
316
|
-
const title = options?.title || (isZh ? '
|
|
316
|
+
const title = options?.title || (isZh ? 'AI 乱炖(暂无更新)' : 'AI 乱炖 (No Updates)');
|
|
317
317
|
const segments: VoiceScriptSegment[] = isZh
|
|
318
318
|
? [
|
|
319
319
|
{
|
|
320
|
-
|
|
320
|
+
// Task 0189 R1/R4: honest no-content episode — the copy names the show
|
|
321
|
+
// (咸话咸说) and the column (AI 乱炖), and never claims a news digest happened.
|
|
322
|
+
text: `大家好,今天是${dateStr}。欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${voiceProfile}。今天没有足够扎实的可聊内容。`,
|
|
321
323
|
gap_ms: 400,
|
|
322
324
|
emotion: 'calm',
|
|
323
325
|
},
|
|
@@ -329,7 +331,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
329
331
|
]
|
|
330
332
|
: [
|
|
331
333
|
{
|
|
332
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
334
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI 乱炖, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
333
335
|
gap_ms: 400,
|
|
334
336
|
emotion: 'calm',
|
|
335
337
|
},
|
|
@@ -352,9 +354,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
352
354
|
});
|
|
353
355
|
}
|
|
354
356
|
|
|
355
|
-
const title =
|
|
356
|
-
options?.title ||
|
|
357
|
-
(isZh ? `今日AI科技漫谈(共${docs.length}条)` : `AI Daily Briefing (${docs.length} stories)`);
|
|
357
|
+
const title = options?.title || (isZh ? `AI 乱炖(共${docs.length}条)` : `AI 乱炖 (${docs.length} stories)`);
|
|
358
358
|
const segments: VoiceScriptSegment[] = [];
|
|
359
359
|
|
|
360
360
|
let hostName =
|
|
@@ -382,9 +382,10 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
382
382
|
? scriptPlan
|
|
383
383
|
? // F613: fixed greeting stays builder-owned; the plan only fills the slot between
|
|
384
384
|
// greeting and first story (the template tail 从「今天科技圈有几条…正题。」 is replaced).
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
// Task 0189 R1: the greeting names the show (咸话咸说) and the column (AI 乱炖).
|
|
386
|
+
`大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。${scriptPlan.intro}`
|
|
387
|
+
: `大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。今天我从 AI 工具、论文和实践中整理了${countText}值得聊的内容${titlesSummary ? `,包括${titlesSummary}` : ''},咱们一条条来聊。`
|
|
388
|
+
: `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI 乱炖, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
388
389
|
|
|
389
390
|
segments.push({
|
|
390
391
|
text: normalizeBroadcastText(introText, isZh),
|
|
@@ -449,11 +450,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
449
450
|
|
|
450
451
|
// 3. Outro segment: Warm, conversational wrap-up. F613: the plan outro replaces the template
|
|
451
452
|
// wrap-up head; the fixed sign-off stays builder-owned.
|
|
453
|
+
// Task 0189 R1: the AI-voice disclosure is part of the builder-owned sign-off (not the plan),
|
|
454
|
+
// so it is spoken in every episode — Apple Podcasts guideline 1.11 requires it in the content.
|
|
452
455
|
const outroText = isZh
|
|
453
456
|
? scriptPlan
|
|
454
|
-
? `${scriptPlan.outro}
|
|
455
|
-
:
|
|
456
|
-
: `That wraps up
|
|
457
|
+
? `${scriptPlan.outro}本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
458
|
+
: `好了,本期AI乱炖就聊到这里。希望今天这些工具、论文和实践的经验对您有启发。本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
459
|
+
: `That wraps up this episode of AI 乱炖. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
457
460
|
|
|
458
461
|
segments.push({
|
|
459
462
|
text: normalizeBroadcastText(outroText, isZh),
|
|
@@ -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
|
|
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
|
};
|
|
@@ -635,6 +635,8 @@ export interface FilterConfig {
|
|
|
635
635
|
minImportance: number;
|
|
636
636
|
minUrgency: number;
|
|
637
637
|
minImpact: number;
|
|
638
|
+
/** Episode floor: when fewer docs survive, importance-only rejects backfill up to this count. */
|
|
639
|
+
minEpisodeItems: number;
|
|
638
640
|
}
|
|
639
641
|
|
|
640
642
|
/** A doc the filter dropped, with every failed check (`category:not-in-allowlist`, `quality:2<3`, …). */
|
|
@@ -659,6 +661,13 @@ export const DEFAULT_FILTER_CONFIG: FilterConfig = {
|
|
|
659
661
|
minImportance: 3,
|
|
660
662
|
minUrgency: 2,
|
|
661
663
|
minImpact: 1,
|
|
664
|
+
// 20261002 dogfood: a weak, policy-heavy day collapsed the episode to 2 spoken items
|
|
665
|
+
// (~5 min audio) because importance (source-tier base) is the first dimension a tail-source
|
|
666
|
+
// day fails. Backfill admits importance-only rejects — quality/urgency/impact/category
|
|
667
|
+
// minimums stay hard — so a thin news day deepens instead of shipping a stub. Floor is 8,
|
|
668
|
+
// not 6: the prompt-layer 选题边界 (script-write) drops policy items AFTER this gate (today
|
|
669
|
+
// 2 of 5), so the floor compensates upstream to land ~4-6 spoken items on a thin day.
|
|
670
|
+
minEpisodeItems: 8,
|
|
662
671
|
};
|
|
663
672
|
|
|
664
673
|
/** One QC_MIN_* threshold: an integer 0–5 or absent (→ fallback); anything else fails loud — never clamped. */
|
|
@@ -671,6 +680,17 @@ function parseFilterThreshold(raw: string | undefined, name: string, fallback: n
|
|
|
671
680
|
return parsed;
|
|
672
681
|
}
|
|
673
682
|
|
|
683
|
+
/** QC_MIN_EPISODE_ITEMS: a positive integer count or absent (→ fallback); anything else fails loud. */
|
|
684
|
+
function parseFloorCount(raw: string | undefined, name: string, fallback: number): number {
|
|
685
|
+
if (raw === undefined || raw.trim() === '') return fallback;
|
|
686
|
+
const value = raw.trim();
|
|
687
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name}='${raw}' is not a positive integer (fail loud, no clamping)`);
|
|
688
|
+
const parsed = Number.parseInt(value, 10);
|
|
689
|
+
if (parsed < 1)
|
|
690
|
+
throw new Error(`${name}=${parsed} must be >= 1 (a 0 floor disables the gate — drop the env instead)`);
|
|
691
|
+
return parsed;
|
|
692
|
+
}
|
|
693
|
+
|
|
674
694
|
/** QC_CATEGORIES: a comma list of MECE categories; unknown entries fail loud (no silent drops). */
|
|
675
695
|
function parseFilterCategories(raw: string | undefined): Category[] {
|
|
676
696
|
if (raw === undefined || raw.trim() === '') return [...DEFAULT_FILTER_CONFIG.categories];
|
|
@@ -697,6 +717,11 @@ export function filterConfigFromEnv(env: Record<string, string | undefined> = pr
|
|
|
697
717
|
),
|
|
698
718
|
minUrgency: parseFilterThreshold(env.QC_MIN_URGENCY, 'QC_MIN_URGENCY', DEFAULT_FILTER_CONFIG.minUrgency),
|
|
699
719
|
minImpact: parseFilterThreshold(env.QC_MIN_IMPACT, 'QC_MIN_IMPACT', DEFAULT_FILTER_CONFIG.minImpact),
|
|
720
|
+
minEpisodeItems: parseFloorCount(
|
|
721
|
+
env.QC_MIN_EPISODE_ITEMS,
|
|
722
|
+
'QC_MIN_EPISODE_ITEMS',
|
|
723
|
+
DEFAULT_FILTER_CONFIG.minEpisodeItems,
|
|
724
|
+
),
|
|
700
725
|
};
|
|
701
726
|
}
|
|
702
727
|
|
|
@@ -749,6 +774,53 @@ export function filterAnnotated(docs: Doc[], config: FilterConfig): { kept: Doc[
|
|
|
749
774
|
return { kept, rejected };
|
|
750
775
|
}
|
|
751
776
|
|
|
777
|
+
function importanceOf(doc: Doc): number {
|
|
778
|
+
const scores = asRecord(asRecord(doc.metadata).scores);
|
|
779
|
+
return typeof scores.importance === 'number' ? scores.importance : 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function planScoreOf(doc: Doc): number {
|
|
783
|
+
const value = asRecord(doc.metadata).planScore;
|
|
784
|
+
return typeof value === 'number' ? value : 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Episode floor (20261002): when the strict filter leaves fewer than `minEpisodeItems` docs,
|
|
789
|
+
* backfill from rejects that failed ONLY the importance minimum — importance is the source-tier
|
|
790
|
+
* base (tail sources carry 2), so on a thin day it is the softest dimension, while quality/
|
|
791
|
+
* urgency/impact/category stay hard gates. Admission order: importance desc, planScore desc,
|
|
792
|
+
* plan order. Admitted docs are stamped `metadata.backfilled: true` and leave the rejected
|
|
793
|
+
* audit (they are spoken, not dropped). Pure: input docs are never mutated.
|
|
794
|
+
*/
|
|
795
|
+
export function backfillToFloor(
|
|
796
|
+
kept: Doc[],
|
|
797
|
+
rejected: RejectedDoc[],
|
|
798
|
+
config: FilterConfig,
|
|
799
|
+
): { kept: Doc[]; rejected: RejectedDoc[]; admitted: Doc[] } {
|
|
800
|
+
if (kept.length >= config.minEpisodeItems) return { kept, rejected, admitted: [] };
|
|
801
|
+
const eligible = rejected
|
|
802
|
+
.map((entry, index) => ({ entry, index }))
|
|
803
|
+
.filter(({ entry }) => entry.reasons.length > 0 && entry.reasons.every((r) => r.startsWith('importance:')));
|
|
804
|
+
eligible.sort((a, b) => {
|
|
805
|
+
const byImportance = importanceOf(b.entry.doc) - importanceOf(a.entry.doc);
|
|
806
|
+
if (byImportance !== 0) return byImportance;
|
|
807
|
+
const byPlanScore = planScoreOf(b.entry.doc) - planScoreOf(a.entry.doc);
|
|
808
|
+
if (byPlanScore !== 0) return byPlanScore;
|
|
809
|
+
return a.index - b.index;
|
|
810
|
+
});
|
|
811
|
+
const taken = eligible.slice(0, config.minEpisodeItems - kept.length);
|
|
812
|
+
const takenIds = new Set(taken.map(({ entry }) => entry.doc.id));
|
|
813
|
+
const admitted = taken.map(({ entry }) => ({
|
|
814
|
+
...entry.doc,
|
|
815
|
+
metadata: { ...asRecord(entry.doc.metadata), backfilled: true },
|
|
816
|
+
}));
|
|
817
|
+
return {
|
|
818
|
+
kept: [...kept, ...admitted],
|
|
819
|
+
rejected: rejected.filter((entry) => !takenIds.has(entry.doc.id)),
|
|
820
|
+
admitted,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
752
824
|
/** Sibling rejected-audit path: `<out>.json` → `<out>.rejected.json` (candidates.json → candidates.rejected.json). */
|
|
753
825
|
export function rejectedOutPath(outputPath: string): string {
|
|
754
826
|
return outputPath.endsWith('.json')
|
|
@@ -792,9 +864,15 @@ async function processFilterIO(inputPath: string, outputPath: string): Promise<C
|
|
|
792
864
|
const config = filterConfigFromEnv();
|
|
793
865
|
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
794
866
|
const { kept, rejected } = filterAnnotated(docs, config);
|
|
795
|
-
const
|
|
867
|
+
const filled = backfillToFloor(kept, rejected, config);
|
|
868
|
+
if (filled.admitted.length > 0) {
|
|
869
|
+
echoError(
|
|
870
|
+
`episode floor backfill: admitted ${filled.admitted.length} importance-only reject(s) to reach ${config.minEpisodeItems} items (${filled.admitted.map((d) => d.id).join(', ')})`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const content = planContent(filled.kept, resolvePlanDate());
|
|
796
874
|
await atomicWrite(outputPath, content);
|
|
797
|
-
await atomicWrite(rejectedOutPath(outputPath), { rejected, config, total: docs.length });
|
|
875
|
+
await atomicWrite(rejectedOutPath(outputPath), { rejected: filled.rejected, config, total: docs.length });
|
|
798
876
|
return content;
|
|
799
877
|
}
|
|
800
878
|
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
2
|
+
Version 1.1.
|
|
3
|
+
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font
|
|
14
|
+
creation efforts of academic and linguistic communities, and to
|
|
15
|
+
provide a free and open framework in which fonts may be shared and
|
|
16
|
+
improved in partnership with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply to
|
|
25
|
+
any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software
|
|
36
|
+
components as distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
39
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
40
|
+
components of the Original Version, by changing formats or by porting
|
|
41
|
+
the Font Software to a new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION & CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
49
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
50
|
+
Font Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
53
|
+
Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the
|
|
64
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
65
|
+
primary font name as presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created using
|
|
77
|
+
the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Noto Sans SC — provenance
|
|
2
|
+
|
|
3
|
+
Shipped static SubsetOTF/SC fonts used by the `compose` text-card composer (task 0180, ADR-028).
|
|
4
|
+
Output is reproducible on every install because these bytes ship with the plugin — no runtime
|
|
5
|
+
download and no system-font lookup.
|
|
6
|
+
|
|
7
|
+
- **Upstream:** https://github.com/notofonts/noto-cjk (Noto Sans CJK SC, static SubsetOTF)
|
|
8
|
+
- **Version:** tag `Sans2.004` (https://github.com/notofonts/noto-cjk/releases/tag/Sans2.004)
|
|
9
|
+
- **Licence:** SIL Open Font License 1.1 (`OFL.txt`, from the repo root `LICENSE` at the same tag)
|
|
10
|
+
|
|
11
|
+
| File | Upstream path at `Sans2.004` | SHA-256 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `NotoSansSC-Regular.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Regular.otf` | `faa6c9df652116dde789d351359f3d7e5d2285a2b2a1f04a2d7244df706d5ea9` |
|
|
14
|
+
| `NotoSansSC-Bold.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Bold.otf` | `c6cb5a93abaa9edc8ee7463b7ebb7f42d618d40e6ed2f7a5371c97b0b64767c0` |
|
|
15
|
+
| `OFL.txt` | `LICENSE` | `6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2` |
|