@ankhorage/paradox 0.1.27 → 0.2.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 (60) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +53 -61
  3. package/dist/analyze/analyze.d.ts +2 -2
  4. package/dist/analyze/analyze.js +51 -35
  5. package/dist/analyze/badges.d.ts +2 -1
  6. package/dist/analyze/badges.js +14 -4
  7. package/dist/analyze/components.js +12 -11
  8. package/dist/analyze/documentation/collectDocumentationCommentsAsync.d.ts +11 -0
  9. package/dist/analyze/documentation/collectDocumentationCommentsAsync.js +72 -0
  10. package/dist/analyze/documentation/findings.d.ts +5 -0
  11. package/dist/analyze/documentation/findings.js +17 -0
  12. package/dist/analyze/documentation/validateDocumentationPolicyAsync.d.ts +13 -0
  13. package/dist/analyze/documentation/validateDocumentationPolicyAsync.js +150 -0
  14. package/dist/analyze/documentation/validateReferencesAsync.d.ts +11 -0
  15. package/dist/analyze/documentation/validateReferencesAsync.js +148 -0
  16. package/dist/analyze/exports.d.ts +4 -0
  17. package/dist/analyze/exports.js +17 -15
  18. package/dist/analyze/modules.js +3 -8
  19. package/dist/analyze/readmeConfig.d.ts +1 -3
  20. package/dist/analyze/readmeConfig.js +8 -19
  21. package/dist/analyze/readmeUsage.d.ts +7 -10
  22. package/dist/analyze/readmeUsage.js +124 -48
  23. package/dist/analyze/semantic/docBlocks.js +18 -48
  24. package/dist/analyze/semantic/exports.js +3 -7
  25. package/dist/analyze/semantic/model.d.ts +0 -2
  26. package/dist/analyze/semantic/paradoxComment.d.ts +1 -11
  27. package/dist/analyze/semantic/paradoxComment.js +1 -43
  28. package/dist/analyze/semantic/tagRegistry.js +2 -1
  29. package/dist/analyze/sequenceScenarios.js +2 -4
  30. package/dist/analyze/sourceFunctions.js +4 -4
  31. package/dist/analyze/types.d.ts +31 -24
  32. package/dist/analyze/usage.d.ts +2 -2
  33. package/dist/analyze/usage.js +3 -33
  34. package/dist/analyze/utils/getExportMetadata.js +11 -40
  35. package/dist/analyze/utils/parseParadoxComment.d.ts +12 -9
  36. package/dist/analyze/utils/parseParadoxComment.js +66 -78
  37. package/dist/cli/index.d.ts +3 -2
  38. package/dist/cli/index.js +3 -2
  39. package/dist/cli/standalone.js +11 -0
  40. package/dist/config/defineParadoxConfig.d.ts +1 -1
  41. package/dist/doc-tags/registry.d.ts +28 -32
  42. package/dist/doc-tags/registry.js +35 -39
  43. package/dist/index.d.ts +1 -1
  44. package/dist/model/buildModel.d.ts +26 -19
  45. package/dist/model/buildModel.js +33 -99
  46. package/dist/model/types.d.ts +27 -20
  47. package/dist/paths/policy.d.ts +1 -1
  48. package/dist/render/renderers/diagrams.js +3 -11
  49. package/dist/render/renderers/html.js +89 -58
  50. package/dist/render/renderers/markdown.js +139 -86
  51. package/dist/render/toFileStem.d.ts +2 -0
  52. package/dist/render/toFileStem.js +8 -0
  53. package/dist/{config/types.d.ts → types/config.d.ts} +3 -5
  54. package/dist/write/write.d.ts +1 -1
  55. package/package.json +2 -1
  56. package/dist/analyze/readmeCli.d.ts +0 -9
  57. package/dist/analyze/readmeCli.js +0 -33
  58. package/dist/analyze/utils/getLeadingParadoxComment.d.ts +0 -10
  59. package/dist/analyze/utils/getLeadingParadoxComment.js +0 -16
  60. /package/dist/{config/types.js → types/config.js} +0 -0
@@ -0,0 +1,150 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { DOCUMENTATION_POLICY } from '@ankhorage/policy/documentation';
4
+ import { Node } from 'ts-morph';
5
+ import { getParadoxComment } from '../utils/getParadoxComment.js';
6
+ import { parseParadoxComment } from '../utils/parseParadoxComment.js';
7
+ import { createDocumentationFinding } from './findings.js';
8
+ import { validateReferencesAsync } from './validateReferencesAsync.js';
9
+ /***
10
+ * Evaluates package documentation evidence against the canonical Ankhorage documentation policy.
11
+ */
12
+ export async function validateDocumentationPolicyAsync(options) {
13
+ return [
14
+ ...validateCommentRules(options.comments),
15
+ ...validateUsageRules(options.comments),
16
+ ...(await validateConfigRulesAsync(options.root, options.project)),
17
+ ...validatePublicApiRules(options.exports),
18
+ ...(await validateReferencesAsync(options.root, options.project, options.comments, {
19
+ validateSeeUrlAsync: options.validateSeeUrlAsync,
20
+ })),
21
+ ];
22
+ }
23
+ /***
24
+ * Validates generic comment syntax rules shared by all documentation contexts.
25
+ */
26
+ function validateCommentRules(comments) {
27
+ return comments.flatMap((comment) => {
28
+ const unsupported = comment.parsed.unsupportedTags.map((tag) => finding('documentation.comment.tag.unsupported', `Unsupported Paradox tag: @${tag}`, comment));
29
+ const code = comment.parsed.hasCodeBlock
30
+ ? [
31
+ finding('documentation.comment.code-block', 'Paradox comments must not contain code blocks.', comment),
32
+ ]
33
+ : [];
34
+ const configLocation = comment.parsed.isConfig && comment.sourcePath !== DOCUMENTATION_POLICY.config.path
35
+ ? [
36
+ finding('documentation.config.location', `@config is allowed only in ${DOCUMENTATION_POLICY.config.path}.`, comment),
37
+ ]
38
+ : [];
39
+ return [...unsupported, ...code, ...configLocation];
40
+ });
41
+ }
42
+ /***
43
+ * Validates canonical usage locations and the single README-promoted programmatic example.
44
+ */
45
+ function validateUsageRules(comments) {
46
+ const usageComments = comments.filter((comment) => comment.parsed.isUsage);
47
+ const findings = usageComments.flatMap((comment) => validateUsageComment(comment));
48
+ const readmeExamples = usageComments.filter((comment) => isBelow(comment.sourcePath, DOCUMENTATION_POLICY.paths.examplesRoot) &&
49
+ comment.parsed.isReadme);
50
+ if (readmeExamples.length !== DOCUMENTATION_POLICY.readmeUsage.exactCount) {
51
+ findings.push(createDocumentationFinding('documentation.usage.readme.unique', `Expected exactly one @usage + @readme example; found ${readmeExamples.length}.`));
52
+ }
53
+ for (const comment of readmeExamples) {
54
+ if (!hasExactlyOneRequiredTag(comment, DOCUMENTATION_POLICY.readmeUsage.requiredTags) ||
55
+ comment.parsed.description === null) {
56
+ findings.push(finding('documentation.usage.readme.metadata', 'README usage requires non-empty @title and prose.', comment));
57
+ }
58
+ }
59
+ return findings;
60
+ }
61
+ /***
62
+ * Validates one usage comment location and forbidden CLI README promotion.
63
+ */
64
+ function validateUsageComment(comment) {
65
+ const allowed = DOCUMENTATION_POLICY.paths.usageRoots.some((root) => isBelow(comment.sourcePath, root));
66
+ const location = allowed
67
+ ? []
68
+ : [
69
+ finding('documentation.usage.location', '@usage is allowed only below examples/** or src/cli/**.', comment),
70
+ ];
71
+ const cliReadme = isBelow(comment.sourcePath, DOCUMENTATION_POLICY.paths.cliRoot) && comment.parsed.isReadme
72
+ ? [
73
+ finding('documentation.usage.readme.cli', '@usage + @readme is not allowed below src/cli/**.', comment),
74
+ ]
75
+ : [];
76
+ return [...location, ...cliReadme];
77
+ }
78
+ /***
79
+ * Validates the canonical configuration file and its one README configuration root.
80
+ */
81
+ async function validateConfigRulesAsync(root, project) {
82
+ const configPath = join(root, DOCUMENTATION_POLICY.config.path);
83
+ if (!(await fileExistsAsync(configPath))) {
84
+ return [
85
+ createDocumentationFinding('documentation.config.file', `Missing canonical config schema: ${DOCUMENTATION_POLICY.config.path}`),
86
+ ];
87
+ }
88
+ const sourceFile = project.getSourceFile(configPath) ?? project.addSourceFileAtPath(configPath);
89
+ const roots = sourceFile.getStatements().flatMap((statement) => {
90
+ if (!Node.isInterfaceDeclaration(statement) && !Node.isTypeAliasDeclaration(statement)) {
91
+ return [];
92
+ }
93
+ const raw = getParadoxComment(statement);
94
+ if (raw === null)
95
+ return [];
96
+ const parsed = parseParadoxComment(raw);
97
+ return parsed.isConfig && parsed.isReadme ? [{ statement, parsed }] : [];
98
+ });
99
+ const findings = [];
100
+ if (roots.length !== DOCUMENTATION_POLICY.config.exactCount) {
101
+ findings.push(createDocumentationFinding('documentation.config.readme.unique', `Expected exactly one @config + @readme root in ${DOCUMENTATION_POLICY.config.path}; found ${roots.length}.`, DOCUMENTATION_POLICY.config.path));
102
+ }
103
+ for (const rootEntry of roots) {
104
+ if (!hasExactlyOneRequiredTag(rootEntry, DOCUMENTATION_POLICY.config.requiredTags) ||
105
+ rootEntry.parsed.description === null) {
106
+ findings.push(createDocumentationFinding('documentation.config.readme.metadata', 'README configuration requires non-empty @title and prose.', DOCUMENTATION_POLICY.config.path, rootEntry.statement.getStartLineNumber()));
107
+ }
108
+ }
109
+ return findings;
110
+ }
111
+ /***
112
+ * Warns when a public callable export has no Paradox description.
113
+ */
114
+ function validatePublicApiRules(exports) {
115
+ return exports.flatMap((entry) => entry.signatures.length > 0 && entry.description === null
116
+ ? [
117
+ createDocumentationFinding('documentation.public-function.description', `Public function ${entry.name} has no Paradox description.`, entry.sourceLocation.filePath, entry.sourceLocation.line),
118
+ ]
119
+ : []);
120
+ }
121
+ /***
122
+ * Checks that each policy-required tag appears exactly once on one parsed documentation item.
123
+ */
124
+ function hasExactlyOneRequiredTag(entry, requiredTags) {
125
+ return requiredTags.every((name) => entry.parsed.tags.filter((tag) => tag.name === name).length === 1);
126
+ }
127
+ /***
128
+ * Creates a finding at one collected comment location.
129
+ */
130
+ function finding(ruleId, message, comment) {
131
+ return createDocumentationFinding(ruleId, message, comment.sourcePath, comment.line);
132
+ }
133
+ /***
134
+ * Checks whether a source path is contained in one canonical root.
135
+ */
136
+ function isBelow(sourcePath, root) {
137
+ return sourcePath === root || sourcePath.startsWith(`${root}/`);
138
+ }
139
+ /***
140
+ * Checks whether a required canonical file exists.
141
+ */
142
+ async function fileExistsAsync(path) {
143
+ try {
144
+ await access(path);
145
+ return true;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
@@ -0,0 +1,11 @@
1
+ import { type Project } from 'ts-morph';
2
+ import type { AnalysisDocumentationFinding } from '../types.js';
3
+ import type { CollectedDocumentationComment } from './collectDocumentationCommentsAsync.js';
4
+ type SeeUrlValidator = (url: string) => Promise<unknown>;
5
+ /***
6
+ * Validates external and executable references carried by documentation comments.
7
+ */
8
+ export declare function validateReferencesAsync(root: string, project: Project, comments: readonly CollectedDocumentationComment[], options?: {
9
+ validateSeeUrlAsync?: SeeUrlValidator;
10
+ }): Promise<AnalysisDocumentationFinding[]>;
11
+ export {};
@@ -0,0 +1,148 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { DOCUMENTATION_POLICY } from '@ankhorage/policy/documentation';
4
+ import { validatePublicHttpsUrlAsync } from '@ankhorage/utility/node/http';
5
+ import { Node } from 'ts-morph';
6
+ import { createDocumentationFinding } from './findings.js';
7
+ /***
8
+ * Validates external and executable references carried by documentation comments.
9
+ */
10
+ export async function validateReferencesAsync(root, project, comments, options = {}) {
11
+ const validateSeeUrlAsync = options.validateSeeUrlAsync ?? validatePublicHttpsUrlAsync;
12
+ const findings = await Promise.all(comments.map(async (comment) => [
13
+ ...(await validateSeeReferencesAsync(comment, validateSeeUrlAsync)),
14
+ ...(await validateSecurityReferencesAsync(root, project, comment)),
15
+ ]));
16
+ return findings.flat();
17
+ }
18
+ /***
19
+ * Validates every @see value through syntax policy and the hardened public HTTPS utility.
20
+ */
21
+ async function validateSeeReferencesAsync(comment, validateSeeUrlAsync) {
22
+ const seeTags = comment.parsed.tags.filter((tag) => tag.name === 'see');
23
+ const findings = await Promise.all(seeTags.map(async (tag) => {
24
+ const url = parseSeeUrl(tag.value);
25
+ if (url === null) {
26
+ return [
27
+ referenceFinding('documentation.see.value', '@see requires a public HTTPS URL without credentials.', comment),
28
+ ];
29
+ }
30
+ try {
31
+ await validateSeeUrlAsync(url);
32
+ return [];
33
+ }
34
+ catch (error) {
35
+ return [
36
+ referenceFinding('documentation.see.reachable', `@see target is not safely reachable: ${readErrorMessage(error)}`, comment),
37
+ ];
38
+ }
39
+ }));
40
+ return findings.flat();
41
+ }
42
+ /***
43
+ * Parses one @see value according to the policy-owned URL syntax contract.
44
+ */
45
+ function parseSeeUrl(value) {
46
+ if (value === null)
47
+ return null;
48
+ try {
49
+ const url = new URL(value);
50
+ if (url.protocol !== DOCUMENTATION_POLICY.see.protocol)
51
+ return null;
52
+ if (url.username || url.password || url.hostname.length === 0)
53
+ return null;
54
+ return url.toString();
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /***
61
+ * Validates every @security reference against one exact colocated executable test.
62
+ */
63
+ async function validateSecurityReferencesAsync(root, project, comment) {
64
+ const securityTags = comment.parsed.tags.filter((tag) => tag.name === 'security');
65
+ return (await Promise.all(securityTags.map((tag) => validateSecurityReferenceAsync(root, project, comment, tag.value)))).flat();
66
+ }
67
+ /***
68
+ * Resolves one colocated security test reference and requires exactly one matching test declaration.
69
+ */
70
+ async function validateSecurityReferenceAsync(root, project, comment, value) {
71
+ const reference = parseSecurityReference(value);
72
+ if (reference === null) {
73
+ return [securityFinding('Invalid @security test reference.', comment)];
74
+ }
75
+ const testPath = join(root, dirname(comment.sourcePath), reference.fileName);
76
+ if (!(await fileExistsAsync(testPath))) {
77
+ return [securityFinding(`Missing colocated security test: ${reference.fileName}`, comment)];
78
+ }
79
+ const sourceFile = project.getSourceFile(testPath) ?? project.addSourceFileAtPath(testPath);
80
+ const matches = sourceFile
81
+ .getDescendants()
82
+ .filter((node) => isNamedTestCall(node, reference.testName));
83
+ return matches.length === 1
84
+ ? []
85
+ : [
86
+ securityFinding(`Expected exactly one test named "${reference.testName}" in ${reference.fileName}; found ${matches.length}.`, comment),
87
+ ];
88
+ }
89
+ /***
90
+ * Parses a same-directory test filename and exact test name from an @security value.
91
+ */
92
+ function parseSecurityReference(value) {
93
+ if (value === null)
94
+ return null;
95
+ const separator = value.indexOf('#');
96
+ if (separator <= 0 || separator === value.length - 1)
97
+ return null;
98
+ const fileName = value.slice(0, separator).trim();
99
+ const testName = value.slice(separator + 1).trim();
100
+ if (basename(fileName) !== fileName || !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(fileName)) {
101
+ return null;
102
+ }
103
+ return testName.length > 0 ? { fileName, testName } : null;
104
+ }
105
+ /***
106
+ * Checks whether a ts-morph node is test()/it() with the exact static string name.
107
+ */
108
+ function isNamedTestCall(node, expectedName) {
109
+ if (!Node.isCallExpression(node))
110
+ return false;
111
+ const callee = node.getExpression().getText();
112
+ if (callee !== 'test' && callee !== 'it')
113
+ return false;
114
+ const [name] = node.getArguments();
115
+ if (!Node.isStringLiteral(name) && !Node.isNoSubstitutionTemplateLiteral(name))
116
+ return false;
117
+ return name.getLiteralText() === expectedName;
118
+ }
119
+ /***
120
+ * Creates the canonical security-reference finding at a comment location.
121
+ */
122
+ function securityFinding(message, comment) {
123
+ return referenceFinding('documentation.security.reference', message, comment);
124
+ }
125
+ /***
126
+ * Creates a reference finding at the owning comment location.
127
+ */
128
+ function referenceFinding(ruleId, message, comment) {
129
+ return createDocumentationFinding(ruleId, message, comment.sourcePath, comment.line);
130
+ }
131
+ /***
132
+ * Checks whether a referenced test file exists.
133
+ */
134
+ async function fileExistsAsync(path) {
135
+ try {
136
+ await access(path);
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ /***
144
+ * Converts an unknown thrown value into a stable diagnostic string.
145
+ */
146
+ function readErrorMessage(error) {
147
+ return error instanceof Error ? error.message : String(error);
148
+ }
@@ -4,7 +4,11 @@ interface AnalyzeExportsResult {
4
4
  exports: AnalysisExport[];
5
5
  config: {
6
6
  exportName: string;
7
+ title: string | null;
8
+ description: string | null;
7
9
  isReadme: boolean;
10
+ see: string[];
11
+ security: string[];
8
12
  } | null;
9
13
  }
10
14
  /***
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, join, normalize, relative } from 'node:path';
2
+ import { uniqueSortedStrings } from '@ankhorage/utility/array';
2
3
  import { Node } from 'ts-morph';
3
4
  import { getExportMetadata } from './utils/getExportMetadata.js';
4
5
  import { getParadoxComment } from './utils/getParadoxComment.js';
@@ -16,16 +17,19 @@ export function analyzeExports(project, options) {
16
17
  for (const symbol of exported) {
17
18
  const resolved = resolveExportSymbol(symbol);
18
19
  const decl = getFirstDeclaration(resolved.getDeclarations());
19
- if (decl === null) {
20
+ if (decl === null)
20
21
  continue;
21
- }
22
22
  const rawComment = getParadoxComment(decl);
23
23
  const parsed = rawComment ? parseParadoxComment(rawComment) : createEmptyMetadata();
24
24
  const name = resolved.getName();
25
25
  if (parsed.isConfig) {
26
26
  config = {
27
27
  exportName: name,
28
+ title: parsed.title,
29
+ description: parsed.description,
28
30
  isReadme: parsed.isReadme,
31
+ see: parsed.see,
32
+ security: parsed.security,
29
33
  };
30
34
  }
31
35
  const metadata = getExportMetadata({
@@ -39,11 +43,13 @@ export function analyzeExports(project, options) {
39
43
  exportsByName.set(name, existing
40
44
  ? {
41
45
  ...existing,
46
+ title: existing.title ?? parsed.title,
42
47
  description: existing.description ?? parsed.description,
43
48
  isReadme: existing.isReadme || parsed.isReadme,
44
- examples: existing.examples.length > 0 ? existing.examples : parsed.examples,
45
- exportPaths: uniqueSorted([...existing.exportPaths, ...metadata.exportPaths]),
46
- relatedSymbols: uniqueSorted([
49
+ see: uniqueSortedStrings([...existing.see, ...parsed.see]),
50
+ security: uniqueSortedStrings([...existing.security, ...parsed.security]),
51
+ exportPaths: uniqueSortedStrings([...existing.exportPaths, ...metadata.exportPaths]),
52
+ relatedSymbols: uniqueSortedStrings([
47
53
  ...existing.relatedSymbols,
48
54
  ...metadata.relatedSymbols,
49
55
  ]),
@@ -56,9 +62,11 @@ export function analyzeExports(project, options) {
56
62
  : {
57
63
  name,
58
64
  node: decl,
65
+ title: parsed.title,
59
66
  description: parsed.description,
60
67
  isReadme: parsed.isReadme,
61
- examples: parsed.examples,
68
+ see: parsed.see,
69
+ security: parsed.security,
62
70
  kind: inferKind(decl),
63
71
  ...metadata,
64
72
  });
@@ -99,12 +107,6 @@ function inferKind(node) {
99
107
  return 'value';
100
108
  return 'unknown';
101
109
  }
102
- /***
103
- * Returns unique string values sorted for deterministic generated output.
104
- */
105
- function uniqueSorted(values) {
106
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
107
- }
108
110
  /***
109
111
  * Normalizes platform-specific path separators for generated documentation output.
110
112
  */
@@ -117,10 +119,10 @@ function toPosixPath(path) {
117
119
  function createEmptyMetadata() {
118
120
  return {
119
121
  description: null,
122
+ title: null,
120
123
  isConfig: false,
121
124
  isReadme: false,
122
- examples: [],
123
- params: {},
124
- returns: null,
125
+ see: [],
126
+ security: [],
125
127
  };
126
128
  }
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, join, normalize, relative } from 'node:path';
2
+ import { uniqueSortedStrings } from '@ankhorage/utility/array';
2
3
  /***
3
4
  * Builds a deterministic module relationship graph for documentation renderers.
4
5
  */
@@ -34,18 +35,12 @@ export function analyzeModules(project, options) {
34
35
  return {
35
36
  path,
36
37
  isEntrypoint: entrypointPaths.has(normalize(sourceFile.getFilePath())),
37
- dependencies: uniqueSorted(dependencies),
38
- exports: uniqueSorted(exports),
38
+ dependencies: uniqueSortedStrings(dependencies),
39
+ exports: uniqueSortedStrings(exports),
39
40
  };
40
41
  })
41
42
  .sort((left, right) => left.path.localeCompare(right.path));
42
43
  }
43
- /***
44
- * Returns unique string values sorted for deterministic generated output.
45
- */
46
- function uniqueSorted(values) {
47
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
48
- }
49
44
  /***
50
45
  * Normalizes platform-specific path separators for generated documentation output.
51
46
  */
@@ -1,12 +1,10 @@
1
1
  export interface AnalysisReadmeConfig {
2
- description: string | null;
3
2
  language: string;
4
3
  code: string;
5
4
  sourcePath: string;
6
5
  }
7
6
  /***
8
- * Collects a README configuration example from the actual Paradox config file when its
9
- * leading Paradox comment is marked with both @config and @readme.
7
+ * Collects the concrete Paradox configuration instance as a README configuration example.
10
8
  */
11
9
  export declare function analyzeReadmeConfig(options: {
12
10
  root: string;
@@ -1,36 +1,22 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { extname, relative } from 'node:path';
3
- import { getLeadingParadoxComment } from './utils/getLeadingParadoxComment.js';
4
3
  /***
5
- * Collects a README configuration example from the actual Paradox config file when its
6
- * leading Paradox comment is marked with both @config and @readme.
4
+ * Collects the concrete Paradox configuration instance as a README configuration example.
7
5
  */
8
6
  export async function analyzeReadmeConfig(options) {
9
7
  if (options.configFilePath === null)
10
8
  return null;
11
9
  const source = await readFile(options.configFilePath, 'utf-8');
12
- const comment = getLeadingParadoxComment(source);
13
- if (comment === null)
14
- return null;
15
- if (!comment.parsed.isConfig || !comment.parsed.isReadme)
16
- return null;
17
10
  const sourcePath = toPosixPath(relative(options.root, options.configFilePath));
18
11
  return {
19
- description: comment.parsed.description,
20
12
  language: getLanguage(sourcePath),
21
- code: removeRange(source, comment.start, comment.end).trim(),
13
+ code: source.trim(),
22
14
  sourcePath,
23
15
  };
24
16
  }
25
- function removeRange(source, start, end) {
26
- const before = source.slice(0, start).trimEnd();
27
- const after = source.slice(end).trimStart();
28
- if (before.length === 0)
29
- return after;
30
- if (after.length === 0)
31
- return before;
32
- return `${before}\n\n${after}`;
33
- }
17
+ /***
18
+ * Returns the Markdown fence language for a configuration source path.
19
+ */
34
20
  function getLanguage(sourcePath) {
35
21
  const extension = extname(sourcePath).toLowerCase();
36
22
  if (extension === '.ts')
@@ -39,6 +25,9 @@ function getLanguage(sourcePath) {
39
25
  return 'js';
40
26
  return '';
41
27
  }
28
+ /***
29
+ * Normalizes filesystem separators for stable documentation paths.
30
+ */
42
31
  function toPosixPath(path) {
43
32
  return path.replaceAll('\\', '/');
44
33
  }
@@ -1,14 +1,11 @@
1
- export interface AnalysisReadmeUsage {
2
- title: string | null;
3
- description: string | null;
4
- language: string;
5
- code: string;
6
- sourcePath: string;
7
- }
1
+ import type { AnalysisUsageEntry } from './types.js';
8
2
  /***
9
- * Collects README usage examples from configured real source files.
3
+ * Collects every canonical usage declaration from examples and CLI source roots.
10
4
  */
11
5
  export declare function analyzeReadmeUsage(options: {
12
6
  root: string;
13
- entrypoints: readonly string[];
14
- }): Promise<AnalysisReadmeUsage[]>;
7
+ }): Promise<AnalysisUsageEntry[]>;
8
+ /***
9
+ * Counts real example directories directly below the canonical examples root.
10
+ */
11
+ export declare function countExampleDirectoriesAsync(root: string): Promise<number>;