opencode-wiki-historian 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/maintain.d.ts +2 -2
- package/dist/maintain.js +10 -7
- package/dist/surface.d.ts +19 -1
- package/dist/surface.js +38 -6
- package/dist/tools/local.js +4 -1
- package/dist/wiki/nav.d.ts +21 -0
- package/dist/wiki/nav.js +43 -0
- package/package.json +1 -1
- package/skills/historian/SKILL.md +5 -5
package/dist/maintain.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface MaintainReport {
|
|
|
91
91
|
childPath: string;
|
|
92
92
|
}[];
|
|
93
93
|
};
|
|
94
|
-
readonly
|
|
94
|
+
readonly flatRootPages: readonly {
|
|
95
95
|
section: string;
|
|
96
96
|
paths: readonly string[];
|
|
97
97
|
}[];
|
|
@@ -114,7 +114,7 @@ export interface MaintainReport {
|
|
|
114
114
|
}[];
|
|
115
115
|
readonly freshness: FreshnessScan | null;
|
|
116
116
|
}
|
|
117
|
-
export declare const MAINTAIN_SCHEMA = "historian.maintain.
|
|
117
|
+
export declare const MAINTAIN_SCHEMA = "historian.maintain.v2";
|
|
118
118
|
/** Trigram-Jaccard bar for calling two (different-path) titles near-duplicates. */
|
|
119
119
|
export declare const DUP_TITLE_THRESHOLD = 0.75;
|
|
120
120
|
export declare function buildMaintainReport(input: MaintainInput, opts?: MaintainOptions): Promise<MaintainReport>;
|
package/dist/maintain.js
CHANGED
|
@@ -17,7 +17,7 @@ import { classifyGenre } from './templates/genres.js';
|
|
|
17
17
|
import { isInternalPath } from './tools/shared.js';
|
|
18
18
|
import { normalize } from './migrate-score.js';
|
|
19
19
|
// --- Constants --------------------------------------------------------------
|
|
20
|
-
export const MAINTAIN_SCHEMA = 'historian.maintain.
|
|
20
|
+
export const MAINTAIN_SCHEMA = 'historian.maintain.v2';
|
|
21
21
|
/** Trigram-Jaccard bar for calling two (different-path) titles near-duplicates. */
|
|
22
22
|
export const DUP_TITLE_THRESHOLD = 0.75;
|
|
23
23
|
const DAY_MS = 86_400_000;
|
|
@@ -58,8 +58,11 @@ function jaccard(a, b) {
|
|
|
58
58
|
function cmpStr(a, b) {
|
|
59
59
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
60
60
|
}
|
|
61
|
+
// bold-merge deliberately leaves (重定向)/(redirect)-suffixed stubs beside their
|
|
62
|
+
// live twins; clustering those pairs reports non-defects and buries true near-dupes.
|
|
63
|
+
const STUB_TITLE_RE = /[((]\s*(?:重定向|redirect)\s*[))]\s*$/i;
|
|
61
64
|
function findDuplicates(rows) {
|
|
62
|
-
const units = rows.map((r) => ({
|
|
65
|
+
const units = rows.filter((r) => !STUB_TITLE_RE.test(r.title)).map((r) => ({
|
|
63
66
|
path: r.path,
|
|
64
67
|
title: r.title,
|
|
65
68
|
grams: titleGrams(normalize(r.title).toLowerCase()),
|
|
@@ -147,7 +150,7 @@ function singleChildDirs(paths) {
|
|
|
147
150
|
}
|
|
148
151
|
return out;
|
|
149
152
|
}
|
|
150
|
-
function
|
|
153
|
+
function flatRootPages(paths) {
|
|
151
154
|
const bySection = new Map();
|
|
152
155
|
for (const path of paths) {
|
|
153
156
|
if (path.split('/').length !== 2)
|
|
@@ -270,7 +273,7 @@ export async function buildMaintainReport(input, opts = {}) {
|
|
|
270
273
|
duplicates: { threshold: DUP_TITLE_THRESHOLD, clusters: findDuplicates(kept) },
|
|
271
274
|
staleness: { topN, oldest: staleness(kept, now, topN) },
|
|
272
275
|
diffusion: { singleChildDirs: singleChildDirs(paths) },
|
|
273
|
-
|
|
276
|
+
flatRootPages: flatRootPages(paths),
|
|
274
277
|
tags: tagVocab(kept),
|
|
275
278
|
redirects,
|
|
276
279
|
sections: sectionDist(kept),
|
|
@@ -313,10 +316,10 @@ export function renderMaintainMarkdown(r) {
|
|
|
313
316
|
L.push(' - none');
|
|
314
317
|
for (const d of r.diffusion.singleChildDirs)
|
|
315
318
|
L.push(` - \`${d.dir}/\` holds only \`${d.childPath}\``);
|
|
316
|
-
L.push('- root
|
|
317
|
-
if (r.
|
|
319
|
+
L.push('- flat root pages per section (depth-2 listing, shelving hint — NOT inbound analysis; true orphans = surface deep links.orphanPages):');
|
|
320
|
+
if (r.flatRootPages.length === 0)
|
|
318
321
|
L.push(' - none');
|
|
319
|
-
for (const o of r.
|
|
322
|
+
for (const o of r.flatRootPages)
|
|
320
323
|
L.push(` - \`${o.section}/\` (${fmt(o.paths.length)}): ${o.paths.map((p) => `\`${p}\``).join(', ')}`);
|
|
321
324
|
L.push('', '## Tag vocabulary', '');
|
|
322
325
|
if (!r.tags.available)
|
package/dist/surface.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { MaintainRow } from './maintain.js';
|
|
15
15
|
import type { Locale } from './wiki/pages.read.js';
|
|
16
|
+
import type { NavSnapshot } from './wiki/nav.js';
|
|
16
17
|
export declare const SURFACE_SCHEMA: "historian.surface.v1";
|
|
17
18
|
/** Minimal shape of a live `pages.list` row the surface diff needs. */
|
|
18
19
|
export interface LiveRow {
|
|
@@ -26,6 +27,12 @@ export interface SurfaceInput {
|
|
|
26
27
|
readonly generatedAt: string;
|
|
27
28
|
readonly baseUrl: string;
|
|
28
29
|
readonly liveInventory?: readonly LiveRow[];
|
|
30
|
+
/**
|
|
31
|
+
* Live primary nav. Issue #1's disease is sidebar exposure, so the check
|
|
32
|
+
* must read the real tree — omit/null reports `available: false` rather
|
|
33
|
+
* than falsely claiming a clean nav.
|
|
34
|
+
*/
|
|
35
|
+
readonly nav?: NavSnapshot | null;
|
|
29
36
|
readonly deep?: boolean;
|
|
30
37
|
readonly readBody?: (path: string, locale: Locale) => Promise<string | null>;
|
|
31
38
|
}
|
|
@@ -39,7 +46,18 @@ export interface SurfaceCoverage {
|
|
|
39
46
|
readonly removedFromLive: number;
|
|
40
47
|
}
|
|
41
48
|
export interface SurfaceNav {
|
|
42
|
-
readonly
|
|
49
|
+
readonly available: boolean;
|
|
50
|
+
readonly mode: string | null;
|
|
51
|
+
/** DYNAMIC/MIXED re-mirror the filesystem page tree into the sidebar — the exact Issue #1 relapse. */
|
|
52
|
+
readonly filesystemExposed: boolean;
|
|
53
|
+
/** Underscore-prefixed (machine-namespace) links explicitly mounted in the curated tree. */
|
|
54
|
+
readonly machineLinks: {
|
|
55
|
+
readonly locale: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
readonly target: string;
|
|
58
|
+
}[];
|
|
59
|
+
/** Informational: underscore segments in the PAGE tree — by design (_meta/_evidence/_sandbox/_data). */
|
|
60
|
+
readonly machinePaths: readonly string[];
|
|
43
61
|
readonly sectionLandingMissing: {
|
|
44
62
|
readonly dir: string;
|
|
45
63
|
readonly pagePaths: number;
|
package/dist/surface.js
CHANGED
|
@@ -16,6 +16,9 @@ import { lintBody } from './lint.js';
|
|
|
16
16
|
import { classifyGenre } from './templates/genres.js';
|
|
17
17
|
export const SURFACE_SCHEMA = 'historian.surface.v1';
|
|
18
18
|
const MACHINE_SEG_RE = /^_/;
|
|
19
|
+
// nav targets carry a leading slash and may pre-pend the locale segment
|
|
20
|
+
// (/zh/_meta/x) — machine check runs on the first real path segment.
|
|
21
|
+
const MACHINE_TARGET_RE = /^\/(?:(?:en|zh)\/)?_[^/]+/;
|
|
19
22
|
const ROOT_EXEMPT = new Set(['home', 'wiki-index']);
|
|
20
23
|
const visible = (r) => r.isPublished !== false && r.isPrivate !== true;
|
|
21
24
|
const rowKey = (path, locale) => `${locale}\u0000${path}`;
|
|
@@ -23,15 +26,24 @@ function isFrontPath(path) {
|
|
|
23
26
|
return !isInternalPath(path) && !path.startsWith('_sandbox/') && !path.startsWith('_data/');
|
|
24
27
|
}
|
|
25
28
|
// --- light tier ---------------------------------------------------------------
|
|
26
|
-
function buildNav(rows, live) {
|
|
29
|
+
function buildNav(rows, live, nav) {
|
|
27
30
|
const paths = new Set();
|
|
28
31
|
for (const r of rows)
|
|
29
32
|
paths.add(r.path);
|
|
30
33
|
for (const r of live ?? [])
|
|
31
34
|
paths.add(r.path);
|
|
32
|
-
const
|
|
35
|
+
const machinePaths = [...new Set([...paths].map((p) => p.split('/')[0]))]
|
|
33
36
|
.filter((s) => MACHINE_SEG_RE.test(s))
|
|
34
37
|
.sort();
|
|
38
|
+
const mode = nav?.mode ?? null;
|
|
39
|
+
const machineLinks = [];
|
|
40
|
+
for (const t of nav?.trees ?? []) {
|
|
41
|
+
for (const it of t.items) {
|
|
42
|
+
if (MACHINE_TARGET_RE.test(it.target)) {
|
|
43
|
+
machineLinks.push({ locale: t.locale, label: it.label, target: it.target });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
const perDir = new Map();
|
|
36
48
|
for (const p of paths) {
|
|
37
49
|
const seg = p.split('/');
|
|
@@ -43,7 +55,14 @@ function buildNav(rows, live) {
|
|
|
43
55
|
.filter(([dir, n]) => n >= 2 && !paths.has(dir))
|
|
44
56
|
.map(([dir, pagePaths]) => ({ dir, pagePaths }))
|
|
45
57
|
.sort((a, b) => b.pagePaths - a.pagePaths || a.dir.localeCompare(b.dir));
|
|
46
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
available: nav != null,
|
|
60
|
+
mode,
|
|
61
|
+
filesystemExposed: mode === 'DYNAMIC' || mode === 'MIXED',
|
|
62
|
+
machineLinks,
|
|
63
|
+
machinePaths,
|
|
64
|
+
sectionLandingMissing,
|
|
65
|
+
};
|
|
47
66
|
}
|
|
48
67
|
function buildCoverage(rows, live) {
|
|
49
68
|
if (live === undefined)
|
|
@@ -269,7 +288,7 @@ export async function buildSurfaceReport(input) {
|
|
|
269
288
|
generatedAt: input.generatedAt,
|
|
270
289
|
deep: input.deep === true,
|
|
271
290
|
coverage: buildCoverage(input.rows, input.liveInventory),
|
|
272
|
-
nav: buildNav(input.rows, input.liveInventory),
|
|
291
|
+
nav: buildNav(input.rows, input.liveInventory, input.nav),
|
|
273
292
|
tagsEmpty: input.rows
|
|
274
293
|
.filter((r) => isFrontPath(r.path) && (r.tags?.length ?? 0) === 0)
|
|
275
294
|
.map((r) => ({ path: r.path, locale: r.locale }))
|
|
@@ -291,8 +310,21 @@ export function renderSurfaceMarkdown(r) {
|
|
|
291
310
|
for (const m of cap(r.coverage.missingFromMap, 40))
|
|
292
311
|
L.push(` - \`${m.locale}/${m.path}\``);
|
|
293
312
|
}
|
|
294
|
-
L.push('', '## 导航 Nav
|
|
295
|
-
|
|
313
|
+
L.push('', '## 导航 Nav(真相 = 实时导航树,非页面树推断)', '');
|
|
314
|
+
if (!r.nav.available) {
|
|
315
|
+
L.push('- ⚠ 导航树不可读(nav.available=false)— 机器段暴露无法核验,请检查 token 的导航读取权限');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
L.push(`- mode: \`${r.nav.mode}\` · 文件系统暴露 filesystemExposed: ${r.nav.filesystemExposed ? '⚠ 是 — DYNAMIC/MIXED 会把页面树镜像回侧栏(Issue #1 复发)' : '否'}`);
|
|
319
|
+
if (r.nav.machineLinks.length > 0) {
|
|
320
|
+
L.push(`- ⚠ 导航树内机器段链接 machineLinks (${r.nav.machineLinks.length}):`);
|
|
321
|
+
for (const m of cap(r.nav.machineLinks, 20))
|
|
322
|
+
L.push(` - [${m.locale}] ${m.label} → \`${m.target}\``);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
L.push(`- 导航树内机器段链接: none ✓(页面树存档段 ${r.nav.machinePaths.map((s) => `\`${s}/\``).join(' ') || '—'} 属设计内,仅备查)`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
296
328
|
if (r.nav.sectionLandingMissing.length > 0) {
|
|
297
329
|
L.push(`- 落地页缺失 sectionLandingMissing(面包屑 404 / 空目录页):`);
|
|
298
330
|
for (const s of r.nav.sectionLandingMissing)
|
package/dist/tools/local.js
CHANGED
|
@@ -12,6 +12,7 @@ import { buildMaintainReport, renderMaintainMarkdown } from '../maintain.js';
|
|
|
12
12
|
import { buildSurfaceReport, renderSurfaceMarkdown } from '../surface.js';
|
|
13
13
|
import { normalizeLocale, PathValidationError } from '../wiki/locale.js';
|
|
14
14
|
import { listPages, readPage } from '../wiki/pages.read.js';
|
|
15
|
+
import { readPrimaryNav } from '../wiki/nav.js';
|
|
15
16
|
import { errEnvelope, okJson, reportUrls, URL_MANDATE } from './shared.js';
|
|
16
17
|
const s = tool.schema;
|
|
17
18
|
const TRANSLATE_ARGS = {
|
|
@@ -102,17 +103,19 @@ async function runMaintain(deps, mapDeps, snapshot, deep) {
|
|
|
102
103
|
}
|
|
103
104
|
: undefined;
|
|
104
105
|
const report = await buildMaintainReport({ rows, mapGeneratedAt: snapshot.generatedAt, mapStaleSeconds: snapshot.staleSeconds }, { deep, readBody });
|
|
106
|
+
const nav = await readPrimaryNav(client);
|
|
105
107
|
const surface = await buildSurfaceReport({
|
|
106
108
|
rows,
|
|
107
109
|
generatedAt: report.generatedAt,
|
|
108
110
|
baseUrl: deps.options.baseUrl,
|
|
109
111
|
liveInventory,
|
|
112
|
+
nav,
|
|
110
113
|
deep,
|
|
111
114
|
readBody,
|
|
112
115
|
});
|
|
113
116
|
return {
|
|
114
117
|
action: 'maintain',
|
|
115
|
-
schema: 'historian.maintain.
|
|
118
|
+
schema: 'historian.maintain.v3',
|
|
116
119
|
deep: report.deep,
|
|
117
120
|
generatedAt: report.generatedAt,
|
|
118
121
|
rowCount: report.rowCount,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type GqlClient } from './client.js';
|
|
2
|
+
export interface NavItem {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly targetType: string;
|
|
5
|
+
readonly target: string;
|
|
6
|
+
}
|
|
7
|
+
export interface NavTree {
|
|
8
|
+
readonly locale: string;
|
|
9
|
+
readonly items: readonly NavItem[];
|
|
10
|
+
}
|
|
11
|
+
export interface NavSnapshot {
|
|
12
|
+
readonly mode: string;
|
|
13
|
+
readonly trees: readonly NavTree[];
|
|
14
|
+
}
|
|
15
|
+
/** Shape-tolerant parse: unknown/nullish shapes degrade to '' entries; a
|
|
16
|
+
* payload without `navigation` is not-a-nav (null), letting the caller mark
|
|
17
|
+
* the check unavailable instead of claiming "clean". */
|
|
18
|
+
export declare function parseNav(raw: unknown): NavSnapshot | null;
|
|
19
|
+
/** Never throws: an unreadable nav (older server, token without navigation
|
|
20
|
+
* read) returns null so the scan degrades to "unavailable", not failure. */
|
|
21
|
+
export declare function readPrimaryNav(client: GqlClient): Promise<NavSnapshot | null>;
|
package/dist/wiki/nav.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Live navigation reader (v0.5.1). Issue #1's actual disease was the SIDEBAR
|
|
2
|
+
// mirroring the filesystem (DYNAMIC/MIXED exposing _meta/_evidence/_sandbox),
|
|
3
|
+
// not the existence of machine-namespace pages — those are by design. So the
|
|
4
|
+
// detector reads the real primary nav (mode + curated flat trees) instead of
|
|
5
|
+
// inferring exposure from the page tree.
|
|
6
|
+
//
|
|
7
|
+
// Shape note: this wiki.js generation REJECTS `children` on NavigationItem
|
|
8
|
+
// (HTTP 400, re-probed 2026-09-08) — flat `items` are the whole truth.
|
|
9
|
+
import { gql } from './client.js';
|
|
10
|
+
const NAV_QUERY = '{ navigation { config { mode } tree { locale items { label targetType target } } } }';
|
|
11
|
+
/** Shape-tolerant parse: unknown/nullish shapes degrade to '' entries; a
|
|
12
|
+
* payload without `navigation` is not-a-nav (null), letting the caller mark
|
|
13
|
+
* the check unavailable instead of claiming "clean". */
|
|
14
|
+
export function parseNav(raw) {
|
|
15
|
+
const nav = raw?.navigation;
|
|
16
|
+
if (nav === undefined || nav === null)
|
|
17
|
+
return null;
|
|
18
|
+
const trees = [];
|
|
19
|
+
for (const t of Array.isArray(nav.tree) ? nav.tree : []) {
|
|
20
|
+
const row = t;
|
|
21
|
+
const items = [];
|
|
22
|
+
for (const i of Array.isArray(row?.items) ? row.items : []) {
|
|
23
|
+
const it = i;
|
|
24
|
+
items.push({
|
|
25
|
+
label: typeof it?.label === 'string' ? it.label : '',
|
|
26
|
+
targetType: typeof it?.targetType === 'string' ? it.targetType : '',
|
|
27
|
+
target: typeof it?.target === 'string' ? it.target : '',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
trees.push({ locale: typeof row?.locale === 'string' ? row.locale : '', items });
|
|
31
|
+
}
|
|
32
|
+
return { mode: typeof nav.config?.mode === 'string' ? nav.config.mode : '', trees };
|
|
33
|
+
}
|
|
34
|
+
/** Never throws: an unreadable nav (older server, token without navigation
|
|
35
|
+
* read) returns null so the scan degrades to "unavailable", not failure. */
|
|
36
|
+
export async function readPrimaryNav(client) {
|
|
37
|
+
try {
|
|
38
|
+
return parseNav(await gql(client, NAV_QUERY, {}));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -200,7 +200,7 @@ G5 现状卡的硬约束:状态块是机读单行(`Active` / `Superseded-by:
|
|
|
200
200
|
| `historian_translate_snippet` | 翻译片段 | `text`, `from`(en/zh), `to`(en/zh) |
|
|
201
201
|
| `historian_search` | 搜索页面 | `query`, `kind`(title/content), `tags?`(1-5 个), `tagsMode?`(all 缺省/any) |
|
|
202
202
|
| `historian_read` | 读取页面 | `path`, `locale` |
|
|
203
|
-
| `historian_map` | 页面地图/时间线/维护扫描 | `action`(show/refresh/timeline/maintain);maintain 可选 `deep`(缺省 false=light 扫);timeline 可选 `days`(近 N 天)与 `path`(前缀过滤);输出人读 markdown + 机读 JSON,zh/en 行独立;maintain 同时返回 surface 接口面体检(信封 `historian.maintain.
|
|
203
|
+
| `historian_map` | 页面地图/时间线/维护扫描 | `action`(show/refresh/timeline/maintain);maintain 可选 `deep`(缺省 false=light 扫);timeline 可选 `days`(近 N 天)与 `path`(前缀过滤);输出人读 markdown + 机读 JSON,zh/en 行独立;maintain 同时返回 surface 接口面体检(信封 `historian.maintain.v3`) |
|
|
204
204
|
| `historian_migrate` | 迁移页面到规范 | `path`, `genre?`, `apply`(false/true) |
|
|
205
205
|
| `historian_delete` | 删除页面 | `path`, `locale`, `confirm`(必须 "yes") |
|
|
206
206
|
| `historian_move` | 移动页面 | `path`, `locale`, `newPath`, `newLocale?`, `confirm`(必须 "yes") |
|
|
@@ -327,21 +327,21 @@ historian_map action:'refresh'
|
|
|
327
327
|
- **light 扫:每次批量写后必跑**——只基于地图行 + 每 locale 一次只读 `pages.list`(便宜,随批走)
|
|
328
328
|
- **deep 扫:每周至多一次**——逐页读正文,跑新鲜度(缺「上次核实于」/ 复核过期)与 `> Redirect:` 存根计数(贵,克制用)
|
|
329
329
|
|
|
330
|
-
light 扫在 maintain 行之外附带 **surface-light**:`coverage`(live 页面与地图不一致)、`nav
|
|
330
|
+
light 扫在 maintain 行之外附带 **surface-light**:`coverage`(live 页面与地图不一致)、`nav`(侧栏真相=实时导航树:mode 非 STATIC 即把页面树镜像回侧栏 / 树内挂 `_` 段链接 / 章节缺落地页→面包屑 404)、`tagsEmpty`;deep 扫附带 **surface-deep**:正文级检测,逐页一次读取、双消费者共享缓存。
|
|
331
331
|
|
|
332
332
|
报告行 → 处置映射表:
|
|
333
333
|
|
|
334
334
|
| 报告行 | 含义 | 处置 |
|
|
335
335
|
|--------|------|------|
|
|
336
336
|
| `missingTwinPaths` | 双语孪生缺口 | 补孪生:翻译腿建 zh(或 en)页,走翻译失败处理 |
|
|
337
|
-
| `duplicates.clusters` | 近重复标题簇(trigram-Jaccard
|
|
337
|
+
| `duplicates.clusters` | 近重复标题簇(trigram-Jaccard 阈值;「(重定向)」存根不参与聚类) | bold-merge 流程:选最完整页为权威(bold),其余走 supersede 或 Redirect 存根 |
|
|
338
338
|
| `staleness.oldest` | 最陈旧页 | mark/refresh:G5 卡重新核实或标记 stale,不静默覆盖 |
|
|
339
|
-
| `
|
|
339
|
+
| `flatRootPages` / `diffusion.singleChildDirs` | 章节根平铺页清单(归架提示,非入链判定)/ 独子目录 | 归架:并入正确章节、建章节索引,或按冻结协议做 Redirect 存根;真孤儿看 surface deep 的 `orphanPages` |
|
|
340
340
|
| `tags.vocabulary` | 标签漂移 | 词表映射:近义标签收敛到主词,`historian_page_update` 批量改 |
|
|
341
341
|
| `redirects.stubs`(deep) | 重定向存根清单 | 核对目标存在、入链已改写;死链存根即修 |
|
|
342
342
|
| `freshness`(deep) | 缺核实戳 / reviewBy 过期 | 回 G5 卡补核;到期页列入下周复核 |
|
|
343
343
|
| `coverage.missingFromMap`(surface) | 新页/迁移未进地图 | `action:'refresh'` 后重扫 |
|
|
344
|
-
| `nav.
|
|
344
|
+
| `nav.filesystemExposed` / `nav.machineLinks`(surface) | 侧栏被 DYNAMIC/MIXED 镜像出页面树 / 导航树里挂了 `_` 段链接 | 导航树手工策划只挂主题章节,mode 固定 STATIC;`nav.available=false` 时先修 token 导航读权限再下结论 |
|
|
345
345
|
| `nav.sectionLandingMissing`(surface) | 章节缺落地页(面包屑 404) | 建章节总览页并链入 wiki-index |
|
|
346
346
|
| `unfinished`(surface deep) | Active 页含 TODO/空节/导言空 | 补全或降回 draft |
|
|
347
347
|
| `stubs` / `links.broken` / `toStubs` / `sameTargetStacks`(deep) | 存根无可点出口 / 死链 / 指存根 / 同页多锚点 | 修出口与目标;锚点收敛到规范页 |
|