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.
- package/agents/project/CODING_STYLE.md +2 -0
- package/cli/commands/docs.ts +20 -0
- package/eslint.js +19 -3
- package/package.json +1 -1
- package/tests/docs-check.test.cjs +17 -0
- package/tests/eslint-rules.test.cjs +44 -0
|
@@ -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.
|
package/cli/commands/docs.ts
CHANGED
|
@@ -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 = ({
|
|
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': [
|
|
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.
|
|
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(
|