@gobing-ai/knowledge-kit 0.0.36 → 0.1.0

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 (159) 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 +2878 -152
  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 +180 -12
  14. package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
  15. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  16. package/plugins/generations/gemini-tts/README.md +4 -3
  17. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  18. package/plugins/generations/image-gen/dist/index.js +158 -12
  19. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  20. package/plugins/generations/omni-voice-gen/README.md +15 -4
  21. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  22. package/plugins/generations/voice-gen/dist/index.js +158 -12
  23. package/plugins/generations/voxcpm-tts/README.md +21 -1
  24. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  25. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  26. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  27. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  30. package/plugins/ingestions/web-search/dist/index.js +158 -12
  31. package/plugins/kk/commands/workflow-run.md +56 -18
  32. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  33. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  34. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  35. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  36. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  37. package/plugins/kk/plugin.json +1 -1
  38. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  39. package/plugins/kk/publish-presets/technical-article.json +14 -0
  40. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  41. package/plugins/kk/scripts/daily-stages.ts +42 -2
  42. package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
  43. package/plugins/kk/scripts/itc-stages.ts +25 -118
  44. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  45. package/plugins/kk/scripts/seo-decisions.ts +477 -0
  46. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  47. package/plugins/kk/scripts/seo-facts.ts +610 -0
  48. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  49. package/plugins/kk/scripts/seo-report.ts +460 -0
  50. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  51. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  52. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  53. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  54. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  55. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  56. package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
  57. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  58. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  59. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +86 -18
  60. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  61. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  62. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  63. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  64. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  65. package/plugins/publishings/bilibili-pub/package.json +20 -0
  66. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  67. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  68. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  69. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  70. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  71. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  72. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  73. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  74. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  75. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  76. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  77. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  78. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  79. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  80. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  81. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  82. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  83. package/plugins/publishings/csdn-pub/package.json +19 -0
  84. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  85. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  86. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  87. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  88. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  89. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  90. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  91. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  92. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  93. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  94. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  95. package/plugins/publishings/jike-pub/package.json +19 -0
  96. package/plugins/publishings/jike-pub/plugin.json +12 -0
  97. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  98. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  99. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  100. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  101. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  102. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  103. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  104. package/plugins/publishings/medium-pub/plugin.json +22 -1
  105. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  106. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  107. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  108. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  109. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  110. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  111. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  112. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  113. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  114. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  115. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  116. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  117. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  118. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  119. package/plugins/publishings/sspai-pub/package.json +19 -0
  120. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  121. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  122. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  123. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  124. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  125. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  126. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  127. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  128. package/plugins/publishings/substack-pub/plugin.json +21 -0
  129. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  130. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  131. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  132. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  133. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  134. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  136. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  137. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  138. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  139. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  140. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  141. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  142. package/plugins/publishings/x-pub/dist/index.js +160 -12
  143. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  144. package/plugins/publishings/x-pub/plugin.json +40 -0
  145. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  146. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  147. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  155. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  156. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  157. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  158. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  159. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -9,8 +9,8 @@ description: This skill should be used when a kk-itc publish plan contains varia
9
9
 
10
10
 
11
11
  This skill adapts one published article into one Content file per `publish-plan.json` variant with
12
- `adapt: true`. It is deliberately **thin**: the plan (built from `TARGET_PROFILES` in
13
- `plugins/kk/scripts/itc-stages.ts`) carries the per-variant locale, format and limits — this skill
12
+ `adapt: true`. It is deliberately **thin**: the plan (built by `buildItcPublishPlan` in
13
+ `apps/cli/src/itc-plan.ts` from each publish manifest's declared profiles) carries the per-variant locale, format and limits — this skill
14
14
  holds **no platform table** and never decides which variants exist.
15
15
 
16
16
  ## Inputs
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: smart-seo
3
+ description: >-
4
+ Run, review and interpret a read-only Smart SEO/GEO audit and turn its evidence into a prioritized
5
+ action plan. Use when the user says "audit this site for SEO", "run a smart SEO audit", "review
6
+ this saved audit", "explain these SEO findings", or "check GEO readiness". Read-only results: no
7
+ site mutation, no ranking and no citation promises.
8
+ version: 0.1.0
9
+ ---
10
+
11
+ # smart-seo — evidence-grounded SEO/GEO audits
12
+
13
+ One entry point for the whole audit lifecycle: intake → run → review evidence → interpret →
14
+ narrative → offline rescoring. There is no second command, subagent or plugin kind; this skill plus
15
+ `plugins/kk/workflows/kk-smart-seo.yaml` and the `kk stage run seo-*` stages are the entire surface.
16
+
17
+ ## 0. The contract — state it before you report
18
+
19
+ - **Read-only.** The audit fetches public pages and writes artifacts under `.works/`; it never edits
20
+ the site, submits URLs, changes accounts, publishes or schedules anything. Remediation is an
21
+ operator action you may recommend, never perform.
22
+ - **No ranking, citation, traffic or revenue promise.** The audit cannot observe indexing, ranking,
23
+ visits, revenue, bot behavior or answer-engine citations. Say "not measured" where that is true.
24
+ - **Label the evidence class** in every statement: observed **measurement**, **heuristic**,
25
+ **unknown**, **model judgment / advisory**, or **consultation**. `references/method.md` §1.
26
+ - **Crawler purposes stay separate:** search-indexing, model-training and user-triggered fetchers
27
+ have different policies and different consequences. `references/seo-geo-sources.md` §2.
28
+ - **`vendors/` is reference-only:** excluded from runtime and install resolution; never read or copy
29
+ it into a shipped file, and never wrap the vendor Python.
30
+ - **Specialist advice is consultation.** Legal, hosting, server, analytics or i18n work outside the
31
+ collected evidence is flagged as consultation with the missing evidence named — never presented
32
+ as an audit finding.
33
+ - **No phantom metrics.** Every number in your answer comes from an artifact field, and you name it.
34
+
35
+ ## 1. Route the request
36
+
37
+ | Input | Path |
38
+ | --- | --- |
39
+ | A public URL / domain | Full workflow run (§3→§4) with a written options file. |
40
+ | A saved audit directory (`audit.json` present) | Skip to review/interpret (§5→§6); rescore with §8 when the rubric or inputs changed. |
41
+ | A question about an existing report | Read `report.json` + `audit.json` + `checks.json`, answer from the fields (§5). |
42
+ | "Make the site rank / get cited" | Explain what the evidence does and does not support (§0, §6) and produce the action plan. |
43
+
44
+ Never invent a run id, work directory or artifact path. Resolve them from the run-bound file
45
+ `.spur/run/<runId>-seo-work-dir.txt`, and read the package root from the running executable
46
+ (`references/operations.md` §1) — never from a hard-coded checkout path.
47
+
48
+ ## 2. Intake and the options file
49
+
50
+ Confirm with the operator: the exact URL, the intended audience locale, any business context worth
51
+ recording (≤ 2,000 characters — operator context, not website evidence), whether measured
52
+ performance evidence is desired (PSI/CrUX keys present), and whether an imported visibility
53
+ snapshot exists. Then write a strict JSON options file — fields, defaults, bounds and the imported
54
+ snapshot contract are in `references/operations.md` §3, §5. Pass the file **path** as data; never
55
+ template a user value into shell source, and refuse NUL input.
56
+
57
+ ## 3. Register the decision catalogs (integrated runs)
58
+
59
+ The workflow's preflight passes `require_decisions=true`: the four SEO catalogs (22 decisions) must
60
+ be served, or preflight fails with the registration pointer. The supported setup is an **append** of
61
+ `<package-root>/plugins/kk/decisions` to the consumer project's `decisions.paths`, preserving every
62
+ existing entry and every maker setting. Verify with `spur decision list|show|status --json` — that
63
+ is configuration evidence, never model health. Exact steps and failure text:
64
+ `references/operations.md` §2. Collection-only stage use and the offline fixture mode need no
65
+ catalogs.
66
+
67
+ ## 4. Run it, bounded
68
+
69
+ ```bash
70
+ spur workflow run <package-root>/plugins/kk/workflows/kk-smart-seo.yaml \
71
+ --vars '{"options_file":"/abs/path/options.json"}' --quiet
72
+ ```
73
+
74
+ `preflight → collect → check → enrich → judge → score → narrative → report`, terminal
75
+ `done | partial | failed`. Leave `work_dir` empty so preflight allocates it and records the
76
+ run-bound path. Show progress from the workflow trace and the run record, not by guessing a
77
+ directory; report `partial` as successful delivery with named missing evidence, and `failed` as the
78
+ only failure state. Full command/artifact/progress detail: `references/operations.md` §6–§8.
79
+ For a run with no network access (fixture mode), the same workflow executes offline — say so
80
+ explicitly and never present fixture-derived numbers as a live site audit.
81
+
82
+ ## 5. Review the evidence before reading any judgment
83
+
84
+ 1. `report.json` — `status`, `narrativeStatus`, `scores`, `coverage`, `actions` (already ordered).
85
+ 2. Start with `P1`, then `P2` actions. For each: severity, category, affected pages, `effort` band,
86
+ and `heuristic` (true = rule-of-thumb over observed facts, false = directly observed).
87
+ 3. Confirm each action against `checks.json`: `status`, `reason`, `evidenceRefs`, `sourceUrl`,
88
+ `sourceCheckedAt`. A failing row with a `heuristic: true` flag is a hypothesis to verify, not a
89
+ defect.
90
+ 4. Read `audit.json` `/limits/*` and `coverage` (`assessed`, `unknown`, `ratio`, `hitLimit`). A
91
+ limit or a large `unknown` count bounds every conclusion; orphan/depth/duplicate observations are
92
+ "among sampled pages".
93
+ 5. Treat policy-looking rows carefully: robots directives, intentional `noindex`, canonical-to-other
94
+ and missing optional files (`robots_missing`, `sitemap_missing`, `not_in_sitemap`,
95
+ `llms_txt_missing`) are **signals requiring operator intent**, not automatic defects.
96
+ 6. When a row's `sourceCheckedAt` is old relative to the question, refresh the primary source first
97
+ (`references/seo-geo-sources.md` §3) instead of asserting behavior from memory.
98
+
99
+ ## 6. Interpret for the operator — per area
100
+
101
+ Explain each area from the recorded evidence and stop where the evidence stops. Rubric, scoring and
102
+ the full "cannot conclude" list live in `references/method.md` §3 and §6.
103
+
104
+ | Area | What the evidence supports | What it must not become |
105
+ | --- | --- | --- |
106
+ | **Technical** | Observed HTTP status/redirect chains, canonical and hreflang tags, indexability directives, headers, HTTPS/host variants, response size and timings. | Proof of indexing, rendering or crawl-budget impact. `js_dependent` is raw-HTML shell suspicion only — no browser rendering. |
107
+ | **On-page** | Title/meta/H1–H3/outline/link/anchor/image facts, structured-data syntax and type inventory, Open Graph presence. | A ranking explanation. Length thresholds are heuristics; structured data is data, and its presence is not an AI or rich-result eligibility requirement. |
108
+ | **Content / editorial** | Visible word counts (an editorial measure, language-limited), duplicate/thin-content observations, sampled text. | A quality verdict or an audience judgment. Model judgments (`source: model`) are advisory with a maker and a confidence. |
109
+ | **Local** | Declared locale/origin facts, contact and business-entity signals, `kk-seo-local-intent`. | Local-pack or maps presence claims — not measured. |
110
+ | **Ecommerce** | Product/category page-type and action classifications, duplicated titles/meta, canonical and redirect observations on product URLs. | Price, feed or merchant-center diagnostics — out of scope; name that as consultation. |
111
+ | **International** | hreflang observations, locale declarations, duplicate-content observations across host/path variants. | A statement that a market does or does not receive traffic. Machine-translation quality is consultation. |
112
+ | **GEO (generative engines)** | Editorial diagnostics — intent satisfaction, first-hand specificity, verifiable provenance, entity consistency, answer-first structure — plus imported citation/visibility snapshots when supplied. | Any citation, mention or inclusion promise. Blocking a training crawler is a policy choice, not a search defect. |
113
+
114
+ ### Crawler policies, kept separate
115
+
116
+ Explain a robots observation by the crawler's **purpose**, never as one verdict, and report the
117
+ observed directive plus the path it applies to:
118
+
119
+ - **Search indexing** (`Googlebot`, `Bingbot`, `OAI-SearchBot`) — affects eligibility for search and
120
+ search-grounded answer surfaces. A deliberate restriction is an operator decision, not a defect.
121
+ - **Model training** (`GPTBot`, `Google-Extended`, `ClaudeBot`, `CCBot`) — a data-policy choice.
122
+ Blocking it is not a search defect; allowing it promises nothing.
123
+ - **User-triggered fetch** (`ChatGPT-User` and similar) — serves a user's explicit request; a block
124
+ limits that session, not search eligibility.
125
+
126
+ A router robots group (the audit's own user agent) is likewise an operator policy. Never claim what
127
+ any crawler actually did: the audit observes the site's declared policy and its own requests only.
128
+
129
+ ### When evidence is missing
130
+
131
+ Say which of the three it is — **not collected** (outside the bounded crawl), **collected but
132
+ unknown** (`status: unknown`), or **not applicable** (`not-applicable`) — and name the input that
133
+ would settle it (a larger budget, a supplied visibility snapshot, an operator answer about intent).
134
+ Consultation is the right label for work this method cannot evidence at all.
135
+
136
+ ## 7. Write the narrative
137
+
138
+ Two different artifacts — keep them apart:
139
+
140
+ - **Validated narrative** (`narrative.json`): the §9 schema — allow-listed headings, fixed
141
+ `fact | action | limit` templates and `evidenceRef` pointers into the materialized `audit.json`,
142
+ bound to its sha256 and run id. Selecting and ordering recorded references is allowed; adding a
143
+ measurement, diagnosis or prediction is not, and any invalid claim rejects the whole narrative
144
+ (which then degrades to the labeled deterministic summary). Schema, worked example and rejection
145
+ cases: `references/narrative.md`.
146
+ - **Advisory prose** (your answer): the explanation, prioritization and operator-facing advice.
147
+ Label evidence classes, cite artifact fields, mark consultation, and describe what the report
148
+ deliberately cannot say. The report accepts no free prose — `references/narrative.md` §4.
149
+
150
+ ## 8. Offline rescoring
151
+
152
+ To re-score a saved audit without touching the network, the source run or its artifacts:
153
+
154
+ ```bash
155
+ kk stage run seo-score <saved-work_dir> <new-revision_dir> # revision dir must be absent/empty
156
+ kk stage run seo-report <new-revision_dir>
157
+ ```
158
+
159
+ The revision copies the validated inputs beside a fresh `audit.json`, keeps the original `runId`
160
+ and adds a `revisionId`, so source artifacts stay immutable and two revisions are comparable as
161
+ durable directories. Never compare scores across runs with different coverage or `methodVersion`,
162
+ and never patch a saved artifact in place. Details: `references/operations.md` §9.
163
+
164
+ ## 9. Failures and limits — report, never paper over
165
+
166
+ Unserved catalogs, invalid options, a populated `work_dir`, a refused network target
167
+ (local/reserved address), budget exhaustion, unavailable optional PSI/CrUX evidence, maker
168
+ fallback/low confidence, malformed predecessors, a rejected narrative and cancellation each have a
169
+ defined handling in `references/operations.md` §8. Three habits matter most: never present an
170
+ exhausted crawl as complete, never turn an unknown or a low-confidence judgment into a defect, and
171
+ never convert "not measured" into "passed".
172
+
173
+ ## 10. References
174
+
175
+ | Reference | Owns |
176
+ | --- | --- |
177
+ | `references/operations.md` | Package-root resolution, catalog registration and failure text, options fields, imported snapshot, run/progress commands, per-artifact map, failure handling, offline rescoring. |
178
+ | `references/method.md` | Evidence classes, the 52-rule checklist structure, score/coverage formulas, action ordering and priorities, how to read a check back to its evidence, and the method's explicit limits. |
179
+ | `references/seo-geo-sources.md` | Primary source registry with checked-at dates and per-source limits, crawler-purpose separation, the source refresh procedure, GEO limits, and the `vendors/` reference-only rule. |
180
+ | `references/narrative.md` | The validated narrative schema, worked example, rejection matrix, and the boundary with advisory prose. |
181
+
182
+ Attribution for the adapted rubric, rule identities and upstream notice:
183
+ `plugins/kk/references/smart-seo-attribution.md`.
@@ -0,0 +1,126 @@
1
+ # Smart SEO — method, rubric and score interpretation
2
+
3
+ The rubric is a **versioned internal checklist**, not a vendor grade and not a search-engine
4
+ verdict. Every claim below is what the artifacts record; nothing here measures indexing, ranking,
5
+ traffic, revenue, bot behavior or citation visibility.
6
+
7
+ ## 1. Evidence classes — label them every time you report
8
+
9
+ | Class | Meaning | Recorded where |
10
+ | --- | --- | --- |
11
+ | **Measurement / observed fact** | A value the audit actually observed within its stated budget (HTTP status, title text, robots directive, response bytes, timings). | `crawl.json` page facts, `checks.json` row `evidenceRefs`, `audit.json` `/facts/*` |
12
+ | **Heuristic** | A bounded rule-of-thumb over observed facts. `heuristic: true` on the check row and on the derived action. Examples: page depth, thin content (visible word count), title length, heading skips, orphan/depth observations "among sampled pages". | `checks.json` / `audit.json` `heuristic` |
13
+ | **Unknown** | Not assessed or not assessable: malformed, unavailable, budget-exhausted, blocked, or outside the collected scope. Never rendered as a pass. | `checks.json` status `unknown`, `coverage.unknown`, `audit.json` `/limits/*` |
14
+ | **Model judgment** | An editorial output from a typed decision (`source: model`, with maker and confidence). Advisory, never a measurement. | `judgments.json`, `audit.json` `/judgments` |
15
+ | **Imported evidence** | A supplied visibility snapshot or PSI/CrUX measurement, with source, collection window and `measurementKind`. | `performance.json`, `visibility.json` |
16
+ | **Consultation** | Advice outside the collected evidence (specialist legal/hosting/i18n/analytics work). Say so explicitly and name the missing evidence. | nowhere — it must not enter the report as a finding |
17
+
18
+ Rule of thumb: a value is a **measurement** only if some artifact records it with a source; a
19
+ judgment, an inference or a threshold over content is a **heuristic** or **advisory** statement.
20
+
21
+ ## 2. Checklist structure
22
+
23
+ 52 deterministic rules across five categories — `technical` (22), `content` (16), `crawl` (8),
24
+ `security` (4), `geo` (2). Each rule keeps a stable identity, a severity, an effort band, a
25
+ `heuristic` flag, a primary `sourceUrl` and a `sourceCheckedAt` date, and one disposition from the
26
+ vendor review (`native`, `adapted`, `informational`, `deferred`).
27
+
28
+ Every eligible rule/subject pair yields exactly **one** row:
29
+
30
+ ```json
31
+ {
32
+ "ruleId": "title_missing",
33
+ "subjectId": "<hashed subject identity>",
34
+ "subjectUrl": "https://example.com/a",
35
+ "status": "fail",
36
+ "category": "content",
37
+ "severity": "high",
38
+ "evidenceRefs": ["https://example.com/a"],
39
+ "sourceUrl": "https://developers.google.com/search/docs/appearing-in-search/...",
40
+ "sourceCheckedAt": "2026-10-14",
41
+ "heuristic": false,
42
+ "reason": "Human-readable reason for this status"
43
+ }
44
+ ```
45
+
46
+ `status` is `pass | fail | unknown | not-applicable`. **The absence of a row is an error, never a
47
+ pass.** `not-applicable` means the rule cannot apply to that subject (for example a site-level rule
48
+ against a page-level subject); it is excluded from both the score and the coverage denominator.
49
+ A `deferred`-disposition rule applies but is not assessed in this method version, so it emits
50
+ `unknown` and counts in `coverage.unknown`.
51
+
52
+ Intent-sensitive observations are not defects by themselves: a robots directive, an intentional
53
+ `noindex`, a canonical pointing elsewhere, a missing optional file or missing optional metadata all
54
+ require operator intent before they become a recommendation. Missing SEO files never turn into
55
+ indexing requirements in the narrative.
56
+
57
+ ## 3. Scores
58
+
59
+ Category score = `round(100 * pass / (pass + fail))` over the **applicable assessed** rows of that
60
+ category, and `null` when that category has no assessed row:
61
+
62
+ | Field | Meaning |
63
+ | --- | --- |
64
+ | `scores.technical` / `content` / `crawl` / `security` | The checklist pass ratio in that category. `null` = undefined denominator, never 0. |
65
+ | `coverage.totalRules` | 52 (the checklist size). |
66
+ | `coverage.applicableRules` | Rows whose status is not `not-applicable`. |
67
+ | `coverage.assessed` | Rows with `pass` or `fail`. |
68
+ | `coverage.unknown` | Applicable rows left `unknown`. |
69
+ | `coverage.ratio` | `assessed / (assessed + unknown)` — how much of the applicable checklist was actually assessed. |
70
+
71
+ Interpretation rules:
72
+
73
+ - There is **no blended overall SEO/GEO grade** in this method version. Report the four category
74
+ ratios with their coverage side by side; a ratio published without its coverage is misleading.
75
+ - `geo` rules are assessed and prioritized like any other row but produce no category score.
76
+ - `informational` rules (vendor-review disposition) are reported but never enter a category score:
77
+ they neither deduct on `fail` nor pad the ratio on `pass`.
78
+ - Unknown and review values never count as successes, and a disabled or unavailable optional area
79
+ is reported as such rather than folded into a pass.
80
+ - Never compare scores across two runs with different coverage, different `methodVersion`, or
81
+ different options — record the denominators with the number.
82
+ - A high score means "few sampled rules failed", not "the site is healthy". A low score does not
83
+ prove a traffic or ranking problem.
84
+
85
+ ## 4. Action ordering and priorities
86
+
87
+ One action per failing rule, carrying the full affected-subject list (aggregation must not hide
88
+ unknown subjects). Stable ID `SEO-<ruleId>-<first12(sha256(subject identity))>` — sorting never
89
+ renumbers an ID, so IDs stay comparable across revisions.
90
+
91
+ Order: severity (`critical`, `high`, `medium`, `low`, `info`) → affected-page count descending →
92
+ effort band ascending → stable ID.
93
+
94
+ | Priority | Meaning |
95
+ | --- | --- |
96
+ | `P1` | Confirmed `critical` or `high` severity failures. |
97
+ | `P2` | Confirmed `medium` severity failures. |
98
+ | `P3` | Confirmed low-severity failures. |
99
+ | `review` | Uncertain items — kept out of fix-first lists until the operator confirms intent or supplies missing evidence. |
100
+
101
+ `effort` is a declared planning band (1–4), not a quote or an estimate in hours. Severity expresses
102
+ the rule's consequence, not its certainty; read it together with `heuristic` and with `coverage`.
103
+ Editorial (content/GEO) scores can justify a review suggestion, never confirm a technical fact.
104
+
105
+ ## 5. Reading a check back to its evidence
106
+
107
+ 1. Take the action's `ruleId`, `affectedSubjects` and stable ID.
108
+ 2. Open `checks.json` and select the rows with that `ruleId`; read `status`, `reason`,
109
+ `evidenceRefs`, `sourceUrl` and `sourceCheckedAt`.
110
+ 3. Read the underlying page facts in `crawl.json` (or the referenced `evidence/pages/*.json`).
111
+ 4. Read `audit.json` `/limits/*` and `coverage` before drawing any conclusion: a limit or a high
112
+ `unknown` count bounds what the finding can mean.
113
+ 5. If the primary source page has changed since `sourceCheckedAt`, refresh the source before
114
+ relying on the rule's stated rationale (see `seo-geo-sources.md`).
115
+
116
+ ## 6. What this method cannot conclude
117
+
118
+ - Whether a URL is indexed, ranking, receiving traffic or being cited by any AI answer engine.
119
+ - What a crawler actually did: the audit observes the site's **declared** policy
120
+ (robots.txt, meta robots, sitemap) and its own requests — never another crawler's behavior.
121
+ - How a browser renders a JavaScript-dependent page: rendering is deferred, so a raw-HTML shell is
122
+ suspicion only.
123
+ - Whether an editorial judgment is correct: adequacy of a value proposition, content depth or
124
+ citability is an opinion with a maker and a confidence, not a fact.
125
+ - Performance causes or fixes: PSI lab metrics and CrUX field data, when supplied, describe
126
+ observed timing, not the work needed to change it.
@@ -0,0 +1,125 @@
1
+ # Smart SEO — the validated narrative contract
2
+
3
+ The narrative is **optional** and **fully validated**. It selects and orders references into the
4
+ materialized `audit.json`; it cannot introduce a measurement, unit, diagnosis or prediction. When
5
+ it is absent or rejected the report is still delivered with `narrativeStatus: omitted|rejected`
6
+ plus a deterministic summary — an optional agent failure is never an audit failure.
7
+
8
+ ## 1. Schema (schema version 1)
9
+
10
+ ```json
11
+ {
12
+ "schemaVersion": 1,
13
+ "runId": "<audit.runId>",
14
+ "auditDigest": "<sha256 of the exact audit.json bytes>",
15
+ "sections": [
16
+ {
17
+ "heading": "summary | priorities | limits",
18
+ "claims": [
19
+ { "template": "fact | action | limit", "evidenceRef": { "artifact": "audit.json", "jsonPointer": "/facts/<key> | /actions/<id> | /limits/<key>" } }
20
+ ]
21
+ }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ Hard limits and exact shapes — every key below is the *only* accepted key at its level:
27
+
28
+ | Rule | Value |
29
+ | --- | --- |
30
+ | Root keys | exactly `schemaVersion`, `runId`, `auditDigest`, `sections` |
31
+ | `schemaVersion` | `1` |
32
+ | `runId` | must equal the audit's `runId` |
33
+ | `auditDigest` | must equal the sha256 of the materialized `audit.json` (stale or unknown digest rejects) |
34
+ | `sections[]` keys | exactly `heading`, `claims`; at least one section; `claims` non-empty |
35
+ | `heading` | one of `summary`, `priorities`, `limits` |
36
+ | `claims[]` keys | exactly `template`, `evidenceRef`; unknown keys anywhere (including a free-prose `text`) reject the whole narrative |
37
+ | `template` | one of `fact`, `action`, `limit` |
38
+ | `evidenceRef` keys | exactly `artifact`, `jsonPointer`; `artifact` must be `audit.json` |
39
+ | `jsonPointer` | exactly two segments: the template's own collection plus one key — `/facts/<key>` for `fact`, `/actions/<actionId>` for `action`, `/limits/<key>` for `limit` |
40
+ | Pointer targets | `fact` must address a recorded scalar observed fact (`/facts/pages_visited`, `/facts/requests_made`, `/facts/total_bytes`, …); `action` must address an existing action **by stable id** (never an array index); `limit` must address a recorded limit (`pagesVisited`, `requestsMade`, `totalBytes`, `elapsedSeconds`, `hitLimit`) |
41
+ | Bounds | ≤ 32 KiB raw and ≤ 30 claims |
42
+
43
+ Prototype keys (`__proto__`, `constructor`, `prototype`) and out-of-collection pointers are refused.
44
+ Rendering is method-owned: each claim becomes an `**Observed fact**`, `**Planned action**` or
45
+ `**Recorded limit**` line with the audit's own value, unit and source, and Markdown-escaped.
46
+
47
+ ## 2. Worked example
48
+
49
+ Given an audit whose `runId` is `8f2c…`, whose `auditDigest` is the real sha256 of `audit.json`,
50
+ and whose actions include `SEO-title_missing-0f115db062b7`:
51
+
52
+ ```json
53
+ {
54
+ "schemaVersion": 1,
55
+ "runId": "8f2c1d0e-…",
56
+ "auditDigest": "<sha256 of audit.json>",
57
+ "sections": [
58
+ {
59
+ "heading": "summary",
60
+ "claims": [
61
+ { "template": "fact", "evidenceRef": { "artifact": "audit.json", "jsonPointer": "/facts/pages_visited" } },
62
+ { "template": "fact", "evidenceRef": { "artifact": "audit.json", "jsonPointer": "/facts/requests_made" } }
63
+ ]
64
+ },
65
+ { "heading": "priorities", "claims": [{ "template": "action", "evidenceRef": { "artifact": "audit.json", "jsonPointer": "/actions/SEO-title_missing-0f115db062b7" } }] },
66
+ { "heading": "limits", "claims": [{ "template": "limit", "evidenceRef": { "artifact": "audit.json", "jsonPointer": "/limits/hitLimit" } }] }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ Renders as (in `report.md`, section 3):
72
+
73
+ ```markdown
74
+ ### Summary
75
+
76
+ - **Observed fact** `pages_visited` = `12` count — source: `crawl.json`
77
+ - **Observed fact** `requests_made` = `34` count — source: `crawl.json`
78
+
79
+ ### Priorities
80
+
81
+ - **Planned action** `SEO-title_missing-0f115db062b7` (high, P1) — Fix title missing — 3 affected page(s), effort band 1
82
+
83
+ ### Limits
84
+
85
+ - **Recorded limit** `hitLimit` = `none`
86
+ ```
87
+
88
+ To produce this in a run: read `<work_dir>/audit.json`, compute its sha256 over the exact file
89
+ bytes, keep the same `runId`, then write the object to `<work_dir>/narrative.json` **before**
90
+ `seo-report` runs (the workflow's `agent.run` step does exactly that, with
91
+ `answerFile: <work_dir>/narrative.json`). Replying `{}` is the documented way to decline: the
92
+ report then falls back to its deterministic summary.
93
+
94
+ ## 3. Rejection cases (each rejects the entire narrative)
95
+
96
+ | Case | Diagnostic shape |
97
+ | --- | --- |
98
+ | Free prose added to a claim (`text: "…"`) or any unknown key | `sections[0].claims[1]: unknown key "text"` |
99
+ | Heading outside the allow list (`"Overview"`) | `sections[0]: heading "Overview" is not an allow-listed heading` |
100
+ | Template mismatch with the pointer (`fact` + `/limits/…`) | `template "fact" requires a pointer into /facts, got "/limits"` |
101
+ | Invented fact or limit key | `unknown fact id "domain_authority"` / `unknown limit id "ranking_position"` |
102
+ | Invented or misspelled action id, or an array index (`/actions/0`) | `unknown action id "0"` |
103
+ | Pointer deeper or shallower than two segments (`/facts/a/b`) | `jsonPointer "…" must address exactly one record (collection/key)` |
104
+ | Stale digest (audit re-scored after the narrative was written) | `narrative auditDigest does not match the materialized audit.json digest (stale or unknown digest)` |
105
+ | Wrong run id | `narrative runId "…" does not match the audit runId` |
106
+ | Non-object root, non-JSON bytes, empty sections, > 32 KiB, > 30 claims | `narrative root must be a JSON object` / `narrative is not valid JSON: …` / `narrative sections must not be empty` |
107
+
108
+ On rejection the stage writes `narrative-diagnostics.json` beside the audit, sets
109
+ `report.json.narrativeStatus: 'rejected'`, and renders the deterministic summary labeled
110
+ `**Narrative rejected**`. Never edit the saved `narrative.json` to force acceptance, and never move
111
+ a rejected claim into the deterministic summary by hand: the summary is method-owned.
112
+
113
+ ## 4. Advisory prose lives outside the report
114
+
115
+ The validated narrative deliberately cannot carry commentary. If the operator asks for
116
+ explanation, interpretation or an action plan in prose, that is the **skill's** output: keep it in
117
+ conversation (or a separate operator-authored document) and:
118
+
119
+ - label every statement as measurement, heuristic, model judgment or unknown (see `method.md`);
120
+ - cite the artifact plus the field (`report.json` action id, `checks.json` row, `audit.json`
121
+ `/facts/*`) for every number;
122
+ - mark specialist advice outside the collected evidence as **consultation**, naming the missing
123
+ evidence and the source that would be needed;
124
+ - make no promise about rankings, citations, traffic, revenue or crawler behavior, and propose no
125
+ change that mutates the site — the audit is read-only, and any remediation is an operator action.
@@ -0,0 +1,202 @@
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
+ **The catalogs' declared schema must resolve from where they live.** Each shipped SEO catalog
56
+ declares `$schema: "@gobing-ai/spur/schemas/decision-catalog.schema.json"`, and Spur resolves that
57
+ schema through the module tree above the catalog's own directory. A global install satisfies this
58
+ by construction (both packages are siblings in the global tree); with a project-local KK install,
59
+ keep `spur` resolvable from the installed package's location or `decision status --json` reports
60
+ the catalogs as `loadErrors` (`Cannot resolve package schema …`) and preflight fails. A load error
61
+ is a setup defect to fix, never a reason to fall back to a maker default silently.
62
+
63
+ ## 3. Options file
64
+
65
+ Write a UTF-8 JSON object (≤ 64 KiB, strict schema — unknown keys are refused) and pass its path
66
+ as data, never as shell source:
67
+
68
+ ```json
69
+ {
70
+ "url": "https://example.com",
71
+ "locale": "en-US",
72
+ "business_context": "",
73
+ "max_pages": 60,
74
+ "max_depth": 5,
75
+ "max_requests": 200,
76
+ "deadline_seconds": 600,
77
+ "semantic_pages": 20,
78
+ "max_pairs": 10,
79
+ "max_keywords": 10,
80
+ "max_decision_calls": 320,
81
+ "performance": false,
82
+ "visibility_file": "",
83
+ "fixture": ""
84
+ }
85
+ ```
86
+
87
+ | Field | Meaning and bounds |
88
+ | --- | --- |
89
+ | `url` | Required HTTP(S) URL or bare domain (bare becomes HTTPS). Userinfo, nonstandard ports, local/reserved addresses and malformed hostnames are refused. |
90
+ | `locale` | BCP-47; describes the report audience and the imported query market. It never assumes the site's language. |
91
+ | `business_context` | Optional operator context ≤ 2,000 UTF-16 units. It is context, not website evidence. |
92
+ | `max_pages` / `max_depth` | 60 / 5 by default; maxima 200 / 10. The page quota counts every attempt, including blocked, redirect and error records. |
93
+ | `max_requests` / `deadline_seconds` | 200 / 600 by default; maxima 500 / 1800. One shared quota and deadline covers robots, sitemaps, pages, probes and link targets. |
94
+ | `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). |
95
+ | `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. |
96
+ | `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. |
97
+ | `visibility_file` | Optional normalized local JSON snapshot (§4). Never an arbitrary provider dump. |
98
+ | `fixture` | Explicit offline fixture **directory**. With a fixture, network access and real maker dispatch are forbidden. Production never falls back to fixture data. |
99
+
100
+ ## 4. Audit scope — the submitted host plus its exact `www` toggle
101
+
102
+ Scope is the normalized submitted hostname with **one** leading `www.` removed when present, or
103
+ added when absent — nothing else. `shop.example.com` admits `shop.example.com` and
104
+ `www.shop.example.com` only: no registrable apex, no sibling subdomains, and default ports stay
105
+ origin-specific. Off-scope redirects and links are recorded, never followed; `www`/non-`www` and
106
+ HTTP/HTTPS transitions are observations, not defects. There is no browser rendering and no outbound
107
+ external-link fetching in this version: state that assessment as **not collected** rather than
108
+ assuming a result.
109
+
110
+ ## 5. Imported visibility snapshot
111
+ ```json
112
+ {
113
+ "schemaVersion": 1,
114
+ "siteOrigin": "https://example.com",
115
+ "locale": "en-US",
116
+ "source": "search-console-export|analytics-export|manual",
117
+ "collectedAt": "2026-10-01T00:00:00Z",
118
+ "measurementKind": "measured",
119
+ "rows": [{ "query": "example query", "url": "https://example.com/a", "clicks": 4, "impressions": 120, "position": 8.5, "citations": 2, "sampleCount": 500, "platform": "chatgpt" }]
120
+ }
121
+ ```
122
+
123
+ Maximum 1 MiB / 500 rows; strings bounded; counts are finite nonnegative numbers; `position` ≥ 1;
124
+ `citations` ≤ `sampleCount` with `platform` required; any `url` must belong to the audit scope.
125
+ Unknown properties, a conflicting locale or site identity, a future timestamp, or evidence older
126
+ than 90 days fails validation before use. Keywords come from this data — never invented volumes.
127
+
128
+ ## 6. Run the workflow
129
+
130
+ ```bash
131
+ spur workflow run <PKG_ROOT>/plugins/kk/workflows/kk-smart-seo.yaml \
132
+ --vars '{"options_file":"/abs/path/options.json"}' --quiet
133
+ ```
134
+
135
+ Leave `work_dir` empty: `seo-preflight` allocates `.works/kk-smart-seo/<UTC-stamp>-<runId>` with
136
+ create-new semantics (an existing non-empty directory is refused) and writes the resolved absolute
137
+ path to `.spur/run/<runId>-seo-work-dir.txt`, which the workflow reads back into `work_dir` before
138
+ collection. A fixed `work_dir` must name an absent or empty directory.
139
+
140
+ Stages: `preflight → collect → check → enrich → judge → score → narrative → report`, terminal
141
+ `done | partial | failed`. `done` means every requested stage assessed its eligible evidence — not
142
+ that the site has no SEO issues. `partial` is successful artifact delivery with missing requested
143
+ evidence (an unavailable optional PSI/CrUX/typed-decision input is never a failure). `failed` is the
144
+ sole failure state.
145
+
146
+ Individual stages stay independently callable for review and re-scoring:
147
+
148
+ ```bash
149
+ kk stage run seo-preflight <work_dir> <options_file> [run_id] [require_decisions]
150
+ kk stage run seo-collect <work_dir>
151
+ kk stage run seo-check <work_dir>
152
+ kk stage run seo-enrich <work_dir>
153
+ kk stage run seo-judge <work_dir>
154
+ kk stage run seo-score <work_dir> [new_revision_dir]
155
+ kk stage run seo-report <work_dir>
156
+ ```
157
+
158
+ ## 7. Progress
159
+
160
+ - `spur workflow trace <run-id>` — the action trace; the state id tells you which stage is running.
161
+ - `.spur/run/<runId>-seo-work-dir.txt` — the run-bound workspace path (never guess it).
162
+ - `.spur/memory/runs/<run-id>.md` — the two-file run record.
163
+ - Stage progress goes to stderr and is captured in the run record; long collection stages are
164
+ bounded by `max_requests`/`deadline_seconds`, so a run always terminates.
165
+
166
+ Artifacts under the work dir, one run id shared by all of them: `options.json`, `manifest.json`,
167
+ `crawl.json`, `checks.json`, `judgments.json`, `performance.json`, `visibility.json`, `audit.json`,
168
+ `digest.md`, `narrative.json` (optional), `report.json`, `report.md`. Every JSON object carries
169
+ `schemaVersion`, `methodVersion`, `runId`, `createdAt`, `inputDigest` and predecessor digests;
170
+ wrong run, version or digest is a hard error. Writes are sibling temp file + rename, so a reader
171
+ never sees a partially written value. The saved audit is durable under `.works/`, not volatile
172
+ `.spur/run/`.
173
+
174
+ ## 8. Failure handling
175
+
176
+ | Symptom | Handling |
177
+ | --- | --- |
178
+ | Missing/unserved SEO catalogs | Apply §2, re-run. Preflight failure text names the missing files/ids. |
179
+ | Invalid options | Fix the named field; the file is validated before any collection. |
180
+ | `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. |
181
+ | 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. |
182
+ | Budget exhaustion (`limits.hitLimit`) | Report it as a limit. Never present an exhausted crawl as a complete one. |
183
+ | 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. |
184
+ | Maker fallback / low confidence | The judgment stays `unknown`/`review`. It is not a site defect and not a measurement. |
185
+ | Malformed predecessor artifact | Hard error, nonzero exit — malformed input is never reinterpreted as an empty result. |
186
+ | 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. |
187
+ | Cancellation | Active sockets and decision children are aborted, completed artifacts are preserved with a cancellation marker, and no new requests or decisions start. |
188
+
189
+ ## 9. Offline rescoring
190
+
191
+ Score a saved audit again without touching the network or the source run:
192
+
193
+ ```bash
194
+ kk stage run seo-score <saved-work_dir> <new-revision_dir> # revision dir must be absent/empty
195
+ kk stage run seo-report <new-revision_dir>
196
+ ```
197
+
198
+ The revision copies the validated bounded inputs (`options.json`, `manifest.json`, `crawl.json`,
199
+ `checks.json`) beside a fresh `audit.json`, keeps the original `runId` and adds a new `revisionId`.
200
+ Source artifacts stay immutable, so a comparison between revisions reads two durable directories.
201
+ Do not compare scores across runs with different coverage: `coverage` records the denominators,
202
+ and the score is undefined (`null`) when no row of that category is applicable.