pasika 0.3.0 → 0.3.2

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 (166) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/index.js +32 -38
  3. package/dist/cli/output.d.ts +3 -0
  4. package/dist/cli/output.js +11 -0
  5. package/dist/enforcement/coverage.js +25 -29
  6. package/dist/enforcement/doctor.d.ts +16 -0
  7. package/dist/enforcement/doctor.js +249 -0
  8. package/dist/enforcement/types.d.ts +0 -3
  9. package/dist/enforcement/types.js +4 -13
  10. package/dist/eslint/pasika/ast-types.d.ts +122 -0
  11. package/dist/eslint/pasika/ast-types.js +1 -0
  12. package/dist/eslint/pasika/index.d.ts +58 -3
  13. package/dist/eslint/pasika/index.js +61 -1
  14. package/dist/eslint/pasika/rules/application-structure.d.ts +2 -0
  15. package/dist/eslint/pasika/rules/application-structure.js +152 -0
  16. package/dist/eslint/pasika/rules/component-conventions.d.ts +16 -0
  17. package/dist/eslint/pasika/rules/component-conventions.js +145 -0
  18. package/dist/eslint/pasika/rules/component-nesting.d.ts +12 -0
  19. package/dist/eslint/pasika/rules/component-nesting.js +69 -0
  20. package/dist/eslint/pasika/rules/config-extraction.d.ts +14 -0
  21. package/dist/eslint/pasika/rules/config-extraction.js +74 -0
  22. package/dist/eslint/pasika/rules/cross-feature-import.d.ts +2 -0
  23. package/dist/eslint/pasika/rules/cross-feature-import.js +75 -0
  24. package/dist/eslint/pasika/rules/css/apply-usage.d.ts +10 -0
  25. package/dist/eslint/pasika/rules/css/apply-usage.js +45 -0
  26. package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +10 -0
  27. package/dist/eslint/pasika/rules/css/base-layer-pair.js +49 -0
  28. package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +10 -0
  29. package/dist/eslint/pasika/rules/css/css-variable-naming.js +39 -0
  30. package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +11 -0
  31. package/dist/eslint/pasika/rules/css/custom-utility-apply.js +52 -0
  32. package/dist/eslint/pasika/rules/css/global-css-location.d.ts +10 -0
  33. package/dist/eslint/pasika/rules/css/global-css-location.js +42 -0
  34. package/dist/eslint/pasika/rules/css/helpers.d.ts +29 -0
  35. package/dist/eslint/pasika/rules/css/helpers.js +93 -0
  36. package/dist/eslint/pasika/rules/css/index.d.ts +13 -0
  37. package/dist/eslint/pasika/rules/css/index.js +25 -0
  38. package/dist/eslint/pasika/rules/css/root-variables.d.ts +11 -0
  39. package/dist/eslint/pasika/rules/css/root-variables.js +53 -0
  40. package/dist/eslint/pasika/rules/css/rule-tester.d.ts +11 -0
  41. package/dist/eslint/pasika/rules/css/rule-tester.js +22 -0
  42. package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +11 -0
  43. package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +76 -0
  44. package/dist/eslint/pasika/rules/css/surface-utility.d.ts +10 -0
  45. package/dist/eslint/pasika/rules/css/surface-utility.js +41 -0
  46. package/dist/eslint/pasika/rules/css/theme-reset.d.ts +9 -0
  47. package/dist/eslint/pasika/rules/css/theme-reset.js +36 -0
  48. package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +11 -0
  49. package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +53 -0
  50. package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +10 -0
  51. package/dist/eslint/pasika/rules/cva-appearance-props.js +77 -0
  52. package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +11 -0
  53. package/dist/eslint/pasika/rules/cva-boolean-variants.js +97 -0
  54. package/dist/eslint/pasika/rules/data-testid-case.d.ts +2 -0
  55. package/dist/eslint/pasika/rules/data-testid-case.js +76 -0
  56. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +13 -1
  57. package/dist/eslint/pasika/rules/enforce-cn-merge.js +46 -44
  58. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +53 -48
  59. package/dist/eslint/pasika/rules/filename-case.js +28 -5
  60. package/dist/eslint/pasika/rules/hook-complexity.d.ts +12 -0
  61. package/dist/eslint/pasika/rules/hook-complexity.js +114 -0
  62. package/dist/eslint/pasika/rules/hook-extraction.d.ts +11 -0
  63. package/dist/eslint/pasika/rules/hook-extraction.js +55 -0
  64. package/dist/eslint/pasika/rules/import-through-index.d.ts +2 -0
  65. package/dist/eslint/pasika/rules/import-through-index.js +52 -0
  66. package/dist/eslint/pasika/rules/interactive-component.d.ts +10 -0
  67. package/dist/eslint/pasika/rules/interactive-component.js +76 -0
  68. package/dist/eslint/pasika/rules/json/index.d.ts +5 -0
  69. package/dist/eslint/pasika/rules/json/index.js +9 -0
  70. package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +10 -0
  71. package/dist/eslint/pasika/rules/json/no-cache-flag.js +45 -0
  72. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +10 -0
  73. package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +41 -0
  74. package/dist/eslint/pasika/rules/json/rule-tester.d.ts +11 -0
  75. package/dist/eslint/pasika/rules/json/rule-tester.js +21 -0
  76. package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +9 -0
  77. package/dist/eslint/pasika/rules/jsx-hygiene.js +108 -0
  78. package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +10 -0
  79. package/dist/eslint/pasika/rules/locale-dotted-path.js +65 -0
  80. package/dist/eslint/pasika/rules/locale-placement.d.ts +13 -0
  81. package/dist/eslint/pasika/rules/locale-placement.js +163 -0
  82. package/dist/eslint/pasika/rules/locales-location.d.ts +11 -0
  83. package/dist/eslint/pasika/rules/locales-location.js +56 -0
  84. package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +5 -0
  85. package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +29 -0
  86. package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +5 -0
  87. package/dist/eslint/pasika/rules/md/example-heading-description.js +30 -0
  88. package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +2 -0
  89. package/dist/eslint/pasika/rules/md/glossary-term-linking.js +99 -0
  90. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +2 -0
  91. package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +46 -0
  92. package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +5 -0
  93. package/dist/eslint/pasika/rules/md/guide-link-anchors.js +50 -0
  94. package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +5 -0
  95. package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +51 -0
  96. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +5 -0
  97. package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +26 -0
  98. package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +5 -0
  99. package/dist/eslint/pasika/rules/md/guide-step-single-link.js +48 -0
  100. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +5 -0
  101. package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +42 -0
  102. package/dist/eslint/pasika/rules/md/helpers.d.ts +25 -0
  103. package/dist/eslint/pasika/rules/md/helpers.js +37 -0
  104. package/dist/eslint/pasika/rules/md/index.d.ts +27 -0
  105. package/dist/eslint/pasika/rules/md/index.js +53 -0
  106. package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +5 -0
  107. package/dist/eslint/pasika/rules/md/no-cross-document-link.js +36 -0
  108. package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +5 -0
  109. package/dist/eslint/pasika/rules/md/no-nested-how-to.js +39 -0
  110. package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +5 -0
  111. package/dist/eslint/pasika/rules/md/no-template-prompt.js +22 -0
  112. package/dist/eslint/pasika/rules/md/overview-length.d.ts +5 -0
  113. package/dist/eslint/pasika/rules/md/overview-length.js +47 -0
  114. package/dist/eslint/pasika/rules/md/overview-present.d.ts +5 -0
  115. package/dist/eslint/pasika/rules/md/overview-present.js +41 -0
  116. package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +5 -0
  117. package/dist/eslint/pasika/rules/md/policy-no-examples.js +28 -0
  118. package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +5 -0
  119. package/dist/eslint/pasika/rules/md/policy-single-document.js +34 -0
  120. package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +5 -0
  121. package/dist/eslint/pasika/rules/md/policy-subject-headings.js +58 -0
  122. package/dist/eslint/pasika/rules/md/project-index.d.ts +17 -0
  123. package/dist/eslint/pasika/rules/md/project-index.js +67 -0
  124. package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +6 -0
  125. package/dist/eslint/pasika/rules/md/reference-block-headings.js +31 -0
  126. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +6 -0
  127. package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +26 -0
  128. package/dist/eslint/pasika/rules/md/requirement-present.d.ts +5 -0
  129. package/dist/eslint/pasika/rules/md/requirement-present.js +41 -0
  130. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +5 -0
  131. package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +46 -0
  132. package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +5 -0
  133. package/dist/eslint/pasika/rules/md/rule-paired-examples.js +36 -0
  134. package/dist/eslint/pasika/rules/md/rule-tester.d.ts +11 -0
  135. package/dist/eslint/pasika/rules/md/rule-tester.js +21 -0
  136. package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +2 -0
  137. package/dist/eslint/pasika/rules/md/support-document-placement.js +36 -0
  138. package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +2 -0
  139. package/dist/eslint/pasika/rules/md/title-matches-file-name.js +46 -0
  140. package/dist/eslint/pasika/rules/named-exports.d.ts +2 -0
  141. package/dist/eslint/pasika/rules/named-exports.js +49 -0
  142. package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +10 -0
  143. package/dist/eslint/pasika/rules/no-eslint-disable.js +33 -0
  144. package/dist/eslint/pasika/rules/no-mixed-concerns.js +9 -22
  145. package/dist/eslint/pasika/rules/no-util-barrel.d.ts +2 -0
  146. package/dist/eslint/pasika/rules/no-util-barrel.js +48 -0
  147. package/dist/eslint/pasika/rules/pure-function-extract.d.ts +9 -0
  148. package/dist/eslint/pasika/rules/pure-function-extract.js +92 -0
  149. package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +13 -0
  150. package/dist/eslint/pasika/rules/shared-style-dedup.js +91 -0
  151. package/dist/eslint/pasika/rules/stay-flat.d.ts +12 -0
  152. package/dist/eslint/pasika/rules/stay-flat.js +81 -0
  153. package/dist/eslint/pasika/rules/support-folder-shape.d.ts +2 -0
  154. package/dist/eslint/pasika/rules/support-folder-shape.js +59 -0
  155. package/dist/eslint/pasika/rules/type-extraction.d.ts +13 -0
  156. package/dist/eslint/pasika/rules/type-extraction.js +85 -0
  157. package/dist/eslint/pasika/rules/ui-state.d.ts +9 -0
  158. package/dist/eslint/pasika/rules/ui-state.js +99 -0
  159. package/dist/eslint/pasika/rules/util-file-name.d.ts +2 -0
  160. package/dist/eslint/pasika/rules/util-file-name.js +48 -0
  161. package/dist/eslint/pasika/rules/value-extraction.d.ts +13 -0
  162. package/dist/eslint/pasika/rules/value-extraction.js +48 -0
  163. package/enforcement/registry.json +196 -190
  164. package/package.json +28 -19
  165. package/dist/enforcement/docs-check.d.ts +0 -17
  166. package/dist/enforcement/docs-check.js +0 -159
@@ -0,0 +1,39 @@
1
+ import { getFilename, getLine, getTextContent } from "./helpers.js";
2
+ export const noNestedHowToRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "How To sections must not nest inside other How To sections.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ root(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-guide.md"))
15
+ return;
16
+ const howToHeadings = [];
17
+ for (const child of node.children) {
18
+ if (child.type !== "heading")
19
+ continue;
20
+ const text = getTextContent(child).trim();
21
+ if (!/^How To\b/.test(text))
22
+ continue;
23
+ howToHeadings.push({ line: getLine(child), depth: child.depth, text });
24
+ }
25
+ // A How To heading at greater depth than the previous How To heading nests inside it.
26
+ for (let i = 1; i < howToHeadings.length; i++) {
27
+ const prev = howToHeadings[i - 1];
28
+ const current = howToHeadings[i];
29
+ if (prev && current && current.depth > prev.depth) {
30
+ context.report({
31
+ node,
32
+ message: `How To section "${current.text}" nests inside another How To section`,
33
+ });
34
+ }
35
+ }
36
+ },
37
+ };
38
+ },
39
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview No leftover bracketed template prompts.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const noTemplatePromptRule: MarkdownRuleDefinition;
@@ -0,0 +1,22 @@
1
+ const TEMPLATE_PROMPT = /^\s*\[[A-Z0-9].*\]\s*$/;
2
+ export const noTemplatePromptRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "No leftover bracketed template prompts.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ text(node) {
13
+ if (TEMPLATE_PROMPT.test(node.value)) {
14
+ context.report({
15
+ node,
16
+ message: "leftover bracketed template prompt",
17
+ });
18
+ }
19
+ },
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Overview must contain at most two sentences.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const overviewLengthRule: MarkdownRuleDefinition;
@@ -0,0 +1,47 @@
1
+ import { getFilename, getLine, getTextContent } from "./helpers.js";
2
+ function countSentences(text) {
3
+ return text.split(/[.!?](?:\s+|$)/).filter((part) => part.trim() !== "").length;
4
+ }
5
+ export const overviewLengthRule = {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description: "Overview must contain at most two sentences.",
10
+ recommended: true,
11
+ },
12
+ },
13
+ create(context) {
14
+ return {
15
+ root(node) {
16
+ const filename = getFilename(context);
17
+ if (!filename.endsWith(".md"))
18
+ return;
19
+ let titleLine = 0;
20
+ for (const child of node.children) {
21
+ if (child.type === "heading" && child.depth === 1) {
22
+ titleLine = getLine(child);
23
+ break;
24
+ }
25
+ }
26
+ if (!titleLine)
27
+ return;
28
+ // Check only the first content paragraph after the title (the overview).
29
+ for (const child of node.children) {
30
+ if (child.type !== "paragraph" || getLine(child) <= titleLine)
31
+ continue;
32
+ const text = getTextContent(child).trim();
33
+ if (!text || text.startsWith("#"))
34
+ continue;
35
+ const sentenceCount = countSentences(text);
36
+ if (sentenceCount > 2) {
37
+ context.report({
38
+ node: child,
39
+ message: `overview uses ${String(sentenceCount)} sentences, at most two are allowed`,
40
+ });
41
+ }
42
+ break;
43
+ }
44
+ },
45
+ };
46
+ },
47
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Overview must follow the title in a documentation file.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const overviewPresentRule: MarkdownRuleDefinition;
@@ -0,0 +1,41 @@
1
+ import { getFilename, getLine, getTextContent } from "./helpers.js";
2
+ export const overviewPresentRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Overview must follow the title in a documentation file.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ root(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith(".md"))
15
+ return;
16
+ let titleLine = 0;
17
+ for (const child of node.children) {
18
+ if (child.type === "heading" && child.depth === 1) {
19
+ titleLine = getLine(child);
20
+ break;
21
+ }
22
+ }
23
+ if (!titleLine)
24
+ return;
25
+ let hasOverview = false;
26
+ for (const child of node.children) {
27
+ if (child.type === "paragraph" && getLine(child) > titleLine && getTextContent(child).trim()) {
28
+ hasOverview = true;
29
+ break;
30
+ }
31
+ }
32
+ if (!hasOverview) {
33
+ context.report({
34
+ node,
35
+ message: "no overview follows the title",
36
+ });
37
+ }
38
+ },
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Policy document must not contain Incorrect/Correct examples.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const policyNoExamplesRule: MarkdownRuleDefinition;
@@ -0,0 +1,28 @@
1
+ import { getFilename, getTextContent } from "./helpers.js";
2
+ export const policyNoExamplesRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Policy document must not contain Incorrect/Correct examples.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ heading(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-policy.md"))
15
+ return;
16
+ if (node.depth !== 2)
17
+ return;
18
+ const text = getTextContent(node).trim();
19
+ if (/^(?:Incorrect|Correct)\b/.test(text)) {
20
+ context.report({
21
+ node,
22
+ message: `policy document contains an example: ${text}`,
23
+ });
24
+ }
25
+ },
26
+ };
27
+ },
28
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview A repository must not have more than one Policy document.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const policySingleDocumentRule: MarkdownRuleDefinition;
@@ -0,0 +1,34 @@
1
+ import { getFilename } from "./helpers.js";
2
+ import { findDocsRoot, getProjectDocs } from "./project-index.js";
3
+ export const policySingleDocumentRule = {
4
+ meta: {
5
+ type: "problem",
6
+ docs: {
7
+ description: "A repository must not have more than one Policy document.",
8
+ recommended: true,
9
+ },
10
+ },
11
+ create(context) {
12
+ return {
13
+ root(node) {
14
+ const filename = getFilename(context);
15
+ if (!filename.endsWith("-policy.md"))
16
+ return;
17
+ const docsRoot = findDocsRoot(filename);
18
+ if (!docsRoot)
19
+ return;
20
+ const docs = getProjectDocs(docsRoot);
21
+ const policyDocs = docs.filter((d) => d.kind === "policy");
22
+ if (policyDocs.length > 1) {
23
+ // Only report on the first policy doc, so the issue is reported once.
24
+ if (policyDocs[0]?.filePath === filename) {
25
+ context.report({
26
+ node,
27
+ message: `${String(policyDocs.length)} policy documents exist`,
28
+ });
29
+ }
30
+ }
31
+ },
32
+ };
33
+ },
34
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Policy document must group its bullets under a heading per subject.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const policySubjectHeadingsRule: MarkdownRuleDefinition;
@@ -0,0 +1,58 @@
1
+ import { getFilename, getLine } from "./helpers.js";
2
+ export const policySubjectHeadingsRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Policy document must group its bullets under a heading per subject.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ root(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-policy.md"))
15
+ return;
16
+ // Collect headings and list items with their line numbers, in document order.
17
+ let titleLine = 0;
18
+ let firstSubjectLine = 0;
19
+ let overviewLine = 0;
20
+ let foundTitle = false;
21
+ let foundOverview = false;
22
+ for (const child of node.children) {
23
+ const line = getLine(child);
24
+ if (!foundTitle && child.type === "heading" && child.depth === 1) {
25
+ titleLine = line;
26
+ foundTitle = true;
27
+ continue;
28
+ }
29
+ if (!foundTitle)
30
+ continue;
31
+ if (!foundOverview && child.type === "paragraph" && line > titleLine) {
32
+ overviewLine = line;
33
+ foundOverview = true;
34
+ continue;
35
+ }
36
+ if (!firstSubjectLine && child.type === "heading" && child.depth === 2 && line > titleLine) {
37
+ firstSubjectLine = line;
38
+ }
39
+ }
40
+ if (!titleLine || !firstSubjectLine)
41
+ return;
42
+ // Any bullet before the first subject heading (and after the overview) is ungrouped.
43
+ const hasUngroupedBullet = node.children.some((child) => {
44
+ if (child.type !== "list")
45
+ return false;
46
+ const line = getLine(child);
47
+ return line > overviewLine && line < firstSubjectLine;
48
+ });
49
+ if (hasUngroupedBullet) {
50
+ context.report({
51
+ node,
52
+ message: "policy bullet appears before the first subject heading",
53
+ });
54
+ }
55
+ },
56
+ };
57
+ },
58
+ };
@@ -0,0 +1,17 @@
1
+ export interface DocMeta {
2
+ /** Absolute path on disk. */
3
+ filePath: string;
4
+ /** Path relative to docs root. */
5
+ doc: string;
6
+ fileName: string;
7
+ kind?: "guide" | "rule" | "reference" | "policy";
8
+ title: string;
9
+ }
10
+ /**
11
+ * Get all documents in the docs folder. Results are cached per docsRoot.
12
+ */
13
+ export declare function getProjectDocs(docsRoot: string): DocMeta[];
14
+ /**
15
+ * Try to find the docsRoot from a file path by walking up to find the docs/ directory.
16
+ */
17
+ export declare function findDocsRoot(filePath: string): string | undefined;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @fileoverview Shared project index for cross-document markdown rules.
3
+ * Scans the docs folder and caches document metadata.
4
+ */
5
+ import { readdirSync, readFileSync, statSync } from "node:fs";
6
+ import path from "node:path";
7
+ const KIND_BY_SUFFIX = [
8
+ ["-rule.md", "rule"],
9
+ ["-guide.md", "guide"],
10
+ ["-reference.md", "reference"],
11
+ ["-policy.md", "policy"],
12
+ ];
13
+ function listMarkdownFiles(dir) {
14
+ return readdirSync(dir).flatMap((entry) => {
15
+ const entryPath = path.join(dir, entry);
16
+ if (statSync(entryPath).isDirectory()) {
17
+ return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
18
+ }
19
+ return entry.endsWith(".md") ? [entryPath] : [];
20
+ });
21
+ }
22
+ function extractTitle(filePath) {
23
+ const content = readFileSync(filePath, "utf8");
24
+ const match = /^# (?<title>.+)$/m.exec(content);
25
+ return match?.groups?.title?.trim() ?? "";
26
+ }
27
+ // Memoized cache keyed by docsRoot
28
+ const cache = new Map();
29
+ /**
30
+ * Get all documents in the docs folder. Results are cached per docsRoot.
31
+ */
32
+ export function getProjectDocs(docsRoot) {
33
+ const cached = cache.get(docsRoot);
34
+ if (cached)
35
+ return cached;
36
+ const files = listMarkdownFiles(docsRoot);
37
+ const docs = files
38
+ .sort((a, b) => a.localeCompare(b))
39
+ .map((filePath) => {
40
+ const fileName = path.basename(filePath);
41
+ const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
42
+ return {
43
+ filePath,
44
+ doc: path.relative(docsRoot, filePath).split(path.sep).join("/"),
45
+ fileName,
46
+ kind,
47
+ title: extractTitle(filePath),
48
+ };
49
+ });
50
+ cache.set(docsRoot, docs);
51
+ return docs;
52
+ }
53
+ /**
54
+ * Try to find the docsRoot from a file path by walking up to find the docs/ directory.
55
+ */
56
+ export function findDocsRoot(filePath) {
57
+ let dir = path.dirname(filePath);
58
+ for (;;) {
59
+ if (path.basename(dir) === "docs" && statSync(dir).isDirectory()) {
60
+ return dir;
61
+ }
62
+ const parent = path.dirname(dir);
63
+ if (parent === dir)
64
+ return undefined;
65
+ dir = parent;
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Reference block headings rule.
3
+ * A Reference with a single lookup block must not add a section heading for it.
4
+ */
5
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
6
+ export declare const referenceBlockHeadingsRule: MarkdownRuleDefinition;
@@ -0,0 +1,31 @@
1
+ import { getFilename } from "./helpers.js";
2
+ export const referenceBlockHeadingsRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Reference block headings rule.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ root(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-reference.md"))
15
+ return;
16
+ const sectionHeadings = [];
17
+ for (const child of node.children) {
18
+ if (child.type === "heading" && child.depth >= 2) {
19
+ sectionHeadings.push(child);
20
+ }
21
+ }
22
+ if (sectionHeadings.length === 1) {
23
+ context.report({
24
+ node: node.children[0] ?? node,
25
+ message: "reference has exactly one section heading, so either a single block is headed or a first block is not",
26
+ });
27
+ }
28
+ },
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Reference documents must not use RFC 2119 vocabulary in prose.
3
+ * Only naming a keyword in code spans is permitted.
4
+ */
5
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
6
+ export declare const referenceNoRfcVocabularyRule: MarkdownRuleDefinition;
@@ -0,0 +1,26 @@
1
+ import { containsRfcKeyword, getFilename } from "./helpers.js";
2
+ export const referenceNoRfcVocabularyRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Reference documents must not use RFC 2119 vocabulary (MUST, MUST NOT, SHOULD, SHOULD NOT, MAY) in prose.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ text(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-reference.md"))
15
+ return;
16
+ const keyword = containsRfcKeyword(node.value);
17
+ if (keyword) {
18
+ context.report({
19
+ node,
20
+ message: `reference uses RFC 2119 vocabulary: ${keyword}`,
21
+ });
22
+ }
23
+ },
24
+ };
25
+ },
26
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Rule and policy documents must state at least one requirement.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const requirementPresentRule: MarkdownRuleDefinition;
@@ -0,0 +1,41 @@
1
+ import { containsRfcKeyword, getFilename, getTextContent } from "./helpers.js";
2
+ /** Check whether a subtree contains a bullet with RFC vocabulary. */
3
+ function hasRequirement(node) {
4
+ if (node.type === "listItem") {
5
+ if (containsRfcKeyword(getTextContent(node)))
6
+ return true;
7
+ }
8
+ if ("children" in node) {
9
+ for (const child of node.children) {
10
+ if (hasRequirement(child))
11
+ return true;
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+ export const requirementPresentRule = {
17
+ meta: {
18
+ type: "problem",
19
+ docs: {
20
+ description: "Rule and policy documents must state at least one requirement.",
21
+ recommended: true,
22
+ },
23
+ },
24
+ create(context) {
25
+ return {
26
+ root(node) {
27
+ const filename = getFilename(context);
28
+ if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) {
29
+ return;
30
+ }
31
+ if (!hasRequirement(node)) {
32
+ const kind = filename.endsWith("-rule.md") ? "rule" : "policy";
33
+ context.report({
34
+ node,
35
+ message: `${kind} document states no requirement`,
36
+ });
37
+ }
38
+ },
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview RFC 2119 vocabulary must appear only in bullet points.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const rfcOnlyInBulletsRule: MarkdownRuleDefinition;
@@ -0,0 +1,46 @@
1
+ import { containsRfcKeyword, getFilename } from "./helpers.js";
2
+ /** Check whether a subtree contains a text node outside a bullet using RFC vocabulary. */
3
+ function checkOutsideBullets(node, context, insideBullet) {
4
+ if (node.type === "listItem") {
5
+ // Everything under a list item is a bullet; stop descending for the
6
+ // outside-bullet check, but nested lists still need walking.
7
+ for (const child of node.children) {
8
+ checkOutsideBullets(child, context, true);
9
+ }
10
+ return;
11
+ }
12
+ if (node.type === "text") {
13
+ const keyword = containsRfcKeyword(node.value);
14
+ if (keyword && !insideBullet) {
15
+ context.report({
16
+ node,
17
+ message: `RFC 2119 vocabulary "${keyword}" appears outside a bullet point`,
18
+ });
19
+ }
20
+ return;
21
+ }
22
+ if ("children" in node) {
23
+ for (const child of node.children) {
24
+ checkOutsideBullets(child, context, insideBullet);
25
+ }
26
+ }
27
+ }
28
+ export const rfcOnlyInBulletsRule = {
29
+ meta: {
30
+ type: "problem",
31
+ docs: {
32
+ description: "RFC 2119 vocabulary must appear only in bullet points.",
33
+ recommended: true,
34
+ },
35
+ },
36
+ create(context) {
37
+ return {
38
+ root(node) {
39
+ const filename = getFilename(context);
40
+ if (!filename.endsWith("-rule.md"))
41
+ return;
42
+ checkOutsideBullets(node, context, false);
43
+ },
44
+ };
45
+ },
46
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Rule must have paired Incorrect/Correct examples.
3
+ */
4
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
5
+ export declare const rulePairedExamplesRule: MarkdownRuleDefinition;
@@ -0,0 +1,36 @@
1
+ import { getFilename, getLine, getTextContent } from "./helpers.js";
2
+ export const rulePairedExamplesRule = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: {
6
+ description: "Rule must have paired Incorrect/Correct examples.",
7
+ recommended: true,
8
+ },
9
+ },
10
+ create(context) {
11
+ return {
12
+ root(node) {
13
+ const filename = getFilename(context);
14
+ if (!filename.endsWith("-rule.md"))
15
+ return;
16
+ const exampleHeadings = [];
17
+ for (const child of node.children) {
18
+ if (child.type === "heading" && child.depth === 2) {
19
+ const text = getTextContent(child).trim();
20
+ if (/^(?:Incorrect|Correct)\b/.test(text)) {
21
+ exampleHeadings.push({ line: getLine(child), text });
22
+ }
23
+ }
24
+ }
25
+ const incorrect = exampleHeadings.filter((h) => h.text.startsWith("Incorrect"));
26
+ const correct = exampleHeadings.filter((h) => h.text.startsWith("Correct"));
27
+ if (incorrect.length === 0 || incorrect.length !== correct.length) {
28
+ context.report({
29
+ node,
30
+ message: `${String(incorrect.length)} Incorrect and ${String(correct.length)} Correct examples`,
31
+ });
32
+ }
33
+ },
34
+ };
35
+ },
36
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared RuleTester wiring for the markdown rule tests.
3
+ *
4
+ * Each `describe` title in a rule test is the exact text of the requirement the
5
+ * case pins, which is how `pasika coverage` verifies that a requirement recorded
6
+ * as lint-enforced has a test behind it.
7
+ */
8
+ import { describe } from "node:test";
9
+ import { RuleTester } from "eslint";
10
+ export declare const mdRuleTester: RuleTester;
11
+ export { describe };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared RuleTester wiring for the markdown rule tests.
3
+ *
4
+ * Each `describe` title in a rule test is the exact text of the requirement the
5
+ * case pins, which is how `pasika coverage` verifies that a requirement recorded
6
+ * as lint-enforced has a test behind it.
7
+ */
8
+ import { describe, it } from "node:test";
9
+ import { RuleTester } from "eslint";
10
+ import markdown from "@eslint/markdown";
11
+ import { mdRules } from "../../index.js";
12
+ RuleTester.describe = describe;
13
+ RuleTester.it = it;
14
+ export const mdRuleTester = new RuleTester({
15
+ language: "markdown/gfm",
16
+ plugins: {
17
+ markdown,
18
+ pasika: { rules: mdRules },
19
+ },
20
+ });
21
+ export { describe };
@@ -0,0 +1,2 @@
1
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
2
+ export declare const supportDocumentPlacementRule: MarkdownRuleDefinition;