@sigloch/se-engine 1.0.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/LICENSE +21 -0
- package/dist/fix-templates.d.ts +24 -0
- package/dist/fix-templates.js +192 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/layer.d.ts +29 -0
- package/dist/layer.js +36 -0
- package/dist/metrics.d.ts +53 -0
- package/dist/metrics.js +97 -0
- package/dist/readiness-compute.d.ts +27 -0
- package/dist/readiness-compute.js +119 -0
- package/dist/rule-apply.d.ts +39 -0
- package/dist/rule-apply.js +136 -0
- package/dist/rule-classify.d.ts +43 -0
- package/dist/rule-classify.js +163 -0
- package/dist/suggest.d.ts +31 -0
- package/dist/suggest.js +63 -0
- package/dist/topology.d.ts +119 -0
- package/dist/topology.js +383 -0
- package/package.json +49 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OntologyGraph, RuleViolation } from '@sigloch/contracts/se';
|
|
2
|
+
export interface ApplyResult {
|
|
3
|
+
/** graph' (a fresh object); === input topology when applied is false. */
|
|
4
|
+
graph: OntologyGraph;
|
|
5
|
+
applied: boolean;
|
|
6
|
+
/** The Format-E-style line applied (label only, not re-parsed). */
|
|
7
|
+
op?: string;
|
|
8
|
+
/** Why nothing was applied (when applied is false). */
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Apply one rule to a graph as a transforming operator.
|
|
13
|
+
*
|
|
14
|
+
* `knownViolations` (CR-SM-228): when the caller already has
|
|
15
|
+
* `evaluateAllRules`'s result (e.g. suggestEdits, which computes it
|
|
16
|
+
* once for the whole firing-rule set), pass it here to skip re-running the
|
|
17
|
+
* full rule evaluation per probe — measured as the other dominant cost
|
|
18
|
+
* alongside the structuredClone above. Omit it for the old behavior
|
|
19
|
+
* (evaluates fresh internally) -- existing 2-arg callers are unaffected.
|
|
20
|
+
*
|
|
21
|
+
* @returns graph' + whether an edit was applied. Deterministic.
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyRule(rule: {
|
|
24
|
+
id: string;
|
|
25
|
+
}, graph: OntologyGraph, knownViolations?: RuleViolation[]): ApplyResult;
|
|
26
|
+
export interface DeltaMark {
|
|
27
|
+
ruleId: string;
|
|
28
|
+
applied: boolean;
|
|
29
|
+
op?: string;
|
|
30
|
+
/** True when applying the rule leaves the measured vector unchanged (noise / mis-classification candidate). */
|
|
31
|
+
emptyDelta: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Mark every firing Operator rule by whether its single additive edit moves the
|
|
35
|
+
* measured vector (`measure`, injected — typically `metrics` flattened to ℝ⁶
|
|
36
|
+
* via `toArray`; für Architektur-Deltas `metrics(g, {layer:'arch'})`).
|
|
37
|
+
* Deterministic; single application from baseline.
|
|
38
|
+
*/
|
|
39
|
+
export declare function markEmptyDelta(graph: OntologyGraph, measure: (g: OntologyGraph) => number[]): DeltaMark[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generischer Violation → additive-edge Applier (promotet aus aimpro
|
|
3
|
+
* src/spike/rule-apply.ts, CR-226/CR-230 → CR-SM-224).
|
|
4
|
+
*
|
|
5
|
+
* KEIN per-Rule-Autofix (~50 Transformationen, bewusst nicht): EIN generischer
|
|
6
|
+
* Hebel — eine Operator-Regel (rule-classify.ts) feuert, ihre erste
|
|
7
|
+
* RuleViolation benennt das Element und (oft) Kandidaten-Ziele; synthetisiert
|
|
8
|
+
* wird genau EINE meta-model-valide Trace → graph' mit veränderter Topologie
|
|
9
|
+
* (Δm ≠ 0 unter `metrics`).
|
|
10
|
+
*
|
|
11
|
+
* CR-230: Validität wird mit `isValidTrace` gegen die gespeicherten
|
|
12
|
+
* `element.type` geprüft, nicht über parseFormatE-Roundtrip — der Applier ist
|
|
13
|
+
* id-format-agnostisch (graphcode `TYPE-slug` genauso wie SemanticId-Fixtures).
|
|
14
|
+
*
|
|
15
|
+
* Constraint-/Ziel-Regeln transformieren nichts: `applied: false`, Graph
|
|
16
|
+
* unangetastet. Spike-2-Grenze bleibt in Kraft: die synthetisierte Kante ist
|
|
17
|
+
* ein generischer Trace — bewertet wird die RICHTUNG (Δm), nie der literale
|
|
18
|
+
* Edit; Vorschläge laufen durchs Gate (nie auto-apply).
|
|
19
|
+
*/
|
|
20
|
+
import { evaluateAllRules, isValidTrace } from '@sigloch/contracts/se';
|
|
21
|
+
import { classOf, MT_IRRELEVANT_POLICY } from './rule-classify.js';
|
|
22
|
+
/** Deterministic timestamp for synthesized traces (no Date.now — keeps metrics stable). */
|
|
23
|
+
const APPLY_TS = '2026-07-26T00:00:00.000Z';
|
|
24
|
+
/**
|
|
25
|
+
* Add one trace to graph' — a SHALLOW copy (new top-level object, new traces
|
|
26
|
+
* array with the addition appended), not a deep structuredClone. Safe because
|
|
27
|
+
* nothing downstream (metrics/buildAdjacency/projectLayer/evaluateAllRules)
|
|
28
|
+
* ever mutates a graph's elements or existing traces in place — they only
|
|
29
|
+
* read. `elements` keeps the same array reference (never touched here);
|
|
30
|
+
* `traces` is a fresh array so the caller's original graph is untouched.
|
|
31
|
+
* CR-SM-228: structuredClone of the whole graph per probe was the dominant
|
|
32
|
+
* cost in suggestEdits' probe loop (measured ~20ms/call on a 382-node graph,
|
|
33
|
+
* ~249ms across 12 probes) — a shallow copy of an additive-only change costs
|
|
34
|
+
* a fraction of that.
|
|
35
|
+
*/
|
|
36
|
+
function addTrace(graph, source, target, type) {
|
|
37
|
+
return {
|
|
38
|
+
...graph,
|
|
39
|
+
traces: [...graph.traces, { source, target, type, weight: 1, created_at: APPLY_TS }],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Apply one rule to a graph as a transforming operator.
|
|
44
|
+
*
|
|
45
|
+
* `knownViolations` (CR-SM-228): when the caller already has
|
|
46
|
+
* `evaluateAllRules`'s result (e.g. suggestEdits, which computes it
|
|
47
|
+
* once for the whole firing-rule set), pass it here to skip re-running the
|
|
48
|
+
* full rule evaluation per probe — measured as the other dominant cost
|
|
49
|
+
* alongside the structuredClone above. Omit it for the old behavior
|
|
50
|
+
* (evaluates fresh internally) -- existing 2-arg callers are unaffected.
|
|
51
|
+
*
|
|
52
|
+
* @returns graph' + whether an edit was applied. Deterministic.
|
|
53
|
+
*/
|
|
54
|
+
export function applyRule(rule, graph, knownViolations) {
|
|
55
|
+
const cls = classOf(rule.id);
|
|
56
|
+
const allViolations = knownViolations ?? evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
|
|
57
|
+
const violations = allViolations.filter((v) => v.rule_id === rule.id);
|
|
58
|
+
if (violations.length === 0) {
|
|
59
|
+
return { graph, applied: false, reason: 'rule does not fire on this graph (empty Δm)' };
|
|
60
|
+
}
|
|
61
|
+
if (cls.class !== 'Operator') {
|
|
62
|
+
return { graph, applied: false, reason: `class=${cls.class}: not a transforming operator` };
|
|
63
|
+
}
|
|
64
|
+
// Find a single additive edge that (a) is meta-model valid for the two
|
|
65
|
+
// elements' stored types (isValidTrace) and (b) links two currently-
|
|
66
|
+
// unconnected nodes, so it genuinely moves the topology.
|
|
67
|
+
const typeById = new Map(graph.elements.map((e) => [e.id, e.type]));
|
|
68
|
+
const v = violations[0];
|
|
69
|
+
const source = v.element_id;
|
|
70
|
+
const srcType = typeById.get(source);
|
|
71
|
+
if (!srcType)
|
|
72
|
+
return { graph, applied: false, reason: 'violation element not in graph' };
|
|
73
|
+
const neighbours = new Set();
|
|
74
|
+
for (const t of graph.traces) {
|
|
75
|
+
if (t.source === source)
|
|
76
|
+
neighbours.add(t.target);
|
|
77
|
+
if (t.target === source)
|
|
78
|
+
neighbours.add(t.source);
|
|
79
|
+
}
|
|
80
|
+
// Candidate partners: violation's ranked candidate_targets first, then any node.
|
|
81
|
+
const partners = [
|
|
82
|
+
...(v.context?.candidate_targets ?? []).map((c) => c.id),
|
|
83
|
+
...graph.elements.map((e) => e.id),
|
|
84
|
+
].filter((id, i, a) => id !== source && !neighbours.has(id) && a.indexOf(id) === i);
|
|
85
|
+
// Trace types: the fix_hint's type first, then the rest.
|
|
86
|
+
const inferred = inferTraceType(v.fix_hint);
|
|
87
|
+
const traceTypes = [inferred, ...TRACE_KEYWORDS.filter((t) => t !== inferred)];
|
|
88
|
+
for (const partner of partners) {
|
|
89
|
+
const tgtType = typeById.get(partner);
|
|
90
|
+
if (!tgtType)
|
|
91
|
+
continue;
|
|
92
|
+
for (const tt of traceTypes) {
|
|
93
|
+
for (const [s, sType, t, tType] of [
|
|
94
|
+
[source, srcType, partner, tgtType],
|
|
95
|
+
[partner, tgtType, source, srcType],
|
|
96
|
+
]) {
|
|
97
|
+
if (isValidTrace({ source: sType, target: tType, type: tt })) {
|
|
98
|
+
return { graph: addTrace(graph, s, t, tt), applied: true, op: `+ ${s} -${tt}-> ${t}` };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { graph, applied: false, reason: 'no meta-model-valid additive edit available' };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Mark every firing Operator rule by whether its single additive edit moves the
|
|
107
|
+
* measured vector (`measure`, injected — typically `metrics` flattened to ℝ⁶
|
|
108
|
+
* via `toArray`; für Architektur-Deltas `metrics(g, {layer:'arch'})`).
|
|
109
|
+
* Deterministic; single application from baseline.
|
|
110
|
+
*/
|
|
111
|
+
export function markEmptyDelta(graph, measure) {
|
|
112
|
+
const base = measure(graph);
|
|
113
|
+
const allViolations = evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
|
|
114
|
+
const firingOperators = [...new Set(allViolations.map((v) => v.rule_id))]
|
|
115
|
+
.filter((id) => classOf(id).class === 'Operator');
|
|
116
|
+
return firingOperators.map((ruleId) => {
|
|
117
|
+
const res = applyRule({ id: ruleId }, graph, allViolations);
|
|
118
|
+
const after = res.applied ? measure(res.graph) : base;
|
|
119
|
+
return { ruleId, applied: res.applied, op: res.op, emptyDelta: after.every((x, i) => x === base[i]) };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Trace types the meta-model accepts (ontology TraceType enum). */
|
|
123
|
+
const TRACE_KEYWORDS = ['verify', 'satisfy', 'allocate', 'compose', 'produces', 'io', 'relation'];
|
|
124
|
+
/**
|
|
125
|
+
* Best-effort trace type from the violation's fix_hint (topology-only — the
|
|
126
|
+
* exact type does not affect `metrics`, but a plausible one keeps the edit
|
|
127
|
+
* legible). Defaults to 'relation'.
|
|
128
|
+
*/
|
|
129
|
+
function inferTraceType(fixHint) {
|
|
130
|
+
const h = (fixHint ?? '').toLowerCase();
|
|
131
|
+
for (const t of TRACE_KEYWORDS) {
|
|
132
|
+
if (new RegExp(`\\b${t}\\b`).test(h))
|
|
133
|
+
return t;
|
|
134
|
+
}
|
|
135
|
+
return 'relation';
|
|
136
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { MetricPolicy } from '@sigloch/contracts/se';
|
|
2
|
+
export type RuleClass = 'Operator' | 'Constraint' | 'Ziel';
|
|
3
|
+
/**
|
|
4
|
+
* Die Policy, mit der der Optimizer Regeln auswertet (CR-SM-233).
|
|
5
|
+
*
|
|
6
|
+
* MT-01/MT-02 sind **Constraints** (siehe `CLASS_MAP`), und `applyRule`/`suggestEdits`
|
|
7
|
+
* verwerfen alles, was nicht `Operator` ist — eine Instabilitäts-Schwelle kann das Ranking
|
|
8
|
+
* hier also nachweislich nicht verändern. Statt eine unvalidierte Zahl mitzuschleppen,
|
|
9
|
+
* die keine Wirkung hat, wertet der Optimizer messend statt urteilend aus. Der Host, der
|
|
10
|
+
* wirklich urteilt (graphcode-Config), reicht seine eigene Policy an `evaluateAllRules`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MT_IRRELEVANT_POLICY: MetricPolicy;
|
|
13
|
+
export interface Classification {
|
|
14
|
+
class: RuleClass;
|
|
15
|
+
/** One-line reason, cold-reader-safe. */
|
|
16
|
+
rationale: string;
|
|
17
|
+
/** True when Operator vs Constraint is genuinely unclear (mixed-intent fix). */
|
|
18
|
+
ambiguous?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Static classification keyed by rule_id. Authoritative; see file header. */
|
|
21
|
+
export declare const CLASS_MAP: Record<string, Classification>;
|
|
22
|
+
/** Classify a single rule id (falls back to ambiguous-unknown). */
|
|
23
|
+
export declare function classOf(ruleId: string): Classification;
|
|
24
|
+
export interface ClassifiedRule {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
severity: 'error' | 'warning' | 'info';
|
|
28
|
+
class: RuleClass;
|
|
29
|
+
rationale: string;
|
|
30
|
+
ambiguous: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Classify the entire live rule catalog (ALL_RULE_DEFS). */
|
|
33
|
+
export declare function classifyAll(): ClassifiedRule[];
|
|
34
|
+
export interface ClassificationStats {
|
|
35
|
+
total: number;
|
|
36
|
+
operators: number;
|
|
37
|
+
constraints: number;
|
|
38
|
+
ziele: number;
|
|
39
|
+
ambiguous: number;
|
|
40
|
+
/** Share of rules with a definite (non-ambiguous) class. */
|
|
41
|
+
eindeutigShare: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function classificationStats(rules?: ClassifiedRule[]): ClassificationStats;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule-Klassifikation Operator/Constraint/Ziel (promotet aus aimpro
|
|
3
|
+
* src/spike/rule-classify.ts, CR-226 → CR-SM-224).
|
|
4
|
+
*
|
|
5
|
+
* Jede SE-Regel im Live-Katalog (@sigloch/contracts/se → ALL_RULE_DEFS) ist ein
|
|
6
|
+
* Check `evaluate(graph) → RuleViolation[]`. Der Optimierer braucht die Teilung:
|
|
7
|
+
* - Operator — der Fix FÜGT eine Trace/ein Element HINZU → Topologie ändert
|
|
8
|
+
* sich → nutzbar als transformierende Aktion (applyRule, J).
|
|
9
|
+
* - Constraint — der Fix entfernt/repariert/reduziert Bestehendes oder ist
|
|
10
|
+
* attribut-/textbasiert (keine Topologieänderung) → Gate-Kandidat.
|
|
11
|
+
* - Ziel — rein bewertend, kein lokaler Fix.
|
|
12
|
+
*
|
|
13
|
+
* Befund (Spike 1): der SE-Katalog enthält nur Operatoren und Constraints; der
|
|
14
|
+
* "Ziel"-Eimer ist leer — die sechs Zielvektoren sind keine Regeln, sondern
|
|
15
|
+
* `metrics()` (src/metrics.ts). Attribut-only-additive Fixes (realRef/testRefs/
|
|
16
|
+
* description) sind Constraints: sie fügen keine Trace/keinen Knoten hinzu,
|
|
17
|
+
* also kann `applyRule` `metrics` nicht bewegen. Echte Mischfälle tragen
|
|
18
|
+
* `ambiguous: true`.
|
|
19
|
+
*
|
|
20
|
+
* Delta zum Spike-Original: RD-04 (Dekompositionsbreite, CR-SM-221) ergänzt;
|
|
21
|
+
* SC-01/SC-03 (BOK-CR-026) und MT-03 (CR-SM-223) sind aus dem Katalog gelöscht
|
|
22
|
+
* und daher hier entfernt. CR-SM-226: +R-28/FC-04/SC-04 (neue Regeln); IO-01
|
|
23
|
+
* ist nicht mehr cross-module-only (Rationale-Text angepasst, Klasse unverändert).
|
|
24
|
+
* CR-GC-366: +R-30 (Wirkketten-Bindung) und +R-31 (io-Verdrahtung), beide Operator —
|
|
25
|
+
* ihr Fix fügt eine Trace hinzu und bewegt damit die Topologie.
|
|
26
|
+
*/
|
|
27
|
+
import { ALL_RULE_DEFS } from '@sigloch/contracts/se';
|
|
28
|
+
import { DEFAULT_METRIC_POLICY } from '@sigloch/contracts/se';
|
|
29
|
+
/**
|
|
30
|
+
* Die Policy, mit der der Optimizer Regeln auswertet (CR-SM-233).
|
|
31
|
+
*
|
|
32
|
+
* MT-01/MT-02 sind **Constraints** (siehe `CLASS_MAP`), und `applyRule`/`suggestEdits`
|
|
33
|
+
* verwerfen alles, was nicht `Operator` ist — eine Instabilitäts-Schwelle kann das Ranking
|
|
34
|
+
* hier also nachweislich nicht verändern. Statt eine unvalidierte Zahl mitzuschleppen,
|
|
35
|
+
* die keine Wirkung hat, wertet der Optimizer messend statt urteilend aus. Der Host, der
|
|
36
|
+
* wirklich urteilt (graphcode-Config), reicht seine eigene Policy an `evaluateAllRules`.
|
|
37
|
+
*/
|
|
38
|
+
export const MT_IRRELEVANT_POLICY = {
|
|
39
|
+
// CR-SM-236: als Abweichung VOM Startwert formuliert, nicht als eigene Werteliste. Waechst
|
|
40
|
+
// die Policy um eine weitere Schwelle, urteilt der Optimizer weiter wie der Rest der Familie,
|
|
41
|
+
// statt sie stillschweigend stillzulegen.
|
|
42
|
+
//
|
|
43
|
+
// Nur MT-01/MT-02 sind hier neutralisiert. CR-01, FM-03 und R-04 behalten ihre Schwelle:
|
|
44
|
+
// FM-03 ist **Operator** (`CLASS_MAP`) — der Optimizer handelt auf seinen Befunden. Es auf
|
|
45
|
+
// `null` zu setzen haette den Vorschlag "TEST(passed) + verify an das Risiko-REQ" ersatzlos
|
|
46
|
+
// entfernt, also die Bewertung geaendert statt nur ihre Herkunft.
|
|
47
|
+
...DEFAULT_METRIC_POLICY,
|
|
48
|
+
instability: null,
|
|
49
|
+
lcom4: null,
|
|
50
|
+
};
|
|
51
|
+
/** Static classification keyed by rule_id. Authoritative; see file header. */
|
|
52
|
+
export const CLASS_MAP = {
|
|
53
|
+
// --- Operators: fix adds a trace/element (topology-changing) ---------------
|
|
54
|
+
'R-01': { class: 'Operator', rationale: 'add verify trace REQ←TEST' },
|
|
55
|
+
'R-02': { class: 'Operator', rationale: 'add satisfy trace FUNC→REQ' },
|
|
56
|
+
'R-05': { class: 'Operator', rationale: 'add verify trace TEST→REQ' },
|
|
57
|
+
'R-10': { class: 'Operator', rationale: 'add io traces to complete the FLOW' },
|
|
58
|
+
'R-14': { class: 'Operator', rationale: 'add compose trace UC→FCHAIN/REQ' },
|
|
59
|
+
'R-15': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC' },
|
|
60
|
+
'R-16': { class: 'Operator', rationale: 'add io trace ACTOR→UC/FLOW' },
|
|
61
|
+
'R-17': { class: 'Operator', rationale: 'add compose trace SYS→child' },
|
|
62
|
+
'R-21': { class: 'Operator', rationale: 'add integration TEST + verify for FUNC↔FUNC link' },
|
|
63
|
+
'R-22': { class: 'Operator', rationale: 'add allocate trace FUNC→MOD' },
|
|
64
|
+
'R-23': { class: 'Operator', rationale: 'add allocate trace MOD←FUNC' },
|
|
65
|
+
'RD-01': { class: 'Operator', rationale: 'resolve REQ by adding satisfy/decompose trace' },
|
|
66
|
+
'MS-01': { class: 'Operator', rationale: 'add CR→MS relation trace (scope)' },
|
|
67
|
+
'MS-03': { class: 'Operator', rationale: 'add CR→MS relation trace (assign milestone)' },
|
|
68
|
+
'UC-01': { class: 'Operator', rationale: 'add compose trace UC→REQ' },
|
|
69
|
+
'UC-02': { class: 'Operator', rationale: 'add io trace UC←ACTOR' },
|
|
70
|
+
'UC-03': { class: 'Operator', rationale: 'add compose trace UC→FCHAIN (scenario)' },
|
|
71
|
+
'UC-05': { class: 'Operator', rationale: 'add REQ(postcondition) via compose trace' },
|
|
72
|
+
'UC-06': { class: 'Operator', rationale: 'add REQ(precondition) via compose trace' },
|
|
73
|
+
'FC-01': { class: 'Operator', rationale: 'connect chain to ACTOR via FLOW io trace' },
|
|
74
|
+
'FC-02': { class: 'Operator', rationale: 'add FCHAIN via compose trace to leaf UC' },
|
|
75
|
+
'SC-02': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
|
|
76
|
+
'PH-01': { class: 'Operator', rationale: 'add logical MOD via compose trace' },
|
|
77
|
+
'IO-01': { class: 'Operator', rationale: 'add FLOW element + io traces between the FUNC pair' },
|
|
78
|
+
'R-28': { class: 'Operator', rationale: 'add FLOW/SCHEMA element(s) to bind architecture levels' },
|
|
79
|
+
// CR-GC-366: beide Fixes fuegen eine Trace hinzu, also Operator wie R-15 (compose) und R-10 (io).
|
|
80
|
+
'R-30': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC to bind the function into a chain' },
|
|
81
|
+
'R-31': { class: 'Operator', rationale: 'add io traces FLOW→FUNC / FUNC→FLOW to wire the function up' },
|
|
82
|
+
'FC-04': { class: 'Operator', rationale: 'add ACTOR→FLOW entry + FUNC→FLOW exit io traces' },
|
|
83
|
+
'SC-04': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
|
|
84
|
+
'CR-R01': { class: 'Operator', rationale: 'add relation traces CR→affected elements' },
|
|
85
|
+
'CR-R04': { class: 'Operator', rationale: 'add relation trace CR→FUNC' },
|
|
86
|
+
'FM-02': { class: 'Operator', rationale: 'create mitigation REQ + compose trace' },
|
|
87
|
+
'FM-03': { class: 'Operator', rationale: 'add TEST(passed) + verify trace to risk REQ' },
|
|
88
|
+
// --- Constraints: remove/repair/reduce, or attribute/text-only -------------
|
|
89
|
+
'R-03': { class: 'Constraint', rationale: 'ASIL isolation — separate mixed levels, non-additive' },
|
|
90
|
+
'R-04': { class: 'Constraint', rationale: 'module too large — split, non-additive' },
|
|
91
|
+
'R-08': { class: 'Constraint', rationale: 'repair dangling trace endpoint' },
|
|
92
|
+
'R-12': { class: 'Constraint', rationale: 'break dependency cycle — remove an edge' },
|
|
93
|
+
'R-18': { class: 'Constraint', rationale: 'invalid trace pattern — change/remove trace' },
|
|
94
|
+
'R-19': { class: 'Constraint', rationale: 'add testRefs attribute — no topology change' },
|
|
95
|
+
// CR-SM-231: R-29 ist NICHT additiv aufloesbar. Der Fix nimmt einen Anspruch WEG, und welcher
|
|
96
|
+
// der konkurrierenden TESTs die Datei wirklich belegt, ist eine fachliche Entscheidung —
|
|
97
|
+
// kein Attribut, das der Optimizer setzen koennte.
|
|
98
|
+
'R-29': { class: 'Constraint', rationale: 'test file claimed twice — which acceptance owns it is a judgement call' },
|
|
99
|
+
'R-20': { class: 'Constraint', rationale: 'add realRef attribute — no topology change' },
|
|
100
|
+
'R-26': { class: 'Constraint', rationale: 'add realRef attribute — no topology change' },
|
|
101
|
+
'R-27': { class: 'Constraint', rationale: 'physical MOD add realRef attribute — no topology change' },
|
|
102
|
+
'RD-02': { class: 'Constraint', rationale: 'decomposition consistency — repair existing' },
|
|
103
|
+
'RD-03': { class: 'Constraint', rationale: 'premature decomposition — remove children' },
|
|
104
|
+
'RD-04': { class: 'Constraint', rationale: 'decomposition breadth 7–11 — split level, non-additive' },
|
|
105
|
+
'MS-02': { class: 'Constraint', rationale: 'dangling dependency — fix relation target' },
|
|
106
|
+
'UC-04': { class: 'Constraint', rationale: 'goal = description text — no topology change' },
|
|
107
|
+
'FC-03': { class: 'Constraint', rationale: 'flatten chain — move nested funcs' },
|
|
108
|
+
'MT-01': { class: 'Constraint', rationale: 'instability threshold — restructure' },
|
|
109
|
+
'MT-02': { class: 'Constraint', rationale: 'cohesion (LCOM4) threshold — restructure' },
|
|
110
|
+
'BQ-01': { class: 'Constraint', rationale: 'unambiguous — text quality' },
|
|
111
|
+
'BQ-02': { class: 'Constraint', rationale: 'verifiable — text quality' },
|
|
112
|
+
'BQ-04': { class: 'Constraint', rationale: 'necessary — text/scope quality' },
|
|
113
|
+
'BQ-06': { class: 'Constraint', rationale: 'conforming — text/format quality' },
|
|
114
|
+
'BQ-07': { class: 'Constraint', rationale: 'complete — text quality' },
|
|
115
|
+
'ND-01': { class: 'Constraint', rationale: 'near-duplicate FUNC — merge/differentiate' },
|
|
116
|
+
'ND-02': { class: 'Constraint', rationale: 'near-duplicate SCHEMA — merge/differentiate' },
|
|
117
|
+
'CR-R02': { class: 'Constraint', rationale: 'done requires commitRef — attribute' },
|
|
118
|
+
'CR-R03': { class: 'Constraint', rationale: 'concurrent mutation — coordinate, non-additive' },
|
|
119
|
+
'AO-D01': { class: 'Constraint', rationale: 'relay node — eliminate/rewire, advisory' },
|
|
120
|
+
'AO-D03': { class: 'Constraint', rationale: 'duplicate path — unify, advisory' },
|
|
121
|
+
'CR-01': { class: 'Constraint', rationale: 'crossing-flow coupling — reduce, advisory' },
|
|
122
|
+
'RT-01': { class: 'Constraint', rationale: 'physical boundary integrity — repair' },
|
|
123
|
+
'NFR-01': { class: 'Constraint', rationale: 'budget overshoot — reduce measured/raise budget' },
|
|
124
|
+
'VR-01': { class: 'Constraint', rationale: 'missing testResult attribute — no topology change' },
|
|
125
|
+
// CR-SM-227 hat AF-01..05 in den Katalog gelegt, ohne sie hier zu klassifizieren;
|
|
126
|
+
// der Deckungstest fiel erst auf, als contracts neu gebaut wurde (stale dist).
|
|
127
|
+
// Ein Freshness-Stamp ist ein Attribut am bestehenden Knoten — keine neue Kante.
|
|
128
|
+
'AF-01': { class: 'Constraint', rationale: 'ConOps freshness stamp — attribute, no topology change' },
|
|
129
|
+
'AF-02': { class: 'Constraint', rationale: 'Trade Study freshness stamp — attribute, no topology change' },
|
|
130
|
+
'AF-03': { class: 'Constraint', rationale: 'Assumption Review freshness stamp — attribute, no topology change' },
|
|
131
|
+
'AF-04': { class: 'Constraint', rationale: 'FMEA freshness stamp — attribute, no topology change' },
|
|
132
|
+
'AF-05': { class: 'Constraint', rationale: 'Implementation Plan freshness stamp — attribute, no topology change' },
|
|
133
|
+
// --- Ambiguous (mixed-intent fix) ------------------------------------------
|
|
134
|
+
'CA-01': { class: 'Constraint', rationale: 'add capabilities OR move FUNC — mixed', ambiguous: true },
|
|
135
|
+
'CL-01': { class: 'Constraint', rationale: 'ConOps completeness — attribute vs added element unclear', ambiguous: true },
|
|
136
|
+
'FM-01': { class: 'Constraint', rationale: 'FMEA S/O/D attributes vs added mitigation — mixed', ambiguous: true },
|
|
137
|
+
};
|
|
138
|
+
const UNKNOWN = { class: 'Constraint', rationale: 'unclassified — not in CLASS_MAP', ambiguous: true };
|
|
139
|
+
/** Classify a single rule id (falls back to ambiguous-unknown). */
|
|
140
|
+
export function classOf(ruleId) {
|
|
141
|
+
return CLASS_MAP[ruleId] ?? UNKNOWN;
|
|
142
|
+
}
|
|
143
|
+
/** Classify the entire live rule catalog (ALL_RULE_DEFS). */
|
|
144
|
+
export function classifyAll() {
|
|
145
|
+
return ALL_RULE_DEFS.map((r) => {
|
|
146
|
+
const c = classOf(r.id);
|
|
147
|
+
return { id: r.id, name: r.name, severity: r.severity, class: c.class, rationale: c.rationale, ambiguous: c.ambiguous ?? false };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export function classificationStats(rules = classifyAll()) {
|
|
151
|
+
const operators = rules.filter((r) => r.class === 'Operator').length;
|
|
152
|
+
const constraints = rules.filter((r) => r.class === 'Constraint').length;
|
|
153
|
+
const ziele = rules.filter((r) => r.class === 'Ziel').length;
|
|
154
|
+
const ambiguous = rules.filter((r) => r.ambiguous).length;
|
|
155
|
+
return {
|
|
156
|
+
total: rules.length,
|
|
157
|
+
operators,
|
|
158
|
+
constraints,
|
|
159
|
+
ziele,
|
|
160
|
+
ambiguous,
|
|
161
|
+
eindeutigShare: rules.length === 0 ? 0 : (rules.length - ambiguous) / rules.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { OntologyGraph } from '@sigloch/contracts/se';
|
|
2
|
+
import { type MetricVector } from './metrics.js';
|
|
3
|
+
import type { MetricLayer } from './layer.js';
|
|
4
|
+
import { type SuggestedEdit } from './fix-templates.js';
|
|
5
|
+
/** Build a target vector (ℝ⁶, canonical dimension order) from named metric weights. */
|
|
6
|
+
export declare function targetFor(weights: Partial<Record<keyof MetricVector, number>>): number[];
|
|
7
|
+
export interface Suggestion {
|
|
8
|
+
ruleId: string;
|
|
9
|
+
/** Fund: das verletzte Element. */
|
|
10
|
+
elementId: string;
|
|
11
|
+
/** Fund: die Regel-Botschaft (erste Violation der Regel). */
|
|
12
|
+
message: string;
|
|
13
|
+
fixHint?: string;
|
|
14
|
+
/** Δm der generischen Richtungssonde (ℝ⁶, kanonische Ordnung). */
|
|
15
|
+
delta: number[];
|
|
16
|
+
/** Projektion von Δm auf das Einheitsziel: >0 hin, <0 weg. */
|
|
17
|
+
score: number;
|
|
18
|
+
/** Rule-spezifischer Template-Edit; fehlt = Fund-Ebene ohne Edit. */
|
|
19
|
+
edit?: SuggestedEdit;
|
|
20
|
+
}
|
|
21
|
+
export interface SuggestOptions {
|
|
22
|
+
/** Top-k (Default: alle). */
|
|
23
|
+
k?: number;
|
|
24
|
+
/** Messebene für Δm — 'arch' für Architektur-Deltas (CR-AIM-235 §3). */
|
|
25
|
+
layer?: MetricLayer;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Rank the firing Operator rules by how much their probe edit moves `graph`
|
|
29
|
+
* toward `target`. Deterministic, score-descending, tiebreak ruleId.
|
|
30
|
+
*/
|
|
31
|
+
export declare function suggestEdits(graph: OntologyGraph, target: number[], opts?: SuggestOptions): Suggestion[];
|
package/dist/suggest.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Greedy one-step optimization suggestions (promotet aus aimpro
|
|
3
|
+
* src/spike/suggest.ts, CR-234 → CR-SM-225) — MIT dem Spike-2-Redesign:
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* Bewusst EIN Schritt von der Baseline — kein Sequencing, keine Interaktion,
|
|
13
|
+
* keine Pareto-Front, keine Suche (Fahrplan-Schritt 5).
|
|
14
|
+
*/
|
|
15
|
+
import { evaluateAllRules } from '@sigloch/contracts/se';
|
|
16
|
+
import { metrics, toArray, METRIC_DIMENSIONS } from './metrics.js';
|
|
17
|
+
import { applyRule } from './rule-apply.js';
|
|
18
|
+
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
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Rank the firing Operator rules by how much their probe edit moves `graph`
|
|
29
|
+
* toward `target`. Deterministic, score-descending, tiebreak ruleId.
|
|
30
|
+
*/
|
|
31
|
+
export function suggestEdits(graph, target, opts = {}) {
|
|
32
|
+
const { k = Infinity, layer = 'all' } = opts;
|
|
33
|
+
const tn = l2(target);
|
|
34
|
+
const t = tn < 1e-12 ? target : target.map((x) => x / tn);
|
|
35
|
+
const measure = (g) => toArray(metrics(g, { layer }));
|
|
36
|
+
const base = measure(graph);
|
|
37
|
+
const violations = evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
|
|
38
|
+
const firstByRule = new Map();
|
|
39
|
+
for (const v of violations)
|
|
40
|
+
if (!firstByRule.has(v.rule_id))
|
|
41
|
+
firstByRule.set(v.rule_id, v);
|
|
42
|
+
const suggestions = [];
|
|
43
|
+
for (const [ruleId, v] of firstByRule) {
|
|
44
|
+
if (classOf(ruleId).class !== 'Operator')
|
|
45
|
+
continue;
|
|
46
|
+
const probe = applyRule({ id: ruleId }, graph, violations);
|
|
47
|
+
if (!probe.applied)
|
|
48
|
+
continue;
|
|
49
|
+
const delta = measure(probe.graph).map((x, i) => x - base[i]);
|
|
50
|
+
const score = delta.reduce((s, x, i) => s + x * t[i], 0);
|
|
51
|
+
suggestions.push({
|
|
52
|
+
ruleId,
|
|
53
|
+
elementId: v.element_id,
|
|
54
|
+
message: v.message,
|
|
55
|
+
fixHint: v.fix_hint,
|
|
56
|
+
delta,
|
|
57
|
+
score,
|
|
58
|
+
edit: fixFor(v, graph) ?? undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
suggestions.sort((a, b) => b.score - a.score || a.ruleId.localeCompare(b.ruleId));
|
|
62
|
+
return Number.isFinite(k) ? suggestions.slice(0, k) : suggestions;
|
|
63
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure topological graph primitives over OntologyGraph (ported from aimpro
|
|
3
|
+
* src/harness/topology.ts, CR-224; extended with optional node weights for
|
|
4
|
+
* CR-AIM-235).
|
|
5
|
+
*
|
|
6
|
+
* Semantikfrei — no element type, attribute or trace semantics enter here;
|
|
7
|
+
* only nodes and directed/undirected connectivity. These primitives feed
|
|
8
|
+
* `metrics()` (src/metrics.ts), which maps them onto the 6 Zielvektoren.
|
|
9
|
+
*
|
|
10
|
+
* Weights: `buildAdjacency(graph, nodeWeight?)` derives symmetric edge weights
|
|
11
|
+
* w(v,w) = (m(v)+m(w))/2 from node masses (default mass 1). With no weight map
|
|
12
|
+
* every weight is 1 and all weighted quantities reduce EXACTLY to the
|
|
13
|
+
* unweighted originals (regression invariant). Community structure (Q, CNM,
|
|
14
|
+
* intra-edge fraction) and component mass read weights; betweenness,
|
|
15
|
+
* cyclomatic redundancy and source→sink paths stay unweighted by design —
|
|
16
|
+
* they measure routing/shape, not mass.
|
|
17
|
+
*
|
|
18
|
+
* All functions are deterministic (fixed node iteration order = graph order,
|
|
19
|
+
* ties broken by id) and dependency-free.
|
|
20
|
+
*/
|
|
21
|
+
import type { OntologyGraph } from '@sigloch/contracts/se';
|
|
22
|
+
/** Directed + undirected adjacency, plus stable node ordering and edge weights. */
|
|
23
|
+
export interface Adjacency {
|
|
24
|
+
/** Node ids in stable order (graph element order). */
|
|
25
|
+
nodes: string[];
|
|
26
|
+
/** Undirected neighbours (deduped, self-loops removed). */
|
|
27
|
+
undirected: Map<string, Set<string>>;
|
|
28
|
+
/** Directed successors (source → targets). */
|
|
29
|
+
out: Map<string, Set<string>>;
|
|
30
|
+
/** Directed predecessors (target → sources). */
|
|
31
|
+
in: Map<string, Set<string>>;
|
|
32
|
+
/** Symmetric undirected edge weights; 1 for every edge when unweighted. */
|
|
33
|
+
w: Map<string, Map<string, number>>;
|
|
34
|
+
/** Node mass (nodeWeight input, default 1). */
|
|
35
|
+
mass: Map<string, number>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build adjacency maps from a graph. Traces referencing unknown nodes are
|
|
39
|
+
* dropped. `nodeWeight` (e.g. LOC per node, see layer.ts `weightNodes`) sets
|
|
40
|
+
* node masses; edge weight = mean of endpoint masses.
|
|
41
|
+
*
|
|
42
|
+
* ORDER-INVARIANT (CR-SM-240): `nodes` and every neighbour set come back in
|
|
43
|
+
* canonical id order, so the SAME graph in a different element/trace order
|
|
44
|
+
* yields a bit-identical Adjacency — and therefore bit-identical metrics.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildAdjacency(graph: OntologyGraph, nodeWeight?: Map<string, number>): Adjacency;
|
|
47
|
+
/**
|
|
48
|
+
* Betweenness centrality (Brandes' algorithm, unweighted undirected), normalized
|
|
49
|
+
* to [0,1] by the number of ordered node pairs (n-1)(n-2). Returns 0 for all
|
|
50
|
+
* nodes when n < 3. Higher = more of the shortest paths route through the node
|
|
51
|
+
* (structural bottleneck).
|
|
52
|
+
*
|
|
53
|
+
* CR-SM-228: index-based typed arrays instead of a fresh Map<string,X> per BFS
|
|
54
|
+
* source. The algorithm and its output are unchanged (bit-identical to the
|
|
55
|
+
* prior Map-based version) -- only the per-source scratch state changes from
|
|
56
|
+
* "allocate 4 new Maps" (O(V) allocation repeated V times = O(V^2) overhead
|
|
57
|
+
* on top of Brandes' own O(V*E), dominant on sparse graphs) to "reset 4
|
|
58
|
+
* preallocated typed arrays via .fill()" (same O(V) work, without the
|
|
59
|
+
* allocation/hashing cost). Measured A/B (1910-node graph, 3915 edges,
|
|
60
|
+
* cloned from graphcode's own SSOT):
|
|
61
|
+
* OLD per-source Map allocation : ~610 ms
|
|
62
|
+
* NEW indexed typed arrays : ~93 ms (6.6x faster, max diff 0)
|
|
63
|
+
* Root cause + measurement: sigloch-modules/docs/cr/open/CR-SM-228-*.md.
|
|
64
|
+
*/
|
|
65
|
+
export declare function betweenness(adj: Adjacency): Map<string, number>;
|
|
66
|
+
/** Max betweenness over all nodes (the worst bottleneck), in [0,1]. */
|
|
67
|
+
export declare function maxBetweenness(adj: Adjacency): number;
|
|
68
|
+
/**
|
|
69
|
+
* Newman modularity Q for a given community assignment, over weighted edges.
|
|
70
|
+
* Q = (1/2m) Σ_ij [A_ij − k_i k_j / 2m] δ(c_i,c_j) ∈ [-0.5, 1]. 0 when edgeless.
|
|
71
|
+
* Unweighted graphs (all edge weights 1) reproduce the classic unweighted Q.
|
|
72
|
+
*/
|
|
73
|
+
export declare function modularityOf(adj: Adjacency, community: Map<string, number>): number;
|
|
74
|
+
/** Newman modularity Q of the graph under greedy CNM communities. */
|
|
75
|
+
export declare function modularityQ(adj: Adjacency): number;
|
|
76
|
+
/**
|
|
77
|
+
* Greedy agglomerative community detection (Clauset–Newman–Moore, weighted):
|
|
78
|
+
* start each node in its own community, repeatedly merge the edge-linked pair
|
|
79
|
+
* that most increases Q, until no merge helps. Returns node→community-id.
|
|
80
|
+
* Edgeless graph → singletons.
|
|
81
|
+
*
|
|
82
|
+
* DETERMINISM (CR-SM-240) — the claim used to be "ties broken by community-id
|
|
83
|
+
* order", which was only half true: the ids are `buildAdjacency`'s node indices,
|
|
84
|
+
* and those were the CALLER's element order. A tie then went to whoever came
|
|
85
|
+
* first in the input, so a permuted graph produced a different partition (and
|
|
86
|
+
* with it different `modifiability`/`coherence`). The ids are canonical since
|
|
87
|
+
* CR-SM-240, so the sentence now holds: `alive` iterates ascending (built 0..n-1,
|
|
88
|
+
* deletions preserve order), each `e[i]` row was filled in canonical node order,
|
|
89
|
+
* and the strict `dq > best` therefore keeps the FIRST maximum in ascending
|
|
90
|
+
* (i, k) order. The invariant lives in `buildAdjacency`; do not re-derive it here.
|
|
91
|
+
*/
|
|
92
|
+
export declare function detectCommunities(adj: Adjacency): Map<string, number>;
|
|
93
|
+
/** Weakly-connected components (undirected reachability), as node-id groups. */
|
|
94
|
+
export declare function components(adj: Adjacency): string[][];
|
|
95
|
+
/** Weakly-connected component sizes (node counts). */
|
|
96
|
+
export declare function componentSizes(adj: Adjacency): number[];
|
|
97
|
+
/**
|
|
98
|
+
* Cyclomatic redundancy density = (m − n + c) / n, where m = undirected edges,
|
|
99
|
+
* n = nodes, c = components. Counts independent cycles per node — the redundant
|
|
100
|
+
* paths that survive a single edge/node failure. 0 for a forest. Unweighted.
|
|
101
|
+
*/
|
|
102
|
+
export declare function redundancyDensity(adj: Adjacency): number;
|
|
103
|
+
/**
|
|
104
|
+
* Coupling/cohesion: the fraction of edge WEIGHT that stays INSIDE a community
|
|
105
|
+
* of the given partition (vs crossing between communities). ∈ [0,1]; higher =
|
|
106
|
+
* more cohesive / less coupled. Community-based, so non-trivial on the
|
|
107
|
+
* near-bipartite, layered SE ontology graph (CR-229). 0 when edgeless.
|
|
108
|
+
*/
|
|
109
|
+
export declare function intraEdgeFraction(adj: Adjacency, community: Map<string, number>): number;
|
|
110
|
+
/**
|
|
111
|
+
* Mean shortest directed path length from sources (in-degree 0) to sinks
|
|
112
|
+
* (out-degree 0), over reachable source→sink pairs only. Returns
|
|
113
|
+
* { meanLength, reachableFraction }. When no source→sink pair is reachable,
|
|
114
|
+
* meanLength is 0 and reachableFraction is 0. Unweighted.
|
|
115
|
+
*/
|
|
116
|
+
export declare function sourceSinkPaths(adj: Adjacency): {
|
|
117
|
+
meanLength: number;
|
|
118
|
+
reachableFraction: number;
|
|
119
|
+
};
|