@ogygie/core 0.5.2 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygie/core",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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",
@@ -67,6 +67,32 @@ describe("auditProductVocabulary — B3 : les alias interdits dans le code produ
67
67
  ).toEqual([]);
68
68
  });
69
69
 
70
+ it("la prose des TITRES DE TESTS est libre — le framework impose de reprendre la spec en français", () => {
71
+ expect(
72
+ auditProductVocabulary({
73
+ vocabulary: VOCABULARY,
74
+ sourceFiles: [
75
+ {
76
+ path: "apps/api/src/features/stock/service.test.ts",
77
+ content: 'it("ÉTANT DONNÉ un widget QUAND on le pose ALORS il tient", () => {});',
78
+ },
79
+ ],
80
+ governedFiles: [],
81
+ }),
82
+ ).toEqual([]);
83
+ });
84
+
85
+ it("un nom de table dans une chaîne reste attrapé (B3 juge les identifiants, où qu'ils soient)", () => {
86
+ const violations = auditProductVocabulary({
87
+ vocabulary: VOCABULARY,
88
+ sourceFiles: [
89
+ { path: "apps/api/src/features/stock/db.schema.ts", content: 'pgTable("widget_codes")' },
90
+ ],
91
+ governedFiles: [],
92
+ });
93
+ expect(violations.map((v) => v.match.identifier)).toEqual(["widget_codes"]);
94
+ });
95
+
70
96
  it("la prose des commentaires est libre dans le code produit", () => {
71
97
  expect(
72
98
  auditProductVocabulary({
@@ -6,8 +6,10 @@
6
6
  *
7
7
  * Two rules, and they do not read the same text:
8
8
  * - PRODUCT SOURCE (B3): only forbidden ALIASES are hunted, and only in the
9
- * machine identifiers — comments are blanked out first. The business prose
10
- * of the project keeps its own words; the code has one term per concept.
9
+ * machine identifiers — comments AND the prose held in string literals are
10
+ * blanked out first (a test title repeats the spec's French sentence by
11
+ * requirement). The business prose of the project keeps its own words; the
12
+ * code has one term per concept.
11
13
  * - GOVERNED FILES (B4): the CANONICAL terms are hunted in the RAW text,
12
14
  * comments and all. A governed file is identical in every Ogygie project, so
13
15
  * it may know no product's domain at all, not even in a comment. That is the
@@ -24,7 +26,12 @@
24
26
  * is a pure function proven in memory with its mutations.
25
27
  */
26
28
  import type { ProductVocabulary } from "./manifest.schema.js";
27
- import { findProductVocabulary, stripComments, type VocabularyMatch } from "./vocabulary.js";
29
+ import {
30
+ findProductVocabulary,
31
+ stripComments,
32
+ stripProseStrings,
33
+ type VocabularyMatch,
34
+ } from "./vocabulary.js";
28
35
 
29
36
  /** One file handed to the audit, already read by the caller. */
30
37
  export interface ScannedFile {
@@ -86,7 +93,8 @@ export function auditProductVocabulary(
86
93
  const violations: VocabularyViolation[] = [];
87
94
 
88
95
  for (const file of input.sourceFiles) {
89
- for (const match of findProductVocabulary(stripComments(file.content), forbiddenAliases)) {
96
+ const judged = stripProseStrings(stripComments(file.content));
97
+ for (const match of findProductVocabulary(judged, forbiddenAliases)) {
90
98
  violations.push({
91
99
  kind: "FORBIDDEN_ALIAS",
92
100
  filePath: file.path,
@@ -11,6 +11,7 @@ import {
11
11
  findProductVocabulary,
12
12
  segmentsOfIdentifier,
13
13
  stripComments,
14
+ stripProseStrings,
14
15
  } from "./vocabulary.js";
15
16
 
16
17
  describe("findProductVocabulary — semantique de l'appariement", () => {
@@ -108,3 +109,34 @@ describe("stripComments — la prose des commentaires est libre", () => {
108
109
  ]);
109
110
  });
110
111
  });
112
+
113
+ describe("stripProseStrings — la prose des chaînes est libre, les tokens sont jugés", () => {
114
+ it("ÉTANT DONNÉ un titre de test en français QUAND il nomme un alias ALORS il n'est PAS jugé", () => {
115
+ // Le framework EXIGE que le titre reprenne la phrase de la spec, en
116
+ // français : un produit dont le domaine parle français était condamné à
117
+ // signaler ses propres tests (FAILURES.md).
118
+ const source = 'it("ÉTANT DONNÉ un widget QUAND on le pose ALORS il tient", () => {});';
119
+ expect(findProductVocabulary(stripProseStrings(source), ["widget"])).toEqual([]);
120
+ });
121
+
122
+ it("un token machine dans une chaîne reste jugé — c'est là que vivent tables et clés", () => {
123
+ const source = 'pgTable("widget_codes"); t("widget.create.title");';
124
+ expect(
125
+ findProductVocabulary(stripProseStrings(source), ["widget"]).map((m) => m.identifier),
126
+ ).toEqual(["widget_codes", "widget"]);
127
+ });
128
+
129
+ it("un template littéral de prose est effacé, sans déplacer les lignes", () => {
130
+ const source = ["const a = 1;", "const label = `Un widget de ${n} cases`;", "const b = 2;"].join("\n");
131
+ const stripped = stripProseStrings(source);
132
+ expect(stripped.split("\n")).toHaveLength(3);
133
+ expect(findProductVocabulary(stripped, ["widget"])).toEqual([]);
134
+ });
135
+
136
+ it("l'identifiant HORS chaîne reste jugé, même sur la ligne d'une prose", () => {
137
+ const source = 'const widgetId = t("le widget est prêt");';
138
+ expect(
139
+ findProductVocabulary(stripProseStrings(source), ["widget"]).map((m) => m.identifier),
140
+ ).toEqual(["widgetId"]);
141
+ });
142
+ });
package/src/vocabulary.ts CHANGED
@@ -116,8 +116,46 @@ export function findProductVocabulary(
116
116
  * one.
117
117
  */
118
118
  export function stripComments(sourceText: string): string {
119
- const blankKeepingNewlines = (text: string): string => text.replace(/[^\n]/g, " ");
120
119
  return sourceText
121
120
  .replace(/\/\*[\s\S]*?\*\//g, blankKeepingNewlines)
122
121
  .replace(/\/\/[^\n]*/g, blankKeepingNewlines);
123
122
  }
123
+
124
+ /** Blanks a run of text, keeping line count and column positions intact. */
125
+ function blankKeepingNewlines(text: string): string {
126
+ return text.replace(/[^\n]/g, " ");
127
+ }
128
+
129
+ /**
130
+ * What a string literal may hold and still be judged: a MACHINE token — a
131
+ * table name, a route path, a translation key, an enum value. Anything with a
132
+ * space, an accent or punctuation beyond these is prose.
133
+ */
134
+ const MACHINE_STRING_CONTENT = /^[A-Za-z0-9_$\-./:@]+$/;
135
+
136
+ /**
137
+ * Blanks out the PROSE held in string literals, keeping the machine tokens.
138
+ *
139
+ * The gate judges identifiers, not language (spec B3). But a test title is
140
+ * prose inside a string — and the framework REQUIRES those titles to repeat
141
+ * the spec's French Given/When/Then verbatim, so a product whose domain word
142
+ * appears in its own spec was condemned to flag its own tests forever
143
+ * (FAILURES.md). Blanking whole strings would be too coarse the other way:
144
+ * `pgTable("vendor_codes")` and a translation key are exactly the identifiers
145
+ * the gate exists to catch, and they live in strings too.
146
+ *
147
+ * So the cut is on the CONTENT: one machine token is judged, anything else is
148
+ * prose. Deliberately lexical, like the comment stripper — a mis-parsed
149
+ * literal can only ever hide a violation, never invent one.
150
+ */
151
+ export function stripProseStrings(sourceText: string): string {
152
+ const blankProse = (literal: string): string => {
153
+ const content = literal.slice(1, -1);
154
+ if (MACHINE_STRING_CONTENT.test(content)) return literal;
155
+ return literal[0] + blankKeepingNewlines(content) + literal[literal.length - 1];
156
+ };
157
+ return sourceText
158
+ .replace(/`(?:[^`\\]|\\[\s\S])*`/g, blankProse)
159
+ .replace(/"(?:[^"\\\n]|\\.)*"/g, blankProse)
160
+ .replace(/'(?:[^'\\\n]|\\.)*'/g, blankProse);
161
+ }