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 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 needle = q.toLowerCase();
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",
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": [