@riebeckite/plugin-diagnostics 0.0.16 → 0.0.18

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
@@ -32,7 +32,7 @@ export default defineConfig({
32
32
 
33
33
  - `addDiagnostics` — runs the analysis at build time; results go to
34
34
  `manifest.diagnostics`
35
- - `onBuildEnd` — with `failOnError: true`, throws `DiagnosticsFailure` when
35
+ - `buildEnd` — with `failOnError: true`, throws `DiagnosticsFailure` when
36
36
  error-level diagnostics exist
37
37
 
38
38
  ## Checks
@@ -48,8 +48,8 @@ export default defineConfig({
48
48
  | `publish-conflict` | `warning` | `publish: true` combined with `draft: true` / `private: true` |
49
49
  | `duplicate-title` | `warning` | Multiple published notes share a title within the same language |
50
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 |
51
+ | `duplicate-content-id` | `error` | Published notes share a stable content ID (`id`) |
52
+ | `invalid-content-id` | `error` | `id` frontmatter violates the stable content ID contract |
53
53
  | `excluded-public` | `warning` | Excluded note is marked `publish: true` |
54
54
  | `publish-boundary` | `warning` | Published content links to or embeds non-published content |
55
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) |
@@ -159,5 +159,4 @@ assertNoErrors(report);
159
159
 
160
160
  ## See also
161
161
 
162
- - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
163
-
162
+ - [Plugin guide](../../../docs/docs/reference/plugin-api.en.md)
package/README_ja.md CHANGED
@@ -24,6 +24,8 @@ export default defineConfig({
24
24
  });
25
25
  ```
26
26
 
27
+ `failOnError: true` を指定すると、`buildEnd` で error レベルの Diagnostics が残っている場合に `DiagnosticsFailure` を送出します。
28
+
27
29
  ## 検出する問題
28
30
 
29
31
  | コード | 既定の重要度 | 内容 |
@@ -37,8 +39,8 @@ export default defineConfig({
37
39
  | `publish-conflict` | warning | `publish: true` と `draft: true` または `private: true` が同居している |
38
40
  | `duplicate-title` | warning | 同じ言語の公開ノート同士でタイトルが重複している |
39
41
  | `slug-collision` | error | 大文字・小文字を区別しない slug が衝突している |
40
- | `duplicate-content-id` | error | 公開ノート同士で安定コンテンツ ID(`id`/`uid`)が重複している |
41
- | `invalid-content-id` | error | `id`/`uid` の frontmatter が安定コンテンツ ID の要件を満たしていない |
42
+ | `duplicate-content-id` | error | 公開ノート同士で安定コンテンツ ID(`id`)が重複している |
43
+ | `invalid-content-id` | error | `id` の frontmatter が安定コンテンツ ID の要件を満たしていない |
42
44
  | `excluded-public` | warning | 除外されたノートに `publish: true` が指定されている |
43
45
  | `publish-boundary` | warning | 公開コンテンツから非公開コンテンツへリンク・埋め込みしている |
44
46
  | `analytics-untracked` | info | 安定 `id` のない公開ノートは analytics プラグインで計測されない(`reportAnalyticsCoverage` で有効化。設定で analytics プラグインが有効な場合は自動で有効化) |
@@ -115,5 +117,4 @@ assertNoErrors(report);
115
117
 
116
118
  ## 関連資料
117
119
 
118
- - [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
119
-
120
+ - [プラグインシステム](../../../docs/docs/reference/plugin-api.md)
package/dist/bin.js CHANGED
@@ -143,7 +143,7 @@ function checkAnalyticsCoverage(source, options, diagnostics) {
143
143
  }
144
144
  }
145
145
  function hasAnalyticsId(note) {
146
- return hasField(note.fm.values, "id") || hasField(note.fm.values, "uid");
146
+ return hasField(note.fm.values, "id");
147
147
  }
148
148
 
149
149
  // src/checks/assets.ts
@@ -180,7 +180,7 @@ function checkContentIdIntegrity(source, options, diagnostics) {
180
180
  "invalid-content-id",
181
181
  `invalid content id frontmatter: ${toErrorMessage(error)}`,
182
182
  void 0,
183
- 'set a unique, trimmed "id" value and remove a conflicting "uid" value'
183
+ 'set a unique, trimmed "id" value'
184
184
  );
185
185
  continue;
186
186
  }
@@ -200,7 +200,7 @@ function checkContentIdIntegrity(source, options, diagnostics) {
200
200
  "duplicate-content-id",
201
201
  `content id "${id}" is used by multiple published notes: ${slugs}`,
202
202
  id,
203
- 'give each note a unique "id" (or "uid") frontmatter value'
203
+ 'give each note a unique "id" frontmatter value'
204
204
  );
205
205
  }
206
206
  }
@@ -258,7 +258,11 @@ async function scanVault(config) {
258
258
  const note = {
259
259
  relativePath: relative,
260
260
  slug,
261
- language: resolveNoteLanguage(relative, config.languages),
261
+ language: resolveNoteLanguage(
262
+ relative,
263
+ fm.values.lang,
264
+ config.languages
265
+ ),
262
266
  markdown,
263
267
  fm,
264
268
  headings: extractHeadings(markdown),
@@ -417,11 +421,23 @@ function fragmentExists(note, fragment) {
417
421
  function normalizeMarkdown(markdown) {
418
422
  return markdown.replace(/\r\n/g, "\n");
419
423
  }
420
- function resolveNoteLanguage(relativePath, languages) {
424
+ function resolveNoteLanguage(relativePath, frontmatterLang, languages) {
421
425
  if (!languages || languages.length === 0) return void 0;
422
- const segment = relativePath.split("/")[0]?.toLowerCase();
423
- if (!segment) return void 0;
424
- return languages.find((language) => language.toLowerCase() === segment);
426
+ if (typeof frontmatterLang === "string" && frontmatterLang.trim()) {
427
+ const value = frontmatterLang.trim().toLowerCase();
428
+ const match = languages.find(
429
+ (language) => language.toLowerCase() === value
430
+ );
431
+ if (match) return match;
432
+ }
433
+ const filename = relativePath.split("/").at(-1)?.replace(/\.md$/i, "") ?? "";
434
+ const lower = filename.toLowerCase();
435
+ for (const language of languages) {
436
+ const tag = language.toLowerCase();
437
+ if (lower.endsWith(`.${tag}`) || lower.endsWith(`-${tag}`) || lower.endsWith(`_${tag}`))
438
+ return language;
439
+ }
440
+ return void 0;
425
441
  }
426
442
  function getExtension(filePath) {
427
443
  const lastSegment = filePath.split("/").pop() ?? filePath;
package/dist/index.js CHANGED
@@ -139,7 +139,7 @@ function checkAnalyticsCoverage(source, options, diagnostics2) {
139
139
  }
140
140
  }
141
141
  function hasAnalyticsId(note) {
142
- return hasField(note.fm.values, "id") || hasField(note.fm.values, "uid");
142
+ return hasField(note.fm.values, "id");
143
143
  }
144
144
 
145
145
  // src/checks/assets.ts
@@ -176,7 +176,7 @@ function checkContentIdIntegrity(source, options, diagnostics2) {
176
176
  "invalid-content-id",
177
177
  `invalid content id frontmatter: ${toErrorMessage(error)}`,
178
178
  void 0,
179
- 'set a unique, trimmed "id" value and remove a conflicting "uid" value'
179
+ 'set a unique, trimmed "id" value'
180
180
  );
181
181
  continue;
182
182
  }
@@ -196,7 +196,7 @@ function checkContentIdIntegrity(source, options, diagnostics2) {
196
196
  "duplicate-content-id",
197
197
  `content id "${id}" is used by multiple published notes: ${slugs}`,
198
198
  id,
199
- 'give each note a unique "id" (or "uid") frontmatter value'
199
+ 'give each note a unique "id" frontmatter value'
200
200
  );
201
201
  }
202
202
  }
@@ -254,7 +254,11 @@ async function scanVault(config) {
254
254
  const note = {
255
255
  relativePath: relative,
256
256
  slug,
257
- language: resolveNoteLanguage(relative, config.languages),
257
+ language: resolveNoteLanguage(
258
+ relative,
259
+ fm.values.lang,
260
+ config.languages
261
+ ),
258
262
  markdown,
259
263
  fm,
260
264
  headings: extractHeadings(markdown),
@@ -413,11 +417,23 @@ function fragmentExists(note, fragment) {
413
417
  function normalizeMarkdown(markdown) {
414
418
  return markdown.replace(/\r\n/g, "\n");
415
419
  }
416
- function resolveNoteLanguage(relativePath, languages) {
420
+ function resolveNoteLanguage(relativePath, frontmatterLang, languages) {
417
421
  if (!languages || languages.length === 0) return void 0;
418
- const segment = relativePath.split("/")[0]?.toLowerCase();
419
- if (!segment) return void 0;
420
- return languages.find((language) => language.toLowerCase() === segment);
422
+ if (typeof frontmatterLang === "string" && frontmatterLang.trim()) {
423
+ const value = frontmatterLang.trim().toLowerCase();
424
+ const match = languages.find(
425
+ (language) => language.toLowerCase() === value
426
+ );
427
+ if (match) return match;
428
+ }
429
+ const filename = relativePath.split("/").at(-1)?.replace(/\.md$/i, "") ?? "";
430
+ const lower = filename.toLowerCase();
431
+ for (const language of languages) {
432
+ const tag = language.toLowerCase();
433
+ if (lower.endsWith(`.${tag}`) || lower.endsWith(`-${tag}`) || lower.endsWith(`_${tag}`))
434
+ return language;
435
+ }
436
+ return void 0;
421
437
  }
422
438
  function getExtension(filePath) {
423
439
  const lastSegment = filePath.split("/").pop() ?? filePath;
@@ -991,6 +1007,7 @@ var PLUGIN_NAME2 = "diagnostics";
991
1007
  var DEFAULT_SEVERITY2 = {
992
1008
  "content-integrity:broken-link": "warning",
993
1009
  "content-integrity:unresolved-wikilink": "warning",
1010
+ "content-integrity:ambiguous-wikilink": "warning",
994
1011
  "content-integrity:broken-asset": "warning",
995
1012
  "content-integrity:duplicate-public-location": "error",
996
1013
  "content-integrity:redirect-target-missing": "warning",
@@ -1006,8 +1023,18 @@ function checkSiteIntegrity(manifest, options = {}) {
1006
1023
  const routeOwners = buildRouteOwners(manifest, diagnostics2, options);
1007
1024
  const publicRoutes = new Set(routeOwners.keys());
1008
1025
  const assetPaths = buildAssetPaths(manifest);
1026
+ const noteSlugs = new Set(manifest.entries.map((entry) => entry.slug));
1027
+ const ambiguities = manifest.contentIndexAmbiguities ?? /* @__PURE__ */ new Map();
1009
1028
  for (const entry of manifest.publicEntries) {
1010
- checkResolvedLinks(entry, publicSlugs, assetPaths, diagnostics2, options);
1029
+ checkResolvedLinks(
1030
+ entry,
1031
+ publicSlugs,
1032
+ assetPaths,
1033
+ noteSlugs,
1034
+ ambiguities,
1035
+ diagnostics2,
1036
+ options
1037
+ );
1011
1038
  checkHtmlReferences(entry, publicRoutes, assetPaths, diagnostics2, options);
1012
1039
  }
1013
1040
  checkRedirectTargets(manifest, diagnostics2, options);
@@ -1049,13 +1076,6 @@ function buildRouteOwners(manifest, diagnostics2, options) {
1049
1076
  });
1050
1077
  }
1051
1078
  }
1052
- for (const output of manifest.generatedOutputs ?? []) {
1053
- reserveRoute(owners, diagnostics2, options, {
1054
- path: normalizeRoutePath(output.path),
1055
- slug: "(generated-output)",
1056
- kind: "output"
1057
- });
1058
- }
1059
1079
  return owners;
1060
1080
  }
1061
1081
  function reserveRoute(owners, diagnostics2, options, next) {
@@ -1072,14 +1092,29 @@ function reserveRoute(owners, diagnostics2, options, next) {
1072
1092
  slug: next.slug.startsWith("(") ? void 0 : next.slug,
1073
1093
  target: next.path,
1074
1094
  message: `Public path "${next.path}" is claimed by both ${describeOwner(previous)} and ${describeOwner(next)}.`,
1075
- suggestion: "Change one permalink, alias, redirect, page path, or generated output path.",
1095
+ suggestion: "Change one permalink, alias, redirect, or page path.",
1076
1096
  meta: { previous, next }
1077
1097
  })
1078
1098
  );
1079
1099
  }
1080
- function checkResolvedLinks(entry, publicSlugs, assetPaths, diagnostics2, options) {
1100
+ function checkResolvedLinks(entry, publicSlugs, assetPaths, noteSlugs, ambiguities, diagnostics2, options) {
1081
1101
  for (const link of entry.links) {
1082
1102
  if (link.kind === "unresolved") {
1103
+ const ambiguity = ambiguities.get(link.raw.toLowerCase());
1104
+ if (ambiguity && ambiguity.length > 0) {
1105
+ diagnostics2.push(
1106
+ createAmbiguousWikilinkDiagnostic(
1107
+ entry,
1108
+ link.raw,
1109
+ link.embed,
1110
+ ambiguity,
1111
+ noteSlugs,
1112
+ publicSlugs,
1113
+ options
1114
+ )
1115
+ );
1116
+ continue;
1117
+ }
1083
1118
  diagnostics2.push(
1084
1119
  diagnostic(options, {
1085
1120
  code: "content-integrity:unresolved-wikilink",
@@ -1125,6 +1160,37 @@ function checkResolvedLinks(entry, publicSlugs, assetPaths, diagnostics2, option
1125
1160
  }
1126
1161
  }
1127
1162
  }
1163
+ function createAmbiguousWikilinkDiagnostic(entry, raw, embed, candidates, noteSlugs, publicSlugs, options) {
1164
+ const visible = [];
1165
+ let unpublishedCount = 0;
1166
+ for (const candidate of candidates) {
1167
+ if (!noteSlugs.has(candidate)) {
1168
+ visible.push(candidate);
1169
+ continue;
1170
+ }
1171
+ if (publicSlugs.has(candidate)) visible.push(candidate);
1172
+ else unpublishedCount += 1;
1173
+ }
1174
+ const described = [...visible];
1175
+ if (unpublishedCount > 0)
1176
+ described.push(`${unpublishedCount} unpublished candidate(s)`);
1177
+ const first = visible[0];
1178
+ const suggestion = first ? `Use an explicit path such as [[${first}]].` : "Rename one of the duplicate targets or reference it by a unique path.";
1179
+ return diagnostic(options, {
1180
+ code: "content-integrity:ambiguous-wikilink",
1181
+ slug: entry.slug,
1182
+ filePath: `${entry.slug}.md`,
1183
+ target: raw,
1184
+ message: `WikiLink target "${raw}" matches multiple candidates: ${described.join(", ")}.`,
1185
+ suggestion,
1186
+ meta: {
1187
+ raw,
1188
+ embed,
1189
+ candidates: visible,
1190
+ ...unpublishedCount > 0 ? { unpublishedCandidateCount: unpublishedCount } : {}
1191
+ }
1192
+ });
1193
+ }
1128
1194
  function checkHtmlReferences(entry, publicRoutes, assetPaths, diagnostics2, options) {
1129
1195
  for (const href of extractAttributes(entry.html, "href")) {
1130
1196
  if (!isSiteLocalReference(href)) continue;
@@ -1225,8 +1291,6 @@ function buildAssetPaths(manifest) {
1225
1291
  }
1226
1292
  for (const asset of manifest.assets ?? [])
1227
1293
  paths.add(asset.path.replace(/^\/+/, ""));
1228
- for (const output of manifest.generatedOutputs ?? [])
1229
- paths.add(output.path.replace(/^\/+/, ""));
1230
1294
  return paths;
1231
1295
  }
1232
1296
  function extractAttributes(html, attribute) {
@@ -1359,7 +1423,7 @@ function diagnostics(options = {}) {
1359
1423
  onManifestCreated: ({ manifest, diagnostics: diagnostics2 }) => {
1360
1424
  diagnostics2.push(...checkSiteIntegrity(manifest, options));
1361
1425
  },
1362
- onBuildEnd: ({ manifest }) => {
1426
+ buildEnd: ({ manifest }) => {
1363
1427
  if (!options.failOnError) return void 0;
1364
1428
  const errors = manifest.diagnostics.filter(
1365
1429
  (diagnostic2) => diagnostic2.pluginName === PLUGIN_NAME3 && diagnostic2.severity === "error"
@@ -5,8 +5,8 @@ import { type NormalizedOptions } from "./shared.js";
5
5
  * Reports published content that the analytics plugin will not track.
6
6
  *
7
7
  * The analytics browser initializer emits a page view only for entries that
8
- * carry Core's source-authored stable content ID (`id`, or the compatible
9
- * `uid`). Content without that ID builds and publishes normally but is silently
8
+ * carry Core's source-authored stable content ID (`id`). Content without that
9
+ * ID builds and publishes normally but is silently
10
10
  * absent from analytics. This check surfaces that gap as factual, read-only
11
11
  * `info` findings; the analytics plugin itself never autofixes content.
12
12
  */
@@ -5,9 +5,8 @@ import { type NormalizedOptions } from "./shared.js";
5
5
  * Checks stable content ID integrity.
6
6
  *
7
7
  * Stable content IDs come from the optional source-authored `id` frontmatter
8
- * field (`uid` is accepted for legacy content). They are the identity key for
9
- * per-content consumers such as analytics page views, so duplicates silently
10
- * merge per-content metrics and conflicting declarations break the build.
8
+ * field. They are the identity key for per-content consumers such as analytics
9
+ * page views, so duplicates silently merge per-content metrics.
11
10
  * This check is intentionally generic: it validates the content-model invariant
12
11
  * without knowing which plugin consumes the ID.
13
12
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-diagnostics",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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.16"
23
+ "@riebeckite/core": "0.0.18"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/mdast": "^4.0.4",