@sigloch/contracts 6.0.0 → 6.3.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.
@@ -1,4 +1,5 @@
1
1
  import { getND02SimilarityMatrix } from './near-duplicate-rules.js';
2
+ import { indexOf } from './graph-index.js';
2
3
  const SCHEMA_OVERLAP_THRESHOLD = 0.5;
3
4
  /**
4
5
  * AO-D01: Relay Node Detection.
@@ -6,13 +7,14 @@ const SCHEMA_OVERLAP_THRESHOLD = 0.5;
6
7
  * and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
7
8
  */
8
9
  export function aoD01RelayNode(graph) {
10
+ const idx = indexOf(graph);
9
11
  const violations = [];
10
- const funcs = graph.elements.filter(e => e.type === 'FUNC');
12
+ const funcs = idx.elementsOfType('FUNC');
11
13
  const funcIdSet = new Set(funcs.map(f => f.id));
12
14
  for (const func of funcs) {
13
15
  // (a) No satisfy traces (FUNC as source) connecting to any REQ
14
16
  const hasSatisfy = graph.traces.some(t => t.type === 'satisfy' &&
15
- t.source === func.id && graph.elements.some(e => e.id === t.target && e.type === 'REQ'));
17
+ t.source === func.id && idx.typeOf(t.target) === 'REQ');
16
18
  if (hasSatisfy)
17
19
  continue;
18
20
  // (b) >=2 outgoing io traces to other FUNCs
@@ -24,7 +26,7 @@ export function aoD01RelayNode(graph) {
24
26
  // (c) Target FUNCs share SCHEMA overlap >= 0.5 (skip check if no matrix)
25
27
  if (!passesSchemaOverlap(graph, ioTargets))
26
28
  continue;
27
- const el = graph.elements.find(e => e.id === func.id);
29
+ const el = idx.byId.get(func.id);
28
30
  violations.push({
29
31
  rule_id: 'AO-D01',
30
32
  severity: 'info',
@@ -37,29 +39,35 @@ export function aoD01RelayNode(graph) {
37
39
  }
38
40
  /** CR-165: Resolve FUNC→SCHEMA via FUNC→FLOW(io) + FLOW→SCHEMA(relation). */
39
41
  function funcToSchemas(graph, funcId) {
42
+ const idx = indexOf(graph);
40
43
  const schemas = new Set();
41
44
  // Find FLOWs connected to this FUNC via io (FUNC→FLOW or FLOW→FUNC)
45
+ //
46
+ // CR-SM-264: die io-Kanten dieser FUNC statt zweier Vollscans ueber ALLE Traces je FUNC
47
+ // (`F x T`) — AO-D03 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
48
+ // dabei egal, die Menge wird nur auf Mitgliedschaft geprueft.
42
49
  const flowIds = new Set();
43
- for (const t of graph.traces) {
44
- if (t.type === 'io') {
45
- if (t.source === funcId && graph.elements.some(e => e.id === t.target && e.type === 'FLOW')) {
46
- flowIds.add(t.target);
47
- }
48
- if (t.target === funcId && graph.elements.some(e => e.id === t.source && e.type === 'FLOW')) {
49
- flowIds.add(t.source);
50
- }
51
- }
52
- }
53
- // Find SCHEMAs connected to those FLOWs via relation
54
- for (const t of graph.traces) {
55
- if (t.type === 'relation' && flowIds.has(t.source) && graph.elements.some(e => e.id === t.target && e.type === 'SCHEMA')) {
50
+ for (const t of idx.out(funcId, 'io'))
51
+ if (idx.typeOf(t.target) === 'FLOW')
52
+ flowIds.add(t.target);
53
+ for (const t of idx.in(funcId, 'io'))
54
+ if (idx.typeOf(t.source) === 'FLOW')
55
+ flowIds.add(t.source);
56
+ // Find SCHEMAs connected to those FLOWs via relation.
57
+ //
58
+ // Hier NICHT ueber `flowIds` iterieren, obwohl das billiger waere: die Einfuegereihenfolge
59
+ // dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D03-Meldung. Der Lauf
60
+ // ueber die relation-Kanten in Graph-Reihenfolge haelt sie identisch — und ist trotzdem
61
+ // deutlich kuerzer als der Vollscan, weil er nur einen Kantentyp sieht.
62
+ for (const t of idx.tracesOfType('relation')) {
63
+ if (flowIds.has(t.source) && idx.typeOf(t.target) === 'SCHEMA')
56
64
  schemas.add(t.target);
57
- }
58
65
  }
59
66
  return schemas;
60
67
  }
61
68
  /** Check if any pair of target FUNCs shares SCHEMA overlap via FLOW→SCHEMA(relation) + ND-02 matrix. */
62
69
  function passesSchemaOverlap(graph, targetFuncIds) {
70
+ const idx = indexOf(graph);
63
71
  const nd02 = getND02SimilarityMatrix();
64
72
  if (!nd02)
65
73
  return true; // no matrix → skip check, assume pass
@@ -93,8 +101,9 @@ function passesSchemaOverlap(graph, targetFuncIds) {
93
101
  * FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
94
102
  */
95
103
  export function aoD03DuplicatePath(graph) {
104
+ const idx = indexOf(graph);
96
105
  const violations = [];
97
- const funcs = graph.elements.filter(e => e.type === 'FUNC');
106
+ const funcs = idx.elementsOfType('FUNC');
98
107
  const funcIdSet = new Set(funcs.map(f => f.id));
99
108
  const seen = new Set();
100
109
  // Pre-compute: FUNC → set of SCHEMA targets via FLOW→SCHEMA(relation) (CR-165)
@@ -141,15 +150,16 @@ export function aoD03DuplicatePath(graph) {
141
150
  // diesen Aus-Zustand nicht: jedes Modulpaar mit >= 1 Kreuzung erzeugte eine info-Meldung.
142
151
  // ---------------------------------------------------------------------------
143
152
  export function cr01CrossingFlowCount(graph, policy) {
153
+ const idx = indexOf(graph);
144
154
  const violations = [];
145
155
  const steps = policy.crossingFlows;
146
156
  if (steps === null)
147
157
  return violations;
148
- const mods = graph.elements.filter(e => e.type === 'MOD');
158
+ const mods = idx.elementsOfType('MOD');
149
159
  // Build MOD → Set<FUNC-id> mapping
150
160
  const modFuncs = new Map();
151
161
  for (const mod of mods) {
152
- const funcIds = new Set(graph.traces.filter(t => t.type === 'allocate' && t.target === mod.id).map(t => t.source));
162
+ const funcIds = new Set(idx.in(mod.id, 'allocate').map(t => t.source));
153
163
  modFuncs.set(mod.id, funcIds);
154
164
  }
155
165
  // Count crossings per module pair
@@ -191,10 +201,11 @@ export function cr01CrossingFlowCount(graph, policy) {
191
201
  // FUNCs must not be allocated directly to physical MODs — use logical sub-modules.
192
202
  // ---------------------------------------------------------------------------
193
203
  export function rt01PhysicalBoundaryIntegrity(graph) {
204
+ const idx = indexOf(graph);
194
205
  const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
195
206
  return graph.traces
196
207
  .filter(t => t.type === 'allocate' && physicalMods.some(m => m.id === t.target))
197
- .filter(t => graph.elements.some(e => e.id === t.source && e.type === 'FUNC'))
208
+ .filter(t => idx.typeOf(t.source) === 'FUNC')
198
209
  .map(t => ({
199
210
  rule_id: 'RT-01',
200
211
  severity: 'error',
@@ -208,10 +219,11 @@ export function rt01PhysicalBoundaryIntegrity(graph) {
208
219
  // Physical MODs should have at least one logical sub-module via compose trace.
209
220
  // ---------------------------------------------------------------------------
210
221
  export function ph01PhysicalModCompleteness(graph) {
222
+ const idx = indexOf(graph);
211
223
  const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
212
224
  return physicalMods
213
225
  .filter(pm => !graph.traces.some(t => t.source === pm.id && t.type === 'compose' &&
214
- graph.elements.some(e => e.id === t.target && e.type === 'MOD')))
226
+ idx.typeOf(t.target) === 'MOD'))
215
227
  .map(pm => ({
216
228
  rule_id: 'PH-01',
217
229
  severity: 'info',
@@ -226,6 +238,7 @@ export function ph01PhysicalModCompleteness(graph) {
226
238
  // FUNC @requires must be satisfied by the physical MOD's @capability.
227
239
  // ---------------------------------------------------------------------------
228
240
  export function ca01CapabilityAllocation(graph) {
241
+ const idx = indexOf(graph);
229
242
  const violations = [];
230
243
  const funcsWithRequires = graph.elements.filter(e => e.type === 'FUNC' && e.attributes?.requires);
231
244
  for (const func of funcsWithRequires) {
@@ -236,15 +249,16 @@ export function ca01CapabilityAllocation(graph) {
236
249
  const allocTrace = graph.traces.find(t => t.source === func.id && t.type === 'allocate');
237
250
  if (!allocTrace)
238
251
  continue;
239
- const logicalMod = graph.elements.find(e => e.id === allocTrace.target && e.type === 'MOD');
252
+ const logicalModCandidate = idx.byId.get(allocTrace.target);
253
+ const logicalMod = logicalModCandidate?.type === 'MOD' ? logicalModCandidate : undefined;
240
254
  if (!logicalMod)
241
255
  continue;
242
256
  // Find physical parent MOD (compose source → logical MOD target)
243
257
  const physParentTrace = graph.traces.find(t => t.type === 'compose' && t.target === logicalMod.id &&
244
- graph.elements.some(e => e.id === t.source && e.type === 'MOD' && e.attributes?.kind === 'physical'));
258
+ (idx.byId.get(t.source)?.type === 'MOD' && idx.byId.get(t.source)?.attributes?.kind === 'physical'));
245
259
  if (!physParentTrace)
246
260
  continue;
247
- const physMod = graph.elements.find(e => e.id === physParentTrace.source);
261
+ const physMod = idx.byId.get(physParentTrace.source);
248
262
  const capabilities = (Array.isArray(physMod.attributes?.capability)
249
263
  ? physMod.attributes.capability
250
264
  : physMod.attributes?.capability ? [physMod.attributes.capability] : []);
@@ -289,9 +303,10 @@ export function ca01CapabilityAllocation(graph) {
289
303
  // andere Regel trifft.
290
304
  // ---------------------------------------------------------------------------
291
305
  export function io01CrossModuleCompleteness(graph) {
306
+ const idx = indexOf(graph);
292
307
  const violations = [];
293
308
  const flowIds = new Set(graph.elements.filter(e => e.type === 'FLOW').map(e => e.id));
294
- const funcIdSet = new Set(graph.elements.filter(e => e.type === 'FUNC').map(e => e.id));
309
+ const funcIdSet = idx.idsOfType('FUNC');
295
310
  /** io-Nachbarschaft FUNC <-> FLOW, richtungslos: die Frage ist "haengt das zusammen?". */
296
311
  const flowsOfFunc = new Map();
297
312
  for (const t of graph.traces) {
@@ -306,7 +321,7 @@ export function io01CrossModuleCompleteness(graph) {
306
321
  flowsOfFunc.set(f, new Set());
307
322
  flowsOfFunc.get(f).add(fl);
308
323
  }
309
- for (const fc of graph.elements.filter(e => e.type === 'FCHAIN')) {
324
+ for (const fc of idx.elementsOfType('FCHAIN')) {
310
325
  const members = graph.traces
311
326
  .filter(t => t.source === fc.id && t.type === 'compose')
312
327
  .map(t => t.target)
@@ -356,7 +371,7 @@ export function io01CrossModuleCompleteness(graph) {
356
371
  for (const id of wired) {
357
372
  if (largestSet.has(id))
358
373
  continue;
359
- const el = graph.elements.find(e => e.id === id);
374
+ const el = idx.byId.get(id);
360
375
  const mod = graph.traces.find(t => t.source === id && t.type === 'allocate')?.target ?? 'unallocated';
361
376
  violations.push({
362
377
  rule_id: 'IO-01',
@@ -368,7 +383,7 @@ export function io01CrossModuleCompleteness(graph) {
368
383
  element_type: 'FUNC',
369
384
  element_name: el?.name ?? id,
370
385
  candidate_targets: largest.slice(0, 3).map(t => {
371
- const te = graph.elements.find(e => e.id === t);
386
+ const te = idx.byId.get(t);
372
387
  return { id: t, type: te?.type ?? 'FUNC', name: te?.name ?? t };
373
388
  }),
374
389
  },
@@ -49,6 +49,7 @@ export declare const CodeFactsSchema: z.ZodObject<{
49
49
  from: z.ZodString;
50
50
  to: z.ZodString;
51
51
  }, z.core.$strip>>>;
52
+ declaredDependencies: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
53
  }, z.core.$strip>;
53
54
  export type CodeFacts = z.infer<typeof CodeFactsSchema>;
54
55
  /** A conformance rule: pure over (graph, facts) — never touches I/O itself. */
@@ -48,6 +48,17 @@ export const CodeFactsSchema = z.object({
48
48
  * given), so pre-CR-212 CodeFacts stay valid.
49
49
  */
50
50
  importEdges: z.array(ImportEdgeSchema).optional(),
51
+ /**
52
+ * Package names the consumer declares in `dependencies` + `devDependencies`, for
53
+ * RC-06 (CR-SM-262). The extractor reads them from the repo's package.json.
54
+ *
55
+ * Optional, and the ABSENT case is silence, not a violation — the opposite of `files`,
56
+ * where a missing key must surface loudly. The asymmetry is deliberate: a missing file
57
+ * entry means the extractor looked and found nothing, while an absent dependency list
58
+ * means it never looked. Treating "never looked" as "declares nothing" would report
59
+ * every external binding in the graph at once, which is noise, not a finding.
60
+ */
61
+ declaredDependencies: z.array(z.string()).optional(),
51
62
  });
52
63
  const missingFile = (facts, file) => facts.files[file]?.exists !== true;
53
64
  // RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
@@ -359,6 +370,61 @@ function importDriftConformance(graph, facts) {
359
370
  };
360
371
  });
361
372
  }
373
+ // ---------------------------------------------------------------------------
374
+ // RC-06: an `external` realRef names a package the consumer actually depends on
375
+ // (CR-SM-262).
376
+ //
377
+ // RC-01..03 skip `external === true` — and rightly so: the path of a foreign package is
378
+ // not in this repo and cannot be resolved here. The consequence was that external bindings
379
+ // were never checked at all and rotted in silence. Found by hand at the graphcode
380
+ // self-model on 2026-08-22, not by a rule: `SCHEMA-metric-vector` carried
381
+ // `realRef.file = "packages/se-optimizer/src/metrics.ts"`, and `@sigloch/se-optimizer` had
382
+ // ceased to exist with CR-SM-248 — `MetricVector` moved to `se-engine`. The node pointed at
383
+ // a package that is not there, and RC-01 said nothing because `external: true` was set.
384
+ //
385
+ // The PATH is unverifiable here; the PACKAGE NAME is not. A path of the shape
386
+ // `packages/<name>/…` claims a workspace package, so `@sigloch/<name>` must appear in the
387
+ // consumer's dependencies. Any other path shape stays unchecked rather than guessed.
388
+ //
389
+ // Severity `warning`, not `error`: the path may deviate from the convention for honest
390
+ // reasons (monorepo restructuring, a linked working copy), and an `error` would block every
391
+ // further mutation through the delta gate. A rotted binding is a completeness signal, not a
392
+ // mismeasurement — unlike R-29, where the number itself becomes wrong.
393
+ // ---------------------------------------------------------------------------
394
+ /** `packages/<name>/…` — the workspace-package path shape RC-06 can decide. */
395
+ const WORKSPACE_PATH = /^packages\/([^/]+)\//;
396
+ /** Package scope of this family. A path claims `<SCOPE>/<name>`. */
397
+ const WORKSPACE_SCOPE = '@sigloch';
398
+ function externalRefMustNameDependency(graph, facts) {
399
+ const declared = facts.declaredDependencies;
400
+ if (declared === undefined)
401
+ return []; // extractor supplied none — silence, s. CodeFactsSchema
402
+ const known = new Set(declared);
403
+ const violations = [];
404
+ for (const el of graph.elements) {
405
+ // `concept: true` has no binding that could rot — a concept node claims nothing about code.
406
+ if (el.attributes?.external !== true || el.attributes?.concept === true)
407
+ continue;
408
+ const parsed = RealRefSchema.safeParse(el.attributes?.realRef);
409
+ if (!parsed.success)
410
+ continue;
411
+ const match = WORKSPACE_PATH.exec(parsed.data.file);
412
+ if (!match)
413
+ continue; // not the shape this rule can decide
414
+ const pkg = `${WORKSPACE_SCOPE}/${match[1]}`;
415
+ if (known.has(pkg))
416
+ continue;
417
+ violations.push({
418
+ rule_id: 'RC-06',
419
+ severity: 'warning',
420
+ element_id: el.id,
421
+ message: `${el.id} binds to '${parsed.data.file}', but '${pkg}' is not a declared dependency`,
422
+ fix_hint: `Add '${pkg}' to dependencies, or re-point the realRef at the package that now owns the symbol`,
423
+ context: { element_type: el.type, element_name: el.name },
424
+ });
425
+ }
426
+ return violations;
427
+ }
362
428
  /** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
363
429
  export const CODE_CONFORMANCE_RULES = [
364
430
  { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
@@ -366,6 +432,7 @@ export const CODE_CONFORMANCE_RULES = [
366
432
  { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
367
433
  { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
368
434
  { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', evaluate: importDriftConformance },
435
+ { id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', evaluate: externalRefMustNameDependency },
369
436
  ];
370
437
  /** Run all RC rules against a graph + extracted code facts. */
371
438
  export function evaluateConformanceRules(graph, facts) {
@@ -87,10 +87,31 @@ function noConcurrentMutation(graph) {
87
87
  return violations;
88
88
  }
89
89
  // ---------------------------------------------------------------------------
90
- // CR-R04: CR must have at least one relation→FUNC trace (CR-207)
90
+ // CR-R04: an OPEN CR must have at least one relation→FUNC trace (CR-207,
91
+ // Grundgesamtheit verengt in CR-SM-255)
92
+ //
93
+ // "Welche Funktionen fasst dieser CR an" ist eine PLANUNGSfrage — sie steuert, bevor
94
+ // gebaut wird. An einem geschlossenen CR ist sie Archaeologie: was er beruehrt hat,
95
+ // steht im Commit und im Diff, nicht im Graphen. Wer die Kanten nachtraeglich zieht,
96
+ // raet, und erfundene Praezision ist teurer als eine fehlende Kante.
97
+ //
98
+ // Gemessen am graphcode-Selbstmodell (graphVersion 171): 41 meldende CRs, davon 40
99
+ // `done` und 1 `dropped` — kein einziger offener. Die Regel feuerte ausschliesslich
100
+ // dort, wo sie nichts mehr steuern kann, und trug damit 41 unbearbeitbare Befunde.
101
+ //
102
+ // Die Statusmenge ist woertlich die von CR-R03 (s. o.): dieselbe Frage nach "noch in
103
+ // Arbeit", dieselbe Antwort. CR-R01 ("ein CR trackt ueberhaupt etwas") bleibt fuer
104
+ // ALLE CRs gueltig — nur die Verschaerfung "und zwar einen FUNC" ist Planung.
91
105
  // ---------------------------------------------------------------------------
92
106
  function crMustHaveFunc(graph) {
93
- const crs = graph.elements.filter(e => e.type === 'CR');
107
+ // Ein CR ganz OHNE status ist kein geschlossener CR — die Regel darf nicht durch ein
108
+ // fehlendes Attribut stumm werden.
109
+ const crs = graph.elements.filter(e => {
110
+ if (e.type !== 'CR')
111
+ return false;
112
+ const status = e.attributes?.status;
113
+ return status === undefined || status === 'open' || status === 'in-progress';
114
+ });
94
115
  return crs
95
116
  .filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' &&
96
117
  graph.elements.some(e => e.id === t.target && e.type === 'FUNC')))
@@ -138,7 +159,7 @@ export const CR_RULES = [
138
159
  // (= alle Elemente) ist die einzige Menge, aus der der Zielknoten garantiert stammt.
139
160
  // Damit ist sie nach R-08/R-18 die dritte 'all'-Regel — bewusst, nicht vergessen.
140
161
  { id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation, domain: ['all'] },
141
- { id: 'CR-R04', name: 'CR must have FUNC', severity: 'warning', evaluate: crMustHaveFunc, domain: ['CR'] },
162
+ { id: 'CR-R04', name: 'Open CR must have FUNC', severity: 'warning', evaluate: crMustHaveFunc, domain: ['CR'] },
142
163
  { id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone, domain: ['CR'] },
143
164
  ];
144
165
  // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
@@ -1,33 +1,36 @@
1
+ import { indexOf } from './graph-index.js';
1
2
  // ---------------------------------------------------------------------------
2
3
  // FC-01: FCHAIN must have Actor boundary (input or output via FLOW→ACTOR io)
3
4
  // ---------------------------------------------------------------------------
4
5
  export function fc01ActorBoundary(graph) {
5
- return graph.elements
6
- .filter(e => e.type === 'FCHAIN')
6
+ // CR-SM-264: dieselbe Frage, einmal statt je Kette neu.
7
+ //
8
+ // Die alte Fassung war die verschachtelte Form, die UC-02 kubisch gemacht hat (CR-SM-261):
9
+ // je FCHAIN alle Traces, darin je io-Kante noch einmal alle Traces, darin je Treffer alle
10
+ // Elemente — `FCHAIN x FUNC x T x T x E`. Im Profil nach Teil a war FC-01 mit 6,3 % die
11
+ // teuerste FCHAIN-Regel.
12
+ //
13
+ // Beide Zweige fragen in Wahrheit DASSELBE: "beruehrt dieses Element eine io-Kante, deren
14
+ // anderes Ende ein ACTOR ist?" Einmal fuer den Zwischenknoten der Kettenglieder, einmal
15
+ // direkt fuer den Eltern-UC. Eine Menge beantwortet beide.
16
+ const idx = indexOf(graph);
17
+ const actorAdjacent = new Set();
18
+ for (const t of idx.tracesOfType('io')) {
19
+ if (idx.typeOf(t.target) === 'ACTOR')
20
+ actorAdjacent.add(t.source);
21
+ if (idx.typeOf(t.source) === 'ACTOR')
22
+ actorAdjacent.add(t.target);
23
+ }
24
+ return idx.elementsOfType('FCHAIN')
7
25
  .filter(fc => {
8
- // Get FUNCs in this FCHAIN via compose
9
- const funcIds = graph.traces
10
- .filter(t => t.source === fc.id && t.type === 'compose')
11
- .map(t => t.target);
12
- // Check if any FUNC has io trace involving an ACTOR (via FLOW)
13
- const hasActorIO = funcIds.some(fid => graph.traces.some(t => {
14
- if (t.type !== 'io')
15
- return false;
16
- // FUNC→FLOW or FLOW→FUNC
17
- const flowId = t.source === fid ? t.target : (t.target === fid ? t.source : null);
18
- if (!flowId)
19
- return false;
20
- // FLOW→ACTOR or ACTOR→FLOW
21
- return graph.traces.some(ft => ft.type === 'io' &&
22
- ((ft.source === flowId && graph.elements.some(e => e.id === ft.target && e.type === 'ACTOR')) ||
23
- (ft.target === flowId && graph.elements.some(e => e.id === ft.source && e.type === 'ACTOR'))));
24
- }));
25
- // Also check direct ACTOR→UC io on the parent UC
26
- const parentUC = graph.traces.find(t => t.type === 'compose' && t.target === fc.id &&
27
- graph.elements.some(e => e.id === t.source && e.type === 'UC'));
28
- const hasDirectActorIO = parentUC && graph.traces.some(t => t.type === 'io' &&
29
- ((t.target === parentUC.source && graph.elements.some(e => e.id === t.source && e.type === 'ACTOR')) ||
30
- (t.source === parentUC.source && graph.elements.some(e => e.id === t.target && e.type === 'ACTOR'))));
26
+ // Mitglieder der Kette — bewusst OHNE Typfilter, wie in der alten Fassung: FC-01 fragt
27
+ // nach dem, was komponiert ist, nicht nach dem, was eine FUNC ist (FC-03 tut das).
28
+ const memberIds = idx.out(fc.id, 'compose').map(t => t.target);
29
+ const hasActorIO = memberIds.some(fid => idx.out(fid, 'io').some(t => actorAdjacent.has(t.target)) ||
30
+ idx.in(fid, 'io').some(t => actorAdjacent.has(t.source)));
31
+ // Der Umweg ueber den Eltern-UC: dessen eigene ACTOR-io zaehlt auch.
32
+ const parentUC = idx.in(fc.id, 'compose').find(t => idx.typeOf(t.source) === 'UC');
33
+ const hasDirectActorIO = parentUC !== undefined && actorAdjacent.has(parentUC.source);
31
34
  return !hasActorIO && !hasDirectActorIO;
32
35
  })
33
36
  .map(fc => ({
@@ -43,17 +46,15 @@ export function fc01ActorBoundary(graph) {
43
46
  // FC-02: Leaf UC (no UC→compose→UC) must have at least one FCHAIN
44
47
  // ---------------------------------------------------------------------------
45
48
  export function fc02LeafUcHasFchain(graph) {
46
- return graph.elements
47
- .filter(e => e.type === 'UC')
49
+ const idx = indexOf(graph);
50
+ return idx.elementsOfType('UC')
48
51
  .filter(uc => {
52
+ const composed = idx.out(uc.id, 'compose');
49
53
  // Leaf = no compose→UC children
50
- const hasUCChild = graph.traces.some(t => t.source === uc.id && t.type === 'compose' &&
51
- graph.elements.some(e => e.id === t.target && e.type === 'UC'));
52
- if (hasUCChild)
54
+ if (composed.some(t => idx.typeOf(t.target) === 'UC'))
53
55
  return false;
54
56
  // Must have compose→FCHAIN
55
- return !graph.traces.some(t => t.source === uc.id && t.type === 'compose' &&
56
- graph.elements.some(e => e.id === t.target && e.type === 'FCHAIN'));
57
+ return !composed.some(t => idx.typeOf(t.target) === 'FCHAIN');
57
58
  })
58
59
  .map(uc => ({
59
60
  rule_id: 'FC-02',
@@ -69,15 +70,13 @@ export function fc02LeafUcHasFchain(graph) {
69
70
  // ---------------------------------------------------------------------------
70
71
  export function fc03FchainFlat(graph) {
71
72
  const violations = [];
72
- const fchains = graph.elements.filter(e => e.type === 'FCHAIN');
73
- for (const fc of fchains) {
74
- const funcIds = graph.traces
75
- .filter(t => t.source === fc.id && t.type === 'compose')
73
+ const idx = indexOf(graph);
74
+ for (const fc of idx.elementsOfType('FCHAIN')) {
75
+ const funcIds = idx.out(fc.id, 'compose')
76
76
  .map(t => t.target)
77
- .filter(id => graph.elements.some(e => e.id === id && e.type === 'FUNC'));
77
+ .filter(id => idx.typeOf(id) === 'FUNC');
78
78
  for (const fid of funcIds) {
79
- const nestedCompose = graph.traces.filter(t => t.source === fid && t.type === 'compose' &&
80
- graph.elements.some(e => e.id === t.target && e.type === 'FUNC'));
79
+ const nestedCompose = idx.out(fid, 'compose').filter(t => idx.typeOf(t.target) === 'FUNC');
81
80
  if (nestedCompose.length > 0) {
82
81
  violations.push({
83
82
  rule_id: 'FC-03',
@@ -85,7 +84,7 @@ export function fc03FchainFlat(graph) {
85
84
  element_id: fid,
86
85
  message: `${fid} in ${fc.id} has nested FUNC compose (should be flat)`,
87
86
  fix_hint: 'Move nested functions to FCHAIN level (flat composition)',
88
- context: { element_type: 'FUNC', element_name: graph.elements.find(e => e.id === fid)?.name ?? fid },
87
+ context: { element_type: 'FUNC', element_name: idx.byId.get(fid)?.name ?? fid },
89
88
  });
90
89
  }
91
90
  }
@@ -105,24 +104,29 @@ export function fc03FchainFlat(graph) {
105
104
  // to have unrelated ACTOR io.
106
105
  // ---------------------------------------------------------------------------
107
106
  export function fc04ActorBounded(graph) {
108
- return graph.elements
109
- .filter(e => e.type === 'FCHAIN')
107
+ // CR-SM-264: die beiden Zwischenmengen einmal je Graph statt |io|^2 je Kette.
108
+ // `actorFed` sind die Knoten, in die ein ACTOR hineinschickt, `actorConsumed` die, aus denen
109
+ // ein ACTOR liest. Beide Richtungen bleiben getrennt — anders als bei FC-01, das nur fragt,
110
+ // OB ein ACTOR anliegt; FC-04 verlangt Eingang UND Ausgang.
111
+ const idx = indexOf(graph);
112
+ const io = idx.tracesOfType('io');
113
+ const actorFed = new Set();
114
+ const actorConsumed = new Set();
115
+ for (const a of io) {
116
+ if (idx.typeOf(a.source) === 'ACTOR')
117
+ actorFed.add(a.target);
118
+ if (idx.typeOf(a.target) === 'ACTOR')
119
+ actorConsumed.add(a.source);
120
+ }
121
+ return idx.elementsOfType('FCHAIN')
110
122
  .filter(fc => {
111
- const funcIds = new Set(graph.traces
112
- .filter(t => t.source === fc.id && t.type === 'compose')
113
- .map(t => t.target)
114
- .filter(id => graph.elements.some(e => e.id === id && e.type === 'FUNC')));
123
+ const funcIds = new Set(idx.out(fc.id, 'compose').map(t => t.target).filter(id => idx.typeOf(id) === 'FUNC'));
115
124
  if (funcIds.size === 0)
116
125
  return false; // R-15 already flags the empty-chain case
117
- const io = graph.traces.filter(t => t.type === 'io');
118
- const isActor = (id) => graph.elements.some(e => e.id === id && e.type === 'ACTOR');
119
- const isFlow = (id) => graph.elements.some(e => e.id === id && e.type === 'FLOW');
120
126
  // Entry: ACTOR→FLOW→FUNC∈chain (something triggers the chain).
121
- const entry = io.some(e => funcIds.has(e.target) && isFlow(e.source) &&
122
- io.some(a => a.target === e.source && isActor(a.source)));
127
+ const entry = io.some(e => funcIds.has(e.target) && idx.typeOf(e.source) === 'FLOW' && actorFed.has(e.source));
123
128
  // Exit: FUNC∈chain→FLOW→ACTOR (the chain produces something back to an actor).
124
- const exit = io.some(e => funcIds.has(e.source) && isFlow(e.target) &&
125
- io.some(a => a.source === e.target && isActor(a.target)));
129
+ const exit = io.some(e => funcIds.has(e.source) && idx.typeOf(e.target) === 'FLOW' && actorConsumed.has(e.target));
126
130
  return !(entry && exit);
127
131
  })
128
132
  .map(fc => ({
@@ -0,0 +1,3 @@
1
+ import type { OntologyGraph } from './ontology.js';
2
+ /** Hebt eine flach committete SSOT in die Form, die der Live-Gate dem Evaluator gibt. */
3
+ export declare function toEvaluableGraph(graph: OntologyGraph): OntologyGraph;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CR-SM-258 / CR-SM-260 — eine committete `*.graph.json` ist FLACH.
3
+ *
4
+ * graphcode schreibt seine SSOT seit CR-GC-219 flach, damit die Datei, die ein Mensch diffed,
5
+ * jeden Wert genau einmal traegt: `{id, type, name, description, ...attributes}`, gar kein
6
+ * `attributes`-Schluessel. Die Regeln lesen aber `e.attributes?.*` — der Zustand, den der
7
+ * Live-Gate aus Kuzu projiziert. Wer die flache Form ungehoben in den Evaluator reicht, macht
8
+ * JEDES Attribut unsichtbar:
9
+ *
10
+ * graphcode-Selbstmodell, graphVersion 171 — flach gelesen | korrekt genestet
11
+ * R-19 123 | 0 R-20 95 | 0 Summe 610 | 374
12
+ *
13
+ * 236 Phantom-Befunde, und CR-R02 (severity error) war umgekehrt komplett stumm. Jede
14
+ * Gate-7-Zahl zwischen CR-SM-246 und CR-SM-258 war davon betroffen.
15
+ *
16
+ * Warum die Hebung hier im QUELLTEXT steht und nicht mehr in der Messdatei (CR-SM-260): sie hat
17
+ * einen zweiten Nutzer bekommen — den Regel-Profillauf in `@sigloch/se-engine`, der dasselbe
18
+ * Selbstmodell liest. Zwei Kopien waeren zwei Projektionen derselben Wahrheit, und die
19
+ * fehlerhafte faellt genau dann nicht auf, wenn sie nur Zeiten misst statt Befunde.
20
+ *
21
+ * Warum nicht `fromOntologyGraph` aus `@sigloch/graph-api-core` (CR-SM-254): das Paket haengt an
22
+ * contracts, nicht umgekehrt — ein Import waere ein Zyklus. Statt die Projektion von Hand
23
+ * nachzubauen (genau der Fehler, der hier repariert wird), leitet die Hebung ihre kanonische
24
+ * Schluesselmenge aus den Zod-Schemata ab. Sie kann damit nicht von der Ontologie wegdriften:
25
+ * ein neues Top-Level-Feld ist sofort auch hier eines.
26
+ */
27
+ import { OntologyElement, Trace } from './ontology.js';
28
+ const TOP_LEVEL_ELEMENT_KEYS = new Set(Object.keys(OntologyElement.shape));
29
+ const TOP_LEVEL_TRACE_KEYS = new Set(Object.keys(Trace.shape));
30
+ function nestAttributes(entry, canonical) {
31
+ if (entry.attributes !== undefined)
32
+ return entry; // schon in der genesteten Form
33
+ const top = {};
34
+ const attributes = {};
35
+ for (const [key, value] of Object.entries(entry)) {
36
+ (canonical.has(key) ? top : attributes)[key] = value;
37
+ }
38
+ // `status` lebt in BEIDEN Welten: als Top-Level-Feld am Schema und unter `attributes`, wo
39
+ // CR-R02/CR-R03/CR-R04 es lesen. Die Kuzu-Projektion liefert beides, also spiegelt die
40
+ // Hebung es ebenfalls — sonst lesen genau die CR-Regeln wieder ins Leere.
41
+ if (entry.status !== undefined)
42
+ attributes.status = entry.status;
43
+ return { ...top, attributes };
44
+ }
45
+ /** Hebt eine flach committete SSOT in die Form, die der Live-Gate dem Evaluator gibt. */
46
+ export function toEvaluableGraph(graph) {
47
+ return {
48
+ ...graph,
49
+ elements: graph.elements.map((e) => nestAttributes(e, TOP_LEVEL_ELEMENT_KEYS)),
50
+ traces: graph.traces.map((t) => nestAttributes(t, TOP_LEVEL_TRACE_KEYS)),
51
+ };
52
+ }
@@ -13,11 +13,12 @@ export declare const GRAMMAR_SNAPSHOT: {
13
13
  readonly versions: {
14
14
  readonly ontology: "7.0.0";
15
15
  readonly metaModel: "2.0.0";
16
- readonly rules: "6.0.0";
16
+ readonly rules: "6.3.0";
17
17
  };
18
18
  readonly elementTypes: readonly ["ACTOR", "CR", "FCHAIN", "FLOW", "FUNC", "MOD", "MS", "REQ", "SCHEMA", "SESSION", "SYS", "TEST", "UC"];
19
19
  readonly traceTypes: readonly ["allocate", "compose", "io", "produces", "relation", "satisfy", "verify"];
20
20
  readonly patterns: readonly ["ACTOR -io-> FLOW", "ACTOR -io-> UC", "CR -relation-> FUNC", "CR -relation-> MOD", "CR -relation-> MS", "CR -relation-> REQ", "CR -relation-> UC", "FCHAIN -compose-> FUNC [1..*]", "FCHAIN -satisfy-> REQ", "FLOW -io-> ACTOR", "FLOW -io-> FUNC", "FLOW -io-> UC", "FLOW -relation-> SCHEMA", "FUNC -allocate-> MOD", "FUNC -compose-> FUNC [0..*]", "FUNC -io-> FLOW", "FUNC -satisfy-> REQ", "MOD -compose-> MOD [0..*]", "MOD -io-> MOD", "MOD -satisfy-> REQ", "MS -compose-> FUNC", "MS -compose-> MS", "MS -compose-> REQ", "MS -compose-> UC", "MS -relation-> MS", "REQ -compose-> REQ [0..*]", "SESSION -produces-> * (audit)", "SYS -compose-> MOD [0..*]", "SYS -compose-> REQ [0..*]", "SYS -compose-> SYS [0..*]", "SYS -compose-> UC [1..*]", "SYS -satisfy-> REQ", "TEST -verify-> REQ", "UC -compose-> FCHAIN [1..*]", "UC -compose-> REQ [1..*]"];
21
21
  readonly attributes: readonly ["CR.rationale: string", "CR.spike: boolean", "CR.status: enum", "FLOW.protocol: string", "FLOW.qos: string", "FUNC.concept: boolean", "FUNC.external: boolean", "FUNC.measuredMs: number", "FUNC.realRef: object", "FUNC.safety_relevant: boolean", "FUNC.sourceFile: string", "FUNC.timingBudgetMs: number", "MOD.concept: boolean", "MOD.external: boolean", "MOD.kind: string", "MOD.path: string", "MOD.realRef: object", "REQ.detection: number", "REQ.occurrence: number", "REQ.severity: number", "SCHEMA.concept: boolean", "SCHEMA.contract: string", "SCHEMA.external: boolean", "SCHEMA.realRef: object", "TEST.concept: boolean", "TEST.sourceFile: string", "TEST.testRefs: array", "UC.operatingMode: string"];
22
22
  readonly rules: readonly ["AF-01 (warning) domain=[graph]", "AF-02 (warning) domain=[graph]", "AF-03 (warning) domain=[graph]", "AF-04 (warning) domain=[graph]", "AF-05 (warning) domain=[graph]", "AO-D01 (info) domain=[FUNC]", "AO-D03 (info) domain=[FUNC]", "BQ-01 (warning) domain=[REQ]", "BQ-02 (warning) domain=[REQ]", "BQ-04 (warning) domain=[REQ]", "BQ-06 (warning) domain=[REQ]", "BQ-07 (warning) domain=[REQ]", "CA-01 (error) domain=[FUNC]", "CL-01 (warning) domain=[ACTOR]", "CR-01 (warning) domain=[MOD]", "CR-R01 (error) domain=[CR]", "CR-R02 (error) domain=[CR]", "CR-R03 (warning) domain=[all]", "CR-R04 (warning) domain=[CR]", "FC-01 (warning) domain=[FCHAIN]", "FC-02 (warning) domain=[UC]", "FC-03 (warning) domain=[FUNC]", "FC-04 (warning) domain=[FCHAIN]", "FM-01 (warning) domain=[REQ]", "FM-02 (warning) domain=[REQ]", "FM-03 (error) domain=[REQ]", "IO-01 (warning) domain=[FUNC]", "MS-01 (warning) domain=[MS]", "MS-02 (error) domain=[MS]", "MS-03 (info) domain=[CR]", "MT-01 (warning) domain=[MOD]", "MT-02 (info) domain=[MOD]", "ND-01 (error) domain=[FUNC]", "ND-02 (error) domain=[SCHEMA]", "NFR-01 (warning) domain=[FCHAIN,FUNC,MOD]", "PH-01 (info) domain=[MOD]", "R-01 (error) domain=[REQ]", "R-02 (warning) domain=[FUNC]", "R-03 (error) domain=[MOD]", "R-04 (warning) domain=[MOD]", "R-05 (warning) domain=[TEST]", "R-08 (error) domain=[all]", "R-10 (warning) domain=[FLOW]", "R-12 (warning) domain=[FUNC]", "R-14 (warning) domain=[UC]", "R-15 (warning) domain=[FCHAIN]", "R-16 (warning) domain=[ACTOR]", "R-17 (warning) domain=[SYS]", "R-18 (error) domain=[all]", "R-19 (warning) domain=[TEST]", "R-20 (warning) domain=[FUNC]", "R-21 (warning) domain=[FCHAIN]", "R-22 (warning) domain=[FUNC]", "R-23 (warning) domain=[MOD]", "R-26 (warning) domain=[SCHEMA]", "R-27 (warning) domain=[MOD]", "R-29 (error) domain=[TEST]", "R-30 (warning) domain=[FUNC]", "R-31 (warning) domain=[FUNC]", "RD-01 (warning) domain=[REQ]", "RD-02 (warning) domain=[REQ]", "RD-03 (info) domain=[REQ]", "RD-04 (warning) domain=[FUNC,MOD,SYS]", "RT-01 (error) domain=[FUNC]", "SC-02 (warning) domain=[SCHEMA]", "SC-04 (warning) domain=[FLOW]", "UC-01 (error) domain=[UC]", "UC-02 (error) domain=[UC]", "UC-03 (warning) domain=[UC]", "UC-04 (warning) domain=[UC]", "UC-05 (info) domain=[UC]", "UC-06 (info) domain=[UC]", "VR-01 (info) domain=[TEST]"];
23
+ readonly conformanceRules: readonly ["RC-01 (error)", "RC-02 (error)", "RC-03 (error)", "RC-04 (warning)", "RC-05 (warning)", "RC-06 (warning)"];
23
24
  };
@@ -13,7 +13,7 @@ export const GRAMMAR_SNAPSHOT = {
13
13
  versions: {
14
14
  ontology: "7.0.0",
15
15
  metaModel: "2.0.0",
16
- rules: "6.0.0",
16
+ rules: "6.3.0",
17
17
  },
18
18
  elementTypes: [
19
19
  "ACTOR",
@@ -181,4 +181,12 @@ export const GRAMMAR_SNAPSHOT = {
181
181
  "UC-06 (info) domain=[UC]",
182
182
  "VR-01 (info) domain=[TEST]",
183
183
  ],
184
+ conformanceRules: [
185
+ "RC-01 (error)",
186
+ "RC-02 (error)",
187
+ "RC-03 (error)",
188
+ "RC-04 (warning)",
189
+ "RC-05 (warning)",
190
+ "RC-06 (warning)",
191
+ ],
184
192
  };