@sigloch/graph-api-core 2.1.0 → 3.1.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
@@ -40,6 +40,41 @@ export interface AuditEntry {
40
40
  * at exactly the records it affected.
41
41
  */
42
42
  rulesetVersion?: string;
43
+ /**
44
+ * PROVENANCE (CR-GC-354) — the four fields that make a record answer the base question
45
+ * of an audit trail: WHO, with WHICH PROMPT, reached WHICH RESULT. The result half was
46
+ * always there; these are the other two.
47
+ *
48
+ * All four are DERIVED at the recording site, never self-declared by the consumer.
49
+ * `consumerId` is the counter-example that motivated this: it IS a self-declared field
50
+ * and 40% of the records carry its anonymous default. A prompt a model writes about
51
+ * itself is a paraphrase — it already contains the interpretation a later consumer
52
+ * would want to predict — so it is worthless as provenance.
53
+ *
54
+ * Session this record was written in — groups the records of ONE conversation.
55
+ * Assigned by the recording host, not accepted from the caller.
56
+ */
57
+ sessionId?: string;
58
+ /**
59
+ * The LLM that emitted the commands (e.g. the executor's configured model). The
60
+ * dimension rule calibration breaks down by ("R-01 dominated rejections: Haiku 26/29,
61
+ * Opus 17/18, devstral 10/23") and the one a client-side transcript cannot supply for
62
+ * a local or third-party model, because no transcript exists there.
63
+ */
64
+ model?: string;
65
+ /**
66
+ * The triggering prompt VERBATIM, truncated by the recording host's policy (the
67
+ * contract states that it is truncated, the host states where).
68
+ *
69
+ * Same absence asymmetry as `rulesPassed`: a missing field means NOT RECORDED, never
70
+ * "empty prompt". A consumer must not read absence as "no prompt was given".
71
+ */
72
+ intent?: string;
73
+ /**
74
+ * `true` when `intent` was truncated. Present only in that case — no silent cut, and
75
+ * no `false` noise on the overwhelming majority of records that fit.
76
+ */
77
+ intentTruncated?: boolean;
43
78
  }
44
79
  export interface AuditLog {
45
80
  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()
@@ -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, UC_RULES, FC_RULES, SC_RULES, CR_RULES, AO_RULES, FM_RULES, VIEW_RULES, AF_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
@@ -68,7 +68,6 @@ export function projectToOntologyGraph(graph) {
68
68
  const GATING_PREFIXES = ['R-', 'RD-', 'MT-'];
69
69
  const SE_RULE_DEFS = [
70
70
  ...V3_RULES,
71
- ...MT_RULES,
72
71
  ...UC_RULES,
73
72
  ...FC_RULES,
74
73
  ...SC_RULES,
@@ -78,23 +77,45 @@ const SE_RULE_DEFS = [
78
77
  ...VIEW_RULES,
79
78
  ...AF_RULES,
80
79
  ];
81
- const SE_RULES = SE_RULE_DEFS.map((def) => ({
82
- id: def.id,
83
- name: def.name,
84
- severity: def.severity,
85
- gating: GATING_PREFIXES.some((p) => def.id.startsWith(p)),
86
- evaluate: (graph) => def.evaluate(projectToOntologyGraph(graph)).map((v) => ({
87
- ruleId: v.rule_id,
88
- ruleName: def.name,
89
- severity: v.severity,
90
- message: v.message,
91
- elementId: v.element_id,
92
- // CR-GC-203 item 1: carry the fix-context through instead of discarding it,
93
- // so rules_get_violations hands the agent candidates + a hint, not just a message.
94
- fixHint: v.fix_hint,
95
- context: v.context,
96
- })),
97
- }));
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
+ }
98
119
  // `label` is the node-table identifier (kuzu table name) — must be a valid
99
120
  // identifier, so it's the ElementType key (SYS, UC, …), not the human
100
121
  // ELEMENT_DESCRIPTIONS text which would break DDL.
@@ -108,13 +129,24 @@ const edgeTypes = Object.fromEntries(TraceType.options.map((tt) => {
108
129
  * Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
109
130
  * to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
110
131
  */
111
- export const SE_DESCRIPTOR = {
112
- name: 'se',
113
- version: ONTOLOGY_VERSION,
114
- nodeTypes,
115
- edgeTypes,
116
- // CR-GC-247: TRACE_PATTERNS is the trace-legality SSOT; validPairs above is kept
117
- // only for Format-E arrow/menu enumeration. isValidTrace validates against these.
118
- patterns: TRACE_PATTERNS,
119
- rules: SE_RULES,
120
- };
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.1.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,7 @@
21
21
  "prepublishOnly": "npm run build && npm run test"
22
22
  },
23
23
  "dependencies": {
24
- "@sigloch/contracts": "^3.2.0",
24
+ "@sigloch/contracts": "^4.1.0",
25
25
  "zod": "^4.3.6"
26
26
  },
27
27
  "license": "MIT",