@gobing-ai/knowledge-kit 0.1.5 → 0.1.7
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 +20 -1
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +3 -1
- package/plugins/generations/daily-article-gen/src/index.ts +5 -3
- package/plugins/generations/voxcpm-tts/README.md +31 -0
- package/plugins/generations/voxcpm-tts/scripts/probe-long-render.py +134 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/audio-quality.ts +635 -0
- package/plugins/kk/scripts/daily-stages.ts +51 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +25 -1
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +3 -1
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +20 -7
- package/plugins/publishings/surfdash-pub/dist/index.js +30 -3
- package/plugins/publishings/surfdash-pub/src/index.ts +33 -6
- package/plugins/publishings/surfdash-pub/src/patch.ts +4 -0
- package/plugins/kk/workflows/feature-verification.yaml +0 -89
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
coverBrief,
|
|
75
75
|
coverNormalizeBody,
|
|
76
76
|
dedupPlanByTitle,
|
|
77
|
+
expandHome,
|
|
77
78
|
fail,
|
|
78
79
|
failWith,
|
|
79
80
|
firstArtifact,
|
|
@@ -1317,7 +1318,10 @@ export function dailyIntro(args: string[]): void {
|
|
|
1317
1318
|
if (rawFile === undefined) {
|
|
1318
1319
|
failWith(1, 'intro-music: voice content carries no mp3Path/audioPath');
|
|
1319
1320
|
}
|
|
1320
|
-
|
|
1321
|
+
// The sting is an operator asset under the config root (ADR-042), not a works artifact:
|
|
1322
|
+
// workflow vars carry it as `~/.config/kk/voices/…`, and a shell does not expand `~` inside
|
|
1323
|
+
// the quoted "${vars.intro_music}" argument, so expansion happens here.
|
|
1324
|
+
const introPath = resolve(expandHome(introSrc));
|
|
1321
1325
|
if (!existsSync(introPath)) failWith(1, `intro-music: intro file not found: ${introPath}`);
|
|
1322
1326
|
const mainPath = resolve(rawFile);
|
|
1323
1327
|
if (!existsSync(mainPath)) failWith(1, `intro-music: generated audio not found: ${mainPath}`);
|
|
@@ -1981,6 +1985,52 @@ export function dailySafetyGate(args: string[]): void {
|
|
|
1981
1985
|
});
|
|
1982
1986
|
}
|
|
1983
1987
|
|
|
1988
|
+
/**
|
|
1989
|
+
* `daily-preflight <workflow.yaml>…` — assert the stage runner that `kk stage` just loaded
|
|
1990
|
+
* advertises every `kk stage <name>` these workflow definitions invoke.
|
|
1991
|
+
*
|
|
1992
|
+
* WHY (2026-10-20): the scheduler runs the CHECKOUT's workflow YAML while `kk stage` resolves the
|
|
1993
|
+
* runner from the INSTALLED package (`apps/cli/src/stage.ts`). A commit that adds a stage therefore
|
|
1994
|
+
* outruns the global install silently, and the mismatch only surfaces where the new stage is first
|
|
1995
|
+
* reached. That day's 04:30 run died 29 minutes in at `safety` with `unknown stage:
|
|
1996
|
+
* daily-safety-verdict` (present in the checkout, absent from the installed 0.1.4) and published
|
|
1997
|
+
* nothing. This runs as the first command of the run's `prepare` node, so the same skew costs one
|
|
1998
|
+
* cheap step and names both the missing stages and the refresh command.
|
|
1999
|
+
*
|
|
2000
|
+
* The known-name set is the dispatch switch of the sibling `kk-workflow-stages.ts` — the file beside
|
|
2001
|
+
* this one, i.e. the copy actually in force, never a checkout-vs-install guess.
|
|
2002
|
+
*/
|
|
2003
|
+
export function dailyPreflight(args: string[]): void {
|
|
2004
|
+
const files = args.filter((arg) => arg !== '');
|
|
2005
|
+
timed('', 'preflight', () => {
|
|
2006
|
+
if (files.length === 0) failWith(2, 'daily-preflight: pass at least one workflow YAML path');
|
|
2007
|
+
const runner = join(import.meta.dir, 'kk-workflow-stages.ts');
|
|
2008
|
+
if (!isFile(runner)) failWith(1, `daily-preflight.runner-missing: ${runner} is not beside this stage body`);
|
|
2009
|
+
const known = new Set(
|
|
2010
|
+
[...readFileSync(runner, 'utf-8').matchAll(/^\s*case '([a-z][a-z0-9-]*)':/gm)].map((match) =>
|
|
2011
|
+
String(match[1]),
|
|
2012
|
+
),
|
|
2013
|
+
);
|
|
2014
|
+
const missing: string[] = [];
|
|
2015
|
+
for (const file of files) {
|
|
2016
|
+
if (!isFile(file)) failWith(1, `daily-preflight.workflow-missing: ${file}`);
|
|
2017
|
+
for (const match of readFileSync(file, 'utf-8').matchAll(/\bkk stage ([a-z][a-z0-9-]*)/g)) {
|
|
2018
|
+
const name = String(match[1]);
|
|
2019
|
+
if (!known.has(name)) missing.push(`${name} (needed by ${basename(file)})`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (missing.length > 0) {
|
|
2023
|
+
failWith(
|
|
2024
|
+
1,
|
|
2025
|
+
`daily-preflight.runner-stale: the installed stage runner ${runner} does not know ${missing.join(', ')} — refresh it (install the released kk package, or rsync plugins/kk/scripts/ per AGENTS.md) before the scheduled run`,
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
console.log(
|
|
2029
|
+
`preflight: stage runner advertises every stage ${files.map((file) => basename(file)).join(' + ')} invokes`,
|
|
2030
|
+
);
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1984
2034
|
/**
|
|
1985
2035
|
* Task 0317 R6 — fold the gate's own classification, its stamp and the `kk-safety-verdict` catalog
|
|
1986
2036
|
* decision into ONE accept/refuse token the `judge -> clean` guard reads as a single predicate
|
|
@@ -159,6 +159,20 @@ async function loadDailyStages(): Promise<typeof import('./daily-stages.ts')> {
|
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Load the audio-quality diagnostic lazily: it shells out to ffprobe/ffmpeg and is only needed when an
|
|
164
|
+
* operator asks for it, so the runner must not pay for it on every stage (and an install without
|
|
165
|
+
* ffmpeg on PATH still runs every other stage).
|
|
166
|
+
*/
|
|
167
|
+
async function loadAudioQuality(): Promise<typeof import('./audio-quality.ts')> {
|
|
168
|
+
try {
|
|
169
|
+
return await import('./audio-quality.ts');
|
|
170
|
+
} catch {
|
|
171
|
+
return fail(
|
|
172
|
+
'audio-quality.ts not found next to kk-workflow-stages.ts — install or refresh the kk package (bun link in a checkout)',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
162
176
|
export const ERRNO_REASONS: Record<string, string> = {
|
|
163
177
|
EACCES: 'Permission denied',
|
|
164
178
|
EROFS: 'Read-only file system',
|
|
@@ -737,7 +751,7 @@ function formatRunDate(): string {
|
|
|
737
751
|
}
|
|
738
752
|
|
|
739
753
|
/** Expand the `~/` and `$HOME/` prefixes the config examples use. */
|
|
740
|
-
function expandHome(p: string): string {
|
|
754
|
+
export function expandHome(p: string): string {
|
|
741
755
|
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
742
756
|
if (p.startsWith('$HOME/')) return join(homedir(), p.slice('$HOME/'.length));
|
|
743
757
|
return p;
|
|
@@ -1638,6 +1652,16 @@ if (import.meta.main)
|
|
|
1638
1652
|
dailyScript(rest);
|
|
1639
1653
|
break;
|
|
1640
1654
|
}
|
|
1655
|
+
case 'audio-quality': {
|
|
1656
|
+
const { runAudioQuality } = await loadAudioQuality();
|
|
1657
|
+
await runAudioQuality(rest);
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
case 'daily-preflight': {
|
|
1661
|
+
const { dailyPreflight } = await loadDailyStages();
|
|
1662
|
+
dailyPreflight(rest);
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1641
1665
|
case 'daily-safety-prep': {
|
|
1642
1666
|
const { dailySafetyPrep } = await loadDailyStages();
|
|
1643
1667
|
dailySafetyPrep(rest);
|
|
@@ -30,7 +30,9 @@ vars:
|
|
|
30
30
|
# env block; smoke render 6.8s). Flip back with vdriver="voicebox".
|
|
31
31
|
vdriver: "gemini"
|
|
32
32
|
# 20260917 review #7: 8s intro sting prepended to every daily output; empty = skip.
|
|
33
|
-
|
|
33
|
+
# The sting is an operator asset: it lives under the config root (ADR-042), never in the works
|
|
34
|
+
# tree. `~` is expanded by the daily-intro stage, so it stays machine-portable.
|
|
35
|
+
intro_music: "~/.config/kk/voices/AI每日资讯_8秒播客片头.mp3"
|
|
34
36
|
# 20260917 review #4/#6: surfdash checkout the patch stage commits to.
|
|
35
37
|
surfdash_root: "/Users/robin/xprojects/surfdash"
|
|
36
38
|
title: ""
|
|
@@ -25,11 +25,17 @@ vars:
|
|
|
25
25
|
# Voice driver switch — voxcpm (voxcpm-tts) | gemini (gemini-tts) | voicebox (voice-gen)
|
|
26
26
|
# | ominivoice (omni-voice-gen). The generate tier below mirrors the fallback order, and the
|
|
27
27
|
# fit/duration gates take their spoken-char rate from this var — switching drivers here is enough.
|
|
28
|
-
# 2026-10-
|
|
29
|
-
#
|
|
30
|
-
#
|
|
28
|
+
# 2026-10-20 (operator decision): voxcpm STAYS the primary. It is audibly echoey - a 40 ms periodic
|
|
29
|
+
# reflection on renders longer than ~30 s plus a pause floor only 16-22 dB below speech, measured by
|
|
30
|
+
# `kk stage audio-quality` on every episode's raw model output since 10-15 (cepstrum 40 ms @ 26-31 dB
|
|
31
|
+
# / pauseFloor -16..-22 dB, versus gemini's 20-25 ms @ 19-20 dB / -34..-37 dB) - and no exposed knob
|
|
32
|
+
# (cfg_value, inference_timesteps, normalize, denoise) or usable checkpoint fixes it (VoxCPM1.5:
|
|
33
|
+
# 80 ms @ 31 dB; VoxCPM-0.5B loses the lattice but measures -15.5 dB at 16 kHz). The operator keeps
|
|
34
|
+
# the local driver for its speed and cost and will monitor it episode by episode until a proper fix
|
|
35
|
+
# exists; flipping this one var to "gemini" is the escape hatch (measured dry: 20-25 ms / -34..-37 dB)
|
|
36
|
+
# at the price of a 1h47m-2h21m remote render.
|
|
31
37
|
vdriver: "voxcpm"
|
|
32
|
-
intro_music: "
|
|
38
|
+
intro_music: "~/.config/kk/voices/AI每日资讯_8秒播客片头.mp3"
|
|
33
39
|
surfdash_root: "/Users/robin/xprojects/surfdash"
|
|
34
40
|
title: ""
|
|
35
41
|
skip_review: "false"
|
|
@@ -113,11 +119,18 @@ nodes:
|
|
|
113
119
|
the artifact directories and the safety revise budget. `kk ledger sync` records runs that
|
|
114
120
|
already finished without a ledger row (failed ones included) — a fail-fast DAG never reaches
|
|
115
121
|
its own `ledger` node, so the next run's prepare is the backfill (design §7).
|
|
122
|
+
The `daily-preflight` stage runs FIRST: the scheduler executes this checkout's YAML while
|
|
123
|
+
`kk stage` resolves its stage runner from the installed package, so a run whose YAML names a
|
|
124
|
+
stage the installed runner lacks must fail here, naming the stage, instead of 29 minutes later
|
|
125
|
+
inside the nested safety machine (2026-10-20: `unknown stage: daily-safety-verdict`, nothing
|
|
126
|
+
published).
|
|
116
127
|
action:
|
|
117
128
|
kind: shell
|
|
118
129
|
options:
|
|
119
130
|
command: >-
|
|
120
|
-
kk daily
|
|
131
|
+
kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
|
|
132
|
+
plugins/kk/workflows/kk-daily-safety.yaml
|
|
133
|
+
&& kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
|
|
121
134
|
--run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
|
|
122
135
|
--setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
|
|
123
136
|
--setting skip_review="${vars.skip_review}" --setting transcode_mp3="${vars.transcode_mp3}"
|
|
@@ -548,7 +561,7 @@ nodes:
|
|
|
548
561
|
kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
|
|
549
562
|
|
|
550
563
|
- id: generate
|
|
551
|
-
description: "Voice tier, primary driver (vars.vdriver — voxcpm
|
|
564
|
+
description: "Voice tier, primary driver (vars.vdriver — voxcpm; its 40 ms lattice is known and monitored, see the vdriver comment). onError: continue — the fallback siblings below are the loud chain (same pattern as 0238 R2)."
|
|
552
565
|
# wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
|
|
553
566
|
# as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
|
|
554
567
|
dependsOn: [script, safety, review, wrap-docs]
|
|
@@ -570,7 +583,7 @@ nodes:
|
|
|
570
583
|
# its plugin id and the cache ignores foreign-driver sections, so a failed attempt can neither be
|
|
571
584
|
# relabeled nor mix voices). Non-final siblings continue on error; the last one is loud.
|
|
572
585
|
- id: generate-fallback
|
|
573
|
-
description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content"
|
|
586
|
+
description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content — the measured-dry escape hatch from voxcpm's lattice"
|
|
574
587
|
dependsOn: [generate]
|
|
575
588
|
condition:
|
|
576
589
|
kind: shell
|
|
@@ -12423,6 +12423,10 @@ ${crossLinkFor(locale, request.dateIso, request.showNotesUrl)}
|
|
|
12423
12423
|
if (md !== before) {
|
|
12424
12424
|
await fs.writeFile(indexMd, md);
|
|
12425
12425
|
written.push(indexMd);
|
|
12426
|
+
const assetsDir = join(dir, "assets");
|
|
12427
|
+
if (await fs.exists(assetsDir) && !written.includes(join(assetsDir, "cover.png"))) {
|
|
12428
|
+
written.push(assetsDir);
|
|
12429
|
+
}
|
|
12426
12430
|
changed += 1;
|
|
12427
12431
|
}
|
|
12428
12432
|
}
|
|
@@ -12437,6 +12441,14 @@ var COVER_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
|
12437
12441
|
function slugify2(str) {
|
|
12438
12442
|
return str.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
|
12439
12443
|
}
|
|
12444
|
+
function normalizeTag(raw) {
|
|
12445
|
+
const match = raw.match(/^\s*sub-category\s*:(.*)$/i);
|
|
12446
|
+
if (match) {
|
|
12447
|
+
const subSlug = slugify2(match[1] ?? "");
|
|
12448
|
+
return subSlug ? `sub-category:${subSlug}` : "";
|
|
12449
|
+
}
|
|
12450
|
+
return slugify2(raw);
|
|
12451
|
+
}
|
|
12440
12452
|
function mapContentToSurfingMarkdown(content, options = {}) {
|
|
12441
12453
|
let title = content.title;
|
|
12442
12454
|
if (!title) {
|
|
@@ -12452,9 +12464,13 @@ function mapContentToSurfingMarkdown(content, options = {}) {
|
|
|
12452
12464
|
description = paragraphs[0];
|
|
12453
12465
|
}
|
|
12454
12466
|
}
|
|
12455
|
-
const rawTags = Array.isArray(content.metadata?.tags) ? content.metadata.tags : Array.isArray(content.metadata?.keywords) ? content.metadata.keywords : [];
|
|
12467
|
+
const rawTags = Array.isArray(content.metadata?.tags) ? [...content.metadata.tags] : Array.isArray(content.metadata?.keywords) ? [...content.metadata.keywords] : [];
|
|
12468
|
+
if (typeof content.metadata?.subCategory === "string" && content.metadata.subCategory.trim().length > 0) {
|
|
12469
|
+
const subRaw = content.metadata.subCategory.trim();
|
|
12470
|
+
rawTags.push(/^sub-category\s*:/i.test(subRaw) ? subRaw : `sub-category:${subRaw}`);
|
|
12471
|
+
}
|
|
12456
12472
|
const tags = [
|
|
12457
|
-
...new Set(rawTags.filter((t) => typeof t === "string" && t.trim().length > 0).map((t) =>
|
|
12473
|
+
...new Set(rawTags.filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => normalizeTag(t)))
|
|
12458
12474
|
].filter((tag) => tag.length > 0);
|
|
12459
12475
|
const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
|
|
12460
12476
|
const firstRefUrl = content.references?.[0]?.url;
|
|
@@ -12743,7 +12759,17 @@ async function patchPost(options) {
|
|
|
12743
12759
|
if (outcome.changed === 0)
|
|
12744
12760
|
echoError(`patch: no post bytes changed for ${outcome.slug}`);
|
|
12745
12761
|
const synced = await syncPost({ postPath: postPath.postPath });
|
|
12746
|
-
const
|
|
12762
|
+
const commitPaths = new Set(outcome.written.map((path) => relative(surfdashRoot, path)));
|
|
12763
|
+
for (const locale of PATCH_LOCALES) {
|
|
12764
|
+
const indexMd = localeIndexPath(postPath.postPath, locale);
|
|
12765
|
+
if (!indexMd)
|
|
12766
|
+
continue;
|
|
12767
|
+
const assetsDir = join2(dirname3(indexMd), "assets");
|
|
12768
|
+
if (existsSync2(assetsDir)) {
|
|
12769
|
+
commitPaths.add(relative(surfdashRoot, assetsDir));
|
|
12770
|
+
}
|
|
12771
|
+
}
|
|
12772
|
+
const commit = await commitPostPaths(surfdashRoot, [...commitPaths], `feat: cover + podcast cross-link for ${outcome.slug} (kk daily patch)`, process.env.SURFDASH_AUTO_PUSH !== "0");
|
|
12747
12773
|
if (!commit.ok)
|
|
12748
12774
|
return fail(`patch: ${commit.error}`);
|
|
12749
12775
|
const metadata = {
|
|
@@ -12922,6 +12948,7 @@ export {
|
|
|
12922
12948
|
main2 as main,
|
|
12923
12949
|
mapContentToSurfingMarkdown,
|
|
12924
12950
|
normalizeLocaleFrontmatter,
|
|
12951
|
+
normalizeTag,
|
|
12925
12952
|
processPublishIO,
|
|
12926
12953
|
pushPostGit,
|
|
12927
12954
|
slugify2 as slugify,
|
|
@@ -6,7 +6,7 @@ import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-
|
|
|
6
6
|
import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
7
7
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
9
|
-
import { type PatchPostOutcome, patchPostFiles } from './patch';
|
|
9
|
+
import { localeIndexPath, PATCH_LOCALES, type PatchPostOutcome, patchPostFiles } from './patch';
|
|
10
10
|
|
|
11
11
|
/** Result target name for this plugin. */
|
|
12
12
|
const TARGET = 'surfdash';
|
|
@@ -62,6 +62,19 @@ export function slugify(str: string): string {
|
|
|
62
62
|
.replace(/[\s_]+/g, '-');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Normalizes a raw tag string for frontmatter: preserves `sub-category:<slug>` prefixes
|
|
67
|
+
* while slugifying the value portion (or ordinary tags).
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeTag(raw: string): string {
|
|
70
|
+
const match = raw.match(/^\s*sub-category\s*:(.*)$/i);
|
|
71
|
+
if (match) {
|
|
72
|
+
const subSlug = slugify(match[1] ?? '');
|
|
73
|
+
return subSlug ? `sub-category:${subSlug}` : '';
|
|
74
|
+
}
|
|
75
|
+
return slugify(raw);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Maps Content object into Surfing frontmatter and body per the publish spec.
|
|
67
80
|
*/
|
|
@@ -86,18 +99,23 @@ export function mapContentToSurfingMarkdown(content: Content, options: MapConten
|
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
const rawTags: unknown[] = Array.isArray(content.metadata?.tags)
|
|
89
|
-
? content.metadata.tags
|
|
102
|
+
? [...content.metadata.tags]
|
|
90
103
|
: Array.isArray(content.metadata?.keywords)
|
|
91
|
-
? content.metadata.keywords
|
|
104
|
+
? [...content.metadata.keywords]
|
|
92
105
|
: [];
|
|
106
|
+
if (typeof content.metadata?.subCategory === 'string' && content.metadata.subCategory.trim().length > 0) {
|
|
107
|
+
const subRaw = content.metadata.subCategory.trim();
|
|
108
|
+
rawTags.push(/^sub-category\s*:/i.test(subRaw) ? subRaw : `sub-category:${subRaw}`);
|
|
109
|
+
}
|
|
93
110
|
// `slugify` drops non-ASCII, so a Chinese tag (`硬件发布`) collapsed to '' and shipped as an empty
|
|
94
111
|
// YAML tag — the site renders it as a blank chip (seen on 20261011-ai-2026-10-11). Case variants
|
|
95
|
-
// also collapsed to the same slug twice (`AI` + `ai`), so the set dedupes.
|
|
112
|
+
// also collapsed to the same slug twice (`AI` + `ai`), so the set dedupes. `normalizeTag` preserves
|
|
113
|
+
// `sub-category:<slug>` colon prefixes so sub-category series tags survive into frontmatter.
|
|
96
114
|
const tags = [
|
|
97
115
|
...new Set(
|
|
98
116
|
rawTags
|
|
99
117
|
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
100
|
-
.map((t: string) =>
|
|
118
|
+
.map((t: string) => normalizeTag(t)),
|
|
101
119
|
),
|
|
102
120
|
].filter((tag) => tag.length > 0);
|
|
103
121
|
|
|
@@ -519,9 +537,18 @@ async function patchPost(options: SurfdashPubOptions): Promise<Result> {
|
|
|
519
537
|
}
|
|
520
538
|
if (outcome.changed === 0) echoError(`patch: no post bytes changed for ${outcome.slug}`);
|
|
521
539
|
const synced = await syncPost({ postPath: postPath.postPath });
|
|
540
|
+
const commitPaths = new Set(outcome.written.map((path) => relative(surfdashRoot, path)));
|
|
541
|
+
for (const locale of PATCH_LOCALES) {
|
|
542
|
+
const indexMd = localeIndexPath(postPath.postPath, locale);
|
|
543
|
+
if (!indexMd) continue;
|
|
544
|
+
const assetsDir = join(dirname(indexMd), 'assets');
|
|
545
|
+
if (existsSync(assetsDir)) {
|
|
546
|
+
commitPaths.add(relative(surfdashRoot, assetsDir));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
522
549
|
const commit = await commitPostPaths(
|
|
523
550
|
surfdashRoot,
|
|
524
|
-
|
|
551
|
+
[...commitPaths],
|
|
525
552
|
`feat: cover + podcast cross-link for ${outcome.slug} (kk daily patch)`,
|
|
526
553
|
process.env.SURFDASH_AUTO_PUSH !== '0',
|
|
527
554
|
);
|
|
@@ -137,6 +137,10 @@ export async function patchPostFiles(request: PatchPostRequest): Promise<PatchPo
|
|
|
137
137
|
if (md !== before) {
|
|
138
138
|
await fs.writeFile(indexMd, md);
|
|
139
139
|
written.push(indexMd);
|
|
140
|
+
const assetsDir = join(dir, 'assets');
|
|
141
|
+
if ((await fs.exists(assetsDir)) && !written.includes(join(assetsDir, 'cover.png'))) {
|
|
142
|
+
written.push(assetsDir);
|
|
143
|
+
}
|
|
140
144
|
changed += 1;
|
|
141
145
|
}
|
|
142
146
|
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Feature-scoped verification pass (ADR-119, feature D62, task 0872; D63 task
|
|
2
|
-
# 0915 remediation folds the evidence contract into a workflow-owned script).
|
|
3
|
-
#
|
|
4
|
-
# Owns a lifecycle boundary no per-task graph owns: the repo-wide checks whose
|
|
5
|
-
# invariant spans the repository rather than a single task's diff. They run
|
|
6
|
-
# once per feature, deliberately the slow pass; `feature-lifecycle`'s verifying
|
|
7
|
-
# entry invokes this workflow (task 0880 caller wiring), and its verifying→done
|
|
8
|
-
# guard refuses to complete the feature until this pass records PASS (ADR-119
|
|
9
|
-
# consequence: a feature is not done until it passes).
|
|
10
|
-
#
|
|
11
|
-
# Run: spur workflow run feature-verification.yaml --vars '{"featureId":"D63"}'
|
|
12
|
-
#
|
|
13
|
-
# No project-side override is required (task 1185, A33). Since the pass moved into the
|
|
14
|
-
# application (1176 / efd78f058) the bundled definition owns it end to end, so a
|
|
15
|
-
# project-layer copy of this file (plus a rooted port under `.spur/scripts/`) is a
|
|
16
|
-
# leftover — the port was written against an entry export contract that has since
|
|
17
|
-
# changed. Remove the override so this definition applies, or migrate the port to the
|
|
18
|
-
# current app-owned operation `runFeatureVerification` (the generated inline bundle's
|
|
19
|
-
# export). If a stale consumer is still reached, feature-lifecycle's `verifying` entry
|
|
20
|
-
# names it: the override path, the consumer script, the export it requires that the
|
|
21
|
-
# shipped entry no longer provides, and the entry path. Reinstalling or upgrading the
|
|
22
|
-
# plugin cannot supply a retired export — only the two remedies above can.
|
|
23
|
-
#
|
|
24
|
-
# Shape: verify -> done | failed. The onEnter step runs `spur feature verify`
|
|
25
|
-
# (D63 task 0915; script-absorption PR-5 retired the feature-verification-steps
|
|
26
|
-
# plugin script): it
|
|
27
|
-
# resolves this very definition for the verifier identity (name, source path,
|
|
28
|
-
# layer, definition digest), records the RUNNING receipt (run-scoped +
|
|
29
|
-
# feature-latest copies), registers the run-scoped receipt as a run artifact,
|
|
30
|
-
# runs the configured verification command via the safe launch splitter, and
|
|
31
|
-
# completes the receipt PASS/FAIL with the after-pass proof-input digest. A
|
|
32
|
-
# changed tree during the pass records FAIL.
|
|
33
|
-
#
|
|
34
|
-
# Reliability (aligned with task-pipeline / ADR-043): no agent.run nodes; the
|
|
35
|
-
# verification command comes from `vars.verificationCmd` below and is never
|
|
36
|
-
# interpolated through a shell. The always-exit-0 onEnter means missing,
|
|
37
|
-
# corrupt or FAIL evidence routes to `failed` via the guard (fail-closed),
|
|
38
|
-
# never a silent PASS; infrastructure errors exit nonzero and surface.
|
|
39
|
-
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
40
|
-
kind: state-machine
|
|
41
|
-
name: feature-verification
|
|
42
|
-
version: "1"
|
|
43
|
-
description: "Feature-scoped verification pass (ADR-119): runs the repo-wide check set once per feature and records a bound PASS/FAIL evidence receipt. Invoked by feature-lifecycle's verifying entry."
|
|
44
|
-
iterationBound: 2
|
|
45
|
-
initialState: verify
|
|
46
|
-
terminalStates:
|
|
47
|
-
- done
|
|
48
|
-
- failed
|
|
49
|
-
failureStates:
|
|
50
|
-
- failed
|
|
51
|
-
vars:
|
|
52
|
-
featureId: "X"
|
|
53
|
-
spurBin: "spur"
|
|
54
|
-
verificationCmd: "bun run spur-check"
|
|
55
|
-
__runId: ""
|
|
56
|
-
|
|
57
|
-
states:
|
|
58
|
-
- id: verify
|
|
59
|
-
description: >
|
|
60
|
-
Run the repo-wide check set via `spur feature verify`, which
|
|
61
|
-
records the bound evidence receipt (verifier identity, checked-input
|
|
62
|
-
digest, run identity) and the coarse status file, always exit 0 so the
|
|
63
|
-
transition guard routes missing/corrupt/FAIL to `failed`.
|
|
64
|
-
onEnter:
|
|
65
|
-
- kind: shell
|
|
66
|
-
options:
|
|
67
|
-
command: >-
|
|
68
|
-
mkdir -p .spur/run; $spurBin feature verify "$featureId" --run-id "$__runId" --no-logo || printf 'FAIL feature-verification failed closed — spur feature verify exited nonzero\n' > ".spur/run/$__runId-feature-verification.status"; exit 0
|
|
69
|
-
|
|
70
|
-
- id: done
|
|
71
|
-
description: Terminal — repo-wide checks passed and the receipt is bound.
|
|
72
|
-
|
|
73
|
-
- id: failed
|
|
74
|
-
description: Terminal — repo-wide checks failed or the evidence is missing/corrupt.
|
|
75
|
-
|
|
76
|
-
transitions:
|
|
77
|
-
- from: verify
|
|
78
|
-
to: done
|
|
79
|
-
description: Repo-wide checks passed — `spur feature verify` completed the receipt with PASS.
|
|
80
|
-
guard:
|
|
81
|
-
kind: shell
|
|
82
|
-
options:
|
|
83
|
-
command: 'test "$(cat .spur/run/$__runId-feature-verification.status 2>/dev/null)" = PASS'
|
|
84
|
-
- from: verify
|
|
85
|
-
to: failed
|
|
86
|
-
terminalReason: failed-check
|
|
87
|
-
description: Repo-wide checks failed (or the evidence is missing/corrupt) — stop at failed.
|
|
88
|
-
guard:
|
|
89
|
-
kind: always
|