@gobing-ai/knowledge-kit 0.1.8 → 0.1.9

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 (30) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.js +725 -1101
  3. package/dist/board/daily-voice/index.js +722 -101
  4. package/dist/board/itc/index.js +13 -0
  5. package/dist/index.js +1792 -102
  6. package/package.json +1 -1
  7. package/plugins/ingestions/horizon-ingest/dist/index.js +24 -1
  8. package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
  9. package/plugins/kk/config.example.yaml +8 -0
  10. package/plugins/kk/plugin.json +1 -1
  11. package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
  12. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
  13. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
  14. package/plugins/kk/scripts/daily-stages.ts +580 -78
  15. package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
  16. package/plugins/kk/scripts/topic-boundary.json +18 -83
  17. package/plugins/kk/workflows/kk-collect.yaml +2 -0
  18. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +75 -6
  19. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
  20. package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
  21. package/plugins/publishings/podcast-pub/README.md +65 -0
  22. package/plugins/publishings/podcast-pub/dist/index.js +1709 -274
  23. package/plugins/publishings/podcast-pub/plugin.json +1 -1
  24. package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
  25. package/plugins/publishings/podcast-pub/src/index.ts +211 -34
  26. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
  27. package/plugins/publishings/podcast-pub/src/seo.ts +1081 -0
  28. package/plugins/publishings/podcast-pub/src/show-notes.ts +71 -6
  29. package/plugins/publishings/surfdash-pub/dist/index.js +66 -5
  30. package/plugins/publishings/surfdash-pub/src/index.ts +94 -10
@@ -0,0 +1,282 @@
1
+ ---
2
+ name: kk-daily-publish3
3
+ kind: dag
4
+ version: "1"
5
+ description: >-
6
+ The publish half of voice3 (design `docs/design/kk-daily-ai-voice3.md` §5, §5a, §5b, task 0323):
7
+ three windows over one frozen bundle. `after-generate` drafts the draft-capable channels right
8
+ after generation, `cn-peak` goes live at 09:00 Asia/Shanghai, and `overseas-rss` runs behind the
9
+ podcast RSS gate. The channel matrix lives in the `daily-ai-voice` publish preset, never here.
10
+ Re-enter a failed window with
11
+ `spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml --vars '{"work_dir":"<day dir>","window":"after-generate"}'`
12
+ — receipts make the rerun a no-op for anything already delivered.
13
+
14
+ # ADR-022: live is launch-only. A workflow file never yields a live send; the operator's launch var
15
+ # (or the installed schedule entry that carries it) is the consent. `dummy` is the rehearsal switch.
16
+ vars:
17
+ work_dir: ""
18
+ run_date: ""
19
+ window: after-generate
20
+ # Empty = the operator's `workflows.kk-daily-ai-voice3.publish_preset` selector decides.
21
+ preset: ""
22
+ channels: ""
23
+ live_channels: ""
24
+ dummy: "false"
25
+ agent: "inline"
26
+ __runId: ""
27
+ __definitionDigest: ""
28
+
29
+ nodes:
30
+ # ── cn-peak: the schedule fires in local time, the audience peaks in Shanghai ──────────────
31
+ - id: wait-until
32
+ description: >-
33
+ cn-peak only: the launchd entry fires at 17:00 local because launchd intervals follow the
34
+ machine's zone, so this node sleeps the remainder to 09:00 Asia/Shanghai (DST-safe, no-op when
35
+ already past).
36
+ condition:
37
+ kind: shell
38
+ options:
39
+ command: 'test "${vars.window}" = "cn-peak"'
40
+
41
+ action:
42
+ kind: shell
43
+ options:
44
+ command: 'kk stage run publish-matrix-wait-until 09:00 Asia/Shanghai'
45
+
46
+ # ── overseas-rss: never publish an episode the feed cannot prove ──────────────────────────
47
+ - id: rss-gate
48
+ description: >-
49
+ The overseas RSS gate, as a node that ALWAYS succeeds: for after-generate and cn-peak it is a
50
+ no-op, and for overseas-rss it records the verdict in `4-publish/rss-gate.json` (a miss is a
51
+ verdict, not a node failure). The graph routes on that artifact — `plan` requires a hit,
52
+ `diagnose` requires a miss — because a failing node would instead cascade-skip its dependents
53
+ through the engine's all-deps-skipped rule and take the incident path with it. `run_date` is
54
+ the previous day by the time this window runs, so the caller passes it; the gate never guesses.
55
+ action:
56
+ kind: shell
57
+ options:
58
+ command: >-
59
+ test "${vars.window}" = "overseas-rss" || { echo "publish3: rss-gate not applicable to ${vars.window}"; exit 0; };
60
+ test -n "${vars.run_date}" || { echo "publish3: overseas-rss needs run_date" >&2; exit 1; };
61
+ kk stage run publish-matrix-rss-gate "${vars.work_dir}" --date "${vars.run_date}" || true
62
+
63
+ # ── every window: plan → check → send-confirm → send ──────────────────────────────────────
64
+ - id: plan
65
+ description: >-
66
+ Freeze this window's target rows and their payloads from the bundle and the preset
67
+ (`4-publish/plan.<window>.json`). Never runs for a gated-miss overseas day: the RSS gate's
68
+ non-zero exit skips its dependents, so an unpublished episode cannot be planned.
69
+ dependsOn: [rss-gate]
70
+ condition:
71
+ kind: shell
72
+ options:
73
+ # A gated miss plans nothing: overseas targets only exist once the feed proves the episode.
74
+ command: >-
75
+ test "${vars.window}" != "overseas-rss"
76
+ || jq -e '.hit == true' "${vars.work_dir}/4-publish/rss-gate.json" >/dev/null 2>&1
77
+
78
+ action:
79
+ kind: shell
80
+ options:
81
+ command: >-
82
+ BUNDLE="${vars.work_dir}/bundle.json";
83
+ test -f "$BUNDLE" || { echo "publish3: no bundle at $BUNDLE" >&2; exit 1; };
84
+ ARGS="--bundle $BUNDLE --window ${vars.window}";
85
+ test -z "${vars.preset}" || ARGS="$ARGS --preset ${vars.preset}";
86
+ test -z "${vars.channels}" || ARGS="$ARGS --channels ${vars.channels}";
87
+ test -z "${vars.live_channels}" || ARGS="$ARGS --live-channels ${vars.live_channels}";
88
+ test "${vars.dummy}" != "true" || ARGS="$ARGS --dummy";
89
+ kk stage run publish-matrix-plan "${vars.work_dir}" $ARGS
90
+
91
+ - id: check
92
+ description: >-
93
+ Readiness probes for the planned non-dummy channels. Warn-only by contract: a stale session is
94
+ recorded in `4-publish/check.<window>.json` and the window still runs, because the send's own
95
+ receipt is the authority on whether a channel could publish.
96
+ dependsOn: [plan]
97
+ action:
98
+ kind: shell
99
+ options:
100
+ command: 'kk stage run publish-matrix-check "${vars.work_dir}" --window "${vars.window}" || true'
101
+
102
+ - id: send-confirm
103
+ description: >-
104
+ Operator approval of a LIVE send: `pause: true` stops the run and RESUME is the approval;
105
+ cancelling is the rejection. Skipped for a draft-only window, so an unattended draft run and
106
+ every rehearsal proceed without a human.
107
+ dependsOn: [check]
108
+ pause: true
109
+ condition:
110
+ kind: shell
111
+ options:
112
+ command: >-
113
+ jq -e '[.rows[] | select(.mode == "live")] | length > 0'
114
+ "${vars.work_dir}/4-publish/plan.${vars.window}.json" >/dev/null 2>&1
115
+
116
+ - id: send
117
+ description: >-
118
+ Dispatch in tier order: tier 0 serial and blocking, later tiers after the tier-0 links resolve.
119
+ Receipts key every target, so a rerun never resends what was delivered, and a dummy row never
120
+ spawns a plugin. `check` is listed beside `send-confirm` on purpose: the engine skips a node only
121
+ when ALL its dependencies were skipped, so a draft window (no live row → `send-confirm` skipped)
122
+ still sends, while a gated/blocked plan (both ancestors skipped) still stops here.
123
+ dependsOn: [check, send-confirm]
124
+ action:
125
+ kind: shell
126
+ options:
127
+ command: 'kk stage run publish-matrix-send "${vars.work_dir}" --window "${vars.window}"'
128
+
129
+ - id: classify
130
+ description: >-
131
+ Turn the window's receipts into `full|partial|none|dummy` (`4-publish/outcome.<window>.json`),
132
+ always exit 0 — routing is this graph's job, not the classifier's.
133
+ dependsOn: [send]
134
+ action:
135
+ kind: shell
136
+ options:
137
+ command: >-
138
+ test -n "${vars.run_date}" || { echo "publish3: classify needs run_date" >&2; exit 1; };
139
+ kk stage run publish-matrix-classify "${vars.work_dir}" --window "${vars.window}" --run-date "${vars.run_date}"
140
+
141
+ # ── after-generate only: the voice2-era tail stages keep working on the bridged names ─────
142
+ - id: surfdash-patch
143
+ description: >-
144
+ after-generate only: cover and podcast cross-link patching plus the CMS re-sync, whenever tier 0
145
+ published something. Reads the legacy `31-publish` names the classifier bridged, so voice2's own
146
+ stage body is reused unchanged.
147
+ dependsOn: [classify]
148
+ condition:
149
+ kind: shell
150
+ options:
151
+ command: >-
152
+ test "${vars.window}" = "after-generate"
153
+ && grep -qxE 'full|partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
154
+
155
+ action:
156
+ kind: shell
157
+ options:
158
+ command: >-
159
+ kk stage run daily-patch-surfdash "${vars.work_dir}" "${vars.run_date}" "$KK_SURFDASH_ROOT" ""
160
+
161
+ - id: partial-marker
162
+ description: "after-generate only: record the partial-publish marker when the classifier said partial."
163
+ dependsOn: [classify]
164
+ condition:
165
+ kind: shell
166
+ options:
167
+ command: >-
168
+ test "${vars.window}" = "after-generate"
169
+ && grep -qx 'partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
170
+
171
+ action:
172
+ kind: shell
173
+ options:
174
+ command: 'kk stage run daily-publish-partial "${vars.work_dir}" "${vars.run_date}"'
175
+
176
+ - id: mark-published
177
+ description: >-
178
+ after-generate only: mark the run's items published in the store once the owned platforms
179
+ delivered. Keyed by the voice3 run id the bundle records, never by this publish run.
180
+ dependsOn: [classify]
181
+ condition:
182
+ kind: shell
183
+ options:
184
+ command: >-
185
+ test "${vars.window}" = "after-generate"
186
+ && grep -qxE 'full|partial' "${vars.work_dir}/4-publish/publish-status.after-generate.txt" 2>/dev/null
187
+
188
+ action:
189
+ kind: shell
190
+ options:
191
+ command: >-
192
+ PARENT="$(jq -r '.parentRunId // .runId' "${vars.work_dir}/bundle.json" 2>/dev/null)";
193
+ test -n "$PARENT" || { echo "publish3: bundle has no run id" >&2; exit 1; };
194
+ kk store mark-published --topic ai-news --run "$PARENT"
195
+
196
+ # ── the incident path: a gated miss is diagnosed, never silently dropped ─────────────────
197
+ - id: diagnose
198
+ description: >-
199
+ overseas-rss only, on a gate miss: classify why the episode is not in the feed
200
+ (`no-run | generation-failed@<node> | no-content | t0-failed | rss-lag`) and write
201
+ `4-publish/incident.json` + `incident.md`. `onError: continue` — a diagnosis must never mask the
202
+ miss it is describing.
203
+ dependsOn: [rss-gate]
204
+ condition:
205
+ kind: shell
206
+ options:
207
+ command: >-
208
+ test "${vars.window}" = "overseas-rss"
209
+ && jq -e '.hit == false' "${vars.work_dir}/4-publish/rss-gate.json" >/dev/null 2>&1
210
+
211
+ action:
212
+ kind: agent.run
213
+ onError: continue
214
+ options:
215
+ role: scribe
216
+ agent: ${vars.agent}
217
+ input: |
218
+ Diagnose a missed overseas publish window. Read only these paths:
219
+ - ${vars.work_dir}/4-publish/rss-gate.json (the miss reason)
220
+ - ${vars.work_dir}/bundle.json (absent = no generation ran)
221
+ - ${vars.work_dir}/4-publish/outcome.after-generate.json and receipts/ (tier-0 evidence)
222
+ Run `spur workflow trace --json` for the voice3 run named in bundle.json if it exists.
223
+ Write ${vars.work_dir}/4-publish/incident.json with exactly
224
+ {"schemaVersion":1,"date":"<run_date>","class":"<one of no-run|generation-failed|<node>|no-content|t0-failed|rss-lag>","reason":"<one line>","evidence":["<paths>"]}
225
+ and ${vars.work_dir}/4-publish/incident.md with 3 sentences of operator prose: what is missing,
226
+ which class, and the one action that would fix it. Never regenerate content.
227
+ expectFile: ${vars.work_dir}/4-publish/incident.json
228
+
229
+ - id: repair
230
+ description: >-
231
+ overseas-rss only, on a gate miss: the ONE safe automatic repair — resend tier-0 targets whose
232
+ receipt is `effect: none`. It never regenerates content and never resends an uncertain live send
233
+ (the shared `--retry` rules), so a lost generation stays an operator decision.
234
+ dependsOn: [diagnose]
235
+ action:
236
+ kind: shell
237
+ options:
238
+ command: 'kk stage run publish-matrix-send "${vars.work_dir}" --window after-generate --retry --tier 0'
239
+
240
+ # ── traceability: one episode, several publish runs ───────────────────────────────────────
241
+ - id: bundle-runs
242
+ description: >-
243
+ Append this run id to `bundle.json` `runs[]`, idempotently, so one day's episode is traceable
244
+ across its generation run and its windows.
245
+ dependsOn: [classify]
246
+ action:
247
+ kind: shell
248
+ options:
249
+ command: >-
250
+ BUNDLE="${vars.work_dir}/bundle.json";
251
+ test -f "$BUNDLE" || exit 0;
252
+ jq --arg id "${vars.__runId}" 'if (.runs // []) | index($id) then . else .runs = ((.runs // []) + [$id]) end'
253
+ "$BUNDLE" > "$BUNDLE.tmp" && mv "$BUNDLE.tmp" "$BUNDLE"
254
+
255
+ - id: report
256
+ description: >-
257
+ The publish half's report: per-window status and every receipt, on one page. It also depends on
258
+ `diagnose`, so a gated-miss day (skipped `bundle-runs`, executed `diagnose`) still writes its
259
+ page — the same all-deps-skipped rule, read the other way.
260
+ dependsOn: [bundle-runs, diagnose]
261
+ action:
262
+ kind: shell
263
+ options:
264
+ # Straight statements, no `{ … }` group and no heredoc: the engine's shell wrapper treats a
265
+ # multi-line group as an interactive read and never returns.
266
+ command: >-
267
+ OUT="${vars.work_dir}/5-report/publish-${vars.window}.md";
268
+ mkdir -p "${vars.work_dir}/5-report";
269
+ STATUS="$(cat "${vars.work_dir}/4-publish/publish-status.${vars.window}.txt" 2>/dev/null || echo unknown)";
270
+ printf '# Publish report — %s — %s\n\n- status: %s\n\n| target | tier | status | effect | dummy |\n| --- | --- | --- | --- | --- |\n'
271
+ "${vars.window}" "${vars.run_date}" "$STATUS" > "$OUT";
272
+ jq -r '.rows[] | "| \(.id) | \(.tier) | \(.status) | \(.effect // "-") | \(.dummy) |"'
273
+ "${vars.work_dir}/4-publish/outcome.${vars.window}.json" >> "$OUT" 2>/dev/null || true
274
+
275
+ - id: ledger
276
+ description: "Append the window's row to the workflow ledger; `onError: continue` — bookkeeping never fails a publish."
277
+ dependsOn: [report]
278
+ action:
279
+ kind: shell
280
+ options:
281
+ command: >-
282
+ kk ledger append --workflow kk-daily-publish3 --work-dir "${vars.work_dir}" --run "${vars.__runId}" || true
@@ -0,0 +1,65 @@
1
+ # podcast-pub
2
+
3
+ Publish a podcast episode to a self-hosted microfeed 1.0.13 instance, and repair
4
+ SEO fields on episodes that are already there.
5
+
6
+ Credentials: `MICROFEED_BASE_URL`, `MICROFEED_API_KEY`. Optional
7
+ `MICROFEED_TIMEOUT_MS` (default 300000).
8
+
9
+ ## Operations
10
+
11
+ `options.operation` selects the call. The default is `publish`.
12
+
13
+ | Operation | What it does |
14
+ | --------- | ------------ |
15
+ | `publish` | Create or upload an episode. A pattern slug is sent only on the first create. |
16
+ | `show-notes` | Render plan `Doc[]` to markdown. No network. A bare JSON array `--in` selects this. |
17
+ | `list` / `get` / `update` / `delete` | Feed and item management. |
18
+ | `validate` | `POST /api/v1/items/validate`. |
19
+ | `backfill-seo` | Fill `_microfeed.seo` and missing iTunes numbers. **Dry-run unless `apply` is set.** |
20
+
21
+ `backfill-seo` options:
22
+
23
+ | Key | Default | Effect |
24
+ | --- | ------- | ------ |
25
+ | `apply` | false | `true` / `"true"` / `"1"` sends the PUTs |
26
+ | `since` / `until` | unset | Inclusive `YYYY-MM-DD` window |
27
+ | `ids` | unset | Item ids to touch |
28
+ | `out` | unset | Write the plan JSON `{ dryRun, items }` |
29
+ | `plan` | unset | With `apply`, PUT this reviewed plan instead of recomputing |
30
+ | `retitle` | false | Also change the RSS title |
31
+ | `rewriteBody` | false | Insert the summary paragraph and move the timeline `<style>` |
32
+ | `applySlug` | false | Set `_microfeed.slug` (moves the public URL; a 409 retries as `-2`) |
33
+ | `overrides` | unset | `[{ id, title?, titleKeywords?, summaryClauses? }]`. `title` is kept verbatim after sanitising (the 10-06 escape hatch) |
34
+
35
+ The repair always proposes SEO fields, even when `PODCAST_PUB_SEO=false`.
36
+ It does not change `guid`, attachments, `date_published`, `status`, or `url`.
37
+
38
+ ## SEO environment
39
+
40
+ Item `metadata.podcast.*` wins over these variables. Empty string means unset.
41
+ Invalid values fail the run and name the key. The full table and the legacy
42
+ combination that reproduces the pre-D9 payload are in `docs/design/podcast-pub.md` (D9).
43
+
44
+ | Key | Default |
45
+ | --- | ------- |
46
+ | `PODCAST_PUB_SEO` | `true` |
47
+ | `PODCAST_PUB_EPISODE_TITLE_MODE` | `as-is` (`headline` builds `{series} · {date}:{keywords}`) |
48
+ | `PODCAST_PUB_EPISODE_TITLE_PATTERN` | `{series} · {date}:{keywords}` |
49
+ | `PODCAST_PUB_SERIES_NAME` / `PODCAST_PUB_SHOW_NAME` | `每日AI乱炖` / `咸话咸说` |
50
+ | `PODCAST_PUB_TITLE_MAX` / `PODCAST_PUB_TITLE_MAX_KEYWORDS` | `55` / `2` |
51
+ | `PODCAST_PUB_SEO_TITLE_SUFFIX` / `PODCAST_PUB_SEO_TITLE_MAX` | `|咸话咸说` / `60` |
52
+ | `PODCAST_PUB_SEO_DESCRIPTION_MIN` / `MAX` / `SOURCE` | `120` / `155` / `plan` |
53
+ | `PODCAST_PUB_SUMMARY_PARAGRAPH` | `true` |
54
+ | `PODCAST_PUB_TIMELINE_INLINE_STYLE` | `after` (`before` is the old position, `omit` drops the style) |
55
+ | `PODCAST_PUB_AUTO_EPISODE_NUMBER` | `true` |
56
+ | `PODCAST_PUB_TITLE_SANITIZE` | `normalize` (`reject` fails on `"`, `\`, `<>`, or control characters) |
57
+ | `PODCAST_PUB_LINK_FIELD` | `none` (`url` copies `articleUrl` onto the item and warns) |
58
+ | `PODCAST_PUB_SLUG` / `PODCAST_PUB_SLUG_PATTERN` | `pattern` / `daily-ai-{date}` |
59
+ | `PODCAST_PUB_SOCIAL_IMAGE` | `false` |
60
+ | `PODCAST_PUB_ROUND_DURATION` | `true` |
61
+
62
+ `PODCAST_PUB_TITLE` is the show-notes H1. It is not the episode title pattern.
63
+
64
+ Do not point this plugin at a live feed from tests. `backfill-seo` against
65
+ production is an operator step, after a dry-run plan has been reviewed.