@zeyos/client 0.3.0 → 0.5.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +41 -2
  3. package/agents/README.md +10 -0
  4. package/agents/shared/zeyos-agent-operating-guide.md +42 -0
  5. package/agents/shared/zeyos-entity-map.md +5 -1
  6. package/agents/shared/zeyos-entity-reference.md +89 -33
  7. package/agents/shared/zeyos-query-patterns.md +26 -0
  8. package/agents/zeyos-calendar-and-scheduling/SKILL.md +45 -0
  9. package/agents/zeyos-calendar-and-scheduling/references/workflows.md +49 -0
  10. package/agents/zeyos-data-quality-and-governance/SKILL.md +43 -0
  11. package/agents/zeyos-data-quality-and-governance/references/workflows.md +51 -0
  12. package/agents/zeyos-document-and-approval/SKILL.md +41 -0
  13. package/agents/zeyos-document-and-approval/references/workflows.md +43 -0
  14. package/agents/zeyos-procurement-and-supplier-performance/SKILL.md +36 -0
  15. package/agents/zeyos-procurement-and-supplier-performance/references/workflows.md +46 -0
  16. package/agents/zeyos-time-tracking/SKILL.md +2 -0
  17. package/agents/zeyos-time-tracking/references/workflows.md +68 -0
  18. package/agents/zeyos-work-management/SKILL.md +4 -3
  19. package/agents/zeyos-work-management/references/workflows.md +39 -1
  20. package/docs/03-cli/02-commands.md +36 -2
  21. package/docs/03-cli/03-configuration.md +1 -0
  22. package/docs/06-okf/01-overview.md +70 -0
  23. package/docs/06-okf/02-producing-and-consuming.md +46 -0
  24. package/docs/06-okf/03-keeping-fresh.md +53 -0
  25. package/docs/06-okf/04-loops.md +58 -0
  26. package/docs/06-okf/_category_.json +9 -0
  27. package/okf/concepts/calendar-timezones.md +10 -0
  28. package/okf/concepts/confirmation-and-side-effects.md +14 -0
  29. package/okf/concepts/counting-and-sums.md +10 -0
  30. package/okf/concepts/currency-and-rounding.md +10 -0
  31. package/okf/concepts/dates-unix-seconds.md +12 -0
  32. package/okf/concepts/enums.md +14 -0
  33. package/okf/concepts/filters-vs-filter.md +14 -0
  34. package/okf/concepts/idempotency-and-deduplication.md +10 -0
  35. package/okf/concepts/index.md +16 -0
  36. package/okf/concepts/null-empty-missing.md +10 -0
  37. package/okf/concepts/official-versus-latest.md +10 -0
  38. package/okf/concepts/operationid-vocabulary.md +17 -0
  39. package/okf/concepts/ownership-versus-attention.md +15 -0
  40. package/okf/concepts/untrusted-business-content.md +10 -0
  41. package/okf/concepts/visibility-column.md +13 -0
  42. package/okf/entities/accounts.md +82 -0
  43. package/okf/entities/actionsteps.md +84 -0
  44. package/okf/entities/addresses.md +50 -0
  45. package/okf/entities/applicationassets.md +43 -0
  46. package/okf/entities/applications.md +62 -0
  47. package/okf/entities/appointments.md +79 -0
  48. package/okf/entities/associations.md +41 -0
  49. package/okf/entities/binfiles.md +32 -0
  50. package/okf/entities/campaigns.md +66 -0
  51. package/okf/entities/categories.md +55 -0
  52. package/okf/entities/channels.md +54 -0
  53. package/okf/entities/comments.md +44 -0
  54. package/okf/entities/components.md +46 -0
  55. package/okf/entities/contacts.md +96 -0
  56. package/okf/entities/contacts2contacts.md +42 -0
  57. package/okf/entities/contracts.md +83 -0
  58. package/okf/entities/couponcodes.md +58 -0
  59. package/okf/entities/coupons.md +69 -0
  60. package/okf/entities/customfields.md +59 -0
  61. package/okf/entities/davservers.md +74 -0
  62. package/okf/entities/devices.md +65 -0
  63. package/okf/entities/documents.md +76 -0
  64. package/okf/entities/dunning.md +82 -0
  65. package/okf/entities/dunning2transactions.md +46 -0
  66. package/okf/entities/entities2channels.md +42 -0
  67. package/okf/entities/events.md +57 -0
  68. package/okf/entities/feedservers.md +67 -0
  69. package/okf/entities/files.md +50 -0
  70. package/okf/entities/follows.md +40 -0
  71. package/okf/entities/forks.md +54 -0
  72. package/okf/entities/groups.md +48 -0
  73. package/okf/entities/groups2users.md +44 -0
  74. package/okf/entities/index.md +93 -0
  75. package/okf/entities/invitations.md +53 -0
  76. package/okf/entities/items.md +95 -0
  77. package/okf/entities/ledgers.md +56 -0
  78. package/okf/entities/likes.md +40 -0
  79. package/okf/entities/links.md +70 -0
  80. package/okf/entities/mailinglists.md +67 -0
  81. package/okf/entities/mailingrecipients.md +45 -0
  82. package/okf/entities/mailservers.md +77 -0
  83. package/okf/entities/messagereads.md +40 -0
  84. package/okf/entities/messages.md +104 -0
  85. package/okf/entities/notes.md +73 -0
  86. package/okf/entities/objects.md +70 -0
  87. package/okf/entities/opportunities.md +87 -0
  88. package/okf/entities/participants.md +52 -0
  89. package/okf/entities/payments.md +76 -0
  90. package/okf/entities/permissions.md +46 -0
  91. package/okf/entities/pricelists.md +70 -0
  92. package/okf/entities/pricelists2accounts.md +46 -0
  93. package/okf/entities/prices.md +49 -0
  94. package/okf/entities/projects.md +72 -0
  95. package/okf/entities/records.md +75 -0
  96. package/okf/entities/relateditems.md +43 -0
  97. package/okf/entities/resources.md +55 -0
  98. package/okf/entities/services.md +64 -0
  99. package/okf/entities/stocktransactions.md +72 -0
  100. package/okf/entities/storages.md +56 -0
  101. package/okf/entities/suppliers.md +51 -0
  102. package/okf/entities/tasks.md +86 -0
  103. package/okf/entities/tickets.md +86 -0
  104. package/okf/entities/transactions.md +118 -0
  105. package/okf/entities/users.md +66 -0
  106. package/okf/entities/weblets.md +66 -0
  107. package/okf/index.md +11 -0
  108. package/okf/log.md +4 -0
  109. package/okf/metrics/account-address-completeness.md +10 -0
  110. package/okf/metrics/cash-received.md +10 -0
  111. package/okf/metrics/index.md +9 -0
  112. package/okf/metrics/invoiced-net-revenue.md +16 -0
  113. package/okf/metrics/open-customers.md +14 -0
  114. package/okf/metrics/overdue-receivables.md +12 -0
  115. package/okf/metrics/stock-movement-by-storage.md +10 -0
  116. package/okf/metrics/supplier-delivery-performance.md +10 -0
  117. package/okf/playbooks/activity-timeline.md +11 -0
  118. package/okf/playbooks/calendar-availability.md +11 -0
  119. package/okf/playbooks/campaign-recipient-coverage.md +12 -0
  120. package/okf/playbooks/customer-360.md +12 -0
  121. package/okf/playbooks/document-approval.md +10 -0
  122. package/okf/playbooks/duplicate-account-review.md +11 -0
  123. package/okf/playbooks/effective-customer-price.md +11 -0
  124. package/okf/playbooks/index.md +13 -0
  125. package/okf/playbooks/missing-billing-addresses.md +12 -0
  126. package/okf/playbooks/revenue-this-year.md +19 -0
  127. package/okf/playbooks/supplier-scorecard.md +10 -0
  128. package/okf/playbooks/ticket-work-packet.md +11 -0
  129. package/package.json +11 -3
  130. package/scripts/data/okf-curation.mjs +446 -0
  131. package/scripts/generate-client.mjs +4 -275
  132. package/scripts/generate-okf.mjs +241 -0
  133. package/scripts/lib/live-test-config.mjs +20 -0
  134. package/scripts/lib/okf.mjs +272 -0
  135. package/scripts/lib/spec-model.mjs +325 -0
  136. package/src/index.js +4 -0
  137. package/src/runtime/okf.js +237 -0
@@ -0,0 +1,272 @@
1
+ // Open Knowledge Format (OKF v0.1) rendering + managed-block splicing.
2
+ //
3
+ // Pure functions only (no fs): given specs/curation in, markdown out. The
4
+ // orchestrator (scripts/generate-okf.mjs) and the runtime consumer
5
+ // (src/runtime/okf.js) reuse the same conformance-relevant constants.
6
+
7
+ // The OKF conformance constants live in the shipped client so there is a single
8
+ // definition; the build-time renderer below imports them. (Generated content is
9
+ // fenced by the markers; the producer rewrites only between them, preserving
10
+ // curated `# Notes`/`# Metrics` prose added by humans or the refiner.)
11
+ import {
12
+ OKF_VERSION,
13
+ GENERATED_START,
14
+ GENERATED_END,
15
+ GENERATED_FRONTMATTER_KEYS as GENERATED_FRONTMATTER_KEY_LIST
16
+ } from '../../src/runtime/okf.js';
17
+
18
+ export { OKF_VERSION, GENERATED_START, GENERATED_END };
19
+
20
+ const GENERATED_FRONTMATTER_KEYS = new Set(GENERATED_FRONTMATTER_KEY_LIST);
21
+
22
+ // ── YAML (minimal; only the scalar/list shapes OKF frontmatter uses) ──────────
23
+
24
+ function needsQuote(s) {
25
+ return /^[\s>|*&!%@`#?,\[\]{}-]/.test(s) || /[:#]\s|: |\s$|^$/.test(s) || /["']/.test(s);
26
+ }
27
+
28
+ function toYamlScalar(value) {
29
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
30
+ if (typeof value === 'number') return String(value);
31
+ const s = String(value);
32
+ if (needsQuote(s)) return JSON.stringify(s); // JSON string is valid YAML double-quoted
33
+ return s;
34
+ }
35
+
36
+ function toYamlValue(value) {
37
+ if (Array.isArray(value)) return `[${value.map((v) => toYamlScalar(v)).join(', ')}]`;
38
+ return toYamlScalar(value);
39
+ }
40
+
41
+ /** Parse a `---`-delimited frontmatter block into ordered raw lines + a key set.
42
+ * Tolerant by design: we only need to know which keys exist so we can preserve
43
+ * human-added ones; we never re-serialize parsed values. */
44
+ export function parseFrontmatter(content) {
45
+ const match = /^---\n([\s\S]*?)\n---\n?/.exec(content || '');
46
+ if (!match) return { keys: new Set(), rawLines: [], body: content || '' };
47
+ const rawLines = match[1].split('\n');
48
+ const keys = new Set();
49
+ for (const line of rawLines) {
50
+ const kv = /^([A-Za-z0-9_]+):/.exec(line);
51
+ if (kv) keys.add(kv[1]);
52
+ }
53
+ return { keys, rawLines, body: (content || '').slice(match[0].length) };
54
+ }
55
+
56
+ function emitFrontmatter(generated, preservedRawLines = []) {
57
+ const lines = [];
58
+ for (const [key, value] of Object.entries(generated)) {
59
+ if (value === undefined || value === null) continue;
60
+ lines.push(`${key}: ${toYamlValue(value)}`);
61
+ }
62
+ for (const raw of preservedRawLines) lines.push(raw);
63
+ return lines.join('\n');
64
+ }
65
+
66
+ // ── Managed-block splice ──────────────────────────────────────────────────────
67
+
68
+ /** Extract the curated body that follows the generated block in an existing
69
+ * file, so it can be carried forward unchanged. */
70
+ function curatedTail(existingBody) {
71
+ const end = existingBody.indexOf(GENERATED_END);
72
+ if (end === -1) return null;
73
+ return existingBody.slice(end + GENERATED_END.length).replace(/^\n+/, '');
74
+ }
75
+
76
+ /**
77
+ * Produce a concept file, rewriting only generated frontmatter + the generated
78
+ * body region. Curated frontmatter keys and the curated body tail are preserved.
79
+ *
80
+ * @param {object} args
81
+ * @param {string?} args.existing Existing file content, or null/'' for new.
82
+ * @param {object} args.frontmatter Generated frontmatter (ordered object).
83
+ * @param {string} args.generatedBody Markdown to place between the markers.
84
+ * @param {string} args.seedBody Curated body to use only when the file is new.
85
+ */
86
+ export function spliceConcept({ existing, frontmatter, generatedBody, seedBody = '' }) {
87
+ const parsed = parseFrontmatter(existing || '');
88
+ const preserved = parsed.rawLines.filter((line) => {
89
+ const kv = /^([A-Za-z0-9_]+):/.exec(line);
90
+ return kv && !GENERATED_FRONTMATTER_KEYS.has(kv[1]);
91
+ });
92
+ const tail = existing ? curatedTail(parsed.body) : null;
93
+ const curated = (tail != null ? tail : seedBody).replace(/\s+$/, '');
94
+
95
+ const fm = emitFrontmatter(frontmatter, preserved);
96
+ const block = `${GENERATED_START}\n${generatedBody.replace(/\s+$/, '')}\n${GENERATED_END}`;
97
+ const parts = [`---\n${fm}\n---`, block];
98
+ if (curated) parts.push(curated);
99
+ return `${parts.join('\n\n')}\n`;
100
+ }
101
+
102
+ /**
103
+ * Replace only the content between the generated markers in an existing file,
104
+ * preserving everything before and after. Unlike spliceConcept this does not own
105
+ * frontmatter — it is for injecting a generated digest into an otherwise curated
106
+ * file (e.g. the shared skill references). Returns null if the markers are absent
107
+ * so the caller can warn rather than silently no-op.
108
+ */
109
+ export function replaceManagedBlock(existing, generatedBody) {
110
+ if (!existing || !existing.includes(GENERATED_START) || !existing.includes(GENERATED_END)) {
111
+ return null;
112
+ }
113
+ const start = existing.indexOf(GENERATED_START);
114
+ const end = existing.indexOf(GENERATED_END);
115
+ const before = existing.slice(0, start);
116
+ const after = existing.slice(end + GENERATED_END.length);
117
+ return `${before}${GENERATED_START}\n${generatedBody.replace(/\s+$/, '')}\n${GENERATED_END}${after}`;
118
+ }
119
+
120
+ // ── Entity rendering ───────────────────────────────────────────────────────────
121
+
122
+ const DASH = '—';
123
+
124
+ function fkCell(field, linkForEntity) {
125
+ if (!field.fk) return DASH;
126
+ const link = linkForEntity(field.fk.table);
127
+ const label = `${field.fk.table}.${field.fk.field}`;
128
+ return link ? `[${field.fk.table}](${link})` : label;
129
+ }
130
+
131
+ function renderSchemaTable(entity, linkForEntity) {
132
+ const header = '| Column | Type | Nullable | Default | Indexed | FK |\n|---|---|---|---|---|---|';
133
+ const rows = entity.fields.map((f) => {
134
+ const def = f.default == null ? DASH : `\`${String(f.default).replace(/\|/g, '\\|')}\``;
135
+ return `| \`${f.name}\` | ${f.type} | ${f.notnull ? 'no' : 'yes'} | ${def} | ${f.indexed ? 'yes' : DASH} | ${fkCell(f, linkForEntity)} |`;
136
+ });
137
+ return `# Schema\n\n${header}\n${rows.join('\n')}`;
138
+ }
139
+
140
+ function renderEnums(entity) {
141
+ const withEnum = entity.fields.filter((f) => f.enum);
142
+ if (!withEnum.length) return '';
143
+ const blocks = withEnum.map((f) => {
144
+ const pairs = Object.entries(f.enum).map(([k, v]) => `\`${k}\` = ${v}`).join(' · ');
145
+ return `### \`${f.name}\`\n\n${pairs}`;
146
+ });
147
+ return `# Enums\n\n${blocks.join('\n\n')}`;
148
+ }
149
+
150
+ function renderForeignKeys(entity, linkForEntity) {
151
+ const fks = entity.fields.filter((f) => f.fk);
152
+ if (!fks.length) return '';
153
+ const items = fks.map((f) => {
154
+ const link = linkForEntity(f.fk.table);
155
+ const target = link ? `[${f.fk.table}](${link})` : f.fk.table;
156
+ return `- \`${f.name}\` → ${target} (\`${f.fk.table}.${f.fk.field}\`)`;
157
+ });
158
+ return `# Foreign Keys\n\n${items.join('\n')}`;
159
+ }
160
+
161
+ function renderIndexes(entity) {
162
+ const idx = entity.indexes.filter((i) => !i.primary);
163
+ if (!idx.length) return '';
164
+ const items = idx.map((i) => {
165
+ const attrs = [i.method, i.unique ? 'unique' : null, i.partial ? 'partial' : null].filter(Boolean).join(', ');
166
+ const keys = i.keys.length ? ` on \`${i.keys.join(', ')}\`` : '';
167
+ return `- \`${i.name}\` — ${attrs}${keys}`;
168
+ });
169
+ const gin = idx.some((i) => i.method === 'gin') || idx.some((i) => i.partial);
170
+ const note = gin
171
+ ? '\n\n> Partial/GIN indexes back the `filters` (plural) query form for foreign-key fields. See [filters-vs-filter](/concepts/filters-vs-filter.md).'
172
+ : '';
173
+ return `# Indexes\n\n${items.join('\n')}${note}`;
174
+ }
175
+
176
+ function renderOperations(ops) {
177
+ const order = ['list', 'get', 'create', 'update', 'delete', 'exists'];
178
+ const present = order.filter((k) => ops[k]).map((k) => `- ${k}: \`${ops[k]}\``);
179
+ const extra = Object.entries(ops)
180
+ .filter(([k]) => !order.includes(k))
181
+ .map(([k, v]) => `- ${k}: \`${v}\``);
182
+ const lines = [...present, ...extra];
183
+ if (!lines.length) return '';
184
+ return `# Operations\n\n${lines.join('\n')}`;
185
+ }
186
+
187
+ /** Render the generated (managed) body for a ZeyOS entity concept. */
188
+ export function renderEntityGeneratedBody({ entity, ops, linkForEntity }) {
189
+ return [
190
+ renderSchemaTable(entity, linkForEntity),
191
+ renderForeignKeys(entity, linkForEntity),
192
+ renderEnums(entity),
193
+ renderIndexes(entity),
194
+ renderOperations(ops)
195
+ ].filter(Boolean).join('\n\n');
196
+ }
197
+
198
+ // ── Index + log rendering ──────────────────────────────────────────────────────
199
+
200
+ /** A sub-directory index.md: sections of `* [Title](url) - description` bullets.
201
+ * No frontmatter (spec §5). */
202
+ export function renderIndex(sections) {
203
+ const out = [];
204
+ for (const section of sections) {
205
+ out.push(`# ${section.heading}`, '');
206
+ for (const item of section.items) {
207
+ const desc = item.description ? ` - ${item.description}` : '';
208
+ out.push(`* [${item.title}](${item.url})${desc}`);
209
+ }
210
+ out.push('');
211
+ }
212
+ return `${out.join('\n').replace(/\s+$/, '')}\n`;
213
+ }
214
+
215
+ /** The bundle-root index.md — the only index that may carry frontmatter (§9). */
216
+ export function renderRootIndex({ sourceSnapshot, sections }) {
217
+ const fm = emitFrontmatter({ okf_version: OKF_VERSION, source_snapshot: sourceSnapshot });
218
+ return `---\n${fm}\n---\n\n${renderIndex(sections)}`;
219
+ }
220
+
221
+ // ── Schema diff → log.md (the freshness changelog) ──────────────────────────────
222
+
223
+ function enumString(enumObj) {
224
+ return enumObj ? Object.entries(enumObj).map(([k, v]) => `${k}=${v}`).join(',') : '';
225
+ }
226
+
227
+ /** Compare two buildEntityModel() outputs and describe what changed, for log.md. */
228
+ export function diffEntityModels(prev, next) {
229
+ const changes = [];
230
+ const prevNames = new Set(Object.keys(prev || {}));
231
+ const nextNames = new Set(Object.keys(next || {}));
232
+
233
+ for (const name of [...nextNames].sort()) {
234
+ if (!prevNames.has(name)) { changes.push({ kind: 'Creation', text: `Entity \`${name}\` added.` }); continue; }
235
+ const a = prev[name];
236
+ const b = next[name];
237
+ const aFields = new Map(a.fields.map((f) => [f.name, f]));
238
+ const bFields = new Map(b.fields.map((f) => [f.name, f]));
239
+ for (const fname of bFields.keys()) {
240
+ if (!aFields.has(fname)) changes.push({ kind: 'Update', text: `\`${name}\`: field \`${fname}\` added.` });
241
+ }
242
+ for (const fname of aFields.keys()) {
243
+ if (!bFields.has(fname)) changes.push({ kind: 'Update', text: `\`${name}\`: field \`${fname}\` removed.` });
244
+ }
245
+ for (const [fname, bf] of bFields) {
246
+ const af = aFields.get(fname);
247
+ if (!af) continue;
248
+ if (af.type !== bf.type) changes.push({ kind: 'Update', text: `\`${name}.${fname}\`: type ${af.type} → ${bf.type}.` });
249
+ if (enumString(af.enum) !== enumString(bf.enum)) changes.push({ kind: 'Update', text: `\`${name}.${fname}\`: enum values changed.` });
250
+ const afk = af.fk ? af.fk.table : '';
251
+ const bfk = bf.fk ? bf.fk.table : '';
252
+ if (afk !== bfk) changes.push({ kind: 'Update', text: `\`${name}.${fname}\`: foreign key ${afk || 'none'} → ${bfk || 'none'}.` });
253
+ }
254
+ }
255
+ for (const name of [...prevNames].sort()) {
256
+ if (!nextNames.has(name)) changes.push({ kind: 'Deprecation', text: `Entity \`${name}\` removed.` });
257
+ }
258
+ return changes;
259
+ }
260
+
261
+ /** Prepend a dated section to log.md (newest first). Idempotent across same-day
262
+ * re-runs only when `changes` is empty (caller skips). */
263
+ export function prependLogEntry({ existing, date, changes, title = 'OKF Update Log' }) {
264
+ const header = `# ${title}`;
265
+ const entryLines = [`## ${date}`, ...changes.map((c) => `* **${c.kind}**: ${c.text}`)];
266
+ const entry = entryLines.join('\n');
267
+ if (!existing || !existing.includes(header)) {
268
+ return `${header}\n\n${entry}\n`;
269
+ }
270
+ const body = existing.slice(existing.indexOf(header) + header.length).replace(/^\n+/, '');
271
+ return `${header}\n\n${entry}\n\n${body.replace(/\s+$/, '')}\n`;
272
+ }
@@ -0,0 +1,325 @@
1
+ // Shared OpenAPI / dbref parsing primitives.
2
+ //
3
+ // Extracted verbatim from scripts/generate-client.mjs so both the client codegen
4
+ // and the OKF producer (scripts/generate-okf.mjs) read the specs the same way.
5
+ // generate-client.mjs imports these unchanged; generate-okf.mjs additionally uses
6
+ // buildEntityModel() for the richer per-entity detail OKF docs need.
7
+
8
+ import { readFile } from 'node:fs/promises';
9
+
10
+ export const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']);
11
+
12
+ export function unescapePointerToken(token) {
13
+ return token.replace(/~1/g, '/').replace(/~0/g, '~');
14
+ }
15
+
16
+ export function pointerGet(doc, ref) {
17
+ if (!ref.startsWith('#/')) {
18
+ throw new Error(`External $ref is not supported: ${ref}`);
19
+ }
20
+
21
+ const parts = ref.slice(2).split('/').map(unescapePointerToken);
22
+ let current = doc;
23
+ for (const part of parts) {
24
+ if (current == null || !Object.prototype.hasOwnProperty.call(current, part)) {
25
+ throw new Error(`Unresolvable $ref: ${ref}`);
26
+ }
27
+ current = current[part];
28
+ }
29
+ return current;
30
+ }
31
+
32
+ export function resolveRef(doc, value) {
33
+ if (!value || typeof value !== 'object') {
34
+ return value;
35
+ }
36
+ if (typeof value.$ref === 'string') {
37
+ return pointerGet(doc, value.$ref);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ export function normalizeServer(server) {
43
+ const urlTemplate = server?.url || '';
44
+ const defaultVariables = {};
45
+
46
+ if (server?.variables && typeof server.variables === 'object') {
47
+ for (const [name, variable] of Object.entries(server.variables)) {
48
+ if (variable && typeof variable.default === 'string') {
49
+ defaultVariables[name] = variable.default;
50
+ }
51
+ }
52
+ }
53
+
54
+ const basePathTemplate = (() => {
55
+ const match = urlTemplate.match(/^https?:\/\/[^/]+(\/.*)$/i);
56
+ return match ? match[1] : '';
57
+ })();
58
+
59
+ return {
60
+ urlTemplate,
61
+ basePathTemplate,
62
+ defaultVariables
63
+ };
64
+ }
65
+
66
+ export function normalizeParameters(doc, pathItem, operation) {
67
+ const combined = [
68
+ ...(Array.isArray(pathItem?.parameters) ? pathItem.parameters : []),
69
+ ...(Array.isArray(operation?.parameters) ? operation.parameters : [])
70
+ ];
71
+
72
+ const indexed = new Map();
73
+ for (const rawParameter of combined) {
74
+ const parameter = resolveRef(doc, rawParameter);
75
+ if (!parameter || typeof parameter !== 'object') {
76
+ continue;
77
+ }
78
+
79
+ const key = `${parameter.in || 'unknown'}:${parameter.name || 'unknown'}`;
80
+ indexed.set(key, {
81
+ name: parameter.name,
82
+ in: parameter.in,
83
+ required: Boolean(parameter.required)
84
+ });
85
+ }
86
+
87
+ const normalized = Array.from(indexed.values()).filter((parameter) => parameter.name && parameter.in);
88
+
89
+ return {
90
+ all: normalized,
91
+ path: normalized.filter((parameter) => parameter.in === 'path').map((parameter) => parameter.name),
92
+ query: normalized.filter((parameter) => parameter.in === 'query').map((parameter) => parameter.name),
93
+ header: normalized.filter((parameter) => parameter.in === 'header').map((parameter) => parameter.name)
94
+ };
95
+ }
96
+
97
+ export function normalizeRequestBody(doc, operation) {
98
+ const requestBody = resolveRef(doc, operation?.requestBody);
99
+ if (!requestBody || typeof requestBody !== 'object') {
100
+ return {
101
+ required: false,
102
+ contentTypes: []
103
+ };
104
+ }
105
+
106
+ const content = requestBody.content && typeof requestBody.content === 'object' ? requestBody.content : {};
107
+ const contentTypes = Object.keys(content);
108
+
109
+ return {
110
+ required: Boolean(requestBody.required),
111
+ contentTypes
112
+ };
113
+ }
114
+
115
+ export function normalizeOperationSecurity(doc, operation) {
116
+ if (Array.isArray(operation?.security)) {
117
+ return operation.security;
118
+ }
119
+ if (Array.isArray(doc.security)) {
120
+ return doc.security;
121
+ }
122
+ return [];
123
+ }
124
+
125
+ export function buildOperationId({ operationId, method, route, existingIds }) {
126
+ if (operationId && !existingIds.has(operationId)) {
127
+ return operationId;
128
+ }
129
+
130
+ const routeToken = route
131
+ .replace(/[^a-zA-Z0-9{}]/g, '_')
132
+ .replace(/[{}]/g, '')
133
+ .replace(/_+/g, '_')
134
+ .replace(/^_|_$/g, '');
135
+
136
+ let candidate = operationId || `${method.toLowerCase()}_${routeToken || 'operation'}`;
137
+ let suffix = 1;
138
+
139
+ while (existingIds.has(candidate)) {
140
+ suffix += 1;
141
+ candidate = `${operationId || `${method.toLowerCase()}_${routeToken || 'operation'}`}_${suffix}`;
142
+ }
143
+
144
+ return candidate;
145
+ }
146
+
147
+ export function collectOperations(doc) {
148
+ const operations = [];
149
+ const existingIds = new Set();
150
+
151
+ for (const [route, pathItem] of Object.entries(doc.paths || {})) {
152
+ for (const [method, operation] of Object.entries(pathItem || {})) {
153
+ if (!HTTP_METHODS.has(method)) {
154
+ continue;
155
+ }
156
+
157
+ const normalizedMethod = method.toUpperCase();
158
+ const parameters = normalizeParameters(doc, pathItem, operation);
159
+ const requestBody = normalizeRequestBody(doc, operation);
160
+ const security = normalizeOperationSecurity(doc, operation);
161
+ const finalOperationId = buildOperationId({
162
+ operationId: operation.operationId,
163
+ method: normalizedMethod,
164
+ route,
165
+ existingIds
166
+ });
167
+
168
+ existingIds.add(finalOperationId);
169
+
170
+ operations.push({
171
+ operationId: finalOperationId,
172
+ summary: operation.summary || '',
173
+ deprecated: Boolean(operation.deprecated),
174
+ method: normalizedMethod,
175
+ path: route,
176
+ security,
177
+ requestBodyRequired: requestBody.required,
178
+ requestContentTypes: requestBody.contentTypes,
179
+ parameterNames: {
180
+ path: parameters.path,
181
+ query: parameters.query,
182
+ header: parameters.header
183
+ }
184
+ });
185
+ }
186
+ }
187
+
188
+ operations.sort((a, b) => {
189
+ if (a.path !== b.path) {
190
+ return a.path.localeCompare(b.path);
191
+ }
192
+ return a.method.localeCompare(b.method);
193
+ });
194
+
195
+ return operations;
196
+ }
197
+
198
+ export function buildServices(specEntries) {
199
+ const services = {};
200
+
201
+ for (const specEntry of specEntries) {
202
+ const { service, file, doc } = specEntry;
203
+ const firstServer = Array.isArray(doc.servers) ? doc.servers[0] : undefined;
204
+
205
+ services[service] = {
206
+ key: service,
207
+ source: file,
208
+ title: doc.info?.title || '',
209
+ version: doc.info?.version || '',
210
+ server: normalizeServer(firstServer),
211
+ globalSecurity: Array.isArray(doc.security) ? doc.security : [],
212
+ operations: collectOperations(doc)
213
+ };
214
+ }
215
+
216
+ return services;
217
+ }
218
+
219
+ // Enum values are documented inline in dbref descriptions as `N`=LABEL pairs,
220
+ // e.g. "Status (`0`=NOTSTARTED, `1`=AWAITINGACCEPTANCE, ...)". Extract them so
221
+ // the client can validate enum inputs and suggest valid values.
222
+ export function parseEnum(description) {
223
+ if (typeof description !== 'string') return null;
224
+ const out = {};
225
+ let count = 0;
226
+ const re = /`(-?\d+)`\s*=\s*([A-Za-z0-9_]+)/g;
227
+ let match;
228
+ while ((match = re.exec(description)) !== null) {
229
+ out[match[1]] = match[2];
230
+ count += 1;
231
+ }
232
+ return count >= 2 ? out : null;
233
+ }
234
+
235
+ // Compact field/enum/foreign-key map derived from openapi/dbref.json. Much
236
+ // smaller than the raw dbref (drops storage, collation, constraints, triggers,
237
+ // stats, etc.) so it ships cheaply and powers runtime introspection.
238
+ export function buildSchema(dbref) {
239
+ const schema = {};
240
+ if (!Array.isArray(dbref)) return schema;
241
+
242
+ for (const entity of dbref) {
243
+ if (!entity || typeof entity !== 'object' || !entity.name || !Array.isArray(entity.fields)) {
244
+ continue;
245
+ }
246
+
247
+ const fields = {};
248
+ for (const field of entity.fields) {
249
+ if (!field || typeof field !== 'object' || !field.name) continue;
250
+ const def = { type: field.type || 'unknown' };
251
+ if (field.indexed) def.indexed = true;
252
+ if (Array.isArray(field.fkeys) && field.fkeys.length > 0 && field.fkeys[0].table) {
253
+ def.fk = field.fkeys[0].table;
254
+ }
255
+ const enumValues = parseEnum(field.description);
256
+ if (enumValues) def.enum = enumValues;
257
+ fields[field.name] = def;
258
+ }
259
+
260
+ schema[entity.name] = { type: entity.type || 'table', fields };
261
+ }
262
+
263
+ return schema;
264
+ }
265
+
266
+ // Richer per-entity model for OKF docs. Unlike buildSchema (compact, for the
267
+ // shipped client), this preserves nullability, defaults, FK target field, the
268
+ // raw field description, and the entity's indexes — including the GIN/partial
269
+ // definitions behind the `filters`-vs-`filter` foreign-key footgun.
270
+ export function buildEntityModel(dbref) {
271
+ const model = {};
272
+ if (!Array.isArray(dbref)) return model;
273
+
274
+ for (const entity of dbref) {
275
+ if (!entity || typeof entity !== 'object' || !entity.name || !Array.isArray(entity.fields)) {
276
+ continue;
277
+ }
278
+
279
+ const fields = entity.fields
280
+ .filter((field) => field && typeof field === 'object' && field.name)
281
+ .map((field) => {
282
+ const fk = Array.isArray(field.fkeys) && field.fkeys.length > 0 && field.fkeys[0].table
283
+ ? { table: field.fkeys[0].table, field: field.fkeys[0].field || 'ID' }
284
+ : null;
285
+ return {
286
+ name: field.name,
287
+ type: field.type || 'unknown',
288
+ notnull: Boolean(field.notnull),
289
+ default: field.default ?? null,
290
+ indexed: Boolean(field.indexed),
291
+ fk,
292
+ enum: parseEnum(field.description),
293
+ description: typeof field.description === 'string' ? field.description : ''
294
+ };
295
+ });
296
+
297
+ const indexes = (Array.isArray(entity.indexes) ? entity.indexes : []).map((index) => ({
298
+ name: index.name,
299
+ method: index.method || 'btree',
300
+ unique: Boolean(index.unique),
301
+ primary: Boolean(index.primary),
302
+ partial: Boolean(index.partial),
303
+ keys: Array.isArray(index.keys) ? index.keys : [],
304
+ def: typeof index.def === 'string' ? index.def : ''
305
+ }));
306
+
307
+ model[entity.name] = { name: entity.name, type: entity.type || 'table', fields, indexes };
308
+ }
309
+
310
+ return model;
311
+ }
312
+
313
+ export async function loadSpec(absolutePath) {
314
+ const raw = await readFile(absolutePath, 'utf8');
315
+ return JSON.parse(raw);
316
+ }
317
+
318
+ export async function loadOptionalSpec(absolutePath) {
319
+ try {
320
+ return await loadSpec(absolutePath);
321
+ } catch (error) {
322
+ if (error?.code === 'ENOENT') return null;
323
+ throw error;
324
+ }
325
+ }
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createZeyosClient } from './runtime/client.js';
2
2
  import { ZeyosApiError, ZeyosValidationError } from './runtime/error.js';
3
3
  import { MemoryTokenStore, normalizeTokenSet, tokenResponseToTokenSet } from './runtime/token-store.js';
4
+ import {
5
+ OKF_VERSION, buildOkf, loadOkfBundle, validateOkfBundle, validateOkfFiles, conceptIdForResource
6
+ } from './runtime/okf.js';
4
7
 
5
8
  /**
6
9
  * @typedef {null|boolean|number|string|JsonValue[]|Record<string, JsonValue>} JsonValue
@@ -83,3 +86,4 @@ export function normalizeCountResult(result) {
83
86
  }
84
87
 
85
88
  export { createZeyosClient, ZeyosApiError, ZeyosValidationError, MemoryTokenStore, normalizeTokenSet, tokenResponseToTokenSet };
89
+ export { OKF_VERSION, buildOkf, loadOkfBundle, validateOkfBundle, validateOkfFiles, conceptIdForResource };