@sigloch/graph-api-core 2.0.0 → 3.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/dist/audit.d.ts CHANGED
@@ -18,6 +18,28 @@ export interface AuditEntry {
18
18
  result: 'applied' | 'rejected' | 'partial';
19
19
  violations?: RuleViolation[];
20
20
  graphVersion: number;
21
+ /**
22
+ * The POSITIVE half of the finding (CR-GC-314): rule IDs that were evaluated for this
23
+ * mutation and returned nothing.
24
+ *
25
+ * `violations` records only what went wrong, so an accepted mutation leaves an empty
26
+ * field — and "rule R-18 checked this edit and passed it" is not recoverable from
27
+ * "no violation". A later learning mechanism can learn from the first statement and
28
+ * nothing at all from the second; that asymmetry is the whole reason this field exists.
29
+ *
30
+ * Rule IDs only, never rule text (REQ-A04). Optional and purely additive: an entry
31
+ * written before this field means "not recorded", NOT "passed nothing" — a consumer
32
+ * must not read absence as an empty pass set (REQ-A05). Old records are deliberately
33
+ * not back-filled: reconstructing them would evaluate against a rule version that did
34
+ * not hold at mutation time, which is itself a provenance violation.
35
+ */
36
+ rulesPassed?: string[];
37
+ /**
38
+ * Version of the rule set this mutation was evaluated against (REQ-A02) — read from
39
+ * the loaded package, never from config, so a rule-set change is visible in the trail
40
+ * at exactly the records it affected.
41
+ */
42
+ rulesetVersion?: string;
21
43
  }
22
44
  export interface AuditLog {
23
45
  record(entry: AuditEntry): Promise<void>;
package/dist/browser.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
15
  export type { RootQueryGraph } from './find-root.js';
16
- export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
16
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
17
17
  export { FormatECodec } from './format-e-codec.js';
18
18
  export { isValidTrace, tracePatternsOf } from './types.js';
19
19
  export type { GraphNode, GraphEdge, Graph } from './types.js';
package/dist/browser.js CHANGED
@@ -12,6 +12,6 @@
12
12
  * @author andreas@siglochconsulting
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
- export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
15
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
16
16
  export { FormatECodec } from './format-e-codec.js';
17
17
  export { isValidTrace, tracePatternsOf } from './types.js';
@@ -38,6 +38,21 @@ export declare class FormatECodec {
38
38
  * `localeCompare`, which is locale-dependent.
39
39
  */
40
40
  private serializeEdges;
41
+ /**
42
+ * CR-GC-334: an object attribute has no place in the inline block — `[k:v,k:v]` splits on
43
+ * commas, and `String({…})` produced the literal `[object Object]`, i.e. the binding was
44
+ * destroyed on write. Structured values therefore go to `@key {json}` follow-lines
45
+ * (`structuredAttrLines`), which `parse()` hydrates back into objects.
46
+ *
47
+ * EDGES keep the inline block (they must stay single-line, see `serializeEdges`), so an
48
+ * object on an EDGE is JSON-stringified inline — legible, but it round-trips only while it
49
+ * contains no comma. No edge in the SE ontology carries a structured attribute today; if one
50
+ * ever does, the inline block is the thing to replace, not this escape.
51
+ */
41
52
  private serializeAttrs;
53
+ /** CR-GC-334: object/array attributes as `@key {json}` lines below the node entry. */
54
+ private structuredAttrLines;
55
+ /** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
56
+ private serializeEdgeAttrs;
42
57
  private edgeTypeToArrow;
43
58
  }
@@ -7,6 +7,7 @@
7
7
  * family (`TYPE-slug`, `Name.TypeAbbr.Counter`, `cand_<hex>`); typing by spelling made
8
8
  * every foreign convention fail silently instead of loudly.
9
9
  */
10
+ import { hydrateAttrValue } from '@sigloch/contracts/se';
10
11
  import { isValidTrace, tracePatternsOf } from './types.js';
11
12
  // ---------------------------------------------------------------------------
12
13
  // Regex patterns
@@ -73,7 +74,11 @@ export class FormatECodec {
73
74
  if (lastOp && (lastOp.type === 'add_node' || lastOp.type === 'update_node' || lastOp.type === 'strict_add_node')) {
74
75
  if (!lastOp.attributes)
75
76
  lastOp.attributes = {};
76
- lastOp.attributes[attrMatch[1]] = attrMatch[2].trim();
77
+ // CR-GC-334: `serialize()` writes objects as `@key {json}`, so `parse()` must read
78
+ // them back as objects. Kept as a string, `realRef`/`testRef` fail their schema and
79
+ // the element reads as UNBOUND — R-19/R-20 fired on every node authored through this
80
+ // path. Same rule as the contracts parser, imported, not re-implemented.
81
+ lastOp.attributes[attrMatch[1]] = hydrateAttrValue(attrMatch[2].trim());
77
82
  }
78
83
  else {
79
84
  errors.push(`@attribute line without preceding node: "${line}"`);
@@ -166,6 +171,9 @@ export class FormatECodec {
166
171
  const descr = node.description ? `|${node.description}` : '';
167
172
  const attrs = this.serializeAttrs(node.attributes);
168
173
  lines.push(`+ ${node.uid}${descr}${attrs}`);
174
+ // CR-GC-334: realRef/testRef & friends as @key {json} — the inline block above
175
+ // cannot carry them, and dropping them here is what made bindings vanish.
176
+ lines.push(...this.structuredAttrLines(node.attributes));
169
177
  }
170
178
  }
171
179
  }
@@ -327,7 +335,7 @@ export class FormatECodec {
327
335
  const entries = [];
328
336
  const groups = new Map();
329
337
  for (const edge of edges) {
330
- const attrs = this.serializeAttrs(edge.attributes);
338
+ const attrs = this.serializeEdgeAttrs(edge.attributes);
331
339
  if (attrs) {
332
340
  entries.push({ sourceId: edge.sourceId, edgeType: edge.edgeType, targets: [edge.targetId], attrs });
333
341
  continue;
@@ -352,13 +360,38 @@ export class FormatECodec {
352
360
  || cmp(a.attrs, b.attrs));
353
361
  return entries.map(e => `+ ${e.sourceId} -${this.edgeTypeToArrow(e.edgeType)}-> ${e.targets.join(', ')}${e.attrs}`);
354
362
  }
363
+ /**
364
+ * CR-GC-334: an object attribute has no place in the inline block — `[k:v,k:v]` splits on
365
+ * commas, and `String({…})` produced the literal `[object Object]`, i.e. the binding was
366
+ * destroyed on write. Structured values therefore go to `@key {json}` follow-lines
367
+ * (`structuredAttrLines`), which `parse()` hydrates back into objects.
368
+ *
369
+ * EDGES keep the inline block (they must stay single-line, see `serializeEdges`), so an
370
+ * object on an EDGE is JSON-stringified inline — legible, but it round-trips only while it
371
+ * contains no comma. No edge in the SE ontology carries a structured attribute today; if one
372
+ * ever does, the inline block is the thing to replace, not this escape.
373
+ */
355
374
  serializeAttrs(attrs) {
356
- const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '');
375
+ const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '' && typeof v !== 'object');
357
376
  if (entries.length === 0)
358
377
  return '';
359
378
  const pairs = entries.map(([k, v]) => `${k}:${String(v)}`);
360
379
  return ` [${pairs.join(',')}]`;
361
380
  }
381
+ /** CR-GC-334: object/array attributes as `@key {json}` lines below the node entry. */
382
+ structuredAttrLines(attrs) {
383
+ return Object.entries(attrs)
384
+ .filter(([, v]) => v != null && typeof v === 'object')
385
+ .map(([k, v]) => `@${k} ${JSON.stringify(v)}`);
386
+ }
387
+ /** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
388
+ serializeEdgeAttrs(attrs) {
389
+ const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '');
390
+ if (entries.length === 0)
391
+ return '';
392
+ const pairs = entries.map(([k, v]) => `${k}:${typeof v === 'object' ? JSON.stringify(v) : String(v)}`);
393
+ return ` [${pairs.join(',')}]`;
394
+ }
362
395
  edgeTypeToArrow(edgeType) {
363
396
  // Use first defined arrow alias
364
397
  const desc = this.ontology.edgeTypes[edgeType];
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { MemoryAdapter } from './memory-adapter.js';
22
22
  export type { TransportAdapter, TransportConfig } from './transport-adapter.js';
23
23
  export { createGraphApi } from './factory.js';
24
24
  export type { GraphApiConfig } from './factory.js';
25
- export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
25
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
26
26
  export { findRoot } from './find-root.js';
27
27
  export type { RootQueryGraph } from './find-root.js';
28
28
  export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { MemoryAdapter } from './memory-adapter.js';
19
19
  // Factory
20
20
  export { createGraphApi } from './factory.js';
21
21
  // SE OntologyDescriptor (derived from @sigloch/contracts/se) [CR-195a]
22
- export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
22
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
23
23
  // Root-Suche — strukturelle Wurzel (SYS ohne eingehende compose), statt UID-Hardcode
24
24
  export { findRoot } from './find-root.js';
25
25
  // Edge ops — update-edge (flip/retype) + merge-nodes, shared by GraphService.mutate()
@@ -12,11 +12,30 @@ export interface RuleViolation {
12
12
  fixHint?: string;
13
13
  /** Carried from the contracts rule: candidate_targets / existing_traces context for fix automation. */
14
14
  context?: unknown;
15
+ /**
16
+ * Stamped by the engine from the rule that produced this violation (CR-GC-312).
17
+ * `false` = visible but not gate-relevant. Absent means gating, so every existing
18
+ * consumer keeps its behaviour without reading the field.
19
+ */
20
+ gating?: boolean;
15
21
  }
16
22
  export interface Rule {
17
23
  id: string;
18
24
  name: string;
19
25
  severity: 'error' | 'warning' | 'info';
26
+ /**
27
+ * May an `error` from this rule BLOCK a write? (CR-GC-312) Default `true`.
28
+ *
29
+ * Severity says how bad a finding is; this says whether the gate acts on it. The
30
+ * two are separate because a rule family can be correct and worth surfacing long
31
+ * before a repo has paid off its backlog — switching one on would otherwise freeze
32
+ * every write on debt the writer did not create. Set `false` to surface a rule in
33
+ * `evaluate`/readiness while it is still being worked down, then promote it.
34
+ *
35
+ * NOT a severity downgrade: the violation keeps `severity: 'error'` and reads as one
36
+ * everywhere it is displayed.
37
+ */
38
+ gating?: boolean;
20
39
  evaluate: (graph: Graph) => RuleViolation[];
21
40
  }
22
41
  export interface RuleEngine {
@@ -10,7 +10,12 @@ export class DefaultRuleEngine {
10
10
  evaluate(graph) {
11
11
  const violations = [];
12
12
  for (const rule of this.rules) {
13
- violations.push(...rule.evaluate(graph));
13
+ // CR-GC-312: stamp `gating` from the rule so a consumer's gate can filter
14
+ // without a second lookup into the catalog. Only stamped when the rule opts
15
+ // OUT — an absent field means gating, the pre-existing behaviour.
16
+ const stamp = rule.gating === false ? { gating: false } : undefined;
17
+ for (const v of rule.evaluate(graph))
18
+ violations.push(stamp ? { ...v, ...stamp } : v);
14
19
  }
15
20
  return violations;
16
21
  }
@@ -9,7 +9,7 @@
9
9
  * - Graph (nodes/edges) ⟷ OntologyGraph (elements/traces)
10
10
  * - RuleViolation (ruleId/…) ⟷ contracts RuleViolation (rule_id/…)
11
11
  */
12
- import { type OntologyGraph } from '@sigloch/contracts/se';
12
+ import { type MetricPolicy, type OntologyGraph } from '@sigloch/contracts/se';
13
13
  import type { Graph, OntologyDescriptor } from './types.js';
14
14
  /**
15
15
  * Project an ontology-agnostic Graph (nodes/edges) onto the SE OntologyGraph
@@ -21,4 +21,13 @@ export declare function projectToOntologyGraph(graph: Graph): OntologyGraph;
21
21
  * Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
22
22
  * to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
23
23
  */
24
+ export declare function createSeDescriptor(policy: MetricPolicy): OntologyDescriptor;
25
+ /**
26
+ * The descriptor for a host without its own metric policy — judged with contracts'
27
+ * `DEFAULT_METRIC_POLICY`, which is a named, grep-able value, not a hidden fallback.
28
+ *
29
+ * A host that holds a configuration builds its own with `createSeDescriptor(policy)`
30
+ * and must then use only that one; two descriptors in one process would be the two
31
+ * thresholds CR-SM-233 removed.
32
+ */
24
33
  export declare const SE_DESCRIPTOR: OntologyDescriptor;
@@ -9,7 +9,7 @@
9
9
  * - Graph (nodes/edges) ⟷ OntologyGraph (elements/traces)
10
10
  * - RuleViolation (ruleId/…) ⟷ contracts RuleViolation (rule_id/…)
11
11
  */
12
- import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, ONTOLOGY_VERSION, } from '@sigloch/contracts/se';
12
+ import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, UC_RULES, FC_RULES, SC_RULES, CR_RULES, AO_RULES, FM_RULES, VIEW_RULES, AF_RULES, ONTOLOGY_VERSION, DEFAULT_METRIC_POLICY, } from '@sigloch/contracts/se';
13
13
  /**
14
14
  * Project an ontology-agnostic Graph (nodes/edges) onto the SE OntologyGraph
15
15
  * (elements/traces) that contracts/se rules evaluate against. Type-specific
@@ -45,28 +45,77 @@ export function projectToOntologyGraph(graph) {
45
45
  /**
46
46
  * The contracts/se rule catalog, adapted to graph-api-core's Rule shape.
47
47
  *
48
- * V3_RULES + MT_RULES (CR-SM-222): the architecture metrics lived in contracts since
49
- * K2 but ran only in aimpro's `evaluateAllRules`, so the governed repos (graphcode,
50
- * graph-view-edit) never saw them. Same adapter, no fork — they inherit by
51
- * consuming this descriptor. MT severities are warning/info, so they stay advisory
52
- * in graphcode's gate.
48
+ * CR-GC-312: this used to be `[...V3_RULES, ...MT_RULES]` — two of the twelve families
49
+ * contracts exports. Everything else (UC, FC, SC, CR, AO, FM, VIEW, AF) was written,
50
+ * versioned and shipped, and then evaluated by nobody: consumers register exactly
51
+ * `SE_DESCRIPTOR.rules`, so ten families were dead weight in the package. Measured on
52
+ * graphcode's SSOT the day this was found: 2 violations reported, 276 findable — among
53
+ * them `CR-R02` ("done but no commitRef", 91×), precisely the graph-vs-reality check
54
+ * the repo believed it had. A use case with no operational chain went unflagged for
55
+ * weeks because `UC-03`/`FC-02` existed and never ran.
56
+ *
57
+ * Two families stay out on purpose:
58
+ * - BQ/ND are the CODING profile (`getRuleDefsForProfile('coding')`), not SE.
59
+ * - RC (conformance) needs `CodeFacts` from a repo checkout, which this package has
60
+ * no access to; graphcode evaluates those itself in `src/conformance.ts`.
61
+ *
62
+ * `gating` (see `Rule`) is what makes the switch-on safe. V3/MT keep their gate power;
63
+ * the newly wired families are advisory until a repo has worked its backlog down —
64
+ * otherwise the 114 pre-existing errors in graphcode's own graph would block every
65
+ * write on debt the writer did not create. Promotion is a per-family decision, not a
66
+ * side effect of being registered.
53
67
  */
54
- const SE_RULES = [...V3_RULES, ...MT_RULES].map((def) => ({
55
- id: def.id,
56
- name: def.name,
57
- severity: def.severity,
58
- evaluate: (graph) => def.evaluate(projectToOntologyGraph(graph)).map((v) => ({
59
- ruleId: v.rule_id,
60
- ruleName: def.name,
61
- severity: v.severity,
62
- message: v.message,
63
- elementId: v.element_id,
64
- // CR-GC-203 item 1: carry the fix-context through instead of discarding it,
65
- // so rules_get_violations hands the agent candidates + a hint, not just a message.
66
- fixHint: v.fix_hint,
67
- context: v.context,
68
- })),
69
- }));
68
+ const GATING_PREFIXES = ['R-', 'RD-', 'MT-'];
69
+ const SE_RULE_DEFS = [
70
+ ...V3_RULES,
71
+ ...UC_RULES,
72
+ ...FC_RULES,
73
+ ...SC_RULES,
74
+ ...CR_RULES,
75
+ ...AO_RULES,
76
+ ...FM_RULES,
77
+ ...VIEW_RULES,
78
+ ...AF_RULES,
79
+ ];
80
+ function adapt(def, evaluate) {
81
+ // (evaluate is already policy-bound by the caller — see seRules below)
82
+ return {
83
+ id: def.id,
84
+ name: def.name,
85
+ severity: def.severity,
86
+ gating: GATING_PREFIXES.some((p) => def.id.startsWith(p)),
87
+ evaluate: (graph) => evaluate(projectToOntologyGraph(graph)).map((v) => ({
88
+ ruleId: v.rule_id,
89
+ ruleName: def.name,
90
+ severity: v.severity,
91
+ message: v.message,
92
+ elementId: v.element_id,
93
+ // CR-GC-203 item 1: carry the fix-context through instead of discarding it,
94
+ // so rules_get_violations hands the agent candidates + a hint, not just a message.
95
+ fixHint: v.fix_hint,
96
+ context: v.context,
97
+ })),
98
+ };
99
+ }
100
+ /**
101
+ * Every rule bound to the judging policy (CR-SM-233, extended by CR-SM-236).
102
+ *
103
+ * A threshold is input, not a constant, and it therefore cannot live in a descriptor
104
+ * constant: a host that configures the value (graphcode's `graphcode.config.jsonc`,
105
+ * CR-GC-329) would otherwise judge against its own number while the gate judged against
106
+ * a second one baked in here.
107
+ *
108
+ * CR-SM-236 removed the former split into a policy-free `SE_RULES` constant plus a
109
+ * policy-bound metric list. MT-01/MT-02 stopped being the only rules with a threshold
110
+ * (CR-01, FM-03 and R-04 joined them), so a policy-free rule constant had become a
111
+ * second, silently-judging path — exactly what CR-SM-233 set out to remove.
112
+ *
113
+ * A `null` threshold means measure, don't judge: the rule stays registered and simply
114
+ * reports nothing.
115
+ */
116
+ function seRules(policy) {
117
+ return [...SE_RULE_DEFS, ...MT_RULES].map((def) => adapt(def, (g) => def.evaluate(g, policy)));
118
+ }
70
119
  // `label` is the node-table identifier (kuzu table name) — must be a valid
71
120
  // identifier, so it's the ElementType key (SYS, UC, …), not the human
72
121
  // ELEMENT_DESCRIPTIONS text which would break DDL.
@@ -80,13 +129,24 @@ const edgeTypes = Object.fromEntries(TraceType.options.map((tt) => {
80
129
  * Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
81
130
  * to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
82
131
  */
83
- export const SE_DESCRIPTOR = {
84
- name: 'se',
85
- version: ONTOLOGY_VERSION,
86
- nodeTypes,
87
- edgeTypes,
88
- // CR-GC-247: TRACE_PATTERNS is the trace-legality SSOT; validPairs above is kept
89
- // only for Format-E arrow/menu enumeration. isValidTrace validates against these.
90
- patterns: TRACE_PATTERNS,
91
- rules: SE_RULES,
92
- };
132
+ export function createSeDescriptor(policy) {
133
+ return {
134
+ name: 'se',
135
+ version: ONTOLOGY_VERSION,
136
+ nodeTypes,
137
+ edgeTypes,
138
+ // CR-GC-247: TRACE_PATTERNS is the trace-legality SSOT; validPairs above is kept
139
+ // only for Format-E arrow/menu enumeration. isValidTrace validates against these.
140
+ patterns: TRACE_PATTERNS,
141
+ rules: seRules(policy),
142
+ };
143
+ }
144
+ /**
145
+ * The descriptor for a host without its own metric policy — judged with contracts'
146
+ * `DEFAULT_METRIC_POLICY`, which is a named, grep-able value, not a hidden fallback.
147
+ *
148
+ * A host that holds a configuration builds its own with `createSeDescriptor(policy)`
149
+ * and must then use only that one; two descriptors in one process would be the two
150
+ * thresholds CR-SM-233 removed.
151
+ */
152
+ export const SE_DESCRIPTOR = createSeDescriptor(DEFAULT_METRIC_POLICY);
package/dist/types.d.ts CHANGED
@@ -113,7 +113,7 @@ export interface FormatEOperation {
113
113
  description?: string;
114
114
  /**
115
115
  * BOK-CR-026: `unknown`, not `string` — object-valued bindings (`realRef`,
116
- * `testRef`) must survive a Format-E mutation as objects, or the element reads as
116
+ * `testRefs`) must survive a Format-E mutation as objects/arrays, or the element reads as
117
117
  * unbound. GraphService already handled them as unknown internally; this is the
118
118
  * public type catching up (a plain `@key value` line still yields a string).
119
119
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/graph-api-core",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,11 +21,11 @@
21
21
  "prepublishOnly": "npm run build && npm run test"
22
22
  },
23
23
  "dependencies": {
24
- "@sigloch/contracts": "^2.0.0",
24
+ "@sigloch/contracts": "^4.0.0",
25
25
  "zod": "^4.3.6"
26
26
  },
27
27
  "license": "MIT",
28
- "description": "Framework-agnostic graph engine core — ontology-typed nodes/traces, rule evaluation, views",
28
+ "description": "Framework-agnostic graph engine core \u2014 ontology-typed nodes/traces, rule evaluation, views",
29
29
  "author": "sigloch-consulting",
30
30
  "repository": {
31
31
  "type": "git",