wayfind 2.0.43 → 2.0.44

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.
@@ -176,6 +176,27 @@ function isRepoExcluded(repo) {
176
176
  return false;
177
177
  }
178
178
 
179
+ /**
180
+ * Check if a repo name matches a team scope pattern list.
181
+ * Patterns ending with '/' are prefix matches (e.g., 'acme/' matches 'acme/api', 'acme/frontend').
182
+ * All other patterns are exact matches.
183
+ * @param {string|null} repo
184
+ * @param {string[]} patterns
185
+ * @returns {boolean}
186
+ */
187
+ function matchesTeamScope(repo, patterns) {
188
+ if (!patterns || patterns.length === 0) return true;
189
+ if (!repo) return false;
190
+ for (const p of patterns) {
191
+ if (p.endsWith('/')) {
192
+ if (repo.startsWith(p)) return true;
193
+ } else if (repo === p) {
194
+ return true;
195
+ }
196
+ }
197
+ return false;
198
+ }
199
+
179
200
  // ── Journal parsing ─────────────────────────────────────────────────────────
180
201
 
181
202
  /**
@@ -432,6 +453,7 @@ async function indexJournals(options = {}) {
432
453
 
433
454
  for (const entry of entries) {
434
455
  if (isRepoExcluded(entry.repo)) continue;
456
+ if (options.repoAllowlist && !matchesTeamScope(entry.repo, options.repoAllowlist)) continue;
435
457
  const id = generateEntryId(date, entry.repo, entry.title);
436
458
  const author = entry.author || options.defaultAuthor || '';
437
459
  const content = buildContent({ ...entry, date, author });
@@ -2290,6 +2312,47 @@ function deduplicateResults(results) {
2290
2312
  return results.filter(r => !absorbedIds.has(r.id));
2291
2313
  }
2292
2314
 
2315
+ /**
2316
+ * Remove entries from a store whose repo doesn't match the allowed patterns.
2317
+ * Trims both index and embeddings. Safe to call repeatedly (idempotent).
2318
+ * @param {string} storePath
2319
+ * @param {string[]} allowedPatterns - prefix patterns (ending '/') or exact names
2320
+ * @returns {{ kept: number, removed: number, removedRepos: string[] }}
2321
+ */
2322
+ async function trimStore(storePath, allowedPatterns) {
2323
+ if (!allowedPatterns || allowedPatterns.length === 0) {
2324
+ throw new Error('allowedPatterns is required — refusing to trim to empty set');
2325
+ }
2326
+ const backend = getBackend(storePath);
2327
+ const idx = backend.loadIndex();
2328
+ const embeddings = backend.loadEmbeddings() || {};
2329
+
2330
+ const keptEntries = {};
2331
+ const keptEmbeddings = {};
2332
+ const removedRepos = [];
2333
+
2334
+ for (const [id, entry] of Object.entries(idx.entries || {})) {
2335
+ if (matchesTeamScope(entry.repo, allowedPatterns)) {
2336
+ keptEntries[id] = entry;
2337
+ if (embeddings[id]) keptEmbeddings[id] = embeddings[id];
2338
+ } else {
2339
+ removedRepos.push(entry.repo);
2340
+ }
2341
+ }
2342
+
2343
+ idx.entries = keptEntries;
2344
+ idx.entryCount = Object.keys(keptEntries).length;
2345
+ idx.lastUpdated = new Date().toISOString();
2346
+ backend.saveIndex(idx);
2347
+ backend.saveEmbeddings(keptEmbeddings);
2348
+
2349
+ return {
2350
+ kept: idx.entryCount,
2351
+ removed: removedRepos.length,
2352
+ removedRepos: [...new Set(removedRepos)].sort(),
2353
+ };
2354
+ }
2355
+
2293
2356
  module.exports = {
2294
2357
  // Parsing
2295
2358
  parseJournalFile,
@@ -2318,8 +2381,12 @@ module.exports = {
2318
2381
 
2319
2382
  // Filtering
2320
2383
  isRepoExcluded,
2384
+ matchesTeamScope,
2321
2385
  applyFilters,
2322
2386
 
2387
+ // Store maintenance
2388
+ trimStore,
2389
+
2323
2390
  // Quality & dedup
2324
2391
  computeQualityScore,
2325
2392
  deduplicateResults,
@@ -1111,8 +1111,16 @@ async function runIndexJournals(args) {
1111
1111
  const journalDir = opts.dir || contentStore.DEFAULT_JOURNAL_DIR;
1112
1112
  const storePath = opts.store || contentStore.resolveStorePath();
1113
1113
 
1114
+ // Load team scope allowlist from context.json — only index repos bound to the active team.
1115
+ const ctxConfig = readContextConfig();
1116
+ const teamId = readRepoTeamBinding() || ctxConfig.default;
1117
+ const teamConfig = teamId && ctxConfig.teams && ctxConfig.teams[teamId];
1118
+ const repoAllowlist = (teamConfig && teamConfig.bound_repos && teamConfig.bound_repos.length > 0)
1119
+ ? teamConfig.bound_repos : undefined;
1120
+
1114
1121
  console.log(`Indexing journals from: ${journalDir}`);
1115
1122
  console.log(`Store: ${storePath}`);
1123
+ if (repoAllowlist) console.log(`Team scope (${teamId}): ${repoAllowlist.join(', ')}`);
1116
1124
  console.log('');
1117
1125
 
1118
1126
  try {
@@ -1120,6 +1128,7 @@ async function runIndexJournals(args) {
1120
1128
  journalDir,
1121
1129
  storePath,
1122
1130
  embeddings: opts.noEmbeddings ? false : undefined,
1131
+ repoAllowlist,
1123
1132
  });
1124
1133
 
1125
1134
  console.log(`Indexed: ${stats.entryCount} entries`);
@@ -3591,6 +3600,21 @@ function contextBind(args) {
3591
3600
  writeRepoTeamBinding(teamId);
3592
3601
  console.log(`Bound this repo to: ${config.teams[teamId].name} (${teamId})`);
3593
3602
  console.log('Journals from this repo will sync to that team\'s context repo.');
3603
+
3604
+ // Derive repo label (e.g., "acme/api") and add to the team's bound_repos allowlist.
3605
+ const cwdParts = process.cwd().split(path.sep);
3606
+ const reposIdx = cwdParts.lastIndexOf('repos');
3607
+ const repoLabel = (reposIdx >= 0 && reposIdx + 2 <= cwdParts.length)
3608
+ ? cwdParts.slice(reposIdx + 1).join('/')
3609
+ : cwdParts[cwdParts.length - 1];
3610
+
3611
+ const team = config.teams[teamId];
3612
+ if (!team.bound_repos) team.bound_repos = [];
3613
+ if (!team.bound_repos.includes(repoLabel)) {
3614
+ team.bound_repos.push(repoLabel);
3615
+ writeContextConfig(config);
3616
+ console.log(`Added "${repoLabel}" to team scope.`);
3617
+ }
3594
3618
  }
3595
3619
 
3596
3620
  function contextList() {
@@ -5000,6 +5024,52 @@ async function runContainerDoctor() {
5000
5024
  }
5001
5025
  }
5002
5026
 
5027
+ // ── Store management ────────────────────────────────────────────────────────
5028
+
5029
+ async function runStoreTrim(args) {
5030
+ const ctxConfig = readContextConfig();
5031
+ const teamId = args[0] || readRepoTeamBinding() || ctxConfig.default;
5032
+ if (!teamId) {
5033
+ console.error('No team ID resolved. Usage: wayfind store trim [team-id]');
5034
+ process.exit(1);
5035
+ }
5036
+ const team = ctxConfig.teams && ctxConfig.teams[teamId];
5037
+ if (!team) {
5038
+ console.error(`Team "${teamId}" not found.`);
5039
+ process.exit(1);
5040
+ }
5041
+ const allowedPatterns = team.bound_repos;
5042
+ if (!allowedPatterns || allowedPatterns.length === 0) {
5043
+ console.error(`Team "${teamId}" has no bound_repos in context.json. Configure them first.`);
5044
+ process.exit(1);
5045
+ }
5046
+ const storePath = contentStore.resolveStorePath(teamId);
5047
+ console.log(`Team: ${team.name} (${teamId})`);
5048
+ console.log(`Store: ${storePath}`);
5049
+ console.log(`Patterns: ${allowedPatterns.join(', ')}`);
5050
+ console.log('');
5051
+ const stats = await contentStore.trimStore(storePath, allowedPatterns);
5052
+ console.log(`Kept: ${stats.kept}`);
5053
+ console.log(`Removed: ${stats.removed}`);
5054
+ if (stats.removedRepos.length > 0) {
5055
+ console.log(`Repos removed:`);
5056
+ stats.removedRepos.forEach(r => console.log(` ${r}`));
5057
+ }
5058
+ }
5059
+
5060
+ async function runStore(args) {
5061
+ const [sub, ...subArgs] = args;
5062
+ switch (sub) {
5063
+ case 'trim':
5064
+ await runStoreTrim(subArgs);
5065
+ break;
5066
+ default:
5067
+ console.error(`Unknown store subcommand: ${sub || ''}`);
5068
+ console.error('Available: trim');
5069
+ process.exit(1);
5070
+ }
5071
+ }
5072
+
5003
5073
  // ── Command registry ────────────────────────────────────────────────────────
5004
5074
 
5005
5075
  const COMMANDS = {
@@ -5274,6 +5344,10 @@ const COMMANDS = {
5274
5344
  desc: 'Scaffold Docker deployment in your team context repo',
5275
5345
  run: (args) => runDeploy(args),
5276
5346
  },
5347
+ store: {
5348
+ desc: 'Manage content store (trim)',
5349
+ run: (args) => runStore(args),
5350
+ },
5277
5351
  onboard: {
5278
5352
  desc: 'Generate an onboarding context pack for a repo',
5279
5353
  run: (args) => runOnboard(args),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.43",
3
+ "version": "2.0.44",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js",