@recallnet/remark-lint-docs-taxonomy 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/package.json +40 -0
  3. package/src/index.js +134 -0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @recallnet/remark-lint-docs-taxonomy
2
+
3
+ Flags Markdown docs whose `doc_type` does not match the policy-defined directory or filename rules.
4
+
5
+ Use with `remark-frontmatter` and a repo policy that includes a `taxonomy` section.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@recallnet/remark-lint-docs-taxonomy",
3
+ "version": "0.2.0",
4
+ "description": "Remark plugin that enforces policy-driven docs taxonomy paths and filenames.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/recallnet/remark-ai.git",
10
+ "directory": "packages/remark-lint-docs-taxonomy"
11
+ },
12
+ "homepage": "https://github.com/recallnet/remark-ai",
13
+ "bugs": {
14
+ "url": "https://github.com/recallnet/remark-ai/issues"
15
+ },
16
+ "keywords": [
17
+ "remark",
18
+ "remark-lint",
19
+ "docs",
20
+ "taxonomy",
21
+ "frontmatter"
22
+ ],
23
+ "exports": "./src/index.js",
24
+ "files": [
25
+ "src"
26
+ ],
27
+ "dependencies": {
28
+ "@recallnet/docs-governance-policy": "workspace:*",
29
+ "yaml": "^2.8.3"
30
+ },
31
+ "peerDependencies": {
32
+ "remark-frontmatter": "^5.0.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ }
40
+ }
package/src/index.js ADDED
@@ -0,0 +1,134 @@
1
+ import { basename, relative, resolve } from "node:path";
2
+
3
+ import {
4
+ DEFAULT_POLICY_PATH,
5
+ isDocsPathExcluded,
6
+ isDocsPathInScope,
7
+ loadDocsPolicy,
8
+ matchesDocsPolicyPattern,
9
+ normalizePath,
10
+ resolveTaxonomyExcludeGlobs,
11
+ resolveTaxonomyRule,
12
+ } from "@recallnet/docs-governance-policy";
13
+ import { parse } from "yaml";
14
+
15
+ const RULE_ID = "docs-taxonomy";
16
+
17
+ function findYamlNode(tree) {
18
+ return tree?.children?.find((node) => node.type === "yaml") ?? null;
19
+ }
20
+
21
+ function parseFrontmatter(tree) {
22
+ const yamlNode = findYamlNode(tree);
23
+ if (!yamlNode?.value) {
24
+ return null;
25
+ }
26
+
27
+ const data = parse(String(yamlNode.value));
28
+ return data && typeof data === "object" && !Array.isArray(data) ? data : null;
29
+ }
30
+
31
+ function compileFilenamePattern(pattern) {
32
+ try {
33
+ return new RegExp(pattern);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function formatList(values) {
40
+ return values.map((value) => `\`${value}\``).join(", ");
41
+ }
42
+
43
+ export default function remarkLintDocsTaxonomy(options = {}) {
44
+ const policyPath = options.policyPath ?? DEFAULT_POLICY_PATH;
45
+
46
+ // @context decision !high [verified:2026-03-27] — Taxonomy still reports unknown doc_type values.
47
+ // Schema validation should catch them in canonical setups, but policy-level enforcement keeps the failure visible if a repo narrows schema coverage.
48
+ return (tree, file) => {
49
+ const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
50
+ const filePath = file.path ? normalizePath(relative(cwd, file.path)) : "";
51
+
52
+ if (!filePath) {
53
+ return;
54
+ }
55
+
56
+ const { policy } = loadDocsPolicy({ cwd, policyPath });
57
+ if (!isDocsPathInScope(filePath, policy)) {
58
+ return;
59
+ }
60
+
61
+ if (isDocsPathExcluded(filePath, resolveTaxonomyExcludeGlobs(policy))) {
62
+ return;
63
+ }
64
+
65
+ const frontmatter = parseFrontmatter(tree);
66
+ if (!frontmatter || typeof frontmatter.doc_type !== "string") {
67
+ return;
68
+ }
69
+
70
+ const docType = frontmatter.doc_type.trim();
71
+ const rule = resolveTaxonomyRule(docType, policy);
72
+ if (!rule) {
73
+ file.message(`Unknown document type '${docType}' in ${filePath}.`, {
74
+ ruleId: RULE_ID,
75
+ source: "@recallnet/remark-lint-docs-taxonomy",
76
+ });
77
+ return;
78
+ }
79
+
80
+ const pathGlobs = Array.isArray(rule.path_globs) ? rule.path_globs : [];
81
+ if (
82
+ pathGlobs.length > 0 &&
83
+ !pathGlobs.some((pattern) => matchesDocsPolicyPattern(filePath, pattern))
84
+ ) {
85
+ file.message(
86
+ `Document type '${docType}' at ${filePath} must live under ${formatList(pathGlobs)}.`,
87
+ {
88
+ ruleId: RULE_ID,
89
+ source: "@recallnet/remark-lint-docs-taxonomy",
90
+ }
91
+ );
92
+ }
93
+
94
+ const filename = basename(filePath);
95
+ const allowedFilenames = Array.isArray(rule.allowed_filenames) ? rule.allowed_filenames : [];
96
+ const filenamePattern =
97
+ typeof rule.filename_pattern === "string"
98
+ ? compileFilenamePattern(rule.filename_pattern)
99
+ : null;
100
+
101
+ if (typeof rule.filename_pattern === "string" && filenamePattern === null) {
102
+ file.message(
103
+ `Taxonomy policy for document type '${docType}' has an invalid filename pattern '${rule.filename_pattern}'.`,
104
+ {
105
+ ruleId: RULE_ID,
106
+ source: "@recallnet/remark-lint-docs-taxonomy",
107
+ }
108
+ );
109
+ return;
110
+ }
111
+
112
+ const matchesFilename =
113
+ allowedFilenames.includes(filename) ||
114
+ (filenamePattern ? filenamePattern.test(filename) : true);
115
+
116
+ if (!matchesFilename) {
117
+ const guidance = [];
118
+ if (typeof rule.filename_pattern === "string") {
119
+ guidance.push(`pattern \`${rule.filename_pattern}\``);
120
+ }
121
+ if (allowedFilenames.length > 0) {
122
+ guidance.push(`allowlist ${formatList(allowedFilenames)}`);
123
+ }
124
+
125
+ file.message(
126
+ `Document type '${docType}' at ${filePath} must use a filename matching ${guidance.join(" or ")}.`,
127
+ {
128
+ ruleId: RULE_ID,
129
+ source: "@recallnet/remark-lint-docs-taxonomy",
130
+ }
131
+ );
132
+ }
133
+ };
134
+ }