proteum 2.5.13 → 2.5.14

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.
@@ -85,6 +85,8 @@ Write anchors in a leading block comment:
85
85
 
86
86
  - `@docs` points at the feature pack that owns the file. Required on every file that default-exports `definePageRoute`, `defineController`, `defineServerRoute`, or `defineServerRoutes`, and on every exported class extending a service base such as `Service` or `UsersManagementService`. Error routes are exempt: they render a status message and carry no feature-specific rule, so requiring a pack for one would manufacture documentation. Add an anchor to an error route only when it really does carry a rule.
87
87
  - Components are not covered automatically. A presentational primitive such as `Icon.tsx` or `Card.tsx` owns no feature, so a blanket rule would manufacture documentation for hundreds of files. Cover the component directories that do own a feature by listing them in `includeDocAnchors` when building the ESLint config, for example `['client/components/paywall/**']`.
88
+ - Infrastructure is exempt the same way error routes are. A transport helper, a metrics router or a rate limiter owns no feature contract, so list those paths in `excludeDocAnchors` rather than pointing them at a pack that does not describe them. An excluded file may still declare anchors, and `valid-doc-anchor` keeps checking any it declares.
89
+ - A feature pack that no code will ever point at, such as a retirement record or a document describing audiences rather than modules, opts out of the orphan report by carrying a `RETIRED` note or a `code-owned: false` line in its README. Without that, the pack is reported as orphaned forever and the backlog stops being actionable.
88
90
  - `@adr` and `@fix` point at the decision record and fix note that constrain the file. Add them where the decision or the bug actually lives, not on every file in the area.
89
91
  - `@rule` states the invariant inline, in full. It is the one anchor that carries content rather than a pointer, because the rule is what an agent needs at the moment of editing. A `@rule` that only says `todo` or repeats the linked title is a defect.
90
92
  - Anchors are not a substitute for the documents. Narrative, alternatives, benchmarks and acceptance stay under `docs/**`; the anchor carries the pointer and the single-sentence rule.
@@ -130,6 +130,25 @@ const resolveAdrReference = (value: string, fromDirectory: string, root: string)
130
130
  );
131
131
  };
132
132
 
133
+ const retiredPackPattern = /^\s*>?\s*RETIRED\b/im;
134
+ const notCodeOwnedPattern = /^\s*>?\s*code-owned:\s*false\s*$/im;
135
+
136
+ /**
137
+ * A pack opts out of the orphan report by saying so in its README, either with
138
+ * a `RETIRED` note or a `code-owned: false` line.
139
+ *
140
+ * Retirement records and intent documents describe decisions and audiences, not
141
+ * modules, so no source file will ever point at them. Reporting those forever
142
+ * trains a reader to ignore the whole list.
143
+ */
144
+ const packOptsOutOfOrphanReport = (featuresDir: string, pack: string) => {
145
+ const readme = path.join(featuresDir, pack, 'README.md');
146
+ if (!fs.existsSync(readme)) return false;
147
+
148
+ const content = fs.readFileSync(readme, 'utf8');
149
+ return retiredPackPattern.test(content) || notCodeOwnedPattern.test(content);
150
+ };
151
+
133
152
  const listFeaturePacks = (root: string) => {
134
153
  const featuresDir = path.join(root, 'docs', 'features');
135
154
  if (!fs.existsSync(featuresDir)) return [];
@@ -137,6 +156,7 @@ const listFeaturePacks = (root: string) => {
137
156
  return fs
138
157
  .readdirSync(featuresDir, { withFileTypes: true })
139
158
  .filter((entry) => entry.isDirectory())
159
+ .filter((entry) => !packOptsOutOfOrphanReport(featuresDir, entry.name))
140
160
  .map((entry) => entry.name);
141
161
  };
142
162
 
package/eslint.js CHANGED
@@ -545,6 +545,7 @@ const createRequireDocAnchorRule = () => ({
545
545
  type: 'object',
546
546
  properties: {
547
547
  definitions: { type: 'array', items: { type: 'string' } },
548
+ exclude: { type: 'array', items: { type: 'string' } },
548
549
  include: { type: 'array', items: { type: 'string' } },
549
550
  requiredTags: { type: 'array', items: { enum: docAnchorTags } },
550
551
  serviceBasePattern: { type: 'string' },
@@ -558,15 +559,22 @@ const createRequireDocAnchorRule = () => ({
558
559
  const definitions = options.definitions || defaultDocAnchorDefinitions;
559
560
  const requiredTags = options.requiredTags || ['docs'];
560
561
  const includes = options.include || [];
562
+ const excludes = options.exclude || [];
561
563
  const serviceBasePattern = new RegExp(options.serviceBasePattern || 'Service$');
562
564
  const filename = context.filename || context.getFilename?.() || '';
563
565
 
566
+ // Infrastructure carries no feature contract, so a project lists those
567
+ // paths here rather than anchoring them to a pack that does not exist.
568
+ // An excluded file may still declare anchors, and `valid-doc-anchor`
569
+ // keeps checking them.
570
+ const excluded = matchesIncludeGlob(filename, excludes);
571
+
564
572
  let reported = false;
565
573
 
566
574
  const reportMissing = (node, subject) => {
567
575
  // One report per file: a service class and an include glob can both
568
576
  // match, and repeating the same instruction adds no information.
569
- if (reported) return;
577
+ if (reported || excluded) return;
570
578
 
571
579
  const anchors = collectFileDocAnchors(context);
572
580
  if (anchors.entries.length === 0) {
@@ -661,7 +669,12 @@ const createValidDocAnchorRule = () => ({
661
669
  },
662
670
  });
663
671
 
664
- const createProteumEslintConfig = ({ docAnchors = 'warn', includeDocAnchors = [], ignores = [] } = {}) => [
672
+ const createProteumEslintConfig = ({
673
+ docAnchors = 'warn',
674
+ excludeDocAnchors = [],
675
+ includeDocAnchors = [],
676
+ ignores = [],
677
+ } = {}) => [
665
678
  {
666
679
  ignores: [...defaultIgnores, ...ignores],
667
680
  },
@@ -706,7 +719,10 @@ const createProteumEslintConfig = ({ docAnchors = 'warn', includeDocAnchors = []
706
719
  // `includeDocAnchors` opts in extra paths, which is how a project
707
720
  // covers the feature-owning components without dragging in every
708
721
  // presentational primitive.
709
- 'proteum/require-doc-anchor': [docAnchors, { include: includeDocAnchors }],
722
+ 'proteum/require-doc-anchor': [
723
+ docAnchors,
724
+ { exclude: excludeDocAnchors, include: includeDocAnchors },
725
+ ],
710
726
  // A stale anchor is always an error: it only fires on files that
711
727
  // already opted in, and a pointer to a deleted document is worse
712
728
  // than no pointer at all.
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.13",
4
+ "version": "2.5.14",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -160,6 +160,23 @@ test('docs check reports a feature pack that no code points at', () => {
160
160
  assert.deepEqual(orphans.map((finding) => finding.subject), ['docs/features/orphan']);
161
161
  });
162
162
 
163
+ test('docs check lets a retired or non-code pack opt out of the orphan report', () => {
164
+ const root = createRoot();
165
+ writeFile(root, 'docs/features/search/README.md', '# Search\n');
166
+ writeFile(root, 'docs/features/pro-preview/README.md', '# Pro Preview\n\n> RETIRED 2026-07-07. Superseded.\n');
167
+ writeFile(root, 'docs/features/persona-journeys/README.md', '# Personas\n\ncode-owned: false\n');
168
+ writeFile(root, 'docs/features/still-orphaned/README.md', '# Orphan\n');
169
+ writeFile(
170
+ root,
171
+ 'apps/product/server/controllers/search.ts',
172
+ '/**\n * @docs docs/features/search\n */\nexport default {};\n',
173
+ );
174
+
175
+ const orphans = kinds(buildDocsCheckReport(root), 'orphan-feature-pack');
176
+
177
+ assert.deepEqual(orphans.map((finding) => finding.subject), ['docs/features/still-orphaned']);
178
+ });
179
+
163
180
  test('docs check ignores generated and vendored directories', () => {
164
181
  const root = createRoot();
165
182
  writeFile(root, 'docs/features/search/README.md', '# Search\n');
@@ -541,6 +541,50 @@ test('proteum lint accepts an included file once it carries an anchor', () => {
541
541
  assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
542
542
  });
543
543
 
544
+ test('proteum lint exempts infrastructure paths listed in excludeDocAnchors', () => {
545
+ const { root } = createDocProject();
546
+ const options = { docAnchors: 'warn', excludeDocAnchors: ['server/services/Utils/**', 'server/routes/debug.ts'] };
547
+
548
+ const utilService = lint(
549
+ `export default class FetchService extends Service {}`,
550
+ path.join(root, 'server', 'services', 'Utils', 'Fetch', 'index.ts'),
551
+ options,
552
+ );
553
+ assert.equal(messagesFor(utilService, requireDocAnchorRuleId).length, 0);
554
+
555
+ const debugRoute = lint(
556
+ `export default defineServerRoutes(() => null);`,
557
+ path.join(root, 'server', 'routes', 'debug.ts'),
558
+ options,
559
+ );
560
+ assert.equal(messagesFor(debugRoute, requireDocAnchorRuleId).length, 0);
561
+
562
+ // A service outside the excluded paths is still required to anchor.
563
+ const covered = lint(
564
+ `export default class SearchService extends Service {}`,
565
+ path.join(root, 'server', 'services', 'Domains', 'search', 'index.ts'),
566
+ options,
567
+ );
568
+ assert.equal(messagesFor(covered, requireDocAnchorRuleId).length, 1);
569
+ });
570
+
571
+ test('proteum lint still validates anchors declared on an excluded file', () => {
572
+ const { root } = createDocProject();
573
+ const messages = lint(
574
+ `
575
+ /**
576
+ * @docs docs/features/deleted-feature
577
+ */
578
+ export default class FetchService extends Service {}
579
+ `,
580
+ path.join(root, 'server', 'services', 'Utils', 'Fetch', 'index.ts'),
581
+ { docAnchors: 'warn', excludeDocAnchors: ['server/services/Utils/**'] },
582
+ );
583
+
584
+ assert.equal(messagesFor(messages, requireDocAnchorRuleId).length, 0);
585
+ assert.equal(messagesFor(messages, validDocAnchorRuleId).length, 1);
586
+ });
587
+
544
588
  test('proteum lint does not require a doc anchor on error routes', () => {
545
589
  const { root } = createDocProject();
546
590
  const messages = lint(