proteum 2.5.12 → 2.5.13
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/cli/commands/docs.ts +54 -1
- package/package.json +1 -1
- package/tests/docs-check.test.cjs +39 -0
package/cli/commands/docs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { renderStep, renderSuccess, renderTitle, renderWarning } from '../presen
|
|
|
8
8
|
|
|
9
9
|
const { collectDocAnchors } = require('../../docAnchors.js') as {
|
|
10
10
|
collectDocAnchors: (sourceText: string) => {
|
|
11
|
+
adr: string[];
|
|
11
12
|
docs: string[];
|
|
12
13
|
fix: string[];
|
|
13
14
|
entries: { line: number; tag: string; value: string }[];
|
|
@@ -20,7 +21,7 @@ const { collectDocAnchors } = require('../../docAnchors.js') as {
|
|
|
20
21
|
|
|
21
22
|
type TDocsCheckFinding = {
|
|
22
23
|
detail: string;
|
|
23
|
-
kind: 'unresolved-anchor' | 'unanchored-fix-note' | 'orphan-feature-pack';
|
|
24
|
+
kind: 'unresolved-anchor' | 'ambiguous-anchor' | 'unanchored-fix-note' | 'orphan-feature-pack';
|
|
24
25
|
subject: string;
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -96,6 +97,39 @@ const resolveAnchorValue = (value: string, fromDirectory: string, root: string)
|
|
|
96
97
|
return roots.some((candidate) => fs.existsSync(path.resolve(candidate, value)));
|
|
97
98
|
};
|
|
98
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an `@adr` reference to the decision records whose filename starts with
|
|
102
|
+
* it. Returns every match, because two records sharing an identifier makes the
|
|
103
|
+
* reference ambiguous rather than merely valid.
|
|
104
|
+
*/
|
|
105
|
+
const resolveAdrReference = (value: string, fromDirectory: string, root: string) => {
|
|
106
|
+
const normalized = value.trim().toLowerCase();
|
|
107
|
+
if (!normalized) return [];
|
|
108
|
+
|
|
109
|
+
const directories: string[] = [];
|
|
110
|
+
let current = fromDirectory;
|
|
111
|
+
while (current) {
|
|
112
|
+
const candidate = path.join(current, 'docs', 'decisions');
|
|
113
|
+
if (fs.existsSync(candidate)) directories.push(candidate);
|
|
114
|
+
const parent = path.dirname(current);
|
|
115
|
+
if (parent === current) break;
|
|
116
|
+
current = parent;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rootCandidate = path.join(root, 'docs', 'decisions');
|
|
120
|
+
if (fs.existsSync(rootCandidate) && !directories.includes(rootCandidate)) directories.push(rootCandidate);
|
|
121
|
+
|
|
122
|
+
// A project with no decisions corpus never fails this check.
|
|
123
|
+
if (directories.length === 0) return null;
|
|
124
|
+
|
|
125
|
+
return directories.flatMap((directory) =>
|
|
126
|
+
fs
|
|
127
|
+
.readdirSync(directory)
|
|
128
|
+
.filter((entry) => entry.toLowerCase().startsWith(normalized))
|
|
129
|
+
.map((entry) => path.relative(root, path.join(directory, entry))),
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
99
133
|
const listFeaturePacks = (root: string) => {
|
|
100
134
|
const featuresDir = path.join(root, 'docs', 'features');
|
|
101
135
|
if (!fs.existsSync(featuresDir)) return [];
|
|
@@ -143,6 +177,24 @@ export const buildDocsCheckReport = (root: string): TDocsCheckReport => {
|
|
|
143
177
|
findings.push({ detail: `@fix ${value}`, kind: 'unresolved-anchor', subject: relative });
|
|
144
178
|
}
|
|
145
179
|
});
|
|
180
|
+
|
|
181
|
+
anchors.adr.forEach((value) => {
|
|
182
|
+
const matches = resolveAdrReference(value, path.dirname(filepath), root);
|
|
183
|
+
if (matches === null) return;
|
|
184
|
+
|
|
185
|
+
if (matches.length === 0) {
|
|
186
|
+
findings.push({ detail: `@adr ${value}`, kind: 'unresolved-anchor', subject: relative });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (matches.length > 1) {
|
|
191
|
+
findings.push({
|
|
192
|
+
detail: `@adr ${value} matches ${matches.length} records: ${matches.join(', ')}`,
|
|
193
|
+
kind: 'ambiguous-anchor',
|
|
194
|
+
subject: relative,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
146
198
|
});
|
|
147
199
|
|
|
148
200
|
listFixNotesRequiringAnchor(root).forEach((note) => {
|
|
@@ -202,6 +254,7 @@ export const run = async (): Promise<void> => {
|
|
|
202
254
|
{ label: 'files anchored', value: String(report.anchoredFiles) },
|
|
203
255
|
]),
|
|
204
256
|
renderFindings(report.findings, 'unresolved-anchor', 'Unresolved anchors'),
|
|
257
|
+
renderFindings(report.findings, 'ambiguous-anchor', 'Ambiguous anchors'),
|
|
205
258
|
renderFindings(report.findings, 'unanchored-fix-note', 'Fix notes with no code anchor'),
|
|
206
259
|
renderFindings(report.findings, 'orphan-feature-pack', 'Feature packs with no inbound anchor'),
|
|
207
260
|
].join('\n\n'),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.13",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -83,6 +83,45 @@ test('docs check resolves anchors aimed at the repo corpus when run from an app
|
|
|
83
83
|
assert.equal(report.anchoredFiles, 1);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test('docs check resolves an adr anchor against the decisions corpus', () => {
|
|
87
|
+
const root = createRoot();
|
|
88
|
+
writeFile(root, 'docs/decisions/ADR-0004-page-query-contracts.md', '# ADR-0004\n');
|
|
89
|
+
writeFile(root, 'apps/product/server/controllers/search.ts', '/**\n * @adr ADR-0004\n */\nexport default {};\n');
|
|
90
|
+
|
|
91
|
+
assert.equal(kinds(buildDocsCheckReport(root), 'unresolved-anchor').length, 0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('docs check reports an adr anchor matching no decision record', () => {
|
|
95
|
+
const root = createRoot();
|
|
96
|
+
writeFile(root, 'docs/decisions/ADR-0004-page-query-contracts.md', '# ADR-0004\n');
|
|
97
|
+
writeFile(root, 'apps/product/server/controllers/search.ts', '/**\n * @adr ADR-9999\n */\nexport default {};\n');
|
|
98
|
+
|
|
99
|
+
const unresolved = kinds(buildDocsCheckReport(root), 'unresolved-anchor');
|
|
100
|
+
|
|
101
|
+
assert.equal(unresolved.length, 1);
|
|
102
|
+
assert.equal(unresolved[0].detail, '@adr ADR-9999');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('docs check reports an adr identifier shared by two decision records', () => {
|
|
106
|
+
const root = createRoot();
|
|
107
|
+
writeFile(root, 'docs/decisions/ADR-0010-catalog-provenance.md', '# ADR-0010\n');
|
|
108
|
+
writeFile(root, 'docs/decisions/ADR-0010-visual-system.md', '# ADR-0010\n');
|
|
109
|
+
writeFile(root, 'apps/product/server/controllers/search.ts', '/**\n * @adr ADR-0010\n */\nexport default {};\n');
|
|
110
|
+
|
|
111
|
+
const ambiguous = kinds(buildDocsCheckReport(root), 'ambiguous-anchor');
|
|
112
|
+
|
|
113
|
+
assert.equal(ambiguous.length, 1);
|
|
114
|
+
assert.equal(/matches 2 records/.test(ambiguous[0].detail), true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('docs check stays silent about adr anchors when a project has no decisions corpus', () => {
|
|
118
|
+
const root = createRoot();
|
|
119
|
+
writeFile(root, 'docs/features/search/README.md', '# Search\n');
|
|
120
|
+
writeFile(root, 'apps/product/server/controllers/search.ts', '/**\n * @adr ADR-0004\n */\nexport default {};\n');
|
|
121
|
+
|
|
122
|
+
assert.equal(kinds(buildDocsCheckReport(root), 'unresolved-anchor').length, 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
86
125
|
test('docs check reports a fix note whose invariant no code anchors', () => {
|
|
87
126
|
const root = createRoot();
|
|
88
127
|
writeFile(root, 'docs/fixes/2026-06-14-watchdog.md', '# Fix\n\n## Agent warning\n\nDo not remove the guards.\n');
|