tripkit 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -5
- package/README.md +4 -1
- package/agent/AGENT-SKILL.md +114 -68
- package/examples/new-england-fall-2026.yaml +471 -0
- package/examples/nyc-long-weekend-2026.yaml +283 -0
- package/examples/oregon-spring-2026.yaml +2 -0
- package/examples/southwest-parks-2026.yaml +463 -0
- package/package.json +6 -2
- package/renderers/html/tripkit-renderer.html +74 -15
- package/schema/tripkit.schema.yaml +7 -0
- package/scripts/check-skill-coverage.js +66 -0
- package/validate.js +67 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Coverage check: every renderer-meaningful schema field must be mentioned
|
|
4
|
+
* by name in the agent skill. Prevents drift when fields are added.
|
|
5
|
+
*
|
|
6
|
+
* Run: node scripts/check-skill-coverage.js
|
|
7
|
+
* (also wired into `npm test`)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
const skill = fs.readFileSync(path.join(ROOT, 'agent', 'AGENT-SKILL.md'), 'utf8');
|
|
15
|
+
|
|
16
|
+
// Hand-curated list of fields the agent must know about. When you add a renderer-
|
|
17
|
+
// meaningful field, add it here and to AGENT-SKILL.md in the same change.
|
|
18
|
+
const REQUIRED_FIELDS = [
|
|
19
|
+
// trip
|
|
20
|
+
'trip.title', 'trip.dates', 'trip.total_days', 'trip.total_stops',
|
|
21
|
+
'trip.origin', 'trip.origin_lat', 'trip.origin_lng',
|
|
22
|
+
'trip.destination_lat', 'trip.destination_lng',
|
|
23
|
+
// days
|
|
24
|
+
'days[].number', 'days[].title', 'days[].date', 'days[].status', 'days[].color',
|
|
25
|
+
'days[].summary', 'days[].weather', 'days[].meals', 'days[].lodging',
|
|
26
|
+
'days[].alerts', 'days[].tips', 'days[].stops',
|
|
27
|
+
// stops
|
|
28
|
+
'stops[].name', 'stops[].lat', 'stops[].lng', 'stops[].type', 'stops[].label',
|
|
29
|
+
'stops[].description', 'stops[].kid_friendly', 'stops[].reservation_required',
|
|
30
|
+
'stops[].navigate_url',
|
|
31
|
+
// lodging
|
|
32
|
+
'lodging.name', 'lodging.lat', 'lodging.lng', 'lodging.confirmation', 'lodging.booked',
|
|
33
|
+
// routes
|
|
34
|
+
'routes[]',
|
|
35
|
+
// theme
|
|
36
|
+
'theme.font_family', 'theme.accent_color', 'theme.map_style', 'theme.dark_mode',
|
|
37
|
+
'theme.hotel_label',
|
|
38
|
+
// agent_context
|
|
39
|
+
'agent_context.preferences', 'agent_context.constraints', 'agent_context.iteration_log',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Each entry above resolves to one or more "tokens" the skill must contain.
|
|
43
|
+
// Strip the path prefix so the lookup is forgiving across formats like
|
|
44
|
+
// `trip.origin_lat`, ``trip.origin_lat``, or just `origin_lat`.
|
|
45
|
+
function lookupTokens(field) {
|
|
46
|
+
const leaf = field.replace(/^.*\./, '').replace(/\[\]$/, '');
|
|
47
|
+
return [field, leaf];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const missing = [];
|
|
51
|
+
for (const field of REQUIRED_FIELDS) {
|
|
52
|
+
const tokens = lookupTokens(field);
|
|
53
|
+
const found = tokens.some(t => skill.includes(t));
|
|
54
|
+
if (!found) missing.push(field);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (missing.length === 0) {
|
|
58
|
+
console.log(`✓ AGENT-SKILL.md covers all ${REQUIRED_FIELDS.length} required fields`);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.error(`✖ AGENT-SKILL.md is missing references to ${missing.length} required field(s):`);
|
|
63
|
+
for (const f of missing) console.error(` - ${f}`);
|
|
64
|
+
console.error('');
|
|
65
|
+
console.error('Either add a mention to agent/AGENT-SKILL.md, or remove it from REQUIRED_FIELDS in this script if the field is no longer renderer-meaningful.');
|
|
66
|
+
process.exit(1);
|
package/validate.js
CHANGED
|
@@ -13,6 +13,40 @@ const HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
|
13
13
|
function isFiniteNumber(n) { return typeof n === 'number' && Number.isFinite(n); }
|
|
14
14
|
function isNonEmptyString(s) { return typeof s === 'string' && s.trim().length > 0; }
|
|
15
15
|
|
|
16
|
+
function distanceMiles(lat1, lng1, lat2, lng2) {
|
|
17
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
18
|
+
const R = 3958.8;
|
|
19
|
+
const dLat = toRad(lat2 - lat1);
|
|
20
|
+
const dLng = toRad(lng2 - lng1);
|
|
21
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
|
|
22
|
+
return 2 * R * Math.asin(Math.sqrt(a));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Renderer auto-generates routes by stitching: prev-lodging → stops → today's-lodging.
|
|
26
|
+
// Pick the same anchor the renderer would use for each end of a day's route.
|
|
27
|
+
function dayEndAnchor(d) {
|
|
28
|
+
if (d.lodging && d.lodging.lat && d.lodging.lng && d.lodging.name !== 'Home') {
|
|
29
|
+
return [d.lodging.lat, d.lodging.lng, 'lodging'];
|
|
30
|
+
}
|
|
31
|
+
const stops = (d.stops || []).filter(s => isFiniteNumber(s.lat) && isFiniteNumber(s.lng));
|
|
32
|
+
if (stops.length > 0) {
|
|
33
|
+
const last = stops[stops.length - 1];
|
|
34
|
+
return [last.lat, last.lng, `stops[${stops.length - 1}]`];
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function dayStartAnchor(d) {
|
|
39
|
+
const stops = (d.stops || []).filter(s => isFiniteNumber(s.lat) && isFiniteNumber(s.lng));
|
|
40
|
+
if (stops.length > 0) return [stops[0].lat, stops[0].lng, 'stops[0]'];
|
|
41
|
+
if (d.lodging && d.lodging.lat && d.lodging.lng && d.lodging.name !== 'Home') {
|
|
42
|
+
return [d.lodging.lat, d.lodging.lng, 'lodging'];
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Threshold above which a single straight-line polyline segment looks visually disjointed.
|
|
48
|
+
const LONG_LEG_MILES = 250;
|
|
49
|
+
|
|
16
50
|
function validate(data) {
|
|
17
51
|
const errors = [];
|
|
18
52
|
const warnings = [];
|
|
@@ -36,6 +70,21 @@ function validate(data) {
|
|
|
36
70
|
if (trip.total_stops != null && !Number.isInteger(trip.total_stops)) {
|
|
37
71
|
err('trip.total_stops', `must be an integer, got ${typeof trip.total_stops}`);
|
|
38
72
|
}
|
|
73
|
+
const checkLatLng = (latKey, lngKey) => {
|
|
74
|
+
const lat = trip[latKey];
|
|
75
|
+
const lng = trip[lngKey];
|
|
76
|
+
if (lat != null && (!isFiniteNumber(lat) || lat < -90 || lat > 90)) {
|
|
77
|
+
err(`trip.${latKey}`, `must be a number in [-90, 90]; got ${lat}`);
|
|
78
|
+
}
|
|
79
|
+
if (lng != null && (!isFiniteNumber(lng) || lng < -180 || lng > 180)) {
|
|
80
|
+
err(`trip.${lngKey}`, `must be a number in [-180, 180]; got ${lng}`);
|
|
81
|
+
}
|
|
82
|
+
if ((lat != null) !== (lng != null)) {
|
|
83
|
+
err(`trip.${latKey}`, `both ${latKey} and ${lngKey} must be set together`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
checkLatLng('origin_lat', 'origin_lng');
|
|
87
|
+
checkLatLng('destination_lat', 'destination_lng');
|
|
39
88
|
}
|
|
40
89
|
|
|
41
90
|
// --- days ---
|
|
@@ -115,6 +164,21 @@ function validate(data) {
|
|
|
115
164
|
warn('trip.total_stops', `declared ${trip.total_stops}, actual stops = ${actualStops}`);
|
|
116
165
|
}
|
|
117
166
|
|
|
167
|
+
// --- continuity check: warn on long inter-day jumps with no intermediate waypoint ---
|
|
168
|
+
if (data.routes == null) {
|
|
169
|
+
for (let i = 0; i + 1 < days.length; i++) {
|
|
170
|
+
const fromAnchor = dayEndAnchor(days[i]);
|
|
171
|
+
const toDay = days[i + 1];
|
|
172
|
+
const stops = (toDay.stops || []).filter(s => isFiniteNumber(s.lat) && isFiniteNumber(s.lng));
|
|
173
|
+
const toAnchor = stops.length > 0 ? [stops[0].lat, stops[0].lng] : dayStartAnchor(toDay);
|
|
174
|
+
if (!fromAnchor || !toAnchor) continue;
|
|
175
|
+
const miles = distanceMiles(fromAnchor[0], fromAnchor[1], toAnchor[0], toAnchor[1]);
|
|
176
|
+
if (miles > LONG_LEG_MILES) {
|
|
177
|
+
warn(`days[${i + 1}].stops`, `first stop is ${Math.round(miles)} mi from end of day ${i + 1} — auto-generated polyline will draw a long straight line. Add an intermediate stop or define a routes[] entry.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
118
182
|
// --- routes (optional) ---
|
|
119
183
|
if (data.routes != null) {
|
|
120
184
|
if (!Array.isArray(data.routes)) {
|
|
@@ -151,6 +215,9 @@ function validate(data) {
|
|
|
151
215
|
if (data.theme.map_style != null && !MAP_STYLES.has(data.theme.map_style)) {
|
|
152
216
|
err('theme.map_style', `must be one of ${[...MAP_STYLES].join(', ')}; got "${data.theme.map_style}"`);
|
|
153
217
|
}
|
|
218
|
+
if (data.theme.hotel_label != null && (typeof data.theme.hotel_label !== 'string' || data.theme.hotel_label.length === 0 || data.theme.hotel_label.length > 4)) {
|
|
219
|
+
err('theme.hotel_label', 'must be a string of 1–4 characters (used as the hotel marker label)');
|
|
220
|
+
}
|
|
154
221
|
}
|
|
155
222
|
|
|
156
223
|
return { errors, warnings };
|