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