@micelio/eslint-config 0.2.5 → 0.2.7

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 CHANGED
@@ -31,6 +31,14 @@ version `AGENTS.md` pins (`` `standard: X.Y` ``), using the SHA-256 hashes in
31
31
  and to `npm run check`. A pinned version newer than the installed package knows fails with a message
32
32
  that says to upgrade the package.
33
33
 
34
+ A repository may also pin its profiles on the same line, comma-separated (`` `standard: 2.0` ``,
35
+ `` `profile: typescript, site-engine` ``; ADR-0001); with no `profile:` pin it is read as
36
+ `typescript`. In `standard-hashes.json` a version maps to one hash (1.4 to 1.8, one file) or to
37
+ `{ "core": "<sha256>", "profiles": { "<profile>": "<sha256>" } }`; for the second form the command
38
+ also compares `docs/standard/<profile>.md` for each pinned profile and names the file that differs.
39
+ A profile the pinned version does not have fails, naming the ones it has (a one-file version has
40
+ `typescript` and `site-engine`).
41
+
34
42
  ## `lint:ee`
35
43
 
36
44
  `micelio-lint-ee [--allow <path>]... [root]` fails when a file git tracks enables Strapi Enterprise
@@ -1,7 +1,20 @@
1
1
  /** Where every Micelio repository keeps its copy of the standard. */
2
2
  export declare const STANDARD_PATH = "docs/engineering-standard.md";
3
- /** The SHA-256 of each released version of the standard, by `major.minor`. */
4
- export type StandardHashes = Record<string, string>;
3
+ /**
4
+ * The hashes of a version in the new form (ADR-0001, point 5): the core, at
5
+ * `docs/engineering-standard.md`, and each profile file, at `docs/standard/<profile>.md`.
6
+ */
7
+ export interface VersionHashes {
8
+ /** The SHA-256 of `docs/engineering-standard.md`. */
9
+ core: string;
10
+ /** The SHA-256 of `docs/standard/<profile>.md`, by profile name. */
11
+ profiles: Record<string, string>;
12
+ }
13
+ /**
14
+ * The hashes of each released version of the standard, by `major.minor`: one string for a version
15
+ * that is a single file (1.4 to 1.8), or {@link VersionHashes} for a version in the new form.
16
+ */
17
+ export type StandardHashes = Record<string, string | VersionHashes>;
5
18
  /** The SHA-256 of each synced file (agent prompts, skills, pull request template), by path in the repository. */
6
19
  export type FileHashes = Record<string, string>;
7
20
  /** What `lintStandard` found. */
@@ -16,21 +29,38 @@ export declare function hashOf(content: Buffer | string): string;
16
29
  /** Reads the version a repository pins in its `AGENTS.md`, or `null` when there is no pin. */
17
30
  export declare function readPin(agents: string): string | null;
18
31
  /**
19
- * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`.
32
+ * Reads the profiles a repository pins in its `AGENTS.md`, in the order written: the names after
33
+ * `profile:`, in backticks, comma-separated (`profile: typescript, site-engine`), on the line of the
34
+ * version pin. A name is returned as written, malformed or not.
35
+ *
36
+ * @returns The profile names; `['typescript']` when the line of the version pin has no `profile:`
37
+ * pin, or there is no version pin (ADR-0001, point 3).
38
+ */
39
+ export declare function readProfiles(agents: string): string[];
40
+ /**
41
+ * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`. A
42
+ * `profile:` pin is left as it is.
20
43
  *
21
44
  * @returns The new text, or `null` when `agents` has no pin to rewrite.
22
45
  */
23
46
  export declare function setPin(agents: string, version: string): string | null;
24
47
  /** Reads the version a copy of the standard says it is, or `null` when its header is missing. */
25
48
  export declare function standardVersion(standard: string): string | null;
26
- /** Loads the hash of each released version of the standard bundled in this package. */
27
- export declare function loadHashes(): StandardHashes;
49
+ /**
50
+ * Loads the hashes of each released version of the standard bundled in this package. An entry that
51
+ * is neither one hash nor a version in the new form is left out.
52
+ *
53
+ * @param file - The parsed `standard-hashes.json`; the one bundled in this package by default.
54
+ */
55
+ export declare function loadHashes(file?: Record<string, unknown>): StandardHashes;
28
56
  /** Loads the hash of each file the sync carries next to the standard, by path in the repository. */
29
57
  export declare function loadFileHashes(): FileHashes;
30
58
  /**
31
59
  * Checks that the copy of the standard in `root` is the one published for the version its
32
60
  * `AGENTS.md` pins (standard, section 16). A repository may pin an older version on purpose:
33
- * the pinned one is checked, not the latest.
61
+ * the pinned one is checked, not the latest. For a version in the new form it also checks the file
62
+ * of each profile `AGENTS.md` pins (`typescript` when it pins none), and fails on a profile that
63
+ * version does not have.
34
64
  *
35
65
  * @param root - The repository root.
36
66
  * It also checks the synced files the repository has (`.claude/agents/*.md`, the skills, the pull
package/dist/standard.js CHANGED
@@ -3,8 +3,25 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  /** Where every Micelio repository keeps its copy of the standard. */
5
5
  export const STANDARD_PATH = 'docs/engineering-standard.md';
6
+ /** Where a repository keeps the file of each profile it pins: `docs/standard/<profile>.md`. */
7
+ const PROFILE_DIR = 'docs/standard';
8
+ /** The profile a repository with no `profile:` pin is read as (ADR-0001, point 3). */
9
+ const DEFAULT_PROFILE = 'typescript';
6
10
  /** The machine-readable pin in `AGENTS.md`: the version in backticks after `standard:`. */
7
11
  const PIN = /(`standard:\s*)(\d+\.\d+)(`)/;
12
+ /**
13
+ * The profile pin in `AGENTS.md`: what follows `profile:` in backticks, on the line of the version
14
+ * pin (ADR-0001, point 3). The names are taken as written, so a malformed one fails as a profile the
15
+ * version does not have.
16
+ */
17
+ const PROFILE_PIN = /`profile:([^`]*)`/;
18
+ /**
19
+ * The profiles a version in the old form has: its one file holds the rules that the split moves to
20
+ * `typescript` and `site-engine` (ADR-0001, point 1).
21
+ */
22
+ const SINGLE_FILE_PROFILES = [DEFAULT_PROFILE, 'site-engine'];
23
+ /** A key of `standard-hashes.json` that names a version, as opposed to `files`. */
24
+ const VERSION_KEY = /^\d+\.\d+$/;
8
25
  /** The sentence of `AGENTS.md` that says the same in words, kept in step with the pin. */
9
26
  const PIN_IN_WORDS = /(follows standard version )(\d+\.\d+)/;
10
27
  /** The version line of the standard itself: `Version 1.4 (2026-10-20).` */
@@ -18,7 +35,21 @@ export function readPin(agents) {
18
35
  return PIN.exec(agents)?.[2] ?? null;
19
36
  }
20
37
  /**
21
- * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`.
38
+ * Reads the profiles a repository pins in its `AGENTS.md`, in the order written: the names after
39
+ * `profile:`, in backticks, comma-separated (`profile: typescript, site-engine`), on the line of the
40
+ * version pin. A name is returned as written, malformed or not.
41
+ *
42
+ * @returns The profile names; `['typescript']` when the line of the version pin has no `profile:`
43
+ * pin, or there is no version pin (ADR-0001, point 3).
44
+ */
45
+ export function readProfiles(agents) {
46
+ const line = agents.split('\n').find((text) => PIN.test(text)) ?? '';
47
+ const pin = PROFILE_PIN.exec(line)?.[1];
48
+ return pin === undefined ? [DEFAULT_PROFILE] : pin.split(',').map((name) => name.trim());
49
+ }
50
+ /**
51
+ * Rewrites the pin of an `AGENTS.md`, and the sentence that repeats it, to `version`. A
52
+ * `profile:` pin is left as it is.
22
53
  *
23
54
  * @returns The new text, or `null` when `agents` has no pin to rewrite.
24
55
  */
@@ -36,11 +67,31 @@ function readHashesFile() {
36
67
  const file = new URL('../standard-hashes.json', import.meta.url);
37
68
  return JSON.parse(readFileSync(file, 'utf8'));
38
69
  }
39
- /** Loads the hash of each released version of the standard bundled in this package. */
40
- export function loadHashes() {
70
+ /** True when `value` is a map of strings to strings. */
71
+ function isStringMap(value) {
72
+ return (typeof value === 'object' &&
73
+ value !== null &&
74
+ Object.values(value).every((hash) => typeof hash === 'string'));
75
+ }
76
+ /** True when `value` is the entry of a version in the new form. */
77
+ function isVersionHashes(value) {
78
+ if (typeof value !== 'object' || value === null)
79
+ return false;
80
+ const entry = value;
81
+ return typeof entry.core === 'string' && isStringMap(entry.profiles);
82
+ }
83
+ /**
84
+ * Loads the hashes of each released version of the standard bundled in this package. An entry that
85
+ * is neither one hash nor a version in the new form is left out.
86
+ *
87
+ * @param file - The parsed `standard-hashes.json`; the one bundled in this package by default.
88
+ */
89
+ export function loadHashes(file = readHashesFile()) {
41
90
  const hashes = {};
42
- for (const [key, value] of Object.entries(readHashesFile())) {
43
- if (typeof value === 'string')
91
+ for (const [key, value] of Object.entries(file)) {
92
+ if (!VERSION_KEY.test(key))
93
+ continue;
94
+ if (typeof value === 'string' || isVersionHashes(value))
44
95
  hashes[key] = value;
45
96
  }
46
97
  return hashes;
@@ -64,10 +115,16 @@ function latestVersion(hashes) {
64
115
  });
65
116
  return latest ?? null;
66
117
  }
118
+ /** The path of the file of `profile` in a repository. */
119
+ function profilePath(profile) {
120
+ return `${PROFILE_DIR}/${profile}.md`;
121
+ }
67
122
  /**
68
123
  * Checks that the copy of the standard in `root` is the one published for the version its
69
124
  * `AGENTS.md` pins (standard, section 16). A repository may pin an older version on purpose:
70
- * the pinned one is checked, not the latest.
125
+ * the pinned one is checked, not the latest. For a version in the new form it also checks the file
126
+ * of each profile `AGENTS.md` pins (`typescript` when it pins none), and fails on a profile that
127
+ * version does not have.
71
128
  *
72
129
  * @param root - The repository root.
73
130
  * It also checks the synced files the repository has (`.claude/agents/*.md`, the skills, the pull
@@ -85,7 +142,8 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
85
142
  message: 'AGENTS.md is missing: it pins the standard version (`standard: X.Y`).',
86
143
  };
87
144
  }
88
- const pin = readPin(readFileSync(agentsPath, 'utf8'));
145
+ const agents = readFileSync(agentsPath, 'utf8');
146
+ const pin = readPin(agents);
89
147
  if (!pin) {
90
148
  return {
91
149
  ok: false,
@@ -104,13 +162,45 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
104
162
  message: `Standard ${pin}, pinned in AGENTS.md, is unknown to this version of @micelio/eslint-config (it knows ${known}): upgrade the package or fix the pin.`,
105
163
  };
106
164
  }
165
+ const core = typeof expected === 'string' ? expected : expected.core;
166
+ const profiles = readProfiles(agents);
167
+ const known = typeof expected === 'string' ? SINGLE_FILE_PROFILES : Object.keys(expected.profiles);
168
+ const unknown = profiles.filter((profile) => !known.includes(profile));
169
+ if (unknown.length > 0) {
170
+ return {
171
+ ok: false,
172
+ message: `AGENTS.md pins profile ${unknown.join(', ')}, which standard ${pin} does not have (it has ${known.join(', ')}): fix the profile pin.`,
173
+ };
174
+ }
107
175
  const actual = hashOf(readFileSync(copyPath));
108
- if (actual !== expected) {
176
+ if (actual !== core) {
109
177
  return {
110
178
  ok: false,
111
- message: `${STANDARD_PATH} does not match standard ${pin}, the version AGENTS.md pins (sha256 ${actual}, published ${expected}). Restore it, or sync it from bogd3v/micelio-engineering.`,
179
+ message: `${STANDARD_PATH} does not match standard ${pin}, the version AGENTS.md pins (sha256 ${actual}, published ${core}). Restore it, or sync it from bogd3v/micelio-engineering.`,
112
180
  };
113
181
  }
182
+ const checked = [STANDARD_PATH];
183
+ if (typeof expected !== 'string') {
184
+ for (const profile of profiles) {
185
+ const path = profilePath(profile);
186
+ const file = join(root, path);
187
+ if (!existsSync(file)) {
188
+ return {
189
+ ok: false,
190
+ message: `${path} is missing; AGENTS.md pins standard ${pin} with profile ${profile}.`,
191
+ };
192
+ }
193
+ const published = expected.profiles[profile];
194
+ const hash = hashOf(readFileSync(file));
195
+ if (hash !== published) {
196
+ return {
197
+ ok: false,
198
+ message: `${path} does not match profile ${profile} of standard ${pin}, the version AGENTS.md pins (sha256 ${hash}, published ${published}). Restore it, or sync it from bogd3v/micelio-engineering.`,
199
+ };
200
+ }
201
+ checked.push(path);
202
+ }
203
+ }
114
204
  const stale = pin === latestVersion(hashes)
115
205
  ? Object.entries(fileHashes)
116
206
  .filter(([path, hash]) => {
@@ -125,5 +215,11 @@ export function lintStandard(root, hashes = loadHashes(), fileHashes = loadFileH
125
215
  message: `${stale.join(', ')} ${stale.length === 1 ? 'does' : 'do'} not match the published copy of standard ${pin}. Restore ${stale.length === 1 ? 'it' : 'them'} if edited by hand, or, in a sync pull request, upgrade @micelio/eslint-config to the release that publishes the new hashes.`,
126
216
  };
127
217
  }
128
- return { ok: true, message: `Standard ${pin}: ${STANDARD_PATH} matches the published copy.` };
218
+ if (checked.length === 1) {
219
+ return { ok: true, message: `Standard ${pin}: ${STANDARD_PATH} matches the published copy.` };
220
+ }
221
+ return {
222
+ ok: true,
223
+ message: `Standard ${pin} (${profiles.join(', ')}): ${checked.join(', ')} match the published copy.`,
224
+ };
129
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micelio/eslint-config",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "The ESLint flat config of the Micelio repositories, the licence allowlist behind lint:licenses and lint:standard.",
5
5
  "license": "MIT-0",
6
6
  "repository": {
@@ -6,14 +6,14 @@
6
6
  "1.8": "bbdbd8b0f7d394ba24471a3b65dbcf7a82b9899a221a39e78ffe819ebbc79249",
7
7
  "files": {
8
8
  ".claude/agents/master.md": "35e9ce4cc637ca814352916dd24e64f25da66f0c3c36fb492e2172c40e6d6416",
9
- ".claude/agents/planner.md": "548a7b62a14c0c07067004fb57934bf572e308063e3ed390262e1d7a69e9fea0",
9
+ ".claude/agents/planner.md": "7bb5a71f7ff8ebf97a6e775c1e85744d668c7ef2601cd8dc52599b9a60c851e5",
10
10
  ".claude/agents/reviewer.md": "8edcd83631b191013217eb9f309b108f54e988708496182877077b65a9b6f1cf",
11
- ".claude/agents/verifier.md": "7fc1c73eec7c64ba669ece73075c0f967b316d81c9389a00ae729acf29505ae8",
12
- ".claude/agents/worker.md": "3f4dafea2266a752a5aba7f26035bdab1c5a4b6591916f0c2be4fdb68ad924ca",
13
- ".claude/skills/ship-pr/SKILL.md": "bd01f1bfcccfef9f50872425f1a2060554a36fa61e6915cfb137e07889c25f25",
11
+ ".claude/agents/verifier.md": "12af503892295de5da0496344b7760c5a349aa81ab7b61db4e9b9b7e1dd757e8",
12
+ ".claude/agents/worker.md": "e2faeb86c51418373978a15dd025f7800ad12a053c962fdb0c58b79251fd8290",
13
+ ".claude/skills/ship-pr/SKILL.md": "ced49dbd6ab9c4513afa2c3afb0350810d3693c4a758612512f9ecadab66f045",
14
14
  ".claude/skills/blind-review/SKILL.md": "62f43d0ccd762ad7197bb48e31e7041689cc1d07e16678dffd9c8e2715be7980",
15
- ".claude/skills/verify-pr/SKILL.md": "6143ac741580154a3121b0ec77c601bbb8345816f53e4947879365c187a6c6a5",
16
- ".github/pull_request_template.md": "10ab5b606b86dc13682387e7a9aed7ce2cf8ef1a227e80cafa1dd8fc50f4481c",
15
+ ".claude/skills/verify-pr/SKILL.md": "3e284973a3c54f5c19de43282307060b876816701112ddc8e922be2773f7469f",
16
+ ".github/pull_request_template.md": "8ce62f8b6d59f14562e6d962d87491c2c3b1c9a8f640afe673f23eced6642ba7",
17
17
  "CLAUDE.md": "2fe9efbc0c4a6e0cbd446afb506911ba28b0068a42f8686676635986d968cefd"
18
18
  }
19
19
  }