spec-up-t 1.4.1 → 1.6.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/assets/compiled/body.js +3 -2
  2. package/assets/compiled/head.css +5 -5
  3. package/assets/css/embedded-libraries/bootstrap.min.css +1 -1
  4. package/assets/css/header-navbar.css +4 -4
  5. package/assets/css/index.css +5 -4
  6. package/assets/css/refs.css +30 -0
  7. package/assets/css/terms-and-definitions.css +89 -1
  8. package/assets/js/github-issues.js +3 -3
  9. package/assets/js/insert-irefs.js +214 -0
  10. package/config/asset-map.json +2 -1
  11. package/examples/read-console-messages.js +102 -0
  12. package/gulpfile.js +42 -1
  13. package/index.js +49 -1
  14. package/package.json +2 -1
  15. package/src/create-docx.js +13 -6
  16. package/src/create-pdf.js +22 -18
  17. package/src/health-check.js +47 -629
  18. package/src/init.js +7 -3
  19. package/src/install-from-boilerplate/config-scripts-keys.js +1 -1
  20. package/src/markdown-it/README.md +2 -14
  21. package/src/markdown-it/index.js +1 -7
  22. package/src/parsers/template-tag-parser.js +42 -4
  23. package/src/pipeline/postprocessing/definition-list-postprocessor.js +4 -2
  24. package/src/pipeline/references/collect-external-references.js +101 -17
  25. package/src/pipeline/references/external-references-service.js +102 -21
  26. package/src/pipeline/references/fetch-terms-from-index.js +62 -1
  27. package/src/pipeline/references/process-xtrefs-data.js +67 -9
  28. package/src/pipeline/references/xtref-utils.js +22 -3
  29. package/src/pipeline/rendering/render-spec-document.js +0 -1
  30. package/src/run-healthcheck.js +177 -0
  31. package/src/utils/logger.js +129 -8
  32. package/src/utils/message-collector.js +144 -0
  33. package/src/utils/regex-patterns.js +3 -1
  34. package/templates/template.html +6 -6
  35. package/test/logger.test.js +290 -0
  36. package/test/message-collector.test.js +286 -0
  37. package/assets/css/insert-trefs.css +0 -1
  38. package/src/markdown-it/link-enhancement.js +0 -98
  39. package/src/utils/LOGGER.md +0 -81
@@ -17,7 +17,11 @@ function validateReferences(references, definitions, render) {
17
17
  }
18
18
  );
19
19
  if (unresolvedRefs.length > 0) {
20
- Logger.info('Unresolved References:', unresolvedRefs);
20
+ Logger.warn(`Unresolved References: ${unresolvedRefs.join(',')}`, {
21
+ context: 'These terms are referenced in your spec but not defined',
22
+ hint: 'Add [[def: term]] definitions for these terms in your terminology files, or check for typos in [[ref: term]] references',
23
+ details: `Count: ${unresolvedRefs.length} unresolved term(s)`
24
+ });
21
25
  }
22
26
 
23
27
  const danglingDefs = [];
@@ -38,7 +42,11 @@ function validateReferences(references, definitions, render) {
38
42
  }
39
43
  })
40
44
  if (danglingDefs.length > 0) {
41
- Logger.info('Dangling Definitions:', danglingDefs);
45
+ Logger.warn(`Dangling Definitions: ${danglingDefs.join(',')}`, {
46
+ context: 'These terms are defined but never referenced in your spec',
47
+ hint: 'Add [[ref: term]] references where needed.',
48
+ details: `Count: ${danglingDefs.length} unused definition(s)`
49
+ });
42
50
  }
43
51
  }
44
52
 
@@ -71,7 +79,11 @@ async function fetchExternalSpecs(spec) {
71
79
  const msg = f.error.response
72
80
  ? `HTTP ${f.error.response.status} for ${f.url}`
73
81
  : `Network error for ${f.url}: ${f.error.message}`;
74
- Logger.error("External spec fetch failed:", msg);
82
+ Logger.error("External spec fetch failed", {
83
+ context: `Attempting to fetch external terminology from: ${f.url}`,
84
+ hint: 'Verify the URL is correct in specs.json under external_specs[].gh_page. Ensure the external spec has published their GitHub Pages site',
85
+ details: msg
86
+ });
75
87
  });
76
88
  }
77
89
 
@@ -93,7 +105,11 @@ async function fetchExternalSpecs(spec) {
93
105
 
94
106
  return extractedTerms;
95
107
  } catch (e) {
96
- Logger.error("Unexpected error in fetchExternalSpecs:", e);
108
+ Logger.error("Unexpected error in fetchExternalSpecs", {
109
+ context: 'Failed while fetching terms from external specifications',
110
+ hint: 'Check your internet connection and verify all external_specs URLs in specs.json are valid. Run with GITHUB_PAT set if accessing private repos',
111
+ details: e.message
112
+ });
97
113
  return [];
98
114
  }
99
115
  }
@@ -117,6 +133,10 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
117
133
 
118
134
  // Add xref terms to the allXTrefs structure
119
135
  // Mark them with source: 'xref' to distinguish from tref entries
136
+ // Track how many terms were matched vs skipped for logging purposes
137
+ let matchedCount = 0;
138
+ let skippedCount = 0;
139
+
120
140
  xrefTerms.forEach(xrefTerm => {
121
141
  // Check if this term already exists (match by externalSpec and term only)
122
142
  // Don't filter by source because entries from markdown scanning don't have source field
@@ -126,20 +146,62 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
126
146
  );
127
147
 
128
148
  if (existingIndex >= 0) {
149
+ // Get the existing entry to check if it's a tref
150
+ const existingXtref = allXTrefs.xtrefs[existingIndex];
151
+
129
152
  // Update existing entry - preserve the existing metadata but add/update content
130
153
  allXTrefs.xtrefs[existingIndex] = {
131
- ...allXTrefs.xtrefs[existingIndex],
154
+ ...existingXtref,
132
155
  content: xrefTerm.content, // Update the content from fetched HTML
156
+ classes: xrefTerm.classes || [], // Update classes from dt element
133
157
  source: xrefTerm.source, // Add source field
134
158
  termId: xrefTerm.termId, // Add termId if not present
135
159
  lastUpdated: new Date().toISOString()
136
160
  };
161
+
162
+ // Check if this is a tref to an external tref (nested tref)
163
+ // A term with 'term-external' class means it's transcluded from another spec
164
+ const isExternalTref = xrefTerm.classes && xrefTerm.classes.includes('term-external');
165
+ const isTref = existingXtref.sourceFiles && existingXtref.sourceFiles.some(sf => sf.type === 'tref');
166
+ const isXref = existingXtref.sourceFiles && existingXtref.sourceFiles.some(sf => sf.type === 'xref');
167
+
168
+ if (isExternalTref && isTref) {
169
+ // Build a readable list of source files for the error message
170
+ const sourceFilesList = existingXtref.sourceFile
171
+ ? existingXtref.sourceFile
172
+ : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
173
+
174
+ // Construct the external repository URL
175
+ const externalRepoUrl = existingXtref.ghPageUrl || existingXtref.repoUrl || `https://github.com/${existingXtref.owner}/${existingXtref.repo}`;
176
+
177
+ Logger.error(`NESTED TREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec}`, {
178
+ context: `Origin: ${sourceFilesList} - This term is itself transcluded from another spec`,
179
+ hint: 'Avoid chaining external references (tref → tref). Reference the original source spec directly, or define the term locally in your spec',
180
+ details: `This creates a complex dependency chain. Repository: ${externalRepoUrl}`
181
+ });
182
+ }
183
+
184
+ if (isExternalTref && isXref) {
185
+ // Build a readable list of source files for the warning message
186
+ const sourceFilesList = existingXtref.sourceFile
187
+ ? existingXtref.sourceFile
188
+ : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
189
+
190
+ // Construct the external repository URL
191
+ const externalRepoUrl = existingXtref.ghPageUrl || existingXtref.repoUrl || `https://github.com/${existingXtref.owner}/${existingXtref.repo}`;
192
+
193
+ Logger.error(`NESTED XREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec}`, {
194
+ context: `Origin: ${sourceFilesList} - This xref points to a term that is already transcluded from elsewhere`,
195
+ hint: 'Use [[xref]] only for terms directly defined in the external spec. For nested references, either reference the original source or define the term locally',
196
+ details: `This creates a chain of external references. Repository: ${externalRepoUrl}`
197
+ });
198
+ }
199
+
200
+ matchedCount++;
137
201
  } else {
138
- // Add new entry (this term wasn't referenced in the markdown)
139
- allXTrefs.xtrefs.push({
140
- ...xrefTerm,
141
- lastUpdated: new Date().toISOString()
142
- });
202
+ // Skip terms that are not referenced in the local markdown files
203
+ // This prevents bloating the xtrefs-data.json with unreferenced terms
204
+ skippedCount++;
143
205
  }
144
206
  });
145
207
 
@@ -150,10 +212,14 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
150
212
  const stringReadyForFileWrite = `const allXTrefs = ${allXTrefsStr};`;
151
213
  fs.writeFileSync(outputPathJS, stringReadyForFileWrite, 'utf8');
152
214
 
153
- Logger.success(`Merged ${xrefTerms.length} xref terms into allXTrefs. Total entries: ${allXTrefs.xtrefs.length}`);
215
+ Logger.success(`Merged xref terms: ${matchedCount} matched, ${skippedCount} skipped (not referenced). Total entries: ${allXTrefs.xtrefs.length}`);
154
216
 
155
217
  } catch (error) {
156
- Logger.error('Error merging xref terms into allXTrefs:', error.message);
218
+ Logger.error('Error merging xref terms into allXTrefs', {
219
+ context: 'Failed while merging external reference terms into the xtrefs cache',
220
+ hint: 'This may indicate corrupted cache files in .cache/xtrefs/. Try deleting the .cache directory and running the build again',
221
+ details: error.message
222
+ });
157
223
  }
158
224
  }
159
225
 
@@ -181,18 +247,18 @@ function extractTermsFromHtml(externalSpec, html) {
181
247
  batch.each((index, termElement) => {
182
248
  try {
183
249
  const $termElement = $(termElement);
184
-
250
+
185
251
  // The id can be on the dt element itself OR on span(s) inside it
186
252
  // Some terms have multiple nested spans with different IDs (e.g., aliases)
187
253
  // We need to extract ALL term IDs from the dt element
188
254
  const termIds = [];
189
-
255
+
190
256
  // First check if dt itself has an id
191
257
  const dtId = $termElement.attr('id');
192
258
  if (dtId && dtId.includes('term:')) {
193
259
  termIds.push(dtId.replace('term:', ''));
194
260
  }
195
-
261
+
196
262
  // Then find all spans with ids that contain 'term:'
197
263
  $termElement.find('span[id*="term:"]').each((i, span) => {
198
264
  const spanId = $(span).attr('id');
@@ -203,17 +269,23 @@ function extractTermsFromHtml(externalSpec, html) {
203
269
  }
204
270
  }
205
271
  });
206
-
272
+
207
273
  // Skip if no valid term IDs found
208
274
  if (termIds.length === 0) {
209
275
  return;
210
276
  }
211
-
277
+
278
+ // Extract classes from the <dt> element to determine if it's a local or external term.
279
+ // This helps identify if a tref to an external resource is itself a tref (term-external).
280
+ const dtClasses = $termElement.attr('class');
281
+ const classArray = dtClasses ? dtClasses.split(/\s+/).filter(Boolean) : [];
282
+ const termClasses = classArray.filter(cls => cls === 'term-local' || cls === 'term-external');
283
+
212
284
  const dd = $termElement.next('dd');
213
285
 
214
286
  if (dd.length > 0) {
215
287
  const ddContent = $.html(dd); // Store the complete DD content once
216
-
288
+
217
289
  // Create a term object for each ID found in this dt element
218
290
  // This handles cases where one term definition has multiple aliases
219
291
  termIds.forEach(termName => {
@@ -221,16 +293,21 @@ function extractTermsFromHtml(externalSpec, html) {
221
293
  externalSpec: externalSpec,
222
294
  term: termName,
223
295
  content: ddContent,
296
+ classes: termClasses, // CSS classes from dt element (term-local or term-external)
224
297
  // Add metadata for consistency with tref structure
225
298
  source: 'xref', // Distinguish from tref entries
226
- termId: `term:${externalSpec}:${termName}`, // Fully qualified term ID
299
+ termId: `term:${termName}`, // Term ID matches the actual HTML anchor format
227
300
  };
228
301
 
229
302
  terms.push(termObj);
230
303
  });
231
304
  }
232
305
  } catch (termError) {
233
- Logger.warn(`Error processing term in ${externalSpec}:`, termError.message);
306
+ Logger.warn(`Error processing term in ${externalSpec}`, {
307
+ context: 'Failed to extract a specific term from external spec HTML',
308
+ hint: 'The external spec may have malformed HTML or non-standard term definitions. Contact the spec maintainer if this persists',
309
+ details: termError.message
310
+ });
234
311
  }
235
312
  });
236
313
 
@@ -244,7 +321,11 @@ function extractTermsFromHtml(externalSpec, html) {
244
321
  return terms;
245
322
 
246
323
  } catch (error) {
247
- Logger.error(`Error extracting terms from external spec '${externalSpec}':`, error.message);
324
+ Logger.error(`Error extracting terms from external spec '${externalSpec}'`, {
325
+ context: 'Failed while parsing HTML from external spec to extract term definitions',
326
+ hint: 'Verify that the external spec is a valid spec-up-t generated document with a proper terms-and-definitions section',
327
+ details: error.message
328
+ });
248
329
  return [];
249
330
  }
250
331
  }
@@ -12,6 +12,16 @@ const Logger = require('../../utils/logger');
12
12
 
13
13
  const CACHE_DIR = getPath('githubcache');
14
14
 
15
+ /**
16
+ * Retrieves the latest commit hash for a specific file in a GitHub repository.
17
+ *
18
+ * @param {string} token - GitHub API token for authentication
19
+ * @param {string} owner - Repository owner
20
+ * @param {string} repo - Repository name
21
+ * @param {string} filePath - Path to the file within the repository
22
+ * @param {Object} headers - HTTP headers for the request
23
+ * @returns {Promise<string|null>} The commit SHA or null if not found
24
+ */
15
25
  async function getFileCommitHash(token, owner, repo, filePath, headers) {
16
26
  try {
17
27
  const normalizedPath = filePath.replace(/^\//, '');
@@ -31,6 +41,27 @@ async function getFileCommitHash(token, owner, repo, filePath, headers) {
31
41
  }
32
42
  }
33
43
 
44
+ /**
45
+ * Fetches all term definitions from an external specification repository.
46
+ * Extracts terms from the generated index.html file and includes CSS classes
47
+ * to identify whether terms are local definitions or external references.
48
+ *
49
+ * @param {string} token - GitHub API token for authentication
50
+ * @param {string} owner - Repository owner
51
+ * @param {string} repo - Repository name
52
+ * @param {Object} [options={}] - Additional options
53
+ * @param {string} [options.ghPageUrl] - GitHub Pages URL for the repository
54
+ * @returns {Promise<Object|null>} Object containing:
55
+ * - {number} timestamp - Unix timestamp when terms were fetched
56
+ * - {string} repository - Full repository path (owner/repo)
57
+ * - {Array<Object>} terms - Array of term objects, each containing:
58
+ * - {string} term - The term identifier
59
+ * - {string} definition - HTML definition content
60
+ * - {Array<string>} classes - CSS classes from the dt element ('term-local' or 'term-external')
61
+ * - {string|null} sha - Commit hash
62
+ * - {string|null} avatarUrl - Avatar URL
63
+ * - {string} outputFileName - Name of the cached file
64
+ */
34
65
  async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
35
66
  try {
36
67
  const headers = token ? { Authorization: `token ${token}` } : {};
@@ -118,6 +149,12 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
118
149
  return;
119
150
  }
120
151
 
152
+ // Extract classes from the <dt> element to determine if it's a local or external term.
153
+ // This helps identify if a tref to an external resource is itself a tref (term-external)
154
+ // or a local definition (term-local).
155
+ const dtClasses = dt.className ? dt.className.split(/\s+/).filter(Boolean) : [];
156
+ const termClasses = dtClasses.filter(cls => cls === 'term-local' || cls === 'term-external');
157
+
121
158
  const definitions = [];
122
159
  let pointer = dt.nextElementSibling;
123
160
  while (pointer && pointer.tagName.toLowerCase() === 'dd') {
@@ -125,7 +162,11 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
125
162
  pointer = pointer.nextElementSibling;
126
163
  }
127
164
 
128
- terms.push({ term: termText, definition: definitions.join('\n') });
165
+ terms.push({
166
+ term: termText,
167
+ definition: definitions.join('\n'),
168
+ classes: termClasses
169
+ });
129
170
  });
130
171
 
131
172
  const timestamp = Date.now();
@@ -163,6 +204,25 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
163
204
  }
164
205
  }
165
206
 
207
+ /**
208
+ * Fetches a specific term definition from an external specification repository.
209
+ * This is a convenience wrapper around fetchAllTermsFromIndex that returns
210
+ * only a single matching term.
211
+ *
212
+ * @param {string} token - GitHub API token for authentication
213
+ * @param {string} term - The specific term to fetch
214
+ * @param {string} owner - Repository owner
215
+ * @param {string} repo - Repository name
216
+ * @param {string} termsDir - Terms directory (currently unused but kept for compatibility)
217
+ * @param {Object} [options={}] - Additional options
218
+ * @param {string} [options.ghPageUrl] - GitHub Pages URL for the repository
219
+ * @returns {Promise<Object|null>} Object containing:
220
+ * - {string} term - The term identifier
221
+ * - {string} content - HTML definition content
222
+ * - {Array<string>} classes - CSS classes ('term-local' or 'term-external')
223
+ * - {string|null} sha - Commit hash
224
+ * - {Object} repository - Repository metadata
225
+ */
166
226
  async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options = {}) {
167
227
  const allTermsData = await fetchAllTermsFromIndex(token, owner, repo, options);
168
228
  if (!allTermsData || !Array.isArray(allTermsData.terms)) {
@@ -179,6 +239,7 @@ async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options =
179
239
  return {
180
240
  term: foundTerm.term,
181
241
  content: foundTerm.definition,
242
+ classes: foundTerm.classes || [],
182
243
  sha: allTermsData.sha,
183
244
  repository: {
184
245
  owner: {
@@ -14,7 +14,11 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
14
14
 
15
15
  allXTrefs.xtrefs = allXTrefs.xtrefs.filter(xtref => {
16
16
  if (!xtref.owner || !xtref.repo || !xtref.repoUrl) {
17
- Logger.error(`Removing incomplete reference: ${xtref.externalSpec}, ${xtref.term}`);
17
+ Logger.error(`Removing incomplete reference: ${xtref.externalSpec}, ${xtref.term}`, {
18
+ context: 'External reference is missing required repository information',
19
+ hint: 'Verify external_specs configuration in specs.json includes github_repo_url for each entry',
20
+ details: `Missing: ${!xtref.owner ? 'owner' : ''} ${!xtref.repo ? 'repo' : ''} ${!xtref.repoUrl ? 'repoUrl' : ''}`
21
+ });
18
22
  return false;
19
23
  }
20
24
  return true;
@@ -51,7 +55,11 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
51
55
  );
52
56
 
53
57
  if (!allTermsData) {
54
- Logger.error(`Could not fetch terms from repository ${repoKey} (${repoUrl})`);
58
+ Logger.error(`Could not fetch terms from repository ${repoKey}`, {
59
+ context: `Failed to retrieve terminology from ${repoUrl}`,
60
+ hint: 'Ensure the repository exists, is accessible, and has published its spec. If it\'s private, set GITHUB_PAT environment variable',
61
+ details: `Repository: ${repoUrl}. Check if GitHub Pages is enabled or if the repo has a valid specs.json`
62
+ });
55
63
  repoGroup.xtrefs.forEach(xtref => {
56
64
  xtref.commitHash = 'not found';
57
65
  xtref.content = 'This term was not found in the external repository.';
@@ -69,12 +77,54 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
69
77
  xtref.commitHash = allTermsData.sha;
70
78
  xtref.content = foundTerm.definition;
71
79
  xtref.avatarUrl = allTermsData.avatarUrl;
80
+ // Copy the classes array from the foundTerm to identify if this is a local or external term.
81
+ // This helps determine if a tref to an external resource is itself a tref (term-external).
82
+ xtref.classes = foundTerm.classes || [];
83
+
84
+ // Check if this is a tref to an external tref (nested tref)
85
+ // A term with 'term-external' class means it's transcluded from another spec
86
+ const isExternalTref = foundTerm.classes && foundTerm.classes.includes('term-external');
87
+ const isTref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'tref');
88
+ const isXref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'xref');
89
+
90
+ if (isExternalTref && isTref) {
91
+ // Build a readable list of source files for the error message
92
+ const sourceFilesList = xtref.sourceFile
93
+ ? xtref.sourceFile
94
+ : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
95
+
96
+ // Construct the external repository URL
97
+ const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
98
+
99
+ Logger.error(`NESTED TREF DETECTED: Term "${xtref.term}" in ${xtref.externalSpec}`, {
100
+ context: `Origin: ${sourceFilesList} - This term is itself a tref transcluded from another spec`,
101
+ hint: 'Avoid chaining trefs (tref → tref). Either reference the original source spec directly, or define the term locally',
102
+ details: `Repository: ${externalRepoUrl}. Nested trefs create complex dependency chains`
103
+ });
104
+ }
105
+
106
+ if (isExternalTref && isXref) {
107
+ // Build a readable list of source files for the warning message
108
+ const sourceFilesList = xtref.sourceFile
109
+ ? xtref.sourceFile
110
+ : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
111
+
112
+ // Construct the external repository URL
113
+ const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
114
+
115
+ Logger.error(`NESTED XREF DETECTED: Term "${xtref.term}" in ${xtref.externalSpec}`, {
116
+ context: `Origin: ${sourceFilesList} - This xref points to a term that is already transcluded elsewhere`,
117
+ hint: 'Use [[xref]] only for terms directly defined in the external spec. For nested refs, reference the original source',
118
+ details: `Repository: ${externalRepoUrl}. This creates a chain of external references`
119
+ });
120
+ }
121
+
72
122
  Logger.success(`Match found for term: ${xtref.term} in ${xtref.externalSpec}`);
73
123
  } else {
74
124
  xtref.commitHash = 'not found';
75
125
  xtref.content = 'This term was not found in the external repository.';
76
126
  xtref.avatarUrl = null;
77
-
127
+
78
128
  // Build a readable list of source files for the error message.
79
129
  // Two possible data structures exist:
80
130
  // 1. xtref.sourceFile is a STRING like "primitive.md"
@@ -85,15 +135,19 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
85
135
  // - Otherwise → extract file names from the sourceFiles array:
86
136
  // - .map(sf => sf.file) extracts just the filename from each object
87
137
  // - .join(', ') combines them into a comma-separated string
88
- const sourceFilesList = xtref.sourceFile
89
- ? xtref.sourceFile
138
+ const sourceFilesList = xtref.sourceFile
139
+ ? xtref.sourceFile
90
140
  : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
91
141
 
92
142
  // Prefer an explicit repo URL if provided on the xtref, otherwise
93
143
  // build a standard GitHub URL from the owner/repo.
94
144
  const githubUrl = xtref.repoUrl || `https://github.com/${repoKey}`;
95
145
 
96
- Logger.error(`Origin: ${sourceFilesList} 👉 No match found for term: ${xtref.term} in ${xtref.externalSpec} (${githubUrl})`);
146
+ Logger.error(`No match found for term: ${xtref.term} in ${xtref.externalSpec}`, {
147
+ context: `Origin: ${sourceFilesList} - Term not found in external repository`,
148
+ hint: 'Check if the term exists in the external spec. Verify spelling, ensure the external spec has published, and confirm the term is in their terminology section',
149
+ details: `Repository: ${githubUrl}. The term may have been renamed or removed`
150
+ });
97
151
  }
98
152
  }
99
153
 
@@ -104,10 +158,14 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
104
158
  const allXTrefsStr = JSON.stringify(allXTrefs, null, 2);
105
159
  fs.writeFileSync(outputPathJSON, allXTrefsStr, 'utf8');
106
160
  const jsPayload = `const allXTrefs = ${allXTrefsStr};`;
107
- fs.writeFileSync(outputPathJS, jsPayload, 'utf8');
108
- fs.writeFileSync(outputPathJSTimeStamped, jsPayload, 'utf8');
161
+ fs.writeFileSync(outputPathJS, jsPayload, 'utf8');
162
+ fs.writeFileSync(outputPathJSTimeStamped, jsPayload, 'utf8');
109
163
  } catch (error) {
110
- Logger.error('An error occurred:', error);
164
+ Logger.error('An error occurred during xtrefs processing', {
165
+ context: 'Failed while processing external references and fetching terms',
166
+ hint: 'Check your internet connection, verify GITHUB_PAT is set if needed, and ensure specs.json external_specs configuration is correct',
167
+ details: error.message
168
+ });
111
169
  }
112
170
  }
113
171
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const { externalReferences, utils } = require('../../utils/regex-patterns');
10
+ const Logger = require('../../utils/logger');
10
11
 
11
12
  /**
12
13
  * Checks if a specific xtref is present in the markdown content.
@@ -43,13 +44,30 @@ function isXTrefInAnyFile(xtref, fileContents) {
43
44
  * @param {object} xtrefObject - Pre-parsed xtref object from template-tag-parser
44
45
  * @param {{ xtrefs: Array<object> }} allXTrefs - Aggregated reference collection
45
46
  * @param {string|null} filename - Originating filename for bookkeeping
47
+ * @param {Array<object>|null} externalSpecs - Array of external_specs from specs.json for validation
46
48
  * @returns {{ xtrefs: Array<object> }} Updated reference collection
47
49
  */
48
- function addXtrefToCollection(xtrefObject, allXTrefs, filename = null) {
50
+ function addXtrefToCollection(xtrefObject, allXTrefs, filename = null, externalSpecs = null) {
49
51
  const referenceType = xtrefObject.referenceType;
50
52
  const cleanXTrefObj = { ...xtrefObject };
51
53
  delete cleanXTrefObj.referenceType;
52
54
 
55
+ // Validate that the external spec exists in specs.json configuration
56
+ if (externalSpecs && Array.isArray(externalSpecs)) {
57
+ const externalSpecExists = externalSpecs.some(
58
+ spec => spec.external_spec === cleanXTrefObj.externalSpec
59
+ );
60
+
61
+ if (!externalSpecExists) {
62
+ const availableSpecs = externalSpecs.map(s => s.external_spec).join(', ');
63
+ Logger.error(
64
+ `External spec "${cleanXTrefObj.externalSpec}" not found in specs.json configuration. ` +
65
+ `Available external specs: ${availableSpecs}. ` +
66
+ `Check [[${referenceType}: ${cleanXTrefObj.externalSpec}, ${cleanXTrefObj.term}]] in ${filename || 'unknown file'}`
67
+ );
68
+ }
69
+ }
70
+
53
71
  const existingIndex = allXTrefs?.xtrefs?.findIndex(existingXTref =>
54
72
  existingXTref.term === cleanXTrefObj.term &&
55
73
  existingXTref.externalSpec === cleanXTrefObj.externalSpec
@@ -135,9 +153,10 @@ function addXtrefToCollection(xtrefObject, allXTrefs, filename = null) {
135
153
  * @param {{ xtrefs: Array<object> }} allXTrefs - Aggregated reference collection.
136
154
  * @param {string|null} filename - Originating filename for bookkeeping.
137
155
  * @param {function} processXTrefObject - Parsing function for xtref strings.
156
+ * @param {Array<object>|null} externalSpecs - Array of external_specs from specs.json for validation.
138
157
  * @returns {{ xtrefs: Array<object> }} Updated reference collection.
139
158
  */
140
- function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, processXTrefObject) {
159
+ function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, processXTrefObject, externalSpecs = null) {
141
160
  if (!processXTrefObject) {
142
161
  throw new Error('processXTrefObject function is required. Import from template-tag-parser.');
143
162
  }
@@ -152,7 +171,7 @@ function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, p
152
171
 
153
172
  xtrefs.forEach(rawXtref => {
154
173
  const xtrefObject = processXTrefObject(rawXtref);
155
- addXtrefToCollection(xtrefObject, allXTrefs, filename);
174
+ addXtrefToCollection(xtrefObject, allXTrefs, filename, externalSpecs);
156
175
  });
157
176
 
158
177
  return allXTrefs;
@@ -111,7 +111,6 @@ async function render(spec, assets, sharedVars, config, template, assetsGlobal,
111
111
  assetsHead: assets.head,
112
112
  assetsBody: assets.body,
113
113
  assetsSvg: assets.svg,
114
- features: Object.keys(features).join(' '),
115
114
  externalReferences: '', // No longer inject DOM HTML - xrefs are in allXTrefs
116
115
  xtrefsData: createScriptElementWithXTrefDataForEmbeddingInHtml(),
117
116
  specLogo: spec.logo,