@gobing-ai/knowledge-kit 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +2478 -1737
- package/package.json +1 -1
- package/plugins/generations/image-gen/README.md +108 -0
- package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
- package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
- package/plugins/generations/image-gen/dist/index.js +102 -58
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
- package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
- package/plugins/generations/image-gen/src/presets.ts +6 -3
- package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
- package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
- package/plugins/ingestions/web-search/src/index.ts +1 -1
- package/plugins/kk/README.md +18 -0
- package/plugins/kk/commands/image-extract.md +1 -0
- package/plugins/kk/commands/image-generate.md +1 -0
- package/plugins/kk/commands/tell-me.md +1 -0
- package/plugins/kk/commands/workflow-run.md +7 -1
- package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
- package/plugins/kk/decisions/kk-itc.yaml +6 -4
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
- package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
- package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
- package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
- package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
- package/plugins/kk/scripts/daily-stages.ts +1403 -156
- package/plugins/kk/scripts/kk-workflow-stages.ts +266 -10
- package/plugins/kk/scripts/safety-artifact-check.ts +277 -0
- package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
- package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/content-judge/SKILL.md +1 -0
- package/plugins/kk/skills/content-safety/SKILL.md +1 -0
- package/plugins/kk/skills/explain-things/SKILL.md +1 -0
- package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
- package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
- package/plugins/kk/skills/publish/SKILL.md +1 -0
- package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
- package/plugins/kk/skills/storm-research/SKILL.md +1 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
- package/plugins/kk/skills/topic/SKILL.md +1 -0
- package/plugins/kk/skills/web-automation/SKILL.md +1 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +177 -128
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
- package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
- package/plugins/kk/workflows/kk-daily-safety.yaml +156 -69
- package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
- package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
- package/plugins/kk/workflows/kk-itc.yaml +36 -4
- package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/csdn-pub/dist/index.js +2 -1
- package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/infoq-pub/dist/index.js +2 -1
- package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/jike-pub/dist/index.js +2 -1
- package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/juejin-pub/dist/index.js +2 -1
- package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
- package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
- package/plugins/publishings/linkedin-pub/package.json +19 -0
- package/plugins/publishings/linkedin-pub/plugin.json +12 -0
- package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
- package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
- package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +46 -8
- package/plugins/publishings/podcast-pub/src/index.ts +62 -5
- package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/sspai-pub/dist/index.js +2 -1
- package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/substack-pub/dist/index.js +2 -1
- package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +461 -53
- package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
- package/plugins/publishings/surfdash-pub/src/index.ts +592 -65
- package/plugins/publishings/threads-pub/dist/index.js +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +12 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +936 -98
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
- package/plugins/publishings/wechatmp-pub/src/flow.ts +1134 -86
- package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
- package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
- package/plugins/publishings/weibo-pub/dist/index.js +2 -1
- package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/x-pub/dist/index.js +9 -1
- package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/x-pub/src/index.ts +11 -0
- package/plugins/publishings/xhs-pub/dist/index.js +2 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
- 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
|
-
'
|
|
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
|
|
1282
|
-
* enter `publish_status` —
|
|
1283
|
-
*
|
|
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
|
|
1305
|
-
const
|
|
1306
|
-
|
|
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',
|
|
@@ -1406,6 +1442,180 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
1406
1442
|
* `KK_SURFDASH_PATCH_OFFLINE=1` keeps fixture tests hermetic — the plugin would otherwise write to,
|
|
1407
1443
|
* commit and push a real checkout.
|
|
1408
1444
|
*/
|
|
1445
|
+
/**
|
|
1446
|
+
* `publish-matrix-partial` (0333): a partial/none window leaves an explicit recovery marker.
|
|
1447
|
+
*
|
|
1448
|
+
* Reads the window's receipt-derived outcome (written by `publish-matrix-classify`) — the legacy
|
|
1449
|
+
* `_17_publish_result.json` aggregate is never an input — names the exact failed target ids, and
|
|
1450
|
+
* points at the matrix rerun contract (`kk stage run publish-matrix-send <work_dir> --window <w>
|
|
1451
|
+
* --retry`). The rerun is receipt-scoped: `--retry` resends only effect-none failures, never a
|
|
1452
|
+
* succeeded or live-uncertain row, so there is no `--retry-from`/`_17` style file replay. A full
|
|
1453
|
+
* window deletes any stale marker from a previous attempt, so the report never re-flags recovered
|
|
1454
|
+
* targets.
|
|
1455
|
+
*/
|
|
1456
|
+
function publishMatrixPartial(args: string[]): void {
|
|
1457
|
+
const flag = (name: string): string | undefined => {
|
|
1458
|
+
const i = args.indexOf(name);
|
|
1459
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
1460
|
+
};
|
|
1461
|
+
const workDir = args[0] ?? '';
|
|
1462
|
+
const window = flag('--window');
|
|
1463
|
+
if (workDir === '' || window === undefined || window === '') {
|
|
1464
|
+
failWith(1, 'usage: publish-matrix-partial <work_dir> --window <w>');
|
|
1465
|
+
}
|
|
1466
|
+
const outcomePath = join(workDir, '4-publish', `outcome.${window}.json`);
|
|
1467
|
+
const outcome = requireJson(outcomePath, 'publish-matrix-partial') as {
|
|
1468
|
+
status?: string;
|
|
1469
|
+
rows?: Array<{ id?: string; status?: string; dummy?: boolean }>;
|
|
1470
|
+
};
|
|
1471
|
+
const marker = join(workDir, '4-publish', `partial-marker.${window}.txt`);
|
|
1472
|
+
timed(workDir, 'publish-matrix-partial', () => {
|
|
1473
|
+
if (outcome.status === 'full') {
|
|
1474
|
+
if (isFile(marker)) sted('rm', marker, () => unlinkSync(marker));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const failed = (outcome.rows ?? [])
|
|
1478
|
+
.filter((row) => row.dummy !== true && row.status !== 'succeeded')
|
|
1479
|
+
.map((row) => row.id ?? '(unknown)')
|
|
1480
|
+
.join(',');
|
|
1481
|
+
sted('mkdir', dirname(marker), () => mkdirSync(dirname(marker), { recursive: true }));
|
|
1482
|
+
sted('write', marker, () =>
|
|
1483
|
+
writeRaw(
|
|
1484
|
+
marker,
|
|
1485
|
+
`window ${window} ended ${outcome.status ?? 'unknown'}: failed targets: ${failed === '' ? '(unparsed)' : failed}\n` +
|
|
1486
|
+
`recovery: kk stage run publish-matrix-send ${workDir} --window ${window} --retry (resends effect-none failures only; receipts scope the retry)\n`,
|
|
1487
|
+
),
|
|
1488
|
+
);
|
|
1489
|
+
console.error(`WARNING: partial publish — failed targets: ${failed === '' ? '(unparsed)' : failed}`);
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* `publish-matrix-patch-surfdash` (0333): patch podcast links into a real surfdash post.
|
|
1495
|
+
*
|
|
1496
|
+
* The patch payload is derived from the window's own receipts, not from legacy aggregate files and
|
|
1497
|
+
* not from a `KK_SURFDASH_ROOT` argument — the plugin resolves its checkout itself. Evidence rules:
|
|
1498
|
+
* the surfdash row must have a real (non-dummy) succeeded receipt whose aggregate metadata carries
|
|
1499
|
+
* an absolute `postPath`; without that there is nothing to patch and the stage records an explicit
|
|
1500
|
+
* skip result instead of fabricating one. Podcast links ride along only when the podcast row is
|
|
1501
|
+
* real too — a dummy or missing episode yields explicit empty `coverUrl`/`showNotesUrl`, never a
|
|
1502
|
+
* fabricated link. `KK_SURFDASH_PATCH_OFFLINE=1` keeps rehearsal runs hermetic the same way the
|
|
1503
|
+
* legacy voice2 stage does.
|
|
1504
|
+
*/
|
|
1505
|
+
function publishMatrixPatchSurfdash(args: string[]): void {
|
|
1506
|
+
const flag = (name: string): string | undefined => {
|
|
1507
|
+
const i = args.indexOf(name);
|
|
1508
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
1509
|
+
};
|
|
1510
|
+
const workDir = args[0] ?? '';
|
|
1511
|
+
const window = flag('--window');
|
|
1512
|
+
if (workDir === '' || window === undefined || window === '') {
|
|
1513
|
+
failWith(1, 'usage: publish-matrix-patch-surfdash <work_dir> --window <w>');
|
|
1514
|
+
}
|
|
1515
|
+
const publishDir = join(workDir, '4-publish');
|
|
1516
|
+
const resultPath = join(publishDir, 'patch-surfdash.result.json');
|
|
1517
|
+
const writeResult = (value: Record<string, unknown>): void => {
|
|
1518
|
+
sted('write', resultPath, () => writeJqText(resultPath, value));
|
|
1519
|
+
};
|
|
1520
|
+
const receipt = (id: string): Record<string, unknown> | null =>
|
|
1521
|
+
readJson(join(publishDir, 'receipts', `${id}.json`));
|
|
1522
|
+
const aggregateTarget = (receiptPath: string | undefined): Record<string, unknown> => {
|
|
1523
|
+
const aggregate = receiptPath === undefined ? null : readJson(receiptPath);
|
|
1524
|
+
const targets = aggregate?.targets;
|
|
1525
|
+
if (!Array.isArray(targets) || targets.length === 0 || targets[0] === null || typeof targets[0] !== 'object') {
|
|
1526
|
+
return {};
|
|
1527
|
+
}
|
|
1528
|
+
return targets[0] as Record<string, unknown>;
|
|
1529
|
+
};
|
|
1530
|
+
timed(workDir, 'publish-matrix-patch-surfdash', () => {
|
|
1531
|
+
const outcome = readJson(join(publishDir, `outcome.${window}.json`)) as {
|
|
1532
|
+
rows?: Array<{ id?: string }>;
|
|
1533
|
+
} | null;
|
|
1534
|
+
const rowId = (prefix: string): string | undefined =>
|
|
1535
|
+
(outcome?.rows ?? []).map((row) => row.id ?? '').find((id) => id.startsWith(prefix));
|
|
1536
|
+
const sdId = rowId('surfdash-pub');
|
|
1537
|
+
const podId = rowId('podcast-pub');
|
|
1538
|
+
if (sdId === undefined) {
|
|
1539
|
+
const error = 'skipped: no surfdash target in this window — nothing to patch';
|
|
1540
|
+
writeResult({ ok: false, target: 'surfdash-pub', error });
|
|
1541
|
+
console.error(`patch-surfdash: ${error}`);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const sdReceipt = receipt(sdId);
|
|
1545
|
+
const sdReal = sdReceipt !== null && sdReceipt?.dummy !== true && sdReceipt?.status === 'succeeded';
|
|
1546
|
+
const sdTarget = aggregateTarget(
|
|
1547
|
+
typeof sdReceipt?.aggregatePath === 'string' ? sdReceipt.aggregatePath : undefined,
|
|
1548
|
+
);
|
|
1549
|
+
const postPath = (sdTarget.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1550
|
+
if (!sdReal || typeof postPath !== 'string' || postPath === '') {
|
|
1551
|
+
const error =
|
|
1552
|
+
sdReceipt === null
|
|
1553
|
+
? 'skipped: no surfdash publish receipt — nothing to patch'
|
|
1554
|
+
: typeof postPath !== 'string' || postPath === ''
|
|
1555
|
+
? 'skipped: surfdash receipt carries no real postPath — nothing to patch'
|
|
1556
|
+
: 'skipped: surfdash receipt is dummy — nothing to patch';
|
|
1557
|
+
writeResult({ ok: false, target: 'surfdash-pub', error });
|
|
1558
|
+
console.error(`patch-surfdash: ${error}`);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
// Links ride along only from real podcast evidence; dummy/missing episodes patch as
|
|
1562
|
+
// explicit empty strings — the patch must never fabricate a link.
|
|
1563
|
+
const podReceipt = podId === undefined ? null : receipt(podId);
|
|
1564
|
+
const podReal = podReceipt !== null && podReceipt?.dummy !== true && podReceipt?.status === 'succeeded';
|
|
1565
|
+
const podTarget = aggregateTarget(
|
|
1566
|
+
typeof podReceipt?.aggregatePath === 'string' ? podReceipt.aggregatePath : undefined,
|
|
1567
|
+
);
|
|
1568
|
+
const podMeta = (podTarget.metadata ?? {}) as Record<string, unknown>;
|
|
1569
|
+
const runDate = String(readJson(join(publishDir, `outcome.${window}.json`))?.runDate ?? '');
|
|
1570
|
+
const payload = join(publishDir, 'patch-surfdash.payload.json');
|
|
1571
|
+
sted('write', payload, () =>
|
|
1572
|
+
writeJqText(payload, {
|
|
1573
|
+
body: '',
|
|
1574
|
+
options: {
|
|
1575
|
+
operation: 'patch',
|
|
1576
|
+
postPath,
|
|
1577
|
+
coverUrl: podReal && typeof podMeta.imageUrl === 'string' ? podMeta.imageUrl : '',
|
|
1578
|
+
showNotesUrl: podReal && typeof podTarget.url === 'string' ? podTarget.url : '',
|
|
1579
|
+
...(runDate.length === 8
|
|
1580
|
+
? { dateIso: `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` }
|
|
1581
|
+
: runDate.length === 10
|
|
1582
|
+
? { dateIso: runDate }
|
|
1583
|
+
: {}),
|
|
1584
|
+
},
|
|
1585
|
+
}),
|
|
1586
|
+
);
|
|
1587
|
+
if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
|
|
1588
|
+
// Offline rehearsals still derive the payload (evidence), but never invoke the plugin.
|
|
1589
|
+
writeResult({ ok: false, target: 'surfdash-pub', error: 'skipped: offline (KK_SURFDASH_PATCH_OFFLINE=1)' });
|
|
1590
|
+
console.error(
|
|
1591
|
+
'patch-surfdash: offline — payload derived, plugin patch skipped (KK_SURFDASH_PATCH_OFFLINE=1)',
|
|
1592
|
+
);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const status = kk(['executor', 'run', 'surfdash-pub', '--in', payload, '--out', resultPath], {
|
|
1596
|
+
unset: NESTED_SPUR_UNSET,
|
|
1597
|
+
soft: true,
|
|
1598
|
+
});
|
|
1599
|
+
const result = readJson(resultPath);
|
|
1600
|
+
if (result?.ok === true) {
|
|
1601
|
+
const meta = (result.metadata ?? {}) as Record<string, unknown>;
|
|
1602
|
+
console.error(
|
|
1603
|
+
`patch-surfdash: ${String(result.message ?? 'patched')} — cms-resync ${String(meta.cmsResync ?? 'ok')}`,
|
|
1604
|
+
);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const meta = (result?.metadata ?? {}) as Record<string, unknown>;
|
|
1608
|
+
if (meta.cmsResync === 'failed') {
|
|
1609
|
+
// The posts are patched and committed; only the CMS re-sync (a soft channel) failed.
|
|
1610
|
+
console.error(
|
|
1611
|
+
`patch-surfdash: WARNING: cms re-sync failed — ${String(result?.error ?? `exit ${status}`)}; fix: kk executor run surfdash-pub --in ${payload} --out ${resultPath}`,
|
|
1612
|
+
);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
failWith(1, `patch-surfdash: ${String(result?.error ?? `surfdash-pub patch exited ${status}`)}`);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1409
1619
|
function dailyPatchSurfdash(args: string[]): void {
|
|
1410
1620
|
const [workDir = '', runDate = '', surfdashRoot = '', pluginsPath = ''] = args;
|
|
1411
1621
|
if (!surfdashRoot || !existsSync(surfdashRoot)) {
|
|
@@ -1415,8 +1625,24 @@ function dailyPatchSurfdash(args: string[]): void {
|
|
|
1415
1625
|
timed(workDir, 'patch-surfdash', () => {
|
|
1416
1626
|
const sdResult = join(workDir, '31-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
1417
1627
|
const podcastResult = join(workDir, '31-publish', `${runDate}_16_publish-podcast_result.json`);
|
|
1418
|
-
|
|
1419
|
-
|
|
1628
|
+
const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
|
|
1629
|
+
const invalidReceipt = (path: string): boolean => {
|
|
1630
|
+
const parsed = readJson(path);
|
|
1631
|
+
return (
|
|
1632
|
+
!isFile(path) ||
|
|
1633
|
+
parsed === null ||
|
|
1634
|
+
!Array.isArray(parsed.targets) ||
|
|
1635
|
+
parsed.targets.length === 0 ||
|
|
1636
|
+
!parsed.targets.every(
|
|
1637
|
+
(target) => target !== null && typeof target === 'object' && !Array.isArray(target),
|
|
1638
|
+
)
|
|
1639
|
+
);
|
|
1640
|
+
};
|
|
1641
|
+
if (invalidReceipt(sdResult) || invalidReceipt(podcastResult)) {
|
|
1642
|
+
const error = 'patch-surfdash: malformed surfdash/podcast publish receipt — nothing to patch';
|
|
1643
|
+
sted('write', resultPath, () => writeJqText(resultPath, { ok: false, target: 'surfdash-pub', error }));
|
|
1644
|
+
console.error(error);
|
|
1645
|
+
return;
|
|
1420
1646
|
}
|
|
1421
1647
|
const targetOf = (path: string): Record<string, unknown> => {
|
|
1422
1648
|
const parsed = readJson(path) as { targets?: Array<Record<string, unknown>> };
|
|
@@ -1433,7 +1659,6 @@ function dailyPatchSurfdash(args: string[]): void {
|
|
|
1433
1659
|
console.error('patch-surfdash: surfdash result carries no postPath — nothing to patch, skip');
|
|
1434
1660
|
return;
|
|
1435
1661
|
}
|
|
1436
|
-
const resultPath = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_sync-result.json`);
|
|
1437
1662
|
if (process.env.KK_SURFDASH_PATCH_OFFLINE === '1') {
|
|
1438
1663
|
// The marker keeps the completeness report's cms-resync row honest in offline runs.
|
|
1439
1664
|
const skipMarker = join(workDir, '31-publish', `${runDate}_23_patch-surfdash_emdash-resync.txt`);
|
|
@@ -1667,6 +1892,11 @@ if (import.meta.main)
|
|
|
1667
1892
|
dailyPreflight(rest);
|
|
1668
1893
|
break;
|
|
1669
1894
|
}
|
|
1895
|
+
case 'daily-safety-manifest': {
|
|
1896
|
+
const { dailySafetyManifest } = await loadDailyStages();
|
|
1897
|
+
dailySafetyManifest(rest);
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1670
1900
|
case 'daily-safety-prep': {
|
|
1671
1901
|
const { dailySafetyPrep } = await loadDailyStages();
|
|
1672
1902
|
dailySafetyPrep(rest);
|
|
@@ -1717,6 +1947,11 @@ if (import.meta.main)
|
|
|
1717
1947
|
dailyQualityReport(rest);
|
|
1718
1948
|
break;
|
|
1719
1949
|
}
|
|
1950
|
+
case 'daily-quality-verdict': {
|
|
1951
|
+
const { dailyQualityVerdict } = await loadDailyStages();
|
|
1952
|
+
dailyQualityVerdict(rest);
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1720
1955
|
case 'daily-publish-prep': {
|
|
1721
1956
|
const { dailyPublishPrep } = await loadDailyStages();
|
|
1722
1957
|
dailyPublishPrep(rest);
|
|
@@ -1727,6 +1962,11 @@ if (import.meta.main)
|
|
|
1727
1962
|
dailyPublishSurfdash(rest);
|
|
1728
1963
|
break;
|
|
1729
1964
|
}
|
|
1965
|
+
case 'daily-prepare-surfdash': {
|
|
1966
|
+
const { dailyPrepareSurfdash } = await loadDailyStages();
|
|
1967
|
+
dailyPrepareSurfdash(rest);
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1730
1970
|
case 'daily-show-notes': {
|
|
1731
1971
|
const { dailyShowNotes } = await loadDailyStages();
|
|
1732
1972
|
dailyShowNotes(rest);
|
|
@@ -1764,6 +2004,12 @@ if (import.meta.main)
|
|
|
1764
2004
|
case 'daily-publish-partial':
|
|
1765
2005
|
dailyPublishPartial(rest);
|
|
1766
2006
|
break;
|
|
2007
|
+
case 'publish-matrix-partial':
|
|
2008
|
+
publishMatrixPartial(rest);
|
|
2009
|
+
break;
|
|
2010
|
+
case 'publish-matrix-patch-surfdash':
|
|
2011
|
+
publishMatrixPatchSurfdash(rest);
|
|
2012
|
+
break;
|
|
1767
2013
|
case 'daily-run-report':
|
|
1768
2014
|
dailyRunReport(rest);
|
|
1769
2015
|
break;
|
|
@@ -1772,11 +2018,21 @@ if (import.meta.main)
|
|
|
1772
2018
|
dailyTranslateLocalize(rest);
|
|
1773
2019
|
break;
|
|
1774
2020
|
}
|
|
2021
|
+
case 'daily-surfdash-refine': {
|
|
2022
|
+
const { dailySurfdashRefine } = await loadDailyStages();
|
|
2023
|
+
dailySurfdashRefine(rest);
|
|
2024
|
+
break;
|
|
2025
|
+
}
|
|
1775
2026
|
case 'daily-translate-sync': {
|
|
1776
2027
|
const { dailyTranslateSync } = await loadDailyStages();
|
|
1777
2028
|
dailyTranslateSync(rest);
|
|
1778
2029
|
break;
|
|
1779
2030
|
}
|
|
2031
|
+
case 'daily-translate-validate': {
|
|
2032
|
+
const { dailyTranslateValidate } = await loadDailyStages();
|
|
2033
|
+
dailyTranslateValidate(rest);
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
1780
2036
|
case 'itc-outline-pick': {
|
|
1781
2037
|
const { itcOutlinePick } = await loadItcStages();
|
|
1782
2038
|
itcOutlinePick(rest);
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
/** 0331: scope-aware artifact root — `24-script` (or empty) keeps the drafts paths byte-identical. */
|
|
45
|
+
function safetyPath(workDir: string, scopeDir: string, runDate: string, name: string): string {
|
|
46
|
+
const dir = scopeDir.trim() === '' || scopeDir.trim() === '24-script' ? '24-script' : scopeDir.trim();
|
|
47
|
+
return join(workDir, dir, `${runDate}_09_${name}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nonEmptyString(value: unknown): value is string {
|
|
51
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stringList(value: unknown): string[] | null {
|
|
55
|
+
if (!Array.isArray(value) || value.some((entry) => !nonEmptyString(entry))) return null;
|
|
56
|
+
return value.map((entry) => String(entry));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortedUnique(values: string[]): string[] | null {
|
|
60
|
+
const unique = [...new Set(values)];
|
|
61
|
+
return unique.length === values.length ? unique.sort() : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reportValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
65
|
+
const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
66
|
+
const report = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_report.json'));
|
|
67
|
+
if (request === null || report === null) return 'review request or report is missing/invalid JSON';
|
|
68
|
+
|
|
69
|
+
const requested = stringList(request.platforms);
|
|
70
|
+
const platforms = report.platforms;
|
|
71
|
+
if (requested === null || !Array.isArray(platforms) || platforms.length === 0) {
|
|
72
|
+
return 'review request platforms or report platforms[] is missing/empty';
|
|
73
|
+
}
|
|
74
|
+
const requestedNames = sortedUnique(requested);
|
|
75
|
+
const actualNames: string[] = [];
|
|
76
|
+
for (const platform of platforms) {
|
|
77
|
+
if (!isObject(platform) || !nonEmptyString(platform.platform)) return 'report platform entry is malformed';
|
|
78
|
+
const name = String(platform.platform);
|
|
79
|
+
if (!REPORT_DECISIONS.has(String(platform.decision))) return `invalid platform decision for ${name}`;
|
|
80
|
+
if (!Array.isArray(platform.findings)) return `findings[] is missing for ${name}`;
|
|
81
|
+
for (const finding of platform.findings) {
|
|
82
|
+
if (!isObject(finding)) return `finding entry is malformed for ${name}`;
|
|
83
|
+
if (finding.disposition !== undefined && !FINDING_DISPOSITIONS.has(String(finding.disposition))) {
|
|
84
|
+
return `invalid finding disposition for ${name}`;
|
|
85
|
+
}
|
|
86
|
+
if (finding.evidenceGap !== undefined && typeof finding.evidenceGap !== 'boolean') {
|
|
87
|
+
return `evidenceGap must be boolean for ${name}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
actualNames.push(name);
|
|
91
|
+
}
|
|
92
|
+
const actualNamesSorted = sortedUnique(actualNames);
|
|
93
|
+
if (
|
|
94
|
+
requestedNames === null ||
|
|
95
|
+
actualNamesSorted === null ||
|
|
96
|
+
requestedNames.join('\0') !== actualNamesSorted.join('\0')
|
|
97
|
+
) {
|
|
98
|
+
return 'report platforms do not exactly match the requested platforms';
|
|
99
|
+
}
|
|
100
|
+
if (!CONFIDENCE.has(String(report.confidence))) return 'report confidence is missing/invalid';
|
|
101
|
+
if (typeof report.escalate !== 'boolean') return 'report escalate must be boolean';
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function judgeValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
106
|
+
const verdict = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
|
|
107
|
+
if (verdict === null) return 'judge verdict is missing/invalid JSON';
|
|
108
|
+
if (!VERDICTS.has(String(verdict.verdict))) return 'judge verdict value is invalid';
|
|
109
|
+
if (!CONFIDENCE.has(String(verdict.confidence))) return 'judge confidence is missing/invalid';
|
|
110
|
+
if (verdict.rubric !== 'compliance') return 'judge rubric must be compliance';
|
|
111
|
+
if (
|
|
112
|
+
typeof verdict.score !== 'number' ||
|
|
113
|
+
!Number.isFinite(verdict.score) ||
|
|
114
|
+
verdict.score < 0 ||
|
|
115
|
+
verdict.score > 1
|
|
116
|
+
) {
|
|
117
|
+
return 'judge score must be a finite number from 0 through 1';
|
|
118
|
+
}
|
|
119
|
+
if (!nonEmptyString(verdict.summary)) return 'judge summary is missing/empty';
|
|
120
|
+
if (!Array.isArray(verdict.findings) || verdict.findings.length === 0) return 'judge findings[] is missing/empty';
|
|
121
|
+
const criteria = new Set<string>();
|
|
122
|
+
for (const finding of verdict.findings) {
|
|
123
|
+
if (!isObject(finding)) return 'judge finding entry is malformed';
|
|
124
|
+
if (!nonEmptyString(finding.criterion)) return 'judge finding criterion is missing/empty';
|
|
125
|
+
if (!FINDING_STATUSES.has(String(finding.status))) return 'judge finding status is invalid';
|
|
126
|
+
if (!nonEmptyString(finding.detail)) return 'judge finding detail is missing/empty';
|
|
127
|
+
criteria.add(String(finding.criterion));
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
verdict.verdict === 'PASS' &&
|
|
131
|
+
verdict.findings.some((finding) => isObject(finding) && finding.status === 'fail')
|
|
132
|
+
) {
|
|
133
|
+
return 'PASS verdict cannot contain a fail finding';
|
|
134
|
+
}
|
|
135
|
+
for (const criterion of COMPLIANCE_CRITERIA) {
|
|
136
|
+
if (!criteria.has(criterion)) return `judge findings omit compliance criterion ${criterion}`;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(verdict.feedback) || verdict.feedback.some((entry) => !nonEmptyString(entry))) {
|
|
139
|
+
return 'judge feedback[] is missing or contains an empty/non-string entry';
|
|
140
|
+
}
|
|
141
|
+
if (!nonEmptyString(verdict.content_ref)) return 'judge content_ref is missing/empty';
|
|
142
|
+
if (!nonEmptyString(verdict.evaluated_at) || !/^\d{4}-\d{2}-\d{2}T/.test(verdict.evaluated_at)) {
|
|
143
|
+
return 'judge evaluated_at must be an ISO-8601 timestamp';
|
|
144
|
+
}
|
|
145
|
+
if (!Number.isFinite(Date.parse(verdict.evaluated_at))) return 'judge evaluated_at is not a valid timestamp';
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The catalog decision may fall back to `escalate` even when the gate admitted `pass` or `exclude`.
|
|
151
|
+
* Only the gate's persisted status represents a real content-safety escalation, so unattended mode
|
|
152
|
+
* rejects that status while attended mode leaves it for the caller's pause node.
|
|
153
|
+
*/
|
|
154
|
+
function publishValid(workDir: string, scopeDir: string, runDate: string, unattendedArg: string): string | null {
|
|
155
|
+
if (unattendedArg !== 'true' && unattendedArg !== 'false') return 'unattended must be true or false';
|
|
156
|
+
const verdictToken = safetyPath(workDir, scopeDir, runDate, 'safety-verdict.status');
|
|
157
|
+
const gateStatusPath = safetyPath(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
158
|
+
let token = '';
|
|
159
|
+
let gateStatus = '';
|
|
160
|
+
try {
|
|
161
|
+
token = readFileSync(verdictToken, 'utf8').trim();
|
|
162
|
+
gateStatus = readFileSync(gateStatusPath, 'utf8').trim();
|
|
163
|
+
} catch {
|
|
164
|
+
return 'safety verdict token or gate status is missing/unreadable';
|
|
165
|
+
}
|
|
166
|
+
if (token !== 'accept') return `safety verdict token is ${token || 'missing'}, not accept`;
|
|
167
|
+
if (!new Set(['pass', 'exclude', 'escalate']).has(gateStatus)) {
|
|
168
|
+
return `gate status ${gateStatus || 'missing'} is not publish-valid`;
|
|
169
|
+
}
|
|
170
|
+
if (unattendedArg === 'true' && gateStatus === 'escalate') {
|
|
171
|
+
return 'unattended run refuses a real safety escalation; attended review is required';
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function draftDigest(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
177
|
+
const hash = createHash('sha256');
|
|
178
|
+
if (scopeDir.trim() === '' || scopeDir.trim() === '24-script') {
|
|
179
|
+
const article = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
180
|
+
const script = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
|
|
181
|
+
try {
|
|
182
|
+
hash.update(readFileSync(article)).update('\0');
|
|
183
|
+
hash.update(readFileSync(script)).update('\0');
|
|
184
|
+
return hash.digest('hex');
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Variants scope: digest over the manifest's sorted keys + current bytes (mirrors the
|
|
190
|
+
// daily-stages scopeDigest algorithm, so request/guard/verdict all bind the same bytes).
|
|
191
|
+
try {
|
|
192
|
+
const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
|
|
193
|
+
variants?: Record<string, { path?: unknown }>;
|
|
194
|
+
} | null;
|
|
195
|
+
if (manifest === null || !isObject(manifest.variants)) return null;
|
|
196
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
197
|
+
const path = join(workDir, String(manifest.variants[key].path));
|
|
198
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
199
|
+
}
|
|
200
|
+
return hash.digest('hex');
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function revisionValid(workDir: string, scopeDir: string, runDate: string): string | null {
|
|
207
|
+
const request = readJson(safetyPath(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
208
|
+
if (request === null || !nonEmptyString(request.digest)) return 'review request digest is missing';
|
|
209
|
+
const isVariants = scopeDir.trim() !== '' && scopeDir.trim() !== '24-script';
|
|
210
|
+
const digest = draftDigest(workDir, scopeDir, runDate);
|
|
211
|
+
if (digest === null) {
|
|
212
|
+
return isVariants
|
|
213
|
+
? 'a manifest-listed variant is missing/unreadable'
|
|
214
|
+
: 'revised article or VoiceScript is missing/unreadable';
|
|
215
|
+
}
|
|
216
|
+
if (digest === request.digest) return isVariants ? 'variant digest is unchanged' : 'draft digest is unchanged';
|
|
217
|
+
if (isVariants) {
|
|
218
|
+
const manifest = readJson(join(workDir, '4-safety-variants', 'manifest.json')) as {
|
|
219
|
+
variants?: Record<string, { path?: unknown }>;
|
|
220
|
+
} | null;
|
|
221
|
+
if (manifest === null || !isObject(manifest.variants)) return 'variants manifest is missing/malformed';
|
|
222
|
+
for (const entry of Object.values(manifest.variants)) {
|
|
223
|
+
const value = readJson(join(workDir, String(entry.path)));
|
|
224
|
+
if (value === null || !nonEmptyString(value.title) || !nonEmptyString(value.body)) {
|
|
225
|
+
return 'revised variant title/body is missing';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
231
|
+
const scriptPath = join(workDir, '24-script', `${runDate}_08_script_voicescript.yaml`);
|
|
232
|
+
const article = readJson(articlePath);
|
|
233
|
+
if (article === null || !nonEmptyString(article.title) || !nonEmptyString(article.body)) {
|
|
234
|
+
return 'revised article title/body is missing';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const validator = join(import.meta.dir, 'validate-voicescript.ts');
|
|
238
|
+
const result = spawnSync(process.execPath, [validator, '--in', scriptPath], {
|
|
239
|
+
encoding: 'utf8',
|
|
240
|
+
stdio: 'ignore',
|
|
241
|
+
});
|
|
242
|
+
if (result.status !== 0) return 'revised VoiceScript failed content validation';
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function main(): number {
|
|
247
|
+
// 0331: an optional trailing scope dir re-roots every artifact path (variants review reads
|
|
248
|
+
// 4-safety-variants); publish-valid keeps `unattended` in the 4th slot it has always had.
|
|
249
|
+
const [kind = '', workDir = '', runDate = '', fourth = '', fifth = ''] = process.argv.slice(2);
|
|
250
|
+
const isPublish = kind === 'publish-valid';
|
|
251
|
+
const unattended = isPublish ? fourth : 'false';
|
|
252
|
+
const scopeDir = isPublish ? fifth : fourth;
|
|
253
|
+
if (!kind || !workDir || !runDate) {
|
|
254
|
+
console.error(
|
|
255
|
+
'usage: safety-artifact-check.ts <report-valid|judge-valid|revision-valid|publish-valid> <work_dir> <run_date> [unattended] [scope_dir]',
|
|
256
|
+
);
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
const reason =
|
|
260
|
+
kind === 'report-valid'
|
|
261
|
+
? reportValid(workDir, scopeDir, runDate)
|
|
262
|
+
: kind === 'judge-valid'
|
|
263
|
+
? judgeValid(workDir, scopeDir, runDate)
|
|
264
|
+
: kind === 'revision-valid'
|
|
265
|
+
? revisionValid(workDir, scopeDir, runDate)
|
|
266
|
+
: kind === 'publish-valid'
|
|
267
|
+
? publishValid(workDir, scopeDir, runDate, unattended)
|
|
268
|
+
: 'unknown predicate';
|
|
269
|
+
if (reason !== null) {
|
|
270
|
+
console.error(`safety-artifact-check: ${kind}: ${reason}`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
console.log(`safety-artifact-check: ${kind}: ok`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
process.exitCode = main();
|