drafted 1.18.3 → 1.18.4
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/mcp/server.mjs +20 -5
- package/mcp/test-search-terms.mjs +42 -0
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -279,6 +279,18 @@ export function stripUrlOrigin(p) {
|
|
|
279
279
|
// segment 0, so `write /skills/<slug>/README.md` silently rewrote SKILL.md — and
|
|
280
280
|
// the create-time README gate then looked unsatisfiable from this tool.
|
|
281
281
|
// Pure + exported (asserted in mcp/test-skill-paths.mjs).
|
|
282
|
+
// Search matches every TERM in the query, not the raw phrase. A whole-string
|
|
283
|
+
// substring test means a natural multi-word search ("tool surface parity MCP")
|
|
284
|
+
// never hits a project whose name/slug/description contains each of those words
|
|
285
|
+
// — while the single word "parity" does. That reads as "no such project" for a
|
|
286
|
+
// project that is right there. Pure + exported (asserted in mcp/test-search-terms.mjs).
|
|
287
|
+
export function matchesAllTerms(parts, query) {
|
|
288
|
+
const terms = String(query || '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
289
|
+
if (!terms.length) return false;
|
|
290
|
+
const hay = parts.map((v) => String(v || '')).join(' ').toLowerCase();
|
|
291
|
+
return terms.every((t) => hay.includes(t));
|
|
292
|
+
}
|
|
293
|
+
|
|
282
294
|
export function splitSkillPath(p) {
|
|
283
295
|
const rest = /^\/skills\/?$/.test(p || '') ? '' : String(p || '').replace(/^\/skills\/?/, '');
|
|
284
296
|
const segs = rest.split('/').filter(Boolean);
|
|
@@ -3094,10 +3106,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3094
3106
|
// addressable paths instead of the whole inventory.
|
|
3095
3107
|
const q = String(query || '').trim();
|
|
3096
3108
|
if (!q) return err(new Error('search requires a query — e.g. fs(search, path="/projects", query="<terms>")'));
|
|
3097
|
-
const
|
|
3098
|
-
const hitProjects = rows.filter(x =>
|
|
3099
|
-
[x.name, x.slug, x.description].some(v => String(v || '').toLowerCase().includes(needle))
|
|
3100
|
-
);
|
|
3109
|
+
const hitProjects = rows.filter(x => matchesAllTerms([x.name, x.slug, x.description], q));
|
|
3101
3110
|
let frames = [];
|
|
3102
3111
|
let frameError = null;
|
|
3103
3112
|
try {
|
|
@@ -3124,6 +3133,12 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3124
3133
|
out.push(`\nFrames matching "${q}" (${frames.length}${frames.length > 25 ? ', first 25' : ''}):\n${lines.join('\n')}`);
|
|
3125
3134
|
} else if (frameError) {
|
|
3126
3135
|
out.push(`\n(frame search unavailable: ${frameError} — project matches above are complete, frame matches were not checked)`);
|
|
3136
|
+
} else {
|
|
3137
|
+
// Say the frame leg ran and found nothing. Printing only the project leg
|
|
3138
|
+
// leaves "frames were never searched" and "no frame matched" looking the
|
|
3139
|
+
// same, and frame search matches LABELS only — so a term that lives in
|
|
3140
|
+
// frame content is a miss, not an absence.
|
|
3141
|
+
out.push(`\nNo frame label matches "${q}" (frame search matches labels, not frame content).`);
|
|
3127
3142
|
}
|
|
3128
3143
|
return ok(out.join('\n'));
|
|
3129
3144
|
}
|
|
@@ -3331,7 +3346,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3331
3346
|
// Express parse projectId as an ARRAY and the scope match nothing.
|
|
3332
3347
|
const res = await api('GET', `/api/search?q=${encodeURIComponent(q)}`);
|
|
3333
3348
|
const frames = Array.isArray(res) ? res : (res?.results || []);
|
|
3334
|
-
if (!frames.length) return ok(`No frames matching "${q}".`);
|
|
3349
|
+
if (!frames.length) return ok(`No frames matching "${q}" (frame search matches labels, not frame content).`);
|
|
3335
3350
|
const lines = frames.slice(0, 50).map(f =>
|
|
3336
3351
|
` /o/${f.orgSlug || f.orgId}/projects/${f.projectSlug || f.projectId}/${f.layer}/${f.lane || ''}/${f.label}`.replace(/\/\//g, '/')
|
|
3337
3352
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Regression for fs(search) term matching on /projects.
|
|
2
|
+
// Run: `node mcp/test-search-terms.mjs`. No framework — asserts the pure core.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: search used to substring-test the WHOLE query against each
|
|
5
|
+
// field, so `query="tool surface parity MCP Flow"` missed a project whose
|
|
6
|
+
// description says "tool surface" and "parity" — while `query="parity"` hit it.
|
|
7
|
+
// An agent reads that miss as "the project does not exist" and stops looking.
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { matchesAllTerms } from './server.mjs';
|
|
10
|
+
|
|
11
|
+
const proj = [
|
|
12
|
+
'Flow AI MCP Server Feature Card',
|
|
13
|
+
'flow-ai-mcp-server-feature-card',
|
|
14
|
+
'Covers the BEO-2527 parity guard for the agent tool surface.',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// The reported failure: a natural multi-word query spanning name AND description.
|
|
18
|
+
assert.equal(matchesAllTerms(proj, 'tool surface parity MCP Flow'), true);
|
|
19
|
+
// The single word that did work must keep working.
|
|
20
|
+
assert.equal(matchesAllTerms(proj, 'parity'), true);
|
|
21
|
+
// Terms match across fields and across a hyphenated slug.
|
|
22
|
+
assert.equal(matchesAllTerms(proj, 'flow feature card'), true);
|
|
23
|
+
assert.equal(matchesAllTerms(proj, 'BEO-2527'), true);
|
|
24
|
+
// Case and surrounding whitespace are irrelevant.
|
|
25
|
+
assert.equal(matchesAllTerms(proj, ' PARITY Guard '), true);
|
|
26
|
+
|
|
27
|
+
// AND, not OR: one unmatched term is a miss. An OR would return most of the org.
|
|
28
|
+
assert.equal(matchesAllTerms(proj, 'parity kubernetes'), false);
|
|
29
|
+
assert.equal(matchesAllTerms(proj, 'kubernetes'), false);
|
|
30
|
+
|
|
31
|
+
// An empty query never matches — the caller rejects it before this, and
|
|
32
|
+
// "every term" over zero terms would otherwise be vacuously true for every row.
|
|
33
|
+
assert.equal(matchesAllTerms(proj, ''), false);
|
|
34
|
+
assert.equal(matchesAllTerms(proj, ' '), false);
|
|
35
|
+
assert.equal(matchesAllTerms(proj, undefined), false);
|
|
36
|
+
|
|
37
|
+
// Null/undefined fields don't throw or match a stray "null"/"undefined" term.
|
|
38
|
+
assert.equal(matchesAllTerms([null, undefined, 'alpha'], 'alpha'), true);
|
|
39
|
+
assert.equal(matchesAllTerms([null, undefined, 'alpha'], 'null'), false);
|
|
40
|
+
|
|
41
|
+
console.log('search term matching ok — every term must hit, across fields, never vacuously');
|
|
42
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.4",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|