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