@sigloch/se-engine 1.6.1 → 1.7.1

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.
@@ -34,7 +34,7 @@
34
34
  * Element-Erzeugung braucht (FCHAIN, REQ-Pre/Postcondition, TEST), sind mit
35
35
  * additiven Kanten prinzipiell nicht ausdrückbar — bewusst kein Template.
36
36
  */
37
- import { isValidTrace, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs, schemaSimilarity } from '@sigloch/contracts/se';
37
+ import { isValidTrace, flowProducers, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs, schemaSimilarity } from '@sigloch/contracts/se';
38
38
  function byId(g, id) {
39
39
  return g.elements.find((e) => e.id === id);
40
40
  }
@@ -139,6 +139,28 @@ function relationToMentioned(types) {
139
139
  * von mehreren" wäre geraten — und ein falsch alloziertes FUNC verfälscht
140
140
  * `coherence`/`modifiability`, also genau die Zahlen, auf denen das Ranking sitzt.
141
141
  */
142
+ /**
143
+ * Der vierte Waechter: wuerde diese Kante einen ZWEITEN Produzenten an einen FLOW haengen?
144
+ * (CR-SM-347)
145
+ *
146
+ * `isValidTrace` prueft die Grammatik, `retireFor` die Obergrenze aus `BOUNDED_PATTERNS` —
147
+ * IO-02 faellt durch beide: die vier io-Zeilen tragen kein `cardinality`-Feld, und
148
+ * Kardinalitaet begrenzt ohnehin die QUELL-, nicht die ZIEL-Seite. Ohne diesen Riegel schlug
149
+ * die R-31-Vorlage im Release 1.7.0 eine Kante vor, die das Gate mit `IO-02:error` abwies.
150
+ * Dieselbe Klasse wie der kinds-Riegel aus CR-SM-342, nur fuer eine Mengen- statt einer
151
+ * Grammatikregel.
152
+ *
153
+ * Das Praedikat kommt AUS der Regel (`flowProducers` in contracts), nicht aus einem Nachbau
154
+ * hier — zwei Fassungen derselben Mengenfrage waeren die zweite Wahrheit aus CR-SM-286/-302.
155
+ * Nur die Produzentenrichtung ist begrenzt: `FLOW -io-> FUNC` (ein weiterer Leser) bleibt
156
+ * frei, das ist die normale 1:n-Form.
157
+ */
158
+ function wuerdeZweitenProduzentenSchaffen(g, source, target, traceType) {
159
+ if (traceType !== 'io' || target.type !== 'FLOW')
160
+ return false;
161
+ const producers = flowProducers(g).get(target.id);
162
+ return !!producers && producers.size >= 1 && !producers.has(source.id);
163
+ }
142
164
  function edgeToMentioned(opts) {
143
165
  return (v, g) => {
144
166
  const el = byId(g, v.element_id);
@@ -164,6 +186,8 @@ function edgeToMentioned(opts) {
164
186
  // entscheidet, ob der Edit ein Anhängen (kein retire), ein Umhängen
165
187
  // (genau ein retire) oder nicht ausdrückbar ist ('blocked' → nächster
166
188
  // Kandidat; Option C statt eines Edits, den das Gate sicher abweist).
189
+ if (wuerdeZweitenProduzentenSchaffen(g, source, target, opts.traceType))
190
+ continue;
167
191
  const retire = retireFor(g, source, target, opts.traceType);
168
192
  if (retire === 'blocked')
169
193
  continue;
@@ -183,10 +207,163 @@ function edgeToMentioned(opts) {
183
207
  return null;
184
208
  };
185
209
  }
210
+ /**
211
+ * Kante zu einem Ziel aus `context.candidate_targets` — der zweite Kandidatenweg (CR-SM-342).
212
+ *
213
+ * `edgeToMentioned` sucht im ELEMENTTEXT. Das traegt, wo der Text das Ziel nennt (CR-Text nennt
214
+ * seine FUNC), und traegt nicht, wo die Regel den Kandidatenkreis ohnehin schon mitliefert.
215
+ * Gemessen an einem vollen Spezifikationslauf: R-02 (86 Befunde), RD-01 (71), R-31 (42) und
216
+ * R-30 (40) tragen ihre `candidate_targets` zu 100 % — 239 Befunde, fuer die ein Template
217
+ * nichts SUCHEN muss. Es gab nur keins.
218
+ *
219
+ * Die Kandidaten kommen aus `toCandidates` und sind bereits nach Token-Ueberlappung mit dem
220
+ * Fund-Element sortiert: der erste, der alle Riegel passiert, gewinnt. Die Riegel sind
221
+ * dieselben wie im Textweg, in derselben Reihenfolge — eine Vorschlagsquelle mehr, kein
222
+ * zweiter Legalitaetsbegriff.
223
+ *
224
+ * WARUM DER KINDS-RIEGEL HIER TRAEGT, nicht nur formal: fuer `satisfy` FUNC→REQ laesst das
225
+ * Muster nur {functional, postcondition, precondition} zu. Am selben Modell gemessen sind
226
+ * 42 von 80 REQ nicht-funktional — eine Vorlage, die blind den erstbesten Kandidaten nimmt,
227
+ * haette in 53 % der Faelle die Kante vorgeschlagen, die R-18 sofort abweist. Das war im Lauf
228
+ * bereits die groesste Ablehnungsklasse (39 von 118 Fehlern); sie zu automatisieren haette
229
+ * den Waste nicht gesenkt, sondern vervielfacht.
230
+ */
231
+ function edgeToCandidate(opts) {
232
+ return (v, g) => {
233
+ const cands = v.context?.candidate_targets;
234
+ if (!Array.isArray(cands) || cands.length === 0)
235
+ return null;
236
+ const el = byId(g, v.element_id);
237
+ if (!el)
238
+ return null;
239
+ for (const cand of cands) {
240
+ const other = byId(g, cand.id);
241
+ if (!other || other.id === el.id)
242
+ continue;
243
+ if (opts.types && !opts.types.includes(other.type))
244
+ continue;
245
+ const source = opts.direction === 'out' ? el : other;
246
+ const target = opts.direction === 'out' ? other : el;
247
+ if (hasTrace(g, source.id, target.id, opts.traceType))
248
+ continue;
249
+ if (!isValidTrace({
250
+ source: source.type, target: target.type, type: opts.traceType,
251
+ sourceKinds: source.kinds, targetKinds: target.kinds,
252
+ }))
253
+ continue;
254
+ if (wuerdeZweitenProduzentenSchaffen(g, source, target, opts.traceType))
255
+ continue;
256
+ const retire = retireFor(g, source, target, opts.traceType);
257
+ if (retire === 'blocked')
258
+ continue;
259
+ const file = retire ? realFileOf(source) : undefined;
260
+ return {
261
+ op: 'add-trace',
262
+ source: source.id,
263
+ target: target.id,
264
+ type: opts.traceType,
265
+ rationale: `${other.id} ist der bestplatzierte Kandidat des Befunds und die Kante ` +
266
+ `${source.type} -${opts.traceType}-> ${target.type} ist zulaessig`,
267
+ ...(retire ? { retire } : {}),
268
+ ...(retire && file ? { codeImpact: { file, targetModule: target.id } } : {}),
269
+ };
270
+ }
271
+ return null;
272
+ };
273
+ }
274
+ /**
275
+ * Die abgewiesene Kante reparieren — UMHAENGEN, nicht anhaengen (CR-SM-342).
276
+ *
277
+ * Eine illegale Kante verschwindet nicht dadurch, dass eine legale danebengelegt wird: der
278
+ * Befund bliebe stehen. Deshalb traegt jeder Vorschlag hier ein `retire` auf genau die
279
+ * beanstandete Kante — Anwendung als EIN Batch [delete, add], wie CR-GC-435 es fuer die
280
+ * Kardinalitaet schon eingefuehrt hat.
281
+ *
282
+ * NUR EINDEUTIGE FAELLE. Nennt der Grund mehr als eine Alternative, ist die Wahl eine
283
+ * Entscheidung und kein Zug — dann `null`, und der Fund bleibt Fund-Ebene. Das ist dieselbe
284
+ * Linie wie R-23s fehlender `uniqueFallback`: lieber keine Empfehlung als eine geratene.
285
+ *
286
+ * Der kinds-Fall hat eine Ausnahme, die KEINE Raterei ist: schlaegt `satisfy` einer FUNC an
287
+ * einem nicht-funktionalen REQ fehl und ist MOD eine zulaessige Quelle, dann ist das Modul
288
+ * der FUNC der Traeger — `FUNC -allocate-> MOD` steht im Graphen und macht die Wahl
289
+ * deterministisch. Gemessen war das die groesste Einzelklasse des Laufs (39 von 118).
290
+ */
291
+ function rejectedEdgeFix(rej, v, g) {
292
+ if (!rej)
293
+ return null;
294
+ const el = byId(g, v.element_id);
295
+ if (!el)
296
+ return null;
297
+ // Quelle und Ziel stehen BEIDE am Befund (CR-SM-342): `element_id` ist die Quelle,
298
+ // `target_id` das Ziel. Die Kante im Graphen zu suchen waere falsch — bei einem dryRun,
299
+ // der sie gerade abgelehnt hat, steht sie im Arbeitsgraphen gar nicht.
300
+ const target = byId(g, rej.target_id);
301
+ if (!target)
302
+ return null;
303
+ const retire = {
304
+ source: el.id,
305
+ target: target.id,
306
+ type: rej.trace_type,
307
+ rationale: `${el.id} -${rej.trace_type}-> ${target.id} ist unzulaessig (${rej.reason}) und muss weichen`,
308
+ };
309
+ if (rej.reason === 'no-pattern') {
310
+ if (rej.candidate_trace_types.length !== 1)
311
+ return null;
312
+ const typ = rej.candidate_trace_types[0];
313
+ if (hasTrace(g, el.id, target.id, typ))
314
+ return null;
315
+ return {
316
+ op: 'add-trace', source: el.id, target: target.id, type: typ,
317
+ rationale: `${rej.source_type} → ${rej.target_type} kennt genau einen Kantentyp: ${typ}`,
318
+ retire,
319
+ };
320
+ }
321
+ // kinds-mismatch / kinds-undeclared: die Kante ist richtig, die QUELLE ist es nicht.
322
+ if (rej.candidate_sources.length === 0)
323
+ return null;
324
+ const traeger = g.traces
325
+ .filter((t) => t.source === el.id && t.type === 'allocate')
326
+ .map((t) => byId(g, t.target))
327
+ .filter((m) => !!m && rej.candidate_sources.includes(m.type));
328
+ if (traeger.length !== 1)
329
+ return null;
330
+ const neu = traeger[0];
331
+ if (hasTrace(g, neu.id, target.id, rej.trace_type))
332
+ return null;
333
+ if (!isValidTrace({
334
+ source: neu.type, target: target.type, type: rej.trace_type,
335
+ sourceKinds: neu.kinds, targetKinds: target.kinds,
336
+ }))
337
+ return null;
338
+ return {
339
+ op: 'add-trace', source: neu.id, target: target.id, type: rej.trace_type,
340
+ rationale: `${target.id} traegt ${rej.field} {${(rej.declared ?? []).join(', ')}} — das Muster ` +
341
+ `${rej.source_type} → ${rej.target_type} laesst das nicht zu, ${neu.id} (${neu.type}) schon, ` +
342
+ `und ${el.id} ist genau dorthin alloziert`,
343
+ retire,
344
+ };
345
+ }
186
346
  export const FIX_TEMPLATES = {
187
347
  // CR-SM-295: SCHEMA gehoert zum Umfang — ein CR, der nur einen Datenvertrag aendert,
188
348
  // ist ein vollstaendiger CR. Die Liste ist woertlich SCOPE_TYPES aus cr-quality-rules.
189
349
  'CR-R01': relationToMentioned(['UC', 'REQ', 'FUNC', 'MOD', 'SCHEMA']),
350
+ // --- Kandidaten-Operatoren (CR-SM-342) -------------------------------------
351
+ // Die vier teuersten Operator-Regeln des gemessenen Laufs. `CLASS_MAP` fuehrte sie
352
+ // laengst als mechanisch loesbar, ihre Befunde tragen ihre Kandidaten zu 100 %, und
353
+ // keine hatte eine Vorlage: 239 Befunde ohne ausfuehrbare Empfehlung.
354
+ // FUNC ohne satisfy → das bestplatzierte REQ, dessen kinds das Muster zulaesst.
355
+ // Der kinds-Riegel ist hier der ganze Punkt, nicht Formsache (s. edgeToCandidate).
356
+ 'R-02': edgeToCandidate({ traceType: 'satisfy', direction: 'out', types: ['REQ'] }),
357
+ // REQ ohne Aufloesung → dieselbe satisfy-Kante, aber der Fund sitzt am REQ:
358
+ // die Kandidaten sind die Quellen (FUNC/MOD/…), also laeuft die Kante herein.
359
+ 'RD-01': edgeToCandidate({ traceType: 'satisfy', direction: 'in' }),
360
+ // FUNC ohne io → der FLOW, den der Befund nennt. Richtung aus der Kante selbst:
361
+ // `isValidTrace` kennt beide Muster (FUNC→FLOW und FLOW→FUNC), und 'out' probiert
362
+ // FUNC→FLOW zuerst; passt nur die Gegenrichtung, faellt der Kandidat durch und der
363
+ // naechste kommt dran — kein Raten ueber die Richtung.
364
+ 'R-31': edgeToCandidate({ traceType: 'io', direction: 'out', types: ['FLOW'] }),
365
+ // FUNC ohne Wirkkette → compose von der FCHAIN herein (einziges legales Muster).
366
+ 'R-30': edgeToCandidate({ traceType: 'compose', direction: 'in', types: ['FCHAIN'] }),
190
367
  // --- Architektur-Operatoren (CR-SM-241) ------------------------------------
191
368
  // Ohne sie hat auf `layer: 'arch'` — graphcodes DEFAULT-Messebene — jeder
192
369
  // Template-Edit Δm = 0, weil CR/MS/UC gar nicht im Teilgraphen liegen.
@@ -223,6 +400,14 @@ export const FIX_TEMPLATES = {
223
400
  * durchgesetzte Untergrenze dazu, traegt dieses Template sie ohne Aenderung.
224
401
  */
225
402
  'R-18': (v, g) => {
403
+ // CR-SM-342: R-18 hat vier Beine, und bis hierher bediente das Template genau eins.
404
+ // Gemessen an einem vollen Lauf: 48 von 56 Befunden kamen aus der KANTEN-GRAMMATIK und
405
+ // fielen in der Zeile darueber heraus, weil sie kein `candidate_targets` tragen — die
406
+ // Regel mit den meisten Ablehnungen war die einzige ohne Empfehlung. Seit CR-SM-341
407
+ // traegt sie ihren Grund als `context.trace_rejection`; hier wird er bedient.
408
+ const rej = v.context?.trace_rejection;
409
+ if (rej)
410
+ return rejectedEdgeFix(rej, v, g);
226
411
  if (!Array.isArray(v.context?.candidate_targets))
227
412
  return null;
228
413
  const el = byId(g, v.element_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -42,9 +42,9 @@
42
42
  "access": "public"
43
43
  },
44
44
  "peerDependencies": {
45
- "@sigloch/contracts": ">=10 <11"
45
+ "@sigloch/contracts": ">=10.8 <11"
46
46
  },
47
47
  "devDependencies": {
48
- "@sigloch/contracts": ">=10 <11"
48
+ "@sigloch/contracts": ">=10.8 <11"
49
49
  }
50
50
  }