@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,460 @@
|
|
|
1
|
+
import type { ActionItem, AssessmentRow } from './seo-rules';
|
|
2
|
+
|
|
3
|
+
export interface AuditArtifact {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
methodVersion: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
revisionId?: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
inputDigest: string;
|
|
10
|
+
predecessorDigests: Record<string, string>;
|
|
11
|
+
status: 'complete' | 'partial';
|
|
12
|
+
targetUrl: string;
|
|
13
|
+
scores: {
|
|
14
|
+
technical: number | null;
|
|
15
|
+
content: number | null;
|
|
16
|
+
crawl: number | null;
|
|
17
|
+
security: number | null;
|
|
18
|
+
};
|
|
19
|
+
coverage: {
|
|
20
|
+
totalRules: number;
|
|
21
|
+
applicableRules: number;
|
|
22
|
+
assessed: number;
|
|
23
|
+
unknown: number;
|
|
24
|
+
ratio: number | null;
|
|
25
|
+
};
|
|
26
|
+
facts: Record<string, { value: string | number | boolean | null; unit: string; source: string }>;
|
|
27
|
+
assessments: AssessmentRow[];
|
|
28
|
+
actions: ActionItem[];
|
|
29
|
+
judgments: { status: 'not-assessed' | 'available'; rows?: unknown[] };
|
|
30
|
+
performance: { status: 'not-assessed' | 'available'; rows?: unknown[] };
|
|
31
|
+
visibility: { status: 'not-assessed' | 'available'; rows?: unknown[] };
|
|
32
|
+
limits: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReportJsonArtifact {
|
|
36
|
+
schemaVersion: 1;
|
|
37
|
+
methodVersion: string;
|
|
38
|
+
runId: string;
|
|
39
|
+
revisionId?: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
targetUrl: string;
|
|
42
|
+
status: 'done' | 'partial';
|
|
43
|
+
scores: AuditArtifact['scores'];
|
|
44
|
+
coverage: AuditArtifact['coverage'];
|
|
45
|
+
actionsCount: number;
|
|
46
|
+
actions: ActionItem[];
|
|
47
|
+
narrativeStatus: 'omitted' | 'included' | 'rejected';
|
|
48
|
+
/** Rejection diagnostics, present only when the narrative was rejected (design §9). */
|
|
49
|
+
narrativeDiagnostics?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ------------------------------------------------------------------ *
|
|
53
|
+
* Narrative contract (design §9, refined handoff 2026-10-14)
|
|
54
|
+
*
|
|
55
|
+
* The agent authors references, never prose: it selects an allow-listed
|
|
56
|
+
* heading and a fixed claim template whose evidenceRef points at an
|
|
57
|
+
* immutable audit record. Rendering is method-owned, so a claim can never
|
|
58
|
+
* introduce a measurement, diagnosis or prediction of its own.
|
|
59
|
+
* ------------------------------------------------------------------ */
|
|
60
|
+
|
|
61
|
+
/** Allow-listed section headings. Unknown headings reject the whole narrative. */
|
|
62
|
+
export const NARRATIVE_HEADINGS = ['summary', 'priorities', 'limits'] as const;
|
|
63
|
+
export type NarrativeHeading = (typeof NARRATIVE_HEADINGS)[number];
|
|
64
|
+
|
|
65
|
+
/** Allow-listed claim templates, each bound to one approved audit collection. */
|
|
66
|
+
export const NARRATIVE_TEMPLATES = ['fact', 'action', 'limit'] as const;
|
|
67
|
+
export type NarrativeTemplate = (typeof NARRATIVE_TEMPLATES)[number];
|
|
68
|
+
|
|
69
|
+
export interface NarrativeClaim {
|
|
70
|
+
template: NarrativeTemplate;
|
|
71
|
+
evidenceRef: { artifact: 'audit.json'; jsonPointer: string };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface NarrativeSection {
|
|
75
|
+
heading: NarrativeHeading;
|
|
76
|
+
claims: NarrativeClaim[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface NarrativeArtifact {
|
|
80
|
+
schemaVersion: 1;
|
|
81
|
+
runId: string;
|
|
82
|
+
auditDigest: string;
|
|
83
|
+
sections: NarrativeSection[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface NarrativeValidation {
|
|
87
|
+
status: 'included' | 'rejected';
|
|
88
|
+
sections: NarrativeSection[];
|
|
89
|
+
diagnostics: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Bounded narrative envelope (design §9): 32 KiB of JSON, at most 30 claims. */
|
|
93
|
+
export const NARRATIVE_MAX_BYTES = 32 * 1024;
|
|
94
|
+
export const NARRATIVE_MAX_CLAIMS = 30;
|
|
95
|
+
|
|
96
|
+
/** Prototype-traversal keys refused at any JSON-pointer segment. */
|
|
97
|
+
const FORBIDDEN_POINTER_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
98
|
+
|
|
99
|
+
/** Method-owned unit labels for the recorded limit collection. */
|
|
100
|
+
const LIMIT_UNITS: Record<string, string> = {
|
|
101
|
+
pagesVisited: 'pages',
|
|
102
|
+
requestsMade: 'requests',
|
|
103
|
+
totalBytes: 'bytes',
|
|
104
|
+
elapsedSeconds: 'seconds',
|
|
105
|
+
hitLimit: '',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Template → approved target collection. A pointer into any other collection is rejected. */
|
|
109
|
+
const TEMPLATE_COLLECTIONS: Record<NarrativeTemplate, string> = {
|
|
110
|
+
fact: 'facts',
|
|
111
|
+
action: 'actions',
|
|
112
|
+
limit: 'limits',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
116
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Exact-key check: every required key present, and no unknown key tolerated. */
|
|
120
|
+
function exactKeys(value: Record<string, unknown>, required: readonly string[]): string | null {
|
|
121
|
+
for (const key of required) {
|
|
122
|
+
if (!Object.hasOwn(value, key)) return `missing key "${key}"`;
|
|
123
|
+
}
|
|
124
|
+
for (const key of Object.keys(value)) {
|
|
125
|
+
if (!required.includes(key)) return `unknown key "${key}"`;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decode a JSON pointer into segments, refusing empty segments, malformed escapes
|
|
132
|
+
* and prototype keys. Returns the failure message, or the decoded segments.
|
|
133
|
+
*/
|
|
134
|
+
function decodePointer(pointer: string): { segments: string[] } | { error: string } {
|
|
135
|
+
if (typeof pointer !== 'string' || !pointer.startsWith('/')) {
|
|
136
|
+
return { error: `jsonPointer "${String(pointer)}" must be a non-empty JSON pointer starting with "/"` };
|
|
137
|
+
}
|
|
138
|
+
const segments = pointer
|
|
139
|
+
.slice(1)
|
|
140
|
+
.split('/')
|
|
141
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
142
|
+
for (const segment of segments) {
|
|
143
|
+
if (segment === '') return { error: `jsonPointer "${pointer}" has an empty segment` };
|
|
144
|
+
if (FORBIDDEN_POINTER_KEYS.has(segment)) {
|
|
145
|
+
return { error: `jsonPointer "${pointer}" references forbidden key "${segment}"` };
|
|
146
|
+
}
|
|
147
|
+
if (segment.includes('\u0000')) return { error: `jsonPointer "${pointer}" contains a NUL character` };
|
|
148
|
+
}
|
|
149
|
+
return { segments };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve one claim against the audit, enforcing both the pointer target type and
|
|
154
|
+
* the approved collection for the declared template. Returns null when valid.
|
|
155
|
+
*/
|
|
156
|
+
function resolveClaim(audit: AuditArtifact, template: NarrativeTemplate, pointer: string): string | null {
|
|
157
|
+
const decoded = decodePointer(pointer);
|
|
158
|
+
if ('error' in decoded) return decoded.error;
|
|
159
|
+
const { segments } = decoded;
|
|
160
|
+
if (segments.length !== 2) {
|
|
161
|
+
return `jsonPointer "${pointer}" must address exactly one record (collection/key)`;
|
|
162
|
+
}
|
|
163
|
+
const [collection, key] = segments as [string, string];
|
|
164
|
+
const expected = TEMPLATE_COLLECTIONS[template];
|
|
165
|
+
if (collection !== expected) {
|
|
166
|
+
return `template "${template}" requires a pointer into /${expected}, got "/${collection}"`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (template === 'fact') {
|
|
170
|
+
if (!Object.hasOwn(audit.facts, key)) return `unknown fact id "${key}"`;
|
|
171
|
+
const fact = audit.facts[key];
|
|
172
|
+
if (!isPlainObject(fact)) return `fact "${key}" is not an observed fact record`;
|
|
173
|
+
const value = fact.value;
|
|
174
|
+
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
175
|
+
return `fact "${key}" is not an allow-listed scalar observation`;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (template === 'action') {
|
|
181
|
+
// Actions carry stable ids, so the pointer addresses a recorded action, not an array index.
|
|
182
|
+
const action = audit.actions.find((item) => item.id === key);
|
|
183
|
+
return action ? null : `unknown action id "${key}"`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!Object.hasOwn(audit.limits ?? {}, key)) return `unknown limit id "${key}"`;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Validate every section/claim and collect the full diagnostic list (no early exit). */
|
|
191
|
+
function validateNarrativeObject(
|
|
192
|
+
value: Record<string, unknown>,
|
|
193
|
+
audit: AuditArtifact,
|
|
194
|
+
auditDigest: string,
|
|
195
|
+
): NarrativeValidation {
|
|
196
|
+
const diagnostics: string[] = [];
|
|
197
|
+
const reject = (message: string): NarrativeValidation => {
|
|
198
|
+
diagnostics.push(message);
|
|
199
|
+
return { status: 'rejected', sections: [], diagnostics };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const rootProblem = exactKeys(value, ['schemaVersion', 'runId', 'auditDigest', 'sections']);
|
|
203
|
+
if (rootProblem) return reject(`narrative root: ${rootProblem}`);
|
|
204
|
+
if (value.schemaVersion !== 1)
|
|
205
|
+
return reject(`narrative schemaVersion must be 1, got "${String(value.schemaVersion)}"`);
|
|
206
|
+
if (value.runId !== audit.runId)
|
|
207
|
+
return reject(`narrative runId "${String(value.runId)}" does not match the audit runId`);
|
|
208
|
+
if (value.auditDigest !== auditDigest) {
|
|
209
|
+
return reject(
|
|
210
|
+
'narrative auditDigest does not match the materialized audit.json digest (stale or unknown digest)',
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!Array.isArray(value.sections)) return reject('narrative sections must be an array');
|
|
214
|
+
if (value.sections.length === 0) return reject('narrative sections must not be empty');
|
|
215
|
+
|
|
216
|
+
const sections: NarrativeSection[] = [];
|
|
217
|
+
let claimCount = 0;
|
|
218
|
+
|
|
219
|
+
value.sections.forEach((rawSection, sectionIndex) => {
|
|
220
|
+
if (!isPlainObject(rawSection)) {
|
|
221
|
+
diagnostics.push(`sections[${sectionIndex}]: not an object`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const sectionProblem = exactKeys(rawSection, ['heading', 'claims']);
|
|
225
|
+
if (sectionProblem) {
|
|
226
|
+
diagnostics.push(`sections[${sectionIndex}]: ${sectionProblem}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const heading = rawSection.heading;
|
|
230
|
+
if (typeof heading !== 'string' || !(NARRATIVE_HEADINGS as readonly string[]).includes(heading)) {
|
|
231
|
+
diagnostics.push(`sections[${sectionIndex}]: heading "${String(heading)}" is not an allow-listed heading`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!Array.isArray(rawSection.claims) || rawSection.claims.length === 0) {
|
|
235
|
+
diagnostics.push(`sections[${sectionIndex}]: claims must be a non-empty array`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const claims: NarrativeClaim[] = [];
|
|
240
|
+
rawSection.claims.forEach((rawClaim, claimIndex) => {
|
|
241
|
+
claimCount += 1;
|
|
242
|
+
const at = `sections[${sectionIndex}].claims[${claimIndex}]`;
|
|
243
|
+
if (!isPlainObject(rawClaim)) {
|
|
244
|
+
diagnostics.push(`${at}: not an object`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const claimProblem = exactKeys(rawClaim, ['template', 'evidenceRef']);
|
|
248
|
+
if (claimProblem) {
|
|
249
|
+
diagnostics.push(`${at}: ${claimProblem}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const template = rawClaim.template;
|
|
253
|
+
if (typeof template !== 'string' || !(NARRATIVE_TEMPLATES as readonly string[]).includes(template)) {
|
|
254
|
+
diagnostics.push(`${at}: template "${String(template)}" is not an allow-listed template`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!isPlainObject(rawClaim.evidenceRef)) {
|
|
258
|
+
diagnostics.push(`${at}: evidenceRef must be an object`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const refProblem = exactKeys(rawClaim.evidenceRef, ['artifact', 'jsonPointer']);
|
|
262
|
+
if (refProblem) {
|
|
263
|
+
diagnostics.push(`${at}: evidenceRef ${refProblem}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (rawClaim.evidenceRef.artifact !== 'audit.json') {
|
|
267
|
+
diagnostics.push(`${at}: evidenceRef.artifact must be "audit.json"`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const pointer = rawClaim.evidenceRef.jsonPointer;
|
|
271
|
+
const resolveProblem = resolveClaim(audit, template as NarrativeTemplate, pointer as string);
|
|
272
|
+
if (resolveProblem) {
|
|
273
|
+
diagnostics.push(`${at}: ${resolveProblem}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
claims.push({
|
|
277
|
+
template: template as NarrativeTemplate,
|
|
278
|
+
evidenceRef: { artifact: 'audit.json', jsonPointer: pointer as string },
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
sections.push({ heading: heading as NarrativeHeading, claims });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (claimCount > NARRATIVE_MAX_CLAIMS) {
|
|
286
|
+
diagnostics.push(`narrative has ${claimCount} claims, exceeding the ${NARRATIVE_MAX_CLAIMS}-claim limit`);
|
|
287
|
+
}
|
|
288
|
+
if (diagnostics.length > 0) return { status: 'rejected', sections: [], diagnostics };
|
|
289
|
+
return { status: 'included', sections, diagnostics: [] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate a serialized narrative against the audit it cites.
|
|
294
|
+
*
|
|
295
|
+
* Any invalid claim rejects the ENTIRE narrative; the caller falls back to the
|
|
296
|
+
* deterministic summary and persists the diagnostics. Unreadable/oversize/unknown
|
|
297
|
+
* shape is a rejection, never an audit failure.
|
|
298
|
+
*/
|
|
299
|
+
export function validateNarrative(raw: string, audit: AuditArtifact, auditDigest: string): NarrativeValidation {
|
|
300
|
+
const byteLength = Buffer.byteLength(raw, 'utf-8');
|
|
301
|
+
if (byteLength > NARRATIVE_MAX_BYTES) {
|
|
302
|
+
return {
|
|
303
|
+
status: 'rejected',
|
|
304
|
+
sections: [],
|
|
305
|
+
diagnostics: [`narrative is ${byteLength} bytes, exceeding the ${NARRATIVE_MAX_BYTES}-byte limit`],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
let parsed: unknown;
|
|
309
|
+
try {
|
|
310
|
+
parsed = JSON.parse(raw);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return { status: 'rejected', sections: [], diagnostics: [`narrative is not valid JSON: ${String(err)}`] };
|
|
313
|
+
}
|
|
314
|
+
if (!isPlainObject(parsed)) {
|
|
315
|
+
return { status: 'rejected', sections: [], diagnostics: ['narrative root must be a JSON object'] };
|
|
316
|
+
}
|
|
317
|
+
return validateNarrativeObject(parsed, audit, auditDigest);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Render one resolved claim through its fixed, method-owned template. */
|
|
321
|
+
function renderClaim(audit: AuditArtifact, claim: NarrativeClaim): string {
|
|
322
|
+
const segments = claim.evidenceRef.jsonPointer
|
|
323
|
+
.slice(1)
|
|
324
|
+
.split('/')
|
|
325
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
326
|
+
const key = segments[1] ?? '';
|
|
327
|
+
|
|
328
|
+
if (claim.template === 'fact') {
|
|
329
|
+
const fact = audit.facts[key];
|
|
330
|
+
const value = fact.value === null ? 'none' : String(fact.value);
|
|
331
|
+
return `- **Observed fact** \`${escapeMarkdown(key)}\` = \`${escapeMarkdown(value)}\`${fact.unit ? ` ${escapeMarkdown(fact.unit)}` : ''} — source: \`${escapeMarkdown(fact.source)}\``;
|
|
332
|
+
}
|
|
333
|
+
if (claim.template === 'action') {
|
|
334
|
+
const action = audit.actions.find((item) => item.id === key) as ActionItem;
|
|
335
|
+
return `- **Planned action** \`${escapeMarkdown(action.id)}\` (${action.severity}, ${action.priority}) — ${escapeMarkdown(action.title)} — ${action.affectedPagesCount} affected page(s), effort band ${action.effort}`;
|
|
336
|
+
}
|
|
337
|
+
const limits = (audit.limits ?? {}) as Record<string, unknown>;
|
|
338
|
+
const unit = LIMIT_UNITS[key] ?? '';
|
|
339
|
+
const value = Object.hasOwn(limits, key) ? limits[key] : undefined;
|
|
340
|
+
const rendered = value === null || value === undefined ? 'none' : String(value);
|
|
341
|
+
return `- **Recorded limit** \`${escapeMarkdown(key)}\` = \`${escapeMarkdown(rendered)}\`${unit ? ` ${escapeMarkdown(unit)}` : ''}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Render validated narrative sections; agents order references, never measurements. */
|
|
345
|
+
export function renderNarrativeSections(audit: AuditArtifact, sections: NarrativeSection[]): string {
|
|
346
|
+
const lines: string[] = [];
|
|
347
|
+
const headingLabels: Record<NarrativeHeading, string> = {
|
|
348
|
+
summary: 'Summary',
|
|
349
|
+
priorities: 'Priorities',
|
|
350
|
+
limits: 'Limits',
|
|
351
|
+
};
|
|
352
|
+
for (const section of sections) {
|
|
353
|
+
lines.push(`### ${headingLabels[section.heading]}`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
for (const claim of section.claims) lines.push(renderClaim(audit, claim));
|
|
356
|
+
lines.push('');
|
|
357
|
+
}
|
|
358
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
359
|
+
return lines.join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Deterministic fallback summary: coverage, category scores, action counts and crawler
|
|
364
|
+
* limits. Emitted when no narrative was supplied, or when one was rejected.
|
|
365
|
+
*/
|
|
366
|
+
export function renderDeterministicSummary(audit: AuditArtifact): string {
|
|
367
|
+
const score = (value: number | null): string => (value === null ? 'N/A (no eligible rules)' : `${value}/100`);
|
|
368
|
+
const counts = { P1: 0, P2: 0, P3: 0, review: 0 };
|
|
369
|
+
for (const action of audit.actions) counts[action.priority] += 1;
|
|
370
|
+
const limits = (audit.limits ?? {}) as Record<string, unknown>;
|
|
371
|
+
const ratioCopy =
|
|
372
|
+
audit.coverage.ratio === null ? 'N/A (no eligible rules)' : `${Math.round(audit.coverage.ratio * 100)}%`;
|
|
373
|
+
return [
|
|
374
|
+
`- Coverage: ${audit.coverage.assessed} assessed of ${audit.coverage.applicableRules} applicable checks (${ratioCopy}), ${audit.coverage.unknown} unknown of ${audit.coverage.totalRules} rules.`,
|
|
375
|
+
`- Category scores: technical ${score(audit.scores.technical)}, content ${score(audit.scores.content)}, crawl ${score(audit.scores.crawl)}, security ${score(audit.scores.security)}.`,
|
|
376
|
+
`- Prioritized actions: ${audit.actions.length} (P1 ${counts.P1}, P2 ${counts.P2}, P3 ${counts.P3}, review ${counts.review}).`,
|
|
377
|
+
`- Crawl limits: pages ${String(limits.pagesVisited ?? 'unknown')}, requests ${String(limits.requestsMade ?? 'unknown')}, bytes ${String(limits.totalBytes ?? 'unknown')}, elapsed ${String(limits.elapsedSeconds ?? 'unknown')}s, hit limit ${limits.hitLimit === null || limits.hitLimit === undefined ? 'none' : String(limits.hitLimit)}.`,
|
|
378
|
+
].join('\n');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function escapeMarkdown(text: string): string {
|
|
382
|
+
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Render deterministic markdown report from audit data. */
|
|
386
|
+
export function renderReportMarkdown(audit: AuditArtifact, narrativeText?: string): string {
|
|
387
|
+
const lines: string[] = [];
|
|
388
|
+
|
|
389
|
+
lines.push(`# Smart SEO Audit Report`);
|
|
390
|
+
lines.push(``);
|
|
391
|
+
lines.push(`- **Target URL:** ${audit.targetUrl}`);
|
|
392
|
+
lines.push(`- **Run ID:** \`${audit.runId}\`${audit.revisionId ? ` (Revision: \`${audit.revisionId}\`)` : ''}`);
|
|
393
|
+
lines.push(`- **Status:** **${audit.status.toUpperCase()}**`);
|
|
394
|
+
lines.push(`- **Method Version:** ${audit.methodVersion} (Sources checked at 2026-10-14)`);
|
|
395
|
+
lines.push(``);
|
|
396
|
+
|
|
397
|
+
lines.push(`## 1. Category Scores & Coverage`);
|
|
398
|
+
lines.push(``);
|
|
399
|
+
lines.push(`| Category | Score | Metric |`);
|
|
400
|
+
lines.push(`| --- | --- | --- |`);
|
|
401
|
+
lines.push(
|
|
402
|
+
`| Technical | ${audit.scores.technical !== null ? `${audit.scores.technical}/100` : 'N/A (no eligible rules)'} | Pass ratio over applicable assessed checks |`,
|
|
403
|
+
);
|
|
404
|
+
lines.push(
|
|
405
|
+
`| Content | ${audit.scores.content !== null ? `${audit.scores.content}/100` : 'N/A (no eligible rules)'} | Pass ratio over applicable assessed checks |`,
|
|
406
|
+
);
|
|
407
|
+
lines.push(
|
|
408
|
+
`| Crawl | ${audit.scores.crawl !== null ? `${audit.scores.crawl}/100` : 'N/A (no eligible rules)'} | Pass ratio over applicable assessed checks |`,
|
|
409
|
+
);
|
|
410
|
+
lines.push(
|
|
411
|
+
`| Security | ${audit.scores.security !== null ? `${audit.scores.security}/100` : 'N/A (no eligible rules)'} | Pass ratio over applicable assessed checks |`,
|
|
412
|
+
);
|
|
413
|
+
lines.push(``);
|
|
414
|
+
lines.push(`- **Total Checklist Rules:** ${audit.coverage.totalRules}`);
|
|
415
|
+
lines.push(`- **Applicable Rules:** ${audit.coverage.applicableRules}`);
|
|
416
|
+
lines.push(`- **Assessed Checks:** ${audit.coverage.assessed}`);
|
|
417
|
+
lines.push(`- **Unknown / Inconclusive:** ${audit.coverage.unknown}`);
|
|
418
|
+
lines.push(
|
|
419
|
+
`- **Coverage Ratio:** ${audit.coverage.ratio !== null ? `${Math.round(audit.coverage.ratio * 100)}%` : 'N/A'}`,
|
|
420
|
+
);
|
|
421
|
+
lines.push(``);
|
|
422
|
+
|
|
423
|
+
lines.push(`## 2. Prioritized Action Items`);
|
|
424
|
+
lines.push(``);
|
|
425
|
+
if (audit.actions.length === 0) {
|
|
426
|
+
lines.push(`No critical or high action items detected.`);
|
|
427
|
+
} else {
|
|
428
|
+
lines.push(`| Priority | ID | Severity | Effort | Affected | Description |`);
|
|
429
|
+
lines.push(`| --- | --- | --- | --- | --- | --- |`);
|
|
430
|
+
for (const act of audit.actions) {
|
|
431
|
+
lines.push(
|
|
432
|
+
`| ${act.priority} | \`${act.id}\` | ${act.severity} | Band ${act.effort} | ${act.affectedPagesCount} | ${escapeMarkdown(act.title)} |`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
lines.push(``);
|
|
437
|
+
|
|
438
|
+
if (narrativeText && narrativeText.trim() !== '') {
|
|
439
|
+
lines.push(`## 3. Executive Narrative`);
|
|
440
|
+
lines.push(``);
|
|
441
|
+
lines.push(narrativeText.trim());
|
|
442
|
+
lines.push(``);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
lines.push(`## 4. Primary Sources & Disclaimers`);
|
|
446
|
+
lines.push(``);
|
|
447
|
+
lines.push(`- RFC 9309 (Robots Exclusion Protocol, 2022) governs robots matching; full Crawl-delay is respected.`);
|
|
448
|
+
lines.push(
|
|
449
|
+
`- Google Search Central canonical guidelines (2026) define canonical hints without proving non-indexing.`,
|
|
450
|
+
);
|
|
451
|
+
lines.push(
|
|
452
|
+
`- Google retired FAQ rich results on 2026-05-07 for every site (the Aug/Sep 2023 change had only restricted eligibility to government/health sites); FAQ schema presence does not guarantee search enhancement.`,
|
|
453
|
+
);
|
|
454
|
+
lines.push(
|
|
455
|
+
`- All crawl-limited findings (e.g. depth, orphan pages) are observations among sampled pages, not whole-web guarantees.`,
|
|
456
|
+
);
|
|
457
|
+
lines.push(``);
|
|
458
|
+
|
|
459
|
+
return lines.join('\n');
|
|
460
|
+
}
|