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.
@@ -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.12",
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');