@polycode-projects/seonix 0.10.7 → 0.10.10

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/bin/cli.mjs CHANGED
@@ -22,6 +22,10 @@
22
22
  // seonix cli index_repository '{"multi_root":"<abs-dir>"}' → estate discovery: every immediate
23
23
  // child directory carrying a .git (dir or file) is indexed as a repo, merged into
24
24
  // <multi_root>/.seonix/ (discovered/skipped counts logged to stderr)
25
+ // seonix cli index_repository --multi-root [path] → same, flag form — no JSON
26
+ // quoting needed; a bare `--multi-root` (no path) defaults to cwd, e.g. `cd <estate> &&
27
+ // npx seonix cli index_repository --multi-root`. `--repo-path [path]` and `--out-root
28
+ // <path>` (always needs a value) are the flag forms of the other two path args.
25
29
  // seonix cli sync '{"multi_root":"<abs-dir>"}' → incrementally re-index the estate: fingerprint
26
30
  // every child repo (git/gitparent/plain), re-extract ONLY the changed ones (per-repo cache in
27
31
  // .seonix/cache/), one union rebuild = exact full-re-index. Unchanged-everything = byte-stable
@@ -54,7 +58,7 @@
54
58
 
55
59
  import { dirname, join } from "node:path";
56
60
  import { existsSync } from "node:fs";
57
- import { readFile, writeFile } from "node:fs/promises";
61
+ import { readFile, writeFile, readdir } from "node:fs/promises";
58
62
  import { fileURLToPath } from "node:url";
59
63
  import { startServer } from "../src/server.mjs";
60
64
  import { dispatchTool, buildContextBundle, TOOLS } from "../src/server.mjs";
@@ -80,6 +84,32 @@ function parsePayload(payload) {
80
84
  catch { return null; }
81
85
  }
82
86
 
87
+ const INDEX_FLAG_KEYS = { "--multi-root": "multi_root", "--repo-path": "repo_path", "--out-root": "out_root" };
88
+
89
+ /** Flag-style alternative to index_repository's JSON payload — `--multi-root [path]` /
90
+ * `--repo-path [path]` (also `--flag=path`), so `npx seonix cli index_repository
91
+ * --multi-root` works without JSON-quoting a path. A bare `--multi-root`/`--repo-path`
92
+ * (no path following, or the payload ends there) defaults to cwd, matching the JSON form's
93
+ * own cwd-default convention. `--out-root` always needs an explicit value — there's no
94
+ * sensible cwd default for it. Returns an args object shaped like the JSON payload, or
95
+ * null if the first token isn't a recognised flag (falls back to JSON parsing). */
96
+ function parseIndexFlags(argv) {
97
+ const args = {};
98
+ for (let i = 0; i < argv.length; i++) {
99
+ const tok = argv[i];
100
+ const eq = tok.indexOf("=");
101
+ const flag = eq === -1 ? tok : tok.slice(0, eq);
102
+ const key = INDEX_FLAG_KEYS[flag];
103
+ if (!key) return null;
104
+ if (eq !== -1) { args[key] = tok.slice(eq + 1); continue; }
105
+ const next = argv[i + 1];
106
+ if (next !== undefined && !next.startsWith("--")) { args[key] = next; i++; }
107
+ else if (key === "multi_root" || key === "repo_path") { args[key] = process.cwd(); }
108
+ else return null;
109
+ }
110
+ return Object.keys(args).length ? args : null;
111
+ }
112
+
83
113
  /** Nearest ancestor of `startDir` (itself first) containing a .seonix/ directory, or null. */
84
114
  function findIndexRoot(startDir = process.cwd()) {
85
115
  let dir = startDir;
@@ -113,6 +143,36 @@ function defaultRepoPath(args, { mustExist = true } = {}) {
113
143
  return args;
114
144
  }
115
145
 
146
+ /** Single-mode index_repository against a directory that (a) isn't itself a git repo and
147
+ * (b) has ≥2 immediate child directories that ARE — the exact signature of an estate root
148
+ * mistaken for one flat repo (the `multi_root` docs' own warning: "repo_path would index it
149
+ * as one flat repo"). Silent consequence: git history extraction fails against the non-repo
150
+ * root (0 commits, no error the caller necessarily notices among the rest of the index
151
+ * output), module ids aren't per-repo-prefixed, and every sub-repo gets flattened together.
152
+ * Best-effort (readdir failure just means no warning, never a crash) — this only ADDS a
153
+ * warning in a case that previously silently mis-indexed, so it can't change any existing
154
+ * single-repo index's output. */
155
+ async function warnIfLooksLikeEstateRoot(repoPath) {
156
+ if (existsSync(join(repoPath, ".git"))) return;
157
+ let entries;
158
+ try { entries = await readdir(repoPath, { withFileTypes: true }); }
159
+ catch { return; }
160
+ let childRepoCount = 0;
161
+ for (const e of entries) {
162
+ if (e.isDirectory() && existsSync(join(repoPath, e.name, ".git"))) childRepoCount++;
163
+ if (childRepoCount >= 2) break;
164
+ }
165
+ if (childRepoCount >= 2) {
166
+ process.stderr.write(
167
+ `seonix: WARNING — ${repoPath} has no .git of its own, but multiple child directories do. ` +
168
+ "This looks like a multi-repo estate root, not a single repo — indexing it this way flattens " +
169
+ "every sub-repo together with NO git history (git log fails against a non-repo root) and no " +
170
+ "per-repo module-id prefixing. If that's not what you want, re-run with " +
171
+ `'{"multi_root":"${repoPath}"}' instead, which discovers each child repo and indexes it properly.\n`,
172
+ );
173
+ }
174
+ }
175
+
116
176
  // Hot tools (server.mjs's TOOLS) carry an inputSchema but no example value, so their
117
177
  // example-args + primary-arg-key aren't derivable the way COLD_TOOLS' are — hand-maintained
118
178
  // here, right next to COLD_TOOLS' import, so a new hot tool is a one-line addition in both places.
@@ -482,9 +542,13 @@ async function main() {
482
542
  // '{"repo_paths":[…],"out_root":"<abs>"}' (explicit n-repo merge) or
483
543
  // '{"multi_root":"<abs>"}' (discover child repos, merge into <multi_root>/.seonix/).
484
544
  if (sub === "index_repository") {
485
- const args = parsePayload(payload);
545
+ const isFlagForm = payload !== undefined && payload.startsWith("--");
546
+ const args = isFlagForm ? parseIndexFlags(process.argv.slice(4)) : parsePayload(payload);
486
547
  if (args === null) {
487
- process.stderr.write("seonix: index_repository expects a JSON arg, e.g. '{\"repo_path\":\"/abs\"}'\n");
548
+ process.stderr.write(
549
+ "seonix: index_repository expects a JSON arg, e.g. '{\"repo_path\":\"/abs\"}', " +
550
+ "or a flag: --multi-root [path], --repo-path [path], --out-root <path>\n",
551
+ );
488
552
  process.exit(2);
489
553
  }
490
554
  const given = ["repo_path", "repo_paths", "multi_root"].filter((k) => args[k] !== undefined);
@@ -557,6 +621,7 @@ async function main() {
557
621
  };
558
622
 
559
623
  if (args.repo_path) {
624
+ await warnIfLooksLikeEstateRoot(args.repo_path);
560
625
  const { graphFile, counts, summary } = await indexRepository(args.repo_path, { ignores: args.ignores !== false, historyDepth, extraIgnores, languages: tomlLanguages, interfaces: tomlInterfaces });
561
626
  emitSummary(graphFile, counts, "");
562
627
  // A1: the human summary MD goes to STDOUT (bench ignores index stdout — safe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/seonix",
3
- "version": "0.10.7",
3
+ "version": "0.10.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.",
@@ -65,11 +65,12 @@
65
65
  "verify:query": "node scripts/verify-query.mjs",
66
66
  "verify:review": "node scripts/verify-review.mjs",
67
67
  "verify:loop": "node scripts/verify-loop.mjs",
68
+ "verify:shipped": "node scripts/verify-shipped.mjs",
68
69
  "refs": "node scripts/fetch-references.mjs",
69
70
  "refs:embeddings": "node scripts/fetch-embeddings.mjs",
70
71
  "audit": "npm audit --audit-level=high",
71
72
  "audit:fix": "npm audit fix",
72
- "test:root": "node --test \"test/**/*.test.mjs\"",
73
+ "test:root": "GIT_CONFIG_GLOBAL=\"$PWD/test/fixtures/global-gitconfig\" node --test \"test/**/*.test.mjs\"",
73
74
  "test": "npm run test:root && npm run chrono:test",
74
75
  "chrono:build": "node chronograph/build.mjs",
75
76
  "chrono:serve": "node chronograph/serve.mjs",
@@ -87,7 +88,7 @@
87
88
  },
88
89
  "dependencies": {
89
90
  "@modelcontextprotocol/sdk": "^1.29.0",
90
- "@polycode-projects/the-mechanical-code-talker": "^0.9.5",
91
+ "@polycode-projects/the-mechanical-code-talker": "^0.9.10",
91
92
  "cytoscape": "^3.30.0",
92
93
  "smol-toml": "^1.7.0",
93
94
  "tree-sitter": "^0.21.1",
@@ -108,6 +108,9 @@
108
108
  if (/\b(inherit|subclass|extend|specializ)/.test(pred)) return "inherits";
109
109
  return null;
110
110
  }
111
+ function normPath(s) {
112
+ return String(s || "").trim().toLowerCase().replace(/^\.\//, "").replace(/^\//, "");
113
+ }
111
114
  function siteOf(ind) {
112
115
  const a = (ind?.attributes || []).find((x) => x.key === "site");
113
116
  if (!a) return null;
@@ -413,7 +416,15 @@
413
416
  "tends to change together with",
414
417
  "tend to change together with",
415
418
  "moves together with",
416
- "move together with"
419
+ "move together with",
420
+ // Track-1 trio (temporal lever): the bare "changed/change together with" form
421
+ // (no "tends to"/"tend to" prefix) was missing outright — "which modules
422
+ // changed together with X" fell through to the "touch(ed)" verb instead (a
423
+ // Commit->Module kind, structurally unable to match a Module subject), always
424
+ // producing a confidently-empty answer regardless of real cochange data.
425
+ "changed together with",
426
+ "change together with",
427
+ "changes together with"
417
428
  ]
418
429
  },
419
430
  reexports: {
@@ -672,7 +683,9 @@
672
683
  "quickly",
673
684
  "real quick",
674
685
  "kinda",
675
- "sorta"
686
+ "sorta",
687
+ "btw",
688
+ "by the way"
676
689
  ]);
677
690
  var CONTEXT_PRONOUNS = Object.freeze(["this", "it", "that", "here", "this one", "that one"]);
678
691
  var NEGATION_FRAMES = Object.freeze([
@@ -980,6 +993,7 @@
980
993
  return LISTING_TAIL_KINDS.has((words[words.length - 1] || "").toLowerCase());
981
994
  };
982
995
  var GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy)(?:\s+there)?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
996
+ var THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(?:\s+(?:so\s+much|a\s+lot|very\s+much|a\s+bunch))?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
983
997
  var MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(?:[,\s]+please)?\??$/i;
984
998
  var SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
985
999
  function applyPreambleFrames(text) {
@@ -988,6 +1002,8 @@
988
1002
  const before = q;
989
1003
  let m = q.match(GREETING_PREAMBLE_RE);
990
1004
  if (m) q = m[1].trim();
1005
+ m = q.match(THANKS_PREAMBLE_RE);
1006
+ if (m) q = m[1].trim();
991
1007
  m = q.match(MODAL_WRAPPER_RE);
992
1008
  if (m) q = m[1].trim();
993
1009
  m = q.match(SHOW_GIVE_ME_RE);
@@ -1001,6 +1017,55 @@
1001
1017
  }
1002
1018
  return q;
1003
1019
  }
1020
+ var SUBORDINATION_FRAMES_RE = /^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
1021
+ function applySubordinationFrames(text) {
1022
+ let q = String(text || "");
1023
+ for (let pass = 0; pass < 3; pass++) {
1024
+ const m = q.match(SUBORDINATION_FRAMES_RE);
1025
+ if (!m) break;
1026
+ q = m[1].trim();
1027
+ }
1028
+ return q;
1029
+ }
1030
+ var CONDITIONAL_VERB_GERUND = Object.freeze({
1031
+ imports: "importing",
1032
+ calls: "calling",
1033
+ touches: "touching",
1034
+ tests: "testing",
1035
+ exports: "exporting",
1036
+ contains: "containing",
1037
+ defines: "defining",
1038
+ uses: "using",
1039
+ "inherits from": "inheriting from"
1040
+ });
1041
+ var CONDITIONAL_KIND_PLURAL = Object.freeze({
1042
+ module: "modules",
1043
+ class: "classes",
1044
+ function: "functions",
1045
+ method: "methods",
1046
+ attribute: "attributes",
1047
+ variable: "variables",
1048
+ commit: "commits",
1049
+ file: "files"
1050
+ });
1051
+ var CONDITIONAL_QUALIFIER_SRC = "public|private|protected|static|abstract|constant|re-?exported|exported|tested|covered|untested|uncovered";
1052
+ var CONDITIONAL_QUALIFIER_RE = new RegExp(
1053
+ "^if\\s+(?:a|an|the)?\\s*(" + Object.keys(CONDITIONAL_KIND_PLURAL).join("|") + ")\\s+(" + Object.keys(CONDITIONAL_VERB_GERUND).join("|") + ")\\s+(.+?),\\s*(?:is|are)\\s+(?:it|that|they|this)\\s+(" + CONDITIONAL_QUALIFIER_SRC + ")\\??$",
1054
+ "i"
1055
+ );
1056
+ var COUNTERFACTUAL_RE = /^if\s+(.+?)\s+(?:were|was)\s+(?:deleted|removed),?\s*what\s+(?:would|might|could)\s+(?:break|fail|be\s+affected)\??$/i;
1057
+ function applyConditionalFrames(text) {
1058
+ const q = String(text || "");
1059
+ const qual = q.match(CONDITIONAL_QUALIFIER_RE);
1060
+ if (qual) {
1061
+ const kind = CONDITIONAL_KIND_PLURAL[qual[1].toLowerCase()];
1062
+ const gerund = CONDITIONAL_VERB_GERUND[qual[2].toLowerCase()];
1063
+ return `${kind} ${gerund} ${qual[3].trim()} and ${qual[4].toLowerCase()}`;
1064
+ }
1065
+ const cf = q.match(COUNTERFACTUAL_RE);
1066
+ if (cf) return `which modules transitively import ${cf[1].trim()}`;
1067
+ return q;
1068
+ }
1004
1069
  function normalizeQuery(text) {
1005
1070
  let q = String(text || "");
1006
1071
  q = q.replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
@@ -1008,6 +1073,8 @@
1008
1073
  q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
1009
1074
  q = q.replace(G_DROP, "$1ing");
1010
1075
  q = applyPreambleFrames(q);
1076
+ q = applySubordinationFrames(q);
1077
+ q = applyConditionalFrames(q);
1011
1078
  if (FILLER_WORDS.length) {
1012
1079
  const fillerRe = new RegExp(
1013
1080
  "\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
@@ -1173,7 +1240,33 @@
1173
1240
  // temporal filler in when-questions ("when was X last touched") — a symbol
1174
1241
  // literally named "last" would be the accepted residual cost, same trade as
1175
1242
  // every other stopword.
1176
- "last"
1243
+ "last",
1244
+ // frequency-adverb filler ("what does X usually change together with", "what does
1245
+ // X typically call") — found live: "usually" glued onto the object term instead of
1246
+ // being stripped, corrupting resolution ("src/core/store.mjs usually" instead of
1247
+ // the module alone). Same trade as every other stopword: a symbol literally named
1248
+ // "usually" would be the accepted residual cost.
1249
+ "usually",
1250
+ "typically",
1251
+ "generally",
1252
+ "normally",
1253
+ "often",
1254
+ "commonly",
1255
+ "mostly",
1256
+ // modal auxiliaries ("what SHOULD i look at first") — found live: with no modal in
1257
+ // this set, "should" reached the cascade's bounded fuzzy-correction step and landed
1258
+ // within edit distance of the unrelated closed-vocab word "hold" ("defines" synonym,
1259
+ // ask-vocab.mjs), corrupting the whole query into "what hold i at". Same trade as
1260
+ // every other stopword: a symbol literally named "should" would be the accepted
1261
+ // residual cost.
1262
+ "should",
1263
+ "would",
1264
+ "could",
1265
+ "can",
1266
+ "will",
1267
+ "shall",
1268
+ "might",
1269
+ "must"
1177
1270
  ]);
1178
1271
  var splitWords = (text) => String(text).replace(/\?+\s*$/, "").replace(/,/g, " ").split(/\s+/).filter(Boolean);
1179
1272
  var wordsOf = (arr) => arr.flatMap((p) => String(p).toLowerCase().split(" "));
@@ -1412,6 +1505,15 @@
1412
1505
  if (hit2) for (let i = hit2.start; i < hit2.end; i += 1) consumed.add(i);
1413
1506
  };
1414
1507
  mark(verbHit);
1508
+ for (const [phrase, kind2] of Object.entries(VERB_TO_KIND)) {
1509
+ if (kind2 !== verbHit.kind) continue;
1510
+ const pWords = phrase.split(" ");
1511
+ const start = verbHit.end;
1512
+ if (pWords.every((w, j) => canonWords[start + j] === w)) {
1513
+ mark({ start, end: start + pWords.length });
1514
+ break;
1515
+ }
1516
+ }
1415
1517
  const entityHit = findPhrase(canonWords, ENTITY_TO_TYPE, consumed);
1416
1518
  mark(entityHit);
1417
1519
  const modifierHit = findPhrase(canonWords, MODIFIER_TO_KIND, consumed);
@@ -2116,12 +2218,21 @@
2116
2218
  }
2117
2219
 
2118
2220
  // node_modules/@polycode-projects/the-mechanical-code-talker/src/ask.mjs
2221
+ var askEdgesOfKindCache = /* @__PURE__ */ new WeakMap();
2119
2222
  function edgesOfKind(graph, kind) {
2223
+ let byKind = askEdgesOfKindCache.get(graph);
2224
+ if (!byKind) {
2225
+ byKind = /* @__PURE__ */ new Map();
2226
+ askEdgesOfKindCache.set(graph, byKind);
2227
+ }
2228
+ const cached3 = byKind.get(kind);
2229
+ if (cached3) return cached3;
2120
2230
  const out = [];
2121
2231
  for (const g of graph.relations) {
2122
2232
  if (relationKind(g) !== kind) continue;
2123
2233
  for (const e of g.edges) out.push(e);
2124
2234
  }
2235
+ byKind.set(kind, out);
2125
2236
  return out;
2126
2237
  }
2127
2238
  var SYMBOL_GRAIN_SIBLING = { calls: "callsSymbol", touches: "touchesSymbol" };
@@ -2146,7 +2257,7 @@
2146
2257
  const [s, p] = PLURAL_FORMS[entityType] || ["result", "results"];
2147
2258
  return n === 1 ? s : p;
2148
2259
  }
2149
- var REVERSE_MISS_VERB = { cochange: "cochanges" };
2260
+ var REVERSE_MISS_VERB = { cochange: "cochanges", reexports: "export" };
2150
2261
  function verbFor(kind) {
2151
2262
  return REVERSE_MISS_VERB[kind] || kind;
2152
2263
  }
@@ -2180,7 +2291,7 @@
2180
2291
  function parseComposite(text, nlp) {
2181
2292
  const w = splitWords(text);
2182
2293
  const lc = w.map((x) => x.toLowerCase());
2183
- return parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseTemporal(w, lc, nlp, 0) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parseRelationalOrQualified(w, lc, nlp, 0);
2294
+ return parseNegation(text, nlp, 0) || parseForwardNegation(w, lc, nlp) || parseTemporal(w, lc, nlp, 0) || parseAnaphora(w, lc, nlp) || parseAggregate(w, lc, nlp) || parseSuperlative(w, lc, nlp) || parseFind(w, lc, nlp, 0) || parseList(w, lc, nlp, 0) || parseNested(w, lc, nlp, 0) || parseRelationalOrQualified(w, lc, nlp, 0);
2184
2295
  }
2185
2296
  function complementAst(entityType, diffAtom) {
2186
2297
  return {
@@ -2509,7 +2620,84 @@
2509
2620
  }
2510
2621
  return { node: "superlative", entityType, metric, metricNoun, extreme: ext };
2511
2622
  }
2623
+ var FIND_LINKERS = /* @__PURE__ */ new Set(["called", "named", "about", "like", "containing", "matching", "with"]);
2624
+ function parseFind(w, lc, nlp, depth) {
2625
+ if (lc[0] !== "find") return null;
2626
+ let i = 1;
2627
+ while (i < lc.length && LIST_SKIP.has(lc[i])) i += 1;
2628
+ if (i >= lc.length) return null;
2629
+ const leadNoun = entityNoun(lc[i]);
2630
+ if (leadNoun && !leadNoun.placeholder && leadNoun.entityType !== "Change" && i + 1 < lc.length && FIND_LINKERS.has(lc[i + 1])) {
2631
+ const term = w.slice(i + 2).join(" ").trim();
2632
+ if (term) return { node: "find", entityType: leadNoun.entityType, term };
2633
+ }
2634
+ const lastNoun = entityNoun(lc[lc.length - 1]);
2635
+ if (lastNoun && !lastNoun.placeholder && lastNoun.entityType !== "Change") {
2636
+ const term = w.slice(i, lc.length - 1).join(" ").trim();
2637
+ if (term) return { node: "find", entityType: lastNoun.entityType, term };
2638
+ }
2639
+ if (lc.some((t) => RELATIVE_PRONOUNS.includes(t))) return null;
2640
+ if (i === lc.length - 1 && /^[a-z]+$/.test(lc[i]) && !VERB_TO_KIND[lc[i]] && !PLACEHOLDER_NOUNS.includes(lc[i])) {
2641
+ return { node: "miss", reason: `"${lc[i]}" isn't a listable kind \u2014 try ${LISTABLE_KINDS}` };
2642
+ }
2643
+ return null;
2644
+ }
2645
+ function parseFindPredicateHead(w, lc) {
2646
+ if (lc[0] !== "find") return null;
2647
+ let i = 1;
2648
+ while (i < lc.length && LIST_SKIP.has(lc[i])) i += 1;
2649
+ let r = -1;
2650
+ for (let k = i + 1; k < lc.length; k += 1) {
2651
+ if (RELATIVE_PRONOUNS.includes(lc[k])) {
2652
+ r = k;
2653
+ break;
2654
+ }
2655
+ }
2656
+ if (r < 0) return null;
2657
+ const noun = entityNoun(lc[r - 1]);
2658
+ if (!noun || noun.placeholder || noun.entityType === "Change") return null;
2659
+ const term = w.slice(i, r - 1).join(" ").trim();
2660
+ if (!term) return null;
2661
+ return { entityType: noun.entityType, term, relIdx: r };
2662
+ }
2663
+ function buildPredicateAtoms(entityType, subjPrefix, predLc, predWords, nlp, depth) {
2664
+ const { branches, ops } = splitBoolean(predLc, predWords);
2665
+ let prevVerb = null;
2666
+ const atoms = [];
2667
+ for (let b = 0; b < branches.length; b += 1) {
2668
+ const bw = branches[b];
2669
+ const blc = bw.map((x) => x.toLowerCase());
2670
+ const op = b === 0 ? "intersection" : ops[b - 1];
2671
+ if (bw.length && blc.every((x) => QUALIFIERS[x])) {
2672
+ atoms.push({ op, kind: "qual", filters: blc });
2673
+ continue;
2674
+ }
2675
+ if (blc[0] === "of" || blc[0] === "in") {
2676
+ atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
2677
+ continue;
2678
+ }
2679
+ let phrase = bw;
2680
+ const vh = findPhrase(blc, VERB_TO_KIND);
2681
+ if (vh) prevVerb = bw.slice(vh.start, vh.end);
2682
+ else if (prevVerb) phrase = [...prevVerb, ...bw];
2683
+ const ast = parseBranchAst(`${subjPrefix} ${phrase.join(" ")}`, nlp, depth);
2684
+ if (!ast || ast.node === "miss") return { miss: ast && ast.reason || "a clause in the combination didn't parse" };
2685
+ atoms.push({ op, kind: "set", ast });
2686
+ }
2687
+ return { atoms };
2688
+ }
2512
2689
  function parseRelationalOrQualified(w, lc, nlp, depth) {
2690
+ const findHead = parseFindPredicateHead(w, lc);
2691
+ if (findHead) {
2692
+ const { entityType: entityType2, term, relIdx } = findHead;
2693
+ const predLc2 = lc.slice(relIdx + 1);
2694
+ const predWords2 = w.slice(relIdx + 1);
2695
+ if (!predLc2.length) return { node: "miss", reason: `a relative clause needs a predicate after "${lc[relIdx]}"` };
2696
+ const built = buildPredicateAtoms(entityType2, `which ${lc[relIdx - 1]}`, predLc2, predWords2, nlp, depth + 1);
2697
+ if (built.miss) return { node: "miss", reason: built.miss };
2698
+ const atoms2 = [{ op: "seed", kind: "set", ast: { node: "find", entityType: entityType2, term } }, ...built.atoms];
2699
+ return atoms2.length === 1 ? atoms2[0].ast : { node: "boolean", entityType: entityType2, atoms: atoms2 };
2700
+ }
2513
2701
  let i = 0;
2514
2702
  while (i < lc.length && FRAME_WORDS.has(lc[i])) i += 1;
2515
2703
  const framed = i > 0;
@@ -2520,8 +2708,9 @@
2520
2708
  }
2521
2709
  const noun = i < lc.length ? entityNoun(lc[i]) : null;
2522
2710
  if (!noun) {
2523
- if ((framed || quals.length) && i + 1 < lc.length && /^[a-z]+$/.test(lc[i]) && !VERB_TO_KIND[lc[i]] && !STOPWORDS2.has(lc[i]) && entityNoun(lc[i + 1])) {
2524
- return { node: "miss", reason: `unknown qualifier "${lc[i]}" \u2014 supported: ${Object.keys(QUALIFIERS).join(", ")}` };
2711
+ const nextNoun = i + 1 < lc.length ? entityNoun(lc[i + 1]) : null;
2712
+ if ((framed || quals.length) && nextNoun && /^[a-z]+$/.test(lc[i]) && !VERB_TO_KIND[lc[i]] && !STOPWORDS2.has(lc[i])) {
2713
+ return { node: "find", entityType: nextNoun.entityType, term: w[i] };
2525
2714
  }
2526
2715
  return null;
2527
2716
  }
@@ -2646,6 +2835,12 @@
2646
2835
  }
2647
2836
  return out;
2648
2837
  }
2838
+ function directoryScopeModules(graph, term) {
2839
+ const norm = normPath(term);
2840
+ if (!norm) return [];
2841
+ const prefix = `${norm}/`;
2842
+ return graph.individuals.filter((i) => i.class === "Module" && normPath(i.label).startsWith(prefix));
2843
+ }
2649
2844
  var qualCache = /* @__PURE__ */ new WeakMap();
2650
2845
  function qualSets(graph) {
2651
2846
  let c = qualCache.get(graph);
@@ -2689,12 +2884,127 @@
2689
2884
  return false;
2690
2885
  }
2691
2886
  }
2887
+ function inheritsEdges(graph) {
2888
+ return edgesOfKind(graph, "inherits");
2889
+ }
2890
+ function directChildrenOf(graph, id) {
2891
+ return inheritsEdges(graph).filter((e) => e.object === id).map((e) => e.subject);
2892
+ }
2893
+ function directParentsOf(graph, id) {
2894
+ return inheritsEdges(graph).filter((e) => e.subject === id).map((e) => e.object);
2895
+ }
2896
+ function descendantsOf(graph, id) {
2897
+ const out = /* @__PURE__ */ new Set();
2898
+ const queue = [...directChildrenOf(graph, id)];
2899
+ while (queue.length) {
2900
+ const next = queue.shift();
2901
+ if (out.has(next)) continue;
2902
+ out.add(next);
2903
+ for (const c of directChildrenOf(graph, next)) if (!out.has(c)) queue.push(c);
2904
+ }
2905
+ return out;
2906
+ }
2907
+ function ancestorsOf(graph, id) {
2908
+ const out = /* @__PURE__ */ new Set();
2909
+ const queue = [...directParentsOf(graph, id)];
2910
+ while (queue.length) {
2911
+ const next = queue.shift();
2912
+ if (out.has(next)) continue;
2913
+ out.add(next);
2914
+ for (const p of directParentsOf(graph, next)) if (!out.has(p)) queue.push(p);
2915
+ }
2916
+ return out;
2917
+ }
2918
+ var inheritsApplicableCache = /* @__PURE__ */ new WeakMap();
2919
+ function inheritsApplicable(graph, entityType) {
2920
+ let byType = inheritsApplicableCache.get(graph);
2921
+ if (!byType) {
2922
+ byType = /* @__PURE__ */ new Map();
2923
+ inheritsApplicableCache.set(graph, byType);
2924
+ }
2925
+ if (byType.has(entityType)) return byType.get(entityType);
2926
+ const ok = inheritsEdges(graph).some((e) => {
2927
+ const s = graph.byId.get(e.subject);
2928
+ const o = graph.byId.get(e.object);
2929
+ return !!s && !!o && s.class === entityType && o.class === entityType;
2930
+ });
2931
+ byType.set(entityType, ok);
2932
+ return ok;
2933
+ }
2934
+ function ownSurfaceHit(ind, termTokens) {
2935
+ const labelLc = String(ind.label || "").toLowerCase();
2936
+ if (termTokens.every((tok) => labelLc.includes(tok))) return "label";
2937
+ const attrs = (ind.attributes || []).map((a) => String(a.value ?? "").toLowerCase());
2938
+ if (termTokens.every((tok) => attrs.some((v) => v.includes(tok)))) return "attr";
2939
+ return null;
2940
+ }
2941
+ var FIND_TIER = { label: 3, chain: 2, attr: 1 };
2942
+ function sortFindHits(hits) {
2943
+ return hits.slice().sort((a, b) => FIND_TIER[b.via] - FIND_TIER[a.via] || String(a.ind.label).length - String(b.ind.label).length).map((h) => h.ind);
2944
+ }
2945
+ function fuzzyFindHit(ind, term) {
2946
+ const tLc = String(term || "").trim().toLowerCase();
2947
+ if (tLc.length < 4) return false;
2948
+ const bound = fuzzyBound(tLc);
2949
+ if (editDistance(String(ind.label || "").toLowerCase(), tLc, bound) <= bound) return true;
2950
+ for (const comp of componentSet(ind.label)) {
2951
+ if (editDistance(comp, tLc, bound) <= bound) return true;
2952
+ }
2953
+ return false;
2954
+ }
2955
+ function computeFind(graph, entityType, term) {
2956
+ const pool = graph.individuals.filter((i) => i.class === entityType);
2957
+ const termTokens = [...componentSet(term)];
2958
+ if (!termTokens.length || !pool.length) return { narrow: [], broad: [] };
2959
+ const cascade = inheritsApplicable(graph, entityType);
2960
+ const narrowHits = [];
2961
+ for (const ind of pool) {
2962
+ const own = ownSurfaceHit(ind, termTokens);
2963
+ if (own) {
2964
+ narrowHits.push({ ind, via: own });
2965
+ continue;
2966
+ }
2967
+ if (!cascade) continue;
2968
+ const viaChain = [...descendantsOf(graph, ind.id)].some((did) => {
2969
+ const d = graph.byId.get(did);
2970
+ return !!d && !!ownSurfaceHit(d, termTokens);
2971
+ });
2972
+ if (viaChain) narrowHits.push({ ind, via: "chain" });
2973
+ }
2974
+ if (narrowHits.length || !cascade) return { narrow: sortFindHits(narrowHits), broad: [] };
2975
+ const broadHits = /* @__PURE__ */ new Map();
2976
+ for (const ind of pool) {
2977
+ for (const ancId of ancestorsOf(graph, ind.id)) {
2978
+ if (!broadHits.has(ancId)) {
2979
+ const anc = graph.byId.get(ancId);
2980
+ if (anc && anc.class === entityType && fuzzyFindHit(anc, term)) {
2981
+ broadHits.set(ancId, { ind: anc, via: "chain" });
2982
+ }
2983
+ }
2984
+ for (const sibId of directChildrenOf(graph, ancId)) {
2985
+ if (sibId === ind.id || broadHits.has(sibId)) continue;
2986
+ const sib = graph.byId.get(sibId);
2987
+ if (sib && sib.class === entityType && fuzzyFindHit(sib, term)) broadHits.set(sibId, { ind: sib, via: "chain" });
2988
+ }
2989
+ }
2990
+ }
2991
+ return { narrow: [], broad: sortFindHits([...broadHits.values()]) };
2992
+ }
2692
2993
  function evalSet(graph, ast, opts) {
2693
2994
  switch (ast.node) {
2694
2995
  case "clause":
2695
2996
  return traverse(graph, ast.clause, opts).matches || [];
2696
2997
  case "allOfClass":
2697
2998
  return graph.individuals.filter((i) => i.class === ast.entityType);
2999
+ // predicate-find (Workstream 2), embedded as a set atom (§6 generalization —
3000
+ // a find-seed inside a boolean/qualifier fold): the narrow-then-broaden
3001
+ // cascade's result, transparently flattened (the "related, not exact" framing
3002
+ // is a top-level RENDER concern — evalComposite's dedicated "find" handling
3003
+ // below, not this generic embedding).
3004
+ case "find": {
3005
+ const { narrow, broad } = computeFind(graph, ast.entityType, ast.term);
3006
+ return narrow.length ? narrow : broad;
3007
+ }
2698
3008
  // the SUBJECTS that have ANY edge of a kind (the existential "modules that import
2699
3009
  // anything") — the positive set an existential negation ("do not import anything")
2700
3010
  // differences off allOfClass to yield "modules that import nothing".
@@ -2722,6 +3032,15 @@
2722
3032
  }
2723
3033
  case "membership": {
2724
3034
  const r = resolveObject(graph, ast.term);
3035
+ if (!(r.match && r.tier === 1)) {
3036
+ const dirMods = directoryScopeModules(graph, ast.term);
3037
+ if (dirMods.length) {
3038
+ if (!ast.entityType || ast.entityType === "Module") return dirMods;
3039
+ const ids2 = new Set(dirMods.map((m) => m.id));
3040
+ const objs2 = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids2)));
3041
+ return objs2.filter((o) => o.class === ast.entityType);
3042
+ }
3043
+ }
2725
3044
  if (!r.match) return [];
2726
3045
  const ids = /* @__PURE__ */ new Set([r.match.id]);
2727
3046
  const objs = uniqueById(MEMBERSHIP_KINDS.flatMap((k) => forwardOverSet(graph, k, ids)));
@@ -2827,11 +3146,27 @@
2827
3146
  if (ast.node === "superlative") return evalSuperlative(graph, ast);
2828
3147
  if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
2829
3148
  if (ast.node === "anaphora") return evalAnaphora(graph, ast, opts);
3149
+ if (ast.node === "find") {
3150
+ const { narrow, broad } = computeFind(graph, ast.entityType, ast.term);
3151
+ return {
3152
+ compositeKind: "find",
3153
+ entityType: ast.entityType,
3154
+ term: ast.term,
3155
+ matches: narrow.length ? narrow : broad,
3156
+ broad: !narrow.length && broad.length > 0
3157
+ };
3158
+ }
2830
3159
  return { compositeKind: "set", matches: evalSet(graph, ast, opts), entityType: ast.entityType || null };
2831
3160
  }
2832
3161
  var compositeList = (matches) => listJoin(matches.slice(0, OVERFLOW_CAP).map((m) => ["Function", "Method"].includes(m.class) ? `${m.label}()` : m.label)) + (matches.length > OVERFLOW_CAP ? `, \u2026and ${matches.length - OVERFLOW_CAP} more` : "");
2833
3162
  function compositionalHint() {
2834
- return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", or (after a listing) "which of those are tested"';
3163
+ return 'compositional queries also work: "which functions call X and call Y", "what calls something that imports X", "public methods of X", "list functions" / "show me the classes", "how many classes", "which module has the most imports", "find me the payment class", or (after a listing) "which of those are tested"';
3164
+ }
3165
+ function describeFindHit(ind) {
3166
+ const label = ["Function", "Method"].includes(ind.class) ? `${ind.label}()` : ind.label;
3167
+ if (ind.class === "Module") return label;
3168
+ const mod = moduleLabelOf(ind);
3169
+ return mod && mod !== "(unknown module)" ? `${label} in ${mod}` : label;
2835
3170
  }
2836
3171
  function renderComposite(parsed, result) {
2837
3172
  if (result.compositeMiss) {
@@ -2852,6 +3187,23 @@
2852
3187
  const hint = !result.scoped && scopeable && result.matches.length > OVERFLOW_CAP ? ` \u2014 narrow with "${nounFor(result.entityType, 2)} in <module>"` : "";
2853
3188
  return { content: `${compositeList(result.matches)}${hint}.`, miss: false, ambiguous: false, matches: result.matches };
2854
3189
  }
3190
+ if (result.compositeKind === "find") {
3191
+ const typeNoun = nounFor(result.entityType, 1);
3192
+ if (!result.matches.length) {
3193
+ return { content: `no ${nounFor(result.entityType, 2)} found matching "${result.term}".`, miss: true, ambiguous: false, matches: [] };
3194
+ }
3195
+ const cited = result.matches.length === 1 ? describeFindHit(result.matches[0]) : compositeList(result.matches);
3196
+ if (result.broad) {
3197
+ return {
3198
+ content: `no exact ${typeNoun} named "${result.term}", but found a related ${result.matches.length === 1 ? typeNoun : nounFor(result.entityType, 2)}: ${cited}.`,
3199
+ miss: false,
3200
+ ambiguous: false,
3201
+ matches: result.matches,
3202
+ relatedNotExact: true
3203
+ };
3204
+ }
3205
+ return { content: `${cited}.`, miss: false, ambiguous: false, matches: result.matches };
3206
+ }
2855
3207
  if (result.compositeKind === "superlative") {
2856
3208
  if (!result.matches.length) return { content: `no ${nounFor(result.entityType, 2)} to rank in this index.`, miss: true, ambiguous: false };
2857
3209
  const lead = result.extreme === "most" ? "the most" : "the fewest";
@@ -2899,11 +3251,11 @@
2899
3251
  function componentSet(s) {
2900
3252
  return new Set(String(s).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
2901
3253
  }
2902
- function resolveObject(graph, term) {
3254
+ function resolveObject(graph, term, { expectedClass = null } = {}) {
2903
3255
  const t = String(term || "").trim();
2904
3256
  if (!t) return { match: null, candidates: [], tier: null, ambiguous: false };
2905
3257
  const tLc = t.toLowerCase();
2906
- const pool = graph.individuals;
3258
+ const pool = expectedClass ? graph.individuals.filter((i) => i.class === expectedClass) : graph.individuals;
2907
3259
  const shaTerm = tLc.match(/^(commit[:\s])?([0-9a-f]{7,40})$/);
2908
3260
  if (shaTerm) {
2909
3261
  const sha = shaTerm[2];
@@ -2924,7 +3276,7 @@
2924
3276
  }
2925
3277
  }
2926
3278
  }
2927
- if (extId) return { match: { id: extId, label: t, class: null }, candidates: [], tier: 2, ambiguous: false };
3279
+ if (extId && !expectedClass) return { match: { id: extId, label: t, class: null }, candidates: [], tier: 2, ambiguous: false };
2928
3280
  const scored = [];
2929
3281
  const dotted = !tLc.includes("/") && /^[\w$]+(\.[\w$]+)+$/.test(tLc);
2930
3282
  if (dotted) {
@@ -2963,7 +3315,7 @@
2963
3315
  };
2964
3316
  }
2965
3317
  let proseResult = null;
2966
- const proseHits = !dotted && typeof lookupByProseTokens === "function" ? lookupByProseTokens(graph.proseIndex, t) : [];
3318
+ const proseHits = !dotted && typeof lookupByProseTokens === "function" ? lookupByProseTokens(graph.proseIndex, t).filter((h) => !expectedClass || graph.byId.get(h.id)?.class === expectedClass) : [];
2967
3319
  if (proseHits.length) {
2968
3320
  const [best, ...rest] = proseHits;
2969
3321
  const bestInd = graph.byId.get(best.id);
@@ -3221,10 +3573,59 @@
3221
3573
  }
3222
3574
  return { matches: matches2, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
3223
3575
  }
3224
- let edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.object === objMatch.id);
3576
+ let gObjMatch = objMatch;
3577
+ let gCandidates = candidates;
3578
+ let gAmbiguous = ambiguous;
3579
+ let gMatchedVia = matchedVia;
3580
+ let grainRefinedNote = "";
3581
+ const wantClass = kindObjectClass(graph, kind);
3582
+ if (wantClass && gObjMatch.class && gObjMatch.class !== wantClass) {
3583
+ const retry = resolveObject(graph, parsed.object, { expectedClass: wantClass });
3584
+ if (retry.match && !retry.ambiguous) {
3585
+ gObjMatch = retry.match;
3586
+ gCandidates = retry.candidates;
3587
+ gAmbiguous = retry.ambiguous;
3588
+ gMatchedVia = retry.matchedVia;
3589
+ } else if ((kind === "tests" || kind === "cochange") && gObjMatch.class !== "Module") {
3590
+ const mid = moduleIdOf2(graph, gObjMatch);
3591
+ const mod = mid && graph.byId.get(mid);
3592
+ if (mod) {
3593
+ grainRefinedNote = `, refined from ${gObjMatch.label} to its containing module`;
3594
+ gObjMatch = mod;
3595
+ } else {
3596
+ return {
3597
+ matches: [],
3598
+ objMatch: gObjMatch,
3599
+ candidates: gCandidates,
3600
+ ambiguous: gAmbiguous,
3601
+ matchedVia: gMatchedVia,
3602
+ wrongGrainMiss: true,
3603
+ wantClass,
3604
+ traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass}, and no containing module could be found to refine to)`
3605
+ };
3606
+ }
3607
+ } else {
3608
+ return {
3609
+ matches: [],
3610
+ objMatch: gObjMatch,
3611
+ candidates: gCandidates,
3612
+ ambiguous: gAmbiguous,
3613
+ matchedVia: gMatchedVia,
3614
+ wrongGrainMiss: true,
3615
+ wantClass,
3616
+ traversal: `"${parsed.object}" resolved to ${gObjMatch.class} ${gObjMatch.label} (grain mismatch: this "${kind}" question needs a ${wantClass})`
3617
+ };
3618
+ }
3619
+ }
3620
+ let edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.object === gObjMatch.id);
3621
+ if (kind === "cochange") {
3622
+ edges = edges.concat(
3623
+ kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === gObjMatch.id).map((e) => ({ ...e, subject: e.object, object: e.subject }))
3624
+ );
3625
+ }
3225
3626
  let extNote = "";
3226
- if (!edges.length && objMatch.class) {
3227
- const extId = `ext:${String(objMatch.label).toLowerCase()}`;
3627
+ if (!edges.length && gObjMatch.class) {
3628
+ const extId = `ext:${String(gObjMatch.label).toLowerCase()}`;
3228
3629
  edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => String(e.object).toLowerCase() === extId);
3229
3630
  if (edges.length) extNote = ` (by name, via unresolved ${extId} references)`;
3230
3631
  }
@@ -3252,7 +3653,14 @@
3252
3653
  matches = [];
3253
3654
  }
3254
3655
  }
3255
- return { matches, objMatch, candidates, traversal: `${kindsFor(kind).join("+")} edges where object = ${objMatch.label}${extNote}${grainNote}`, ambiguous, matchedVia };
3656
+ return {
3657
+ matches,
3658
+ objMatch: gObjMatch,
3659
+ candidates: gCandidates,
3660
+ traversal: `${kindsFor(kind).join("+")} edges where object = ${gObjMatch.label}${extNote}${grainNote}${grainRefinedNote}`,
3661
+ ambiguous: gAmbiguous,
3662
+ matchedVia: gMatchedVia
3663
+ };
3256
3664
  }
3257
3665
  function moduleLabelOf(ind) {
3258
3666
  if (ind.class === "Module") return ind.label;
@@ -3313,6 +3721,15 @@
3313
3721
  ambiguous: false
3314
3722
  };
3315
3723
  }
3724
+ if (result.wrongGrainMiss) {
3725
+ const gotNoun = result.objMatch.class ? nounFor(result.objMatch.class, 1) : "term";
3726
+ const wantNoun = nounFor(result.wantClass, 1);
3727
+ return {
3728
+ content: `"${parsed.object}" resolved to the ${gotNoun} ${result.objMatch.label}, but this question needs a ${wantNoun} \u2014 no ${wantNoun} named "${parsed.object}" was found in the index.`,
3729
+ miss: true,
3730
+ ambiguous: false
3731
+ };
3732
+ }
3316
3733
  if (parsed.shape === "meta") {
3317
3734
  if (!result.objMatch) {
3318
3735
  return {
@@ -3365,8 +3782,10 @@
3365
3782
  if (result.ambiguous) {
3366
3783
  const pool = [result.objMatch, ...result.candidates || []].filter(Boolean);
3367
3784
  const noun = pool.length && pool.every((i) => i.class === "Commit") ? "commit" : "module";
3785
+ const shown = pool.slice(0, OVERFLOW_CAP).map((i) => i.label);
3786
+ const extra2 = pool.length > OVERFLOW_CAP ? `, \u2026and ${pool.length - OVERFLOW_CAP} more` : "";
3368
3787
  return {
3369
- content: `"${parsed.object}" matches more than one ${noun} ambiguously \u2014 please narrow the term.`,
3788
+ content: `"${parsed.object}" matches more than one ${noun} ambiguously \u2014 did you mean ${listJoin(shown)}${extra2}? Try one of those.`,
3370
3789
  miss: false,
3371
3790
  ambiguous: true,
3372
3791
  candidates: pool.map((i) => i.label)
@@ -3451,7 +3870,7 @@
3451
3870
  if (!result.matches.length) {
3452
3871
  if (parsed.shape === "forward") {
3453
3872
  return {
3454
- content: `${result.objMatch.label} has no ${parsed.kind} edges in the index.`,
3873
+ content: `${result.objMatch.label} has no ${verbFor(parsed.kind)} edges in the index.`,
3455
3874
  miss: true,
3456
3875
  ambiguous: false
3457
3876
  };
@@ -0,0 +1 @@
1
+ 0.9.10