xdrs-core 0.37.2 → 0.38.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/011-core-scope-type.md +8 -0
- package/lib/lint.js +263 -126
- package/lib/lint.test.js +269 -4
- package/package.json +1 -1
package/lib/lint.js
CHANGED
|
@@ -37,6 +37,26 @@ const ARTICLE_MAX_WORDS = 8000;
|
|
|
37
37
|
const RESEARCH_MAX_WORDS = 5000;
|
|
38
38
|
const SKILL_MAX_WORDS = 6500;
|
|
39
39
|
|
|
40
|
+
const NORMATIVE_KEYWORDS_RE = /\bMUST NOT\b|\bMUST\b|\bSHOULD NOT\b|\bSHOULD\b|\bMAY\b|\bREQUIRED\b|\bOPTIONAL\b/;
|
|
41
|
+
|
|
42
|
+
const RESEARCH_SECTION_LIMITS = {
|
|
43
|
+
'## Abstract': 200,
|
|
44
|
+
'## Introduction': 700,
|
|
45
|
+
'## Methods': 1200,
|
|
46
|
+
'## Results': 1800,
|
|
47
|
+
'## Discussion': 1000,
|
|
48
|
+
'## Conclusion': 400,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const DOC_TYPE_POLICY_REF = {
|
|
52
|
+
Policy: '[_core-adr-policy-002]',
|
|
53
|
+
Skill: '[_core-adr-policy-003]',
|
|
54
|
+
Article: '[_core-adr-policy-004]',
|
|
55
|
+
Research: '[_core-adr-policy-006]',
|
|
56
|
+
Plan: '[_core-adr-policy-007]',
|
|
57
|
+
Slide: '[_core-adr-policy-009]',
|
|
58
|
+
};
|
|
59
|
+
|
|
40
60
|
function runLintCli(args) {
|
|
41
61
|
if (args.includes('--help') || args.includes('-h')) {
|
|
42
62
|
printHelp();
|
|
@@ -50,6 +70,11 @@ function runLintCli(args) {
|
|
|
50
70
|
|
|
51
71
|
if (result.errors.length === 0) {
|
|
52
72
|
console.log(`Lint passed for ${toDisplayPath(result.xdrsRoot)}`);
|
|
73
|
+
if (result.readOnlyScopes.size > 0) {
|
|
74
|
+
for (const scopeName of result.readOnlyScopes) {
|
|
75
|
+
console.log(`- READ-ONLY scope "${scopeName}": one or more follows -core scopes are not present in this workspace; no changes must be made to this scope [_core-adr-policy-011.08-unavailable-core-read-only]`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
53
78
|
return 0;
|
|
54
79
|
}
|
|
55
80
|
|
|
@@ -84,10 +109,11 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
84
109
|
const resolvedTarget = path.resolve(targetPath);
|
|
85
110
|
const xdrsRoot = resolveXdrsRoot(resolvedTarget);
|
|
86
111
|
const errors = [];
|
|
112
|
+
const readOnlyScopes = new Set();
|
|
87
113
|
|
|
88
114
|
if (!existsDirectory(xdrsRoot)) {
|
|
89
|
-
errors.push(`Missing XDRS root directory: ${toDisplayPath(xdrsRoot)}`);
|
|
90
|
-
return { xdrsRoot, errors };
|
|
115
|
+
errors.push(`Missing XDRS root directory: ${toDisplayPath(xdrsRoot)} [_core-adr-policy-001]`);
|
|
116
|
+
return { xdrsRoot, errors, readOnlyScopes };
|
|
91
117
|
}
|
|
92
118
|
|
|
93
119
|
const repoRoot = path.dirname(xdrsRoot);
|
|
@@ -126,22 +152,22 @@ function lintWorkspace(targetPath, options = {}) {
|
|
|
126
152
|
|
|
127
153
|
for (const entry of rootEntries) {
|
|
128
154
|
if (entry.isFile() && entry.name !== 'index.md') {
|
|
129
|
-
errors.push(`Unexpected file at .xdrs root: ${entry.name}`);
|
|
155
|
+
errors.push(`Unexpected file at .xdrs root: ${entry.name} [_core-adr-policy-001]`);
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
for (const scopeEntry of scopeEntries) {
|
|
134
|
-
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes);
|
|
160
|
+
lintScopeDirectory(xdrsRoot, scopeEntry.name, errors, actualTypeIndexes, ignoreExternal, effectiveExternalScopes, knownScopeTypes, readOnlyScopes);
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
const rootIndexPath = path.join(xdrsRoot, 'index.md');
|
|
138
164
|
if (!existsFile(rootIndexPath)) {
|
|
139
|
-
errors.push('Missing required root index: .xdrs/index.md');
|
|
165
|
+
errors.push('Missing required root index: .xdrs/index.md [_core-adr-policy-001]');
|
|
140
166
|
} else {
|
|
141
167
|
lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, effectiveExternalScopes);
|
|
142
168
|
}
|
|
143
169
|
|
|
144
|
-
return { xdrsRoot, errors };
|
|
170
|
+
return { xdrsRoot, errors, readOnlyScopes };
|
|
145
171
|
}
|
|
146
172
|
|
|
147
173
|
function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, externalScopes = new Set()) {
|
|
@@ -149,14 +175,17 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
149
175
|
const repoRoot = path.dirname(xdrsRoot);
|
|
150
176
|
|
|
151
177
|
if (!content.includes(REQUIRED_ROOT_INDEX_TEXT)) {
|
|
152
|
-
errors.push(`Root index is missing required override text: ${toDisplayPath(rootIndexPath)}`);
|
|
178
|
+
errors.push(`Root index is missing required override text: ${toDisplayPath(rootIndexPath)} [_core-adr-policy-001]`);
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
const links = parseLocalLinks(content, path.dirname(rootIndexPath), repoRoot);
|
|
156
182
|
for (const linkPath of links) {
|
|
157
183
|
if (!fs.existsSync(linkPath)) {
|
|
158
184
|
if (isExternalScopeLink(linkPath, xdrsRoot, externalScopes)) continue;
|
|
159
|
-
errors.push(`Broken link in root index: ${displayPath(rootIndexPath, linkPath)}`);
|
|
185
|
+
errors.push(`Broken link in root index: ${displayPath(rootIndexPath, linkPath)} [_core-adr-policy-001]`);
|
|
186
|
+
}
|
|
187
|
+
if (isCanonicalTypeIndex(linkPath, xdrsRoot)) {
|
|
188
|
+
errors.push(`Root index must not link directly to type indexes: ${displayPath(rootIndexPath, linkPath)} [_core-adr-policy-001]`);
|
|
160
189
|
}
|
|
161
190
|
}
|
|
162
191
|
|
|
@@ -166,7 +195,7 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
166
195
|
|
|
167
196
|
for (const linkPath of links) {
|
|
168
197
|
if (isPathInside(localScopePath, linkPath) || normalizePath(linkPath) === localScopePath) {
|
|
169
|
-
errors.push(`Root index must not link into _local scope: ${displayPath(rootIndexPath, linkPath)}`);
|
|
198
|
+
errors.push(`Root index must not link into _local scope: ${displayPath(rootIndexPath, linkPath)} [_core-adr-policy-001]`);
|
|
170
199
|
}
|
|
171
200
|
}
|
|
172
201
|
|
|
@@ -182,7 +211,7 @@ function lintRootIndex(rootIndexPath, xdrsRoot, actualTypeIndexes, errors, exter
|
|
|
182
211
|
for (const scopeName of scopesWithTypeIndexes) {
|
|
183
212
|
const scopeIndexPath = normalizePath(path.join(xdrsRoot, scopeName, 'index.md'));
|
|
184
213
|
if (!linkedSet.has(scopeIndexPath)) {
|
|
185
|
-
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))}`);
|
|
214
|
+
errors.push(`Root index is missing scope index link: ${toDisplayPath(path.join(xdrsRoot, scopeName, 'index.md'))} [_core-adr-policy-001]`);
|
|
186
215
|
}
|
|
187
216
|
}
|
|
188
217
|
}
|
|
@@ -196,13 +225,13 @@ function lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope,
|
|
|
196
225
|
for (const linkPath of links) {
|
|
197
226
|
if (!fs.existsSync(linkPath)) {
|
|
198
227
|
if (isExternalScopeLink(linkPath, xdrsRoot, externalScopes)) continue;
|
|
199
|
-
errors.push(`Broken link in scope index: ${displayPath(scopeIndexPath, linkPath)}`);
|
|
228
|
+
errors.push(`Broken link in scope index: ${displayPath(scopeIndexPath, linkPath)} [_core-adr-policy-001]`);
|
|
200
229
|
}
|
|
201
230
|
}
|
|
202
231
|
|
|
203
232
|
for (const typeIndexPath of typeIndexesInScope) {
|
|
204
233
|
if (!linkedSet.has(normalizePath(typeIndexPath))) {
|
|
205
|
-
errors.push(`Scope index ${toDisplayPath(scopeIndexPath)} is missing link to type index: ${toDisplayPath(typeIndexPath)}`);
|
|
234
|
+
errors.push(`Scope index ${toDisplayPath(scopeIndexPath)} is missing link to type index: ${toDisplayPath(typeIndexPath)} [_core-adr-policy-001]`);
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
}
|
|
@@ -223,11 +252,11 @@ function extractParentScopeType(policyFilePath) {
|
|
|
223
252
|
return bodyMatch ? bodyMatch[1] : null;
|
|
224
253
|
}
|
|
225
254
|
|
|
226
|
-
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map()) {
|
|
255
|
+
function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes = new Map(), readOnlyScopes = new Set()) {
|
|
227
256
|
const content = fs.readFileSync(scopeIndexPath, 'utf8');
|
|
228
257
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
229
258
|
if (!fmMatch) {
|
|
230
|
-
errors.push(`Scope index must start with a YAML frontmatter block: ${toDisplayPath(scopeIndexPath)}`);
|
|
259
|
+
errors.push(`Scope index must start with a YAML frontmatter block: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
231
260
|
return;
|
|
232
261
|
}
|
|
233
262
|
const block = fmMatch[1];
|
|
@@ -235,22 +264,22 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
235
264
|
|
|
236
265
|
for (const key of fm.topLevelKeys) {
|
|
237
266
|
if (!SCOPE_INDEX_ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
238
|
-
errors.push(`Scope index frontmatter has unknown field "${key}": ${toDisplayPath(scopeIndexPath)}`);
|
|
267
|
+
errors.push(`Scope index frontmatter has unknown field "${key}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
239
268
|
}
|
|
240
269
|
}
|
|
241
270
|
|
|
242
271
|
const scopeTypeMatch = block.match(/^scope-type:\s*(.+)$/m);
|
|
243
272
|
const scopeType = scopeTypeMatch ? scopeTypeMatch[1].trim() : null;
|
|
244
273
|
if (!scopeType) {
|
|
245
|
-
errors.push(`Scope index frontmatter must include a scope-type field: ${toDisplayPath(scopeIndexPath)}`);
|
|
274
|
+
errors.push(`Scope index frontmatter must include a scope-type field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.08-def-valid-iff-policy-exists]`);
|
|
246
275
|
return;
|
|
247
276
|
}
|
|
248
277
|
if (scopeType.startsWith('_') && scopeType !== '_local') {
|
|
249
|
-
errors.push(`Scope type "${scopeType}" uses a reserved "_" prefix; only "_local" is a valid underscore-prefixed scope type: ${toDisplayPath(scopeIndexPath)}`);
|
|
278
|
+
errors.push(`Scope type "${scopeType}" uses a reserved "_" prefix; only "_local" is a valid underscore-prefixed scope type: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.06-def-no-underscore-prefix]`);
|
|
250
279
|
return;
|
|
251
280
|
}
|
|
252
281
|
if (!knownScopeTypes.has(scopeType) && scopeType !== '_local') {
|
|
253
|
-
errors.push(`Scope index scope-type "${scopeType}" has no corresponding ${scopeType}-scope-type policy in the principles of any scope: ${toDisplayPath(scopeIndexPath)}`);
|
|
282
|
+
errors.push(`Scope index scope-type "${scopeType}" has no corresponding ${scopeType}-scope-type policy in the principles of any scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.08-def-valid-iff-policy-exists]`);
|
|
254
283
|
return;
|
|
255
284
|
}
|
|
256
285
|
// Validate inheritance chain: ensure all ancestors exist in knownScopeTypes
|
|
@@ -263,11 +292,11 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
263
292
|
const parentType = extractParentScopeType(policyPath);
|
|
264
293
|
if (!parentType) break;
|
|
265
294
|
if (visited.has(parentType)) {
|
|
266
|
-
errors.push(`Scope type "${currentType}" declares parent "${parentType}" which creates a cycle in the scope-type inheritance chain: ${toDisplayPath(scopeIndexPath)}`);
|
|
295
|
+
errors.push(`Scope type "${currentType}" declares parent "${parentType}" which creates a cycle in the scope-type inheritance chain: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.07-def-parent-scope-type]`);
|
|
267
296
|
break;
|
|
268
297
|
}
|
|
269
298
|
if (!knownScopeTypes.has(parentType)) {
|
|
270
|
-
errors.push(`Scope type "${currentType}" declares parent "${parentType}" but no ${parentType}-scope-type policy exists in any scope: ${toDisplayPath(scopeIndexPath)}`);
|
|
299
|
+
errors.push(`Scope type "${currentType}" declares parent "${parentType}" but no ${parentType}-scope-type policy exists in any scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-010.07-def-parent-scope-type]`);
|
|
271
300
|
break;
|
|
272
301
|
}
|
|
273
302
|
visited.add(parentType);
|
|
@@ -275,51 +304,51 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
275
304
|
}
|
|
276
305
|
}
|
|
277
306
|
if (scopeName === '_core' && scopeType !== 'core') {
|
|
278
|
-
errors.push(`Scope "_core" must have scope-type "core": ${toDisplayPath(scopeIndexPath)}`);
|
|
307
|
+
errors.push(`Scope "_core" must have scope-type "core": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-011.01-scope-type-name]`);
|
|
279
308
|
}
|
|
280
309
|
if (scopeName === '_local' && scopeType !== '_local') {
|
|
281
|
-
errors.push(`Scope "_local" must have scope-type "_local": ${toDisplayPath(scopeIndexPath)}`);
|
|
310
|
+
errors.push(`Scope "_local" must have scope-type "_local": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-015.01-scope-type-name]`);
|
|
282
311
|
}
|
|
283
312
|
if (scopeType === 'core' && !scopeName.includes('core')) {
|
|
284
|
-
errors.push(`Scope with type "core" must have "core" in its name: ${toDisplayPath(scopeIndexPath)}`);
|
|
313
|
+
errors.push(`Scope with type "core" must have "core" in its name: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-011.03-naming-convention]`);
|
|
285
314
|
}
|
|
286
315
|
if (scopeType === '_local' && scopeName !== '_local') {
|
|
287
|
-
errors.push(`Scope type "_local" is reserved for the "_local" scope: ${toDisplayPath(scopeIndexPath)}`);
|
|
316
|
+
errors.push(`Scope type "_local" is reserved for the "_local" scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-015.03-exclusive-reservation]`);
|
|
288
317
|
}
|
|
289
318
|
if (scopeType === 'reference' && !scopeName.includes('-ref-')) {
|
|
290
|
-
errors.push(`Scope with type "reference" must follow the naming pattern {domain}-ref-{name} (e.g. "security-ref-baseline"): ${toDisplayPath(scopeIndexPath)}`);
|
|
319
|
+
errors.push(`Scope with type "reference" must follow the naming pattern {domain}-ref-{name} (e.g. "security-ref-baseline"): ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-012.03-naming-convention]`);
|
|
291
320
|
}
|
|
292
321
|
if (scopeType === 'platform' && !scopeName.includes('-plat-')) {
|
|
293
|
-
errors.push(`Scope with type "platform" must follow the naming pattern {domain}-plat-{name} (e.g. "cloud-plat-aws"): ${toDisplayPath(scopeIndexPath)}`);
|
|
322
|
+
errors.push(`Scope with type "platform" must follow the naming pattern {domain}-plat-{name} (e.g. "cloud-plat-aws"): ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-013.03-naming-convention]`);
|
|
294
323
|
}
|
|
295
324
|
|
|
296
325
|
if (!fm.name) {
|
|
297
|
-
errors.push(`Scope index frontmatter must include a non-empty name field: ${toDisplayPath(scopeIndexPath)}`);
|
|
326
|
+
errors.push(`Scope index frontmatter must include a non-empty name field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
298
327
|
} else if (fm.name !== scopeName) {
|
|
299
|
-
errors.push(`Scope index frontmatter name must match scope directory name "${scopeName}": ${toDisplayPath(scopeIndexPath)}`);
|
|
328
|
+
errors.push(`Scope index frontmatter name must match scope directory name "${scopeName}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
300
329
|
}
|
|
301
330
|
|
|
302
331
|
if (!fm.description) {
|
|
303
|
-
errors.push(`Scope index frontmatter must include a non-empty description field: ${toDisplayPath(scopeIndexPath)}`);
|
|
332
|
+
errors.push(`Scope index frontmatter must include a non-empty description field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
304
333
|
} else if (fm.descriptionText && countWords(fm.descriptionText) > 40) {
|
|
305
|
-
errors.push(`Scope index frontmatter description must be 40 words or fewer: ${toDisplayPath(scopeIndexPath)}`);
|
|
334
|
+
errors.push(`Scope index frontmatter description must be 40 words or fewer: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
if (!fm.appliedTo) {
|
|
309
|
-
errors.push(`Scope index frontmatter must include an apply-to field: ${toDisplayPath(scopeIndexPath)}`);
|
|
338
|
+
errors.push(`Scope index frontmatter must include an apply-to field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
310
339
|
} else {
|
|
311
340
|
const words = countWords(fm.appliedTo);
|
|
312
341
|
if (words === 0) {
|
|
313
|
-
errors.push(`Scope index frontmatter apply-to must not be empty: ${toDisplayPath(scopeIndexPath)}`);
|
|
342
|
+
errors.push(`Scope index frontmatter apply-to must not be empty: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
314
343
|
} else if (words > 30) {
|
|
315
|
-
errors.push(`Scope index frontmatter apply-to must be 30 words or fewer: ${toDisplayPath(scopeIndexPath)}`);
|
|
344
|
+
errors.push(`Scope index frontmatter apply-to must be 30 words or fewer: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
316
345
|
}
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
if (!fm.validFrom) {
|
|
320
|
-
errors.push(`Scope index frontmatter must include a valid-from field: ${toDisplayPath(scopeIndexPath)}`);
|
|
349
|
+
errors.push(`Scope index frontmatter must include a valid-from field: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
321
350
|
} else if (!isIsoDate(fm.validFrom)) {
|
|
322
|
-
errors.push(`Scope index frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(scopeIndexPath)}`);
|
|
351
|
+
errors.push(`Scope index frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
323
352
|
}
|
|
324
353
|
|
|
325
354
|
let followsEntries = [];
|
|
@@ -330,7 +359,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
330
359
|
const commaSeparatedPattern = /^[a-zA-Z_][a-zA-Z0-9_-]*(\s*,\s*[a-zA-Z_][a-zA-Z0-9_-]*)+$/;
|
|
331
360
|
if (followsValue) {
|
|
332
361
|
if (!scopeNamePattern.test(followsValue) && !commaSeparatedPattern.test(followsValue)) {
|
|
333
|
-
errors.push(`Scope index frontmatter follows must be a core scope name or list of core scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
362
|
+
errors.push(`Scope index frontmatter follows must be a core scope name or list of core scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
334
363
|
}
|
|
335
364
|
} else {
|
|
336
365
|
const followsStart = block.indexOf(followsLineMatch[0]) + followsLineMatch[0].length;
|
|
@@ -341,7 +370,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
341
370
|
if (itemMatch) {
|
|
342
371
|
const item = (itemMatch[1] || '').trim();
|
|
343
372
|
if (!item || !scopeNamePattern.test(item)) {
|
|
344
|
-
errors.push(`Scope index frontmatter follows entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
373
|
+
errors.push(`Scope index frontmatter follows entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
345
374
|
hasInvalidEntry = true;
|
|
346
375
|
break;
|
|
347
376
|
}
|
|
@@ -351,7 +380,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
351
380
|
}
|
|
352
381
|
}
|
|
353
382
|
if (!hasInvalidEntry && listItems.length === 0) {
|
|
354
|
-
errors.push(`Scope index frontmatter follows must be a core scope name or list of core scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
383
|
+
errors.push(`Scope index frontmatter follows must be a core scope name or list of core scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
355
384
|
}
|
|
356
385
|
}
|
|
357
386
|
if (xdrsRoot) {
|
|
@@ -373,26 +402,26 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
373
402
|
const seenFollows = new Set();
|
|
374
403
|
for (const entry of entries) {
|
|
375
404
|
if (seenFollows.has(entry)) {
|
|
376
|
-
errors.push(`Scope index frontmatter follows has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)}`);
|
|
405
|
+
errors.push(`Scope index frontmatter follows has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
377
406
|
}
|
|
378
407
|
seenFollows.add(entry);
|
|
379
408
|
if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(entry)) {
|
|
380
409
|
if (entry === scopeName) {
|
|
381
|
-
errors.push(`Scope index frontmatter follows must not reference the scope itself: ${toDisplayPath(scopeIndexPath)}`);
|
|
410
|
+
errors.push(`Scope index frontmatter follows must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
382
411
|
continue;
|
|
383
412
|
}
|
|
384
413
|
if (entry === '_core') {
|
|
385
|
-
errors.push(`Scope index frontmatter follows must not reference "_core" as it is always applied implicitly: ${toDisplayPath(scopeIndexPath)}`);
|
|
414
|
+
errors.push(`Scope index frontmatter follows must not reference "_core" as it is always applied implicitly: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
386
415
|
continue;
|
|
387
416
|
}
|
|
388
417
|
const followedIndexPath = path.join(xdrsRoot, entry, 'index.md');
|
|
389
418
|
if (!existsFile(followedIndexPath)) {
|
|
390
|
-
|
|
419
|
+
readOnlyScopes.add(scopeName);
|
|
391
420
|
} else {
|
|
392
421
|
const followedContent = fs.readFileSync(followedIndexPath, 'utf8');
|
|
393
422
|
const followedTypeMatch = followedContent.match(/^scope-type:\s*(.+)$/m);
|
|
394
423
|
if (!followedTypeMatch || followedTypeMatch[1].trim() !== 'core') {
|
|
395
|
-
errors.push(`Scope index frontmatter follows references scope "${entry}" which is not a core-type scope: ${toDisplayPath(scopeIndexPath)}`);
|
|
424
|
+
errors.push(`Scope index frontmatter follows references scope "${entry}" which is not a core-type scope: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
396
425
|
}
|
|
397
426
|
}
|
|
398
427
|
}
|
|
@@ -406,7 +435,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
406
435
|
const scopeNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
407
436
|
if (relatedScopesValue) {
|
|
408
437
|
if (!scopeNamePattern.test(relatedScopesValue)) {
|
|
409
|
-
errors.push(`Scope index frontmatter related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
438
|
+
errors.push(`Scope index frontmatter related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
410
439
|
}
|
|
411
440
|
} else {
|
|
412
441
|
const relatedScopesStart = block.indexOf(relatedScopesLineMatch[0]) + relatedScopesLineMatch[0].length;
|
|
@@ -417,7 +446,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
417
446
|
if (itemMatch) {
|
|
418
447
|
const item = (itemMatch[1] || '').trim();
|
|
419
448
|
if (!item || !scopeNamePattern.test(item)) {
|
|
420
|
-
errors.push(`Scope index frontmatter related-scopes entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
449
|
+
errors.push(`Scope index frontmatter related-scopes entries must be non-empty scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
421
450
|
hasInvalidEntry = true;
|
|
422
451
|
break;
|
|
423
452
|
}
|
|
@@ -427,7 +456,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
427
456
|
}
|
|
428
457
|
}
|
|
429
458
|
if (!hasInvalidEntry && listItems.length === 0) {
|
|
430
|
-
errors.push(`Scope index frontmatter related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)}`);
|
|
459
|
+
errors.push(`Scope index frontmatter related-scopes must be a scope name or list of scope names: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
431
460
|
}
|
|
432
461
|
}
|
|
433
462
|
if (xdrsRoot) {
|
|
@@ -444,20 +473,20 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
444
473
|
const seenRelated = new Set();
|
|
445
474
|
for (const entry of entries) {
|
|
446
475
|
if (seenRelated.has(entry)) {
|
|
447
|
-
errors.push(`Scope index frontmatter related-scopes has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)}`);
|
|
476
|
+
errors.push(`Scope index frontmatter related-scopes has duplicate entry "${entry}": ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
448
477
|
}
|
|
449
478
|
seenRelated.add(entry);
|
|
450
479
|
if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(entry)) {
|
|
451
480
|
if (entry === scopeName) {
|
|
452
|
-
errors.push(`Scope index frontmatter related-scopes must not reference the scope itself: ${toDisplayPath(scopeIndexPath)}`);
|
|
481
|
+
errors.push(`Scope index frontmatter related-scopes must not reference the scope itself: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
453
482
|
continue;
|
|
454
483
|
}
|
|
455
484
|
if (followsEntries.includes(entry)) {
|
|
456
|
-
errors.push(`Scope index frontmatter related-scopes must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)}`);
|
|
485
|
+
errors.push(`Scope index frontmatter related-scopes must not repeat scope "${entry}" already declared in follows: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
457
486
|
continue;
|
|
458
487
|
}
|
|
459
488
|
if (!existsFile(path.join(xdrsRoot, entry, 'index.md'))) {
|
|
460
|
-
errors.push(`Scope index frontmatter related-scopes references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)}`);
|
|
489
|
+
errors.push(`Scope index frontmatter related-scopes references scope "${entry}" which does not exist in the workspace: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
461
490
|
}
|
|
462
491
|
}
|
|
463
492
|
}
|
|
@@ -465,7 +494,7 @@ function lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot,
|
|
|
465
494
|
}
|
|
466
495
|
}
|
|
467
496
|
|
|
468
|
-
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map()) {
|
|
497
|
+
function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, ignoreExternal, externalScopes, knownScopeTypes = new Map(), readOnlyScopes = new Set()) {
|
|
469
498
|
const scopePath = path.join(xdrsRoot, scopeName);
|
|
470
499
|
|
|
471
500
|
if (ignoreExternal && externalScopes.has(scopeName)) {
|
|
@@ -473,7 +502,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
473
502
|
}
|
|
474
503
|
|
|
475
504
|
if (!isValidScopeName(scopeName)) {
|
|
476
|
-
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)}`);
|
|
505
|
+
errors.push(`Invalid scope name: ${toDisplayPath(scopePath)} [_core-adr-policy-001]`);
|
|
477
506
|
}
|
|
478
507
|
|
|
479
508
|
const typeIndexesInScope = [];
|
|
@@ -482,7 +511,7 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
482
511
|
const entryPath = path.join(scopePath, entry.name);
|
|
483
512
|
if (entry.isDirectory()) {
|
|
484
513
|
if (!TYPE_NAMES.has(entry.name)) {
|
|
485
|
-
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
514
|
+
errors.push(`Unexpected directory under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
486
515
|
continue;
|
|
487
516
|
}
|
|
488
517
|
lintTypeDirectory(xdrsRoot, scopeName, entry.name, errors, actualTypeIndexes, externalScopes);
|
|
@@ -497,15 +526,15 @@ function lintScopeDirectory(xdrsRoot, scopeName, errors, actualTypeIndexes, igno
|
|
|
497
526
|
continue;
|
|
498
527
|
}
|
|
499
528
|
|
|
500
|
-
errors.push(`Unexpected file under scope ${scopeName}: ${toDisplayPath(entryPath)}`);
|
|
529
|
+
errors.push(`Unexpected file under scope ${scopeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
501
530
|
}
|
|
502
531
|
|
|
503
532
|
const scopeIndexPath = path.join(scopePath, 'index.md');
|
|
504
533
|
if (!existsFile(scopeIndexPath)) {
|
|
505
|
-
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)}`);
|
|
534
|
+
errors.push(`Missing required scope index: ${toDisplayPath(scopeIndexPath)} [_core-adr-policy-001]`);
|
|
506
535
|
} else {
|
|
507
536
|
lintScopeIndex(scopeIndexPath, xdrsRoot, scopeName, typeIndexesInScope, errors, externalScopes);
|
|
508
|
-
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes);
|
|
537
|
+
lintScopeIndexFrontmatter(scopeIndexPath, scopeName, errors, xdrsRoot, knownScopeTypes, readOnlyScopes);
|
|
509
538
|
}
|
|
510
539
|
lintScopeLocalStandards(xdrsRoot, scopeName, errors);
|
|
511
540
|
}
|
|
@@ -533,7 +562,7 @@ function lintScopeLocalStandards(xdrsRoot, scopeName, errors) {
|
|
|
533
562
|
}
|
|
534
563
|
if (foundPolicies.length > 1) {
|
|
535
564
|
const paths = foundPolicies.map((p) => toDisplayPath(p)).join(', ');
|
|
536
|
-
errors.push(`Scope "${scopeName}" has more than one scope-local standards policy (name ending with "${corePolicySuffix}"): ${paths}`);
|
|
565
|
+
errors.push(`Scope "${scopeName}" has more than one scope-local standards policy (name ending with "${corePolicySuffix}"): ${paths} [_core-adr-policy-010.10-local-placement]`);
|
|
537
566
|
}
|
|
538
567
|
}
|
|
539
568
|
|
|
@@ -544,7 +573,7 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
544
573
|
const artifacts = [];
|
|
545
574
|
|
|
546
575
|
if (!existsFile(indexPath)) {
|
|
547
|
-
errors.push(`Missing canonical index: ${toDisplayPath(indexPath)}`);
|
|
576
|
+
errors.push(`Missing canonical index: ${toDisplayPath(indexPath)} [_core-adr-policy-001]`);
|
|
548
577
|
} else {
|
|
549
578
|
actualTypeIndexes.push(indexPath);
|
|
550
579
|
}
|
|
@@ -554,13 +583,13 @@ function lintTypeDirectory(xdrsRoot, scopeName, typeName, errors, actualTypeInde
|
|
|
554
583
|
const entryPath = path.join(typePath, entry.name);
|
|
555
584
|
if (entry.isFile()) {
|
|
556
585
|
if (entry.name !== 'index.md') {
|
|
557
|
-
errors.push(`Unexpected file under ${scopeName}/${typeName}: ${toDisplayPath(entryPath)}`);
|
|
586
|
+
errors.push(`Unexpected file under ${scopeName}/${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
558
587
|
}
|
|
559
588
|
continue;
|
|
560
589
|
}
|
|
561
590
|
|
|
562
591
|
if (!ALLOWED_SUBJECTS[typeName].has(entry.name)) {
|
|
563
|
-
errors.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)}`);
|
|
592
|
+
errors.push(`Invalid subject folder for ${typeName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
564
593
|
continue;
|
|
565
594
|
}
|
|
566
595
|
|
|
@@ -601,12 +630,12 @@ function lintSubjectDirectory(xdrsRoot, scopeName, typeName, subjectName, xdrsNu
|
|
|
601
630
|
continue;
|
|
602
631
|
}
|
|
603
632
|
|
|
604
|
-
errors.push(`Unexpected directory under ${scopeName}/${typeName}/${subjectName}: ${toDisplayPath(entryPath)}`);
|
|
633
|
+
errors.push(`Unexpected directory under ${scopeName}/${typeName}/${subjectName}: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
605
634
|
continue;
|
|
606
635
|
}
|
|
607
636
|
|
|
608
637
|
if (!NUMBERED_FILE_RE.test(entry.name)) {
|
|
609
|
-
errors.push(`Invalid Policy file name: ${toDisplayPath(entryPath)}`);
|
|
638
|
+
errors.push(`Invalid Policy file name: ${toDisplayPath(entryPath)} [_core-adr-policy-001]`);
|
|
610
639
|
continue;
|
|
611
640
|
}
|
|
612
641
|
|
|
@@ -631,25 +660,26 @@ function lintXdrsElementFile(xdrsRoot, scopeName, typeName, filePath, xdrsNumber
|
|
|
631
660
|
const number = match[1];
|
|
632
661
|
const previous = xdrsNumbers.get(number);
|
|
633
662
|
if (previous) {
|
|
634
|
-
errors.push(`Duplicate Policy number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)}`);
|
|
663
|
+
errors.push(`Duplicate Policy number ${number} in ${scopeName}/${typeName}: ${toDisplayPath(previous)} and ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
635
664
|
} else {
|
|
636
665
|
xdrsNumbers.set(number, filePath);
|
|
637
666
|
}
|
|
638
667
|
|
|
639
668
|
if (baseName !== baseName.toLowerCase()) {
|
|
640
|
-
errors.push(`Policy file name must be lowercase: ${toDisplayPath(filePath)}`);
|
|
669
|
+
errors.push(`Policy file name must be lowercase: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
641
670
|
}
|
|
642
671
|
|
|
643
672
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
644
673
|
const expectedHeader = `# ${scopeName}-${TYPE_TO_ID[typeName]}-${number}:`;
|
|
645
674
|
const firstLine = firstNonEmptyLine(stripFrontmatter(content));
|
|
646
675
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
647
|
-
errors.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)}`);
|
|
676
|
+
errors.push(`Policy title must start with "${expectedHeader}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
648
677
|
}
|
|
649
678
|
|
|
650
679
|
const expectedName = extractExpectedXdrsNameFromHeading(firstLine)
|
|
651
680
|
|| `${scopeName}-${TYPE_TO_ID[typeName]}-${number}-${match[2]}`;
|
|
652
681
|
lintXdrsElementFrontmatter(content, expectedName, filePath, errors);
|
|
682
|
+
lintStructuredRuleBlocks(content, filePath, errors);
|
|
653
683
|
lintRequiredSections(content, filePath, XDRS_REQUIRED_SECTIONS, 'Policy', errors);
|
|
654
684
|
lintNoEmojis(content, filePath, 'Policy', errors);
|
|
655
685
|
lintWordCount(content, filePath, 'Policy', POLICY_MAX_WORDS, errors);
|
|
@@ -684,42 +714,45 @@ function slugifyTitle(title) {
|
|
|
684
714
|
function lintXdrsElementFrontmatter(content, expectedName, filePath, errors) {
|
|
685
715
|
const fm = extractFrontmatter(content);
|
|
686
716
|
if (!fm.present) {
|
|
687
|
-
errors.push(`Policy must start with a YAML frontmatter block: ${toDisplayPath(filePath)}`);
|
|
717
|
+
errors.push(`Policy must start with a YAML frontmatter block: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
688
718
|
return;
|
|
689
719
|
}
|
|
690
720
|
if (!fm.name) {
|
|
691
|
-
errors.push(`Policy frontmatter must include a non-empty name field: ${toDisplayPath(filePath)}`);
|
|
721
|
+
errors.push(`Policy frontmatter must include a non-empty name field: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
692
722
|
} else {
|
|
693
723
|
if (fm.name !== expectedName) {
|
|
694
|
-
errors.push(`Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)}`);
|
|
724
|
+
errors.push(`Policy frontmatter name must be "${expectedName}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
725
|
+
}
|
|
726
|
+
if (fm.name.endsWith('-')) {
|
|
727
|
+
errors.push(`Policy frontmatter name must not end with a hyphen: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
695
728
|
}
|
|
696
729
|
if (fm.name.length > 64) {
|
|
697
|
-
errors.push(`Policy frontmatter name must be 64 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
730
|
+
errors.push(`Policy frontmatter name must be 64 characters or fewer: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
698
731
|
}
|
|
699
732
|
}
|
|
700
733
|
if (!fm.description) {
|
|
701
|
-
errors.push(`Policy frontmatter must include a non-empty description field: ${toDisplayPath(filePath)}`);
|
|
734
|
+
errors.push(`Policy frontmatter must include a non-empty description field: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
702
735
|
} else if (fm.descriptionText && fm.descriptionText.length > 1024) {
|
|
703
|
-
errors.push(`Policy frontmatter description must be 1024 characters or fewer: ${toDisplayPath(filePath)}`);
|
|
736
|
+
errors.push(`Policy frontmatter description must be 1024 characters or fewer: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
704
737
|
}
|
|
705
738
|
if (!fm.validFrom) {
|
|
706
|
-
errors.push(`Policy frontmatter must include a valid-from field: ${toDisplayPath(filePath)}`);
|
|
739
|
+
errors.push(`Policy frontmatter must include a valid-from field: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
707
740
|
} else if (!isIsoDate(fm.validFrom)) {
|
|
708
|
-
errors.push(`Policy frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)}`);
|
|
741
|
+
errors.push(`Policy frontmatter valid-from must be a valid ISO date YYYY-MM-DD: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
709
742
|
}
|
|
710
743
|
if (!fm.appliedTo) {
|
|
711
|
-
errors.push(`Policy frontmatter must include an apply-to field: ${toDisplayPath(filePath)}`);
|
|
744
|
+
errors.push(`Policy frontmatter must include an apply-to field: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
712
745
|
} else {
|
|
713
746
|
const words = countWords(fm.appliedTo);
|
|
714
747
|
if (words === 0) {
|
|
715
|
-
errors.push(`Policy frontmatter apply-to must not be empty: ${toDisplayPath(filePath)}`);
|
|
748
|
+
errors.push(`Policy frontmatter apply-to must not be empty: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
716
749
|
} else if (words >= 40) {
|
|
717
|
-
errors.push(`Policy frontmatter apply-to must be under 40 words: ${toDisplayPath(filePath)}`);
|
|
750
|
+
errors.push(`Policy frontmatter apply-to must be under 40 words: ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
718
751
|
}
|
|
719
752
|
}
|
|
720
753
|
for (const key of fm.topLevelKeys) {
|
|
721
754
|
if (!POLICY_ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
722
|
-
errors.push(`Policy frontmatter has unknown field "${key}": ${toDisplayPath(filePath)}`);
|
|
755
|
+
errors.push(`Policy frontmatter has unknown field "${key}": ${toDisplayPath(filePath)} [_core-adr-policy-002]`);
|
|
723
756
|
}
|
|
724
757
|
}
|
|
725
758
|
}
|
|
@@ -732,26 +765,26 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
732
765
|
for (const entry of entries) {
|
|
733
766
|
const entryPath = path.join(skillsPath, entry.name);
|
|
734
767
|
if (!entry.isDirectory()) {
|
|
735
|
-
errors.push(`Unexpected file in skills directory: ${toDisplayPath(entryPath)}`);
|
|
768
|
+
errors.push(`Unexpected file in skills directory: ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
736
769
|
continue;
|
|
737
770
|
}
|
|
738
771
|
|
|
739
772
|
const match = entry.name.match(NUMBERED_DIR_RE);
|
|
740
773
|
if (!match) {
|
|
741
|
-
errors.push(`Invalid skill package name: ${toDisplayPath(entryPath)}`);
|
|
774
|
+
errors.push(`Invalid skill package name: ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
742
775
|
continue;
|
|
743
776
|
}
|
|
744
777
|
|
|
745
778
|
const number = match[1];
|
|
746
779
|
const previous = skillNumbers.get(number);
|
|
747
780
|
if (previous) {
|
|
748
|
-
errors.push(`Duplicate skill number ${number} in ${scopeName}/${typeName}/${subjectName}/skills: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
781
|
+
errors.push(`Duplicate skill number ${number} in ${scopeName}/${typeName}/${subjectName}/skills: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
749
782
|
} else {
|
|
750
783
|
skillNumbers.set(number, entryPath);
|
|
751
784
|
}
|
|
752
785
|
|
|
753
786
|
if (entry.name !== entry.name.toLowerCase()) {
|
|
754
|
-
errors.push(`Skill package name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
787
|
+
errors.push(`Skill package name must be lowercase: ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
755
788
|
}
|
|
756
789
|
|
|
757
790
|
const skillFilePath = path.join(entryPath, 'SKILL.md');
|
|
@@ -768,11 +801,11 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
768
801
|
continue;
|
|
769
802
|
}
|
|
770
803
|
|
|
771
|
-
errors.push(`Unexpected directory in skill package: ${toDisplayPath(packageEntryPath)}`);
|
|
804
|
+
errors.push(`Unexpected directory in skill package: ${toDisplayPath(packageEntryPath)} [_core-adr-policy-003]`);
|
|
772
805
|
}
|
|
773
806
|
|
|
774
807
|
if (!existsFile(skillFilePath)) {
|
|
775
|
-
errors.push(`Missing SKILL.md in skill package: ${toDisplayPath(entryPath)}`);
|
|
808
|
+
errors.push(`Missing SKILL.md in skill package: ${toDisplayPath(entryPath)} [_core-adr-policy-003]`);
|
|
776
809
|
continue;
|
|
777
810
|
}
|
|
778
811
|
|
|
@@ -782,27 +815,27 @@ function lintSkillsDirectory(xdrsRoot, scopeName, typeName, subjectName, skillsP
|
|
|
782
815
|
lintRequiredSections(skillContent, skillFilePath, SKILL_REQUIRED_SECTIONS, 'Skill', errors);
|
|
783
816
|
const skillFm = extractFrontmatter(skillContent);
|
|
784
817
|
if (!skillFm.present) {
|
|
785
|
-
errors.push(`SKILL.md must start with a YAML frontmatter block: ${toDisplayPath(skillFilePath)}`);
|
|
818
|
+
errors.push(`SKILL.md must start with a YAML frontmatter block: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
786
819
|
} else {
|
|
787
820
|
if (!skillFm.name) {
|
|
788
|
-
errors.push(`SKILL.md frontmatter must include a non-empty name field: ${toDisplayPath(skillFilePath)}`);
|
|
821
|
+
errors.push(`SKILL.md frontmatter must include a non-empty name field: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
789
822
|
} else {
|
|
790
823
|
const expectedSkillName = `${number}-${match[2]}`;
|
|
791
824
|
if (skillFm.name !== expectedSkillName) {
|
|
792
|
-
errors.push(`Skill frontmatter name must be "${expectedSkillName}": ${toDisplayPath(skillFilePath)}`);
|
|
825
|
+
errors.push(`Skill frontmatter name must be "${expectedSkillName}": ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
793
826
|
}
|
|
794
827
|
if (skillFm.name.length > 64) {
|
|
795
|
-
errors.push(`SKILL.md frontmatter name must be 64 characters or fewer: ${toDisplayPath(skillFilePath)}`);
|
|
828
|
+
errors.push(`SKILL.md frontmatter name must be 64 characters or fewer: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
796
829
|
}
|
|
797
830
|
}
|
|
798
831
|
if (!skillFm.description) {
|
|
799
|
-
errors.push(`SKILL.md frontmatter must include a non-empty description field: ${toDisplayPath(skillFilePath)}`);
|
|
832
|
+
errors.push(`SKILL.md frontmatter must include a non-empty description field: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
800
833
|
} else if (skillFm.descriptionText && skillFm.descriptionText.length > 1024) {
|
|
801
|
-
errors.push(`SKILL.md frontmatter description must be 1024 characters or fewer: ${toDisplayPath(skillFilePath)}`);
|
|
834
|
+
errors.push(`SKILL.md frontmatter description must be 1024 characters or fewer: ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
802
835
|
}
|
|
803
836
|
for (const key of skillFm.topLevelKeys) {
|
|
804
837
|
if (!SKILL_ALLOWED_FRONTMATTER_KEYS.has(key)) {
|
|
805
|
-
errors.push(`SKILL.md frontmatter has unknown field "${key}": ${toDisplayPath(skillFilePath)}`);
|
|
838
|
+
errors.push(`SKILL.md frontmatter has unknown field "${key}": ${toDisplayPath(skillFilePath)} [_core-adr-policy-003]`);
|
|
806
839
|
}
|
|
807
840
|
}
|
|
808
841
|
}
|
|
@@ -828,13 +861,13 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
828
861
|
}
|
|
829
862
|
|
|
830
863
|
if (!entry.isFile()) {
|
|
831
|
-
errors.push(`Unexpected directory in articles folder: ${toDisplayPath(entryPath)}`);
|
|
864
|
+
errors.push(`Unexpected directory in articles folder: ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
|
|
832
865
|
continue;
|
|
833
866
|
}
|
|
834
867
|
|
|
835
868
|
const match = entry.name.match(NUMBERED_FILE_RE);
|
|
836
869
|
if (!match) {
|
|
837
|
-
errors.push(`Invalid article file name: ${toDisplayPath(entryPath)}`);
|
|
870
|
+
errors.push(`Invalid article file name: ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
|
|
838
871
|
continue;
|
|
839
872
|
}
|
|
840
873
|
|
|
@@ -843,13 +876,13 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
843
876
|
const number = match[1];
|
|
844
877
|
const previous = articleNumbers.get(number);
|
|
845
878
|
if (previous) {
|
|
846
|
-
errors.push(`Duplicate article number ${number} in ${scopeName}/${typeName}/${subjectName}/articles: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
879
|
+
errors.push(`Duplicate article number ${number} in ${scopeName}/${typeName}/${subjectName}/articles: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
|
|
847
880
|
} else {
|
|
848
881
|
articleNumbers.set(number, entryPath);
|
|
849
882
|
}
|
|
850
883
|
|
|
851
884
|
if (entry.name !== entry.name.toLowerCase()) {
|
|
852
|
-
errors.push(`Article file name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
885
|
+
errors.push(`Article file name must be lowercase: ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
|
|
853
886
|
}
|
|
854
887
|
|
|
855
888
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
@@ -857,7 +890,7 @@ function lintArticlesDirectory(xdrsRoot, scopeName, typeName, subjectName, artic
|
|
|
857
890
|
const expectedHeader = `# ${scopeName}-${typeId}-article-${number}:`;
|
|
858
891
|
const firstLine = firstNonEmptyLine(content);
|
|
859
892
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
860
|
-
errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
893
|
+
errors.push(`Article title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-004]`);
|
|
861
894
|
}
|
|
862
895
|
|
|
863
896
|
lintRequiredSections(content, entryPath, ARTICLE_REQUIRED_SECTIONS, 'Article', errors);
|
|
@@ -883,13 +916,13 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
883
916
|
}
|
|
884
917
|
|
|
885
918
|
if (!entry.isFile()) {
|
|
886
|
-
errors.push(`Unexpected directory in researches folder: ${toDisplayPath(entryPath)}`);
|
|
919
|
+
errors.push(`Unexpected directory in researches folder: ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
|
|
887
920
|
continue;
|
|
888
921
|
}
|
|
889
922
|
|
|
890
923
|
const match = entry.name.match(NUMBERED_FILE_RE);
|
|
891
924
|
if (!match) {
|
|
892
|
-
errors.push(`Invalid research file name: ${toDisplayPath(entryPath)}`);
|
|
925
|
+
errors.push(`Invalid research file name: ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
|
|
893
926
|
continue;
|
|
894
927
|
}
|
|
895
928
|
|
|
@@ -898,13 +931,13 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
898
931
|
const number = match[1];
|
|
899
932
|
const previous = researchNumbers.get(number);
|
|
900
933
|
if (previous) {
|
|
901
|
-
errors.push(`Duplicate research number ${number} in ${scopeName}/${typeName}/${subjectName}/researches: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
934
|
+
errors.push(`Duplicate research number ${number} in ${scopeName}/${typeName}/${subjectName}/researches: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
|
|
902
935
|
} else {
|
|
903
936
|
researchNumbers.set(number, entryPath);
|
|
904
937
|
}
|
|
905
938
|
|
|
906
939
|
if (entry.name !== entry.name.toLowerCase()) {
|
|
907
|
-
errors.push(`Research file name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
940
|
+
errors.push(`Research file name must be lowercase: ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
|
|
908
941
|
}
|
|
909
942
|
|
|
910
943
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
@@ -912,11 +945,12 @@ function lintResearchDirectory(xdrsRoot, scopeName, typeName, subjectName, resea
|
|
|
912
945
|
const expectedHeader = `# ${scopeName}-${typeId}-research-${number}:`;
|
|
913
946
|
const firstLine = firstNonEmptyLine(content);
|
|
914
947
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
915
|
-
errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
948
|
+
errors.push(`Research title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-006]`);
|
|
916
949
|
}
|
|
917
950
|
|
|
918
951
|
lintRequiredSections(content, entryPath, RESEARCH_REQUIRED_SECTIONS, 'Research', errors);
|
|
919
952
|
lintResearchIntroductionQuestion(content, entryPath, errors);
|
|
953
|
+
lintResearchSectionWordLimits(content, entryPath, errors);
|
|
920
954
|
lintNoEmojis(content, entryPath, 'Research', errors);
|
|
921
955
|
lintWordCount(content, entryPath, 'Research', RESEARCH_MAX_WORDS, errors);
|
|
922
956
|
lintDocumentLinks(entryPath, xdrsRoot, scopeName, errors, externalScopes);
|
|
@@ -939,13 +973,13 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
939
973
|
}
|
|
940
974
|
|
|
941
975
|
if (!entry.isFile()) {
|
|
942
|
-
errors.push(`Unexpected directory in plans folder: ${toDisplayPath(entryPath)}`);
|
|
976
|
+
errors.push(`Unexpected directory in plans folder: ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
|
|
943
977
|
continue;
|
|
944
978
|
}
|
|
945
979
|
|
|
946
980
|
const match = entry.name.match(NUMBERED_FILE_RE);
|
|
947
981
|
if (!match) {
|
|
948
|
-
errors.push(`Invalid plan file name: ${toDisplayPath(entryPath)}`);
|
|
982
|
+
errors.push(`Invalid plan file name: ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
|
|
949
983
|
continue;
|
|
950
984
|
}
|
|
951
985
|
|
|
@@ -954,13 +988,13 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
954
988
|
const number = match[1];
|
|
955
989
|
const previous = planNumbers.get(number);
|
|
956
990
|
if (previous) {
|
|
957
|
-
errors.push(`Duplicate plan number ${number} in ${scopeName}/${typeName}/${subjectName}/plans: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)}`);
|
|
991
|
+
errors.push(`Duplicate plan number ${number} in ${scopeName}/${typeName}/${subjectName}/plans: ${toDisplayPath(previous)} and ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
|
|
958
992
|
} else {
|
|
959
993
|
planNumbers.set(number, entryPath);
|
|
960
994
|
}
|
|
961
995
|
|
|
962
996
|
if (entry.name !== entry.name.toLowerCase()) {
|
|
963
|
-
errors.push(`Plan file name must be lowercase: ${toDisplayPath(entryPath)}`);
|
|
997
|
+
errors.push(`Plan file name must be lowercase: ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
|
|
964
998
|
}
|
|
965
999
|
|
|
966
1000
|
const content = fs.readFileSync(entryPath, 'utf8');
|
|
@@ -968,7 +1002,7 @@ function lintPlansDirectory(xdrsRoot, scopeName, typeName, subjectName, plansPat
|
|
|
968
1002
|
const expectedHeader = `# ${scopeName}-${typeId}-plan-${number}:`;
|
|
969
1003
|
const firstLine = firstNonEmptyLine(content);
|
|
970
1004
|
if (!firstLine.startsWith(expectedHeader)) {
|
|
971
|
-
errors.push(`Plan title must start with "${expectedHeader}": ${toDisplayPath(entryPath)}`);
|
|
1005
|
+
errors.push(`Plan title must start with "${expectedHeader}": ${toDisplayPath(entryPath)} [_core-adr-policy-007]`);
|
|
972
1006
|
}
|
|
973
1007
|
|
|
974
1008
|
lintRequiredSections(content, entryPath, PLAN_REQUIRED_SECTIONS, 'Plan', errors);
|
|
@@ -988,7 +1022,7 @@ function lintNoEmojis(content, filePath, docType, errors) {
|
|
|
988
1022
|
for (let i = 0; i < lines.length; i++) {
|
|
989
1023
|
if (ignoredLines[i]) continue;
|
|
990
1024
|
if (EMOJI_RE.test(lines[i])) {
|
|
991
|
-
errors.push(`${docType} must not contain emojis: ${toDisplayPath(filePath)}:${i + 1}`);
|
|
1025
|
+
errors.push(`${docType} must not contain emojis: ${toDisplayPath(filePath)}:${i + 1} ${DOC_TYPE_POLICY_REF[docType] || ''}`);
|
|
992
1026
|
}
|
|
993
1027
|
}
|
|
994
1028
|
}
|
|
@@ -997,7 +1031,7 @@ function lintWordCount(content, filePath, docType, maxWords, errors) {
|
|
|
997
1031
|
const body = stripFrontmatter(content);
|
|
998
1032
|
const wordCount = countWords(body);
|
|
999
1033
|
if (wordCount > maxWords) {
|
|
1000
|
-
errors.push(`${docType} exceeds maximum word count of ${maxWords} (${wordCount} words): ${toDisplayPath(filePath)}`);
|
|
1034
|
+
errors.push(`${docType} exceeds maximum word count of ${maxWords} (${wordCount} words): ${toDisplayPath(filePath)} ${DOC_TYPE_POLICY_REF[docType] || ''}`);
|
|
1001
1035
|
}
|
|
1002
1036
|
}
|
|
1003
1037
|
|
|
@@ -1024,7 +1058,7 @@ function lintResearchIntroductionQuestion(content, filePath, errors) {
|
|
|
1024
1058
|
}
|
|
1025
1059
|
|
|
1026
1060
|
if (!hasQuestion) {
|
|
1027
|
-
errors.push(`Research ## Introduction must contain a "Question:" line: ${toDisplayPath(filePath)}`);
|
|
1061
|
+
errors.push(`Research ## Introduction must contain a "Question:" line: ${toDisplayPath(filePath)} [_core-adr-policy-006]`);
|
|
1028
1062
|
}
|
|
1029
1063
|
}
|
|
1030
1064
|
|
|
@@ -1039,7 +1073,7 @@ function lintRequiredSections(content, filePath, requiredSections, docType, erro
|
|
|
1039
1073
|
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
1040
1074
|
for (const section of requiredSections) {
|
|
1041
1075
|
if (findHeadingLine(lines, ignoredLines, section) === -1) {
|
|
1042
|
-
errors.push(`${docType} is missing required section "${section}": ${toDisplayPath(filePath)}`);
|
|
1076
|
+
errors.push(`${docType} is missing required section "${section}": ${toDisplayPath(filePath)} ${DOC_TYPE_POLICY_REF[docType] || ''}`);
|
|
1043
1077
|
}
|
|
1044
1078
|
}
|
|
1045
1079
|
}
|
|
@@ -1050,17 +1084,17 @@ function lintPlanExpectedEndDate(content, filePath, errors) {
|
|
|
1050
1084
|
const expectedEndDateLines = findFieldLines(lines, ignoredLines, 'Expected end date:');
|
|
1051
1085
|
|
|
1052
1086
|
if (expectedEndDateLines.length === 0) {
|
|
1053
|
-
errors.push(`Plan must include an Expected end date: field: ${toDisplayPath(filePath)}`);
|
|
1087
|
+
errors.push(`Plan must include an Expected end date: field: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
|
|
1054
1088
|
return;
|
|
1055
1089
|
}
|
|
1056
1090
|
|
|
1057
1091
|
if (expectedEndDateLines.length > 1) {
|
|
1058
|
-
errors.push(`Plan must not repeat Expected end date: ${toDisplayPath(filePath)}`);
|
|
1092
|
+
errors.push(`Plan must not repeat Expected end date: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
|
|
1059
1093
|
}
|
|
1060
1094
|
|
|
1061
1095
|
const value = lines[expectedEndDateLines[0]].slice('Expected end date:'.length).trim().replace(/\.$/, '');
|
|
1062
1096
|
if (!isIsoDate(value)) {
|
|
1063
|
-
errors.push(`Plan Expected end date: must be a valid ISO date in YYYY-MM-DD format: ${toDisplayPath(filePath)}`);
|
|
1097
|
+
errors.push(`Plan Expected end date: must be a valid ISO date in YYYY-MM-DD format: ${toDisplayPath(filePath)} [_core-adr-policy-007]`);
|
|
1064
1098
|
}
|
|
1065
1099
|
}
|
|
1066
1100
|
|
|
@@ -1075,12 +1109,12 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors, externalScopes =
|
|
|
1075
1109
|
for (const linkPath of localLinks) {
|
|
1076
1110
|
if (!fs.existsSync(linkPath)) {
|
|
1077
1111
|
if (isExternalScopeLink(linkPath, xdrsRoot, externalScopes)) continue;
|
|
1078
|
-
errors.push(`Broken link in canonical index ${toDisplayPath(indexPath)}: ${displayPath(indexPath, linkPath)}`);
|
|
1112
|
+
errors.push(`Broken link in canonical index ${toDisplayPath(indexPath)}: ${displayPath(indexPath, linkPath)} [_core-adr-policy-001]`);
|
|
1079
1113
|
continue;
|
|
1080
1114
|
}
|
|
1081
1115
|
|
|
1082
1116
|
if (scopeName !== '_local' && (isPathInside(localScopePath, linkPath) || normalizePath(linkPath) === localScopePath)) {
|
|
1083
|
-
errors.push(`Non-_local document must not link into _local scope: ${displayPath(indexPath, linkPath)}`);
|
|
1117
|
+
errors.push(`Non-_local document must not link into _local scope: ${displayPath(indexPath, linkPath)} [_core-adr-policy-001]`);
|
|
1084
1118
|
}
|
|
1085
1119
|
|
|
1086
1120
|
linkedSet.add(normalizePath(linkPath));
|
|
@@ -1088,7 +1122,7 @@ function lintTypeIndex(indexPath, xdrsRoot, artifacts, errors, externalScopes =
|
|
|
1088
1122
|
|
|
1089
1123
|
for (const artifactPath of artifacts) {
|
|
1090
1124
|
if (!linkedSet.has(normalizePath(artifactPath))) {
|
|
1091
|
-
errors.push(`Canonical index ${toDisplayPath(indexPath)} is missing an entry for ${toDisplayPath(artifactPath)}`);
|
|
1125
|
+
errors.push(`Canonical index ${toDisplayPath(indexPath)} is missing an entry for ${toDisplayPath(artifactPath)} [_core-adr-policy-001]`);
|
|
1092
1126
|
}
|
|
1093
1127
|
}
|
|
1094
1128
|
}
|
|
@@ -1120,7 +1154,7 @@ function lintOrphanAssets(assetsDir, documentPaths, xdrsRoot, errors) {
|
|
|
1120
1154
|
|
|
1121
1155
|
for (const assetPath of assetTree.files) {
|
|
1122
1156
|
if (!referencedAssets.has(assetPath)) {
|
|
1123
|
-
errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)}`);
|
|
1157
|
+
errors.push(`Orphan asset file not referenced by any document: ${toDisplayPath(assetPath)} [_core-adr-policy-001]`);
|
|
1124
1158
|
}
|
|
1125
1159
|
}
|
|
1126
1160
|
|
|
@@ -1137,23 +1171,23 @@ function lintSlideFile(filePath, documentPaths, xdrsRoot, errors) {
|
|
|
1137
1171
|
const fileName = path.basename(filePath);
|
|
1138
1172
|
|
|
1139
1173
|
if (fileName !== fileName.toLowerCase()) {
|
|
1140
|
-
errors.push(`Slide file name must be lowercase: ${toDisplayPath(filePath)}`);
|
|
1174
|
+
errors.push(`Slide file name must be lowercase: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1141
1175
|
}
|
|
1142
1176
|
|
|
1143
1177
|
if (fileName.length > SLIDE_MAX_NAME_LENGTH) {
|
|
1144
|
-
errors.push(`Slide file name must be ${SLIDE_MAX_NAME_LENGTH} characters or fewer: ${toDisplayPath(filePath)}`);
|
|
1178
|
+
errors.push(`Slide file name must be ${SLIDE_MAX_NAME_LENGTH} characters or fewer: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1145
1179
|
}
|
|
1146
1180
|
|
|
1147
1181
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
1148
1182
|
|
|
1149
1183
|
const fm = extractFrontmatter(content);
|
|
1150
1184
|
if (!fm.present) {
|
|
1151
|
-
errors.push(`Slide file must start with a YAML frontmatter block containing marp: true: ${toDisplayPath(filePath)}`);
|
|
1185
|
+
errors.push(`Slide file must start with a YAML frontmatter block containing marp: true: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1152
1186
|
} else {
|
|
1153
1187
|
const frontmatterBody = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)[1];
|
|
1154
1188
|
const firstKey = frontmatterBody.trimStart().split(/\r?\n/)[0];
|
|
1155
1189
|
if (!/^marp:\s*true$/.test(firstKey)) {
|
|
1156
|
-
errors.push(`Slide frontmatter must include marp: true as the first key: ${toDisplayPath(filePath)}`);
|
|
1190
|
+
errors.push(`Slide frontmatter must include marp: true as the first key: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1157
1191
|
}
|
|
1158
1192
|
}
|
|
1159
1193
|
|
|
@@ -1167,7 +1201,18 @@ function lintSlideFile(filePath, documentPaths, xdrsRoot, errors) {
|
|
|
1167
1201
|
|
|
1168
1202
|
const linksToParent = documentPaths.some((docPath) => slideLinkSet.has(normalizePath(docPath)));
|
|
1169
1203
|
if (!linksToParent) {
|
|
1170
|
-
errors.push(`Slide file must contain a link back to its parent document: ${toDisplayPath(filePath)}`);
|
|
1204
|
+
errors.push(`Slide file must contain a link back to its parent document: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Check that at least one parent document links to this slide file
|
|
1208
|
+
const parentLinksToSlide = documentPaths.some((docPath) => {
|
|
1209
|
+
if (!existsFile(docPath)) return false;
|
|
1210
|
+
const docContent = fs.readFileSync(docPath, 'utf8');
|
|
1211
|
+
const docLinks = parseLocalLinks(docContent, path.dirname(docPath), repoRoot);
|
|
1212
|
+
return docLinks.some((l) => normalizePath(l) === normalizePath(filePath));
|
|
1213
|
+
});
|
|
1214
|
+
if (!parentLinksToSlide) {
|
|
1215
|
+
errors.push(`Parent document must contain a link to its slide file: ${toDisplayPath(filePath)} [_core-adr-policy-009]`);
|
|
1171
1216
|
}
|
|
1172
1217
|
}
|
|
1173
1218
|
|
|
@@ -1212,25 +1257,25 @@ function lintDocumentLinks(documentPath, xdrsRoot, scopeName, errors, externalSc
|
|
|
1212
1257
|
const isResourceLink = shouldValidateResourceLink(link.rawTarget);
|
|
1213
1258
|
|
|
1214
1259
|
if (!isResourceLink && link.isAbsolutePath) {
|
|
1215
|
-
errors.push(`Absolute path links are not allowed; use relative paths in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget}`);
|
|
1260
|
+
errors.push(`Absolute path links are not allowed; use relative paths in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
1216
1261
|
}
|
|
1217
1262
|
|
|
1218
1263
|
if (!fs.existsSync(link.resolvedPath)) {
|
|
1219
1264
|
if (isExternalScopeLink(link.resolvedPath, xdrsRoot, externalScopes)) continue;
|
|
1220
1265
|
if (isResourceLink) {
|
|
1221
|
-
errors.push(`Broken asset link in ${toDisplayPath(documentPath)}: ${link.rawTarget}`);
|
|
1266
|
+
errors.push(`Broken asset link in ${toDisplayPath(documentPath)}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
1222
1267
|
} else {
|
|
1223
|
-
errors.push(`Broken local link in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget}`);
|
|
1268
|
+
errors.push(`Broken local link in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
1224
1269
|
}
|
|
1225
1270
|
continue;
|
|
1226
1271
|
}
|
|
1227
1272
|
|
|
1228
1273
|
if (scopeName !== '_local' && (isPathInside(localScopePath, link.resolvedPath) || normalizePath(link.resolvedPath) === localScopePath)) {
|
|
1229
|
-
errors.push(`Non-_local document must not link into _local scope in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget}`);
|
|
1274
|
+
errors.push(`Non-_local document must not link into _local scope in ${toDisplayPath(documentPath)}:${index + 1}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
1230
1275
|
}
|
|
1231
1276
|
|
|
1232
1277
|
if (isResourceLink && !isPathInside(resourceDir, link.resolvedPath)) {
|
|
1233
|
-
errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget}`);
|
|
1278
|
+
errors.push(`Asset links in ${toDisplayPath(documentPath)} must point to ${toDisplayPath(resourceDir)}: ${link.rawTarget} [_core-adr-policy-001]`);
|
|
1234
1279
|
}
|
|
1235
1280
|
}
|
|
1236
1281
|
}
|
|
@@ -1553,6 +1598,98 @@ function isPathInside(parentPath, childPath) {
|
|
|
1553
1598
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1554
1599
|
}
|
|
1555
1600
|
|
|
1601
|
+
function lintStructuredRuleBlocks(content, filePath, errors) {
|
|
1602
|
+
const body = stripFrontmatter(content);
|
|
1603
|
+
const lines = body.split(/\r?\n/);
|
|
1604
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
1605
|
+
|
|
1606
|
+
const ruleNumbers = new Map(); // number -> line index
|
|
1607
|
+
const ruleEntries = [];
|
|
1608
|
+
|
|
1609
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1610
|
+
if (ignoredLines[i]) continue;
|
|
1611
|
+
const m = lines[i].match(/^#### (\d+)-(.*)$/);
|
|
1612
|
+
if (!m) continue;
|
|
1613
|
+
const number = m[1];
|
|
1614
|
+
const title = m[2].trim();
|
|
1615
|
+
ruleEntries.push({ lineIndex: i, number, title });
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
for (let r = 0; r < ruleEntries.length; r++) {
|
|
1619
|
+
const { lineIndex, number, title } = ruleEntries[r];
|
|
1620
|
+
|
|
1621
|
+
if (!/^\d{2}$/.test(number)) {
|
|
1622
|
+
errors.push(`Policy structured rule number must be exactly two digits ("${number}" in "${lines[lineIndex].trim()}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.02-rule-numbering-must-be-stable]`);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (title.length === 0 || !/^[a-z][a-z0-9-]*$/.test(title)) {
|
|
1626
|
+
errors.push(`Policy structured rule title must be in kebab-case ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]`);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const wordCount = title.split('-').filter(Boolean).length;
|
|
1630
|
+
if (wordCount > 12) {
|
|
1631
|
+
errors.push(`Policy structured rule title must be 12 words or fewer ("${title}"): ${toDisplayPath(filePath)} [_core-adr-policy-008.01-always-use-numbered-rules-for-strong-or-referenceable-policies]`);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (ruleNumbers.has(number)) {
|
|
1635
|
+
errors.push(`Duplicate structured rule number "${number}" in Policy: ${toDisplayPath(filePath)} [_core-adr-policy-008.02-rule-numbering-must-be-stable]`);
|
|
1636
|
+
} else {
|
|
1637
|
+
ruleNumbers.set(number, lineIndex);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const bodyStart = lineIndex + 1;
|
|
1641
|
+
let bodyEnd = lines.length;
|
|
1642
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
1643
|
+
if (!ignoredLines[i] && /^#{1,4}/.test(lines[i])) {
|
|
1644
|
+
bodyEnd = i;
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const ruleBodyText = lines.slice(bodyStart, bodyEnd).join('\n');
|
|
1649
|
+
|
|
1650
|
+
if (!NORMATIVE_KEYWORDS_RE.test(ruleBodyText)) {
|
|
1651
|
+
errors.push(`Policy structured rule body must contain normative language (MUST/SHOULD/MAY) in "${number}-${title}": ${toDisplayPath(filePath)} [_core-adr-policy-008.03-rule-body-must-use-normative-language]`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function lintResearchSectionWordLimits(content, filePath, errors) {
|
|
1657
|
+
const lines = content.split(/\r?\n/);
|
|
1658
|
+
const ignoredLines = findIgnoredMarkdownLines(lines);
|
|
1659
|
+
|
|
1660
|
+
const sectionStarts = {};
|
|
1661
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1662
|
+
if (ignoredLines[i]) continue;
|
|
1663
|
+
const trimmed = lines[i].trim();
|
|
1664
|
+
for (const sectionName of Object.keys(RESEARCH_SECTION_LIMITS)) {
|
|
1665
|
+
if (trimmed === sectionName) {
|
|
1666
|
+
sectionStarts[sectionName] = i;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
for (const sectionName of Object.keys(RESEARCH_SECTION_LIMITS)) {
|
|
1672
|
+
const startLine = sectionStarts[sectionName];
|
|
1673
|
+
if (startLine === undefined) continue;
|
|
1674
|
+
|
|
1675
|
+
let endLine = lines.length;
|
|
1676
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
1677
|
+
if (!ignoredLines[i] && /^## /.test(lines[i].trim())) {
|
|
1678
|
+
endLine = i;
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const sectionText = lines.slice(startLine + 1, endLine).join(' ');
|
|
1684
|
+
const wordCount = countWords(sectionText);
|
|
1685
|
+
const limit = RESEARCH_SECTION_LIMITS[sectionName];
|
|
1686
|
+
|
|
1687
|
+
if (wordCount > limit) {
|
|
1688
|
+
errors.push(`Research ${sectionName} section exceeds ${limit} words (${wordCount} words): ${toDisplayPath(filePath)} [_core-adr-policy-006]`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1556
1693
|
module.exports = {
|
|
1557
1694
|
runLintCli,
|
|
1558
1695
|
lintWorkspace
|