@gobing-ai/knowledge-kit 0.0.37 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +654 -16
  3. package/dist/board/collect/index.js +191 -64
  4. package/dist/board/daily-voice/index.css +679 -12
  5. package/dist/board/daily-voice/index.js +185 -66
  6. package/dist/board/itc/index.css +679 -13
  7. package/dist/board/itc/index.js +188 -66
  8. package/dist/index.js +2864 -139
  9. package/package.json +3 -2
  10. package/plugins/generations/content-gen/dist/index.js +158 -12
  11. package/plugins/generations/core-facts-gen/dist/index.js +158 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +158 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +158 -12
  14. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  15. package/plugins/generations/gemini-tts/README.md +4 -3
  16. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  17. package/plugins/generations/image-gen/dist/index.js +158 -12
  18. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  19. package/plugins/generations/omni-voice-gen/README.md +15 -4
  20. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  21. package/plugins/generations/voice-gen/dist/index.js +158 -12
  22. package/plugins/generations/voxcpm-tts/README.md +21 -1
  23. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  24. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  25. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  26. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  27. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/web-search/dist/index.js +158 -12
  30. package/plugins/kk/commands/workflow-run.md +56 -18
  31. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  32. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  33. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  34. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  35. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  36. package/plugins/kk/plugin.json +1 -1
  37. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  38. package/plugins/kk/publish-presets/technical-article.json +14 -0
  39. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  40. package/plugins/kk/scripts/daily-stages.ts +42 -2
  41. package/plugins/kk/scripts/itc-stages.ts +25 -118
  42. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  43. package/plugins/kk/scripts/seo-decisions.ts +491 -0
  44. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  45. package/plugins/kk/scripts/seo-facts.ts +610 -0
  46. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  47. package/plugins/kk/scripts/seo-report.ts +460 -0
  48. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  49. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  50. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  51. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  52. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  53. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  54. package/plugins/kk/skills/smart-seo/references/operations.md +209 -0
  55. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  56. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  57. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
  58. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  59. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  60. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  61. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  62. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  63. package/plugins/publishings/bilibili-pub/package.json +20 -0
  64. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  65. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  66. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  67. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  68. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  69. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  70. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  71. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  72. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  73. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  74. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  75. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  76. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  77. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  78. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  79. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  80. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  81. package/plugins/publishings/csdn-pub/package.json +19 -0
  82. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  83. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  84. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  85. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  86. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  87. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  88. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  89. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  90. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  91. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  92. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  93. package/plugins/publishings/jike-pub/package.json +19 -0
  94. package/plugins/publishings/jike-pub/plugin.json +12 -0
  95. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  96. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  97. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  99. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  100. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  101. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  102. package/plugins/publishings/medium-pub/plugin.json +22 -1
  103. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  104. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  105. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  106. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  107. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  108. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  109. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  110. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  111. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  112. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  113. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  114. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  115. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  116. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  117. package/plugins/publishings/sspai-pub/package.json +19 -0
  118. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  119. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  120. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  121. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  122. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  123. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  125. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  126. package/plugins/publishings/substack-pub/plugin.json +21 -0
  127. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  128. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  129. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  130. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  131. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  132. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  133. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  134. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  136. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  137. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  138. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  139. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  140. package/plugins/publishings/x-pub/dist/index.js +160 -12
  141. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  142. package/plugins/publishings/x-pub/plugin.json +40 -0
  143. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  144. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  145. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  146. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  147. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  155. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  156. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  157. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -0,0 +1,209 @@
1
+ # Smart SEO — operations
2
+
3
+ Setup, run, progress, and failure handling for the `kk-smart-seo` audit. The workflow is the
4
+ orchestrator; `kk` owns every step (ADR-007). Nothing here mutates a site.
5
+
6
+ ## 1. Resolve the KK package root
7
+
8
+ Both the stages and the decision catalogs live in the **installed** package, not in a checkout:
9
+
10
+ ```bash
11
+ # The real path of the executable that is actually running (follows symlinks):
12
+ readlink -f "$(command -v kk)" # -> <pkg>/dist/index.js
13
+ PKG_ROOT="$(dirname "$(dirname "$(readlink -f "$(command -v kk)")")")"
14
+ ```
15
+
16
+ `kk stage run` resolves `plugins/kk/scripts/kk-workflow-stages.ts` next to its own entry: a real
17
+ checkout uses the checked-in runner, an installed entry always uses its own package copy. An
18
+ unrelated ancestor `plugins/` directory is never selected, so a run from an unrelated cwd is safe.
19
+
20
+ ## 2. Register the SEO decision catalogs (required for the integrated workflow)
21
+
22
+ `decisions.paths` in the consumer project config (`.spur/config.yaml`) is the whole search path.
23
+ **Append** one entry — never replace the list:
24
+
25
+ ```yaml
26
+ decisions:
27
+ paths:
28
+ - <existing entry> # keep every existing entry
29
+ - <PKG_ROOT>/plugins/kk/decisions # appended: the 4 SEO catalogs, 22 decisions
30
+ maker: <existing maker> # keep the maker and makers: mapping untouched
31
+ ```
32
+
33
+ `seo-preflight` with `require_decisions=true` (the workflow's own call) fails with this pointer
34
+ when the catalogs are not served:
35
+
36
+ ```text
37
+ seo-preflight: SEO decision catalogs are not served — missing catalog file(s): kk-seo-site.yaml, ...
38
+ Registration fix: append `<kk-package-root>/plugins/kk/decisions` to the project config `decisions.paths`
39
+ (.spur/config.yaml), preserving every existing entry and maker setting, then re-run preflight.
40
+ ```
41
+
42
+ Verify without dispatching a maker (configuration evidence, never model health):
43
+
44
+ ```bash
45
+ spur decision list --json # must serve the 22 kk-seo-* ids
46
+ spur decision show kk-seo-page-type --json # -> catalog, layer, effective maker + source
47
+ spur decision status --json # -> load errors, duplicate ids, per-decision effective maker
48
+ ```
49
+
50
+ Collection-only stage use (`require_decisions=false`) and the explicit offline fixture mode need
51
+ no catalogs. Do not run an audit that silently writes user-global configuration; the append above
52
+ is a one-time operator setup, and it is repeated only when an upgrade changes the resolved
53
+ package root.
54
+
55
+ Every decision in those catalogs declares exactly two parameters — `subject_id` and `locale`
56
+ (plus `candidates` on `kk-seo-keyword-page`). Spur treats a declared parameter with no default as
57
+ required and rejects an undeclared one, so a direct probe like `spur decision run
58
+ kk-seo-page-type --param subject_id=…` is an input rejection before any maker runs, not a
59
+ judgment. Pass both parameters, and never invent a third (`url`, for example, is carried inside
60
+ the compact evidence packets, not as a parameter).
61
+
62
+ **The catalogs' declared schema must resolve from where they live.** Each shipped SEO catalog
63
+ declares `$schema: "@gobing-ai/spur/schemas/decision-catalog.schema.json"`, and Spur resolves that
64
+ schema through the module tree above the catalog's own directory. A global install satisfies this
65
+ by construction (both packages are siblings in the global tree); with a project-local KK install,
66
+ keep `spur` resolvable from the installed package's location or `decision status --json` reports
67
+ the catalogs as `loadErrors` (`Cannot resolve package schema …`) and preflight fails. A load error
68
+ is a setup defect to fix, never a reason to fall back to a maker default silently.
69
+
70
+ ## 3. Options file
71
+
72
+ Write a UTF-8 JSON object (≤ 64 KiB, strict schema — unknown keys are refused) and pass its path
73
+ as data, never as shell source:
74
+
75
+ ```json
76
+ {
77
+ "url": "https://example.com",
78
+ "locale": "en-US",
79
+ "business_context": "",
80
+ "max_pages": 60,
81
+ "max_depth": 5,
82
+ "max_requests": 200,
83
+ "deadline_seconds": 600,
84
+ "semantic_pages": 20,
85
+ "max_pairs": 10,
86
+ "max_keywords": 10,
87
+ "max_decision_calls": 320,
88
+ "performance": false,
89
+ "visibility_file": "",
90
+ "fixture": ""
91
+ }
92
+ ```
93
+
94
+ | Field | Meaning and bounds |
95
+ | --- | --- |
96
+ | `url` | Required HTTP(S) URL or bare domain (bare becomes HTTPS). Userinfo, nonstandard ports, local/reserved addresses and malformed hostnames are refused. |
97
+ | `locale` | BCP-47; describes the report audience and the imported query market. It never assumes the site's language. |
98
+ | `business_context` | Optional operator context ≤ 2,000 UTF-16 units. It is context, not website evidence. |
99
+ | `max_pages` / `max_depth` | 60 / 5 by default; maxima 200 / 10. The page quota counts every attempt, including blocked, redirect and error records. |
100
+ | `max_requests` / `deadline_seconds` | 200 / 600 by default; maxima 500 / 1800. One shared quota and deadline covers robots, sitemaps, pages, probes and link targets. |
101
+ | `semantic_pages` / `max_pairs` / `max_keywords` | 20 / 10 / 10 by default; maxima 60 / 40 / 30. Selection is deterministic (homepage, observed depth/inlinks, URL tie break). |
102
+ | `max_decision_calls` | 320 by default, maximum 1000. Every attempt consumes one; there is no retry on an unknown-charge call. Exhaustion is an explicitly partial assessment. |
103
+ | `performance` | `false` by default. `true` requests PSI mobile/desktop and CrUX for at most 3 selected pages; the stage shares a 300 s deadline and a 12-call cap. |
104
+ | `visibility_file` | Optional normalized local JSON snapshot (§4). Never an arbitrary provider dump. |
105
+ | `fixture` | Explicit offline fixture **directory**. With a fixture, network access and real maker dispatch are forbidden. Production never falls back to fixture data. |
106
+
107
+ ## 4. Audit scope — the submitted host plus its exact `www` toggle
108
+
109
+ Scope is the normalized submitted hostname with **one** leading `www.` removed when present, or
110
+ added when absent — nothing else. `shop.example.com` admits `shop.example.com` and
111
+ `www.shop.example.com` only: no registrable apex, no sibling subdomains, and default ports stay
112
+ origin-specific. Off-scope redirects and links are recorded, never followed; `www`/non-`www` and
113
+ HTTP/HTTPS transitions are observations, not defects. There is no browser rendering and no outbound
114
+ external-link fetching in this version: state that assessment as **not collected** rather than
115
+ assuming a result.
116
+
117
+ ## 5. Imported visibility snapshot
118
+ ```json
119
+ {
120
+ "schemaVersion": 1,
121
+ "siteOrigin": "https://example.com",
122
+ "locale": "en-US",
123
+ "source": "search-console-export|analytics-export|manual",
124
+ "collectedAt": "2026-10-01T00:00:00Z",
125
+ "measurementKind": "measured",
126
+ "rows": [{ "query": "example query", "url": "https://example.com/a", "clicks": 4, "impressions": 120, "position": 8.5, "citations": 2, "sampleCount": 500, "platform": "chatgpt" }]
127
+ }
128
+ ```
129
+
130
+ Maximum 1 MiB / 500 rows; strings bounded; counts are finite nonnegative numbers; `position` ≥ 1;
131
+ `citations` ≤ `sampleCount` with `platform` required; any `url` must belong to the audit scope.
132
+ Unknown properties, a conflicting locale or site identity, a future timestamp, or evidence older
133
+ than 90 days fails validation before use. Keywords come from this data — never invented volumes.
134
+
135
+ ## 6. Run the workflow
136
+
137
+ ```bash
138
+ spur workflow run <PKG_ROOT>/plugins/kk/workflows/kk-smart-seo.yaml \
139
+ --vars '{"options_file":"/abs/path/options.json"}' --quiet
140
+ ```
141
+
142
+ Leave `work_dir` empty: `seo-preflight` allocates `.works/kk-smart-seo/<UTC-stamp>-<runId>` with
143
+ create-new semantics (an existing non-empty directory is refused) and writes the resolved absolute
144
+ path to `.spur/run/<runId>-seo-work-dir.txt`, which the workflow reads back into `work_dir` before
145
+ collection. A fixed `work_dir` must name an absent or empty directory.
146
+
147
+ Stages: `preflight → collect → check → enrich → judge → score → narrative → report`, terminal
148
+ `done | partial | failed`. `done` means every requested stage assessed its eligible evidence — not
149
+ that the site has no SEO issues. `partial` is successful artifact delivery with missing requested
150
+ evidence (an unavailable optional PSI/CrUX/typed-decision input is never a failure). `failed` is the
151
+ sole failure state.
152
+
153
+ Individual stages stay independently callable for review and re-scoring:
154
+
155
+ ```bash
156
+ kk stage run seo-preflight <work_dir> <options_file> [run_id] [require_decisions]
157
+ kk stage run seo-collect <work_dir>
158
+ kk stage run seo-check <work_dir>
159
+ kk stage run seo-enrich <work_dir>
160
+ kk stage run seo-judge <work_dir>
161
+ kk stage run seo-score <work_dir> [new_revision_dir]
162
+ kk stage run seo-report <work_dir>
163
+ ```
164
+
165
+ ## 7. Progress
166
+
167
+ - `spur workflow trace <run-id>` — the action trace; the state id tells you which stage is running.
168
+ - `.spur/run/<runId>-seo-work-dir.txt` — the run-bound workspace path (never guess it).
169
+ - `.spur/memory/runs/<run-id>.md` — the two-file run record.
170
+ - Stage progress goes to stderr and is captured in the run record; long collection stages are
171
+ bounded by `max_requests`/`deadline_seconds`, so a run always terminates.
172
+
173
+ Artifacts under the work dir, one run id shared by all of them: `options.json`, `manifest.json`,
174
+ `crawl.json`, `checks.json`, `judgments.json`, `performance.json`, `visibility.json`, `audit.json`,
175
+ `digest.md`, `narrative.json` (optional), `report.json`, `report.md`. Every JSON object carries
176
+ `schemaVersion`, `methodVersion`, `runId`, `createdAt`, `inputDigest` and predecessor digests;
177
+ wrong run, version or digest is a hard error. Writes are sibling temp file + rename, so a reader
178
+ never sees a partially written value. The saved audit is durable under `.works/`, not volatile
179
+ `.spur/run/`.
180
+
181
+ ## 8. Failure handling
182
+
183
+ | Symptom | Handling |
184
+ | --- | --- |
185
+ | Missing/unserved SEO catalogs | Apply §2, re-run. Preflight failure text names the missing files/ids. |
186
+ | Invalid options | Fix the named field; the file is validated before any collection. |
187
+ | `work_dir` already populated | Use the run's own bound directory, or a new empty one. Never clear a directory from a previous run — preflight never deletes it. |
188
+ | Network policy refusal (local/reserved address, peer mismatch, redirect off-scope) | The collected observation is recorded as unavailable/blocked. Do not work around it: an off-scope or non-public target is out of the audit's trust boundary. |
189
+ | Budget exhaustion (`limits.hitLimit`) | Report it as a limit. Never present an exhausted crawl as a complete one. |
190
+ | Optional evidence unavailable (no PSI/CrUX key, insufficient sample, 429, timeout) | Recorded as an availability outcome; the report is `partial`, never failed and never a zero score. |
191
+ | Maker fallback / low confidence | The judgment stays `unknown`/`review`. It is not a site defect and not a measurement. |
192
+ | Malformed predecessor artifact | Hard error, nonzero exit — malformed input is never reinterpreted as an empty result. |
193
+ | Narrative absent or rejected | The report is still delivered with `narrativeStatus: omitted|rejected` and a deterministic summary; an optional agent failure is not an audit failure. |
194
+ | Cancellation | Active sockets and decision children are aborted, completed artifacts are preserved with a cancellation marker, and no new requests or decisions start. |
195
+
196
+ ## 9. Offline rescoring
197
+
198
+ Score a saved audit again without touching the network or the source run:
199
+
200
+ ```bash
201
+ kk stage run seo-score <saved-work_dir> <new-revision_dir> # revision dir must be absent/empty
202
+ kk stage run seo-report <new-revision_dir>
203
+ ```
204
+
205
+ The revision copies the validated bounded inputs (`options.json`, `manifest.json`, `crawl.json`,
206
+ `checks.json`) beside a fresh `audit.json`, keeps the original `runId` and adds a new `revisionId`.
207
+ Source artifacts stay immutable, so a comparison between revisions reads two durable directories.
208
+ Do not compare scores across runs with different coverage: `coverage` records the denominators,
209
+ and the score is undefined (`null`) when no row of that category is applicable.
@@ -0,0 +1,90 @@
1
+ # Smart SEO — sources, crawler policy and GEO limits
2
+
3
+ Every rule row carries the primary `sourceUrl` and the `sourceCheckedAt` date that justified it, and
4
+ the report header prints the method version's checked-at date. This reference is the refresh
5
+ procedure and the per-source limits — not a license to restate a source as a measurement.
6
+
7
+ ## 1. Primary sources (checked 2026-10-14)
8
+
9
+ | Source | What it owns | Limit / unknown |
10
+ | --- | --- | --- |
11
+ | [Google AI optimization guide](https://developers.google.com/search/docs/fundamentals/ai-optimization-guide) | Original, useful content plus technical access are what matter for Google AI surfaces. Special AI files, rigid chunking and schema are not eligibility requirements. | It promises no citation, ranking or inclusion. |
12
+ | [Google AI features](https://developers.google.com/search/docs/appearance/ai-features) | Snippet eligibility and indexing are prerequisites; crawl observations cannot verify inclusion. | Reporting surface is described inconsistently across Google pages and depends on the account; never freeze an availability claim. |
13
+ | [Google common crawlers](https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers) | `Google-Extended` controls separate uses and does not control Search inclusion. | Robots groups express the site owner's **declared** policy only. |
14
+ | [OpenAI crawler documentation](https://developers.openai.com/api/docs/bots) | `OAI-SearchBot` is for search, `GPTBot` for training, `ChatGPT-User` is user-triggered with different semantics. | Blocking training is not a search defect; no OpenAI behavior is observable from the audit. |
15
+ | [Google Search updates](https://developers.google.com/search/updates) | Dated change log; FAQ rich results were fully retired on 2026-05-07 (eligibility had already been restricted to authoritative government/health sites in Aug/Sep 2023). | Any removal date must be re-read from this page, not from memory. |
16
+ | [PSI API notice](https://developers.google.com/speed/docs/insights/v5/get-started) | PSI setup and the announced CrUX removal from PSI. | The removal is announced, not completed — do not report it as done. |
17
+ | [Web Vitals](https://web.dev/articles/vitals) | Field p75 LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 defines a Core Web Vitals pass. | Lab TBT cannot substitute for INP; a missing field sample is `unknown`, never zero. |
18
+
19
+ Supporting rule-level sources (RFC 9309 for robots matching, Google canonicalization and robots-meta
20
+ docs, sitemap docs) are recorded per row in `plugins/kk/scripts/seo-rules.ts` (`sourceUrl`).
21
+
22
+ ## 2. Crawler policy — keep the three purposes separate
23
+
24
+ The audit reads the site's declared crawler policy and the audit's own requests. It cannot observe
25
+ what any other crawler did. When you explain crawler findings, name the purpose explicitly:
26
+
27
+ | Purpose | Typical user agents (as documented by their owners) | What a block means |
28
+ | --- | --- | --- |
29
+ | **Search / answer-surface indexing** | `Googlebot`, `Bingbot`, `OAI-SearchBot` | Affects eligibility for search and AI answer surfaces that use a search index. Operator intent required: a deliberate restriction is a decision, not a defect. |
30
+ | **Model training corpus** | `GPTBot`, `Google-Extended`, `ClaudeBot`, `CCBot` | A policy choice about training data. Blocking training is **not** a search defect, and allowing it is not a ranking or citation strategy. |
31
+ | **User-triggered fetch** | `ChatGPT-User` and similar on-demand fetchers | Different robots semantics: the fetch serves a user's explicit request. A block limits that user's session, not search eligibility. |
32
+
33
+ Rules the audit uses here: `robots_blocks_site` (adapted, critical), `ai_bots_blocked` and
34
+ `llms_txt_missing` (informational), `js_dependent` (informational). Report the observed directive,
35
+ the path it applies to and the operator-intent caveat — never "AI crawlers cannot see your site".
36
+
37
+ Also state what the audit itself did: its own crawl honors robots.txt, the declared `Crawl-delay`
38
+ and one shared page/request/byte/deadline budget, and it sends no credentials or cookies.
39
+
40
+ ## 3. Source refresh procedure
41
+
42
+ Do this before changing a rule, re-publishing a rubric statement, or answering a question whose
43
+ answer depends on current engine behavior:
44
+
45
+ 1. Open the rule's `sourceUrl` from the failing row (or the table above) and re-read the relevant
46
+ section. Do not refresh from a summary, a vendor README or a search-result snippet.
47
+ 2. Record today's date as the new `sourceCheckedAt` for that rule and update `sourceUrl` if the
48
+ owner moved the page. Never silently keep an old checked-at date.
49
+ 3. If the source contradicts the rule's current rationale, treat that as a **rule defect**: the
50
+ rationale, disposition and severity all change together, and the change is a method-version
51
+ change — not an in-run edit of a saved audit.
52
+ 4. If two sources disagree, keep the more conservative reading, report the uncertainty, and say
53
+ which pages disagree. Vendor self-reported model agreement is not human calibration.
54
+ 5. Never re-state an announced-but-incomplete change as shipped (for example the PSI/CrUX notice).
55
+
56
+ Audits stay reproducible: a saved `audit.json` keeps the `sourceCheckedAt` it was produced with,
57
+ and offline rescoring reuses the saved check rows rather than re-reading live sources.
58
+
59
+ ## 4. GEO (generative-engine) diagnostics and their limits
60
+
61
+ GEO work in this method is **editorial diagnostics with explicit uncertainty**: intent
62
+ satisfaction, first-hand specificity, verifiable provenance, entity consistency and self-contained
63
+ answer-first facts. They are model judgments (`source: model`) with a maker and confidence, or
64
+ heuristic content checks — never measurements of an answer engine's behavior. The two `geo`
65
+ category rules and the page-level semantic decisions feed the action list exactly like other rows.
66
+
67
+ What this means in practice:
68
+
69
+ - Never state or imply that a change will produce citations, mentions, rankings or traffic.
70
+ - No synthetic visibility percentage is generated from a quality score. A visibility number exists
71
+ only when the operator supplied an imported snapshot (§5 of `operations.md`), and it is reported
72
+ with its source, window, platform and denominator.
73
+ - Locale-specific query and citation samples are dated snapshots, not live data. A missing
74
+ platform, denominator or sample count makes the row unusable — drop it rather than guess.
75
+ - "Answer-first" (heading fit, self-contained answer, next step) is an editorial rubric applied to
76
+ the observed page text. It is not a snippet or citation predictor.
77
+ - Where the evidence does not exist (a specific engine's answer for a specific query, an actual
78
+ citation rate, a training-corpus inclusion), say that it is unknown and name what would be needed
79
+ to know it. Live provider integration, rank tracking and Search Console OAuth are out of scope in
80
+ this version.
81
+
82
+ ## 5. Vendors and third-party code
83
+
84
+ `vendors/` is reference-only: it is excluded from runtime and install resolution, and nothing may
85
+ be read or copied out of it into a shipped file. The audit is native Bun/TypeScript — no Python
86
+ path, no browser/rendering runtime, no bundled font assets. Vendor rule identities are retained for
87
+ traceability, but their behavior reflects the dispositions in `plugins/kk/references/smart-seo-attribution.md`
88
+ (shipped beside this skill), not the vendor's weighted grades or relative impact scale. The full vendor
89
+ review lives in the source repository at `docs/plans/2026-10-14-smart-seo-vendor-review.md`; it is
90
+ design-time context and is not installed, so never resolve a runtime path through it.
@@ -10,8 +10,20 @@
10
10
  #
11
11
  # Run: spur workflow run feature-verification.yaml --vars '{"featureId":"D63"}'
12
12
  #
13
- # Shape: verify -> done | failed. The onEnter step runs the standard script
14
- # `feature-verification-steps.ts` (no public CLI verb, D63 task 0915): it
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
15
27
  # resolves this very definition for the verifier identity (name, source path,
16
28
  # layer, definition digest), records the RUNNING receipt (run-scoped +
17
29
  # feature-latest copies), registers the run-scoped receipt as a run artifact,
@@ -40,36 +52,28 @@ vars:
40
52
  featureId: "X"
41
53
  spurBin: "spur"
42
54
  verificationCmd: "bun run spur-check"
55
+ # Operator-declared confidence level; empty derives it from the pass (HIGH when the selected
56
+ # definition's own command exited 0 with an unchanged proof-input digest, MEDIUM under a caller
57
+ # `--cmd` override, LOW otherwise). The completion boundary refuses a PASS carrying no level or
58
+ # LOW unless the receipt records the acknowledgement below.
59
+ confidence: ""
60
+ # Operator acknowledgement of a LOW level. It rides the receipt, so the transition reads it from
61
+ # the evidence rather than from whatever flags a later command happens to pass.
62
+ ackLowConfidence: "false"
43
63
  __runId: ""
44
64
 
45
65
  states:
46
66
  - id: verify
47
67
  description: >
48
- Resolve this run's installed sp script root via Superskill, then run the
49
- pinned installed verifier with Bun against the repo-wide check set. The
50
- verifier records the bound evidence receipt (verifier identity,
51
- checked-input digest, run identity) and the coarse status file. Both
52
- actions fail closed — a locator/script failure, an unresolved or
53
- non-installed root, an absent verifier, or a failing check yields a
54
- non-PASS status — and always exit 0, so the transition guard routes
55
- missing/corrupt/FAIL to `failed`.
68
+ Run the repo-wide check set via `spur feature verify`, which
69
+ records the bound evidence receipt (verifier identity, checked-input
70
+ digest, run identity) and the coarse status file, always exit 0 so the
71
+ transition guard routes missing/corrupt/FAIL to `failed`.
56
72
  onEnter:
57
73
  - kind: shell
58
74
  options:
59
75
  command: >-
60
- S="$(superskill script path sp script-root.mjs 2>/dev/null)" || S="";
61
- if [ -n "$S" ] && [ -f "$S" ]; then node "$S" --run-id "$__runId" --spur-bin "$spurBin";
62
- else echo "script-root failed closed — run 'superskill install sp --marketplace gobing-ai/spur'" >&2;
63
- fi;
64
- exit 0
65
- - kind: shell
66
- options:
67
- command: >-
68
- mkdir -p .spur/run;
69
- DIR="$(jq -r 'select(.mode == "installed" and (.dir | type == "string") and .dir != "") | .dir' ".spur/run/$__runId-script-root.json" 2>/dev/null)" || DIR="";
70
- [ -n "$DIR" ] && [ -f "$DIR/feature-verification-steps.mjs" ] && S="$DIR/feature-verification-steps.mjs";
71
- [ -f "$S" ] && bun "$S" verify --feature-id "$featureId" --run-id "$__runId" --spur-bin "$spurBin" || printf 'FAIL feature-verification failed closed — unresolved root or verifier failure\n' > ".spur/run/$__runId-feature-verification.status";
72
- exit 0
76
+ mkdir -p .spur/run; ACK=""; [ "$ackLowConfidence" = "true" ] && ACK="--ack-low-confidence"; $spurBin feature verify "$featureId" --run-id "$__runId" --confidence "$confidence" $ACK --no-logo || printf 'FAIL feature-verification failed closed — spur feature verify exited nonzero\n' > ".spur/run/$__runId-feature-verification.status"; exit 0
73
77
 
74
78
  - id: done
75
79
  description: Terminal — repo-wide checks passed and the receipt is bound.
@@ -80,7 +84,7 @@ states:
80
84
  transitions:
81
85
  - from: verify
82
86
  to: done
83
- description: Repo-wide checks passed — the script completed the receipt with PASS.
87
+ description: Repo-wide checks passed — `spur feature verify` completed the receipt with PASS.
84
88
  guard:
85
89
  kind: shell
86
90
  options:
@@ -22,10 +22,13 @@ vars:
22
22
  window: "24h"
23
23
  language: "zh"
24
24
  voice_profile: "robin-news"
25
- # Voice driver switch — voicebox (voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts)
26
- # | voxcpm (voxcpm-tts, opt-in; ADR-036; provisional 5.6 字/秒 estimate). Same default as
27
- # kk-daily-ai-voice: gemini-tts (20260929 operator override, verified live).
28
- vdriver: "gemini"
25
+ # Voice driver switch — voxcpm (voxcpm-tts) | gemini (gemini-tts) | voicebox (voice-gen)
26
+ # | ominivoice (omni-voice-gen). The generate tier below mirrors the fallback order, and the
27
+ # fit/duration gates take their spoken-char rate from this var — switching drivers here is enough.
28
+ # 2026-10-15 production test (operator decision): voxcpm first, then gemini, then omni. voxcpm
29
+ # rendered that episode in 21.8 min at a measured 5.19 字/秒 (fitted at 5.2); if the audio verdict
30
+ # is good this is the standing order, otherwise revert this default to "gemini".
31
+ vdriver: "voxcpm"
29
32
  intro_music: ".works/kk-daily-ai-voice/AI每日资讯_8秒播客片头.mp3"
30
33
  surfdash_root: "/Users/robin/xprojects/surfdash"
31
34
  title: ""
@@ -258,7 +261,7 @@ nodes:
258
261
  command: >-
259
262
  kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
260
263
  && test "${vars.plan_translate_enabled}" = "true"
261
- && ! test -s "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
264
+ && ! jq -e --argjson n "$(jq '.metadata.docs|length' "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json")" 'type=="array" and length==$n and all(.[];type=="object")' "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
262
265
  action:
263
266
  kind: agent.run
264
267
  # 0238 R2: a stochastic executor no-op must not end the run here — the retry sibling decides.
@@ -268,21 +271,28 @@ nodes:
268
271
  agent: ${vars.agent}
269
272
  # ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
270
273
  # The anchor is shared with plan-translate-retry so the two attempts cannot drift apart.
274
+ # 20261015 forensics: headless agy's terminal sandbox denies workspace writes for EVERY
275
+ # shell command (command() rules are promptless-but-sandboxed; unsandboxed() is dead), so
276
+ # the producer must deliver via the file-write tool; and the condition gate checks JSON
277
+ # validity (array, length == candidates docs, all objects) because a pi token-cap staged
278
+ # write once flowed 4/12 items downstream. Keep both when editing prompt or condition.
271
279
  input: &plan-translate-input |
272
- Read ${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json — metadata.docs (Doc[]) is the plan; don't edit it.
273
- For needsTranslation items, adapt "title", "body" and "metadata.brief" into spoken simplified Chinese — full coverage, not a summary: keep every fact, number, name, quote and hedge (口播全长改编,不摘要不省略), body ≈0.6–0.7× the source.
274
- Titles read as a news event; keep product/model names verbatim, preserve body URLs, use 「」 not ASCII double quotes.
275
- Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no duplicated labels or hedging (「读者宜另行核实」「仍待验证」) — merge their substance into prose or drop.
276
- Keep other fields (id, sourceUri, mediaType, metadata) as-is; no comments or trailing commas.
277
- Output strictly valid JSON — a same-length, same-order array — to ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
280
+ Read ${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json; metadata.docs (Doc[]) is the plan.
281
+ For needsTranslation items adapt "title", "body" and "metadata.brief" into spoken simplified Chinese — keep every fact, number, name, quote, hedge (口播全长改编,不摘要不省略), body ≈0.6–0.7× source.
282
+ Titles read as news; keep product/model names and body URLs verbatim, use 「」 not ASCII quotes.
283
+ Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no hedging labels (「读者宜另行核实」「仍待验证」) — merge or drop.
284
+ Keep id/sourceUri/mediaType/metadata as-is; no comments/trailing commas.
285
+ Deliver with your file-write tool (headless agy sandbox blocks shell writes); the file must end as ONE valid JSON array — no split markers/placeholders.
286
+ Output the JSON array — same length and order — to ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
278
287
  expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
279
288
  allowAppend: false
280
289
 
281
290
  - id: plan-translate-retry
282
291
  description: >-
283
- Bounded recovery for plan-translate (0238 R2): runs only when the primary settled without its
284
- expectFile, retries the same input on a different tier, and carries NO onError — a second miss
285
- fails the run loudly instead of letting an untranslated plan flow downstream.
292
+ Bounded recovery for plan-translate (0238 R2): runs only when the primary settled without a
293
+ VALID expectFile (missing or contract-violating JSON), retries the same input on a different
294
+ tier, and carries NO onError — a second miss fails the run loudly instead of letting an
295
+ untranslated or truncated plan flow downstream.
286
296
  dependsOn: [plan-translate]
287
297
  condition:
288
298
  kind: shell
@@ -290,7 +300,7 @@ nodes:
290
300
  command: >-
291
301
  kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
292
302
  && test "${vars.plan_translate_enabled}" = "true"
293
- && ! test -s "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
303
+ && ! jq -e --argjson n "$(jq '.metadata.docs|length' "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json")" 'type=="array" and length==$n and all(.[];type=="object")' "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
294
304
  action:
295
305
  kind: agent.run
296
306
  options:
@@ -535,7 +545,7 @@ nodes:
535
545
  kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
536
546
 
537
547
  - id: generate
538
- description: "Invoke the voice driver plugin (vdriver: gemini -> gemini-tts, voxcpm -> voxcpm-tts) to generate the audio"
548
+ description: "Voice tier, primary driver (vars.vdriver — voxcpm while the 2026-10-15 production test runs). onError: continue — the fallback siblings below are the loud chain (same pattern as 0238 R2)."
539
549
  # wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
540
550
  # as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
541
551
  dependsOn: [script, safety, review, wrap-docs]
@@ -547,18 +557,76 @@ nodes:
547
557
  action:
548
558
  kind: shell
549
559
  options:
560
+ onError: continue
550
561
  command: >-
551
562
  OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}"
552
563
 
564
+ # Driver fallback chain (2026-10-15, operator decision): voxcpm -> gemini -> omni. Each sibling
565
+ # runs the same stage with a pinned driver; its condition re-checks the pool AND that NO upstream
566
+ # driver left a valid, passing whole-document Content (the stage stamps metadata.generator with
567
+ # its plugin id and the cache ignores foreign-driver sections, so a failed attempt can neither be
568
+ # relabeled nor mix voices). Non-final siblings continue on error; the last one is loud.
569
+ - id: generate-fallback
570
+ description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content"
571
+ dependsOn: [generate]
572
+ condition:
573
+ kind: shell
574
+ options:
575
+ command: >-
576
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
577
+ && ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
578
+ action:
579
+ kind: shell
580
+ options:
581
+ onError: continue
582
+ command: >-
583
+ kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}"
584
+
585
+ - id: generate-fallback2
586
+ description: "Voice tier fallback 2 (last, loud): omni-voice-gen when neither voxcpm nor gemini delivered"
587
+ dependsOn: [generate-fallback]
588
+ condition:
589
+ kind: shell
590
+ options:
591
+ command: >-
592
+ kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1
593
+ && ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts" or .metadata.generator == "kk:omni-voice-gen") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
594
+ action:
595
+ kind: shell
596
+ options:
597
+ command: >-
598
+ OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "ominivoice" "1" "${vars.episode_max_minutes}"
599
+
553
600
  - id: intro
554
601
  description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
555
- dependsOn: [generate]
602
+ dependsOn: [generate, generate-fallback, generate-fallback2]
556
603
  action:
557
604
  kind: shell
558
605
  options:
559
606
  command: >-
560
607
  kk stage daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
561
608
 
609
+ # ---- advisory voice audit (2026-10-16): never fails the run ----
610
+ # The 20261015 episode shipped 59 different speakers because an instruct-mode profile pins none,
611
+ # and nothing but a human listening caught it. This grades the final render by per-segment median
612
+ # F0 (a speaker change swings it tens of Hz) and prints the table plus a PASS/FAIL line. It is
613
+ # deliberately advisory: `|| true` keeps its non-zero exit out of the run, and onError covers a
614
+ # missing venv (the check needs librosa from the voxcpm plugin's environment).
615
+ - id: voice-audit
616
+ description: "Advisory speaker-consistency audit of the joined episode (warn-only, never fails the run)"
617
+ dependsOn: [intro]
618
+ condition:
619
+ kind: shell
620
+ options:
621
+ command: >-
622
+ test -f "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
623
+ action:
624
+ kind: shell
625
+ options:
626
+ command: >-
627
+ kk stage daily-voice-audit "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
628
+ onError: continue
629
+
562
630
  - id: duration
563
631
  description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates, exit 0 — 0266); above episode_max_minutes it still fails loud. Duration is authoritative."
564
632
  dependsOn: [intro]