@nusoft/nuos-build-catalogue 0.33.3 → 0.36.0

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.
@@ -32,6 +32,13 @@
32
32
  * idle-timeout (the keep_alive: "1m" we sent) cleans up within a
33
33
  * minute.
34
34
  *
35
+ * **Bounded footprint while loaded.** Beyond unloading promptly, each call
36
+ * also pins `options.num_ctx` (see EMBED_NUM_CTX) so the model loads with an
37
+ * embedding-sized context window instead of inheriting the daemon's
38
+ * chat-sized OLLAMA_CONTEXT_LENGTH. Without this the 639MB model loads at
39
+ * ~5.7GB resident; with it, ~1.1GB. This is what keeps a reindex from pushing
40
+ * a developer's machine into swap.
41
+ *
35
42
  * Sizing note — the new 0.6b default is ~600MB on disk and runs
36
43
  * comfortably on any modern laptop, including CPU-only. The 4b variant
37
44
  * (~2.5GB) and 8b variant (~4.7GB, benefits from ~16GB RAM + Metal)
@@ -32,6 +32,13 @@
32
32
  * idle-timeout (the keep_alive: "1m" we sent) cleans up within a
33
33
  * minute.
34
34
  *
35
+ * **Bounded footprint while loaded.** Beyond unloading promptly, each call
36
+ * also pins `options.num_ctx` (see EMBED_NUM_CTX) so the model loads with an
37
+ * embedding-sized context window instead of inheriting the daemon's
38
+ * chat-sized OLLAMA_CONTEXT_LENGTH. Without this the 639MB model loads at
39
+ * ~5.7GB resident; with it, ~1.1GB. This is what keeps a reindex from pushing
40
+ * a developer's machine into swap.
41
+ *
35
42
  * Sizing note — the new 0.6b default is ~600MB on disk and runs
36
43
  * comfortably on any modern laptop, including CPU-only. The 4b variant
37
44
  * (~2.5GB) and 8b variant (~4.7GB, benefits from ~16GB RAM + Metal)
@@ -47,6 +54,16 @@ const KNOWN_DIMENSIONS = {
47
54
  'qwen3-embedding:4b': 2560,
48
55
  'qwen3-embedding:0.6b': 1024,
49
56
  };
57
+ // Context window for embedding loads. The Ollama daemon's global
58
+ // OLLAMA_CONTEXT_LENGTH — set high for chat models (commonly 32K–64K) — is
59
+ // inherited by every model that doesn't override it. Inherited unchanged, it
60
+ // inflates the 639MB qwen3-embedding:0.6b model to ~5.7GB resident, which is
61
+ // enough to push a 16–18GB developer machine into swap during a reindex.
62
+ // Embedding inputs are capped at ~600 tokens (MAX_CHUNK_CHARS in
63
+ // indexer/chunk.ts), so a 2048-token window leaves ~3x headroom and never
64
+ // truncates a chunk. Measured 2026-06-01 (qwen3-embedding:0.6b, Apple Silicon):
65
+ // inherited 32K ctx → 5.7GB resident; num_ctx 2048 → 1.1GB resident.
66
+ const EMBED_NUM_CTX = 2048;
50
67
  export class OllamaEmbedder {
51
68
  dimensions;
52
69
  modelId;
@@ -68,7 +85,13 @@ export class OllamaEmbedder {
68
85
  const probe = await fetch(`${host}/api/embed`, {
69
86
  method: 'POST',
70
87
  headers: { 'content-type': 'application/json' },
71
- body: JSON.stringify({ model: modelId, input: 'probe' }),
88
+ body: JSON.stringify({
89
+ model: modelId,
90
+ input: 'probe',
91
+ // Pin the context window here too — the probe is what first loads the
92
+ // model, so without it the probe alone would pull in the full ~5.7GB.
93
+ options: { num_ctx: EMBED_NUM_CTX },
94
+ }),
72
95
  });
73
96
  if (!probe.ok) {
74
97
  const body = await probe.text().catch(() => '<unreadable>');
@@ -121,6 +144,9 @@ export class OllamaEmbedder {
121
144
  // Keep the model warm only for the duration of one operation.
122
145
  // dispose() at the end of the run sends keep_alive: 0 to unload.
123
146
  keep_alive: '1m',
147
+ // Cap the context window so the model loads at ~1.1GB rather than
148
+ // inheriting the daemon's chat-sized window and ballooning to ~5.7GB.
149
+ options: { num_ctx: EMBED_NUM_CTX },
124
150
  }),
125
151
  });
126
152
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nusoft/nuos-build-catalogue",
3
- "version": "0.33.3",
3
+ "version": "0.36.0",
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": {
@@ -19,15 +19,16 @@
19
19
  "build": "rm -rf dist && tsc && chmod +x dist/cli.js",
20
20
  "prepublishOnly": "npm run build",
21
21
  "verify-storage": "tsx scripts/verify-persistence.ts",
22
- "test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/mode.test.ts tests/render.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts tests/wu-active.test.ts tests/install-claude-hooks.test.ts tests/protocols-in-sync.test.ts tests/end-of-session.test.ts tests/hooks-in-sync.test.ts tests/memory-store-separation.test.ts",
22
+ "test": "tsx --test tests/chunk.test.ts tests/metadata.test.ts tests/crawl.test.ts tests/migrate.test.ts tests/commands-read.test.ts tests/regenerate.test.ts tests/commands-write.test.ts tests/ac-parse.test.ts tests/create.test.ts tests/init.test.ts tests/wu-111-soak-findings.test.ts tests/plan.test.ts tests/mode.test.ts tests/render.test.ts tests/swarm.test.ts tests/setup-progress-bar.test.ts tests/setup-ollama-pull.test.ts tests/setup-run-llm-setup.test.ts tests/wu-active.test.ts tests/install-claude-hooks.test.ts tests/protocols-in-sync.test.ts tests/end-of-session.test.ts tests/hooks-in-sync.test.ts tests/memory-store-separation.test.ts tests/state-compile.test.ts tests/state-drift-check.test.ts tests/hook-isolation.test.ts",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "index": "tsx src/cli.ts index",
25
25
  "search": "tsx src/cli.ts search"
26
26
  },
27
27
  "dependencies": {
28
- "@nusoft/nuvector": "^0.1.5",
29
28
  "@nusoft/nuflow": "^0.4.1",
30
- "@nusoft/nuflow-pack-nuos-build-catalogue": "^0.1.0"
29
+ "@nusoft/nuflow-pack-nuos-build-catalogue": "^0.3.0",
30
+ "@nusoft/nuvector": "^0.1.5",
31
+ "@nusoft/nuwiki": "^0.3.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@nusoft/nuflow": "file:../nuflow",
@@ -177,6 +177,49 @@ if [[ -n "$locked_decisions" ]]; then
177
177
  EXIT_CODE=1
178
178
  fi
179
179
 
180
+ # ---------- Rule 3: STATE.md generated-region drift block (WU 113b Stage B) ---
181
+
182
+ # Only run when docs/build/STATE.md is in the staged changes.
183
+ # Guard on nuos-catalogue being present and supporting `state drift-check`.
184
+ # Fail-open: if the binary is absent, old (doesn't know drift-check), or
185
+ # errors for any infra reason, skip this check silently — a missing binary
186
+ # must never block all commits.
187
+ #
188
+ # Old-binary detection: an old binary (< 0.35.0) exits non-zero with
189
+ # "unknown state subcommand: drift-check" on stderr. We distinguish this
190
+ # from a genuine drift finding by checking whether the output contains the
191
+ # drift-specific marker phrase. If the output does NOT contain "generated regions"
192
+ # (the phrase only the new drift-check command emits), we skip.
193
+ staged_state_md=$(git diff --cached --name-only | grep -F 'docs/build/STATE.md' || true)
194
+
195
+ if [[ -n "$staged_state_md" ]]; then
196
+ dim "[nuos:pre-commit] STATE.md generated-region drift check (WU 113b)"
197
+
198
+ if ! command -v nuos-catalogue > /dev/null 2>&1; then
199
+ dim "[nuos:pre-commit] nuos-catalogue not found — skipping STATE.md drift check"
200
+ else
201
+ # Run drift-check; capture output + exit code.
202
+ drift_output=$(nuos-catalogue state drift-check 2>&1) || drift_exit=$?
203
+ drift_exit=${drift_exit:-0}
204
+
205
+ if [[ $drift_exit -ne 0 ]]; then
206
+ # Non-zero exit — check whether this is a genuine drift finding or an
207
+ # infra/version problem (old binary, missing store, etc.).
208
+ if echo "$drift_output" | grep -qF 'generated regions'; then
209
+ # Confirmed generated-region drift — block the commit.
210
+ red "✖ STATE.md generated-region drift — BLOCKED (WU 113b enforcement):"
211
+ echo "$drift_output" | while IFS= read -r line; do echo " $line"; done
212
+ log_event "state-drift-block" "generated-region drift detected"
213
+ EXIT_CODE=1
214
+ else
215
+ # Not a drift finding (unknown subcommand from old binary, infra error, etc.)
216
+ # — skip silently (fail open).
217
+ dim "[nuos:pre-commit] STATE.md drift check returned non-zero (not a drift finding) — skipping"
218
+ fi
219
+ fi
220
+ fi
221
+ fi
222
+
180
223
  # ---------- Result ------------------------------------------------------
181
224
 
182
225
  if [[ $EXIT_CODE -eq 0 ]]; then
@@ -177,6 +177,49 @@ if [[ -n "$locked_decisions" ]]; then
177
177
  EXIT_CODE=1
178
178
  fi
179
179
 
180
+ # ---------- Rule 3: STATE.md generated-region drift block (WU 113b Stage B) ---
181
+
182
+ # Only run when docs/build/STATE.md is in the staged changes.
183
+ # Guard on nuos-catalogue being present and supporting `state drift-check`.
184
+ # Fail-open: if the binary is absent, old (doesn't know drift-check), or
185
+ # errors for any infra reason, skip this check silently — a missing binary
186
+ # must never block all commits.
187
+ #
188
+ # Old-binary detection: an old binary (< 0.35.0) exits non-zero with
189
+ # "unknown state subcommand: drift-check" on stderr. We distinguish this
190
+ # from a genuine drift finding by checking whether the output contains the
191
+ # drift-specific marker phrase. If the output does NOT contain "generated regions"
192
+ # (the phrase only the new drift-check command emits), we skip.
193
+ staged_state_md=$(git diff --cached --name-only | grep -F 'docs/build/STATE.md' || true)
194
+
195
+ if [[ -n "$staged_state_md" ]]; then
196
+ dim "[nuos:pre-commit] STATE.md generated-region drift check (WU 113b)"
197
+
198
+ if ! command -v nuos-catalogue > /dev/null 2>&1; then
199
+ dim "[nuos:pre-commit] nuos-catalogue not found — skipping STATE.md drift check"
200
+ else
201
+ # Run drift-check; capture output + exit code.
202
+ drift_output=$(nuos-catalogue state drift-check 2>&1) || drift_exit=$?
203
+ drift_exit=${drift_exit:-0}
204
+
205
+ if [[ $drift_exit -ne 0 ]]; then
206
+ # Non-zero exit — check whether this is a genuine drift finding or an
207
+ # infra/version problem (old binary, missing store, etc.).
208
+ if echo "$drift_output" | grep -qF 'generated regions'; then
209
+ # Confirmed generated-region drift — block the commit.
210
+ red "✖ STATE.md generated-region drift — BLOCKED (WU 113b enforcement):"
211
+ echo "$drift_output" | while IFS= read -r line; do echo " $line"; done
212
+ log_event "state-drift-block" "generated-region drift detected"
213
+ EXIT_CODE=1
214
+ else
215
+ # Not a drift finding (unknown subcommand from old binary, infra error, etc.)
216
+ # — skip silently (fail open).
217
+ dim "[nuos:pre-commit] STATE.md drift check returned non-zero (not a drift finding) — skipping"
218
+ fi
219
+ fi
220
+ fi
221
+ fi
222
+
180
223
  # ---------- Result ------------------------------------------------------
181
224
 
182
225
  if [[ $EXIT_CODE -eq 0 ]]; then
@@ -17,7 +17,13 @@
17
17
  "database": null,
18
18
  "deployment": null,
19
19
  "externalServices": [],
20
- "notes": null
20
+ "notes": null,
21
+ "services": {
22
+ "database": { "provider": "none", "migrations": "none", "deployOnMerge": false, "secrets": [] },
23
+ "jobs": { "provider": "none", "deployOnMerge": false, "secrets": [] },
24
+ "web": { "provider": "none", "autoDeploy": false, "secrets": [] }
25
+ },
26
+ "servicesComment": "Backend service wiring (D150 / WU 231). Per service: 'provider', whether it 'deployOnMerge' to main, and the 'secrets' it needs; 'database' also names its 'migrations' tool; 'web' declares 'autoDeploy' (e.g. Vercel's Git integration). Set provider to 'none' to opt a service out. When a provider is set, `nuos init` scaffolds the deploy automation (build packages → migrate deploy → jobs deploy via the installed CLI) and the doctor verifies the secrets exist and flags deploy drift. Providers — database: supabase|neon|railway|none; jobs: trigger.dev|none; web: vercel|none."
21
27
  },
22
28
  "testing": {
23
29
  "framework": "vitest",