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
package/README.md CHANGED
@@ -16,9 +16,9 @@ docs/
16
16
  enforcement/
17
17
  registry.json # requirement → enforcement, keyed by content hash
18
18
  coverage.ts # reconciles the docs against the registry
19
- docs-check.ts # the documentation guide, mechanically
20
19
  eslint/
21
20
  pasika/rules/ # the lint rules, with fixture tests beside them
21
+ pasika/rules/md/ # the documentation-guide rules, linting docs/ itself
22
22
  cli/
23
23
  index.ts # the `pasika` command
24
24
  ```
@@ -48,7 +48,6 @@ The `note` field is where a check's known gap is recorded, so a partial check ne
48
48
  | --- | --- |
49
49
  | `eslint` | An ESLint rule reports it, and a fixture test titled with the requirement pins it |
50
50
  | `doctor` | A `pasika doctor` check reports it |
51
- | `docs-check` | A `pasika docs` check reports it |
52
51
  | `planned` | Mechanically checkable, not written yet; `note` names the intended check |
53
52
  | `judgment` | No mechanical check can decide it; `note` says why |
54
53
  | `permission` | The requirement grants permission, so there is nothing to check |
@@ -57,9 +56,9 @@ The `note` field is where a check's known gap is recorded, so a partial check ne
57
56
 
58
57
  ## Commands
59
58
 
59
+ The documentation guide itself is linted: the `pasika/*` markdown rules run over `docs/**/*.md` and report title, overview, structure, example-pairing, and RFC 2119 violations at the exact node.
60
+
60
61
  ```bash
61
- npx pasika docs # check a docs/ folder against the documentation guide
62
- npx pasika docs --dir content # check another folder
63
62
  npx pasika coverage # check that every requirement has recorded enforcement
64
63
  npx pasika coverage --accept # record reworded and removed requirements
65
64
  ```
@@ -116,6 +115,10 @@ The ruleset applies to `src/**` only, so a repository without a `src/` tree pass
116
115
  | `pasika/component-placement` † | The folder a component's consumers imply |
117
116
  | `pasika/support-file-placement` † | The folder a hook, type, schema, constant, or utility belongs in |
118
117
 
118
+ ### Documentation rules
119
+
120
+ The `pasika/*` markdown rules enforce the documentation guide over `docs/**/*.md`: file-name suffixes and titles, overview presence and length, guide step structure, Incorrect/Correct pairing, policy document shape, reference block headings, RFC 2119 placement, and template hygiene. They run through `@eslint/markdown` in `eslint.config.ts`; `pasika coverage` verifies each has a test and a registry entry.
121
+
119
122
  Run `pasika coverage --json` for the exact requirement each rule covers.
120
123
 
121
124
  ### † Cross-file rules
@@ -133,7 +136,6 @@ Both are inert in a repository with no `src/` tree.
133
136
  npm run lint
134
137
  npm run typecheck
135
138
  npm run test
136
- npm run docs
137
139
  npm run coverage
138
140
  npm run build
139
141
  ```
package/dist/cli/index.js CHANGED
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import { Command } from "commander";
5
5
  import { buildCoverageReport, classifyRequirement, readRegistry, writeRegistry, } from "../enforcement/coverage.js";
6
6
  import { enforcementKindSchema } from "../enforcement/types.js";
7
- import { checkDocs } from "../enforcement/docs-check.js";
7
+ import { runDoctor } from "../enforcement/doctor.js";
8
+ import { log, error, json } from "./output.js";
8
9
  const REGISTRY_RELATIVE_PATH = path.join("enforcement", "registry.json");
9
10
  /** Walks up from `startDir` to the directory that holds the enforcement registry. */
10
11
  function findRegistryRoot(startDir) {
@@ -30,30 +31,6 @@ function truncate(text, width) {
30
31
  }
31
32
  const program = new Command();
32
33
  program.name("pasika").description("Applies and diagnoses the pasika framework.");
33
- program
34
- .command("docs")
35
- .description("Check documentation against the documentation guide.")
36
- .option("--dir <path>", "documentation folder to check", "docs")
37
- .option("--json", "print findings as JSON")
38
- .action((options) => {
39
- const docsRoot = path.resolve(options.dir);
40
- if (!existsSync(docsRoot)) {
41
- console.error(`No documentation folder at ${docsRoot}`);
42
- process.exit(1);
43
- }
44
- const { docs, findings } = checkDocs(docsRoot);
45
- if (options.json) {
46
- console.log(JSON.stringify({ documents: docs.length, findings }, undefined, 2));
47
- process.exit(findings.length > 0 ? 1 : 0);
48
- }
49
- for (const finding of findings) {
50
- console.log(` ✗ ${finding.doc}:${String(finding.line)} ${finding.check} ${finding.message}`);
51
- }
52
- console.log(findings.length === 0
53
- ? `\n✓ ${String(docs.length)} documents pass`
54
- : `\n${String(docs.length)} documents checked · ${String(findings.length)} findings`);
55
- process.exit(findings.length > 0 ? 1 : 0);
56
- });
57
34
  program
58
35
  .command("coverage")
59
36
  .description("Check that every documented requirement has recorded enforcement.")
@@ -66,19 +43,19 @@ program
66
43
  .action((options) => {
67
44
  const root = findRegistryRoot(process.cwd());
68
45
  if (!root) {
69
- console.error(`No ${REGISTRY_RELATIVE_PATH} found in this directory or any parent.`);
46
+ error(`No ${REGISTRY_RELATIVE_PATH} found in this directory or any parent.`);
70
47
  process.exit(1);
71
48
  }
72
49
  const docsRoot = path.join(root, "docs");
73
50
  if (!existsSync(docsRoot)) {
74
- console.error(`No documentation folder at ${docsRoot}. Run coverage inside the pasika repository.`);
51
+ error(`No documentation folder at ${docsRoot}. Run coverage inside the pasika repository.`);
75
52
  process.exit(1);
76
53
  }
77
54
  const registryPath = path.join(root, REGISTRY_RELATIVE_PATH);
78
55
  if (options.classify !== undefined) {
79
56
  const kind = enforcementKindSchema.safeParse(options.kind);
80
57
  if (!kind.success) {
81
- console.error(`✗ --kind must be one of ${enforcementKindSchema.options.join(", ")}.`);
58
+ error(`✗ --kind must be one of ${enforcementKindSchema.options.join(", ")}.`);
82
59
  process.exit(1);
83
60
  }
84
61
  try {
@@ -91,10 +68,10 @@ program
91
68
  const change = result.previousKind === undefined
92
69
  ? `recorded as ${result.requirement.kind}`
93
70
  : `reclassified from ${result.previousKind} to ${result.requirement.kind}`;
94
- console.log(`✓ ${change}: ${result.requirement.text}`);
71
+ log(`✓ ${change}: ${result.requirement.text}`);
95
72
  }
96
- catch (error) {
97
- console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
73
+ catch (err) {
74
+ error(`✗ ${err instanceof Error ? err.message : String(err)}`);
98
75
  process.exit(1);
99
76
  }
100
77
  }
@@ -104,29 +81,46 @@ program
104
81
  rulesDir: path.join(root, "eslint", "pasika", "rules"),
105
82
  });
106
83
  if (options.json) {
107
- console.log(JSON.stringify(report, undefined, 2));
84
+ json(report);
108
85
  process.exit(report.issues.length > 0 ? 1 : 0);
109
86
  }
110
87
  for (const issue of report.issues) {
111
88
  const where = issue.line === undefined ? issue.doc : `${issue.doc}:${String(issue.line)}`;
112
- console.log(` ✗ ${ISSUE_LABELS[issue.kind]} ${truncate(issue.text, 76)}`);
113
- console.log(` ${where}${issue.detail ? `\n ${issue.detail}` : ""}`);
89
+ log(` ✗ ${ISSUE_LABELS[issue.kind]} ${truncate(issue.text, 76)}`);
90
+ log(` ${where}${issue.detail ? `\n ${issue.detail}` : ""}`);
114
91
  }
115
92
  const { counts } = report;
116
- console.log([
93
+ log([
117
94
  "",
118
95
  `${String(report.total)} requirements · ${String(report.mechanical)} mechanically enforced`,
119
- ` eslint ${String(counts.eslint)} · doctor ${String(counts.doctor)} · docs-check ${String(counts["docs-check"])}`,
96
+ ` eslint ${String(counts.eslint)} · doctor ${String(counts.doctor)}`,
120
97
  ` planned ${String(counts.planned)} · judgment ${String(counts.judgment)} · permission ${String(counts.permission)}`,
121
98
  ` unclassified ${String(report.issues.filter((issue) => issue.kind === "new").length)}`,
122
99
  ].join("\n"));
123
100
  if (options.accept) {
124
101
  writeRegistry(registryPath, report.nextRegistry);
125
102
  const accepted = report.issues.filter((issue) => issue.kind === "changed" || issue.kind === "removed");
126
- console.log(`\nAccepted ${String(accepted.length)} change(s) into ${REGISTRY_RELATIVE_PATH}.`);
127
- console.log("Requirements reported as new still need a classification.");
103
+ log(`\nAccepted ${String(accepted.length)} change(s) into ${REGISTRY_RELATIVE_PATH}.`);
104
+ log("Requirements reported as new still need a classification.");
128
105
  process.exit(report.issues.some((issue) => issue.kind === "new") ? 1 : 0);
129
106
  }
130
107
  process.exit(report.issues.length > 0 ? 1 : 0);
131
108
  });
109
+ program
110
+ .command("doctor")
111
+ .description("Diagnose gaps between the repository and the pasika framework baseline.")
112
+ .option("--json", "print findings as JSON")
113
+ .action((options) => {
114
+ const findings = runDoctor(process.cwd());
115
+ if (options.json) {
116
+ json({ findings });
117
+ process.exit(findings.some((f) => f.severity === "error") ? 1 : 0);
118
+ }
119
+ for (const finding of findings) {
120
+ const icon = finding.severity === "error" ? "✗" : "⚠";
121
+ log(` ${icon} ${finding.check} ${finding.message}`);
122
+ }
123
+ log(findings.length === 0 ? "\n✓ No gaps found" : `\n${String(findings.length)} finding(s)`);
124
+ process.exit(findings.some((f) => f.severity === "error") ? 1 : 0);
125
+ });
132
126
  program.parse();
@@ -0,0 +1,3 @@
1
+ export declare function log(message: string): void;
2
+ export declare function error(message: string): void;
3
+ export declare function json(data: unknown): void;
@@ -0,0 +1,11 @@
1
+ /* eslint-disable no-console -- cli-reports-to-terminal: stdout is the CLI's output channel */
2
+ export function log(message) {
3
+ console.log(message);
4
+ }
5
+ export function error(message) {
6
+ console.error(message);
7
+ }
8
+ export function json(data) {
9
+ console.log(JSON.stringify(data, undefined, 2));
10
+ }
11
+ /* eslint-enable no-console -- re-enable after CLI output block */
@@ -1,8 +1,7 @@
1
- import { readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
- import { pasikaRuleIds } from "../eslint/pasika/index.js";
5
- import { DOCS_CHECKS } from "./docs-check.js";
4
+ import { allPasikaRuleIds } from "../eslint/pasika/index.js";
6
5
  import { parseDocs } from "./parse-docs.js";
7
6
  import { MECHANICAL_KINDS, registrySchema } from "./types.js";
8
7
  /** Ratio of shared words, used only to guess which stored requirement a reworded bullet came from. */
@@ -13,17 +12,27 @@ function similarity(left, right) {
13
12
  const union = new Set([...leftWords, ...rightWords]).size;
14
13
  return union === 0 ? 0 : shared / union;
15
14
  }
16
- /** Titles passed to `describe` or `test` in the rule test files. */
15
+ /** Titles passed to `describe` or `test` in the rule test files, recursing into subfolders. */
17
16
  function collectTestTitles(rulesDir) {
18
17
  const titles = new Set();
19
- for (const entry of readdirSync(rulesDir)) {
20
- if (!entry.endsWith(".test.ts"))
21
- continue;
22
- const body = readFileSync(path.join(rulesDir, entry), "utf8");
23
- for (const match of body.matchAll(/\b(?:describe|test|it)\(\s*"(?<title>(?:[^"\\]|\\.)*)"/g)) {
24
- titles.add((match.groups?.title ?? "").replaceAll('\\"', '"'));
18
+ const visit = (dir) => {
19
+ for (const entry of readdirSync(dir)) {
20
+ const entryPath = path.join(dir, entry);
21
+ if (statSync(entryPath).isDirectory()) {
22
+ visit(entryPath);
23
+ continue;
24
+ }
25
+ if (!entry.endsWith(".test.ts"))
26
+ continue;
27
+ const body = readFileSync(entryPath, "utf8");
28
+ const titlePattern = /\b(?:describe|test|it)\(\s*(?:"(?<double>(?:[^"\\]|\\.)*)"|'(?<single>(?:[^'\\]|\\.)*)')/g;
29
+ for (const match of body.matchAll(titlePattern)) {
30
+ const title = match.groups?.double ?? match.groups?.single ?? "";
31
+ titles.add(title.replaceAll('\\\\"', '"').replaceAll("\\\\'", "'"));
32
+ }
25
33
  }
26
- }
34
+ };
35
+ visit(rulesDir);
27
36
  return titles;
28
37
  }
29
38
  /** A requirement several checks cover lists them comma-separated. */
@@ -33,19 +42,16 @@ function refParts(ref) {
33
42
  .map((part) => part.trim())
34
43
  .filter(Boolean);
35
44
  }
36
- function isRefKnown(requirement, docsChecks) {
45
+ function isRefKnown(requirement) {
37
46
  const parts = refParts(requirement.ref);
38
47
  if (requirement.kind === "eslint")
39
- return parts.length > 0 && parts.every((part) => pasikaRuleIds.includes(part));
40
- if (requirement.kind === "docs-check")
41
- return parts.length > 0 && parts.every((part) => docsChecks.has(part));
48
+ return parts.length > 0 && parts.every((part) => allPasikaRuleIds.includes(part));
42
49
  // Doctor checks do not exist yet, so a `doctor` entry is a forward reference.
43
50
  return true;
44
51
  }
45
52
  export function buildCoverageReport(options) {
46
53
  const { docsRoot, registry, rulesDir } = options;
47
54
  const docs = parseDocs(docsRoot);
48
- const docsChecks = new Set(DOCS_CHECKS);
49
55
  const testTitles = collectTestTitles(rulesDir);
50
56
  const byHash = new Map(registry.requirements.map((requirement) => [requirement.hash, requirement]));
51
57
  const matched = new Set();
@@ -53,10 +59,9 @@ export function buildCoverageReport(options) {
53
59
  const counts = {
54
60
  eslint: 0,
55
61
  doctor: 0,
56
- "docs-check": 0,
62
+ planned: 0,
57
63
  judgment: 0,
58
64
  permission: 0,
59
- planned: 0,
60
65
  };
61
66
  const nextRequirements = [];
62
67
  const parsed = docs.flatMap((doc) => doc.requirements.map((requirement) => ({ doc: doc.doc, requirement })));
@@ -66,7 +71,7 @@ export function buildCoverageReport(options) {
66
71
  matched.add(requirement.hash);
67
72
  counts[recorded.kind] += 1;
68
73
  nextRequirements.push({ ...recorded, doc });
69
- if (!isRefKnown(recorded, docsChecks)) {
74
+ if (!isRefKnown(recorded)) {
70
75
  issues.push({
71
76
  kind: "unknown-ref",
72
77
  doc,
@@ -150,20 +155,11 @@ export function classifyRequirement(options) {
150
155
  if (input.kind === "eslint") {
151
156
  if (refs.length === 0)
152
157
  throw new Error('Kind "eslint" needs --ref naming the rule that reports it.');
153
- const unknown = refs.filter((ref) => !pasikaRuleIds.includes(ref));
158
+ const unknown = refs.filter((ref) => !allPasikaRuleIds.includes(ref));
154
159
  if (unknown.length > 0) {
155
160
  throw new Error(`--ref ${unknown.map((ref) => `"${ref}"`).join(", ")} is not a rule in the plugin.`);
156
161
  }
157
162
  }
158
- else if (input.kind === "docs-check") {
159
- if (refs.length === 0)
160
- throw new Error('Kind "docs-check" needs --ref naming the check that reports it.');
161
- const known = new Set(DOCS_CHECKS);
162
- const unknown = refs.filter((ref) => !known.has(ref));
163
- if (unknown.length > 0) {
164
- throw new Error(`--ref ${unknown.map((ref) => `"${ref}"`).join(", ")} is not a documentation check.`);
165
- }
166
- }
167
163
  else if (input.kind !== "doctor" && refs.length > 0) {
168
164
  throw new Error(`Kind "${input.kind}" takes no --ref, because nothing reports it.`);
169
165
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `pasika doctor` checks.
3
+ *
4
+ * Diagnoses gaps between a consumer repository and the pasika framework
5
+ * baseline. Scope: dependencies, configuration, managed files, and
6
+ * source structure. Code-level and path-level rules belong to ESLint.
7
+ */
8
+ export interface DoctorFinding {
9
+ /** Which requirement this relates to. */
10
+ check: string;
11
+ /** Human-readable description. */
12
+ message: string;
13
+ /** Severity: error blocks adoption, warning is informational. */
14
+ severity: "error" | "warning";
15
+ }
16
+ export declare function runDoctor(cwd: string): DoctorFinding[];
@@ -0,0 +1,249 @@
1
+ /**
2
+ * `pasika doctor` checks.
3
+ *
4
+ * Diagnoses gaps between a consumer repository and the pasika framework
5
+ * baseline. Scope: dependencies, configuration, managed files, and
6
+ * source structure. Code-level and path-level rules belong to ESLint.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { z } from "zod";
11
+ const packageJsonSchema = z.object({
12
+ dependencies: z.record(z.string(), z.string()).optional(),
13
+ devDependencies: z.record(z.string(), z.string()).optional(),
14
+ scripts: z.record(z.string(), z.string()).optional(),
15
+ });
16
+ function readPackageJson(filePath) {
17
+ try {
18
+ const result = packageJsonSchema.safeParse(JSON.parse(fs.readFileSync(filePath, "utf8")));
19
+ if (result.success)
20
+ return result.data;
21
+ }
22
+ catch {
23
+ // missing or unparseable
24
+ }
25
+ return {};
26
+ }
27
+ /**
28
+ * Check that pasika and zirka are installed at compatible versions.
29
+ */
30
+ function checkFrameworkPackages(pkg) {
31
+ const findings = [];
32
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ if (!allDeps.pasika) {
34
+ findings.push({
35
+ check: "pasika-installed",
36
+ message: "pasika is not listed in package.json.",
37
+ severity: "error",
38
+ });
39
+ }
40
+ if (!allDeps.zirka) {
41
+ findings.push({
42
+ check: "zirka-installed",
43
+ message: "zirka is not listed in package.json.",
44
+ severity: "error",
45
+ });
46
+ }
47
+ return findings;
48
+ }
49
+ /**
50
+ * Check that application source lives under src/.
51
+ */
52
+ function checkSourceRoot(cwd) {
53
+ const findings = [];
54
+ const srcDir = path.join(cwd, "src");
55
+ if (!fs.existsSync(srcDir)) {
56
+ findings.push({
57
+ check: "source-under-src",
58
+ message: "Application source must live under src/.",
59
+ severity: "error",
60
+ });
61
+ return findings;
62
+ }
63
+ // Check for common non-src source directories at the project root
64
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
65
+ const suspicious = entries.filter((e) => e.isDirectory() &&
66
+ !e.name.startsWith(".") &&
67
+ e.name !== "src" &&
68
+ e.name !== "node_modules" &&
69
+ e.name !== "dist" &&
70
+ e.name !== "build" &&
71
+ e.name !== ".vulyk" &&
72
+ e.name !== "docs" &&
73
+ e.name !== "enforcement" &&
74
+ e.name !== "eslint" &&
75
+ e.name !== "cli" &&
76
+ e.name !== "__tests__" &&
77
+ e.name !== "test" &&
78
+ e.name !== "tests" &&
79
+ e.name !== "coverage" &&
80
+ e.name !== ".github" &&
81
+ e.name !== ".agents" &&
82
+ e.name !== ".husky" &&
83
+ e.name !== ".next" &&
84
+ e.name !== ".turbo" &&
85
+ e.name !== ".vercel" &&
86
+ e.name !== ".cache" &&
87
+ e.name !== ".tmp" &&
88
+ e.name !== "tmp" &&
89
+ e.name !== "public" &&
90
+ e.name !== "static" &&
91
+ e.name !== "assets" &&
92
+ e.name !== "bin" &&
93
+ e.name !== "vendor");
94
+ if (suspicious.length > 0) {
95
+ const names = suspicious.map((e) => e.name).join(", ");
96
+ findings.push({
97
+ check: "source-under-src",
98
+ message: `Possible source directories outside src/: ${names}.`,
99
+ severity: "warning",
100
+ });
101
+ }
102
+ return findings;
103
+ }
104
+ /**
105
+ * Find the global CSS file that registers Tailwind.
106
+ */
107
+ function findGlobalStylesheet(cwd) {
108
+ const candidates = ["src/app/globals.css", "src/styles/globals.css", "src/globals.css"];
109
+ for (const candidate of candidates) {
110
+ const full = path.join(cwd, candidate);
111
+ if (fs.existsSync(full))
112
+ return full;
113
+ }
114
+ // Search for any CSS file containing @import "tailwindcss"
115
+ const srcDir = path.join(cwd, "src");
116
+ if (!fs.existsSync(srcDir))
117
+ return undefined;
118
+ const walk = (dir) => {
119
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
120
+ const full = path.join(dir, entry.name);
121
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
122
+ const found = walk(full);
123
+ if (found)
124
+ return found;
125
+ }
126
+ if (entry.isFile() && entry.name.endsWith(".css")) {
127
+ const content = fs.readFileSync(full, "utf8");
128
+ if (content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'")) {
129
+ return full;
130
+ }
131
+ }
132
+ }
133
+ return undefined;
134
+ };
135
+ return walk(srcDir);
136
+ }
137
+ /**
138
+ * Check that the repository has a global stylesheet entry point that
139
+ * registers Tailwind. The stylesheet's contents are the ESLint CSS rules'
140
+ * concern, so doctor only checks that it exists.
141
+ */
142
+ function checkGlobalStylesheet(cwd) {
143
+ const findings = [];
144
+ const stylesheet = findGlobalStylesheet(cwd);
145
+ if (!stylesheet) {
146
+ findings.push({
147
+ check: "global-stylesheet",
148
+ message: 'No global stylesheet found. Create src/app/globals.css with @import "tailwindcss".',
149
+ severity: "error",
150
+ });
151
+ }
152
+ return findings;
153
+ }
154
+ /**
155
+ * Check that configuration files follow the pasika baseline.
156
+ *
157
+ * Repositories must take their lint, format, and TypeScript configuration
158
+ * from pasika and zirka rather than restating it locally.
159
+ */
160
+ function checkConfigBaseline(cwd) {
161
+ const findings = [];
162
+ // eslint.config.ts must exist and reference zirka
163
+ const eslintConfigs = ["eslint.config.ts", "eslint.config.mjs", "eslint.config.js"];
164
+ const eslintConfig = eslintConfigs.find((name) => fs.existsSync(path.join(cwd, name)));
165
+ if (!eslintConfig) {
166
+ findings.push({
167
+ check: "config-baseline",
168
+ message: "No eslint config found. Create eslint.config.ts extending the pasika/zirka baseline.",
169
+ severity: "error",
170
+ });
171
+ }
172
+ else {
173
+ const content = fs.readFileSync(path.join(cwd, eslintConfig), "utf8");
174
+ if (!content.includes("zirka")) {
175
+ findings.push({
176
+ check: "config-baseline",
177
+ message: `eslint config does not reference zirka. Use the pasika/zirka baseline configuration.`,
178
+ severity: "warning",
179
+ });
180
+ }
181
+ }
182
+ // tsconfig.json must exist
183
+ if (!fs.existsSync(path.join(cwd, "tsconfig.json"))) {
184
+ findings.push({
185
+ check: "config-baseline",
186
+ message: "No tsconfig.json found. Create one extending the pasika baseline.",
187
+ severity: "error",
188
+ });
189
+ }
190
+ return findings;
191
+ }
192
+ /**
193
+ * Check that vulyk-managed files have not been hand-edited.
194
+ *
195
+ * Files listed in the .vulyk manifest are generated by vulyk commands
196
+ * and must not be edited directly.
197
+ */
198
+ function checkManagedFiles(cwd) {
199
+ const findings = [];
200
+ const vulykDir = path.join(cwd, ".vulyk");
201
+ if (!fs.existsSync(vulykDir))
202
+ return findings;
203
+ const manifestPath = path.join(vulykDir, "manifest.json");
204
+ if (!fs.existsSync(manifestPath))
205
+ return findings;
206
+ try {
207
+ const manifestEntrySchema = z.looseObject({
208
+ targets: z.array(z.string()).optional(),
209
+ });
210
+ const manifestSchema = z.record(z.string(), manifestEntrySchema);
211
+ const result = manifestSchema.safeParse(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
212
+ if (!result.success)
213
+ return findings;
214
+ for (const [, entry] of Object.entries(result.data)) {
215
+ const targets = entry.targets ?? [];
216
+ for (const target of targets) {
217
+ const targetPath = path.join(cwd, target);
218
+ if (fs.existsSync(targetPath)) {
219
+ // File exists — check it hasn't been modified since vulyk last wrote it
220
+ const stat = fs.statSync(targetPath);
221
+ const vulykStat = fs.statSync(manifestPath);
222
+ // If target is newer than the manifest, it was edited after vulyk wrote it
223
+ if (stat.mtimeMs > vulykStat.mtimeMs + 1000) {
224
+ findings.push({
225
+ check: "managed-file-edit",
226
+ message: `File "${target}" appears to have been edited after vulyk wrote it. Use vulyk commands to update managed files.`,
227
+ severity: "warning",
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ catch {
235
+ // manifest unreadable
236
+ }
237
+ return findings;
238
+ }
239
+ export function runDoctor(cwd) {
240
+ const pkgPath = path.join(cwd, "package.json");
241
+ const pkg = readPackageJson(pkgPath);
242
+ return [
243
+ ...checkFrameworkPackages(pkg),
244
+ ...checkConfigBaseline(cwd),
245
+ ...checkManagedFiles(cwd),
246
+ ...checkSourceRoot(cwd),
247
+ ...checkGlobalStylesheet(cwd),
248
+ ];
249
+ }
@@ -11,7 +11,6 @@ import { z } from "zod";
11
11
  export declare const enforcementKindSchema: z.ZodEnum<{
12
12
  eslint: "eslint";
13
13
  doctor: "doctor";
14
- "docs-check": "docs-check";
15
14
  judgment: "judgment";
16
15
  permission: "permission";
17
16
  planned: "planned";
@@ -23,7 +22,6 @@ export declare const requirementSchema: z.ZodObject<{
23
22
  kind: z.ZodEnum<{
24
23
  eslint: "eslint";
25
24
  doctor: "doctor";
26
- "docs-check": "docs-check";
27
25
  judgment: "judgment";
28
26
  permission: "permission";
29
27
  planned: "planned";
@@ -39,7 +37,6 @@ export declare const registrySchema: z.ZodObject<{
39
37
  kind: z.ZodEnum<{
40
38
  eslint: "eslint";
41
39
  doctor: "doctor";
42
- "docs-check": "docs-check";
43
40
  judgment: "judgment";
44
41
  permission: "permission";
45
42
  planned: "planned";
@@ -13,8 +13,6 @@ export const enforcementKindSchema = z.enum([
13
13
  "eslint",
14
14
  /** A `pasika doctor` check reports it. */
15
15
  "doctor",
16
- /** A `pasika docs` check reports it. */
17
- "docs-check",
18
16
  /** No mechanical check can decide it; a reviewer or agent applies it. */
19
17
  "judgment",
20
18
  /** The requirement grants permission, so there is nothing to check. */
@@ -31,8 +29,8 @@ export const requirementSchema = z.object({
31
29
  hash: z.string(),
32
30
  kind: enforcementKindSchema,
33
31
  /**
34
- * Identifier of the check that covers this requirement: an ESLint rule id, a
35
- * doctor check id, or a docs-check id. Several ids are comma-separated.
32
+ * Identifier of the check that covers this requirement: an ESLint rule id or
33
+ * a doctor check id. Several ids are comma-separated.
36
34
  * Absent for judgment and permission.
37
35
  */
38
36
  ref: z.string().optional(),
@@ -47,13 +45,6 @@ export const registrySchema = z.object({
47
45
  requirements: z.array(requirementSchema),
48
46
  });
49
47
  /** Every kind, in the order the coverage summary prints them. */
50
- export const ENFORCEMENT_KINDS = [
51
- "eslint",
52
- "doctor",
53
- "docs-check",
54
- "planned",
55
- "judgment",
56
- "permission",
57
- ];
48
+ export const ENFORCEMENT_KINDS = ["eslint", "doctor", "planned", "judgment", "permission"];
58
49
  /** The kinds that count as mechanically enforced today. */
59
- export const MECHANICAL_KINDS = ["eslint", "doctor", "docs-check"];
50
+ export const MECHANICAL_KINDS = ["eslint", "doctor"];