@gobing-ai/knowledge-kit 0.1.11 → 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 +2359 -1721
- 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-stages.ts +346 -48
- package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
- package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
- 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 +9 -7
- 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 +39 -31
- 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/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/surfdash-pub/dist/index.js +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +3 -1
- 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 +5 -2
- package/plugins/publishings/wechatmp-pub/src/flow.ts +11 -3
- package/plugins/publishings/x-pub/dist/index.js +7 -0
- package/plugins/publishings/x-pub/src/index.ts +11 -0
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
PII_FINDING_CATEGORY,
|
|
90
90
|
pluginsArgs,
|
|
91
91
|
readJson,
|
|
92
|
-
readSafetyReviseCount,
|
|
93
92
|
readTopicBoundary,
|
|
94
93
|
requireJson,
|
|
95
94
|
requireSidecar,
|
|
@@ -716,13 +715,23 @@ function refuseScriptBelowFloor(
|
|
|
716
715
|
}
|
|
717
716
|
|
|
718
717
|
export function dailyEpisodeDuration(args: string[]): void {
|
|
719
|
-
const [workDir = '', runDate = '', minArg = '', maxArg = '', vdriver = ''] = args;
|
|
718
|
+
const [workDir = '', runDate = '', minArg = '', maxArg = '', vdriver = '', runId = ''] = args;
|
|
720
719
|
if (!workDir) fail('missing work_dir');
|
|
721
720
|
if (!runDate) fail('missing run_date');
|
|
722
721
|
const minMinutes = Number(minArg);
|
|
723
722
|
const maxMinutes = Number(maxArg);
|
|
724
723
|
if (!Number.isFinite(minMinutes) || minMinutes <= 0) fail(`invalid min_minutes: ${minArg || '(empty)'}`);
|
|
725
724
|
if (!Number.isFinite(maxMinutes) || maxMinutes < minMinutes) fail(`invalid max_minutes: ${maxArg || '(empty)'}`);
|
|
725
|
+
// 0329 F06: the publish path must not survive a refusal. The pool-gate token flips to EMPTY so
|
|
726
|
+
// this run's own remaining fan-out skips, and a stale bundle.json from an earlier run (whose
|
|
727
|
+
// runs[] a publish window could still consume) is dropped.
|
|
728
|
+
const invalidatePublishPath = (): void => {
|
|
729
|
+
if (runId !== '') {
|
|
730
|
+
const token = join('.spur', 'run', `${runId}-pool-gate.status`);
|
|
731
|
+
if (isFile(token)) sted('write', token, () => writeRaw(token, 'EMPTY\n'));
|
|
732
|
+
}
|
|
733
|
+
rmSync(join(workDir, 'bundle.json'), { force: true });
|
|
734
|
+
};
|
|
726
735
|
const contentPath = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
|
|
727
736
|
if (!isFile(contentPath)) failWith(1, `episode-duration: audio content not found: ${contentPath}`);
|
|
728
737
|
let raw: unknown;
|
|
@@ -751,7 +760,8 @@ export function dailyEpisodeDuration(args: string[]): void {
|
|
|
751
760
|
// let the pool-gated publish fan package a sub-floor episode — so finish the emptying here.
|
|
752
761
|
if (isFile(refusal)) {
|
|
753
762
|
if (docs.length > 0) emptyCandidates(workDir, runDate);
|
|
754
|
-
|
|
763
|
+
invalidatePublishPath();
|
|
764
|
+
console.error(`episode-duration: duration refusal already recorded (${refusal}) — nothing to do`);
|
|
755
765
|
return;
|
|
756
766
|
}
|
|
757
767
|
const estimated = estimateSpokenMinutes(docs, vdriver);
|
|
@@ -767,6 +777,7 @@ export function dailyEpisodeDuration(args: string[]): void {
|
|
|
767
777
|
}),
|
|
768
778
|
);
|
|
769
779
|
emptyCandidates(workDir, runDate);
|
|
780
|
+
invalidatePublishPath();
|
|
770
781
|
console.error(
|
|
771
782
|
`episode-duration: episode ran ${shown} min (estimate ${estimated.minutes} min), below the ${minMinutes}–${maxMinutes} min requirement — candidates emptied, the run closes as no-content (see ${refusal})`,
|
|
772
783
|
);
|
|
@@ -1878,12 +1889,13 @@ export function buildSocialContent(
|
|
|
1878
1889
|
|
|
1879
1890
|
/**
|
|
1880
1891
|
* Length budgets the social authoring prompt targets, enforced here as a backstop (task 0327 R3).
|
|
1881
|
-
* X counts CJK at 2 (`weighted`); weibo and
|
|
1892
|
+
* X counts CJK at 2 (`weighted`); weibo, threads and linkedin count characters.
|
|
1882
1893
|
*/
|
|
1883
1894
|
const SOCIAL_LIMITS: Record<string, SocialLimit> = {
|
|
1884
1895
|
x: { max: 280, weighted: true },
|
|
1885
1896
|
weibo: { max: 2000 },
|
|
1886
1897
|
threads: { max: 500 },
|
|
1898
|
+
linkedin: { max: 3000 },
|
|
1887
1899
|
};
|
|
1888
1900
|
|
|
1889
1901
|
/** Variant key → file name: `social:x` becomes `social_x.content.json`. */
|
|
@@ -1892,27 +1904,36 @@ function variantFileName(key: string): string {
|
|
|
1892
1904
|
}
|
|
1893
1905
|
|
|
1894
1906
|
/**
|
|
1895
|
-
* `daily-bundle-variants <workDir> <runDate> <pluginsPath
|
|
1896
|
-
* (task 0327 R2/R3).
|
|
1907
|
+
* `daily-bundle-variants <workDir> <runDate> <pluginsPath> [cardsEnabled] [coverEnabled]` — the
|
|
1908
|
+
* generate-side payload builder (task 0327 R2/R3).
|
|
1897
1909
|
*
|
|
1898
1910
|
* Everything the publish side used to assemble inline is produced here instead, once, as bundle
|
|
1899
1911
|
* variants under `30-variants/`. Links are the `{{article_url}}` / `{{podcast_url}}` placeholders,
|
|
1900
1912
|
* because at generate time nothing is published yet; `publish-matrix-send` resolves them from the
|
|
1901
1913
|
* tier-0 receipts. A variant whose source artifact is absent (channel disabled, cards never
|
|
1902
1914
|
* rendered, social copy not authored) is skipped with a log line, never invented.
|
|
1915
|
+
*
|
|
1916
|
+
* 0329 F16: the caller forwards the run's `cards_enabled` / `cover_enabled` switches — a disabled
|
|
1917
|
+
* producer's variant is a skip, not a stale re-read of a previous run's file — and every stale
|
|
1918
|
+
* variant file is wiped before writing, so `30-variants/` after this stage contains only what
|
|
1919
|
+
* THIS pass produced.
|
|
1903
1920
|
*/
|
|
1904
1921
|
export function dailyBundleVariants(args: string[]): void {
|
|
1905
|
-
const [workDir = '', runDate = '', _pluginsPath = ''] = args;
|
|
1922
|
+
const [workDir = '', runDate = '', _pluginsPath = '', cardsEnabledArg = '', coverEnabledArg = ''] = args;
|
|
1906
1923
|
timed(workDir, 'bundle-variants', () => {
|
|
1907
1924
|
const outDir = join(workDir, '30-variants');
|
|
1908
1925
|
sted('mkdir', outDir, () => mkdirSync(outDir, { recursive: true }));
|
|
1926
|
+
// 0329 F16: a rerun after a thinner day must not leave yesterday's variants behind.
|
|
1927
|
+
for (const entry of readdirSync(outDir)) {
|
|
1928
|
+
if (entry.endsWith('.content.json')) rmSync(join(outDir, entry), { force: true });
|
|
1929
|
+
}
|
|
1909
1930
|
const articlePath = join(workDir, '22-article', `${runDate}_06_article_content.json`);
|
|
1910
1931
|
const prepPath = join(workDir, '31-publish', `${runDate}_13_publish-prep_content.json`);
|
|
1911
1932
|
const notesPath = join(workDir, '31-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
1912
1933
|
const cardsPath = join(workDir, '25-xhs', 'content.json');
|
|
1913
1934
|
const coverPath = join(workDir, '23-cover', `${runDate}_07_cover_cover.png`);
|
|
1914
1935
|
const socialDir = join(workDir, '26-social');
|
|
1915
|
-
const cover = isFile(coverPath) ? coverPath : undefined;
|
|
1936
|
+
const cover = coverEnabledArg !== 'false' && isFile(coverPath) ? coverPath : undefined;
|
|
1916
1937
|
const links = PLACEHOLDER_LINKS;
|
|
1917
1938
|
const written: string[] = [];
|
|
1918
1939
|
const skipped: string[] = [];
|
|
@@ -1966,9 +1987,12 @@ export function dailyBundleVariants(args: string[]): void {
|
|
|
1966
1987
|
);
|
|
1967
1988
|
}
|
|
1968
1989
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1990
|
+
if (cardsEnabledArg === 'false') skipped.push('cards (cards_enabled=false)');
|
|
1991
|
+
else {
|
|
1992
|
+
const cards = readJson(cardsPath);
|
|
1993
|
+
if (cards === null) skipped.push('cards (no rendered card series)');
|
|
1994
|
+
else write('cards@xhs-pub', buildXhsCardsContent(cards, { publishStatus: 'draft' }));
|
|
1995
|
+
}
|
|
1972
1996
|
|
|
1973
1997
|
for (const platform of Object.keys(SOCIAL_LIMITS)) {
|
|
1974
1998
|
const copyPath = join(socialDir, `${platform}.md`);
|
|
@@ -2048,6 +2072,8 @@ export function readKeyPointItems(filePath: string): string[] | undefined {
|
|
|
2048
2072
|
flush();
|
|
2049
2073
|
current = item[1].trim();
|
|
2050
2074
|
folded = current === '>' || current === '>-' || current === '|-' || current === '|';
|
|
2075
|
+
// The block indicator is YAML syntax, not text: the item starts on the next line.
|
|
2076
|
+
if (folded) current = '';
|
|
2051
2077
|
continue;
|
|
2052
2078
|
}
|
|
2053
2079
|
if (current === undefined || !folded) continue;
|
|
@@ -3081,6 +3107,30 @@ export function dailyQualityReport(args: string[]): void {
|
|
|
3081
3107
|
});
|
|
3082
3108
|
}
|
|
3083
3109
|
|
|
3110
|
+
/**
|
|
3111
|
+
* 0329 F23 — the verdict half of `quality`. The report only printed, so a failed audio qc sailed
|
|
3112
|
+
* on into show-notes, the bundle and the publish windows (2026-10-25: an episode at 41/100 was
|
|
3113
|
+
* packaged and queued for tier-0 sends). Reads the same `metadata.qc` as `daily-quality-report`
|
|
3114
|
+
* and fails loud when the verdict is missing or `passed` is false.
|
|
3115
|
+
*/
|
|
3116
|
+
export function dailyQualityVerdict(args: string[]): void {
|
|
3117
|
+
const [workDir = '', runDate = ''] = args;
|
|
3118
|
+
const content = join(workDir, '30-audio', `${runDate}_11_generate_content.json`);
|
|
3119
|
+
timed(workDir, 'quality-verdict', () => {
|
|
3120
|
+
if (!isFile(content)) failWith(1, `quality-verdict: no audio content: ${content}`);
|
|
3121
|
+
const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
|
|
3122
|
+
if (qc === undefined) failWith(1, `quality-verdict: no qc verdict in ${content}`);
|
|
3123
|
+
if (qc.passed !== true) {
|
|
3124
|
+
const issues = Array.isArray(qc.criticalIssues) ? qc.criticalIssues : [];
|
|
3125
|
+
failWith(
|
|
3126
|
+
1,
|
|
3127
|
+
`quality-verdict: audio qc failed (overallScore ${String(qc.overallScore ?? '?')}) — ${issues.map(String).join('; ') || 'no issue detail on the verdict'}`,
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
console.log(`quality-verdict: audio qc passed (overallScore ${String(qc.overallScore ?? '?')})`);
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3084
3134
|
/**
|
|
3085
3135
|
* 0171 R3: Apple Podcasts reads the enclosure's ID3 tags, so the published MP3 carries
|
|
3086
3136
|
* title/artist/album/date. `-c copy` moves the audio frames without a re-encode, and the temp
|
|
@@ -3111,18 +3161,19 @@ export function dailyQualityReport(args: string[]): void {
|
|
|
3111
3161
|
* Fail-closed where it matters: a missing report, verdict or confidence still blocks.
|
|
3112
3162
|
*/
|
|
3113
3163
|
export function dailySafetyGate(args: string[]): void {
|
|
3114
|
-
const [workDir = '', runDate = '', maxReviseArg = '0'] = args;
|
|
3164
|
+
const [workDir = '', runDate = '', maxReviseArg = '0', scopeDir = ''] = args;
|
|
3165
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3115
3166
|
const maxRevise = Number.parseInt(maxReviseArg, 10);
|
|
3116
|
-
const statusFile =
|
|
3117
|
-
const stampFile =
|
|
3167
|
+
const statusFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
3168
|
+
const stampFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_stamp.json');
|
|
3118
3169
|
timed(workDir, 'safety-judge', () => {
|
|
3119
|
-
const request = readJson(
|
|
3120
|
-
const report = readJson(
|
|
3121
|
-
const verdict = readJson(
|
|
3170
|
+
const request = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-review_request.json'));
|
|
3171
|
+
const report = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-review_report.json'));
|
|
3172
|
+
const verdict = readJson(scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_verdict.json'));
|
|
3122
3173
|
const platforms = Array.isArray(report?.platforms) ? (report.platforms as Record<string, unknown>[]) : [];
|
|
3123
3174
|
const decisions = platforms.map((platform) => platform?.decision);
|
|
3124
|
-
const digest =
|
|
3125
|
-
const revisions =
|
|
3175
|
+
const digest = scopeDigest(workDir, scopeDir, runDate);
|
|
3176
|
+
const revisions = readScopeCounter(workDir, scopeDir);
|
|
3126
3177
|
const platformFindings = (platform: Record<string, unknown>): Record<string, unknown>[] =>
|
|
3127
3178
|
Array.isArray(platform?.findings) ? (platform.findings as Record<string, unknown>[]) : [];
|
|
3128
3179
|
// 20261007 narrowing (operator decision): on a clean pass, only a finding the reviewer
|
|
@@ -3153,9 +3204,12 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3153
3204
|
// data-handling), which is where an independent reviewer can grade it.
|
|
3154
3205
|
const blockedPlatforms = platforms.filter((platform) => platform?.decision === 'BLOCK');
|
|
3155
3206
|
const excludedPlatforms = blockedPlatforms.map((platform) => String(platform?.platform ?? '')).filter(Boolean);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
)
|
|
3207
|
+
// 20261104: a category-3 `note` is the reviewer recording that it checked and found NO PII —
|
|
3208
|
+
// counting it turned an admission-only BLOCK into a whole-run block. Any other disposition,
|
|
3209
|
+
// or none (legacy reports), still counts: fail-closed.
|
|
3210
|
+
const isPiiFinding = (finding: Record<string, unknown>): boolean =>
|
|
3211
|
+
Number(finding?.category) === PII_FINDING_CATEGORY && finding?.disposition !== 'note';
|
|
3212
|
+
const piiBlock = blockedPlatforms.some((platform) => platformFindings(platform).some(isPiiFinding));
|
|
3159
3213
|
// A BLOCK that names no finding is a contract violation, not a signal: there is nothing to
|
|
3160
3214
|
// remediate and nothing to exclude, so it blocks (fail-closed).
|
|
3161
3215
|
const unnamedBlock =
|
|
@@ -3219,7 +3273,7 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3219
3273
|
status = 'block';
|
|
3220
3274
|
const named = blockedPlatforms
|
|
3221
3275
|
.flatMap((platform) => platformFindings(platform))
|
|
3222
|
-
.filter(
|
|
3276
|
+
.filter(isPiiFinding)
|
|
3223
3277
|
.map((finding) => `${String(finding?.item ?? '?')}(category ${String(finding?.category)})`)
|
|
3224
3278
|
.join(', ');
|
|
3225
3279
|
const criteria = verdictFails.map((finding) => String(finding?.criterion)).join(', ');
|
|
@@ -3246,11 +3300,12 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
3246
3300
|
if (status === 'pass' || status === 'escalate' || status === 'exclude') {
|
|
3247
3301
|
sted('write', stampFile, () => writeJsonText(stampFile, { digest, status, excludedPlatforms }));
|
|
3248
3302
|
}
|
|
3249
|
-
if (status === 'block' && !edited) {
|
|
3303
|
+
if (status === 'block' && drafts && !edited) {
|
|
3250
3304
|
// A quality block poisons the isFile() draft cache: every same-day retry would
|
|
3251
3305
|
// re-fail on unchanged bytes (20261002 dogfood — two judge-fail runs replayed until
|
|
3252
3306
|
// the drafts were moved by hand). Quarantine (mv, never rm) so the next run
|
|
3253
3307
|
// regenerates from scratch; the 09_* verdict artifacts stay in 24-script for audit.
|
|
3308
|
+
// Variants scope never quarantines: the bundle is regenerated, not hand-repaired.
|
|
3254
3309
|
const q = join(workDir, '24-script', `.quarantine-${runDate}-judge-block`);
|
|
3255
3310
|
mkdirSync(q, { recursive: true });
|
|
3256
3311
|
for (const path of safetyDraftPaths(workDir, runDate)) {
|
|
@@ -3291,10 +3346,27 @@ export function dailyPreflight(args: string[]): void {
|
|
|
3291
3346
|
String(match[1]),
|
|
3292
3347
|
),
|
|
3293
3348
|
);
|
|
3349
|
+
// 0329 F21: the `publish-matrix-*` stages never appear in kk-workflow-stages' switch —
|
|
3350
|
+
// `kk stage` routes them through apps/cli's publish-matrix-stage.ts — so without this
|
|
3351
|
+
// roster every workflow invoking them died as runner-stale.
|
|
3352
|
+
const PUBLISH_MATRIX_STAGES = [
|
|
3353
|
+
'publish-matrix-plan',
|
|
3354
|
+
'publish-matrix-check',
|
|
3355
|
+
'publish-matrix-send',
|
|
3356
|
+
'publish-matrix-classify',
|
|
3357
|
+
'publish-matrix-bundle',
|
|
3358
|
+
'publish-matrix-rss-gate',
|
|
3359
|
+
'publish-matrix-wait-until',
|
|
3360
|
+
'publish-matrix-validate',
|
|
3361
|
+
'publish-matrix-settle',
|
|
3362
|
+
] as const;
|
|
3363
|
+
for (const name of PUBLISH_MATRIX_STAGES) known.add(name);
|
|
3294
3364
|
const missing: string[] = [];
|
|
3295
3365
|
for (const file of files) {
|
|
3296
3366
|
if (!isFile(file)) failWith(1, `daily-preflight.workflow-missing: ${file}`);
|
|
3297
|
-
|
|
3367
|
+
// 0329 F21: accept both spellings — `kk stage <name>` and the ADR-037 canonical
|
|
3368
|
+
// `kk stage run <name>`.
|
|
3369
|
+
for (const match of readFileSync(file, 'utf-8').matchAll(/\bkk stage (?:run )?([a-z][a-z0-9-]*)/g)) {
|
|
3298
3370
|
const name = String(match[1]);
|
|
3299
3371
|
if (!known.has(name)) missing.push(`${name} (needed by ${basename(file)})`);
|
|
3300
3372
|
}
|
|
@@ -3325,11 +3397,12 @@ export function dailyPreflight(args: string[]): void {
|
|
|
3325
3397
|
* offending value on stderr. Never exits non-zero: routing belongs to the transitions.
|
|
3326
3398
|
*/
|
|
3327
3399
|
export function dailySafetyVerdict(args: string[]): void {
|
|
3328
|
-
const [workDir = '', runDate = ''] = args;
|
|
3329
|
-
const
|
|
3330
|
-
const
|
|
3331
|
-
const
|
|
3332
|
-
const
|
|
3400
|
+
const [workDir = '', runDate = '', scopeDir = ''] = args;
|
|
3401
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3402
|
+
const statusFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_status.txt');
|
|
3403
|
+
const stampFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-judge_stamp.json');
|
|
3404
|
+
const decisionFile = scopeArtifact(workDir, scopeDir, runDate, 'safety-verdict.decision');
|
|
3405
|
+
const token = scopeArtifact(workDir, scopeDir, runDate, 'safety-verdict.status');
|
|
3333
3406
|
const ADMITTED = new Set(['pass', 'escalate', 'exclude']);
|
|
3334
3407
|
timed(workDir, 'safety-verdict-check', () => {
|
|
3335
3408
|
const status = isFile(statusFile) ? readFileSync(statusFile, 'utf-8').trim() : '';
|
|
@@ -3349,8 +3422,51 @@ export function dailySafetyVerdict(args: string[]): void {
|
|
|
3349
3422
|
} else if (!ADMITTED.has(verdict)) {
|
|
3350
3423
|
refusal = `kk-daily-safety.judge.decision-refused: kk-safety-verdict [${verdict}] must admit pass|escalate|exclude, so these drafts must not publish`;
|
|
3351
3424
|
}
|
|
3425
|
+
// 0331 variants scope: an admitted outcome also writes the evidence the publish matrix
|
|
3426
|
+
// freezes into the bundle — status, the exclusion record and the sha256 of every reviewed
|
|
3427
|
+
// variant's CURRENT bytes. Recomputed here (not copied from the stamp) so bytes edited
|
|
3428
|
+
// after the gate's verdict are caught at the last instant; a mismatch is a refusal, never
|
|
3429
|
+
// silently re-bound evidence.
|
|
3430
|
+
let evidence: Record<string, unknown> | null = null;
|
|
3431
|
+
if (refusal === '' && !drafts) {
|
|
3432
|
+
const stampJson = readJson(stampFile) as {
|
|
3433
|
+
digest?: unknown;
|
|
3434
|
+
status?: unknown;
|
|
3435
|
+
excludedPlatforms?: unknown;
|
|
3436
|
+
} | null;
|
|
3437
|
+
const manifest = readManifest(workDir);
|
|
3438
|
+
const hash = createHash('sha256');
|
|
3439
|
+
const variants: Record<string, string> = {};
|
|
3440
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
3441
|
+
const path = join(workDir, manifest.variants[key].path);
|
|
3442
|
+
if (!isFile(path)) {
|
|
3443
|
+
refusal = `kk-daily-safety.judge.verdict-refused: registered variant file is missing: ${path} — re-run the review on a fresh manifest`;
|
|
3444
|
+
break;
|
|
3445
|
+
}
|
|
3446
|
+
const sha = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
3447
|
+
variants[key] = sha;
|
|
3448
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
3449
|
+
}
|
|
3450
|
+
const digest = hash.digest('hex');
|
|
3451
|
+
if (refusal === '' && stampJson?.digest !== digest) {
|
|
3452
|
+
refusal = `kk-daily-safety.judge.verdict-refused: variant bytes changed after the review (reviewed ${String(stampJson?.digest ?? 'missing')}, on disk ${digest}) — re-run the variants safety review`;
|
|
3453
|
+
}
|
|
3454
|
+
if (refusal === '') {
|
|
3455
|
+
evidence = {
|
|
3456
|
+
status: stampJson?.status,
|
|
3457
|
+
digest,
|
|
3458
|
+
excludedPlatforms: Array.isArray(stampJson?.excludedPlatforms) ? stampJson.excludedPlatforms : [],
|
|
3459
|
+
variants,
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3352
3463
|
if (refusal !== '') console.error(refusal);
|
|
3353
3464
|
sted('write', token, () => writeRaw(token, refusal === '' ? 'accept' : 'refuse'));
|
|
3465
|
+
if (evidence !== null) {
|
|
3466
|
+
const evidencePath = join(scopeDirOf(workDir, scopeDir), 'safety.json');
|
|
3467
|
+
sted('write', evidencePath, () => writeJsonText(evidencePath, evidence));
|
|
3468
|
+
console.log(`safety-verdict: evidence written to ${evidencePath}`);
|
|
3469
|
+
}
|
|
3354
3470
|
});
|
|
3355
3471
|
}
|
|
3356
3472
|
|
|
@@ -3361,41 +3477,183 @@ export function dailySafetyVerdict(args: string[]): void {
|
|
|
3361
3477
|
* here fails inside the loop instead of at `wrap-docs`, after the run has moved on. A broken
|
|
3362
3478
|
* revision spends the budget too: an unparsable draft must not be retried forever.
|
|
3363
3479
|
*/
|
|
3480
|
+
// ---- 0331 scope machinery -------------------------------------------------
|
|
3481
|
+
// The nested kk-daily-safety machine serves two review surfaces: the classic drafts scope
|
|
3482
|
+
// (article + VoiceScript under 24-script) and the variants scope (final publish variants under
|
|
3483
|
+
// 30-variants, registered by daily-safety-manifest, artifacts under 4-safety-variants). The scope
|
|
3484
|
+
// is ONE trailing `scope_dir` argument: `24-script` (or empty) keeps every path byte-identical to
|
|
3485
|
+
// the pre-0331 behavior, any other value re-roots the artifact set and switches the digest to the
|
|
3486
|
+
// manifest-listed variant bytes.
|
|
3487
|
+
|
|
3488
|
+
export const SAFETY_VARIANTS_DIR = '4-safety-variants';
|
|
3489
|
+
|
|
3490
|
+
function isDraftsScope(scopeDir: string): boolean {
|
|
3491
|
+
const dir = scopeDir.trim();
|
|
3492
|
+
return dir === '' || dir === '24-script';
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
function scopeDirOf(workDir: string, scopeDir: string): string {
|
|
3496
|
+
return isDraftsScope(scopeDir) ? join(workDir, '24-script') : join(workDir, scopeDir.trim());
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
function scopeArtifact(workDir: string, scopeDir: string, runDate: string, name: string): string {
|
|
3500
|
+
return join(scopeDirOf(workDir, scopeDir), `${runDate}_09_${name}`);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
/** The remediation budget is per scope: a variants review never inherits the drafts pass count. */
|
|
3504
|
+
function scopeCounter(workDir: string, scopeDir: string): string {
|
|
3505
|
+
return isDraftsScope(scopeDir)
|
|
3506
|
+
? join(workDir, SAFETY_REVISE_COUNT)
|
|
3507
|
+
: join(workDir, `${SAFETY_REVISE_COUNT}-${scopeDir.trim().replace(/[^\w-]/g, '_')}`);
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
function readScopeCounter(workDir: string, scopeDir: string): number {
|
|
3511
|
+
const path = scopeCounter(workDir, scopeDir);
|
|
3512
|
+
if (!isFile(path)) return 0;
|
|
3513
|
+
const value = Number.parseInt(readFileSync(path, 'utf-8'), 10);
|
|
3514
|
+
return Number.isNaN(value) ? 0 : value;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
interface SafetyManifest {
|
|
3518
|
+
schemaVersion: number;
|
|
3519
|
+
runDate: string;
|
|
3520
|
+
variants: Record<string, { path: string; sha256: string }>;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
function readManifest(workDir: string): SafetyManifest {
|
|
3524
|
+
const path = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3525
|
+
const manifest = readJson(path) as SafetyManifest | null;
|
|
3526
|
+
if (manifest === null || !manifest.variants || typeof manifest.variants !== 'object') {
|
|
3527
|
+
failWith(
|
|
3528
|
+
1,
|
|
3529
|
+
`safety-variants: ${path} is missing or malformed — run "kk stage run daily-safety-manifest <work_dir> <run_date>" first`,
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
return manifest;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
/** Digest over the exact bytes under review: drafts = safetyDigest; variants = sorted manifest keys + current bytes. */
|
|
3536
|
+
function scopeDigest(workDir: string, scopeDir: string, runDate: string): string {
|
|
3537
|
+
if (isDraftsScope(scopeDir)) return safetyDigest(workDir, runDate);
|
|
3538
|
+
const manifest = readManifest(workDir);
|
|
3539
|
+
const hash = createHash('sha256');
|
|
3540
|
+
for (const key of Object.keys(manifest.variants).sort()) {
|
|
3541
|
+
const path = join(workDir, manifest.variants[key].path);
|
|
3542
|
+
if (!isFile(path)) {
|
|
3543
|
+
failWith(
|
|
3544
|
+
1,
|
|
3545
|
+
`safety-variants: registered variant file is missing: ${path} — re-run daily-bundle-variants and the manifest`,
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
hash.update(key).update('\0').update(readFileSync(path)).update('\0');
|
|
3549
|
+
}
|
|
3550
|
+
return hash.digest('hex');
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
/** The `content` array the review request hands the agents: what to read, per scope. */
|
|
3554
|
+
function scopeContent(workDir: string, scopeDir: string, runDate: string): unknown[] {
|
|
3555
|
+
if (isDraftsScope(scopeDir)) return safetyDraftPaths(workDir, runDate);
|
|
3556
|
+
return Object.entries(readManifest(workDir).variants)
|
|
3557
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
3558
|
+
.map(([key, entry]) => ({ key, path: entry.path, sha256: entry.sha256 }));
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
/**
|
|
3562
|
+
* Register the run's final publish variants for the variants-scope safety review (0331): every
|
|
3563
|
+
* `30-variants/*.content.json` becomes one manifest entry keyed by its file name with `_` read as
|
|
3564
|
+
* the plugin.operation separator. The manifest is the review contract — the request digest, the
|
|
3565
|
+
* gate's edit check and the bundle's frozen evidence all resolve through it, so an empty or
|
|
3566
|
+
* missing variants directory fails here, before any reviewer spends tokens.
|
|
3567
|
+
*/
|
|
3568
|
+
export function dailySafetyManifest(args: string[]): void {
|
|
3569
|
+
const [workDir = '', runDate = ''] = args;
|
|
3570
|
+
timed(workDir, 'safety-manifest', () => {
|
|
3571
|
+
const dir = join(workDir, '30-variants');
|
|
3572
|
+
if (!existsSync(dir)) {
|
|
3573
|
+
failWith(1, `safety-manifest: ${dir} does not exist — run the bundle-variants stage first`);
|
|
3574
|
+
}
|
|
3575
|
+
const files = readdirSync(dir)
|
|
3576
|
+
.filter((file) => file.endsWith('.content.json'))
|
|
3577
|
+
.sort();
|
|
3578
|
+
if (files.length === 0) {
|
|
3579
|
+
failWith(1, `safety-manifest: no *.content.json under ${dir} — nothing to review; was the bundle built?`);
|
|
3580
|
+
}
|
|
3581
|
+
const variants: Record<string, { path: string; sha256: string }> = {};
|
|
3582
|
+
for (const file of files) {
|
|
3583
|
+
const text = readFileSync(join(dir, file), 'utf-8');
|
|
3584
|
+
variants[file.replace(/\.content\.json$/, '').replace(/_/g, ':')] = {
|
|
3585
|
+
path: `30-variants/${file}`,
|
|
3586
|
+
sha256: createHash('sha256').update(text).digest('hex'),
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
3589
|
+
mkdirSync(join(workDir, SAFETY_VARIANTS_DIR), { recursive: true });
|
|
3590
|
+
const manifestPath = join(workDir, SAFETY_VARIANTS_DIR, 'manifest.json');
|
|
3591
|
+
sted('write', manifestPath, () => writeJsonText(manifestPath, { schemaVersion: 1, runDate, variants }));
|
|
3592
|
+
console.log(`safety-manifest: ${files.length} variant(s) registered for review`);
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3364
3596
|
/**
|
|
3365
3597
|
* Open a content-safety review of the current drafts: drop every artifact of an earlier review
|
|
3366
3598
|
* (a retry must be re-reviewed on its final version) and write the request the two agent.run
|
|
3367
3599
|
* steps read. The program passes context only — platform policy lives in the kk:content-safety
|
|
3368
|
-
* skill's reference files, never here.
|
|
3600
|
+
* skill's reference files, never here. 0331: an optional trailing scope_dir re-roots the whole
|
|
3601
|
+
* artifact set for the variants review (see the scope machinery above).
|
|
3369
3602
|
*/
|
|
3370
3603
|
export function dailySafetyPrep(args: string[]): void {
|
|
3371
|
-
const [workDir = '', runDate = '', platforms = ''] = args;
|
|
3604
|
+
const [workDir = '', runDate = '', platforms = '', scopeDir = ''] = args;
|
|
3605
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3372
3606
|
timed(workDir, 'safety-review', () => {
|
|
3373
|
-
const digest =
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3607
|
+
const digest = scopeDigest(workDir, scopeDir, runDate);
|
|
3608
|
+
if (drafts) {
|
|
3609
|
+
// Structural snapshot of the VoiceScript BEFORE any remediation edit. `daily-safety-revise`
|
|
3610
|
+
// restores it when the remediation pass changes the item-section structure without a recorded
|
|
3611
|
+
// exclusion: the agent is allowed to drop a 播报形态 line, not an item the article kept,
|
|
3612
|
+
// because `daily-wrap-docs` derives the spoken item sections from `gap_ms: 1200` and fails the
|
|
3613
|
+
// run when they no longer match the episode's included items (2026-10-21 04:30 run: 3
|
|
3614
|
+
// segments removed → 5 sections vs a 6-item plan → `generate` refused at 05:03 and the whole
|
|
3615
|
+
// day published nothing).
|
|
3616
|
+
const [, scriptPath] = safetyDraftPaths(workDir, runDate);
|
|
3617
|
+
const snapshot = safetyPath(workDir, runDate, 'safety-prep_script-snapshot.yaml');
|
|
3618
|
+
if (isFile(scriptPath))
|
|
3619
|
+
sted('write', snapshot, () => writeRaw(snapshot, readFileSync(scriptPath, 'utf-8')));
|
|
3620
|
+
} else {
|
|
3621
|
+
// The remediation budget belongs to this scope's attempt, not to the day.
|
|
3622
|
+
const counter = scopeCounter(workDir, scopeDir);
|
|
3623
|
+
sted('write', counter, () => writeFileSync(counter, '0\n'));
|
|
3624
|
+
}
|
|
3384
3625
|
for (const name of [
|
|
3385
3626
|
'safety-review_report.json',
|
|
3386
3627
|
'safety-judge_verdict.json',
|
|
3387
3628
|
'safety-judge_status.txt',
|
|
3388
3629
|
'safety-judge_stamp.json',
|
|
3630
|
+
'safety-verdict.decision',
|
|
3631
|
+
'safety-verdict.status',
|
|
3632
|
+
// A previous review's evidence must never survive into a new request: the bundle
|
|
3633
|
+
// freezes exactly what the current request's verdict admits. Evidence is the one
|
|
3634
|
+
// unprefixed artifact, so the runDate-prefixed sweep would miss it.
|
|
3635
|
+
'safety.json',
|
|
3389
3636
|
]) {
|
|
3390
|
-
const stale =
|
|
3637
|
+
const stale =
|
|
3638
|
+
name === 'safety.json' && !drafts
|
|
3639
|
+
? join(scopeDirOf(workDir, scopeDir), name)
|
|
3640
|
+
: scopeArtifact(workDir, scopeDir, runDate, name);
|
|
3391
3641
|
if (isFile(stale)) unlinkSync(stale);
|
|
3392
3642
|
}
|
|
3393
|
-
const request =
|
|
3643
|
+
const request = scopeArtifact(workDir, scopeDir, runDate, 'safety-review_request.json');
|
|
3394
3644
|
const targets = platforms
|
|
3395
3645
|
.split(',')
|
|
3396
3646
|
.map((platform) => platform.trim())
|
|
3397
3647
|
.filter(Boolean);
|
|
3398
|
-
sted('write', request, () =>
|
|
3648
|
+
sted('write', request, () =>
|
|
3649
|
+
writeJsonText(request, {
|
|
3650
|
+
digest,
|
|
3651
|
+
platforms: targets,
|
|
3652
|
+
mode: 'detect-only',
|
|
3653
|
+
scope: drafts ? 'drafts' : 'variants',
|
|
3654
|
+
content: scopeContent(workDir, scopeDir, runDate),
|
|
3655
|
+
}),
|
|
3656
|
+
);
|
|
3399
3657
|
});
|
|
3400
3658
|
}
|
|
3401
3659
|
|
|
@@ -3429,8 +3687,30 @@ export function dailySafetyPrep(args: string[]): void {
|
|
|
3429
3687
|
* revision spends the budget too: an unparsable draft must not be retried forever.
|
|
3430
3688
|
*/
|
|
3431
3689
|
export function dailySafetyRevise(args: string[]): void {
|
|
3432
|
-
const [workDir = '', runDate = ''] = args;
|
|
3690
|
+
const [workDir = '', runDate = '', scopeDir = ''] = args;
|
|
3691
|
+
const drafts = isDraftsScope(scopeDir);
|
|
3433
3692
|
timed(workDir, 'safety-revise', () => {
|
|
3693
|
+
if (!drafts) {
|
|
3694
|
+
// Variants scope: every manifest-listed file must stay parseable Content with the
|
|
3695
|
+
// fields the publish half reads; the structural VoiceScript machinery does not apply.
|
|
3696
|
+
for (const entry of Object.values(readManifest(workDir).variants)) {
|
|
3697
|
+
const path = join(workDir, entry.path);
|
|
3698
|
+
const value = requireJson(path, 'safety-revise') as {
|
|
3699
|
+
title?: unknown;
|
|
3700
|
+
body?: unknown;
|
|
3701
|
+
};
|
|
3702
|
+
for (const field of ['title', 'body'] as const) {
|
|
3703
|
+
if (typeof value[field] !== 'string' || String(value[field]).trim() === '') {
|
|
3704
|
+
failWith(1, `safety-revise: variant ${path} ${field} must be a non-empty string`);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
const counter = scopeCounter(workDir, scopeDir);
|
|
3709
|
+
const next = readScopeCounter(workDir, scopeDir) + 1;
|
|
3710
|
+
sted('write', counter, () => writeFileSync(counter, `${next}\n`));
|
|
3711
|
+
console.log(`safety-revise: remediation pass ${next} applied to manifest variants; re-validated`);
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3434
3714
|
const [articlePath, scriptPath] = safetyDraftPaths(workDir, runDate);
|
|
3435
3715
|
const article = requireJson(articlePath, 'safety-revise');
|
|
3436
3716
|
for (const field of ['title', 'body'] as const) {
|
|
@@ -3442,7 +3722,7 @@ export function dailySafetyRevise(args: string[]): void {
|
|
|
3442
3722
|
assertScriptContent(scriptPath);
|
|
3443
3723
|
restoreStructureIfBroken(workDir, runDate, scriptPath);
|
|
3444
3724
|
const path = join(workDir, SAFETY_REVISE_COUNT);
|
|
3445
|
-
const next =
|
|
3725
|
+
const next = readScopeCounter(workDir, scopeDir) + 1;
|
|
3446
3726
|
sted('write', path, () => writeFileSync(path, `${next}\n`));
|
|
3447
3727
|
console.log(`safety-revise: remediation pass ${next} applied; drafts re-validated`);
|
|
3448
3728
|
});
|
|
@@ -3945,6 +4225,23 @@ function normalizeTranslatedLocales(zhPostPath: string, failUnsafe = true): void
|
|
|
3945
4225
|
}
|
|
3946
4226
|
}
|
|
3947
4227
|
|
|
4228
|
+
/**
|
|
4229
|
+
* The translate agents run with cwd = this repo, and both executor sandboxes (codex workspace-write,
|
|
4230
|
+
* claude acceptEdits) refuse writes into the Surfdash checkout — on 2026-11-04 both exited 0 with
|
|
4231
|
+
* en/ja untouched. So they write `<runDate>_21_translate_<locale>.md` under the work dir and this
|
|
4232
|
+
* installs each over its scaffold. The output is consumed (renamed) so a rerun never clobbers an
|
|
4233
|
+
* operator's later repair of the installed post.
|
|
4234
|
+
*/
|
|
4235
|
+
function installTranslatedLocales(workDir: string, runDate: string, zhPostPath: string): void {
|
|
4236
|
+
for (const locale of ['en', 'ja'] as const) {
|
|
4237
|
+
const translated = join(workDir, '31-publish', `${runDate}_21_translate_${locale}.md`);
|
|
4238
|
+
if (!isFile(translated)) continue;
|
|
4239
|
+
const target = zhPostPath.replace('/zh/', `/${locale}/`);
|
|
4240
|
+
sted('write', target, () => writeRaw(target, readFileSync(translated, 'utf-8')));
|
|
4241
|
+
renameSync(translated, `${translated}.installed`);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
|
|
3948
4245
|
export function dailySurfdashRefine(args: string[]): void {
|
|
3949
4246
|
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
3950
4247
|
const marker = join(workDir, '31-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
|
|
@@ -3957,6 +4254,7 @@ export function dailySurfdashRefine(args: string[]): void {
|
|
|
3957
4254
|
failWith(1, `surfdash-refine: zh marker must name an existing post: ${marker}`);
|
|
3958
4255
|
}
|
|
3959
4256
|
validateLocaleMarkers(workDir, runDate, zhPostPath, 'surfdash-refine');
|
|
4257
|
+
installTranslatedLocales(workDir, runDate, zhPostPath);
|
|
3960
4258
|
// The validator is intentionally read-only. Repair known series spellings before
|
|
3961
4259
|
// site-owned refine computes managed fields, otherwise the final publisher would
|
|
3962
4260
|
// rewrite the refined bytes after the locale fingerprint was admitted.
|