@riebeckite/plugin-diagnostics 0.0.12 → 0.0.14

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/README.md CHANGED
@@ -1,132 +1,159 @@
1
- # @riebeckite/plugin-diagnostics
2
-
3
- Content diagnostics for Obsidian vaults / Riebeckite content: broken links,
4
- frontmatter issues, orphan notes, unused assets, and more. Usable as a build
5
- plugin, a programmatic API, and a CLI.
6
-
7
- [日本語](./README_ja.md)
8
-
9
- ## Overview
10
-
11
- `diagnostics()` analyzes the content directory and reports problems as
12
- diagnostics during the build. The same checks are available through
13
- `runDiagnostics()` and the `riebeckite-diagnostics` CLI.
14
-
15
- ## Usage (plugin)
16
-
17
- ```ts
18
- import { defineConfig } from "@riebeckite/core";
19
- import { diagnostics } from "@riebeckite/plugin-diagnostics";
20
-
21
- export default defineConfig({
22
- // ...
23
- plugins: [
24
- diagnostics({
25
- reportUnusedAssets: true,
26
- reportOrphans: true,
27
- requiredFrontmatter: ["title"],
28
- }),
29
- ],
30
- });
31
- ```
32
-
33
- - `addDiagnostics` — runs the analysis at build time; results go to
34
- `manifest.diagnostics`
35
- - `onBuildEnd` — with `failOnError: true`, throws `DiagnosticsFailure` when
36
- error-level diagnostics exist
37
-
38
- ## Checks
39
-
40
- | Code | Default severity | What it detects |
41
- | ---- | ---------------- | --------------- |
42
- | `broken-wikilink` | `error` | Wikilink target or fragment does not resolve |
43
- | `broken-image` | `error` | Embedded image or image link does not exist |
44
- | `broken-link` | `error` | Markdown link points to a missing or excluded note/file |
45
- | `unused-asset` | `warning` | Image never referenced by any note (`reportUnusedAssets`) |
46
- | `orphan-note` | `info` | Published note has no incoming links (`reportOrphans`) |
47
- | `missing-frontmatter` | `warning` | No frontmatter, or required fields missing |
48
- | `publish-conflict` | `warning` | `publish: true` combined with `draft: true` / `private: true` |
49
- | `duplicate-title` | `warning` | Multiple published notes share a title |
50
- | `slug-collision` | `error` | Slugs collide case-insensitively |
51
- | `duplicate-content-id` | `error` | Published notes share a stable content ID (`id`/`uid`) |
52
- | `invalid-content-id` | `error` | `id`/`uid` frontmatter violates the stable content ID contract |
53
- | `excluded-public` | `warning` | Excluded note is marked `publish: true` |
54
- | `publish-boundary` | `warning` | Published content links to or embeds non-published content |
55
- | `analytics-untracked` | `info` | Published note has no stable `id` and the analytics plugin will not track it (enabled with `reportAnalyticsCoverage`, or automatically when the config enables the analytics plugin) |
56
- | `internal-error` | `error` | Content analysis failed |
57
-
58
- ## Options
59
-
60
- | Option | Type | Default | Description |
61
- | ------ | ---- | ------- | ----------- |
62
- | `failOnError` | `boolean` | `false` | Fail the build on error-level diagnostics |
63
- | `reportUnusedAssets` | `boolean` | `false` | Report unreferenced images |
64
- | `reportOrphans` | `boolean` | `false` | Report published notes without incoming links |
65
- | `reportAnalyticsCoverage` | `boolean` | `false` | Report published notes the analytics plugin will not track (auto-enabled when the resolved config enables the analytics plugin) |
66
- | `requiredFrontmatter` | `string[]` | `[]` | Required frontmatter fields |
67
- | `severity` | `Partial<Record<DiagnosticCode, DiagnosticSeverity>>` | table above | Override severity per code |
68
- | `exclude` | `string[]` | `[]` | Extra exclude globs |
69
- | `publishStrategy` | `"explicit" \| "selective"` | from config | Publication filter strategy |
70
-
71
- ## CLI
72
-
73
- ```bash
74
- riebeckite-diagnostics --config riebeckite.config.ts
75
- riebeckite-diagnostics --content ./content --report-orphans
76
- ```
77
-
78
- | Option | Description |
79
- | ------ | ----------- |
80
- | `--config <path>` | Path to `riebeckite.config.ts` (loaded under tsx) |
81
- | `--content <dir>` | Content directory to analyze (default: `.`) |
82
- | `--exclude <glob>` | Extra exclude glob (repeatable) |
83
- | `--publish-strategy <mode>` | `explicit` \| `selective` (default: `selective`) |
84
- | `--report-unused-assets` | Report images never referenced by any note |
85
- | `--report-orphans` | Report published notes with no incoming links |
86
- | `--report-analytics-coverage` | Report published notes without a stable content ID (auto-enabled for `--config` when the config enables the analytics plugin) |
87
- | `--required-frontmatter <f>` | Comma-separated required frontmatter fields |
88
- | `--fail-on-error` | Exit with code 1 when errors are found (default) |
89
- | `--exit-on <severity>` | Exit with code 1 at/above severity (`info` \| `warning` \| `error`) |
90
- | `--format <text\|json>` | Output format (default: `text`) |
91
- | `--no-color` | Disable ANSI colors |
92
- | `-h`, `--help` | Show help |
93
-
94
- Exit codes: `0` no errors, `1` errors reported (or `--exit-on` threshold met),
95
- `2` invalid arguments.
96
-
97
- With `--config`, options from the config's `diagnostics` plugin are used as
98
- defaults; explicit CLI flags override them, and `exclude` lists are merged.
99
-
100
- ## Programmatic API
101
-
102
- ```ts
103
- import {
104
- assertNoErrors,
105
- formatDiagnostics,
106
- runDiagnostics,
107
- } from "@riebeckite/plugin-diagnostics";
108
-
109
- const report = await runDiagnostics("./content", { reportOrphans: true });
110
- console.log(formatDiagnostics(report));
111
- assertNoErrors(report);
112
- ```
113
-
114
- `runDiagnostics(config | path, options?)` returns a `DiagnosticsReport` with
115
- `diagnostics`, `errors`, `warnings`, `infos`, `hasErrors`, `hasWarnings`,
116
- `summary`, and `byCode`.
117
-
118
- ## Exports
119
-
120
- - `diagnostics(options?)` / `diagnosticsPlugin` — plugin factory
121
- - `runDiagnostics(target, options?)` — run the analysis directly
122
- - `analyzeContent(config, options?)` — raw analysis returning `Diagnostic[]`
123
- - `hasEnabledAnalyticsPlugin(config?)` — whether the resolved config enables the analytics plugin
124
- - Report helpers: `buildReport`, `formatDiagnostics`, `groupByCode`,
125
- `summarize`, `assertNoErrors`, `DiagnosticsFailure`
126
- - Types: `DiagnosticsOptions`, `DiagnosticsReport`, `DiagnosticsSummary`,
127
- `AnalyzerContentConfig`
128
- - CLI: `riebeckite-diagnostics`
129
-
130
- ## See also
131
-
132
- - [Plugin guide](../../../docs/en/plugin-system.md)
1
+ # @riebeckite/plugin-diagnostics
2
+
3
+ Content diagnostics for Obsidian vaults / Riebeckite content: site-wide
4
+ reference integrity, frontmatter issues, orphan notes, unused assets, and more.
5
+ Usable as a build plugin, a programmatic API, and a CLI.
6
+
7
+ [日本語](./README_ja.md)
8
+
9
+ ## Overview
10
+
11
+ `diagnostics()` analyzes the content directory and reports problems as
12
+ diagnostics during the build. The same checks are available through
13
+ `runDiagnostics()` and the `riebeckite-diagnostics` CLI.
14
+
15
+ ## Usage (plugin)
16
+
17
+ ```ts
18
+ import { defineConfig } from "@riebeckite/core";
19
+ import { diagnostics } from "@riebeckite/plugin-diagnostics";
20
+
21
+ export default defineConfig({
22
+ // ...
23
+ plugins: [
24
+ diagnostics({
25
+ reportUnusedAssets: true,
26
+ reportOrphans: true,
27
+ requiredFrontmatter: ["title"],
28
+ }),
29
+ ],
30
+ });
31
+ ```
32
+
33
+ - `addDiagnostics` — runs the analysis at build time; results go to
34
+ `manifest.diagnostics`
35
+ - `onBuildEnd` — with `failOnError: true`, throws `DiagnosticsFailure` when
36
+ error-level diagnostics exist
37
+
38
+ ## Checks
39
+
40
+ | Code | Default severity | What it detects |
41
+ | ---- | ---------------- | --------------- |
42
+ | `broken-wikilink` | `error` | Wikilink target or fragment does not resolve |
43
+ | `broken-image` | `error` | Embedded image or image link does not exist |
44
+ | `broken-link` | `error` | Markdown link points to a missing or excluded note/file |
45
+ | `unused-asset` | `warning` | Image never referenced by any note (`reportUnusedAssets`) |
46
+ | `orphan-note` | `info` | Published note has no incoming links (`reportOrphans`) |
47
+ | `missing-frontmatter` | `warning` | No frontmatter, or required fields missing |
48
+ | `publish-conflict` | `warning` | `publish: true` combined with `draft: true` / `private: true` |
49
+ | `duplicate-title` | `warning` | Multiple published notes share a title |
50
+ | `slug-collision` | `error` | Slugs collide case-insensitively |
51
+ | `duplicate-content-id` | `error` | Published notes share a stable content ID (`id`/`uid`) |
52
+ | `invalid-content-id` | `error` | `id`/`uid` frontmatter violates the stable content ID contract |
53
+ | `excluded-public` | `warning` | Excluded note is marked `publish: true` |
54
+ | `publish-boundary` | `warning` | Published content links to or embeds non-published content |
55
+ | `analytics-untracked` | `info` | Published note has no stable `id` and the analytics plugin will not track it (enabled with `reportAnalyticsCoverage`, or automatically when the config enables the analytics plugin) |
56
+ | `content-integrity:broken-link` | `warning` | A published page links to a site-local route that is not published or registered |
57
+ | `content-integrity:unresolved-wikilink` | `warning` | A published entry contains a WikiLink that the configured WikiLink/content index resolution did not resolve |
58
+ | `content-integrity:broken-asset` | `warning` | A published page references a local asset that is not in resolved content assets, plugin assets, generated outputs, or public routes |
59
+ | `content-integrity:duplicate-public-location` | `error` | Two published entries or generated routes claim the same final public path |
60
+ | `content-integrity:redirect-target-missing` | `warning` | A public redirect points at content whose public target is unavailable |
61
+ | `content-integrity:redirect-cycle` | `error` | Public redirects form a cycle |
62
+ | `content-integrity:redirect-public-location-conflict` | `error` | A redirect path conflicts with another public route |
63
+ | `internal-error` | `error` | Content analysis failed |
64
+
65
+ ## Site-wide content integrity
66
+
67
+ When used as a Riebeckite plugin, diagnostics runs site-wide integrity checks
68
+ from the resolved manifest instead of rescanning content for link rules. It uses
69
+ `publicEntries`, resolved `ContentLink` metadata, `ContentPublicLocation`,
70
+ `publicRedirects`, plugin page paths, plugin assets, and generated outputs.
71
+ This keeps permalink, alias, rename redirect, l10n, publish/exclude, and Page
72
+ System behavior aligned with the build pipeline.
73
+
74
+ It does not check external HTTP reachability, SEO, spelling, Lighthouse, or
75
+ automatic repairs. External URLs (`http:`, `https:`, `mailto:`, `tel:`, `data:`),
76
+ protocol-relative URLs, and fragment-only links are ignored. Query strings and
77
+ fragments are stripped before route existence checks. Plugin authors should
78
+ register generated routes through Page Types and generated files/assets through
79
+ the existing plugin output/asset contracts so integrity checks can treat them as
80
+ valid public targets.
81
+
82
+ `runDiagnostics()` and the standalone CLI still use the filesystem/content-source
83
+ analyzer because no full manifest is available in that mode.
84
+
85
+ ## Options
86
+
87
+ | Option | Type | Default | Description |
88
+ | ------ | ---- | ------- | ----------- |
89
+ | `failOnError` | `boolean` | `false` | Fail the build on error-level diagnostics |
90
+ | `reportUnusedAssets` | `boolean` | `false` | Report unreferenced images |
91
+ | `reportOrphans` | `boolean` | `false` | Report published notes without incoming links |
92
+ | `reportAnalyticsCoverage` | `boolean` | `false` | Report published notes the analytics plugin will not track (auto-enabled when the resolved config enables the analytics plugin) |
93
+ | `requiredFrontmatter` | `string[]` | `[]` | Required frontmatter fields |
94
+ | `severity` | `Partial<Record<DiagnosticCode, DiagnosticSeverity>>` | table above | Override severity per code |
95
+ | `exclude` | `string[]` | `[]` | Extra exclude globs |
96
+ | `publishStrategy` | `"explicit" \| "selective"` | from config | Publication filter strategy |
97
+
98
+ ## CLI
99
+
100
+ ```bash
101
+ riebeckite-diagnostics --config riebeckite.config.ts
102
+ riebeckite-diagnostics --content ./content --report-orphans
103
+ ```
104
+
105
+ | Option | Description |
106
+ | ------ | ----------- |
107
+ | `--config <path>` | Path to `riebeckite.config.ts` (loaded under tsx) |
108
+ | `--content <dir>` | Content directory to analyze (default: `.`) |
109
+ | `--exclude <glob>` | Extra exclude glob (repeatable) |
110
+ | `--publish-strategy <mode>` | `explicit` \| `selective` (default: `selective`) |
111
+ | `--report-unused-assets` | Report images never referenced by any note |
112
+ | `--report-orphans` | Report published notes with no incoming links |
113
+ | `--report-analytics-coverage` | Report published notes without a stable content ID (auto-enabled for `--config` when the config enables the analytics plugin) |
114
+ | `--required-frontmatter <f>` | Comma-separated required frontmatter fields |
115
+ | `--fail-on-error` | Exit with code 1 when errors are found (default) |
116
+ | `--exit-on <severity>` | Exit with code 1 at/above severity (`info` \| `warning` \| `error`) |
117
+ | `--format <text\|json>` | Output format (default: `text`) |
118
+ | `--no-color` | Disable ANSI colors |
119
+ | `-h`, `--help` | Show help |
120
+
121
+ Exit codes: `0` no errors, `1` errors reported (or `--exit-on` threshold met),
122
+ `2` invalid arguments.
123
+
124
+ With `--config`, options from the config's `diagnostics` plugin are used as
125
+ defaults; explicit CLI flags override them, and `exclude` lists are merged.
126
+
127
+ ## Programmatic API
128
+
129
+ ```ts
130
+ import {
131
+ assertNoErrors,
132
+ formatDiagnostics,
133
+ runDiagnostics,
134
+ } from "@riebeckite/plugin-diagnostics";
135
+
136
+ const report = await runDiagnostics("./content", { reportOrphans: true });
137
+ console.log(formatDiagnostics(report));
138
+ assertNoErrors(report);
139
+ ```
140
+
141
+ `runDiagnostics(config | path, options?)` returns a `DiagnosticsReport` with
142
+ `diagnostics`, `errors`, `warnings`, `infos`, `hasErrors`, `hasWarnings`,
143
+ `summary`, and `byCode`.
144
+
145
+ ## Exports
146
+
147
+ - `diagnostics(options?)` / `diagnosticsPlugin` — plugin factory
148
+ - `runDiagnostics(target, options?)` — run the analysis directly
149
+ - `analyzeContent(config, options?)` — raw analysis returning `Diagnostic[]`
150
+ - `hasEnabledAnalyticsPlugin(config?)` — whether the resolved config enables the analytics plugin
151
+ - Report helpers: `buildReport`, `formatDiagnostics`, `groupByCode`,
152
+ `summarize`, `assertNoErrors`, `DiagnosticsFailure`
153
+ - Types: `DiagnosticsOptions`, `DiagnosticsReport`, `DiagnosticsSummary`,
154
+ `AnalyzerContentConfig`
155
+ - CLI: `riebeckite-diagnostics`
156
+
157
+ ## See also
158
+
159
+ - [Plugin guide](../../../docs/en/reference/plugin-api.md)
package/README_ja.md CHANGED
@@ -1,103 +1,118 @@
1
- # @riebeckite/plugin-diagnostics
2
-
3
- コンテンツのリンク切れ、公開設定の矛盾、frontmatter の不足などを検出するプラグインです。ビルド時の診断だけでなく、CLI とプログラムからの実行にも対応します。
4
-
5
- [English](./README.md)
6
-
7
- ## まずはビルドに診断を加える
8
-
9
- 通常は `diagnostics()` を設定に登録します。診断結果は manifest に入り、`failOnError` を有効にするとエラーがあったビルドを失敗させられます。
10
-
11
- ```ts
12
- import { defineConfig } from "@riebeckite/core";
13
- import { diagnostics } from "@riebeckite/plugin-diagnostics";
14
-
15
- export default defineConfig({
16
- // ...
17
- plugins: [
18
- diagnostics({
19
- reportUnusedAssets: true,
20
- reportOrphans: true,
21
- requiredFrontmatter: ["title"],
22
- }),
23
- ],
24
- });
25
- ```
26
-
27
- ## 検出する問題
28
-
29
- | コード | 既定の重要度 | 内容 |
30
- | --- | --- | --- |
31
- | `broken-wikilink` | error | Wikiリンクの参照先またはフラグメントを解決できない |
32
- | `broken-image` | error | 埋め込み画像または画像リンクの参照先がない |
33
- | `broken-link` | error | Markdown リンクが存在しない、または除外されたノートを指す |
34
- | `unused-asset` | warning | どのノートからも参照されない画像 |
35
- | `orphan-note` | info | 公開ノートのうち、他のノートからリンクされていないもの |
36
- | `missing-frontmatter` | warning | frontmatter がない、または必須フィールドが欠けている |
37
- | `publish-conflict` | warning | `publish: true` と `draft: true` または `private: true` が同居している |
38
- | `duplicate-title` | warning | 公開ノート同士でタイトルが重複している |
39
- | `slug-collision` | error | 大文字・小文字を区別しない slug が衝突している |
40
- | `duplicate-content-id` | error | 公開ノート同士で安定コンテンツ ID(`id`/`uid`)が重複している |
41
- | `invalid-content-id` | error | `id`/`uid` の frontmatter が安定コンテンツ ID の契約を満たしていない |
42
- | `excluded-public` | warning | 除外されたノートに `publish: true` が指定されている |
43
- | `publish-boundary` | warning | 公開コンテンツから非公開コンテンツへリンク・埋め込みしている |
44
- | `analytics-untracked` | info | 安定 `id` のない公開ノートは analytics プラグインで計測されない(`reportAnalyticsCoverage` で有効化。設定で analytics プラグインが有効な場合は自動で有効化) |
45
- | `internal-error` | error | コンテンツ解析中に処理できないエラーが発生した |
46
-
47
- 未使用アセットと孤立ノートは、明示的に有効化した場合だけ確認します。意図的に孤立させるトップページなどがあるなら、`reportOrphans` の結果を公開方針と照らして判断してください。
48
-
49
- ## 設定項目
50
-
51
- | 項目 | 既定値 | 説明 |
52
- | --- | --- | --- |
53
- | `failOnError` | `false` | error レベルの診断があればビルドを失敗させる |
54
- | `reportUnusedAssets` | `false` | 未参照画像を報告する |
55
- | `reportOrphans` | `false` | 孤立した公開ノートを報告する |
56
- | `reportAnalyticsCoverage` | `false` | analytics プラグインで計測されない公開ノートを報告する(解決済み設定で analytics プラグインが有効なら自動で有効化) |
57
- | `requiredFrontmatter` | `[]` | 必須にする frontmatter フィールド |
58
- | `severity` | コードごとの既定値 | 診断コード別の重要度を上書きする |
59
- | `exclude` | `[]` | 解析対象から追加で除外する glob |
60
- | `publishStrategy` | 設定ファイルの値 | 公開ノートの判定方法 |
61
-
62
- ## CI や編集時には CLI を使う
63
-
64
- 設定を読み込んで実行する場合は、次のようにします。
65
-
66
- ```bash
67
- riebeckite-diagnostics --config riebeckite.config.ts
68
- riebeckite-diagnostics --content ./content --report-orphans
69
- ```
70
-
71
- `--format json` は機械処理用、`--exit-on warning` は警告以上を CI の失敗条件にしたい場合に使います。終了コードは、問題なしが `0`、指定したしきい値以上の診断があれば `1`、引数の誤りは `2` です。`--config` 使用時は設定内のオプションを既定値にし、CLI で明示した値を優先します。
72
-
73
- ## アプリケーションから実行する
74
-
75
- エディタ連携や独自のレポートには `runDiagnostics()` を使えます。
76
-
77
- ```ts
78
- import {
79
- assertNoErrors,
80
- formatDiagnostics,
81
- runDiagnostics,
82
- } from "@riebeckite/plugin-diagnostics";
83
-
84
- const report = await runDiagnostics("./content", { reportOrphans: true });
85
- console.log(formatDiagnostics(report));
86
- assertNoErrors(report);
87
- ```
88
-
89
- `DiagnosticsReport` には診断の一覧に加え、エラー・警告・情報の件数、コード別の集計、`hasErrors` と `hasWarnings` が入ります。
90
-
91
- ## 主なエクスポート
92
-
93
- - `diagnostics(options?)` / `diagnosticsPlugin`: プラグインを作成する
94
- - `runDiagnostics(target, options?)`: 診断と集計を実行する
95
- - `analyzeContent(config, options?)`: 生の `Diagnostic[]` を取得する
96
- - `hasEnabledAnalyticsPlugin(config?)`: 解決済み設定で analytics プラグインが有効かを判定する
97
- - `formatDiagnostics`、`summarize`、`groupByCode`、`assertNoErrors`: レポート処理用の補助関数
98
- - `DiagnosticsFailure`: `failOnError` または `assertNoErrors` が送出するエラー
99
- - `riebeckite-diagnostics`: CLI
100
-
101
- ## 関連資料
102
-
103
- - [プラグインシステム](../../../docs/ja/plugin-system.md)
1
+ # @riebeckite/plugin-diagnostics
2
+
3
+ サイト全体の参照整合性、公開設定の矛盾、frontmatter の不足などを検出するプラグインです。ビルド時の診断だけでなく、CLI とプログラムからの実行にも対応します。
4
+
5
+ [English](./README.md)
6
+
7
+ ## まずはビルドに診断を加える
8
+
9
+ 通常は `diagnostics()` を設定に登録します。診断結果は manifest に入り、`failOnError` を有効にするとエラーがあったビルドを失敗させられます。
10
+
11
+ ```ts
12
+ import { defineConfig } from "@riebeckite/core";
13
+ import { diagnostics } from "@riebeckite/plugin-diagnostics";
14
+
15
+ export default defineConfig({
16
+ // ...
17
+ plugins: [
18
+ diagnostics({
19
+ reportUnusedAssets: true,
20
+ reportOrphans: true,
21
+ requiredFrontmatter: ["title"],
22
+ }),
23
+ ],
24
+ });
25
+ ```
26
+
27
+ ## 検出する問題
28
+
29
+ | コード | 既定の重要度 | 内容 |
30
+ | --- | --- | --- |
31
+ | `broken-wikilink` | error | Wikiリンクの参照先またはフラグメントを解決できない |
32
+ | `broken-image` | error | 埋め込み画像または画像リンクの参照先がない |
33
+ | `broken-link` | error | Markdown リンクが存在しない、または除外されたノートを指す |
34
+ | `unused-asset` | warning | どのノートからも参照されない画像 |
35
+ | `orphan-note` | info | 公開ノートのうち、他のノートからリンクされていないもの |
36
+ | `missing-frontmatter` | warning | frontmatter がない、または必須フィールドが欠けている |
37
+ | `publish-conflict` | warning | `publish: true` と `draft: true` または `private: true` が同居している |
38
+ | `duplicate-title` | warning | 公開ノート同士でタイトルが重複している |
39
+ | `slug-collision` | error | 大文字・小文字を区別しない slug が衝突している |
40
+ | `duplicate-content-id` | error | 公開ノート同士で安定コンテンツ ID(`id`/`uid`)が重複している |
41
+ | `invalid-content-id` | error | `id`/`uid` の frontmatter が安定コンテンツ ID の契約を満たしていない |
42
+ | `excluded-public` | warning | 除外されたノートに `publish: true` が指定されている |
43
+ | `publish-boundary` | warning | 公開コンテンツから非公開コンテンツへリンク・埋め込みしている |
44
+ | `analytics-untracked` | info | 安定 `id` のない公開ノートは analytics プラグインで計測されない(`reportAnalyticsCoverage` で有効化。設定で analytics プラグインが有効な場合は自動で有効化) |
45
+ | `content-integrity:broken-link` | warning | 公開ページが、公開または登録されていないサイト内経路へリンクしている |
46
+ | `content-integrity:unresolved-wikilink` | warning | 公開ページ内の Wikiリンクを、設定済みの解決規則で解決できない |
47
+ | `content-integrity:broken-asset` | warning | 公開ページが、解決済みアセット・プラグインアセット・生成出力・公開経路のいずれにもないローカルアセットを参照している |
48
+ | `content-integrity:duplicate-public-location` | error | 複数の公開コンテンツまたは生成経路が、同じ最終公開パスを所有している |
49
+ | `content-integrity:redirect-target-missing` | warning | 公開リダイレクトの転送先コンテンツが公開されない |
50
+ | `content-integrity:redirect-cycle` | error | 公開リダイレクトが循環している |
51
+ | `content-integrity:redirect-public-location-conflict` | error | リダイレクト元のパスが別の公開経路と衝突している |
52
+ | `internal-error` | error | コンテンツ解析中に処理できないエラーが発生した |
53
+
54
+ 未使用アセットと孤立ノートは、明示的に有効化した場合だけ確認します。意図的に孤立させるトップページなどがあるなら、`reportOrphans` の結果を公開方針と照らして判断してください。
55
+
56
+ ## サイト全体の参照整合性
57
+
58
+ Riebeckite のプラグインとして使う場合、リンク関係の検査はコンテンツを再走査せず、解決済み manifest から行います。`publicEntries`、解決済み `ContentLink`、`ContentPublicLocation`、`publicRedirects`、プラグインページのパス、プラグインアセット、生成出力を参照します。これにより permalink、alias、rename によるリダイレクト、l10n、公開・除外設定、Page System の扱いがビルド処理と揃います。
59
+
60
+ 外部 URL の疎通確認、SEO、スペルチェック、Lighthouse、自動修復は行いません。`http:`、`https:`、`mailto:`、`tel:`、`data:`、プロトコル相対 URL、ページ内フラグメントだけのリンクは対象外です。経路の存在確認では、クエリ文字列とフラグメントを除いて判定します。生成ページや生成アセットを提供するプラグインは、Page Type、生成出力、アセット登録の既存契約を使ってください。登録されていれば、整合性検査でも有効な公開先として扱われます。
61
+
62
+ `runDiagnostics()` と単体 CLI は、完全な manifest を持たないため、従来の content source 解析を使います。
63
+
64
+ ## 設定項目
65
+
66
+ | 項目 | 既定値 | 説明 |
67
+ | --- | --- | --- |
68
+ | `failOnError` | `false` | error レベルの診断があればビルドを失敗させる |
69
+ | `reportUnusedAssets` | `false` | 未参照画像を報告する |
70
+ | `reportOrphans` | `false` | 孤立した公開ノートを報告する |
71
+ | `reportAnalyticsCoverage` | `false` | analytics プラグインで計測されない公開ノートを報告する(解決済み設定で analytics プラグインが有効なら自動で有効化) |
72
+ | `requiredFrontmatter` | `[]` | 必須にする frontmatter フィールド |
73
+ | `severity` | コードごとの既定値 | 診断コード別の重要度を上書きする |
74
+ | `exclude` | `[]` | 解析対象から追加で除外する glob |
75
+ | `publishStrategy` | 設定ファイルの値 | 公開ノートの判定方法 |
76
+
77
+ ## CI や編集時には CLI を使う
78
+
79
+ 設定を読み込んで実行する場合は、次のようにします。
80
+
81
+ ```bash
82
+ riebeckite-diagnostics --config riebeckite.config.ts
83
+ riebeckite-diagnostics --content ./content --report-orphans
84
+ ```
85
+
86
+ `--format json` は機械処理用、`--exit-on warning` は警告以上を CI の失敗条件にしたい場合に使います。終了コードは、問題なしが `0`、指定したしきい値以上の診断があれば `1`、引数の誤りは `2` です。`--config` 使用時は設定内のオプションを既定値にし、CLI で明示した値を優先します。
87
+
88
+ ## アプリケーションから実行する
89
+
90
+ エディタ連携や独自のレポートには `runDiagnostics()` を使えます。
91
+
92
+ ```ts
93
+ import {
94
+ assertNoErrors,
95
+ formatDiagnostics,
96
+ runDiagnostics,
97
+ } from "@riebeckite/plugin-diagnostics";
98
+
99
+ const report = await runDiagnostics("./content", { reportOrphans: true });
100
+ console.log(formatDiagnostics(report));
101
+ assertNoErrors(report);
102
+ ```
103
+
104
+ `DiagnosticsReport` には診断の一覧に加え、エラー・警告・情報の件数、コード別の集計、`hasErrors` と `hasWarnings` が入ります。
105
+
106
+ ## 主なエクスポート
107
+
108
+ - `diagnostics(options?)` / `diagnosticsPlugin`: プラグインを作成する
109
+ - `runDiagnostics(target, options?)`: 診断と集計を実行する
110
+ - `analyzeContent(config, options?)`: 生の `Diagnostic[]` を取得する
111
+ - `hasEnabledAnalyticsPlugin(config?)`: 解決済み設定で analytics プラグインが有効かを判定する
112
+ - `formatDiagnostics`、`summarize`、`groupByCode`、`assertNoErrors`: レポート処理用の補助関数
113
+ - `DiagnosticsFailure`: `failOnError` または `assertNoErrors` が送出するエラー
114
+ - `riebeckite-diagnostics`: CLI
115
+
116
+ ## 関連資料
117
+
118
+ - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
package/dist/bin.js CHANGED
@@ -818,14 +818,16 @@ async function analyzeContent(config, options = {}) {
818
818
  const state = createAnalysisState();
819
819
  const diagnostics = [];
820
820
  for (const note of source.includedNotes) {
821
- checkWikilinks(note, source, state, normalizedOptions, diagnostics);
822
- checkMarkdownReferences(
823
- note,
824
- source,
825
- state,
826
- normalizedOptions,
827
- diagnostics
828
- );
821
+ if (!options.skipReferenceIntegrity) {
822
+ checkWikilinks(note, source, state, normalizedOptions, diagnostics);
823
+ checkMarkdownReferences(
824
+ note,
825
+ source,
826
+ state,
827
+ normalizedOptions,
828
+ diagnostics
829
+ );
830
+ }
829
831
  checkFrontmatter(note, source, state, normalizedOptions, diagnostics);
830
832
  }
831
833
  checkSlugCollisions(source, normalizedOptions, diagnostics);
@@ -834,7 +836,8 @@ async function analyzeContent(config, options = {}) {
834
836
  checkOrphans(source, state, normalizedOptions, diagnostics);
835
837
  checkUnusedAssets(source, state, normalizedOptions, diagnostics);
836
838
  checkExcludedPublic(source, normalizedOptions, diagnostics);
837
- checkPublishBoundary(source, normalizedOptions, diagnostics);
839
+ if (!options.skipReferenceIntegrity)
840
+ checkPublishBoundary(source, normalizedOptions, diagnostics);
838
841
  checkAnalyticsCoverage(source, normalizedOptions, diagnostics);
839
842
  return diagnostics;
840
843
  }
package/dist/index.js CHANGED
@@ -814,14 +814,16 @@ async function analyzeContent(config, options = {}) {
814
814
  const state = createAnalysisState();
815
815
  const diagnostics2 = [];
816
816
  for (const note of source.includedNotes) {
817
- checkWikilinks(note, source, state, normalizedOptions, diagnostics2);
818
- checkMarkdownReferences(
819
- note,
820
- source,
821
- state,
822
- normalizedOptions,
823
- diagnostics2
824
- );
817
+ if (!options.skipReferenceIntegrity) {
818
+ checkWikilinks(note, source, state, normalizedOptions, diagnostics2);
819
+ checkMarkdownReferences(
820
+ note,
821
+ source,
822
+ state,
823
+ normalizedOptions,
824
+ diagnostics2
825
+ );
826
+ }
825
827
  checkFrontmatter(note, source, state, normalizedOptions, diagnostics2);
826
828
  }
827
829
  checkSlugCollisions(source, normalizedOptions, diagnostics2);
@@ -830,7 +832,8 @@ async function analyzeContent(config, options = {}) {
830
832
  checkOrphans(source, state, normalizedOptions, diagnostics2);
831
833
  checkUnusedAssets(source, state, normalizedOptions, diagnostics2);
832
834
  checkExcludedPublic(source, normalizedOptions, diagnostics2);
833
- checkPublishBoundary(source, normalizedOptions, diagnostics2);
835
+ if (!options.skipReferenceIntegrity)
836
+ checkPublishBoundary(source, normalizedOptions, diagnostics2);
834
837
  checkAnalyticsCoverage(source, normalizedOptions, diagnostics2);
835
838
  return diagnostics2;
836
839
  }
@@ -855,16 +858,16 @@ function buildReport(diagnostics2) {
855
858
  const errors = [];
856
859
  const warnings = [];
857
860
  const infos = [];
858
- for (const diagnostic of diagnostics2) {
859
- switch (diagnostic.severity) {
861
+ for (const diagnostic2 of diagnostics2) {
862
+ switch (diagnostic2.severity) {
860
863
  case "error":
861
- errors.push(diagnostic);
864
+ errors.push(diagnostic2);
862
865
  break;
863
866
  case "warning":
864
- warnings.push(diagnostic);
867
+ warnings.push(diagnostic2);
865
868
  break;
866
869
  default:
867
- infos.push(diagnostic);
870
+ infos.push(diagnostic2);
868
871
  break;
869
872
  }
870
873
  }
@@ -883,8 +886,8 @@ function summarize(diagnostics2) {
883
886
  let error = 0;
884
887
  let warning = 0;
885
888
  let info = 0;
886
- for (const diagnostic of diagnostics2) {
887
- switch (diagnostic.severity) {
889
+ for (const diagnostic2 of diagnostics2) {
890
+ switch (diagnostic2.severity) {
888
891
  case "error":
889
892
  error++;
890
893
  break;
@@ -900,10 +903,10 @@ function summarize(diagnostics2) {
900
903
  }
901
904
  function groupByCode(diagnostics2) {
902
905
  const byCode = /* @__PURE__ */ new Map();
903
- for (const diagnostic of diagnostics2) {
904
- const group = byCode.get(diagnostic.code) ?? [];
905
- group.push(diagnostic);
906
- byCode.set(diagnostic.code, group);
906
+ for (const diagnostic2 of diagnostics2) {
907
+ const group = byCode.get(diagnostic2.code) ?? [];
908
+ group.push(diagnostic2);
909
+ byCode.set(diagnostic2.code, group);
907
910
  }
908
911
  return byCode;
909
912
  }
@@ -922,23 +925,23 @@ function formatDiagnostics(input, options = {}) {
922
925
  );
923
926
  lines.push("");
924
927
  const ordered = [...report.diagnostics].sort(compareDiagnostics);
925
- for (const diagnostic of ordered) {
928
+ for (const diagnostic2 of ordered) {
926
929
  const icon = {
927
930
  error: `${color.error}✖`,
928
931
  warning: `${color.yellow}⚠`,
929
932
  info: `${color.cyan}ℹ`
930
- }[diagnostic.severity];
933
+ }[diagnostic2.severity];
931
934
  const location = [
932
- diagnostic.filePath ?? "",
933
- diagnostic.line != null ? `:${diagnostic.line}${diagnostic.column != null ? `:${diagnostic.column}` : ""}` : ""
935
+ diagnostic2.filePath ?? "",
936
+ diagnostic2.line != null ? `:${diagnostic2.line}${diagnostic2.column != null ? `:${diagnostic2.column}` : ""}` : ""
934
937
  ].join("");
935
- lines.push(`${icon} ${color.reset}[${diagnostic.code}] ${location}`);
936
- lines.push(` ${diagnostic.message}`);
937
- if (diagnostic.target) {
938
- lines.push(` ${color.dim}target: ${diagnostic.target}${color.reset}`);
938
+ lines.push(`${icon} ${color.reset}[${diagnostic2.code}] ${location}`);
939
+ lines.push(` ${diagnostic2.message}`);
940
+ if (diagnostic2.target) {
941
+ lines.push(` ${color.dim}target: ${diagnostic2.target}${color.reset}`);
939
942
  }
940
- if (diagnostic.suggestion) {
941
- lines.push(` ${color.cyan}→ ${diagnostic.suggestion}${color.reset}`);
943
+ if (diagnostic2.suggestion) {
944
+ lines.push(` ${color.cyan}→ ${diagnostic2.suggestion}${color.reset}`);
942
945
  }
943
946
  }
944
947
  return lines.join("\n");
@@ -973,8 +976,303 @@ var noColors = {
973
976
  cyan: ""
974
977
  };
975
978
 
976
- // index.ts
979
+ // src/site_integrity.ts
977
980
  var PLUGIN_NAME2 = "diagnostics";
981
+ var DEFAULT_SEVERITY2 = {
982
+ "content-integrity:broken-link": "warning",
983
+ "content-integrity:unresolved-wikilink": "warning",
984
+ "content-integrity:broken-asset": "warning",
985
+ "content-integrity:duplicate-public-location": "error",
986
+ "content-integrity:redirect-target-missing": "warning",
987
+ "content-integrity:redirect-cycle": "error",
988
+ "content-integrity:redirect-public-location-conflict": "error"
989
+ };
990
+ function checkSiteIntegrity(manifest, options = {}) {
991
+ const diagnostics2 = [];
992
+ const publicSlugs = new Set(
993
+ manifest.publicEntries.map((entry) => entry.slug)
994
+ );
995
+ const routeOwners = buildRouteOwners(manifest, diagnostics2, options);
996
+ const publicRoutes = new Set(routeOwners.keys());
997
+ const assetPaths = buildAssetPaths(manifest);
998
+ for (const entry of manifest.publicEntries) {
999
+ checkResolvedLinks(entry, publicSlugs, assetPaths, diagnostics2, options);
1000
+ checkHtmlReferences(entry, publicRoutes, assetPaths, diagnostics2, options);
1001
+ }
1002
+ checkRedirectTargets(manifest, publicRoutes, diagnostics2, options);
1003
+ checkRedirectCycles(manifest, diagnostics2, options);
1004
+ return diagnostics2;
1005
+ }
1006
+ function buildRouteOwners(manifest, diagnostics2, options) {
1007
+ const owners = /* @__PURE__ */ new Map();
1008
+ for (const entry of manifest.publicEntries) {
1009
+ reserveRoute(owners, diagnostics2, options, {
1010
+ path: normalizeRoutePath(entry.publicLocation.permalink),
1011
+ slug: entry.slug,
1012
+ kind: "content"
1013
+ });
1014
+ }
1015
+ for (const [path, redirect] of manifest.publicRedirects) {
1016
+ reserveRoute(owners, diagnostics2, options, {
1017
+ path: normalizeRoutePath(path),
1018
+ slug: redirect.slug,
1019
+ kind: "redirect"
1020
+ });
1021
+ }
1022
+ const pagePaths = manifest.pagePaths ?? [];
1023
+ for (const path of pagePaths) {
1024
+ reserveRoute(owners, diagnostics2, options, {
1025
+ path: normalizeRoutePath(path),
1026
+ slug: "(plugin-page)",
1027
+ kind: "page"
1028
+ });
1029
+ }
1030
+ for (const output of manifest.generatedOutputs ?? []) {
1031
+ reserveRoute(owners, diagnostics2, options, {
1032
+ path: normalizeRoutePath(output.path),
1033
+ slug: "(generated-output)",
1034
+ kind: "output"
1035
+ });
1036
+ }
1037
+ return owners;
1038
+ }
1039
+ function reserveRoute(owners, diagnostics2, options, next) {
1040
+ const previous = owners.get(next.path);
1041
+ if (!previous) {
1042
+ owners.set(next.path, next);
1043
+ return;
1044
+ }
1045
+ if (previous.slug === next.slug && previous.kind === next.kind) return;
1046
+ const code = previous.kind === "redirect" || next.kind === "redirect" ? "content-integrity:redirect-public-location-conflict" : "content-integrity:duplicate-public-location";
1047
+ diagnostics2.push(
1048
+ diagnostic(options, {
1049
+ code,
1050
+ slug: next.slug.startsWith("(") ? void 0 : next.slug,
1051
+ target: next.path,
1052
+ message: `Public path "${next.path}" is claimed by both ${describeOwner(previous)} and ${describeOwner(next)}.`,
1053
+ suggestion: "Change one permalink, alias, redirect, page path, or generated output path.",
1054
+ meta: { previous, next }
1055
+ })
1056
+ );
1057
+ }
1058
+ function checkResolvedLinks(entry, publicSlugs, assetPaths, diagnostics2, options) {
1059
+ for (const link of entry.links) {
1060
+ if (link.kind === "unresolved") {
1061
+ diagnostics2.push(
1062
+ diagnostic(options, {
1063
+ code: "content-integrity:unresolved-wikilink",
1064
+ slug: entry.slug,
1065
+ filePath: `${entry.slug}.md`,
1066
+ target: link.raw,
1067
+ message: `WikiLink target "${link.raw}" could not be resolved.`,
1068
+ suggestion: "Create the target note/asset or update the WikiLink target.",
1069
+ meta: { raw: link.raw, embed: link.embed }
1070
+ })
1071
+ );
1072
+ continue;
1073
+ }
1074
+ if (link.kind === "note" && link.slug && !publicSlugs.has(link.slug)) {
1075
+ diagnostics2.push(
1076
+ diagnostic(options, {
1077
+ code: "content-integrity:broken-link",
1078
+ slug: entry.slug,
1079
+ filePath: `${entry.slug}.md`,
1080
+ target: link.slug,
1081
+ message: `Content link "${link.raw}" resolves to unpublished content "${link.slug}".`,
1082
+ suggestion: "Publish the target content or remove the reference from published content.",
1083
+ meta: {
1084
+ raw: link.raw,
1085
+ resolvedSlug: link.slug,
1086
+ reason: "unpublished-target"
1087
+ }
1088
+ })
1089
+ );
1090
+ }
1091
+ if ((link.kind === "image" || link.kind === "attachment") && link.slug && !assetPaths.has(link.slug)) {
1092
+ diagnostics2.push(
1093
+ diagnostic(options, {
1094
+ code: "content-integrity:broken-asset",
1095
+ slug: entry.slug,
1096
+ filePath: `${entry.slug}.md`,
1097
+ target: link.slug,
1098
+ message: `Asset reference "${link.raw}" resolves to "${link.slug}", but no asset is published for that path.`,
1099
+ suggestion: "Add the asset, fix the reference, or register the generated asset from a plugin.",
1100
+ meta: { raw: link.raw, resolvedPath: link.slug }
1101
+ })
1102
+ );
1103
+ }
1104
+ }
1105
+ }
1106
+ function checkHtmlReferences(entry, publicRoutes, assetPaths, diagnostics2, options) {
1107
+ for (const href of extractAttributes(entry.html, "href")) {
1108
+ if (!isSiteLocalReference(href)) continue;
1109
+ const path = normalizeReferencePath(href, entry.publicLocation.permalink);
1110
+ if (!path || publicRoutes.has(path)) continue;
1111
+ diagnostics2.push(
1112
+ diagnostic(options, {
1113
+ code: "content-integrity:broken-link",
1114
+ slug: entry.slug,
1115
+ filePath: entry.publicLocation.permalink,
1116
+ target: href,
1117
+ message: `Internal link target "${href}" does not match a public route.`,
1118
+ suggestion: "Update the link, create the target content, or register the generated route from a plugin.",
1119
+ meta: { normalizedTarget: path }
1120
+ })
1121
+ );
1122
+ }
1123
+ for (const src of extractAttributes(entry.html, "src")) {
1124
+ if (!isSiteLocalReference(src)) continue;
1125
+ const path = normalizeReferencePath(src, entry.publicLocation.permalink);
1126
+ const assetPath = path?.replace(/^\/+/, "");
1127
+ if (!path || publicRoutes.has(path) || assetPath && assetPaths.has(assetPath))
1128
+ continue;
1129
+ diagnostics2.push(
1130
+ diagnostic(options, {
1131
+ code: "content-integrity:broken-asset",
1132
+ slug: entry.slug,
1133
+ filePath: entry.publicLocation.permalink,
1134
+ target: src,
1135
+ message: `Local asset target "${src}" does not match a published asset or route.`,
1136
+ suggestion: "Add the asset, fix the reference, or register the generated asset from a plugin.",
1137
+ meta: { normalizedTarget: path }
1138
+ })
1139
+ );
1140
+ }
1141
+ }
1142
+ function checkRedirectTargets(manifest, publicRoutes, diagnostics2, options) {
1143
+ for (const [path, redirect] of manifest.publicRedirects) {
1144
+ const owner = manifest.bySlug.get(redirect.slug);
1145
+ const target = owner ? normalizeRoutePath(owner.publicLocation.permalink) : null;
1146
+ if (target && publicRoutes.has(target)) continue;
1147
+ diagnostics2.push(
1148
+ diagnostic(options, {
1149
+ code: "content-integrity:redirect-target-missing",
1150
+ slug: redirect.slug,
1151
+ target: path,
1152
+ message: `Redirect "${path}" points to content "${redirect.slug}", but its public target is not available.`,
1153
+ suggestion: "Publish the target content or remove the redirect.",
1154
+ meta: { redirect: path, targetSlug: redirect.slug, target }
1155
+ })
1156
+ );
1157
+ }
1158
+ }
1159
+ function checkRedirectCycles(manifest, diagnostics2, options) {
1160
+ const redirectTargets = /* @__PURE__ */ new Map();
1161
+ const reportedCycles = /* @__PURE__ */ new Set();
1162
+ for (const [path, redirect] of manifest.publicRedirects) {
1163
+ const owner = manifest.bySlug.get(redirect.slug);
1164
+ if (owner)
1165
+ redirectTargets.set(
1166
+ normalizeRoutePath(path),
1167
+ normalizeRoutePath(owner.publicLocation.permalink)
1168
+ );
1169
+ }
1170
+ for (const start of redirectTargets.keys()) {
1171
+ const seen = /* @__PURE__ */ new Map();
1172
+ const chain = [];
1173
+ let current = start;
1174
+ while (current) {
1175
+ const previousIndex = seen.get(current);
1176
+ if (previousIndex !== void 0) {
1177
+ const cycle = [...chain.slice(previousIndex), current];
1178
+ const key = canonicalCycleKey(cycle);
1179
+ if (reportedCycles.has(key)) break;
1180
+ reportedCycles.add(key);
1181
+ diagnostics2.push(
1182
+ diagnostic(options, {
1183
+ code: "content-integrity:redirect-cycle",
1184
+ target: cycle.join(" -> "),
1185
+ message: `Redirect cycle detected: ${cycle.join(" -> ")}.`,
1186
+ suggestion: "Remove or retarget one redirect in the cycle.",
1187
+ meta: { cycle }
1188
+ })
1189
+ );
1190
+ break;
1191
+ }
1192
+ seen.set(current, chain.length);
1193
+ chain.push(current);
1194
+ current = redirectTargets.get(current);
1195
+ }
1196
+ }
1197
+ }
1198
+ function buildAssetPaths(manifest) {
1199
+ const paths = /* @__PURE__ */ new Set();
1200
+ for (const entry of manifest.entries) {
1201
+ for (const asset of entry.assets) paths.add(asset.path);
1202
+ }
1203
+ for (const asset of manifest.assets ?? [])
1204
+ paths.add(asset.path.replace(/^\/+/, ""));
1205
+ for (const output of manifest.generatedOutputs ?? [])
1206
+ paths.add(output.path.replace(/^\/+/, ""));
1207
+ return paths;
1208
+ }
1209
+ function extractAttributes(html, attribute) {
1210
+ const pattern = new RegExp(
1211
+ `${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`,
1212
+ "gi"
1213
+ );
1214
+ const values = [];
1215
+ for (let match = pattern.exec(html); match; match = pattern.exec(html)) {
1216
+ const value = match[1] ?? match[2] ?? match[3];
1217
+ if (value) values.push(value);
1218
+ }
1219
+ return values;
1220
+ }
1221
+ function isSiteLocalReference(value) {
1222
+ if (!value || value.startsWith("#") || value.startsWith("//")) return false;
1223
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false;
1224
+ return true;
1225
+ }
1226
+ function normalizeReferencePath(value, basePath) {
1227
+ const path = value.split(/[?#]/)[0] ?? "";
1228
+ if (!path || path === "/") return path || null;
1229
+ try {
1230
+ return normalizeRoutePath(resolveReferencePath(decodeURI(path), basePath));
1231
+ } catch {
1232
+ return null;
1233
+ }
1234
+ }
1235
+ function resolveReferencePath(path, basePath) {
1236
+ if (path.startsWith("/")) return path;
1237
+ const baseSegments = basePath.split("/").filter(Boolean);
1238
+ if (!basePath.endsWith("/")) baseSegments.pop();
1239
+ const segments = [...baseSegments, ...path.split("/")];
1240
+ const normalized = [];
1241
+ for (const segment of segments) {
1242
+ if (!segment || segment === ".") continue;
1243
+ if (segment === "..") {
1244
+ normalized.pop();
1245
+ continue;
1246
+ }
1247
+ normalized.push(segment);
1248
+ }
1249
+ return `/${normalized.join("/")}`;
1250
+ }
1251
+ function normalizeRoutePath(value) {
1252
+ const path = `/${value.split(/[?#]/)[0]?.split("/").filter(Boolean).join("/") ?? ""}`;
1253
+ return path === "/" ? path : path.replace(/\/$/, "");
1254
+ }
1255
+ function describeOwner(owner) {
1256
+ return `${owner.kind} "${owner.slug}"`;
1257
+ }
1258
+ function canonicalCycleKey(cycle) {
1259
+ const closed = cycle.at(0) === cycle.at(-1) ? cycle.slice(0, -1) : [...cycle];
1260
+ const rotations = closed.map((_, index) => [
1261
+ ...closed.slice(index),
1262
+ ...closed.slice(0, index)
1263
+ ]);
1264
+ return rotations.map((rotation) => rotation.join("\0")).sort()[0] ?? "";
1265
+ }
1266
+ function diagnostic(options, input) {
1267
+ return {
1268
+ pluginName: PLUGIN_NAME2,
1269
+ severity: options.severity?.[input.code] ?? input.severity ?? DEFAULT_SEVERITY2[input.code] ?? "warning",
1270
+ ...input
1271
+ };
1272
+ }
1273
+
1274
+ // index.ts
1275
+ var PLUGIN_NAME3 = "diagnostics";
978
1276
  async function runDiagnostics(target, options = {}) {
979
1277
  const config = toAnalyzerConfig(target, options);
980
1278
  try {
@@ -985,24 +1283,27 @@ async function runDiagnostics(target, options = {}) {
985
1283
  }
986
1284
  function diagnostics(options = {}) {
987
1285
  return definePlugin({
988
- name: PLUGIN_NAME2,
1286
+ name: PLUGIN_NAME3,
989
1287
  options,
990
1288
  addDiagnostics: async (context) => {
991
1289
  const config = context.config;
992
1290
  if (!config) return [];
993
1291
  try {
994
- return await analyzeContent(
995
- toAnalyzerConfig(config, options),
996
- analyticsCoverageOptions(config, options)
997
- );
1292
+ return await analyzeContent(toAnalyzerConfig(config, options), {
1293
+ ...analyticsCoverageOptions(config, options),
1294
+ skipReferenceIntegrity: true
1295
+ });
998
1296
  } catch (error) {
999
1297
  return [createInternalError(error)];
1000
1298
  }
1001
1299
  },
1300
+ onManifestCreated: ({ manifest, diagnostics: diagnostics2 }) => {
1301
+ diagnostics2.push(...checkSiteIntegrity(manifest, options));
1302
+ },
1002
1303
  onBuildEnd: ({ manifest }) => {
1003
1304
  if (!options.failOnError) return void 0;
1004
1305
  const errors = manifest.diagnostics.filter(
1005
- (diagnostic) => diagnostic.pluginName === PLUGIN_NAME2 && diagnostic.severity === "error"
1306
+ (diagnostic2) => diagnostic2.pluginName === PLUGIN_NAME3 && diagnostic2.severity === "error"
1006
1307
  );
1007
1308
  if (errors.length > 0) {
1008
1309
  throw new DiagnosticsFailure(buildReport(errors));
@@ -1027,7 +1328,7 @@ function analyticsCoverageOptions(config, options) {
1027
1328
  function createInternalError(error) {
1028
1329
  const message = error instanceof Error ? error.message : String(error);
1029
1330
  return {
1030
- pluginName: PLUGIN_NAME2,
1331
+ pluginName: PLUGIN_NAME3,
1031
1332
  code: "internal-error",
1032
1333
  severity: "error",
1033
1334
  message: `content analysis failed: ${message}`
@@ -0,0 +1,5 @@
1
+ import type { ContentManifest, Diagnostic, DiagnosticCode, DiagnosticSeverity } from "@riebeckite/core";
2
+ export type SiteIntegrityOptions = {
3
+ readonly severity?: Partial<Record<DiagnosticCode, DiagnosticSeverity>>;
4
+ };
5
+ export declare function checkSiteIntegrity(manifest: ContentManifest, options?: SiteIntegrityOptions): Diagnostic[];
@@ -8,6 +8,7 @@ export type DiagnosticsOptions = {
8
8
  severity?: Partial<Record<DiagnosticCode, DiagnosticSeverity>>;
9
9
  exclude?: string[];
10
10
  publishStrategy?: "explicit" | "selective";
11
+ skipReferenceIntegrity?: boolean;
11
12
  };
12
13
  export type DiagnosticsSummary = {
13
14
  total: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-diagnostics",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "remark-parse": "^11.0.0",
21
21
  "unified": "^11.0.5",
22
22
  "unist-util-visit": "^5.1.0",
23
- "@riebeckite/core": "0.0.12"
23
+ "@riebeckite/core": "0.0.14"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/mdast": "^4.0.4",