@njeybe/nativ 2.4.0 → 2.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.1
4
+
5
+ ### Fixed & Improved
6
+
7
+ - **Resilient Spec Slices Resolution.** Enhanced `resolveSpecSlices` in `src/core/spec-slices.ts` to gracefully resolve non-standard and shorthand contract/spec references across `.ai/` contracts:
8
+ - **Shorthand Markdown Section Anchors:** Supports numeric and prefix section references (e.g. `ui_specs.md#6`, `ui_specs.md#15`, `ui_specs.md#15.1`) by matching section prefixes when exact heading slug matches are not found.
9
+ - **JSON Root Keys without Leading Slash:** Automatically resolves JSON references missing a leading slash (e.g. `api_contracts.json#confirmationPageContracts`, `api_contracts.json#legalDocumentsContract`) by falling back to root property pointers and dot-path navigation.
10
+ - **Route Names Lookup:** Resolves named route references (e.g. `api_contracts.json#coaches.payment-qr`, `company.coaches.payment-qr`) across `routingPolicy.playerRoutes`, `venueScopedRoutes`, and other route collections.
11
+ - **Mail Events by Trigger:** Resolves mail event references by trigger name (e.g. `api_contracts.json#Coach Lesson Booked`, `Coach Payment Proof Uploaded`).
12
+ - **Database Schema Tables:** Resolves table references directly by name in `relevantTables` and `tables` collections (e.g. `db_schema.json#legal_documents`, `db_schema.json#coach_bookings`, `db_schema.json#sports`).
13
+
3
14
  ## 2.4.0
4
15
 
5
16
  ### Added
@@ -16,9 +16,7 @@ function cap(text, max) {
16
16
  return { text, truncated: false };
17
17
  return { text: text.slice(0, Math.max(0, max)) + TRUNCATION_MARKER, truncated: true };
18
18
  }
19
- function markdownSection(content, anchor) {
20
- const wanted = decodeURIComponent(anchor).toLowerCase();
21
- const lines = content.split(/\r?\n/);
19
+ function findSection(lines, predicate) {
22
20
  const seen = new Map();
23
21
  let inFence = false;
24
22
  let start = -1;
@@ -40,17 +38,41 @@ function markdownSection(content, anchor) {
40
38
  return lines.slice(start, i).join('\n').trimEnd();
41
39
  continue;
42
40
  }
43
- const base = slugifyHeading(m[2]);
41
+ const headingText = m[2];
42
+ const base = slugifyHeading(headingText);
44
43
  const count = seen.get(base) ?? 0;
45
44
  seen.set(base, count + 1);
46
45
  const slug = count === 0 ? base : `${base}-${count}`;
47
- if (slug === wanted) {
46
+ if (predicate(slug, headingText)) {
48
47
  start = i;
49
48
  level = hLevel;
50
49
  }
51
50
  }
52
51
  return start >= 0 ? lines.slice(start).join('\n').trimEnd() : null;
53
52
  }
53
+ function markdownSection(content, anchor) {
54
+ const wanted = decodeURIComponent(anchor).trim().toLowerCase();
55
+ const lines = content.split(/\r?\n/);
56
+ // 1. Exact slug match (standard markdown anchor)
57
+ const exact = findSection(lines, (slug) => slug === wanted);
58
+ if (exact !== null)
59
+ return exact;
60
+ // 2. Prefix / section number match (e.g. "6" or "15.1" matching "## 6. Coach..." or "### 15.1 Shared Pieces")
61
+ const prefixMatch = findSection(lines, (_slug, headingText) => {
62
+ const raw = headingText.trim().toLowerCase();
63
+ if (raw === wanted)
64
+ return true;
65
+ if (raw.startsWith(`${wanted}.`) || raw.startsWith(`${wanted} `) || raw.startsWith(`${wanted}:`))
66
+ return true;
67
+ const base = slugifyHeading(headingText);
68
+ if (base.startsWith(`${wanted}-`) || base === wanted)
69
+ return true;
70
+ return false;
71
+ });
72
+ if (prefixMatch !== null)
73
+ return prefixMatch;
74
+ return null;
75
+ }
54
76
  function jsonPointerValue(doc, pointer) {
55
77
  if (pointer === '')
56
78
  return { found: true, value: doc };
@@ -80,6 +102,73 @@ function jsonPointerValue(doc, pointer) {
80
102
  }
81
103
  return { found: true, value: current };
82
104
  }
105
+ function getByDotPath(obj, pathStr) {
106
+ const parts = pathStr.split('.');
107
+ let cur = obj;
108
+ for (const p of parts) {
109
+ if (cur && typeof cur === 'object' && Object.prototype.hasOwnProperty.call(cur, p)) {
110
+ cur = cur[p];
111
+ }
112
+ else {
113
+ return { found: false };
114
+ }
115
+ }
116
+ return { found: true, value: cur };
117
+ }
118
+ function findEntityInDoc(doc, ref) {
119
+ if (!doc || typeof doc !== 'object')
120
+ return { found: false };
121
+ // 1. Direct table match in relevantTables or tables
122
+ const tables = Array.isArray(doc.relevantTables) ? doc.relevantTables : (Array.isArray(doc.tables) ? doc.tables : null);
123
+ if (tables) {
124
+ const table = tables.find((t) => t && (t.name === ref || t.tableName === ref || t.id === ref));
125
+ if (table)
126
+ return { found: true, value: table };
127
+ }
128
+ if (doc.tables && typeof doc.tables === 'object' && !Array.isArray(doc.tables) && Object.prototype.hasOwnProperty.call(doc.tables, ref)) {
129
+ return { found: true, value: doc.tables[ref] };
130
+ }
131
+ // 2. Mail events trigger match
132
+ if (Array.isArray(doc.mailEvents)) {
133
+ const event = doc.mailEvents.find((m) => m && (m.trigger === ref || m.endpoint === ref || m.id === ref));
134
+ if (event)
135
+ return { found: true, value: event };
136
+ }
137
+ // 3. Routes match in routingPolicy or routes
138
+ const routeLists = [];
139
+ if (doc.routingPolicy && typeof doc.routingPolicy === 'object') {
140
+ if (Array.isArray(doc.routingPolicy.playerRoutes))
141
+ routeLists.push(doc.routingPolicy.playerRoutes);
142
+ if (Array.isArray(doc.routingPolicy.venueScopedRoutes))
143
+ routeLists.push(doc.routingPolicy.venueScopedRoutes);
144
+ if (Array.isArray(doc.routingPolicy.adminRoutes))
145
+ routeLists.push(doc.routingPolicy.adminRoutes);
146
+ if (Array.isArray(doc.routingPolicy.routes))
147
+ routeLists.push(doc.routingPolicy.routes);
148
+ }
149
+ if (Array.isArray(doc.routes))
150
+ routeLists.push(doc.routes);
151
+ for (const list of routeLists) {
152
+ const route = list.find((r) => r && (r.name === ref ||
153
+ r.id === ref ||
154
+ (Array.isArray(r.routeNames) && r.routeNames.includes(ref))));
155
+ if (route)
156
+ return { found: true, value: route };
157
+ }
158
+ // 4. Contract list items (e.g. confirmationPageContracts array)
159
+ for (const key of Object.keys(doc)) {
160
+ const val = doc[key];
161
+ if (Array.isArray(val)) {
162
+ const match = val.find((item) => item && typeof item === 'object' && (item.name === ref ||
163
+ item.id === ref ||
164
+ item.trigger === ref ||
165
+ (Array.isArray(item.routeNames) && item.routeNames.includes(ref))));
166
+ if (match)
167
+ return { found: true, value: match };
168
+ }
169
+ }
170
+ return { found: false };
171
+ }
83
172
  function joinUrl(base, p) {
84
173
  return typeof base === 'string' ? base.replace(/\/+$/, '') + p : p;
85
174
  }
@@ -115,9 +204,32 @@ function endpointValue(doc, fragment) {
115
204
  return byId ? { found: true, value: byId } : { found: false };
116
205
  }
117
206
  function jsonRefValue(doc, fragment) {
118
- if (fragment === '' || fragment.startsWith('/'))
207
+ if (fragment === '')
119
208
  return jsonPointerValue(doc, fragment);
120
- return endpointValue(doc, fragment);
209
+ // 1. Direct JSON pointer starting with /
210
+ if (fragment.startsWith('/')) {
211
+ const ptr = jsonPointerValue(doc, fragment);
212
+ if (ptr.found)
213
+ return ptr;
214
+ }
215
+ else {
216
+ // 2. JSON pointer or top-level property without leading slash (e.g. #confirmationPageContracts)
217
+ const ptr = jsonPointerValue(doc, `/${fragment}`);
218
+ if (ptr.found)
219
+ return ptr;
220
+ // 3. Dot-path property navigation (e.g. #contract.section)
221
+ if (fragment.includes('.')) {
222
+ const dot = getByDotPath(doc, fragment);
223
+ if (dot.found)
224
+ return dot;
225
+ }
226
+ }
227
+ // 4. Endpoint lookup by method + path or id
228
+ const endpoint = endpointValue(doc, fragment);
229
+ if (endpoint.found)
230
+ return endpoint;
231
+ // 5. Named entity lookup: route names, mail event triggers, DB schema tables
232
+ return findEntityInDoc(doc, fragment);
121
233
  }
122
234
  /**
123
235
  * Resolves specRefs ("ui_specs.md#anchor", "api_contracts.json#/paths/~1x",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@njeybe/nativ",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Role-based multi-agent workflow harness: Claude Code by default, other AI agents pluggable",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nativ",
3
3
  "displayName": "nativ",
4
- "version": "2.4.0",
4
+ "version": "2.4.1",
5
5
  "description": "Role-based multi-agent workflow for Claude Code: architect, worker, verifier and explorer agents, task-scope enforcement, and the nativ MCP server.",
6
6
  "author": {
7
7
  "name": "JB Natividad"