@ogygie/core 0.11.1 → 0.12.0

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
@@ -58,7 +58,7 @@ core tel quel.
58
58
  4. **Zod aux frontières.** Toute entrée/sortie a un schéma Zod, source
59
59
  unique de vérité (validation runtime + type inféré + doc) ; le graphe
60
60
  d'un `schemas.ts` n'autorise par défaut QUE `zod` (`contract-purity`,
61
- D29).
61
+ AGENTS.md §Structure — l'exception des CONTRATS).
62
62
  5. **Une devise de travail par tenant.** L'argent est un entier en centimes
63
63
  + code ISO 4217 ; aucune conversion de change ne traverse le core. Les
64
64
  futurs helpers monétaires naîtront ici sous cette hypothèse.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/core",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Ogygie framework core: manifest/spec/error schemas, generic tenancy and gate tooling. Ships TypeScript sources — Ogygie products run tsx/vitest by construction (D1).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @intent Mutation tests of the bundle-budget gate logic (D22: a known
2
+ * @intent Mutation tests of the bundle-budget gate logic (AGENTS.md workflow 7: a known
3
3
  * violation must be DETECTED, not just the happy path).
4
4
  */
5
5
  import { describe, expect, it } from "vitest";
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @intent Testable logic of the CI gate `ogygie:verify-bundle` (D22: a gate is
2
+ * @intent Testable logic of the CI gate `ogygie:verify-bundle` (AGENTS.md
3
+ * workflow 7: a gate is
3
4
  * born with its mutation test — see bundle-budget.test.ts). PURE on purpose
4
5
  * (no node imports — the core tsconfig has no node types): the script
5
6
  * scripts/verify-bundle.ts reads the files and gzips them, this module decides
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @intent Proves the transitive contract gate AND its mutations (D22): each
2
+ * @intent Proves the transitive contract gate AND its mutations (AGENTS.md
3
+ * workflow 7): each
3
4
  * case below is a way the server has come back — or could come back — into a
4
5
  * contract graph, and the audit must catch every one of them without crying
5
6
  * wolf on the erased forms. The keystone case is the historical incident
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @intent TRANSITIVE purity of the contract entry points (DECISIONS.md D29):
2
+ * @intent TRANSITIVE purity of the contract entry points (AGENTS.md
3
+ * §Structure, the CONTRACTS exception):
3
4
  * from a feature's `schemas.ts`, no module with a runtime effect may be
4
5
  * REACHABLE — neither directly nor through any chain of intermediaries.
5
6
  *
@@ -18,12 +19,12 @@
18
19
  * (AGENTS.md §Structure), the exact edge of the incident above.
19
20
  * Plus one honesty rule: a module the walk cannot READ is reported, never
20
21
  * skipped — a gate that silently swallows what it does not understand is
21
- * worse than no gate (D22).
22
+ * worse than no gate.
22
23
  *
23
24
  * Generic tooling, hence the core (D10 amended): it knows the framework's
24
25
  * vocabulary (apps, features, schemas.ts, index.ts) and not one word of the
25
26
  * product's domain. The filesystem is INJECTED (`readModule`), so the walk is
26
- * a pure function, tested in memory with its mutations (D22).
27
+ * a pure function, tested in memory with its mutations (AGENTS.md workflow 7).
27
28
  */
28
29
  import { parseModuleImports } from "./module-purity.js";
29
30
 
@@ -9,7 +9,7 @@
9
9
  * eliminate — so it breaks the build instead.
10
10
  *
11
11
  * These functions are pure (source text in, findings out) so the negative tests
12
- * required by DECISIONS.md D22 can feed them known violations directly;
12
+ * required by AGENTS.md workflow 7 can feed them known violations directly;
13
13
  * scripts/verify-specs.ts only reports what they return.
14
14
  */
15
15
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @intent GENERIC manifest-driven gateway factory (D27), framework side: this
2
+ * @intent GENERIC manifest-driven gateway factory, framework side: this
3
3
  * module knows ZERO product vocabulary (D10 amended — generic tooling lives in
4
4
  * the core), and the vocabulary test next to it proves it. It derives a pure
5
5
  * relay from an Ogygie manifest: every declared route becomes a typed relay to
@@ -382,7 +382,7 @@ export async function createManifestGateway(
382
382
  file: null,
383
383
  cause: `${request.method} ${request.url} is not declared in the manifest`,
384
384
  suggestedFix:
385
- "Declare the route in ogygie.manifest.json — an undeclared route is unreachable by design (D27).",
385
+ "Declare the route in ogygie.manifest.json — an undeclared route is unreachable by design.",
386
386
  }),
387
387
  );
388
388
  });
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @intent Mutation tests of the manifest front-feature extension (D22: the
2
+ * @intent Mutation tests of the manifest front-feature extension (AGENTS.md
3
+ * workflow 7: the
3
4
  * gate evolution is born with its negative tests). The api-feature path is
4
5
  * already covered end-to-end by verify-manifest in CI.
5
6
  */
@@ -140,7 +141,7 @@ describe("manifest — invalidations croisées des routes (spec manifest-client
140
141
  });
141
142
  });
142
143
 
143
- // Gateway extension (D27): the gateway app type exists, and
144
+ // Gateway extension: the gateway app type exists, and
144
145
  // a gateway feature — like a front one — OWNS no routes and no tables (it
145
146
  // RELAYS the api's declared routes, it never declares its own).
146
147
  function manifestWithGatewayFeature(feature: Record<string, unknown>) {
@@ -160,7 +161,7 @@ function manifestWithGatewayFeature(feature: Record<string, unknown>) {
160
161
  return base;
161
162
  }
162
163
 
163
- describe("manifest — app gateway (D27)", () => {
164
+ describe("manifest — app gateway", () => {
164
165
  it("une feature gateway valide (app: gateway, routes/dbTables vides) est acceptée", () => {
165
166
  const parsed = OgygieManifestSchema.safeParse(manifestWithGatewayFeature({}));
166
167
  expect(parsed.success).toBe(true);
@@ -317,7 +318,7 @@ describe("manifest — ogygieVersion et bloc framework (spec ogygie-standalone-r
317
318
  });
318
319
  });
319
320
 
320
- describe("manifest — app gateway, suite (D27)", () => {
321
+ describe("manifest — app gateway, suite", () => {
321
322
  it("MUTATION (bis) : une feature gateway qui déclare une table DB est refusée", () => {
322
323
  const parsed = OgygieManifestSchema.safeParse(
323
324
  manifestWithGatewayFeature({ dbTables: ["orders"] }),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @intent Proves the purity checker AND its mutations (DECISIONS.md D22): a
2
+ * @intent Proves the purity checker AND its mutations (AGENTS.md workflow 7): a
3
3
  * gate is only worth its negative test. Every case below is a source a pure
4
4
  * module could plausibly drift into — the checker must catch each one, and
5
5
  * must NOT cry wolf on the erased forms.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @intent Mutation tests of the shared-table gates (DECISIONS.md D22: every
2
+ * @intent Mutation tests of the shared-table gates (AGENTS.md workflow 7: every
3
3
  * gate is born with its negative test). The shared-table exception is a hole
4
4
  * in the multi-tenant rail — each way of abusing it is fed here as a KNOWN
5
5
  * VIOLATION and must be DETECTED, never silently tolerated.
@@ -14,7 +14,8 @@
14
14
  * the raw-db ban unenforceable by inspection.
15
15
  *
16
16
  * These functions are pure (source text in, findings out) so that the negative
17
- * tests required by DECISIONS.md D22 can feed them known violations directly —
17
+ * mutation tests required by AGENTS.md workflow 7 can feed them known
18
+ * violations directly —
18
19
  * scripts/lint-interdits.ts and scripts/verify-manifest.ts only report what
19
20
  * they return. The module is GENERIC infrastructure (D10 amended): it speaks
20
21
  * "tenant", never a word of the product's domain.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @intent Mutation tests of the spec-frontmatter format (DECISIONS.md D22:
2
+ * @intent Mutation tests of the spec-frontmatter format (AGENTS.md workflow 7:
3
3
  * every gate is born — and evolves — with its negative test). Each test feeds
4
4
  * the parser a KNOWN VIOLATION and requires DETECTION, never a silent
5
5
  * default: the three traps consigned in FAILURES.md (2026-07-30, session 7)
@@ -92,13 +92,13 @@ status: validated
92
92
 
93
93
  it("MUTATION — une virgule INTERNE à une entrée citée ne casse plus l'entrée en fragments (split conscient des guillemets)", () => {
94
94
  const parsed = parseSpecFrontmatter(
95
- 'deferred: ["INTERVENTIONS_AND_TIMESHEET (D17, imputation MO/machines) blockedBy:interventions"]',
95
+ 'deferred: ["ARCHIVE_EXPORT (bulk export of archived records) blockedBy:archive"]',
96
96
  );
97
97
 
98
98
  expect(parsed.problems).toEqual([]);
99
99
  // ONE element, comma preserved — the old split produced two broken shards.
100
100
  expect(parsed.value["deferred"]).toEqual([
101
- "INTERVENTIONS_AND_TIMESHEET (D17, imputation MO/machines) blockedBy:interventions",
101
+ "ARCHIVE_EXPORT (bulk export of archived records) blockedBy:archive",
102
102
  ]);
103
103
  // …and that whole entry satisfies the DeferredBehaviorSchema regex.
104
104
  const validated = SpecFrontmatterSchema.safeParse({
@@ -4,18 +4,20 @@
4
4
  * machine-readable frontmatter + mandatory canonical sections. The CI
5
5
  * validator (scripts/verify-specs.ts) checks the frontmatter AND the sections;
6
6
  * this module owns the restricted frontmatter format — scalars and arrays of
7
- * scalars, inline or multiline (D13) — through parseSpecFrontmatter, which REFUSES what it
7
+ * scalars, inline or multiline — through parseSpecFrontmatter, which REFUSES what it
8
8
  * cannot read instead of silently swallowing it (a multiline array once read
9
9
  * as `deferred: []` without a sound — FAILURES.md 2026-07-30, DECISIONS.md
10
- * D22: every gate is born with its mutation test, see spec.schema.test.ts).
10
+ * AGENTS.md workflow 7: every gate is born with its mutation test, see
11
+ * spec.schema.test.ts).
11
12
  * Section titles stay FRENCH on purpose: specs are human-facing documents
12
- * (DECISIONS.md D11 — humans of the project read and validate them in French).
13
+ * (AGENTS.md principle 8 — humans of the project read and validate them in
14
+ * French, only the machine-readable half is English).
13
15
  */
14
16
  import { z } from "zod";
15
17
  import { FeatureNameSchema } from "./manifest.schema.js";
16
18
 
17
19
  /**
18
- * One deferred behavior (DECISIONS.md D13): a behavior the spec describes but
20
+ * One deferred behavior: a behavior the spec describes but
19
21
  * whose implementation is blocked by a feature that does not exist yet.
20
22
  * Format: "<stable error code or short label> blockedBy:<feature-name>".
21
23
  * The frontmatter parser only reads arrays of STRINGS (inline or multiline),
@@ -40,7 +42,7 @@ export const SpecFrontmatterSchema = z.object({
40
42
  deferred: z
41
43
  .array(DeferredBehaviorSchema)
42
44
  .default([])
43
- .describe("Behaviors deferred to a later feature (DECISIONS.md D13)"),
45
+ .describe("Behaviors deferred to a later feature, blocked by name"),
44
46
  });
45
47
  export type SpecFrontmatter = z.infer<typeof SpecFrontmatterSchema>;
46
48
 
@@ -62,7 +64,7 @@ export const SECTIONS_SPEC_OBLIGATOIRES = [
62
64
  * - status: draft → no implementation may exist
63
65
  * - status: validated → implementation is allowed
64
66
  * - status: implemented → the feature exists in the manifest with status "delivered"
65
- * AND `deferred` is empty (D13 — nothing left pending)
67
+ * AND `deferred` is empty (nothing left pending)
66
68
  */
67
69
 
68
70
  // ----------------------------------------------------------- format parser
@@ -123,7 +125,7 @@ function unquote(raw: string, location: string, problems: string[]): string | nu
123
125
  * Everything it cannot read still becomes a problem instead of a silent
124
126
  * default: an array never closed before the end of the frontmatter, a spilled
125
127
  * element line OUTSIDE any array, an unbalanced quote — each once made a spec
126
- * lie by omission (FAILURES.md 2026-07-30, D22).
128
+ * lie by omission (FAILURES.md 2026-07-30).
127
129
  */
128
130
  export function parseSpecFrontmatter(text: string): ParsedSpecFrontmatter {
129
131
  const value: Record<string, unknown> = {};
@@ -135,7 +137,7 @@ export function parseSpecFrontmatter(text: string): ParsedSpecFrontmatter {
135
137
  const colonIndex = trimmed.indexOf(":");
136
138
  if (colonIndex === -1 || trimmed.startsWith('"') || trimmed.startsWith("'")) {
137
139
  problems.push(
138
- `unparseable line ${JSON.stringify(trimmed)}: expected \`key: value\` — a spilled array element outside any array? (scalars and arrays of scalars only, D13)`,
140
+ `unparseable line ${JSON.stringify(trimmed)}: expected \`key: value\` — a spilled array element outside any array? (scalars and arrays of scalars only)`,
139
141
  );
140
142
  continue;
141
143
  }
@@ -153,7 +155,7 @@ export function parseSpecFrontmatter(text: string): ParsedSpecFrontmatter {
153
155
  }
154
156
  if (!closed) {
155
157
  problems.push(
156
- `key "${key}": array opened with "[" and never closed with "]" before the end of the frontmatter — a truncated array would be silently read as empty, hence refused (D13)`,
158
+ `key "${key}": array opened with "[" and never closed with "]" before the end of the frontmatter — a truncated array would be silently read as empty, hence refused`,
157
159
  );
158
160
  continue;
159
161
  }
@@ -9,6 +9,7 @@ import { describe, expect, it } from "vitest";
9
9
  import {
10
10
  auditProductVocabulary,
11
11
  canonicalByAlias,
12
+ derogatedFeatureIdsOf,
12
13
  vocabularyRootsOf,
13
14
  } from "./vocabulary-audit.js";
14
15
 
@@ -198,3 +199,101 @@ describe("dérivations du bloc vocabulary", () => {
198
199
  ]);
199
200
  });
200
201
  });
202
+
203
+ describe("auditProductVocabulary — B3 : la dérogation des ids de features (D16)", () => {
204
+ /** The product's features are named `widgets` — the very forbidden alias. */
205
+ const FEATURE_IDS = ["widgets", "gadget-catalog"];
206
+
207
+ it("ÉTANT DONNÉ un id de feature qui EST un alias interdit QUAND le code en dérive ses identifiants ALORS rien n'est signalé", () => {
208
+ const violations = auditProductVocabulary({
209
+ vocabulary: VOCABULARY,
210
+ sourceFiles: [
211
+ {
212
+ path: "apps/api/src/features/widgets/routes.ts",
213
+ content: [
214
+ 'import { listWidgets } from "../features/widgets/service.js";',
215
+ "export function registerWidgetsRoutes() {}",
216
+ ].join("\n"),
217
+ },
218
+ ],
219
+ governedFiles: [],
220
+ featureIds: FEATURE_IDS,
221
+ });
222
+ expect(violations).toEqual([]);
223
+ });
224
+
225
+ it("MUTATION — sans la dérogation, ces mêmes identifiants dérivés sont TOUS signalés", () => {
226
+ const violations = auditProductVocabulary({
227
+ vocabulary: VOCABULARY,
228
+ sourceFiles: [
229
+ {
230
+ path: "apps/api/src/features/widgets/routes.ts",
231
+ content: [
232
+ 'import { listWidgets } from "../features/widgets/service.js";',
233
+ "export function registerWidgetsRoutes() {}",
234
+ ].join("\n"),
235
+ },
236
+ ],
237
+ governedFiles: [],
238
+ });
239
+ expect(violations.map((violation) => violation.match.identifier).sort()).toEqual([
240
+ "listWidgets",
241
+ "registerWidgetsRoutes",
242
+ "widgets",
243
+ ]);
244
+ });
245
+
246
+ it("la dérogation couvre la DÉRIVATION, jamais un nom seulement préfixé par la racine", () => {
247
+ const violations = auditProductVocabulary({
248
+ vocabulary: VOCABULARY,
249
+ sourceFiles: [
250
+ { path: "apps/api/src/features/widgets/service.ts", content: "const widgetCode = 1;" },
251
+ ],
252
+ governedFiles: [],
253
+ featureIds: FEATURE_IDS,
254
+ });
255
+ // `widgetCode` carries the root but NOT the whole feature id `widgets`.
256
+ expect(violations).toHaveLength(1);
257
+ expect(violations[0]?.match.identifier).toBe("widgetCode");
258
+ });
259
+
260
+ it("un id de feature multi-segments couvre ses dérivés, et lui seul", () => {
261
+ const violations = auditProductVocabulary({
262
+ vocabulary: VOCABULARY,
263
+ sourceFiles: [
264
+ {
265
+ path: "apps/api/src/features/gadget-catalog/service.ts",
266
+ content: ["const gadgetCatalogRows = [];", "const gadgetCatalogue = [];"].join("\n"),
267
+ },
268
+ ],
269
+ governedFiles: [],
270
+ featureIds: FEATURE_IDS,
271
+ });
272
+ expect(violations.map((violation) => violation.match.identifier)).toEqual([
273
+ "gadgetCatalogue",
274
+ ]);
275
+ });
276
+
277
+ it("la dérogation ne touche PAS la règle des fichiers gouvernés (B4)", () => {
278
+ const violations = auditProductVocabulary({
279
+ vocabulary: VOCABULARY,
280
+ sourceFiles: [],
281
+ governedFiles: [{ path: "AGENTS.md", content: "le gadget du produit" }],
282
+ featureIds: ["gadget"],
283
+ });
284
+ expect(violations).toHaveLength(1);
285
+ expect(violations[0]?.kind).toBe("GOVERNED_FILE");
286
+ });
287
+ });
288
+
289
+ describe("derogatedFeatureIdsOf — ce que la dérogation excuse VRAIMENT", () => {
290
+ it("ne retient que les ids de features qui portent un alias interdit", () => {
291
+ expect(
292
+ derogatedFeatureIdsOf(VOCABULARY, ["widgets", "invoices", "gadget-catalog"]),
293
+ ).toEqual(["widgets", "gadget-catalog"]);
294
+ });
295
+
296
+ it("sans vocabulaire déclaré, aucune dérogation à annoncer", () => {
297
+ expect(derogatedFeatureIdsOf(undefined, ["widgets"])).toEqual([]);
298
+ });
299
+ });
@@ -30,6 +30,7 @@ import {
30
30
  findProductVocabulary,
31
31
  stripComments,
32
32
  stripProseStrings,
33
+ tokenCarriesExactName,
33
34
  type VocabularyMatch,
34
35
  } from "./vocabulary.js";
35
36
 
@@ -61,6 +62,14 @@ export interface VocabularyAuditInput {
61
62
  readonly sourceFiles: readonly ScannedFile[];
62
63
  /** Framework-governed files as they exist in the project (B4). */
63
64
  readonly governedFiles: readonly ScannedFile[];
65
+ /**
66
+ * The manifest's feature ids. Identifiers DERIVED from one of them are not
67
+ * judged against the forbidden aliases (B3 derogation, D16): a feature id
68
+ * names a document and follows the project's language, while the code
69
+ * derives its folders, import paths and function names from it. Absent or
70
+ * empty, nothing is excused.
71
+ */
72
+ readonly featureIds?: readonly string[];
64
73
  }
65
74
 
66
75
  /**
@@ -81,6 +90,23 @@ export function canonicalByAlias(vocabulary: ProductVocabulary): ReadonlyMap<str
81
90
  return byAlias;
82
91
  }
83
92
 
93
+ /**
94
+ * The feature ids the derogation actually covers (D16): those whose own name
95
+ * carries a forbidden root. A feature id that collides with nothing excuses
96
+ * nothing, and must NOT be announced — a derogation list padded with harmless
97
+ * entries is a list nobody reads, which is how a derogation gets forgotten.
98
+ */
99
+ export function derogatedFeatureIdsOf(
100
+ vocabulary: ProductVocabulary | undefined,
101
+ featureIds: readonly string[],
102
+ ): readonly string[] {
103
+ if (vocabulary === undefined || vocabulary.entries.length === 0) return [];
104
+ const forbiddenAliases = [...canonicalByAlias(vocabulary).keys()];
105
+ return featureIds.filter(
106
+ (featureId) => findProductVocabulary(featureId, forbiddenAliases).length > 0,
107
+ );
108
+ }
109
+
84
110
  export function auditProductVocabulary(
85
111
  input: VocabularyAuditInput,
86
112
  ): readonly VocabularyViolation[] {
@@ -92,9 +118,14 @@ export function auditProductVocabulary(
92
118
  const canonicalTerms = vocabulary.entries.map((entry) => entry.canonical);
93
119
  const violations: VocabularyViolation[] = [];
94
120
 
121
+ const featureIds = input.featureIds ?? [];
122
+ const derivesFromAFeatureId = (identifier: string): boolean =>
123
+ featureIds.some((featureId) => tokenCarriesExactName(identifier, featureId));
124
+
95
125
  for (const file of input.sourceFiles) {
96
126
  const judged = stripProseStrings(stripComments(file.content));
97
127
  for (const match of findProductVocabulary(judged, forbiddenAliases)) {
128
+ if (derivesFromAFeatureId(match.identifier)) continue;
98
129
  violations.push({
99
130
  kind: "FORBIDDEN_ALIAS",
100
131
  filePath: file.path,
package/src/vocabulary.ts CHANGED
@@ -75,6 +75,30 @@ function tokenCarriesRoot(
75
75
  return false;
76
76
  }
77
77
 
78
+ /**
79
+ * Does this token carry that whole name as an EXACT run of segments? Unlike
80
+ * `tokenCarriesRoot`, the last segment must match in full: this serves the
81
+ * feature-id derogation (D16), where the point is to excuse what DERIVES from
82
+ * a declared name — `registerOrdersRoutes` for the feature `orders` — and
83
+ * never a name that merely starts the same way (`orderCode` stays judged).
84
+ */
85
+ export function tokenCarriesExactName(token: string, name: string): boolean {
86
+ const tokenSegments = segmentsOfIdentifier(token);
87
+ const nameSegments = segmentsOfIdentifier(name);
88
+ if (nameSegments.length === 0) return false;
89
+ for (let start = 0; start + nameSegments.length <= tokenSegments.length; start += 1) {
90
+ let allMatch = true;
91
+ for (let offset = 0; offset < nameSegments.length; offset += 1) {
92
+ if (tokenSegments[start + offset] !== nameSegments[offset]) {
93
+ allMatch = false;
94
+ break;
95
+ }
96
+ }
97
+ if (allMatch) return true;
98
+ }
99
+ return false;
100
+ }
101
+
78
102
  /**
79
103
  * Every forbidden root the text names, in order of appearance, deduplicated by
80
104
  * (root, identifier): one finding per distinct faulty identifier, however many
@@ -1,5 +1,6 @@
1
1
  /**
2
- * @intent Mutation tests of the web lint gate (D22): each KNOWN violation is
2
+ * @intent Mutation tests of the web lint gate (AGENTS.md workflow 7): each
3
+ * KNOWN violation is
3
4
  * proven detected, and each legitimate pattern is proven accepted.
4
5
  */
5
6
  import { describe, expect, it } from "vitest";
@@ -75,7 +76,7 @@ describe("web-lint — valeurs Tailwind arbitraires", () => {
75
76
  });
76
77
  });
77
78
 
78
- describe("web-lint — URL absolue d'API (D27)", () => {
79
+ describe("web-lint — URL absolue d'API (AGENTS.md §Sécurité 8)", () => {
79
80
  it("MUTATION : un fetch sur une URL absolue vers /api est détecté", () => {
80
81
  const source = `export function useOrders() { return fetch("http://localhost:3000/api/orders"); }`;
81
82
  const violations = findAbsoluteApiUrls(source);
@@ -88,7 +89,7 @@ describe("web-lint — URL absolue d'API (D27)", () => {
88
89
  expect(findAbsoluteApiUrls(source)).toHaveLength(1);
89
90
  });
90
91
 
91
- it("un appel relatif /api/... passe (la règle même de D27)", () => {
92
+ it("un appel relatif /api/... passe (la règle même du §Sécurité 8)", () => {
92
93
  const source = `export function useOrders() { return fetch("/api/orders"); }`;
93
94
  expect(findAbsoluteApiUrls(source)).toHaveLength(0);
94
95
  });
package/src/web-lint.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @intent Testable logic of the front part of the `pnpm lint` gate (the web
3
3
  * shell spec's §Périmètre: "hardcoded string in JSX" and "arbitrary Tailwind
4
- * value" both break the CI — D22: born with its mutation tests in
4
+ * value" both break the CI — born with its mutation tests in
5
5
  * web-lint.test.ts). Built on the TypeScript AST (not regexes): JSX text is
6
6
  * only legitimate inside a <Trans> element (Lingui macro — fr is the source
7
7
  * locale, so the French text IS the ICU source message), and human-facing
@@ -27,7 +27,7 @@ const HUMAN_TEXT = /\p{L}{2}/u;
27
27
  const ARBITRARY_TAILWIND_VALUE = /[\w-]+-\[[^\]\s]+\]/;
28
28
 
29
29
  /** Absolute URL whose path enters /api — the front must never know the API
30
- * origin (D27, AGENTS.md §Sécurité 8): it calls `/api/...` relatively. */
30
+ * origin (AGENTS.md §Sécurité 8): it calls `/api/...` relatively. */
31
31
  const ABSOLUTE_API_URL = /https?:\/\/[^\s"'`]*\/api([/?#]|$)/;
32
32
 
33
33
  function lineOf(sourceFile: ts.SourceFile, node: ts.Node): number {
@@ -84,7 +84,7 @@ export function findHardcodedJsxText(source: string, fileName = "file.tsx"): Web
84
84
  return violations;
85
85
  }
86
86
 
87
- /** Absolute API URLs in any string or template literal (D27: `/api` stays
87
+ /** Absolute API URLs in any string or template literal (`/api` stays
88
88
  * relative — an absolute URL would tie the front to an API origin the
89
89
  * gateway exists to hide). Scans .ts AND .tsx: data-access files are where
90
90
  * such URLs appear first. */
@@ -110,7 +110,7 @@ export function findAbsoluteApiUrls(source: string, fileName = "file.ts"): WebLi
110
110
  if (match !== null) {
111
111
  violations.push({
112
112
  line: lineOf(sourceFile, node),
113
- rule: "absolute API URL — the front calls /api/... RELATIVELY, it must not know the API origin (D27)",
113
+ rule: "absolute API URL — the front calls /api/... RELATIVELY, it must not know the API origin (AGENTS.md §Sécurité 8)",
114
114
  excerpt: match[0].slice(0, 60),
115
115
  });
116
116
  }