@nusoft/nuos-build-catalogue 0.20.0 → 0.20.1

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.
@@ -296,16 +296,13 @@ export async function cmdInstallProtocols(prompt, options = {}) {
296
296
  prompt.print('');
297
297
  prompt.print('Checking local semantic search (Ollama + qwen3-embedding:0.6b):');
298
298
  await reportLlmStatus((msg) => prompt.print(` ${msg}`));
299
- // Auto-build the search index when the LLM is ready but the index
300
- // isn't present yet (typical upgrade-path state: pre-0.19 install +
301
- // someone just added docs/build/ content). When the index already
302
- // exists this is a no-op; when the LLM isn't ready the helper skips
303
- // with a hint that's already printed by reportLlmStatus.
299
+ // Auto-build/refresh the search index when the LLM is ready. The
300
+ // indexer is incremental via per-file SHA hashes: a no-change project
301
+ // takes ~1s, a project with N changed files takes O(N) embed calls.
302
+ // When the LLM stack isn't ready the helper skips silently — the
303
+ // status was already reported above by reportLlmStatus.
304
304
  const { ensureIndexBuilt } = await import('../setup/auto-index.js');
305
- const indexResult = await ensureIndexBuilt({ cwd });
306
- if (indexResult.kind === 'just_built') {
307
- prompt.print(` ✓ Built search index (${indexResult.indexed} files, ${indexResult.chunks} chunks).`);
308
- }
305
+ await ensureIndexBuilt({ cwd });
309
306
  return { output: '', exitCode: 0 };
310
307
  }
311
308
  /**
@@ -15,13 +15,17 @@
15
15
  * @module setup/auto-index
16
16
  */
17
17
  /** Outcome of an auto-index attempt. */
18
- export type AutoIndexResult = {
19
- kind: 'already_built';
20
- indexPath: string;
21
- } | {
22
- kind: 'just_built';
18
+ export type AutoIndexResult =
19
+ /**
20
+ * The indexer ran. `indexed` includes both freshly-embedded files and
21
+ * re-embedded changed ones. `unchanged` is non-zero on subsequent
22
+ * runs — those files were SHA-matched and skipped without embedding.
23
+ */
24
+ {
25
+ kind: 'ran';
23
26
  indexPath: string;
24
27
  indexed: number;
28
+ unchanged: number;
25
29
  chunks: number;
26
30
  durationMs: number;
27
31
  } | {
@@ -43,10 +47,11 @@ export interface AutoIndexOptions {
43
47
  force?: boolean;
44
48
  }
45
49
  /**
46
- * Build the first search index when conditions allow. Idempotent: returns
47
- * `already_built` and prints nothing when the index file exists (unless
48
- * `force` is set). Returns `skipped_llm_not_ready` with a hint when the
49
- * Ollama probe fails the caller prints the hint and the user runs
50
+ * Run the indexer when conditions allow. Always runs (the indexer is
51
+ * incremental unchanged files are SHA-skipped without embedding work),
52
+ * so this both *creates* the index on first call and *refreshes* it on
53
+ * subsequent calls. Returns `skipped_llm_not_ready` with a hint when
54
+ * the Ollama probe fails — the caller prints the hint and the user runs
50
55
  * `setup-llm` to fix things.
51
56
  *
52
57
  * Never throws on user-facing failures.
@@ -19,10 +19,11 @@ import { resolveBuildRoot, resolveCatalogueRoot, resolveHashPath, resolveIndexPa
19
19
  import { DEFAULT_OLLAMA_HOST, detectModelPresent, detectOllamaApi } from './ollama-detect.js';
20
20
  import { DEFAULT_EMBEDDING_MODEL } from './run-llm-setup.js';
21
21
  /**
22
- * Build the first search index when conditions allow. Idempotent: returns
23
- * `already_built` and prints nothing when the index file exists (unless
24
- * `force` is set). Returns `skipped_llm_not_ready` with a hint when the
25
- * Ollama probe fails the caller prints the hint and the user runs
22
+ * Run the indexer when conditions allow. Always runs (the indexer is
23
+ * incremental unchanged files are SHA-skipped without embedding work),
24
+ * so this both *creates* the index on first call and *refreshes* it on
25
+ * subsequent calls. Returns `skipped_llm_not_ready` with a hint when
26
+ * the Ollama probe fails — the caller prints the hint and the user runs
26
27
  * `setup-llm` to fix things.
27
28
  *
28
29
  * Never throws on user-facing failures.
@@ -49,10 +50,13 @@ export async function ensureIndexBuilt(opts = {}) {
49
50
  catch {
50
51
  return { kind: 'skipped_no_catalogue' };
51
52
  }
52
- // Fast path: index file already exists and we're not forcing a rebuild.
53
- if (existsSync(indexPath) && !opts.force) {
54
- return { kind: 'already_built', indexPath };
55
- }
53
+ // We do not short-circuit on `existsSync(indexPath)` the indexer is
54
+ // already incremental via the per-file SHA hash store, so running it
55
+ // when the index is up-to-date is cheap (~1s on a 270-file catalogue
56
+ // with no changes). Short-circuiting here would leave newer files
57
+ // un-embedded until the user ran `nuos-catalogue index` manually,
58
+ // which is exactly the discoverability gap the auto-index is meant to
59
+ // close.
56
60
  // Probe the LLM stack — index requires Ollama + the model. If either
57
61
  // is missing, skip with a hint pointing at setup-llm.
58
62
  const apiHost = process.env.NUOS_CATALOGUE_OLLAMA_HOST ?? DEFAULT_OLLAMA_HOST;
@@ -73,10 +77,17 @@ export async function ensureIndexBuilt(opts = {}) {
73
77
  hint: 'Run `nuos-catalogue setup-llm` to pull the embedding model (~600 MB), then re-run `nuos-catalogue index`.',
74
78
  };
75
79
  }
76
- // LLM is ready. Build the index. We import lazily so the cold-start
77
- // path of `install-protocols` (where the index usually already exists)
78
- // doesn't pay the embedder-loading cost.
79
- out('Building search index for docs/build/ … (first run may take ~30 seconds)\n');
80
+ // LLM is ready. Run the indexer. The first run on a fresh project is
81
+ // ~30s of starter-kit content; subsequent runs are fast — the
82
+ // per-file SHA hashes mean unchanged files are skipped without
83
+ // embedding.
84
+ const isFirstRun = !existsSync(indexPath);
85
+ if (isFirstRun) {
86
+ out('Building search index for docs/build/ … (first run may take ~30 seconds)\n');
87
+ }
88
+ else {
89
+ out('Refreshing search index (incremental — only changed files are re-embedded)…\n');
90
+ }
80
91
  try {
81
92
  const { selectEmbedderFromEnv } = await import('../embedder/select.js');
82
93
  const { openStore } = await import('../store/open.js');
@@ -92,12 +103,23 @@ export async function ensureIndexBuilt(opts = {}) {
92
103
  force: Boolean(opts.force),
93
104
  dryRun: false,
94
105
  });
95
- out(`✓ Indexed ${report.indexed} file(s), ${report.chunks} chunks embedded in ` +
96
- `${(report.durationMs / 1000).toFixed(1)}s\n`);
106
+ const changed = report.indexed + report.updated;
107
+ const secs = (report.durationMs / 1000).toFixed(1);
108
+ if (isFirstRun) {
109
+ out(`✓ Indexed ${report.indexed} file(s), ${report.chunks} chunks embedded in ${secs}s\n`);
110
+ }
111
+ else if (changed === 0) {
112
+ out(`✓ Index up-to-date (${report.unchanged} files checked, none changed) in ${secs}s\n`);
113
+ }
114
+ else {
115
+ out(`✓ Re-indexed ${changed} changed file(s) (${report.unchanged} unchanged), ` +
116
+ `${report.chunks} chunks embedded in ${secs}s\n`);
117
+ }
97
118
  return {
98
- kind: 'just_built',
119
+ kind: 'ran',
99
120
  indexPath,
100
- indexed: report.indexed,
121
+ indexed: changed,
122
+ unchanged: report.unchanged,
101
123
  chunks: report.chunks,
102
124
  durationMs: report.durationMs,
103
125
  };
@@ -109,7 +131,7 @@ export async function ensureIndexBuilt(opts = {}) {
109
131
  }
110
132
  catch (err) {
111
133
  const message = err instanceof Error ? err.message : String(err);
112
- out(`\n✗ Index build failed: ${message}\n`);
134
+ out(`\n✗ Index refresh failed: ${message}\n`);
113
135
  out('Re-run `nuos-catalogue index` manually to retry.\n');
114
136
  return { kind: 'failed', error: message };
115
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nusoft/nuos-build-catalogue",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "NuOS build-catalogue tooling: semantic search (WU 110) + migration runner that lifts markdown artefacts into JSON-backed workflow records (WU 111, Phase G).",
5
5
  "type": "module",
6
6
  "bin": {