@gobing-ai/knowledge-kit 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +128 -25
  3. package/package.json +1 -1
  4. package/plugins/kk/plugin.json +1 -1
  5. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  6. package/plugins/kk/scripts/daily-stages.ts +1057 -108
  7. package/plugins/kk/scripts/kk-workflow-stages.ts +76 -10
  8. package/plugins/kk/scripts/safety-artifact-check.ts +235 -0
  9. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +168 -121
  10. package/plugins/kk/workflows/kk-daily-safety.yaml +129 -50
  11. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  12. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  13. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  14. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  15. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  16. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  17. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  18. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  19. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  20. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  21. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  22. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  23. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  24. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  25. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  26. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  27. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  28. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  29. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  30. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  31. package/plugins/publishings/surfdash-pub/dist/index.js +460 -52
  32. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  33. package/plugins/publishings/surfdash-pub/src/index.ts +589 -64
  34. package/plugins/publishings/wechatmp-pub/dist/index.js +933 -98
  35. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  36. package/plugins/publishings/wechatmp-pub/src/flow.ts +1125 -85
  37. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  38. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  39. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  40. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  41. package/plugins/publishings/x-pub/dist/index.js +2 -1
  42. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  43. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  44. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  45. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  46. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
@@ -1263,7 +1263,7 @@ function dailyPublishCompleteness(args: string[]): void {
1263
1263
  .join('; ');
1264
1264
  console.error(
1265
1265
  `WARNING: publish completeness — ${completeness.missing.join(', ')} did not complete: ${details}. ` +
1266
- 'Independent channels never enter publish_status; re-run their stages for this work dir (see ' +
1266
+ 'Side-channel and locale rows never enter publish_status; re-run the relevant stages for this work dir (see ' +
1267
1267
  `${completenessFile})`,
1268
1268
  );
1269
1269
  }
@@ -1278,9 +1278,10 @@ function targetsOf(path: string): Record<string, unknown>[] {
1278
1278
  }
1279
1279
 
1280
1280
  /**
1281
- * The publish tail's independent channels (locales, XHS, WeChat) and the surfdash CMS re-sync never
1282
- * enter `publish_status` — they are non-blocking by design (ADR-022/025/026). That is also why a
1283
- * tail driven by hand can skip every one of them and still leave a `full` status file behind:
1281
+ * The publish tail's auxiliary channels (locales, XHS, WeChat) and the surfdash CMS re-sync never
1282
+ * enter `publish_status` — only the primary Surfdash + podcast pair does. Locales now validate
1283
+ * before the primary Surfdash publish; XHS/WeChat remain non-blocking by design. A
1284
+ * tail driven by hand can still skip auxiliary channels and leave a `full` status file behind:
1284
1285
  * 20261002 shipped zh-only surfdash with no XHS and no WeChat draft, and nothing in the run's own
1285
1286
  * artifacts said so. Record each channel's state beside the status file so the gap is visible.
1286
1287
  */
@@ -1301,9 +1302,44 @@ function publishCompleteness(
1301
1302
  detail: 'translate markers absent (en/ja never scaffolded)',
1302
1303
  });
1303
1304
  } else {
1304
- const syncResultFile = at('24_translate-sync_sync-result.json');
1305
- const syncResult = isFile(syncResultFile) ? readJson(syncResultFile) : null;
1306
- if (syncResult === null) {
1305
+ const validationResultFile = at('24_translate-validate_result.json');
1306
+ const legacySyncResultFile = at('24_translate-sync_sync-result.json');
1307
+ const hasValidationResult = isFile(validationResultFile);
1308
+ const validationResult = hasValidationResult ? readJson(validationResultFile) : null;
1309
+ const syncResult = isFile(legacySyncResultFile) ? readJson(legacySyncResultFile) : null;
1310
+ if (hasValidationResult) {
1311
+ const primaryPublished = targetsOf(at('14_publish-surfdash_result.json')).some(
1312
+ (entry) => entry.target === 'surfdash-pub' && entry.ok === true && entry.status === 'published',
1313
+ );
1314
+ if (validationResult?.ok !== true) {
1315
+ rows.push({
1316
+ channel: 'locales',
1317
+ state: 'failed',
1318
+ detail: String(validationResult?.error ?? 'translate-validate receipt is missing or malformed'),
1319
+ });
1320
+ } else if (!primaryPublished) {
1321
+ rows.push({
1322
+ channel: 'locales',
1323
+ state: 'failed',
1324
+ detail: 'local validation passed, but final Surfdash publish did not complete',
1325
+ });
1326
+ } else {
1327
+ const untranslated = ['en', 'ja'].filter((locale) => {
1328
+ const marker = readFileSync(at(`20_translate-localize_${locale}-post-path.txt`), 'utf-8').trim();
1329
+ if (!isFile(marker)) return true;
1330
+ const head = readFileSync(marker, 'utf-8').split('---')[1] ?? '';
1331
+ return /^draft:\s*true\s*$/m.test(head);
1332
+ });
1333
+ rows.push({
1334
+ channel: 'locales',
1335
+ state: untranslated.length === 0 ? 'done' : 'failed',
1336
+ detail:
1337
+ untranslated.length === 0
1338
+ ? 'en/ja drafts validated and published with the primary Surfdash sync'
1339
+ : `${untranslated.join(', ')} draft is still a scaffold (draft: true) — not translated, so it stays unpublished`,
1340
+ });
1341
+ }
1342
+ } else if (syncResult === null) {
1307
1343
  rows.push({
1308
1344
  channel: 'locales',
1309
1345
  state: 'not-run',
@@ -1415,8 +1451,24 @@ function dailyPatchSurfdash(args: string[]): void {
1415
1451
  timed(workDir, 'patch-surfdash', () => {
1416
1452
  const sdResult = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
1417
1453
  const podcastResult = join(workDir, '31-publish', `${runDate}_16_publish-podcast_result.json`);
1418
- if (!isFile(sdResult) || !isFile(podcastResult)) {
1419
- failWith(1, 'patch-surfdash: missing surfdash/podcast publish results — run the publish states first');
1454
+ const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
1455
+ const invalidReceipt = (path: string): boolean => {
1456
+ const parsed = readJson(path);
1457
+ return (
1458
+ !isFile(path) ||
1459
+ parsed === null ||
1460
+ !Array.isArray(parsed.targets) ||
1461
+ parsed.targets.length === 0 ||
1462
+ !parsed.targets.every(
1463
+ (target) => target !== null && typeof target === 'object' && !Array.isArray(target),
1464
+ )
1465
+ );
1466
+ };
1467
+ if (invalidReceipt(sdResult) || invalidReceipt(podcastResult)) {
1468
+ const error = 'patch-surfdash: malformed surfdash/podcast publish receipt — nothing to patch';
1469
+ sted('write', resultPath, () => writeJqText(resultPath, { ok: false, target: 'surfdash-pub', error }));
1470
+ console.error(error);
1471
+ return;
1420
1472
  }
1421
1473
  const targetOf = (path: string): Record<string, unknown> => {
1422
1474
  const parsed = readJson(path) as { targets?: Array<Record<string, unknown>> };
@@ -1433,7 +1485,6 @@ function dailyPatchSurfdash(args: string[]): void {
1433
1485
  console.error('patch-surfdash: surfdash result carries no postPath — nothing to patch, skip');
1434
1486
  return;
1435
1487
  }
1436
- const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
1437
1488
  if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
1438
1489
  // The marker keeps the completeness report's cms-resync row honest in offline runs.
1439
1490
  const skipMarker = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_emdash-resync.txt`);
@@ -1727,6 +1778,11 @@ if (import.meta.main)
1727
1778
  dailyPublishSurfdash(rest);
1728
1779
  break;
1729
1780
  }
1781
+ case 'daily-prepare-surfdash': {
1782
+ const { dailyPrepareSurfdash } = await loadDailyStages();
1783
+ dailyPrepareSurfdash(rest);
1784
+ break;
1785
+ }
1730
1786
  case 'daily-show-notes': {
1731
1787
  const { dailyShowNotes } = await loadDailyStages();
1732
1788
  dailyShowNotes(rest);
@@ -1772,11 +1828,21 @@ if (import.meta.main)
1772
1828
  dailyTranslateLocalize(rest);
1773
1829
  break;
1774
1830
  }
1831
+ case 'daily-surfdash-refine': {
1832
+ const { dailySurfdashRefine } = await loadDailyStages();
1833
+ dailySurfdashRefine(rest);
1834
+ break;
1835
+ }
1775
1836
  case 'daily-translate-sync': {
1776
1837
  const { dailyTranslateSync } = await loadDailyStages();
1777
1838
  dailyTranslateSync(rest);
1778
1839
  break;
1779
1840
  }
1841
+ case 'daily-translate-validate': {
1842
+ const { dailyTranslateValidate } = await loadDailyStages();
1843
+ dailyTranslateValidate(rest);
1844
+ break;
1845
+ }
1780
1846
  case 'itc-outline-pick': {
1781
1847
  const { itcOutlinePick } = await loadItcStages();
1782
1848
  itcOutlinePick(rest);
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from 'node:child_process';
3
+ /**
4
+ * Pure safety-workflow transition predicates.
5
+ *
6
+ * The safety state machine cannot put a condition on an individual action. These predicates keep
7
+ * retry routing outside the agent actions: a successful primary artifact is preserved, while a
8
+ * missing or malformed artifact takes the bounded retry edge. The revision predicate also uses the
9
+ * safety request digest rather than Spur's Git working-tree `requireDiff` contract because daily
10
+ * drafts live under the ignored `.works` tree. The publish predicate keeps a real safety escalation
11
+ * out of unattended publication without mistaking a catalog fallback value for one.
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ import { readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ const REPORT_DECISIONS = new Set(['OK', 'RISK', 'BLOCK']);
18
+ const CONFIDENCE = new Set(['HIGH', 'MEDIUM', 'LOW']);
19
+ const FINDING_DISPOSITIONS = new Set(['note', 'fix', 'gap', 'escalate']);
20
+ const FINDING_STATUSES = new Set(['pass', 'fail', 'warn']);
21
+ const COMPLIANCE_CRITERIA = new Set([
22
+ 'no-prohibited-claims',
23
+ 'disclosure-presence',
24
+ 'jurisdictional-rules',
25
+ 'data-handling',
26
+ ]);
27
+ const VERDICTS = new Set(['PASS', 'FAIL', 'NEEDS_REVISION']);
28
+
29
+ type JsonObject = Record<string, unknown>;
30
+
31
+ function isObject(value: unknown): value is JsonObject {
32
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+
35
+ function readJson(path: string): JsonObject | null {
36
+ try {
37
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
38
+ return isObject(value) ? value : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function safetyPath(workDir: string, runDate: string, name: string): string {
45
+ return join(workDir, '24-script', `${runDate}_09_${name}`);
46
+ }
47
+
48
+ function nonEmptyString(value: unknown): value is string {
49
+ return typeof value === 'string' && value.trim() !== '';
50
+ }
51
+
52
+ function stringList(value: unknown): string[] | null {
53
+ if (!Array.isArray(value) || value.some((entry) => !nonEmptyString(entry))) return null;
54
+ return value.map((entry) => String(entry));
55
+ }
56
+
57
+ function sortedUnique(values: string[]): string[] | null {
58
+ const unique = [...new Set(values)];
59
+ return unique.length === values.length ? unique.sort() : null;
60
+ }
61
+
62
+ function reportValid(workDir: string, runDate: string): string | null {
63
+ const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
64
+ const report = readJson(safetyPath(workDir, runDate, 'safety-review_report.json'));
65
+ if (request === null || report === null) return 'review request or report is missing/invalid JSON';
66
+
67
+ const requested = stringList(request.platforms);
68
+ const platforms = report.platforms;
69
+ if (requested === null || !Array.isArray(platforms) || platforms.length === 0) {
70
+ return 'review request platforms or report platforms[] is missing/empty';
71
+ }
72
+ const requestedNames = sortedUnique(requested);
73
+ const actualNames: string[] = [];
74
+ for (const platform of platforms) {
75
+ if (!isObject(platform) || !nonEmptyString(platform.platform)) return 'report platform entry is malformed';
76
+ const name = String(platform.platform);
77
+ if (!REPORT_DECISIONS.has(String(platform.decision))) return `invalid platform decision for ${name}`;
78
+ if (!Array.isArray(platform.findings)) return `findings[] is missing for ${name}`;
79
+ for (const finding of platform.findings) {
80
+ if (!isObject(finding)) return `finding entry is malformed for ${name}`;
81
+ if (finding.disposition !== undefined && !FINDING_DISPOSITIONS.has(String(finding.disposition))) {
82
+ return `invalid finding disposition for ${name}`;
83
+ }
84
+ if (finding.evidenceGap !== undefined && typeof finding.evidenceGap !== 'boolean') {
85
+ return `evidenceGap must be boolean for ${name}`;
86
+ }
87
+ }
88
+ actualNames.push(name);
89
+ }
90
+ const actualNamesSorted = sortedUnique(actualNames);
91
+ if (
92
+ requestedNames === null ||
93
+ actualNamesSorted === null ||
94
+ requestedNames.join('\0') !== actualNamesSorted.join('\0')
95
+ ) {
96
+ return 'report platforms do not exactly match the requested platforms';
97
+ }
98
+ if (!CONFIDENCE.has(String(report.confidence))) return 'report confidence is missing/invalid';
99
+ if (typeof report.escalate !== 'boolean') return 'report escalate must be boolean';
100
+ return null;
101
+ }
102
+
103
+ function judgeValid(workDir: string, runDate: string): string | null {
104
+ const verdict = readJson(safetyPath(workDir, runDate, 'safety-judge_verdict.json'));
105
+ if (verdict === null) return 'judge verdict is missing/invalid JSON';
106
+ if (!VERDICTS.has(String(verdict.verdict))) return 'judge verdict value is invalid';
107
+ if (!CONFIDENCE.has(String(verdict.confidence))) return 'judge confidence is missing/invalid';
108
+ if (verdict.rubric !== 'compliance') return 'judge rubric must be compliance';
109
+ if (
110
+ typeof verdict.score !== 'number' ||
111
+ !Number.isFinite(verdict.score) ||
112
+ verdict.score < 0 ||
113
+ verdict.score > 1
114
+ ) {
115
+ return 'judge score must be a finite number from 0 through 1';
116
+ }
117
+ if (!nonEmptyString(verdict.summary)) return 'judge summary is missing/empty';
118
+ if (!Array.isArray(verdict.findings) || verdict.findings.length === 0) return 'judge findings[] is missing/empty';
119
+ const criteria = new Set<string>();
120
+ for (const finding of verdict.findings) {
121
+ if (!isObject(finding)) return 'judge finding entry is malformed';
122
+ if (!nonEmptyString(finding.criterion)) return 'judge finding criterion is missing/empty';
123
+ if (!FINDING_STATUSES.has(String(finding.status))) return 'judge finding status is invalid';
124
+ if (!nonEmptyString(finding.detail)) return 'judge finding detail is missing/empty';
125
+ criteria.add(String(finding.criterion));
126
+ }
127
+ if (
128
+ verdict.verdict === 'PASS' &&
129
+ verdict.findings.some((finding) => isObject(finding) && finding.status === 'fail')
130
+ ) {
131
+ return 'PASS verdict cannot contain a fail finding';
132
+ }
133
+ for (const criterion of COMPLIANCE_CRITERIA) {
134
+ if (!criteria.has(criterion)) return `judge findings omit compliance criterion ${criterion}`;
135
+ }
136
+ if (!Array.isArray(verdict.feedback) || verdict.feedback.some((entry) => !nonEmptyString(entry))) {
137
+ return 'judge feedback[] is missing or contains an empty/non-string entry';
138
+ }
139
+ if (!nonEmptyString(verdict.content_ref)) return 'judge content_ref is missing/empty';
140
+ if (!nonEmptyString(verdict.evaluated_at) || !/^\d{4}-\d{2}-\d{2}T/.test(verdict.evaluated_at)) {
141
+ return 'judge evaluated_at must be an ISO-8601 timestamp';
142
+ }
143
+ if (!Number.isFinite(Date.parse(verdict.evaluated_at))) return 'judge evaluated_at is not a valid timestamp';
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * The catalog decision may fall back to `escalate` even when the gate admitted `pass` or `exclude`.
149
+ * Only the gate's persisted status represents a real content-safety escalation, so unattended mode
150
+ * rejects that status while attended mode leaves it for the caller's pause node.
151
+ */
152
+ function publishValid(workDir: string, runDate: string, unattendedArg: string): string | null {
153
+ if (unattendedArg !== 'true' && unattendedArg !== 'false') return 'unattended must be true or false';
154
+ const verdictToken = safetyPath(workDir, runDate, 'safety-verdict.status');
155
+ const gateStatusPath = safetyPath(workDir, runDate, 'safety-judge_status.txt');
156
+ let token = '';
157
+ let gateStatus = '';
158
+ try {
159
+ token = readFileSync(verdictToken, 'utf8').trim();
160
+ gateStatus = readFileSync(gateStatusPath, 'utf8').trim();
161
+ } catch {
162
+ return 'safety verdict token or gate status is missing/unreadable';
163
+ }
164
+ if (token !== 'accept') return `safety verdict token is ${token || 'missing'}, not accept`;
165
+ if (!new Set(['pass', 'exclude', 'escalate']).has(gateStatus)) {
166
+ return `gate status ${gateStatus || 'missing'} is not publish-valid`;
167
+ }
168
+ if (unattendedArg === 'true' && gateStatus === 'escalate') {
169
+ return 'unattended run refuses a real safety escalation; attended review is required';
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function draftDigest(workDir: string, runDate: string): string | null {
175
+ const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
176
+ const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
177
+ try {
178
+ const hash = createHash('sha256');
179
+ hash.update(readFileSync(article)).update('\0');
180
+ hash.update(readFileSync(script)).update('\0');
181
+ return hash.digest('hex');
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ function revisionValid(workDir: string, runDate: string): string | null {
188
+ const request = readJson(safetyPath(workDir, runDate, 'safety-review_request.json'));
189
+ const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
190
+ const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
191
+ if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
192
+ if (draftDigest(workDir, runDate) === null) return 'revised article or VoiceScript is missing/unreadable';
193
+ if (draftDigest(workDir, runDate) === request.digest) return 'draft digest is unchanged';
194
+
195
+ const article = readJson(articlePath);
196
+ if (article === null || !nonEmptyString(article.title) || !nonEmptyString(article.body)) {
197
+ return 'revised article title/body is missing';
198
+ }
199
+
200
+ const validator = join(import.meta.dir, 'validate-voicescript.ts');
201
+ const result = spawnSync(process.execPath, [validator, '--in', scriptPath], {
202
+ encoding: 'utf8',
203
+ stdio: 'ignore',
204
+ });
205
+ if (result.status !== 0) return 'revised VoiceScript failed content validation';
206
+ return null;
207
+ }
208
+
209
+ function main(): number {
210
+ const [kind = '', workDir = '', runDate = '', unattended = 'false'] = process.argv.slice(2);
211
+ if (!kind || !workDir || !runDate) {
212
+ console.error(
213
+ 'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended]',
214
+ );
215
+ return 2;
216
+ }
217
+ const reason =
218
+ kind === 'report-valid'
219
+ ? reportValid(workDir, runDate)
220
+ : kind === 'judge-valid'
221
+ ? judgeValid(workDir, runDate)
222
+ : kind === 'revision-valid'
223
+ ? revisionValid(workDir, runDate)
224
+ : kind === 'publish-valid'
225
+ ? publishValid(workDir, runDate, unattended)
226
+ : 'unknown predicate';
227
+ if (reason !== null) {
228
+ console.error(`safety-artifact-check: ${kind}: ${reason}`);
229
+ return 1;
230
+ }
231
+ console.log(`safety-artifact-check: ${kind}: ok`);
232
+ return 0;
233
+ }
234
+
235
+ process.exitCode = main();