@sigloch/se-engine 1.2.0 → 1.4.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/dist/suggest.js CHANGED
@@ -2,39 +2,43 @@
2
2
  * Greedy one-step optimization suggestions (promotet aus aimpro
3
3
  * src/spike/suggest.ts, CR-234 → CR-SM-225) — MIT dem Spike-2-Redesign:
4
4
  *
5
- * AUSGELIEFERT wird die Fund-Ebene: Violation (Element + Message) + Richtung
6
- * (score = Δm·t̂) + Δm. Der generische applyRule-Trace bleibt eine interne
7
- * Richtungssonde als Edit ausgeliefert wird NUR, was ein rule-spezifisches
8
- * Fix-Template (fix-templates.ts) deterministisch aus dem Elementtext herleitet.
9
- * Δm rankt, es gibt nie frei: Anwendung läuft immer durchs Gate (3-Tier-Verdict,
10
- * graphcode graph_suggest → dryRun-Preview), nie auto-apply.
5
+ * AUSGELIEFERT wird die Fund-Ebene: Violation (Element + Message) + Richtung. Der generische
6
+ * applyRule-Trace bleibt eine interne Richtungssonde als Edit ausgeliefert wird NUR, was ein
7
+ * rule-spezifisches Fix-Template (fix-templates.ts) deterministisch aus dem Elementtext
8
+ * herleitet. Der Score rankt, er gibt nie frei: Anwendung läuft immer durchs Gate
9
+ * (3-Tier-Verdict, graphcode graph_suggest dryRun-Preview), nie auto-apply.
10
+ *
11
+ * CR-SM-292: gerankt wird nach der Senkung des CHEBYSHEV-Scores (`steer.ts`), nicht mehr nach
12
+ * `Δm · t̂`. Δm bleibt als Ablesung im Ergebnis, rankt aber nichts mehr — die Begründung steht
13
+ * vollständig im Kopf von `steer.ts`.
11
14
  *
12
15
  * Bewusst EIN Schritt von der Baseline — kein Sequencing, keine Interaktion,
13
16
  * keine Pareto-Front, keine Suche (Fahrplan-Schritt 5).
14
17
  */
15
18
  import { evaluateAllRules } from '@sigloch/contracts/se';
16
- import { metrics, toArray, METRIC_DIMENSIONS } from './metrics.js';
19
+ import { metrics, toArray } from './metrics.js';
20
+ import { steerScore, beatsBySteer } from './steer.js';
17
21
  import { applyRule } from './rule-apply.js';
18
22
  import { classOf, MT_IRRELEVANT_POLICY } from './rule-classify.js';
19
- import { fixFor } from './fix-templates.js';
20
- /** Build a target vector (ℝ⁶, canonical dimension order) from named metric weights. */
21
- export function targetFor(weights) {
22
- return METRIC_DIMENSIONS.map((d) => weights[d] ?? 0);
23
- }
24
- function l2(v) {
25
- return Math.sqrt(v.reduce((s, x) => s + x * x, 0));
26
- }
23
+ import { fixFor, mergeCandidates, applyMergeEdit, MERGE_OPERATOR_ID } from './fix-templates.js';
27
24
  /**
28
- * Rank the firing Operator rules by how much their probe edit moves `graph`
29
- * toward `target`. Deterministic, score-descending, tiebreak ruleId.
25
+ * Rankt die feuernden Operator-Regeln danach, wie weit ihr Sonden-Edit den **Chebyshev-Score**
26
+ * senkt (CR-SM-292). Deterministisch, score-absteigend, Tiebreak ruleId/elementId.
27
+ *
28
+ * Vorher stand hier `Δm · t̂` gegen einen Zielvektor aus `target-profile.json`. Der Parameter ist
29
+ * mit diesem CR ersatzlos entfallen — dreimal widerlegt, und ein zweiter Pfad daneben wäre genau
30
+ * das, was der Regelsatz verbietet.
30
31
  */
31
- export function suggestEdits(graph, target, opts = {}) {
32
+ export function suggestEdits(graph, opts = {}) {
32
33
  const { k = Infinity, layer = 'all' } = opts;
33
- const tn = l2(target);
34
- const t = tn < 1e-12 ? target : target.map((x) => x / tn);
35
34
  const measure = (g) => toArray(metrics(g, { layer }));
36
35
  const base = measure(graph);
37
36
  const violations = evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
37
+ // Der Score braucht die Befunde MIT ihren Schwellen; `MT_IRRELEVANT_POLICY` legt MT-01/MT-02
38
+ // still (`null`), sie tragen dort also keine `context.threshold` und fallen im Score
39
+ // automatisch weg — dieselbe Konvention wie in `steerScore`, kein Sonderfall hier.
40
+ const baseSteer = steerScore(violations);
41
+ const steerOf = (g) => steerScore(evaluateAllRules(g, MT_IRRELEVANT_POLICY));
38
42
  const firstByRule = new Map();
39
43
  for (const v of violations)
40
44
  if (!firstByRule.has(v.rule_id))
@@ -47,17 +51,90 @@ export function suggestEdits(graph, target, opts = {}) {
47
51
  if (!probe.applied)
48
52
  continue;
49
53
  const delta = measure(probe.graph).map((x, i) => x - base[i]);
50
- const score = delta.reduce((s, x, i) => s + x * t[i], 0);
54
+ const steer = steerOf(probe.graph);
51
55
  suggestions.push({
52
56
  ruleId,
53
57
  elementId: v.element_id,
54
58
  message: v.message,
55
59
  fixHint: v.fix_hint,
56
60
  delta,
57
- score,
61
+ score: baseSteer.score - steer.score,
62
+ steer,
63
+ removesElements: probe.graph.elements.length < graph.elements.length,
58
64
  edit: fixFor(v, graph) ?? undefined,
59
65
  });
60
66
  }
61
- suggestions.sort((a, b) => b.score - a.score || a.ruleId.localeCompare(b.ruleId));
67
+ // CR-SM-302 — die Regel, deren KLASSE nicht reicht. `CLASS_MAP` ist je `rule_id`; R-18
68
+ // meldet vier Beine, und drei davon (illegales Paar, Kardinalitaets-Obergrenze,
69
+ // compose-Baum) heilt nur ein Loeschen — deshalb steht die Regel dort zu Recht als
70
+ // `Constraint`. Das vierte Bein ist das Gegenteil: es fehlt genau EINE Kante, deren Typ und
71
+ // Zieltyp das Meta-Modell vorgibt. Trennen kann das nicht die Klasse, sondern nur das
72
+ // TEMPLATE — es liefert genau dort einen Edit und sonst `null`.
73
+ //
74
+ // Deshalb ist hier der gelieferte Edit das Kriterium, und Δm kommt aus IHM statt aus der
75
+ // generischen Sonde: dieselbe Linie wie der Merge-Operator unten und wie CR-GC-431
76
+ // ("gerankt wird, was ausgeliefert wird"). Die Sonde ist fuer eine Constraint-Regel
77
+ // ohnehin nicht anwendbar — vor diesem Block fiel der Fund deshalb ganz heraus, mitsamt
78
+ // dem vertragsbauenden Arm des Aktionsraums (CR-GC-430: COHESIVE 5 -> 2 Schritte).
79
+ for (const [ruleId, v] of firstByRule) {
80
+ if (classOf(ruleId).class === 'Operator')
81
+ continue;
82
+ const edit = fixFor(v, graph);
83
+ if (!edit || edit.op !== 'add-trace')
84
+ continue;
85
+ const kept = edit.retire
86
+ ? graph.traces.filter((t) => !(t.source === edit.retire.source && t.target === edit.retire.target && t.type === edit.retire.type))
87
+ : graph.traces;
88
+ const after = {
89
+ ...graph,
90
+ traces: [...kept, { source: edit.source, target: edit.target, type: edit.type }],
91
+ };
92
+ const steer = steerOf(after);
93
+ suggestions.push({
94
+ ruleId,
95
+ elementId: v.element_id,
96
+ message: v.message,
97
+ fixHint: v.fix_hint,
98
+ delta: measure(after).map((x, i) => x - base[i]),
99
+ score: baseSteer.score - steer.score,
100
+ steer,
101
+ removesElements: false,
102
+ edit,
103
+ });
104
+ }
105
+ // CR-GC-444 — der KONSOLIDIERUNGS-Operator, die eine Quelle ohne Violation.
106
+ // Ein Merge repariert keine Regel (deshalb kein Fund, deshalb `MERGE_OPERATOR_ID`
107
+ // statt einer erfundenen Regel-ID) — er legt zwei Knoten zusammen, die denselben
108
+ // Vertrag tragen, und bewegt damit die Metrik. Δm kommt hier NICHT aus der
109
+ // generischen `applyRule`-Sonde, sondern aus dem echten In-Memory-Merge: das
110
+ // Δm des Zuges, der auch ausgeliefert wird.
111
+ for (const edit of mergeCandidates(graph)) {
112
+ const merged = applyMergeEdit(graph, edit);
113
+ const delta = measure(merged).map((x, i) => x - base[i]);
114
+ const steer = steerOf(merged);
115
+ suggestions.push({
116
+ ruleId: MERGE_OPERATOR_ID,
117
+ elementId: edit.source,
118
+ message: edit.rationale,
119
+ fixHint: 'Als EIN graph_mutate-Batch anwenden: merge-nodes(source→target) plus die gekoppelten ' +
120
+ '`merges` — getrennt weist das Gate den FLOW-Merge über R-18 ab (FLOW -relation-> SCHEMA ist 1..1).',
121
+ delta,
122
+ score: baseSteer.score - steer.score,
123
+ steer,
124
+ // Ein Merge entfernt per Definition einen Knoten. Die Sperre (CR-SM-291 §7.2 Grenze 1)
125
+ // trifft ihn damit bewusst: er bleibt Kandidat, rankt aber nie über einem Zug, der
126
+ // nichts wegnimmt.
127
+ removesElements: true,
128
+ edit,
129
+ });
130
+ }
131
+ // Tiebreak bis auf elementId: seit dem Konsolidierungs-Operator kann DIESELBE
132
+ // Kennung mehrfach vorkommen, und eine stabile Ordnung ist Teil des Vertrags.
133
+ // `beatsBySteer` trägt die Zerstörungs-Sperre und den Score; die Kennungen sind der
134
+ // Determinismus-Anker.
135
+ const asCandidate = (x) => ({ score: x.steer, removesElements: x.removesElements });
136
+ suggestions.sort((a, b) => beatsBySteer(asCandidate(a), asCandidate(b)) ||
137
+ a.ruleId.localeCompare(b.ruleId) ||
138
+ a.elementId.localeCompare(b.elementId));
62
139
  return Number.isFinite(k) ? suggestions.slice(0, k) : suggestions;
63
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
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": ">=5 <7"
45
+ "@sigloch/contracts": ">=10 <11"
46
46
  },
47
47
  "devDependencies": {
48
- "@sigloch/contracts": "^6.0.0"
48
+ "@sigloch/contracts": ">=10 <11"
49
49
  }
50
50
  }