@kubun/protocol 0.10.0 → 0.11.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 +1,320 @@
1
- export const booleanModel={type:"object",properties:{type:{type:"string",const:"boolean"},default:{type:"boolean"}},required:["type"],additionalProperties:!1};export const integerModel={type:"object",properties:{type:{type:"string",const:"integer"},default:{type:"integer"},minimum:{type:"integer"},maximum:{type:"integer"}},required:["type"],additionalProperties:!1};export const numberModel={type:"object",properties:{type:{type:"string",const:"number"},default:{type:"number"},min:{type:"number"},max:{type:"number"}},required:["type"],additionalProperties:!1};export const stringConstModel={type:"object",properties:{type:{type:"string",const:"string"},title:{type:"string"},const:{type:"string"},default:{type:"string"}},required:["type","const"],additionalProperties:!1};let e={type:"string",pattern:"^[A-Z]+[A-Z_]+[A-Z]$"};export const stringEnumModel={type:"object",properties:{type:{type:"string",const:"string"},title:{type:"string"},enum:{type:"array",items:e},default:e},required:["type","title","enum"],additionalProperties:!1};export const supportedStringFormat={type:"string",enum:["date","date-time","duration","email","time","uri"]};export const stringFormatModel={type:"object",properties:{type:{type:"string",const:"string"},format:supportedStringFormat,title:{type:"string"},default:{type:"string"},minLength:{type:"integer"},maxLength:{type:"integer"}},required:["type","format"],additionalProperties:!1};export const stringCustomModel={type:"object",properties:{type:{type:"string",const:"string"},title:{type:"string"},pattern:{type:"string"},default:{type:"string"},minLength:{type:"integer"},maxLength:{type:"integer"}},required:["type"],additionalProperties:!1};export const stringModel={anyOf:[stringConstModel,stringCustomModel,stringEnumModel,stringFormatModel]};export const scalarModel={anyOf:[booleanModel,integerModel,numberModel,stringModel]};export const refModel={type:"object",properties:{$ref:{type:"string"}},required:["$ref"],additionalProperties:!1};export const nameString={type:"string",pattern:"^[A-Z]+[A-Za-z0-9]+$"};export const arrayModel={type:"object",properties:{type:{type:"string",const:"array"},title:nameString,items:refModel,uniqueItems:{type:"boolean"},minItems:{type:"integer"},maxItems:{type:"integer"}},required:["type","title","items"],additionalProperties:!1};export const baseObjectModel={type:"object",properties:{type:{type:"string",const:"object"},properties:{type:"object",patternProperties:{"^[a-z]+[A-Za-z0-9]+$":refModel},additionalProperties:!1},required:{type:"array",items:{type:"string"}},additionalProperties:{type:"boolean"}},required:["type","properties","required","additionalProperties"],additionalProperties:!1};export const objectModel={type:"object",properties:{...baseObjectModel.properties,title:nameString},required:[...baseObjectModel.required,"title"],additionalProperties:!1};export const jsonObjectModel={type:"object",properties:{type:{type:"string",const:"object"},title:{type:"string",const:"JSONObject"},additionalProperties:{type:"boolean",const:!0}},required:["type","title","additionalProperties"],additionalProperties:!1};export const shapeModel={anyOf:[arrayModel,objectModel,jsonObjectModel]};export const typeModel={anyOf:[scalarModel,shapeModel]};export const referencesRecordModel={type:"object",additionalProperties:typeModel};
1
+ export const booleanModel = {
2
+ type: 'object',
3
+ properties: {
4
+ type: {
5
+ type: 'string',
6
+ const: 'boolean'
7
+ },
8
+ default: {
9
+ type: 'boolean'
10
+ }
11
+ },
12
+ required: [
13
+ 'type'
14
+ ],
15
+ additionalProperties: false
16
+ };
17
+ export const integerModel = {
18
+ type: 'object',
19
+ properties: {
20
+ type: {
21
+ type: 'string',
22
+ const: 'integer'
23
+ },
24
+ default: {
25
+ type: 'integer'
26
+ },
27
+ minimum: {
28
+ type: 'integer'
29
+ },
30
+ maximum: {
31
+ type: 'integer'
32
+ }
33
+ },
34
+ required: [
35
+ 'type'
36
+ ],
37
+ additionalProperties: false
38
+ };
39
+ export const numberModel = {
40
+ type: 'object',
41
+ properties: {
42
+ type: {
43
+ type: 'string',
44
+ const: 'number'
45
+ },
46
+ default: {
47
+ type: 'number'
48
+ },
49
+ min: {
50
+ type: 'number'
51
+ },
52
+ max: {
53
+ type: 'number'
54
+ }
55
+ },
56
+ required: [
57
+ 'type'
58
+ ],
59
+ additionalProperties: false
60
+ };
61
+ export const stringConstModel = {
62
+ type: 'object',
63
+ properties: {
64
+ type: {
65
+ type: 'string',
66
+ const: 'string'
67
+ },
68
+ title: {
69
+ type: 'string'
70
+ },
71
+ const: {
72
+ type: 'string'
73
+ },
74
+ default: {
75
+ type: 'string'
76
+ }
77
+ },
78
+ required: [
79
+ 'type',
80
+ 'const'
81
+ ],
82
+ additionalProperties: false
83
+ };
84
+ // Uppercase characters and "_" to match GraphQL enum
85
+ const enumString = {
86
+ type: 'string',
87
+ pattern: '^[A-Z]+[A-Z_]+[A-Z]$'
88
+ };
89
+ export const stringEnumModel = {
90
+ type: 'object',
91
+ properties: {
92
+ type: {
93
+ type: 'string',
94
+ const: 'string'
95
+ },
96
+ title: {
97
+ type: 'string'
98
+ },
99
+ enum: {
100
+ type: 'array',
101
+ items: enumString
102
+ },
103
+ default: enumString
104
+ },
105
+ required: [
106
+ 'type',
107
+ 'title',
108
+ 'enum'
109
+ ],
110
+ additionalProperties: false
111
+ };
112
+ export const supportedStringFormat = {
113
+ type: 'string',
114
+ enum: [
115
+ 'date',
116
+ 'date-time',
117
+ 'duration',
118
+ 'email',
119
+ 'time',
120
+ 'uri'
121
+ ]
122
+ };
123
+ export const stringFormatModel = {
124
+ type: 'object',
125
+ properties: {
126
+ type: {
127
+ type: 'string',
128
+ const: 'string'
129
+ },
130
+ format: supportedStringFormat,
131
+ title: {
132
+ type: 'string'
133
+ },
134
+ default: {
135
+ type: 'string'
136
+ },
137
+ minLength: {
138
+ type: 'integer'
139
+ },
140
+ maxLength: {
141
+ type: 'integer'
142
+ }
143
+ },
144
+ required: [
145
+ 'type',
146
+ 'format'
147
+ ],
148
+ additionalProperties: false
149
+ };
150
+ export const stringCustomModel = {
151
+ type: 'object',
152
+ properties: {
153
+ type: {
154
+ type: 'string',
155
+ const: 'string'
156
+ },
157
+ title: {
158
+ type: 'string'
159
+ },
160
+ pattern: {
161
+ type: 'string'
162
+ },
163
+ default: {
164
+ type: 'string'
165
+ },
166
+ minLength: {
167
+ type: 'integer'
168
+ },
169
+ maxLength: {
170
+ type: 'integer'
171
+ }
172
+ },
173
+ required: [
174
+ 'type'
175
+ ],
176
+ additionalProperties: false
177
+ };
178
+ export const stringModel = {
179
+ anyOf: [
180
+ stringConstModel,
181
+ stringCustomModel,
182
+ stringEnumModel,
183
+ stringFormatModel
184
+ ]
185
+ };
186
+ export const scalarModel = {
187
+ anyOf: [
188
+ booleanModel,
189
+ integerModel,
190
+ numberModel,
191
+ stringModel
192
+ ]
193
+ };
194
+ export const refModel = {
195
+ type: 'object',
196
+ properties: {
197
+ $ref: {
198
+ type: 'string'
199
+ }
200
+ },
201
+ required: [
202
+ '$ref'
203
+ ],
204
+ additionalProperties: false
205
+ };
206
+ // Valid GraphQL name
207
+ export const nameString = {
208
+ type: 'string',
209
+ pattern: '^[A-Z]+[A-Za-z0-9]+$'
210
+ };
211
+ export const arrayModel = {
212
+ type: 'object',
213
+ properties: {
214
+ type: {
215
+ type: 'string',
216
+ const: 'array'
217
+ },
218
+ title: nameString,
219
+ items: refModel,
220
+ uniqueItems: {
221
+ type: 'boolean'
222
+ },
223
+ minItems: {
224
+ type: 'integer'
225
+ },
226
+ maxItems: {
227
+ type: 'integer'
228
+ }
229
+ },
230
+ required: [
231
+ 'type',
232
+ 'title',
233
+ 'items'
234
+ ],
235
+ additionalProperties: false
236
+ };
237
+ export const baseObjectModel = {
238
+ type: 'object',
239
+ properties: {
240
+ type: {
241
+ type: 'string',
242
+ const: 'object'
243
+ },
244
+ properties: {
245
+ type: 'object',
246
+ patternProperties: {
247
+ '^[a-z]+[A-Za-z0-9]+$': refModel
248
+ },
249
+ additionalProperties: false
250
+ },
251
+ required: {
252
+ type: 'array',
253
+ items: {
254
+ type: 'string'
255
+ }
256
+ },
257
+ additionalProperties: {
258
+ type: 'boolean'
259
+ }
260
+ },
261
+ required: [
262
+ 'type',
263
+ 'properties',
264
+ 'required',
265
+ 'additionalProperties'
266
+ ],
267
+ additionalProperties: false
268
+ };
269
+ export const objectModel = {
270
+ type: 'object',
271
+ properties: {
272
+ ...baseObjectModel.properties,
273
+ title: nameString
274
+ },
275
+ required: [
276
+ ...baseObjectModel.required,
277
+ 'title'
278
+ ],
279
+ additionalProperties: false
280
+ };
281
+ export const jsonObjectModel = {
282
+ type: 'object',
283
+ properties: {
284
+ type: {
285
+ type: 'string',
286
+ const: 'object'
287
+ },
288
+ title: {
289
+ type: 'string',
290
+ const: 'JSONObject'
291
+ },
292
+ additionalProperties: {
293
+ type: 'boolean',
294
+ const: true
295
+ }
296
+ },
297
+ required: [
298
+ 'type',
299
+ 'title',
300
+ 'additionalProperties'
301
+ ],
302
+ additionalProperties: false
303
+ };
304
+ export const shapeModel = {
305
+ anyOf: [
306
+ arrayModel,
307
+ objectModel,
308
+ jsonObjectModel
309
+ ]
310
+ };
311
+ export const typeModel = {
312
+ anyOf: [
313
+ scalarModel,
314
+ shapeModel
315
+ ]
316
+ };
317
+ export const referencesRecordModel = {
318
+ type: 'object',
319
+ additionalProperties: typeModel
320
+ };
@@ -1 +1,324 @@
1
- import{DocumentModelID as e,digestJSON as t}from"@kubun/id";import{ClusterBuilder as l,clusterToRecord as n}from"./cluster.js";function r(e){if(!e.startsWith("#"))return null;let t=Number.parseInt(e.slice(1),10);return Number.isNaN(t)?null:t}export function extractFieldsMetaDeps(e){return e.map(e=>{let t=new Set;for(let l of Object.values(e.fieldsMeta??{}))if("document"===l.type&&null!=l.model){let e=r(l.model);null!=e&&t.add(e)}return t})}export function extractInterfaceDeps(e){return e.map(e=>{let t=new Set;for(let l of e.interfaces??[]){let e=r(l);null!=e&&t.add(e)}return t})}export function findStronglyConnectedComponents(e){let t=e.length,l=Array(t).fill(-1),n=Array(t).fill(-1),r=Array(t).fill(!1),o=[],s=[],f=0;for(let i=0;i<t;i++)-1===l[i]&&function i(a){for(let s of(l[a]=f,n[a]=f,f++,o.push(a),r[a]=!0,e[a]))!(s>=t)&&(-1===l[s]?(i(s),n[a]=Math.min(n[a],n[s])):r[s]&&(n[a]=Math.min(n[a],l[s])));if(n[a]===l[a]){let e=[],t=o.pop();for(;null!=t&&(r[t]=!1,e.push(t),t!==a);)t=o.pop();s.push(e)}}(i);return s.map(e=>e.sort((e,t)=>e-t)).sort((e,t)=>e[0]-t[0])}export function findClusters(e){return findStronglyConnectedComponents(extractFieldsMetaDeps(e))}export class GraphBuilder{#e=[];#t;constructor(e={}){this.#t=e.externalClusters??{}}add(e){return this.#e.push(e),this}addAll(e){return this.#e.push(...e),this}build(){let o=Object.keys(this.#t).length>0?this.#t:void 0,s=function(o,s){if(0===o.length)return[];let f=extractFieldsMetaDeps(o),i=extractInterfaceDeps(o),a=findStronglyConnectedComponents(f),u=function(e,t,l){let n=new Map;for(let t=0;t<e.length;t++)for(let l of e[t])n.set(l,t);let r=Array.from({length:e.length},()=>new Set);for(let o=0;o<e.length;o++)for(let s of e[o]){for(let e of t[s]){let t=n.get(e);null!=t&&t!==o&&r[o].add(t)}for(let e of l[s]){let t=n.get(e);null!=t&&t!==o&&r[o].add(t)}}let o=r.map(e=>e.size),s=o.map((e,t)=>0===e?t:-1).filter(e=>e>=0),f=[];for(;s.length>0;){let e=s.shift();if(null==e)break;f.push(e);for(let t=0;t<r.length;t++)r[t].has(e)&&(o[t]--,0===o[t]&&s.push(t))}return f}(a,i,f),d=new Map,p={...s??{}},m=[];for(let s of u){let f=a[s],u=function(e,t,l,n){let o=new Set(t),s=new Map;for(let e of t){let t=new Set;for(let n of l[e])o.has(n)&&t.add(n);s.set(e,t)}let f=new Map;for(let e of t)f.set(e,s.get(e)?.size??0);let i=[],a=t.filter(e=>(f.get(e)??0)===0).map(t=>({originalIndex:t,model:e[t]})).sort((e,t)=>e.model.name.localeCompare(t.model.name));for(;a.length>0;){let t=a.shift();if(null==t)break;for(let[l,n]of(i.push(t),s))if(n.has(t.originalIndex)){let t=(f.get(l)??1)-1;if(f.set(l,t),0===t){let t={originalIndex:l,model:e[l]},n=a.findIndex(e=>e.model.name.localeCompare(t.model.name)>0);-1===n?a.push(t):a.splice(n,0,t)}}}let u=new Map;for(let e=0;e<i.length;e++)u.set(i[e].originalIndex,e);return i.map(({model:e})=>{let t={...e};if(null!=t.interfaces&&(t.interfaces=t.interfaces.map(e=>{let t=r(e);if(null==t)return e;let l=n.get(t);if(null!=l)return l;let o=u.get(t);return null!=o?`#${o}`:e})),null!=t.fieldsMeta){let e={...t.fieldsMeta};for(let[t,l]of Object.entries(e))if("document"===l.type&&null!=l.model){let o=r(l.model);if(null==o)continue;let s=n.get(o);if(null!=s){e[t]={...l,model:s};continue}let f=u.get(o);null!=f&&(e[t]={...l,model:`#${f}`})}t.fieldsMeta=e}return t})}(o,f,i,d),c=new l({externalClusters:Object.keys(p).length>0?p:void 0});c.addAll(u);let h=c.build();m.push(h);let g=t(h.models);for(let t=0;t<f.length;t++){let l=f[t],n=o[l].name,r=h.models.findIndex(e=>e.name===n);if(-1!==r){let t=new e(g,r);d.set(l,t.toString())}}Object.assign(p,n(h))}return m}(this.#e,o),f={};for(let e of s)Object.assign(f,n(e));return f}}
1
+ import { DocumentModelID, digestJSON } from '@kubun/id';
2
+ import { ClusterBuilder, clusterToRecord } from './cluster.js';
3
+ /**
4
+ * Parse local index from a reference string like '#0', '#1', etc.
5
+ * Returns null if the reference is not a local index.
6
+ */ function parseLocalIndex(ref) {
7
+ if (!ref.startsWith('#')) return null;
8
+ const index = Number.parseInt(ref.slice(1), 10);
9
+ return Number.isNaN(index) ? null : index;
10
+ }
11
+ /**
12
+ * Extract fieldsMeta document reference dependencies for each model.
13
+ * Returns directed edges only (A→B does not imply B→A).
14
+ * Used for SCC grouping — only circular fieldsMeta forces co-location.
15
+ */ export function extractFieldsMetaDeps(models) {
16
+ return models.map((model)=>{
17
+ const deps = new Set();
18
+ for (const meta of Object.values(model.fieldsMeta ?? {})){
19
+ if (meta.type === 'document' && meta.model != null) {
20
+ const index = parseLocalIndex(meta.model);
21
+ if (index != null) deps.add(index);
22
+ }
23
+ }
24
+ return deps;
25
+ });
26
+ }
27
+ /**
28
+ * Extract interface implementation dependencies for each model.
29
+ * These form a DAG (no circular interfaces). Used for ordering
30
+ * within a cluster so ClusterBuilder can resolve interfaces eagerly.
31
+ */ export function extractInterfaceDeps(models) {
32
+ return models.map((model)=>{
33
+ const deps = new Set();
34
+ for (const iface of model.interfaces ?? []){
35
+ const index = parseLocalIndex(iface);
36
+ if (index != null) deps.add(index);
37
+ }
38
+ return deps;
39
+ });
40
+ }
41
+ /**
42
+ * Find strongly connected components using Tarjan's algorithm.
43
+ * Used on fieldsMeta directed graph — only circular references (A→B→A)
44
+ * force models into the same cluster. One-way refs don't group.
45
+ * Returns arrays of model indices, one per SCC.
46
+ */ export function findStronglyConnectedComponents(deps) {
47
+ const n = deps.length;
48
+ const indices = new Array(n).fill(-1);
49
+ const lowlinks = new Array(n).fill(-1);
50
+ const onStack = new Array(n).fill(false);
51
+ const stack = [];
52
+ const sccs = [];
53
+ let currentIndex = 0;
54
+ function strongConnect(node) {
55
+ indices[node] = currentIndex;
56
+ lowlinks[node] = currentIndex;
57
+ currentIndex++;
58
+ stack.push(node);
59
+ onStack[node] = true;
60
+ for (const dep of deps[node]){
61
+ if (dep >= n) continue;
62
+ if (indices[dep] === -1) {
63
+ strongConnect(dep);
64
+ lowlinks[node] = Math.min(lowlinks[node], lowlinks[dep]);
65
+ } else if (onStack[dep]) {
66
+ lowlinks[node] = Math.min(lowlinks[node], indices[dep]);
67
+ }
68
+ }
69
+ if (lowlinks[node] === indices[node]) {
70
+ const scc = [];
71
+ let member = stack.pop();
72
+ while(member != null){
73
+ onStack[member] = false;
74
+ scc.push(member);
75
+ if (member === node) break;
76
+ member = stack.pop();
77
+ }
78
+ sccs.push(scc);
79
+ }
80
+ }
81
+ for(let i = 0; i < n; i++){
82
+ if (indices[i] === -1) {
83
+ strongConnect(i);
84
+ }
85
+ }
86
+ // Sort each SCC internally and sort SCCs by smallest index
87
+ return sccs.map((scc)=>scc.sort((a, b)=>a - b)).sort((a, b)=>a[0] - b[0]);
88
+ }
89
+ /**
90
+ * Find cluster groups from model definitions.
91
+ * Groups are determined by strongly connected components in the
92
+ * fieldsMeta directed graph. Only circular fieldsMeta references
93
+ * force models into the same cluster.
94
+ */ export function findClusters(models) {
95
+ const fieldsMetaDeps = extractFieldsMetaDeps(models);
96
+ return findStronglyConnectedComponents(fieldsMetaDeps);
97
+ }
98
+ /**
99
+ * Sort models within a cluster group by interface deps (topological)
100
+ * with name as tiebreaker. Rewrite intra-cluster index refs to new
101
+ * positions. Cross-cluster refs are rewritten to global DocumentModelID
102
+ * strings using the provided mapping.
103
+ */ function prepareClusterModels(models, group, interfaceDeps, globalIDMap) {
104
+ const groupSet = new Set(group);
105
+ // Topological sort on interface deps within this group, name tiebreaker
106
+ const localInterfaceDeps = new Map();
107
+ for (const origIdx of group){
108
+ const filtered = new Set();
109
+ for (const dep of interfaceDeps[origIdx]){
110
+ if (groupSet.has(dep)) filtered.add(dep);
111
+ }
112
+ localInterfaceDeps.set(origIdx, filtered);
113
+ }
114
+ const remaining = new Map();
115
+ for (const origIdx of group){
116
+ remaining.set(origIdx, localInterfaceDeps.get(origIdx)?.size ?? 0);
117
+ }
118
+ const sorted = [];
119
+ const ready = group.filter((idx)=>(remaining.get(idx) ?? 0) === 0).map((idx)=>({
120
+ originalIndex: idx,
121
+ model: models[idx]
122
+ })).sort((a, b)=>a.model.name.localeCompare(b.model.name));
123
+ while(ready.length > 0){
124
+ const next = ready.shift();
125
+ if (next == null) break;
126
+ sorted.push(next);
127
+ for (const [origIdx, deps] of localInterfaceDeps){
128
+ if (deps.has(next.originalIndex)) {
129
+ const count = (remaining.get(origIdx) ?? 1) - 1;
130
+ remaining.set(origIdx, count);
131
+ if (count === 0) {
132
+ const entry = {
133
+ originalIndex: origIdx,
134
+ model: models[origIdx]
135
+ };
136
+ const insertIdx = ready.findIndex((r)=>r.model.name.localeCompare(entry.model.name) > 0);
137
+ if (insertIdx === -1) {
138
+ ready.push(entry);
139
+ } else {
140
+ ready.splice(insertIdx, 0, entry);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ // Build mapping: original index → new cluster-internal index
147
+ const internalIndexMap = new Map();
148
+ for(let newIndex = 0; newIndex < sorted.length; newIndex++){
149
+ internalIndexMap.set(sorted[newIndex].originalIndex, newIndex);
150
+ }
151
+ // Rewrite references
152
+ return sorted.map(({ model })=>{
153
+ const rewritten = {
154
+ ...model
155
+ };
156
+ // Rewrite interface references
157
+ if (rewritten.interfaces != null) {
158
+ rewritten.interfaces = rewritten.interfaces.map((ref)=>{
159
+ const origIndex = parseLocalIndex(ref);
160
+ if (origIndex == null) return ref;
161
+ // Cross-cluster: use global ID
162
+ const globalID = globalIDMap.get(origIndex);
163
+ if (globalID != null) return globalID;
164
+ // Intra-cluster: use new position
165
+ const newIndex = internalIndexMap.get(origIndex);
166
+ if (newIndex != null) return `#${newIndex}`;
167
+ return ref;
168
+ });
169
+ }
170
+ // Rewrite fieldsMeta document references
171
+ if (rewritten.fieldsMeta != null) {
172
+ const newFieldsMeta = {
173
+ ...rewritten.fieldsMeta
174
+ };
175
+ for (const [key, meta] of Object.entries(newFieldsMeta)){
176
+ if (meta.type === 'document' && meta.model != null) {
177
+ const origIndex = parseLocalIndex(meta.model);
178
+ if (origIndex == null) continue;
179
+ // Cross-cluster: use global ID
180
+ const globalID = globalIDMap.get(origIndex);
181
+ if (globalID != null) {
182
+ newFieldsMeta[key] = {
183
+ ...meta,
184
+ model: globalID
185
+ };
186
+ continue;
187
+ }
188
+ // Intra-cluster: use new position
189
+ const newIndex = internalIndexMap.get(origIndex);
190
+ if (newIndex != null) {
191
+ newFieldsMeta[key] = {
192
+ ...meta,
193
+ model: `#${newIndex}`
194
+ };
195
+ }
196
+ }
197
+ }
198
+ rewritten.fieldsMeta = newFieldsMeta;
199
+ }
200
+ return rewritten;
201
+ });
202
+ }
203
+ /**
204
+ * Topologically sort cluster groups by inter-group dependencies
205
+ * (interfaces + one-way fieldsMeta). Returns group indices in build order.
206
+ */ function sortGroupsByDeps(groups, interfaceDeps, fieldsMetaDeps) {
207
+ // Map each model index to its group index
208
+ const modelToGroup = new Map();
209
+ for(let gi = 0; gi < groups.length; gi++){
210
+ for (const modelIdx of groups[gi]){
211
+ modelToGroup.set(modelIdx, gi);
212
+ }
213
+ }
214
+ // Build inter-group dependency graph
215
+ const groupDeps = Array.from({
216
+ length: groups.length
217
+ }, ()=>new Set());
218
+ for(let gi = 0; gi < groups.length; gi++){
219
+ for (const modelIdx of groups[gi]){
220
+ // Interface deps
221
+ for (const dep of interfaceDeps[modelIdx]){
222
+ const depGroup = modelToGroup.get(dep);
223
+ if (depGroup != null && depGroup !== gi) {
224
+ groupDeps[gi].add(depGroup);
225
+ }
226
+ }
227
+ // One-way fieldsMeta deps (to models outside this group)
228
+ for (const dep of fieldsMetaDeps[modelIdx]){
229
+ const depGroup = modelToGroup.get(dep);
230
+ if (depGroup != null && depGroup !== gi) {
231
+ groupDeps[gi].add(depGroup);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ // Kahn's topological sort
237
+ const remaining = groupDeps.map((deps)=>deps.size);
238
+ const ready = remaining.map((count, idx)=>count === 0 ? idx : -1).filter((idx)=>idx >= 0);
239
+ const sorted = [];
240
+ while(ready.length > 0){
241
+ const next = ready.shift();
242
+ if (next == null) break;
243
+ sorted.push(next);
244
+ for(let gi = 0; gi < groupDeps.length; gi++){
245
+ if (groupDeps[gi].has(next)) {
246
+ remaining[gi]--;
247
+ if (remaining[gi] === 0) {
248
+ ready.push(gi);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ return sorted;
254
+ }
255
+ function buildOptimalClusters(models, externalClusters) {
256
+ if (models.length === 0) {
257
+ return [];
258
+ }
259
+ const fieldsMetaDeps = extractFieldsMetaDeps(models);
260
+ const interfaceDeps = extractInterfaceDeps(models);
261
+ const groups = findStronglyConnectedComponents(fieldsMetaDeps);
262
+ const buildOrder = sortGroupsByDeps(groups, interfaceDeps, fieldsMetaDeps);
263
+ // Track global IDs of already-built models: original input index → global DocumentModelID string
264
+ const globalIDMap = new Map();
265
+ // Accumulate built clusters for use as externalClusters by later groups
266
+ const builtClusters = {
267
+ ...externalClusters ?? {}
268
+ };
269
+ const result = [];
270
+ for (const groupIdx of buildOrder){
271
+ const group = groups[groupIdx];
272
+ const prepared = prepareClusterModels(models, group, interfaceDeps, globalIDMap);
273
+ const builder = new ClusterBuilder({
274
+ externalClusters: Object.keys(builtClusters).length > 0 ? builtClusters : undefined
275
+ });
276
+ builder.addAll(prepared);
277
+ const cluster = builder.build();
278
+ result.push(cluster);
279
+ // Record global IDs for models in this cluster
280
+ const clusterDigest = digestJSON(cluster.models);
281
+ for(let i = 0; i < group.length; i++){
282
+ // Find which cluster-internal index corresponds to this original model
283
+ // prepareClusterModels sorts by interface deps + name, so we match by name
284
+ const origIdx = group[i];
285
+ const modelName = models[origIdx].name;
286
+ const clusterInternalIdx = cluster.models.findIndex((m)=>m.name === modelName);
287
+ if (clusterInternalIdx !== -1) {
288
+ const globalID = new DocumentModelID(clusterDigest, clusterInternalIdx);
289
+ globalIDMap.set(origIdx, globalID.toString());
290
+ }
291
+ }
292
+ Object.assign(builtClusters, clusterToRecord(cluster));
293
+ }
294
+ return result;
295
+ }
296
+ /**
297
+ * Higher-level builder that produces optimally-split clusters from model
298
+ * definitions. Independent models get their own cluster; cross-referencing
299
+ * models are grouped together. This maximizes content-addressed
300
+ * deduplication while keeping the ClusterBuilder as an internal detail.
301
+ */ export class GraphBuilder {
302
+ #models = [];
303
+ #externalClusters;
304
+ constructor(params = {}){
305
+ this.#externalClusters = params.externalClusters ?? {};
306
+ }
307
+ add(model) {
308
+ this.#models.push(model);
309
+ return this;
310
+ }
311
+ addAll(models) {
312
+ this.#models.push(...models);
313
+ return this;
314
+ }
315
+ build() {
316
+ const externalClusters = Object.keys(this.#externalClusters).length > 0 ? this.#externalClusters : undefined;
317
+ const clusters = buildOptimalClusters(this.#models, externalClusters);
318
+ const record = {};
319
+ for (const cluster of clusters){
320
+ Object.assign(record, clusterToRecord(cluster));
321
+ }
322
+ return record;
323
+ }
324
+ }
@@ -1 +1,16 @@
1
- export const binaryStringValue={title:"KubunBinaryValue",type:"string",pattern:"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"};export const didStringValue={title:"KubunDIDValue",type:"string",pattern:"^did:[a-z0-9]+:"};export const signedJWTStringValue={title:"KubunSignedJWTValue",type:"string",pattern:"^[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+$"};
1
+ export const binaryStringValue = {
2
+ title: 'KubunBinaryValue',
3
+ type: 'string',
4
+ // Base 64 encoding
5
+ pattern: '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$'
6
+ };
7
+ export const didStringValue = {
8
+ title: 'KubunDIDValue',
9
+ type: 'string',
10
+ pattern: '^did:[a-z0-9]+:'
11
+ };
12
+ export const signedJWTStringValue = {
13
+ title: 'KubunSignedJWTValue',
14
+ type: 'string',
15
+ pattern: '^[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+.[a-zA-Z0-9_-]+$'
16
+ };
package/lib/semantic.js CHANGED
@@ -1 +1,23 @@
1
- export const KubunSpanNames={GRAPH_QUERY:"kubun.graph.query",GRAPH_MUTATE:"kubun.graph.mutate",GRAPH_SUBSCRIBE:"kubun.graph.subscribe",GRAPH_DEPLOY:"kubun.graph.deploy",GRAPHQL_EXECUTE:"kubun.graphql.execute",DATA_QUERY:"kubun.data.query",DATA_MUTATE:"kubun.data.mutate",DATA_TRANSACTION:"kubun.data.transaction"};export const KubunAttributeKeys={GRAPH_ID:"kubun.graph.id",GRAPHQL_OPERATION_TYPE:"graphql.operation.type",GRAPHQL_DOCUMENT:"graphql.document",GRAPHQL_VARIABLES:"graphql.variables",MODEL_ID:"kubun.model.id",DOCUMENT_ID:"kubun.document.id",DOCUMENTS_COUNT:"kubun.documents.count",MUTATION_TYPE:"kubun.mutation.type",MUTATION_DATA:"kubun.mutation.data",QUERY_FILTER:"kubun.query.filter",ERROR_CODE:"kubun.error.code"};
1
+ export const KubunSpanNames = {
2
+ GRAPH_QUERY: 'kubun.graph.query',
3
+ GRAPH_MUTATE: 'kubun.graph.mutate',
4
+ GRAPH_SUBSCRIBE: 'kubun.graph.subscribe',
5
+ GRAPH_DEPLOY: 'kubun.graph.deploy',
6
+ GRAPHQL_EXECUTE: 'kubun.graphql.execute',
7
+ DATA_QUERY: 'kubun.data.query',
8
+ DATA_MUTATE: 'kubun.data.mutate',
9
+ DATA_TRANSACTION: 'kubun.data.transaction'
10
+ };
11
+ export const KubunAttributeKeys = {
12
+ GRAPH_ID: 'kubun.graph.id',
13
+ GRAPHQL_OPERATION_TYPE: 'graphql.operation.type',
14
+ GRAPHQL_DOCUMENT: 'graphql.document',
15
+ GRAPHQL_VARIABLES: 'graphql.variables',
16
+ MODEL_ID: 'kubun.model.id',
17
+ DOCUMENT_ID: 'kubun.document.id',
18
+ DOCUMENTS_COUNT: 'kubun.documents.count',
19
+ MUTATION_TYPE: 'kubun.mutation.type',
20
+ MUTATION_DATA: 'kubun.mutation.data',
21
+ QUERY_FILTER: 'kubun.query.filter',
22
+ ERROR_CODE: 'kubun.error.code'
23
+ };