@sigloch/contracts 0.7.0 → 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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * RC conformance rules (CR-GC-253) — graph↔code resolution, the RESOLUTION
3
- * twins of the presence rules R-20 (codeRef) / R-19 (testRef).
3
+ * twins of the presence rules R-20 (FUNC realRef) / R-19 (testRef).
4
4
  *
5
5
  * Rules stay in THIS library (one rule base per onto set — no rule definitions
6
6
  * in executor codebases). They are pure functions over (graph, facts): all
@@ -8,7 +8,7 @@
8
8
  * from the repo), so this module stays I/O-free and browser-bundlable.
9
9
  *
10
10
  * CodeFacts semantics: `files` is keyed by repo-relative path and MUST contain
11
- * an entry for every file referenced by a codeRef/testRef the extractor saw.
11
+ * an entry for every file referenced by a realRef/testRef the extractor saw.
12
12
  * A MISSING key is treated like `exists:false` — an extractor gap must surface
13
13
  * loudly as a violation, never as a silent pass.
14
14
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * RC conformance rules (CR-GC-253) — graph↔code resolution, the RESOLUTION
3
- * twins of the presence rules R-20 (codeRef) / R-19 (testRef).
3
+ * twins of the presence rules R-20 (FUNC realRef) / R-19 (testRef).
4
4
  *
5
5
  * Rules stay in THIS library (one rule base per onto set — no rule definitions
6
6
  * in executor codebases). They are pure functions over (graph, facts): all
@@ -8,14 +8,14 @@
8
8
  * from the repo), so this module stays I/O-free and browser-bundlable.
9
9
  *
10
10
  * CodeFacts semantics: `files` is keyed by repo-relative path and MUST contain
11
- * an entry for every file referenced by a codeRef/testRef the extractor saw.
11
+ * an entry for every file referenced by a realRef/testRef the extractor saw.
12
12
  * A MISSING key is treated like `exists:false` — an extractor gap must surface
13
13
  * loudly as a violation, never as a silent pass.
14
14
  *
15
15
  * @sigloch/contracts/se — single source of truth for SE validation rules.
16
16
  */
17
17
  import { z } from 'zod/v4';
18
- import { CodeRefSchema, TestRefSchema, SchemaRefSchema } from './ontology.js';
18
+ import { RealRefSchema, TestRefSchema } from './ontology.js';
19
19
  /** Parser facts about one source file (extracted by the executor). */
20
20
  export const FileFactsSchema = z.object({
21
21
  /** File exists on disk (repo-relative path). */
@@ -50,10 +50,11 @@ export const CodeFactsSchema = z.object({
50
50
  importEdges: z.array(ImportEdgeSchema).optional(),
51
51
  });
52
52
  const missingFile = (facts, file) => facts.files[file]?.exists !== true;
53
- // RC-01: every valid codeRef must resolve — file on disk, symbol declared in it.
54
- // Presence is R-20's concern (incl. concept/external/decomposition-parent
53
+ // RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
54
+ // it. Presence is R-20's concern (incl. concept/external/decomposition-parent
55
55
  // exemptions, CR-GC-244); RC-01 only judges bindings that exist. `lang:'prompt'`
56
- // is realized by a skill file — file-exists is the whole binding.
56
+ // is realized by a skill file — file-exists is the whole binding. A realRef with
57
+ // no `symbol` (CR-228) is also file-only: resolution stops at file-exists.
57
58
  function codeRefMustResolve(graph, facts) {
58
59
  const violations = [];
59
60
  for (const el of graph.elements) {
@@ -61,7 +62,7 @@ function codeRefMustResolve(graph, facts) {
61
62
  continue;
62
63
  if (el.attributes?.concept === true || el.attributes?.external === true)
63
64
  continue;
64
- const parsed = CodeRefSchema.safeParse(el.attributes?.codeRef);
65
+ const parsed = RealRefSchema.safeParse(el.attributes?.realRef);
65
66
  if (!parsed.success)
66
67
  continue; // no/invalid binding → R-20 territory
67
68
  const ref = parsed.data;
@@ -70,20 +71,20 @@ function codeRefMustResolve(graph, facts) {
70
71
  rule_id: 'RC-01',
71
72
  severity: 'error',
72
73
  element_id: el.id,
73
- message: `${el.id} codeRef.file '${ref.file}' does not exist on disk`,
74
+ message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
74
75
  fix_hint: 'Re-realize the FUNC (graph_realize) against the current source tree, or fix the moved/renamed file path',
75
76
  context: { element_type: el.type, element_name: el.name },
76
77
  });
77
78
  continue;
78
79
  }
79
- if (ref.lang === 'prompt')
80
- continue; // skill file existing IS the binding
80
+ if (ref.lang === 'prompt' || ref.symbol === undefined)
81
+ continue; // skill/file-only binding IS the binding
81
82
  if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
82
83
  violations.push({
83
84
  rule_id: 'RC-01',
84
85
  severity: 'error',
85
86
  element_id: el.id,
86
- message: `${el.id} codeRef.symbol '${ref.symbol}' is not declared in '${ref.file}'`,
87
+ message: `${el.id} realRef.symbol '${ref.symbol}' is not declared in '${ref.file}'`,
87
88
  fix_hint: 'The symbol was renamed or removed — re-realize the FUNC against the current code',
88
89
  context: { element_type: el.type, element_name: el.name },
89
90
  });
@@ -131,11 +132,11 @@ function testRefMustResolve(graph, facts) {
131
132
  }
132
133
  return violations;
133
134
  }
134
- // RC-03: every valid SCHEMA schemaRef must resolve — file on disk, symbol declared
135
- // in it (CR-211). Presence (a SCHEMA with NO schemaRef) is the concern of the
136
- // R-26 presence rule, not RC-03; concept/external SCHEMAs are exempt there and
137
- // here. Severity error, like RC-01: a bound-but-broken schema IS a defect. Fires
138
- // until schemaRefs exist (it only judges SCHEMAs that carry one).
135
+ // RC-03: every valid SCHEMA realRef must resolve — file on disk, symbol declared
136
+ // in it (CR-211, unified CR-228). Presence (a SCHEMA with NO realRef) is the
137
+ // concern of the R-26 presence rule, not RC-03; concept/external SCHEMAs are exempt
138
+ // there and here. Severity error, like RC-01: a bound-but-broken schema IS a defect.
139
+ // A symbol-less realRef stops at file-exists. Fires until realRefs exist.
139
140
  function schemaRefMustResolve(graph, facts) {
140
141
  const violations = [];
141
142
  for (const el of graph.elements) {
@@ -143,7 +144,7 @@ function schemaRefMustResolve(graph, facts) {
143
144
  continue;
144
145
  if (el.attributes?.concept === true || el.attributes?.external === true)
145
146
  continue;
146
- const parsed = SchemaRefSchema.safeParse(el.attributes?.schemaRef);
147
+ const parsed = RealRefSchema.safeParse(el.attributes?.realRef);
147
148
  if (!parsed.success)
148
149
  continue; // no/invalid binding → R-26 territory
149
150
  const ref = parsed.data;
@@ -152,18 +153,20 @@ function schemaRefMustResolve(graph, facts) {
152
153
  rule_id: 'RC-03',
153
154
  severity: 'error',
154
155
  element_id: el.id,
155
- message: `${el.id} schemaRef.file '${ref.file}' does not exist on disk`,
156
+ message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
156
157
  fix_hint: 'Re-bind the SCHEMA (graph_realize) to the current source tree, or fix the moved/renamed file path',
157
158
  context: { element_type: el.type, element_name: el.name },
158
159
  });
159
160
  continue;
160
161
  }
162
+ if (ref.symbol === undefined)
163
+ continue; // file-only binding
161
164
  if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
162
165
  violations.push({
163
166
  rule_id: 'RC-03',
164
167
  severity: 'error',
165
168
  element_id: el.id,
166
- message: `${el.id} schemaRef.symbol '${ref.symbol}' is not a declared export in '${ref.file}'`,
169
+ message: `${el.id} realRef.symbol '${ref.symbol}' is not a declared export in '${ref.file}'`,
167
170
  fix_hint: 'The schema export was renamed or removed — re-bind the SCHEMA against the current code',
168
171
  context: { element_type: el.type, element_name: el.name },
169
172
  });
@@ -174,11 +177,11 @@ function schemaRefMustResolve(graph, facts) {
174
177
  // RC-04: a bound SCHEMA that the graph says is realized at an interface must
175
178
  // actually be parsed there (CR-211). The graph gives the check LOCATIONS: FUNCs
176
179
  // io-connected to a FLOW whose data format IS this SCHEMA (FUNC ─io→ FLOW
177
- // ─relation→ SCHEMA) and that carry a resolvable codeRef. If ≥1 such FUNC exists
178
- // but NONE of their codeRef files import AND parse (`.parse`/`.safeParse`) the
180
+ // ─relation→ SCHEMA) and that carry a resolvable realRef. If ≥1 such FUNC exists
181
+ // but NONE of their realRef files import AND parse (`.parse`/`.safeParse`) the
179
182
  // schema symbol, the modelled validation is missing. Severity warn (the parse may
180
183
  // legitimately sit in a framework layer, not the FUNC's own file). Skips when the
181
- // SCHEMA has no schemaRef, or no io-connected realized FUNC to check against.
184
+ // SCHEMA has no realRef (or a symbol-less one), or no io-connected realized FUNC.
182
185
  function schemaRefMustBeUsed(graph, facts) {
183
186
  const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
184
187
  const violations = [];
@@ -187,8 +190,8 @@ function schemaRefMustBeUsed(graph, facts) {
187
190
  continue;
188
191
  if (el.attributes?.concept === true || el.attributes?.external === true)
189
192
  continue;
190
- const parsed = SchemaRefSchema.safeParse(el.attributes?.schemaRef);
191
- if (!parsed.success)
193
+ const parsed = RealRefSchema.safeParse(el.attributes?.realRef);
194
+ if (!parsed.success || parsed.data.symbol === undefined)
192
195
  continue;
193
196
  const ref = parsed.data;
194
197
  // The FLOWs whose data format is this SCHEMA (FLOW ─relation→ SCHEMA).
@@ -203,11 +206,11 @@ function schemaRefMustBeUsed(graph, facts) {
203
206
  ((flowIds.has(t.target) && typeOf.get(t.source) === 'FUNC') ||
204
207
  (flowIds.has(t.source) && typeOf.get(t.target) === 'FUNC')))
205
208
  .map(t => (flowIds.has(t.target) ? t.source : t.target)));
206
- // Realized FUNCs among them: a resolvable codeRef whose file exists.
209
+ // Realized FUNCs among them: a resolvable realRef whose file exists.
207
210
  const realizedFiles = [];
208
211
  for (const fnId of funcIds) {
209
212
  const fn = graph.elements.find(e => e.id === fnId);
210
- const cr = CodeRefSchema.safeParse(fn?.attributes?.codeRef);
213
+ const cr = RealRefSchema.safeParse(fn?.attributes?.realRef);
211
214
  if (cr.success && !missingFile(facts, cr.data.file))
212
215
  realizedFiles.push(cr.data.file);
213
216
  }
@@ -255,12 +258,12 @@ function importDriftConformance(graph, facts) {
255
258
  const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
256
259
  if (modIds.length === 0)
257
260
  return [];
258
- // 1. file → MOD. Direct codeRef bindings first.
261
+ // 1. file → MOD. Direct realRef bindings first.
259
262
  const fileToMod = new Map();
260
263
  for (const el of graph.elements) {
261
264
  if (el.type !== 'FUNC')
262
265
  continue;
263
- const cr = CodeRefSchema.safeParse(el.attributes?.codeRef);
266
+ const cr = RealRefSchema.safeParse(el.attributes?.realRef);
264
267
  if (!cr.success)
265
268
  continue;
266
269
  const modTrace = graph.traces.find(t => t.source === el.id && t.type === 'allocate' && typeOf.get(t.target) === 'MOD');
@@ -352,10 +355,10 @@ function importDriftConformance(graph, facts) {
352
355
  }
353
356
  /** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
354
357
  export const CODE_CONFORMANCE_RULES = [
355
- { id: 'RC-01', name: 'codeRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
358
+ { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
356
359
  { id: 'RC-02', name: 'testRef resolves to a runnable test', severity: 'error', evaluate: testRefMustResolve },
357
- { id: 'RC-03', name: 'schemaRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
358
- { id: 'RC-04', name: 'schemaRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
360
+ { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
361
+ { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
359
362
  { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', evaluate: importDriftConformance },
360
363
  ];
361
364
  /** Run all RC rules against a graph + extracted code facts. */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ElementUid — the family's canonical element id form (CR-SM-217).
3
+ *
4
+ * <TYPE>-<slug> ACTOR-claude-code · REQ-safety · MOD-harness
5
+ *
6
+ * Decided on 2026-07-27 over the previous `Name.TypeAbbr.Counter` schema, in this order:
7
+ *
8
+ * 1. **Token-neutral.** Measured on the graphcode SSOT graph (1871 uid occurrences,
9
+ * cl100k_base): `ACTOR-claude-code.ACTOR` 5.28 tok/uid vs `claudecode.ACTOR.001`
10
+ * 5.30 — 0.3 % apart. The spelling costs nothing, so it is decided on correctness.
11
+ * 2. **It is what the real graphs use.** 626 of 1145 family elements are already
12
+ * `TYPE-slug`; the schema's declared canon had 89.
13
+ * 3. **It keeps a free type cross-check.** CR-SM-216 moved the type into the `### <TYPE>`
14
+ * section, which removes the "type appears twice" redundancy that used to catch a
15
+ * mis-sectioned node. A `TYPE-slug` uid carries the type in its prefix without paying
16
+ * for it (`REQ-safety` = 3 tokens, `REQ-safety.REQ` = 5), so the check survives.
17
+ *
18
+ * `parseElementUid` exists **only** for that cross-check. It is never a type source —
19
+ * the type comes from the graph or from the Format-E section (CR-SM-216 contract).
20
+ *
21
+ * **Not every id is an ElementUid, and that is allowed.** Ids that mirror an external
22
+ * identifier live in their own namespace and are simply unchecked, never rejected:
23
+ * CR elements carry their change-request number (`CR-GC-100`, 88 in graphcode, 123 in
24
+ * graph-view-edit) and graphify's pre-gate candidates are `cand_<hex>`. The canon binds
25
+ * *new* ids minted through `toElementUid`; existing ids stay until their repo touches
26
+ * them.
27
+ */
28
+ import { z } from 'zod/v4';
29
+ import { ElementType } from './ontology.js';
30
+ /** Longest slug we accept; keeps ids readable and index-friendly. */
31
+ export declare const MAX_SLUG_LENGTH = 60;
32
+ export declare const ElementUid: z.ZodString;
33
+ export type ElementUid = z.infer<typeof ElementUid>;
34
+ /**
35
+ * Build a canonical uid. Pure and deterministic — the same (type, name) always yields
36
+ * the same uid.
37
+ *
38
+ * Collisions are the **caller's** problem: only the caller knows the namespace, so
39
+ * disambiguation (`-2`, `-3`) belongs there. Documented rather than hidden behind a
40
+ * counter that would make this function stateful.
41
+ */
42
+ export declare function toElementUid(type: ElementType, name: string): string;
43
+ /**
44
+ * Split a uid into its parts. Throws when the prefix is not an ElementType.
45
+ *
46
+ * ⚠️ For the prefix↔section cross-check only — **never** as a type source. Deriving a
47
+ * type from an id's spelling is what made aimpro CR-230 drop an entire graph silently.
48
+ */
49
+ export declare function parseElementUid(uid: string): {
50
+ type: ElementType;
51
+ slug: string;
52
+ };
53
+ /** Non-throwing variant: `undefined` for ids of any other convention (legacy, graphify candidates). */
54
+ export declare function tryParseElementUid(uid: string): {
55
+ type: ElementType;
56
+ slug: string;
57
+ } | undefined;
58
+ /** True when `uid` is a canonical ElementUid. */
59
+ export declare function isElementUid(uid: string): boolean;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ElementUid — the family's canonical element id form (CR-SM-217).
3
+ *
4
+ * <TYPE>-<slug> ACTOR-claude-code · REQ-safety · MOD-harness
5
+ *
6
+ * Decided on 2026-07-27 over the previous `Name.TypeAbbr.Counter` schema, in this order:
7
+ *
8
+ * 1. **Token-neutral.** Measured on the graphcode SSOT graph (1871 uid occurrences,
9
+ * cl100k_base): `ACTOR-claude-code.ACTOR` 5.28 tok/uid vs `claudecode.ACTOR.001`
10
+ * 5.30 — 0.3 % apart. The spelling costs nothing, so it is decided on correctness.
11
+ * 2. **It is what the real graphs use.** 626 of 1145 family elements are already
12
+ * `TYPE-slug`; the schema's declared canon had 89.
13
+ * 3. **It keeps a free type cross-check.** CR-SM-216 moved the type into the `### <TYPE>`
14
+ * section, which removes the "type appears twice" redundancy that used to catch a
15
+ * mis-sectioned node. A `TYPE-slug` uid carries the type in its prefix without paying
16
+ * for it (`REQ-safety` = 3 tokens, `REQ-safety.REQ` = 5), so the check survives.
17
+ *
18
+ * `parseElementUid` exists **only** for that cross-check. It is never a type source —
19
+ * the type comes from the graph or from the Format-E section (CR-SM-216 contract).
20
+ *
21
+ * **Not every id is an ElementUid, and that is allowed.** Ids that mirror an external
22
+ * identifier live in their own namespace and are simply unchecked, never rejected:
23
+ * CR elements carry their change-request number (`CR-GC-100`, 88 in graphcode, 123 in
24
+ * graph-view-edit) and graphify's pre-gate candidates are `cand_<hex>`. The canon binds
25
+ * *new* ids minted through `toElementUid`; existing ids stay until their repo touches
26
+ * them.
27
+ */
28
+ import { z } from 'zod/v4';
29
+ import { ElementType } from './ontology.js';
30
+ /** Longest slug we accept; keeps ids readable and index-friendly. */
31
+ export const MAX_SLUG_LENGTH = 60;
32
+ /** Built from the live ElementType enum — there is no second type catalogue. */
33
+ const UID_RE = new RegExp(`^(${ElementType.options.join('|')})-([a-z0-9]+(?:-[a-z0-9]+)*)$`);
34
+ export const ElementUid = z.string()
35
+ .regex(UID_RE, 'ElementUid must match <TYPE>-<kebab-slug>')
36
+ .refine(uid => (uid.split('-').slice(1).join('-')).length <= MAX_SLUG_LENGTH, `slug must be at most ${MAX_SLUG_LENGTH} characters`);
37
+ /**
38
+ * Build a canonical uid. Pure and deterministic — the same (type, name) always yields
39
+ * the same uid.
40
+ *
41
+ * Collisions are the **caller's** problem: only the caller knows the namespace, so
42
+ * disambiguation (`-2`, `-3`) belongs there. Documented rather than hidden behind a
43
+ * counter that would make this function stateful.
44
+ */
45
+ export function toElementUid(type, name) {
46
+ const slug = name
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-+|-+$/g, '')
50
+ .slice(0, MAX_SLUG_LENGTH)
51
+ .replace(/-+$/, '');
52
+ return `${type}-${slug}`;
53
+ }
54
+ /**
55
+ * Split a uid into its parts. Throws when the prefix is not an ElementType.
56
+ *
57
+ * ⚠️ For the prefix↔section cross-check only — **never** as a type source. Deriving a
58
+ * type from an id's spelling is what made aimpro CR-230 drop an entire graph silently.
59
+ */
60
+ export function parseElementUid(uid) {
61
+ const m = UID_RE.exec(uid);
62
+ if (!m)
63
+ throw new Error(`Not an ElementUid: "${uid}" — expected <TYPE>-<kebab-slug>`);
64
+ return { type: m[1], slug: m[2] };
65
+ }
66
+ /** Non-throwing variant: `undefined` for ids of any other convention (legacy, graphify candidates). */
67
+ export function tryParseElementUid(uid) {
68
+ const m = UID_RE.exec(uid);
69
+ return m ? { type: m[1], slug: m[2] } : undefined;
70
+ }
71
+ /** True when `uid` is a canonical ElementUid. */
72
+ export function isElementUid(uid) {
73
+ return ElementUid.safeParse(uid).success;
74
+ }
@@ -23,7 +23,9 @@ export const ALL_RULE_DEFS = [
23
23
  ...FM_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
24
24
  ...VIEW_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
25
25
  ];
26
- const SE_PREFIXES = ['R-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-'];
26
+ // CR-SM-221: 'RD-' was missing 'RD-01'.startsWith('R-') is false, so the
27
+ // decomposition rules ran in `default` only and never in the `se` profile.
28
+ const SE_PREFIXES = ['R-', 'RD-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-'];
27
29
  const CODING_PREFIXES = ['BQ-', 'ND-'];
28
30
  export function getRuleDefsForProfile(profile) {
29
31
  if (profile === 'se')
@@ -1,17 +1,33 @@
1
1
  /**
2
2
  * Format E Parser — Graph mutations from compact text format.
3
3
  * SE-relevant subset: nodes (+/-/~) and edges (+/-), no chat-canvas or views.
4
- * ~150 LOC, no external dependencies beyond @sigloch/contracts/se.
4
+ *
5
+ * CR-SM-216 (Format-E v2): the element type comes from the `### <TYPE>` section a node
6
+ * is declared under, never from the spelling of its id. The old id-derived typing made
7
+ * every consumer with a different id convention fail *silently* — aimpro CR-230 lost a
8
+ * whole graph that way (`TYPE-slug` ids rejected, result empty, no error).
5
9
  *
6
10
  * @sigloch/contracts/se
7
11
  */
12
+ import { ElementType } from './ontology.js';
8
13
  import type { TraceType, OntologyGraph } from './ontology.js';
9
14
  export interface FormatEOperation {
10
15
  type: 'add_node' | 'remove_node' | 'update_node' | 'add_edge' | 'remove_edge' | 'strict_add_node' | 'strict_add_edge';
11
16
  semanticId: string;
17
+ /**
18
+ * CR-SM-216: the element type, taken from the node's `### <TYPE>` section. Set on
19
+ * every node-creating operation — consumers must read it instead of re-deriving a
20
+ * type from the id.
21
+ */
22
+ elementType?: ElementType;
12
23
  description?: string;
13
- /** CR-147: Parsed @key value attributes from lines below the node entry. */
14
- attributes?: Record<string, string>;
24
+ /**
25
+ * CR-147: Parsed @key value attributes from lines below the node entry.
26
+ * Values are strings, EXCEPT JSON object/array literals which are hydrated
27
+ * (BOK-CR-026) — object-valued bindings like `realRef`/`testRef` must reach
28
+ * `attributes` as objects or R-26/R-19 reject them as invalid.
29
+ */
30
+ attributes?: Record<string, unknown>;
15
31
  sourceId?: string;
16
32
  targetId?: string;
17
33
  traceType?: TraceType;
@@ -22,7 +38,23 @@ export interface FormatEDiff {
22
38
  }
23
39
  /** Extract a ```format-e block from LLM output. Returns null if not found. */
24
40
  export declare function extractFormatE(llmOutput: string): string | null;
41
+ export interface ParseFormatEOptions {
42
+ /**
43
+ * CR-SM-216: resolve the type of a uid that this text does not declare. A mutation
44
+ * diff adding edges between existing nodes carries no `## Nodes` block, so the
45
+ * caller binds this to its store. Without it such a diff is an error, never a
46
+ * silent skip.
47
+ */
48
+ resolveType?: (uid: string) => ElementType | undefined;
49
+ }
25
50
  /** Parse a Format E text block into validated operations. */
26
- export declare function parseFormatE(input: string): FormatEDiff;
27
- /** Serialize an OntologyGraph to compact Format E text. */
51
+ export declare function parseFormatE(input: string, options?: ParseFormatEOptions): FormatEDiff;
52
+ /**
53
+ * Serialize an OntologyGraph to compact Format E text.
54
+ *
55
+ * CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
56
+ * per section instead of once per id. Measured on the graphcode SSOT graph (369
57
+ * elements), 12 section headers cost ~48 tokens where a per-node type attribute would
58
+ * have cost ~1476.
59
+ */
28
60
  export declare function serializeToFormatE(graph: OntologyGraph): string;