xdrs-core 0.18.0 → 0.20.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/.xdrs/_core/adrs/principles/001-xdrs-core.md +23 -15
- package/.xdrs/_core/adrs/principles/002-xdr-standards.md +1 -1
- package/.xdrs/_core/adrs/principles/003-skill-standards.md +3 -3
- package/.xdrs/_core/adrs/principles/004-article-standards.md +2 -2
- package/.xdrs/_core/adrs/principles/006-research-standards.md +2 -2
- package/.xdrs/_core/adrs/principles/007-plan-standards.md +2 -2
- package/.xdrs/_core/adrs/principles/skills/002-write-xdr/SKILL.md +7 -4
- package/.xdrs/_core/adrs/principles/skills/003-write-skill/SKILL.md +6 -3
- package/.xdrs/_core/adrs/principles/skills/004-write-article/SKILL.md +6 -3
- package/.xdrs/_core/adrs/principles/skills/005-write-research/SKILL.md +6 -3
- package/.xdrs/_core/adrs/principles/skills/006-write-plan/SKILL.md +11 -2
- package/.xdrs/_core/index.md +56 -0
- package/.xdrs/index.md +1 -2
- package/README.md +8 -8
- package/lib/lint.js +157 -55
- package/lib/lint.test.js +234 -23
- package/package.json +1 -1
package/lib/lint.js
CHANGED
|
@@ -22,7 +22,7 @@ const NUMBERED_FILE_RE = /^(\d{3,})-([a-z0-9-]+)\.md$/;
|
|
|
22
22
|
const NUMBERED_DIR_RE = /^(\d{3,})-([a-z0-9-]+)$/;
|
|
23
23
|
const REQUIRED_ROOT_INDEX_TEXT = 'XDRs in scopes listed last override the ones listed first';
|
|
24
24
|
const SUBJECT_ARTIFACT_DIRS = new Set(['skills', 'articles', 'researches', 'plans']);
|
|
25
|
-
const RESOURCE_DIR_NAME = 'assets';
|
|
25
|
+
const RESOURCE_DIR_NAME = '.assets';
|
|
26
26
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp']);
|
|
27
27
|
|
|
28
28
|
function runLintCli(args) {
|
|
@@ -34,7 +34,7 @@ function runLintCli(args) {
|
|
|
34
34
|
const all = args.includes('--all');
|
|
35
35
|
const pathArgs = args.filter((a) => !a.startsWith('--'));
|
|
36
36
|
const targetPath = pathArgs[0] || '.';
|
|
37
|
-
const result = lintWorkspace(targetPath, {
|
|
37
|
+
const result = lintWorkspace(targetPath, { ignoreExternal: !all });
|
|
38
38
|
|
|
39
39
|
if (result.errors.length === 0) {
|
|
40
40
|
console.log(`Lint passed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
@@ -53,12 +53,12 @@ function printHelp() {
|
|
|
53
53
|
console.log('Usage: xdrs-core lint [options] [path]\n');
|
|
54
54
|
console.log('Lint the XDR tree rooted at [path]/.xdrs or at [path] when [path] already points to .xdrs.');
|
|
55
55
|
console.log('\nOptions:');
|
|
56
|
-
console.log(' --all Check all files, including
|
|
56
|
+
console.log(' --all Check all files, including files from external scopes distributed via .filedist (default: skip external scopes)');
|
|
57
57
|
console.log('\nAll other commands continue to be delegated to the bundled filedist CLI.');
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function lintWorkspace(targetPath, options = {}) {
|
|
61
|
-
const {
|
|
61
|
+
const { ignoreExternal = true } = options;
|
|
62
62
|
const resolvedTarget = path.resolve(targetPath);
|
|
63
63
|
const xdrsRoot = path.basename(resolvedTarget) === '.xdrs'
|
|
64
64
|
? resolvedTarget
|
|
@@ -70,6 +70,10 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
70
70
|
return { xdrsRoot, errors };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
74
|
+
const filedistPaths = loadFiledist(repoRoot);
|
|
75
|
+
const externalScopes = getExternalScopes(filedistPaths, xdrsRoot);
|
|
76
|
+
|
|
73
77
|
const actualTypeIndexes = [];
|
|
74
78
|
const rootEntries = safeReadDir(xdrsRoot, errors, 'read XDR root directory');
|
|
75
79
|
const scopeEntries = rootEntries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
@@ -81,7 +85,7 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
for (const scopeEntry of scopeEntries) {
|
|
84
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes,
|
|
88
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, externalScopes);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
@@ -109,8 +113,8 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors) {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
const
|
|
113
|
-
const linkedSet = new Set(
|
|
116
|
+
const linkedScopeIndexes = links.filter((linkPath) => isScopeIndex(linkPath, xdrsRoot));
|
|
117
|
+
const linkedSet = new Set(linkedScopeIndexes.map(normalizePath));
|
|
114
118
|
const localScopePath = normalizePath(path.join(xdrsRoot, '_local'));
|
|
115
119
|
|
|
116
120
|
for (const linkPath of links) {
|
|
@@ -119,21 +123,46 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors) {
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
// Collect non-_local scopes that have type indexes and check their scope index is linked
|
|
127
|
+
const scopesWithTypeIndexes = new Set();
|
|
122
128
|
for (const indexPath of actualTypeIndexes) {
|
|
123
129
|
const scopeName = path.basename(path.dirname(path.dirname(indexPath)));
|
|
124
|
-
if (scopeName
|
|
125
|
-
|
|
130
|
+
if (scopeName !== '_local') {
|
|
131
|
+
scopesWithTypeIndexes.add(scopeName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const scopeName of scopesWithTypeIndexes) {
|
|
136
|
+
const scopeIndexPath = normalizePath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
137
|
+
if (!linkedSet.has(scopeIndexPath)) {
|
|
138
|
+
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors) {
|
|
144
|
+
const content = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
145
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
146
|
+
const links = parseLocalLinks(content, path.dirname(scopeIndexPath), repoRoot);
|
|
147
|
+
const linkedSet = new Set(links.map(normalizePath));
|
|
148
|
+
|
|
149
|
+
for (const linkPath of links) {
|
|
150
|
+
if (!fs.existsSync(linkPath)) {
|
|
151
|
+
errors.push(`Broken link in scope index: ${displayPath(scopeIndexPath, linkPath)}`);
|
|
126
152
|
}
|
|
127
|
-
|
|
128
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const typeIndexPath of typeIndexesInScope) {
|
|
156
|
+
if (!linkedSet.has(normalizePath(typeIndexPath))) {
|
|
157
|
+
errors.push(`Scope index ${toDisplayPath(scopeIndexPath)} is missing link to type index: ${toDisplayPath(typeIndexPath)}`);
|
|
129
158
|
}
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
161
|
|
|
133
|
-
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes,
|
|
162
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes) {
|
|
134
163
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
135
164
|
|
|
136
|
-
if (
|
|
165
|
+
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
137
166
|
return;
|
|
138
167
|
}
|
|
139
168
|
|
|
@@ -141,6 +170,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
141
170
|
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)}`);
|
|
142
171
|
}
|
|
143
172
|
|
|
173
|
+
const typeIndexesInScope = [];
|
|
144
174
|
const entries = safeReadDir(scopePath, errors, `read scope directory ${scopeName}`);
|
|
145
175
|
for (const entry of entries) {
|
|
146
176
|
const entryPath = path.join(scopePath, entry.name);
|
|
@@ -149,15 +179,30 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
149
179
|
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
150
180
|
continue;
|
|
151
181
|
}
|
|
152
|
-
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes
|
|
182
|
+
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes);
|
|
183
|
+
const typeIndexPath = path.join(entryPath, 'index.md');
|
|
184
|
+
if (existsFile(typeIndexPath)) {
|
|
185
|
+
typeIndexesInScope.push(typeIndexPath);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (entry.name === 'index.md') {
|
|
153
191
|
continue;
|
|
154
192
|
}
|
|
155
193
|
|
|
156
194
|
errors.push(`Unexpected file under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
157
195
|
}
|
|
196
|
+
|
|
197
|
+
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
198
|
+
if (!existsFile(scopeIndexPath)) {
|
|
199
|
+
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)}`);
|
|
200
|
+
} else {
|
|
201
|
+
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors);
|
|
202
|
+
}
|
|
158
203
|
}
|
|
159
204
|
|
|
160
|
-
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes
|
|
205
|
+
function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeIndexes) {
|
|
161
206
|
const typePath = path.join(xdrsRoot, scopeName, typeName);
|
|
162
207
|
const indexPath = path.join(typePath, 'index.md');
|
|
163
208
|
const xdrNumbers = new Map();
|
|
@@ -165,7 +210,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
165
210
|
|
|
166
211
|
if (!existsFile(indexPath)) {
|
|
167
212
|
errors.push(`Missing canonical index: ${toDisplayPath(indexPath)}`);
|
|
168
|
-
} else
|
|
213
|
+
} else {
|
|
169
214
|
actualTypeIndexes.push(indexPath);
|
|
170
215
|
}
|
|
171
216
|
|
|
@@ -184,15 +229,15 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
184
229
|
continue;
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrNumbers, errors
|
|
232
|
+
artifacts.push(...lintSubjectDirectory(xdrsRoot, scopeName, typeName, entry.name, xdrNumbers, errors));
|
|
188
233
|
}
|
|
189
234
|
|
|
190
|
-
if (existsFile(indexPath)
|
|
235
|
+
if (existsFile(indexPath)) {
|
|
191
236
|
lintTypeIndex(indexPath, xdrsRoot, artifacts, errors);
|
|
192
237
|
}
|
|
193
238
|
}
|
|
194
239
|
|
|
195
|
-
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNumbers, errors
|
|
240
|
+
function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNumbers, errors) {
|
|
196
241
|
const subjectPath = path.join(xdrsRoot, scopeName, typeName, subjectName);
|
|
197
242
|
const artifacts = [];
|
|
198
243
|
const entries = safeReadDir(subjectPath, errors, `read subject directory ${scopeName}/${typeName}/${subjectName}`);
|
|
@@ -205,19 +250,19 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
|
|
|
205
250
|
continue;
|
|
206
251
|
}
|
|
207
252
|
if (entry.name === 'skills') {
|
|
208
|
-
artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
253
|
+
artifacts.push(...lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
209
254
|
continue;
|
|
210
255
|
}
|
|
211
256
|
if (entry.name === 'articles') {
|
|
212
|
-
artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
257
|
+
artifacts.push(...lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
213
258
|
continue;
|
|
214
259
|
}
|
|
215
260
|
if (entry.name === 'researches') {
|
|
216
|
-
artifacts.push(...lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
261
|
+
artifacts.push(...lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
217
262
|
continue;
|
|
218
263
|
}
|
|
219
264
|
if (entry.name === 'plans') {
|
|
220
|
-
artifacts.push(...lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors
|
|
265
|
+
artifacts.push(...lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, entryPath, errors));
|
|
221
266
|
continue;
|
|
222
267
|
}
|
|
223
268
|
|
|
@@ -230,14 +275,14 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrNum
|
|
|
230
275
|
continue;
|
|
231
276
|
}
|
|
232
277
|
|
|
233
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
278
|
artifacts.push(entryPath);
|
|
238
279
|
lintXdrFile(xdrsRoot, scopeName, typeName, entryPath, xdrNumbers, errors);
|
|
239
280
|
}
|
|
240
281
|
|
|
282
|
+
const subjectAssetsDir = path.join(subjectPath, RESOURCE_DIR_NAME);
|
|
283
|
+
const xdrDocsInSubject = artifacts.filter((p) => path.dirname(p) === subjectPath);
|
|
284
|
+
lintOrphanAssets(subjectAssetsDir, xdrDocsInSubject, xdrsRoot, errors);
|
|
285
|
+
|
|
241
286
|
return artifacts;
|
|
242
287
|
}
|
|
243
288
|
|
|
@@ -327,7 +372,7 @@ function lintXdrFrontmatter(content, expectedName, filePath, errors) {
|
|
|
327
372
|
}
|
|
328
373
|
}
|
|
329
374
|
|
|
330
|
-
function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors
|
|
375
|
+
function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsPath, errors) {
|
|
331
376
|
const artifacts = [];
|
|
332
377
|
const skillNumbers = new Map();
|
|
333
378
|
const entries = safeReadDir(skillsPath, errors, `read skills directory ${scopeName}/${typeName}/${subjectName}/skills`);
|
|
@@ -364,10 +409,6 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
364
409
|
continue;
|
|
365
410
|
}
|
|
366
411
|
|
|
367
|
-
if (shouldSkipReadOnlyPath(skillFilePath, ignoreReadOnly)) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
412
|
artifacts.push(skillFilePath);
|
|
372
413
|
|
|
373
414
|
const skillContent = fs.readFileSync(skillFilePath, 'utf8');
|
|
@@ -386,12 +427,13 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
386
427
|
}
|
|
387
428
|
|
|
388
429
|
lintDocumentLinks(skillFilePath, xdrsRoot, scopeName, errors);
|
|
430
|
+
lintOrphanAssets(path.join(entryPath, RESOURCE_DIR_NAME), [skillFilePath], xdrsRoot, errors);
|
|
389
431
|
}
|
|
390
432
|
|
|
391
433
|
return artifacts;
|
|
392
434
|
}
|
|
393
435
|
|
|
394
|
-
function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, articlesPath, errors
|
|
436
|
+
function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, articlesPath, errors) {
|
|
395
437
|
const artifacts = [];
|
|
396
438
|
const articleNumbers = new Map();
|
|
397
439
|
const entries = safeReadDir(articlesPath, errors, `read articles directory ${scopeName}/${typeName}/${subjectName}/articles`);
|
|
@@ -413,10 +455,6 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
413
455
|
continue;
|
|
414
456
|
}
|
|
415
457
|
|
|
416
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
458
|
artifacts.push(entryPath);
|
|
421
459
|
|
|
422
460
|
const number = match[1];
|
|
@@ -441,10 +479,12 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
441
479
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
442
480
|
}
|
|
443
481
|
|
|
482
|
+
lintOrphanAssets(path.join(articlesPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
483
|
+
|
|
444
484
|
return artifacts;
|
|
445
485
|
}
|
|
446
486
|
|
|
447
|
-
function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, researchPath, errors
|
|
487
|
+
function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, researchPath, errors) {
|
|
448
488
|
const artifacts = [];
|
|
449
489
|
const researchNumbers = new Map();
|
|
450
490
|
const entries = safeReadDir(researchPath, errors, `read research directory ${scopeName}/${typeName}/${subjectName}/researches`);
|
|
@@ -466,10 +506,6 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
466
506
|
continue;
|
|
467
507
|
}
|
|
468
508
|
|
|
469
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
509
|
artifacts.push(entryPath);
|
|
474
510
|
|
|
475
511
|
const number = match[1];
|
|
@@ -494,10 +530,12 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
494
530
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
495
531
|
}
|
|
496
532
|
|
|
533
|
+
lintOrphanAssets(path.join(researchPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
534
|
+
|
|
497
535
|
return artifacts;
|
|
498
536
|
}
|
|
499
537
|
|
|
500
|
-
function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPath, errors
|
|
538
|
+
function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPath, errors) {
|
|
501
539
|
const artifacts = [];
|
|
502
540
|
const planNumbers = new Map();
|
|
503
541
|
const entries = safeReadDir(plansPath, errors, `read plans directory ${scopeName}/${typeName}/${subjectName}/plans`);
|
|
@@ -519,10 +557,6 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
519
557
|
continue;
|
|
520
558
|
}
|
|
521
559
|
|
|
522
|
-
if (shouldSkipReadOnlyPath(entryPath, ignoreReadOnly)) {
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
560
|
artifacts.push(entryPath);
|
|
527
561
|
|
|
528
562
|
const number = match[1];
|
|
@@ -548,6 +582,8 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
548
582
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors);
|
|
549
583
|
}
|
|
550
584
|
|
|
585
|
+
lintOrphanAssets(path.join(plansPath, RESOURCE_DIR_NAME), artifacts, xdrsRoot, errors);
|
|
586
|
+
|
|
551
587
|
return artifacts;
|
|
552
588
|
}
|
|
553
589
|
|
|
@@ -599,6 +635,46 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors) {
|
|
|
599
635
|
}
|
|
600
636
|
}
|
|
601
637
|
|
|
638
|
+
function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
|
|
639
|
+
if (!existsDirectory(assetsDir)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const entries = safeReadDir(assetsDir, errors, `read assets directory ${toDisplayPath(assetsDir)}`);
|
|
644
|
+
if (entries.length === 0) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const assetFiles = entries
|
|
649
|
+
.filter((entry) => entry.isFile())
|
|
650
|
+
.map((entry) => normalizePath(path.join(assetsDir, entry.name)));
|
|
651
|
+
|
|
652
|
+
if (assetFiles.length === 0) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const repoRoot = path.dirname(xdrsRoot);
|
|
657
|
+
const referencedAssets = new Set();
|
|
658
|
+
|
|
659
|
+
for (const docPath of documentPaths) {
|
|
660
|
+
if (!existsFile(docPath)) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const content = fs.readFileSync(docPath, 'utf8');
|
|
664
|
+
const docDir = path.dirname(docPath);
|
|
665
|
+
const links = parseLocalLinks(content, docDir, repoRoot);
|
|
666
|
+
for (const linkPath of links) {
|
|
667
|
+
referencedAssets.add(normalizePath(linkPath));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const assetPath of assetFiles) {
|
|
672
|
+
if (!referencedAssets.has(assetPath)) {
|
|
673
|
+
errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
602
678
|
function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors) {
|
|
603
679
|
const lines = fs.readFileSync(documentPath, 'utf8').split(/\r?\n/);
|
|
604
680
|
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
@@ -693,6 +769,11 @@ function isCanonicalTypeIndex(filePath, xdrsRoot) {
|
|
|
693
769
|
return relative.length === 3 && TYPE_NAMES.has(relative[1]) && relative[2] === 'index.md';
|
|
694
770
|
}
|
|
695
771
|
|
|
772
|
+
function isScopeIndex(filePath, xdrsRoot) {
|
|
773
|
+
const relative = relativeFrom(xdrsRoot, filePath).split(path.sep);
|
|
774
|
+
return relative.length === 2 && relative[1] === 'index.md';
|
|
775
|
+
}
|
|
776
|
+
|
|
696
777
|
function shouldValidateResourceLink(rawTarget) {
|
|
697
778
|
const normalizedTargetPath = normalizeLocalLinkTarget(rawTarget);
|
|
698
779
|
if (!normalizedTargetPath) {
|
|
@@ -857,17 +938,38 @@ function existsFile(filePath) {
|
|
|
857
938
|
}
|
|
858
939
|
}
|
|
859
940
|
|
|
860
|
-
function
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return
|
|
864
|
-
} catch {
|
|
865
|
-
return true;
|
|
941
|
+
function loadFiledist(repoRoot) {
|
|
942
|
+
const filedistPath = path.join(repoRoot, '.filedist');
|
|
943
|
+
if (!existsFile(filedistPath)) {
|
|
944
|
+
return new Set();
|
|
866
945
|
}
|
|
946
|
+
const content = fs.readFileSync(filedistPath, 'utf8');
|
|
947
|
+
const paths = new Set();
|
|
948
|
+
for (const line of content.split(/\r?\n/)) {
|
|
949
|
+
const trimmed = line.trim();
|
|
950
|
+
if (!trimmed) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const parts = trimmed.split('|');
|
|
954
|
+
if (parts.length >= 2) {
|
|
955
|
+
paths.add(normalizePath(path.join(repoRoot, parts[0])));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return paths;
|
|
867
959
|
}
|
|
868
960
|
|
|
869
|
-
function
|
|
870
|
-
|
|
961
|
+
function getExternalScopes(filedistPaths, xdrsRoot) {
|
|
962
|
+
const externalScopes = new Set();
|
|
963
|
+
for (const filePath of filedistPaths) {
|
|
964
|
+
const relative = path.relative(xdrsRoot, filePath);
|
|
965
|
+
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
966
|
+
const parts = relative.split(path.sep);
|
|
967
|
+
if (parts.length >= 1 && parts[0]) {
|
|
968
|
+
externalScopes.add(parts[0]);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return externalScopes;
|
|
871
973
|
}
|
|
872
974
|
|
|
873
975
|
function displayPath(indexPath, targetPath) {
|