@gobing-ai/knowledge-kit 0.0.37 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/board/collect/index.css +654 -16
- package/dist/board/collect/index.js +191 -64
- package/dist/board/daily-voice/index.css +679 -12
- package/dist/board/daily-voice/index.js +185 -66
- package/dist/board/itc/index.css +679 -13
- package/dist/board/itc/index.js +188 -66
- package/dist/index.js +2864 -139
- package/package.json +3 -2
- package/plugins/generations/content-gen/dist/index.js +158 -12
- package/plugins/generations/core-facts-gen/dist/index.js +158 -12
- package/plugins/generations/daily-article-gen/dist/index.js +158 -12
- package/plugins/generations/dailynews-gen/dist/index.js +158 -12
- package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
- package/plugins/generations/gemini-tts/README.md +4 -3
- package/plugins/generations/gemini-tts/dist/index.js +158 -12
- package/plugins/generations/image-gen/dist/index.js +158 -12
- package/plugins/generations/news-report-gen/dist/index.js +158 -12
- package/plugins/generations/omni-voice-gen/README.md +15 -4
- package/plugins/generations/omni-voice-gen/profiles.json +1 -2
- package/plugins/generations/voice-gen/dist/index.js +158 -12
- package/plugins/generations/voxcpm-tts/README.md +21 -1
- package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
- package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
- package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
- package/plugins/ingestions/web-search/dist/index.js +158 -12
- package/plugins/kk/commands/workflow-run.md +56 -18
- package/plugins/kk/decisions/kk-itc.yaml +46 -0
- package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
- package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
- package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
- package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/chinese-article.json +14 -0
- package/plugins/kk/publish-presets/technical-article.json +14 -0
- package/plugins/kk/references/smart-seo-attribution.md +85 -0
- package/plugins/kk/scripts/daily-stages.ts +42 -2
- package/plugins/kk/scripts/itc-stages.ts +25 -118
- package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
- package/plugins/kk/scripts/seo-decisions.ts +477 -0
- package/plugins/kk/scripts/seo-enrich.ts +728 -0
- package/plugins/kk/scripts/seo-facts.ts +610 -0
- package/plugins/kk/scripts/seo-fetch.ts +766 -0
- package/plugins/kk/scripts/seo-report.ts +460 -0
- package/plugins/kk/scripts/seo-rules.ts +1882 -0
- package/plugins/kk/scripts/seo-stages.ts +1044 -0
- package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
- package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
- package/plugins/kk/skills/smart-seo/references/method.md +126 -0
- package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
- package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
- package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
- package/plugins/kk/workflows/feature-verification.yaml +28 -24
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
- package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
- package/plugins/kk/workflows/kk-itc.yaml +208 -682
- package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
- package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
- package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
- package/plugins/publishings/bilibili-pub/package.json +20 -0
- package/plugins/publishings/bilibili-pub/plugin.json +12 -0
- package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
- package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
- package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
- package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
- package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
- package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
- package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
- package/plugins/publishings/cnblogs-pub/package.json +19 -0
- package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
- package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
- package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
- package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
- package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
- package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
- package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
- package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
- package/plugins/publishings/csdn-pub/package.json +19 -0
- package/plugins/publishings/csdn-pub/plugin.json +12 -0
- package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
- package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
- package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
- package/plugins/publishings/emdash-pub/dist/index.js +158 -12
- package/plugins/publishings/emdash-pub/plugin.json +22 -1
- package/plugins/publishings/infoq-pub/dist/index.js +160 -12
- package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/infoq-pub/plugin.json +21 -0
- package/plugins/publishings/jike-pub/dist/index.js +21379 -0
- package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
- package/plugins/publishings/jike-pub/package.json +19 -0
- package/plugins/publishings/jike-pub/plugin.json +12 -0
- package/plugins/publishings/jike-pub/src/index.ts +800 -0
- package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
- package/plugins/publishings/jike-pub/tsconfig.json +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +160 -12
- package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/juejin-pub/plugin.json +21 -0
- package/plugins/publishings/medium-pub/dist/index.js +158 -12
- package/plugins/publishings/medium-pub/plugin.json +22 -1
- package/plugins/publishings/podcast-pub/dist/index.js +158 -12
- package/plugins/publishings/qiita-pub/dist/index.js +158 -12
- package/plugins/publishings/qiita-pub/plugin.json +24 -1
- package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
- package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
- package/plugins/publishings/shipinhao-pub/package.json +19 -0
- package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
- package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
- package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
- package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
- package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
- package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
- package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
- package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
- package/plugins/publishings/sspai-pub/package.json +19 -0
- package/plugins/publishings/sspai-pub/plugin.json +12 -0
- package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
- package/plugins/publishings/sspai-pub/src/index.ts +167 -0
- package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
- package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
- package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
- package/plugins/publishings/substack-pub/dist/index.js +160 -12
- package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/substack-pub/plugin.json +21 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
- package/plugins/publishings/surfdash-pub/plugin.json +22 -1
- package/plugins/publishings/threads-pub/dist/index.js +158 -12
- package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
- package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
- package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
- package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
- package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
- package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
- package/plugins/publishings/weibo-pub/dist/index.js +160 -12
- package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/weibo-pub/plugin.json +35 -0
- package/plugins/publishings/x-pub/dist/index.js +160 -12
- package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/x-pub/plugin.json +40 -0
- package/plugins/publishings/xhs-pub/dist/index.js +160 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
- package/plugins/publishings/xhs-pub/plugin.json +37 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
- package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
- package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
- package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
- package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
- package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +158 -12
- package/plugins/publishings/zenn-pub/plugin.json +22 -1
- package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import type { CrawlArtifact, PageRecord, SeoOptions } from './seo-facts';
|
|
5
|
+
import { assertSeoNotCancelled, SEO_CANCEL_SIGNALS, SeoStageCancelledError, writeJsonAtomic } from './seo-fetch';
|
|
6
|
+
|
|
7
|
+
export interface JudgmentEntry {
|
|
8
|
+
id: string; // decision id, e.g. "kk-seo-page-type"
|
|
9
|
+
subjectId: string;
|
|
10
|
+
subjectUrl: string;
|
|
11
|
+
type: 'choice' | 'score' | 'noul';
|
|
12
|
+
value: string | number | boolean;
|
|
13
|
+
confidence: number | null;
|
|
14
|
+
maker: string;
|
|
15
|
+
source: string; // "model" | "default" | "fallback" | "deterministic"
|
|
16
|
+
reason: string;
|
|
17
|
+
qualified: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface JudgmentsArtifact {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
methodVersion: string;
|
|
23
|
+
runId: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
inputDigest: string;
|
|
26
|
+
predecessorDigests: Record<string, string>;
|
|
27
|
+
status: 'complete' | 'partial';
|
|
28
|
+
entries: JudgmentEntry[];
|
|
29
|
+
summary: {
|
|
30
|
+
totalCalls: number;
|
|
31
|
+
successfulCalls: number;
|
|
32
|
+
qualifiedCalls: number;
|
|
33
|
+
fallbackCalls: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DecisionStubHandler = (id: string, params: Record<string, string>) => unknown;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Produce compact serialized evidence JSON <= 1,800 UTF-16 units.
|
|
41
|
+
*/
|
|
42
|
+
export function buildCompactEvidencePacket(
|
|
43
|
+
page: PageRecord,
|
|
44
|
+
siteOrigin: string,
|
|
45
|
+
): {
|
|
46
|
+
factsJson: string;
|
|
47
|
+
excerptJson: string;
|
|
48
|
+
} {
|
|
49
|
+
const facts = page.facts;
|
|
50
|
+
const rawFacts = {
|
|
51
|
+
url: page.url,
|
|
52
|
+
title: facts?.title ?? null,
|
|
53
|
+
metaDescription: facts?.metaDescription ?? null,
|
|
54
|
+
h1: facts?.headings.find((h) => h.level === 1)?.text ?? null,
|
|
55
|
+
canonical: facts?.canonical ?? null,
|
|
56
|
+
wordCount: facts?.wordCount ?? 0,
|
|
57
|
+
};
|
|
58
|
+
let factsJson = JSON.stringify(rawFacts);
|
|
59
|
+
if (factsJson.length > 1800) {
|
|
60
|
+
factsJson = JSON.stringify({
|
|
61
|
+
url: page.url,
|
|
62
|
+
title: facts?.title ? `${facts.title.slice(0, 100)}...` : null,
|
|
63
|
+
meta: facts?.metaDescription ? `${facts.metaDescription.slice(0, 150)}...` : null,
|
|
64
|
+
wordCount: facts?.wordCount ?? 0,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const text = facts?.textSample || '';
|
|
69
|
+
const openingText = text.slice(0, 400);
|
|
70
|
+
const middleText = text.length > 800 ? text.slice(400, 800) : '';
|
|
71
|
+
|
|
72
|
+
const excerptObj = {
|
|
73
|
+
url: page.url,
|
|
74
|
+
siteOrigin,
|
|
75
|
+
originalLength: text.length,
|
|
76
|
+
openingText,
|
|
77
|
+
middleText,
|
|
78
|
+
truncated: text.length > 800,
|
|
79
|
+
};
|
|
80
|
+
let excerptJson = JSON.stringify(excerptObj);
|
|
81
|
+
if (excerptJson.length > 1800) {
|
|
82
|
+
excerptJson = JSON.stringify({
|
|
83
|
+
url: page.url,
|
|
84
|
+
openingText: openingText.slice(0, 300),
|
|
85
|
+
truncated: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { factsJson, excerptJson };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Execute spur decision run via child process with strict bounds.
|
|
94
|
+
*/
|
|
95
|
+
export function executeDecisionRun(
|
|
96
|
+
decisionId: string,
|
|
97
|
+
params: Record<string, string>,
|
|
98
|
+
evidencePaths: string[],
|
|
99
|
+
options: { spurBin?: string; timeoutMs?: number; stub?: DecisionStubHandler } = {},
|
|
100
|
+
): JudgmentEntry {
|
|
101
|
+
const spurBin = options.spurBin || 'spur';
|
|
102
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
103
|
+
const subjectId = params.subject_id || '';
|
|
104
|
+
const subjectUrl = params.url || subjectId;
|
|
105
|
+
|
|
106
|
+
if (options.stub) {
|
|
107
|
+
try {
|
|
108
|
+
const raw = options.stub(decisionId, params) as Record<string, unknown>;
|
|
109
|
+
return validateAndNormalizeDecisionResult(decisionId, subjectId, subjectUrl, raw);
|
|
110
|
+
} catch (err: unknown) {
|
|
111
|
+
return {
|
|
112
|
+
id: decisionId,
|
|
113
|
+
subjectId,
|
|
114
|
+
subjectUrl,
|
|
115
|
+
type: 'choice',
|
|
116
|
+
value: 'unclear',
|
|
117
|
+
confidence: null,
|
|
118
|
+
maker: 'stub',
|
|
119
|
+
source: 'fallback',
|
|
120
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
121
|
+
qualified: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Cancellation refuses new dispatch (design §4); the stage aborts instead of recording fallbacks.
|
|
127
|
+
assertSeoNotCancelled();
|
|
128
|
+
|
|
129
|
+
const args = ['decision', 'run', decisionId];
|
|
130
|
+
for (const [k, v] of Object.entries(params)) {
|
|
131
|
+
args.push('--param', `${k}=${v}`);
|
|
132
|
+
}
|
|
133
|
+
for (const ev of evidencePaths) {
|
|
134
|
+
args.push('--evidence', ev);
|
|
135
|
+
}
|
|
136
|
+
args.push('--json');
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const res = spawnSync(spurBin, args, {
|
|
140
|
+
encoding: 'utf-8',
|
|
141
|
+
timeout: timeoutMs,
|
|
142
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// The signal handlers cannot run while spawnSync blocks, so a cancellation delivered to the
|
|
146
|
+
// process group is observed through the child's own termination signal instead: the child
|
|
147
|
+
// was killed by the same cancellation, which means this stage must stop rather than record
|
|
148
|
+
// the kill as a fallback result and dispatch another decision.
|
|
149
|
+
// spawnSync kills a timed-out child with SIGTERM too; that is this decision's timeout, not a
|
|
150
|
+
// cancellation, so it records a fallback before the signal check can misread it.
|
|
151
|
+
const timedOut = (res.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
|
|
152
|
+
if (!timedOut && res.signal !== null && (SEO_CANCEL_SIGNALS as readonly string[]).includes(res.signal)) {
|
|
153
|
+
throw new SeoStageCancelledError(res.signal as NodeJS.Signals, decisionId);
|
|
154
|
+
}
|
|
155
|
+
// A cancellation that arrived while the child ran already terminated it with the process
|
|
156
|
+
// group; the loop must stop here rather than treat the killed child as a fallback result.
|
|
157
|
+
assertSeoNotCancelled();
|
|
158
|
+
|
|
159
|
+
if (res.status !== 0 || !res.stdout) {
|
|
160
|
+
return {
|
|
161
|
+
id: decisionId,
|
|
162
|
+
subjectId,
|
|
163
|
+
subjectUrl,
|
|
164
|
+
type: 'choice',
|
|
165
|
+
value: 'unclear',
|
|
166
|
+
confidence: null,
|
|
167
|
+
maker: 'unknown',
|
|
168
|
+
source: 'fallback',
|
|
169
|
+
reason: timedOut
|
|
170
|
+
? `decision run timed out after ${timeoutMs}ms`
|
|
171
|
+
: res.stderr
|
|
172
|
+
? res.stderr.slice(0, 200)
|
|
173
|
+
: `exit status ${res.status}`,
|
|
174
|
+
qualified: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parsed = JSON.parse(res.stdout);
|
|
179
|
+
return validateAndNormalizeDecisionResult(decisionId, subjectId, subjectUrl, parsed);
|
|
180
|
+
} catch (err: unknown) {
|
|
181
|
+
// Cancellation is control flow, not a fallback decision: it must abort the stage.
|
|
182
|
+
if (err instanceof SeoStageCancelledError) throw err;
|
|
183
|
+
return {
|
|
184
|
+
id: decisionId,
|
|
185
|
+
subjectId,
|
|
186
|
+
subjectUrl,
|
|
187
|
+
type: 'choice',
|
|
188
|
+
value: 'unclear',
|
|
189
|
+
confidence: null,
|
|
190
|
+
maker: 'unknown',
|
|
191
|
+
source: 'fallback',
|
|
192
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
193
|
+
qualified: false,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate returned decision structure and apply KK confidence policy.
|
|
200
|
+
*/
|
|
201
|
+
export function validateAndNormalizeDecisionResult(
|
|
202
|
+
decisionId: string,
|
|
203
|
+
subjectId: string,
|
|
204
|
+
subjectUrl: string,
|
|
205
|
+
raw: Record<string, unknown>,
|
|
206
|
+
): JudgmentEntry {
|
|
207
|
+
const type = (typeof raw.type === 'string' ? raw.type : 'choice') as 'choice' | 'score' | 'noul';
|
|
208
|
+
const val = raw.value !== undefined ? raw.value : raw.choice !== undefined ? raw.choice : raw.noul;
|
|
209
|
+
const confidence = typeof raw.confidence === 'number' && Number.isFinite(raw.confidence) ? raw.confidence : null;
|
|
210
|
+
const maker = typeof raw.maker === 'string' && raw.maker.trim() !== '' ? raw.maker : 'unknown';
|
|
211
|
+
const source = typeof raw.source === 'string' && raw.source.trim() !== '' ? raw.source : 'default';
|
|
212
|
+
const reason = typeof raw.reason === 'string' ? raw.reason : '';
|
|
213
|
+
|
|
214
|
+
let qualified = false;
|
|
215
|
+
|
|
216
|
+
if (source === 'model') {
|
|
217
|
+
if (type === 'choice' || type === 'score') {
|
|
218
|
+
qualified = confidence !== null && confidence >= 0.8;
|
|
219
|
+
} else if (type === 'noul') {
|
|
220
|
+
const prob = typeof val === 'number' ? val : typeof raw.probability === 'number' ? raw.probability : 0.5;
|
|
221
|
+
qualified = prob >= 0.8 || prob <= 0.2;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let normalizedValue: string | number | boolean = 'unclear';
|
|
226
|
+
if (type === 'score') {
|
|
227
|
+
normalizedValue = typeof val === 'number' && Number.isInteger(val) ? val : 0;
|
|
228
|
+
} else if (type === 'noul') {
|
|
229
|
+
normalizedValue = typeof val === 'boolean' ? val : false;
|
|
230
|
+
} else {
|
|
231
|
+
normalizedValue = typeof val === 'string' ? val : 'unclear';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
id: decisionId,
|
|
236
|
+
subjectId,
|
|
237
|
+
subjectUrl,
|
|
238
|
+
type,
|
|
239
|
+
value: normalizedValue,
|
|
240
|
+
confidence,
|
|
241
|
+
maker,
|
|
242
|
+
source,
|
|
243
|
+
reason,
|
|
244
|
+
qualified,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Execute seo-judge stage across crawl artifact.
|
|
250
|
+
*/
|
|
251
|
+
export async function executeSeoJudgeStage(
|
|
252
|
+
workDir: string,
|
|
253
|
+
options: { spurBin?: string; stub?: DecisionStubHandler } = {},
|
|
254
|
+
): Promise<JudgmentsArtifact> {
|
|
255
|
+
const optionsPath = join(workDir, 'options.json');
|
|
256
|
+
const crawlPath = join(workDir, 'crawl.json');
|
|
257
|
+
|
|
258
|
+
const optionsRaw = readFileSync(optionsPath, 'utf-8');
|
|
259
|
+
const crawlRaw = readFileSync(crawlPath, 'utf-8');
|
|
260
|
+
|
|
261
|
+
const opts: SeoOptions = JSON.parse(optionsRaw);
|
|
262
|
+
const crawl: CrawlArtifact = JSON.parse(crawlRaw);
|
|
263
|
+
|
|
264
|
+
const deadlineMs = Date.now() + 600 * 1000; // 600s max stage deadline
|
|
265
|
+
const maxCalls = opts.max_decision_calls || 320;
|
|
266
|
+
let callCount = 0;
|
|
267
|
+
|
|
268
|
+
const evidenceDir = join(workDir, 'evidence', 'decisions');
|
|
269
|
+
mkdirSync(evidenceDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
const entries: JudgmentEntry[] = [];
|
|
272
|
+
const siteUrl = opts.url;
|
|
273
|
+
const siteOrigin = new URL(siteUrl).origin;
|
|
274
|
+
|
|
275
|
+
// 1. Site-level decisions (5 decisions)
|
|
276
|
+
const siteDecisions = [
|
|
277
|
+
'kk-seo-business-model',
|
|
278
|
+
'kk-seo-value-proposition',
|
|
279
|
+
'kk-seo-entity-clarity',
|
|
280
|
+
'kk-seo-topical-focus',
|
|
281
|
+
'kk-seo-local-intent',
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const siteContextFile = join(evidenceDir, 'site-context.json');
|
|
285
|
+
const siteContextContent = JSON.stringify({
|
|
286
|
+
origin: siteOrigin,
|
|
287
|
+
businessContext: (opts.business_context || '').slice(0, 1000),
|
|
288
|
+
locale: opts.locale || 'en-US',
|
|
289
|
+
});
|
|
290
|
+
writeJsonAtomic(siteContextFile, JSON.parse(siteContextContent));
|
|
291
|
+
|
|
292
|
+
for (const dId of siteDecisions) {
|
|
293
|
+
if (callCount >= maxCalls || Date.now() >= deadlineMs) break;
|
|
294
|
+
callCount += 1;
|
|
295
|
+
const entry = executeDecisionRun(
|
|
296
|
+
dId,
|
|
297
|
+
{ subject_id: siteOrigin, locale: opts.locale },
|
|
298
|
+
[siteContextFile],
|
|
299
|
+
options,
|
|
300
|
+
);
|
|
301
|
+
entries.push(entry);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 2. Page-level decisions (13 decisions per semantic page)
|
|
305
|
+
const availablePages = crawl.pages.filter((p) => p.availability === 'available' && p.facts);
|
|
306
|
+
const selectedPages = availablePages.slice(0, opts.semantic_pages || 20);
|
|
307
|
+
|
|
308
|
+
for (const page of selectedPages) {
|
|
309
|
+
if (callCount >= maxCalls || Date.now() >= deadlineMs) break;
|
|
310
|
+
const { factsJson, excerptJson } = buildCompactEvidencePacket(page, siteOrigin);
|
|
311
|
+
const pFactsFile = join(evidenceDir, `p-${page.subjectId}-facts.json`);
|
|
312
|
+
const pExcerptFile = join(evidenceDir, `p-${page.subjectId}-excerpt.json`);
|
|
313
|
+
writeJsonAtomic(pFactsFile, JSON.parse(factsJson));
|
|
314
|
+
writeJsonAtomic(pExcerptFile, JSON.parse(excerptJson));
|
|
315
|
+
|
|
316
|
+
const isHomepage = page.url === siteUrl || page.url === `${siteOrigin}/`;
|
|
317
|
+
|
|
318
|
+
// Page-level question list with applicability checks
|
|
319
|
+
const pDecisions: string[] = [];
|
|
320
|
+
if (!isHomepage) {
|
|
321
|
+
pDecisions.push('kk-seo-page-type');
|
|
322
|
+
} else {
|
|
323
|
+
// Deterministic homepage type
|
|
324
|
+
entries.push({
|
|
325
|
+
id: 'kk-seo-page-type',
|
|
326
|
+
subjectId: page.subjectId,
|
|
327
|
+
subjectUrl: page.url,
|
|
328
|
+
type: 'choice',
|
|
329
|
+
value: 'homepage',
|
|
330
|
+
confidence: 1.0,
|
|
331
|
+
maker: 'deterministic',
|
|
332
|
+
source: 'deterministic',
|
|
333
|
+
reason: 'Root URL classified deterministically as homepage',
|
|
334
|
+
qualified: true,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
pDecisions.push(
|
|
339
|
+
'kk-seo-search-intent',
|
|
340
|
+
'kk-seo-page-action',
|
|
341
|
+
'kk-seo-importance',
|
|
342
|
+
'kk-seo-helpfulness',
|
|
343
|
+
'kk-seo-specificity',
|
|
344
|
+
'kk-seo-trust',
|
|
345
|
+
'kk-seo-citability',
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (page.facts?.title) {
|
|
349
|
+
pDecisions.push('kk-seo-title-fit');
|
|
350
|
+
}
|
|
351
|
+
if (page.facts?.metaDescription) {
|
|
352
|
+
pDecisions.push('kk-seo-meta-fit');
|
|
353
|
+
}
|
|
354
|
+
if (page.facts?.headings.length) {
|
|
355
|
+
pDecisions.push('kk-seo-heading-fit');
|
|
356
|
+
}
|
|
357
|
+
pDecisions.push('kk-seo-answer-first', 'kk-seo-next-step');
|
|
358
|
+
|
|
359
|
+
for (const dId of pDecisions) {
|
|
360
|
+
if (callCount >= maxCalls || Date.now() >= deadlineMs) break;
|
|
361
|
+
callCount += 1;
|
|
362
|
+
const entry = executeDecisionRun(
|
|
363
|
+
dId,
|
|
364
|
+
{ subject_id: page.url, locale: opts.locale, url: page.url },
|
|
365
|
+
[pFactsFile, pExcerptFile, siteContextFile],
|
|
366
|
+
options,
|
|
367
|
+
);
|
|
368
|
+
entries.push(entry);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 3. Pair-level decisions (kk-seo-intent-overlap)
|
|
373
|
+
const pairsToCompare: Array<[PageRecord, PageRecord]> = [];
|
|
374
|
+
for (let i = 0; i < selectedPages.length; i += 1) {
|
|
375
|
+
for (let j = i + 1; j < selectedPages.length; j += 1) {
|
|
376
|
+
const pA = selectedPages[i];
|
|
377
|
+
const pB = selectedPages[j];
|
|
378
|
+
if (pA && pB && pairsToCompare.length < (opts.max_pairs || 10)) {
|
|
379
|
+
pairsToCompare.push([pA, pB]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const [pA, pB] of pairsToCompare) {
|
|
385
|
+
if (callCount >= maxCalls || Date.now() >= deadlineMs) break;
|
|
386
|
+
const pairId = [pA.subjectId, pB.subjectId].sort().join('::');
|
|
387
|
+
const pairEvFile = join(evidenceDir, `pair-${pairId}.json`);
|
|
388
|
+
const pairPayload = {
|
|
389
|
+
pageA: { url: pA.url, title: pA.facts?.title, h1: pA.facts?.headings.find((h) => h.level === 1)?.text },
|
|
390
|
+
pageB: { url: pB.url, title: pB.facts?.title, h1: pB.facts?.headings.find((h) => h.level === 1)?.text },
|
|
391
|
+
};
|
|
392
|
+
writeJsonAtomic(pairEvFile, pairPayload);
|
|
393
|
+
|
|
394
|
+
callCount += 1;
|
|
395
|
+
const entry = executeDecisionRun(
|
|
396
|
+
'kk-seo-intent-overlap',
|
|
397
|
+
{ subject_id: pairId, locale: opts.locale, url: `${pA.url} :: ${pB.url}` },
|
|
398
|
+
[pairEvFile],
|
|
399
|
+
options,
|
|
400
|
+
);
|
|
401
|
+
entries.push(entry);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 4. Keyword decisions: suppressed when visibility is missing
|
|
405
|
+
const hasVisibility = Boolean(opts.visibility_file && existsSync(resolve(opts.visibility_file)));
|
|
406
|
+
if (hasVisibility) {
|
|
407
|
+
try {
|
|
408
|
+
const visRaw = readFileSync(resolve(opts.visibility_file), 'utf-8');
|
|
409
|
+
const vis = JSON.parse(visRaw);
|
|
410
|
+
const rows = Array.isArray(vis.rows) ? vis.rows : [];
|
|
411
|
+
const maxKeywords = opts.max_keywords || 10;
|
|
412
|
+
const keywordsToEvaluate = rows.slice(0, maxKeywords);
|
|
413
|
+
|
|
414
|
+
for (const kwRow of keywordsToEvaluate) {
|
|
415
|
+
if (callCount >= maxCalls || Date.now() >= deadlineMs) break;
|
|
416
|
+
const query = typeof kwRow.query === 'string' ? kwRow.query : '';
|
|
417
|
+
if (!query) continue;
|
|
418
|
+
|
|
419
|
+
// kk-seo-keyword-other-brand
|
|
420
|
+
callCount += 1;
|
|
421
|
+
entries.push(
|
|
422
|
+
executeDecisionRun(
|
|
423
|
+
'kk-seo-keyword-other-brand',
|
|
424
|
+
{ subject_id: query, locale: opts.locale },
|
|
425
|
+
[siteContextFile],
|
|
426
|
+
options,
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// kk-seo-keyword-page
|
|
431
|
+
const candidates = selectedPages.slice(0, 30).map((p) => ({
|
|
432
|
+
subjectId: p.subjectId,
|
|
433
|
+
url: p.url,
|
|
434
|
+
title: p.facts?.title || '',
|
|
435
|
+
}));
|
|
436
|
+
callCount += 1;
|
|
437
|
+
entries.push(
|
|
438
|
+
executeDecisionRun(
|
|
439
|
+
'kk-seo-keyword-page',
|
|
440
|
+
{ subject_id: query, locale: opts.locale, candidates: JSON.stringify(candidates) },
|
|
441
|
+
[siteContextFile],
|
|
442
|
+
options,
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
} catch {
|
|
447
|
+
// ignore malformed visibility snapshot
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const summary = {
|
|
452
|
+
totalCalls: callCount,
|
|
453
|
+
successfulCalls: entries.length,
|
|
454
|
+
// The deterministic homepage entry is not a decision call, so it stays out of the call tallies.
|
|
455
|
+
qualifiedCalls: entries.filter((e) => e.qualified && e.source !== 'deterministic').length,
|
|
456
|
+
fallbackCalls: entries.filter((e) => e.source === 'fallback' || e.source === 'default').length,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const isPartial = callCount >= maxCalls || Date.now() >= deadlineMs;
|
|
460
|
+
|
|
461
|
+
const judgmentsArtifact: JudgmentsArtifact = {
|
|
462
|
+
schemaVersion: 1,
|
|
463
|
+
methodVersion: '1.0.0',
|
|
464
|
+
runId: crawl.runId,
|
|
465
|
+
createdAt: new Date().toISOString(),
|
|
466
|
+
inputDigest: crawl.inputDigest,
|
|
467
|
+
predecessorDigests: {
|
|
468
|
+
'crawl.json': crawl.predecessorDigests['crawl.json'] || 'abc',
|
|
469
|
+
},
|
|
470
|
+
status: isPartial ? 'partial' : 'complete',
|
|
471
|
+
entries,
|
|
472
|
+
summary,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
writeJsonAtomic(join(workDir, 'judgments.json'), judgmentsArtifact);
|
|
476
|
+
return judgmentsArtifact;
|
|
477
|
+
}
|