@recallnet/remark-lint-docs-freshness 0.1.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.
- package/README.md +10 -0
- package/package.json +40 -0
- package/src/index.js +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @recallnet/remark-lint-docs-freshness
|
|
2
|
+
|
|
3
|
+
Checks Markdown frontmatter `reviewed` dates against policy-defined `max_age_days`.
|
|
4
|
+
|
|
5
|
+
Use with `remark-frontmatter`:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
9
|
+
import remarkLintDocsFreshness from "@recallnet/remark-lint-docs-freshness";
|
|
10
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@recallnet/remark-lint-docs-freshness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remark plugin that checks docs reviewed dates against repo policy.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/recallnet/remark.git",
|
|
10
|
+
"directory": "packages/remark-lint-docs-freshness"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/recallnet/remark",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/recallnet/remark/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"remark",
|
|
18
|
+
"remark-lint",
|
|
19
|
+
"frontmatter",
|
|
20
|
+
"documentation",
|
|
21
|
+
"freshness"
|
|
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,109 @@
|
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_POLICY_PATH,
|
|
5
|
+
isDocsPathInScope,
|
|
6
|
+
loadDocsPolicy,
|
|
7
|
+
matchesDocsPolicyPattern,
|
|
8
|
+
normalizePath,
|
|
9
|
+
resolveDocsReviewPolicy,
|
|
10
|
+
resolveReviewPolicyConfig,
|
|
11
|
+
} from "@recallnet/docs-governance-policy";
|
|
12
|
+
import { parse } from "yaml";
|
|
13
|
+
|
|
14
|
+
const RULE_ID = "docs-freshness";
|
|
15
|
+
|
|
16
|
+
function findYamlNode(tree) {
|
|
17
|
+
return tree?.children?.find((node) => node.type === "yaml") ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseFrontmatter(tree) {
|
|
21
|
+
const yamlNode = findYamlNode(tree);
|
|
22
|
+
if (!yamlNode?.value) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = parse(String(yamlNode.value));
|
|
27
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dateDiffDays(older, newer) {
|
|
31
|
+
const olderMs = new Date(`${older}T00:00:00Z`).getTime();
|
|
32
|
+
const newerMs = new Date(`${newer}T00:00:00Z`).getTime();
|
|
33
|
+
return Math.floor((newerMs - olderMs) / 86400000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function remarkLintDocsFreshness(options = {}) {
|
|
37
|
+
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
|
38
|
+
const policyPath = options.policyPath ?? DEFAULT_POLICY_PATH;
|
|
39
|
+
|
|
40
|
+
// @context decision !high [verified:2026-03-25] — Freshness resolves policy from repo state on every file run.
|
|
41
|
+
// That keeps single-file editor linting aligned with whole-repo QA instead of relying on external batching state.
|
|
42
|
+
return (tree, file) => {
|
|
43
|
+
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
44
|
+
const filePath = file.path ? normalizePath(relative(cwd, file.path)) : "";
|
|
45
|
+
const { policy } = loadDocsPolicy({ cwd, policyPath });
|
|
46
|
+
|
|
47
|
+
if (!filePath || !isDocsPathInScope(filePath, policy)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const excluded = Array.isArray(policy.frontmatter_exclude_globs)
|
|
52
|
+
? policy.frontmatter_exclude_globs.some((pattern) =>
|
|
53
|
+
matchesDocsPolicyPattern(filePath, pattern)
|
|
54
|
+
)
|
|
55
|
+
: false;
|
|
56
|
+
|
|
57
|
+
if (excluded) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const frontmatter = parseFrontmatter(tree);
|
|
62
|
+
if (!frontmatter) {
|
|
63
|
+
file.message("Document freshness could not be checked because frontmatter is unreadable.", {
|
|
64
|
+
ruleId: RULE_ID,
|
|
65
|
+
source: "@recallnet/remark-lint-docs-freshness",
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reviewPolicyId = String(
|
|
71
|
+
frontmatter.review_policy || resolveDocsReviewPolicy(filePath, policy) || ""
|
|
72
|
+
);
|
|
73
|
+
const reviewPolicy = resolveReviewPolicyConfig(reviewPolicyId, policy);
|
|
74
|
+
if (!reviewPolicy) {
|
|
75
|
+
file.message(`Unknown review policy '${reviewPolicyId}'.`, {
|
|
76
|
+
ruleId: RULE_ID,
|
|
77
|
+
source: "@recallnet/remark-lint-docs-freshness",
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (reviewPolicy.mode !== "periodic") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const reviewed = String(frontmatter.reviewed || "");
|
|
87
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(reviewed)) {
|
|
88
|
+
file.message(
|
|
89
|
+
"Frontmatter field 'reviewed' must be a YYYY-MM-DD date for periodic review policies.",
|
|
90
|
+
{
|
|
91
|
+
ruleId: RULE_ID,
|
|
92
|
+
source: "@recallnet/remark-lint-docs-freshness",
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ageDays = dateDiffDays(reviewed, today);
|
|
99
|
+
if (ageDays > Number(reviewPolicy.max_age_days || 0)) {
|
|
100
|
+
file.message(
|
|
101
|
+
`Document is stale: reviewed=${reviewed} age_days=${ageDays} max_age_days=${reviewPolicy.max_age_days}.`,
|
|
102
|
+
{
|
|
103
|
+
ruleId: RULE_ID,
|
|
104
|
+
source: "@recallnet/remark-lint-docs-freshness",
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|