@nusoft/nuos-build-catalogue 0.33.1 → 0.35.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.
- package/README.md +3 -3
- package/dist/cli.js +48 -0
- package/dist/commands/end-of-session.js +67 -14
- package/dist/commands/memory.d.ts +9 -2
- package/dist/commands/memory.js +167 -7
- package/dist/commands/state-compile.d.ts +108 -0
- package/dist/commands/state-compile.js +793 -0
- package/dist/embedder/ollama.d.ts +7 -0
- package/dist/embedder/ollama.js +27 -1
- package/dist/path-resolution.d.ts +9 -0
- package/dist/path-resolution.js +16 -0
- package/package.json +5 -4
- package/scripts/hooks/pre-commit +71 -4
- package/templates/hooks/pre-commit +71 -4
|
@@ -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)
|
package/dist/embedder/ollama.js
CHANGED
|
@@ -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({
|
|
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) {
|
|
@@ -52,6 +52,15 @@ export declare function resolveCatalogueRoot(flag: string | boolean | undefined,
|
|
|
52
52
|
export declare function resolveIndexDir(buildRoot: string, ctx?: ResolutionContext): string;
|
|
53
53
|
export declare function resolveWorkflowsPath(buildRoot: string, flag: string | boolean | undefined, ctx?: ResolutionContext): string;
|
|
54
54
|
export declare function resolveIndexPath(buildRoot: string, flag: string | boolean | undefined, ctx?: ResolutionContext): string;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the cross-agent memory store path. Always co-located with the
|
|
57
|
+
* doc-index in the same `.nuos-catalogue/` directory, but in a separate
|
|
58
|
+
* file (`memory.nv`) so that the doc-index reindex (which holds an
|
|
59
|
+
* exclusive lock on `index.nv`) never contends with memory writes.
|
|
60
|
+
* Resolves `NUOS_CATALOGUE_MEMORY_PATH` env var when set; otherwise
|
|
61
|
+
* derives from `resolveIndexDir`. See D131.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveMemoryPath(buildRoot: string, flag: string | boolean | undefined, ctx?: ResolutionContext): string;
|
|
55
64
|
export declare function resolveHashPath(buildRoot: string, flag: string | boolean | undefined, ctx?: ResolutionContext): string;
|
|
56
65
|
/**
|
|
57
66
|
* Soft warning surfaced after a `migrate` or `regenerate` run: if the
|
package/dist/path-resolution.js
CHANGED
|
@@ -108,6 +108,22 @@ export function resolveIndexPath(buildRoot, flag, ctx) {
|
|
|
108
108
|
return path.resolve(flag);
|
|
109
109
|
return path.join(resolveIndexDir(buildRoot, ctx), 'index.nv');
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the cross-agent memory store path. Always co-located with the
|
|
113
|
+
* doc-index in the same `.nuos-catalogue/` directory, but in a separate
|
|
114
|
+
* file (`memory.nv`) so that the doc-index reindex (which holds an
|
|
115
|
+
* exclusive lock on `index.nv`) never contends with memory writes.
|
|
116
|
+
* Resolves `NUOS_CATALOGUE_MEMORY_PATH` env var when set; otherwise
|
|
117
|
+
* derives from `resolveIndexDir`. See D131.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveMemoryPath(buildRoot, flag, ctx) {
|
|
120
|
+
if (typeof flag === 'string' && flag.length > 0)
|
|
121
|
+
return path.resolve(flag);
|
|
122
|
+
const env = ctxEnv(ctx);
|
|
123
|
+
if (env.NUOS_CATALOGUE_MEMORY_PATH)
|
|
124
|
+
return path.resolve(env.NUOS_CATALOGUE_MEMORY_PATH);
|
|
125
|
+
return path.join(resolveIndexDir(buildRoot, ctx), 'memory.nv');
|
|
126
|
+
}
|
|
111
127
|
export function resolveHashPath(buildRoot, flag, ctx) {
|
|
112
128
|
if (typeof flag === 'string' && flag.length > 0)
|
|
113
129
|
return path.resolve(flag);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nusoft/nuos-build-catalogue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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": {
|
|
@@ -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",
|
|
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.
|
|
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",
|
package/scripts/hooks/pre-commit
CHANGED
|
@@ -134,14 +134,38 @@ fi
|
|
|
134
134
|
# ---------- Rule 2: active-decision modification block (WU 111 ship) ---
|
|
135
135
|
|
|
136
136
|
dim "[nuos:pre-commit] active-decision modification check"
|
|
137
|
-
|
|
137
|
+
#
|
|
138
|
+
# "Immutable once accepted" — this blocks edits only to a decision whose
|
|
139
|
+
# status *in HEAD* is already a locked state (`accepted` or `active`).
|
|
140
|
+
# Editing a still-`proposed` decision is allowed: promoting it to
|
|
141
|
+
# accepted/active is the sanctioned lifecycle step, not a violation, and
|
|
142
|
+
# proposed decisions are in-flight by design. New decision files are
|
|
143
|
+
# additions (excluded by --diff-filter=M), so a decision born `accepted`
|
|
144
|
+
# is never blocked on creation. The locked-status check uses the HEAD
|
|
145
|
+
# pre-image, so flipping an accepted decision back to `proposed` to sneak
|
|
146
|
+
# a substantive edit is still caught.
|
|
147
|
+
candidate_decisions=$(git diff --cached --name-only --diff-filter=M \
|
|
138
148
|
| grep -E '^docs/build/decisions/D[0-9]+.*\.md$' \
|
|
139
149
|
| grep -v '/superseded/' \
|
|
140
150
|
|| true)
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
locked_decisions=""
|
|
153
|
+
if [[ -n "$candidate_decisions" ]]; then
|
|
154
|
+
while IFS= read -r f; do
|
|
155
|
+
[[ -z "$f" ]] && continue
|
|
156
|
+
head_status=$(git show "HEAD:$f" 2>/dev/null \
|
|
157
|
+
| grep -m1 -E '^\*\*Status:\*\*' \
|
|
158
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*//' \
|
|
159
|
+
| awk '{print tolower($1)}')
|
|
160
|
+
case "$head_status" in
|
|
161
|
+
accepted|active) locked_decisions+="${f}"$'\n' ;;
|
|
162
|
+
esac
|
|
163
|
+
done <<< "$candidate_decisions"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
if [[ -n "$locked_decisions" ]]; then
|
|
143
167
|
red "✖ active-decision modification — BLOCKED (WU 111 enforcement):"
|
|
144
|
-
while IFS= read -r f; do echo " — $f"; done <<< "$
|
|
168
|
+
while IFS= read -r f; do [[ -n "$f" ]] && echo " — $f"; done <<< "$locked_decisions"
|
|
145
169
|
red " Decisions are immutable once accepted. The discipline is to write a"
|
|
146
170
|
red " superseding D-NNN+1 and link forward. Use:"
|
|
147
171
|
red " nuos-catalogue decision supersede <target> --by=<new-D> --reason=\"...\""
|
|
@@ -149,10 +173,53 @@ if [[ -n "$modified_decisions" ]]; then
|
|
|
149
173
|
red " If this edit is a non-substantive typo fix or link cleanup that does"
|
|
150
174
|
red " not change the decision's meaning, you may bypass this block with"
|
|
151
175
|
red " --no-verify. CLAUDE.md prohibits --no-verify for substantive changes."
|
|
152
|
-
log_event "active-decision-block" "$(echo "$
|
|
176
|
+
log_event "active-decision-block" "$(echo "$locked_decisions" | tr '\n' ',')"
|
|
153
177
|
EXIT_CODE=1
|
|
154
178
|
fi
|
|
155
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
|
+
|
|
156
223
|
# ---------- Result ------------------------------------------------------
|
|
157
224
|
|
|
158
225
|
if [[ $EXIT_CODE -eq 0 ]]; then
|
|
@@ -134,14 +134,38 @@ fi
|
|
|
134
134
|
# ---------- Rule 2: active-decision modification block (WU 111 ship) ---
|
|
135
135
|
|
|
136
136
|
dim "[nuos:pre-commit] active-decision modification check"
|
|
137
|
-
|
|
137
|
+
#
|
|
138
|
+
# "Immutable once accepted" — this blocks edits only to a decision whose
|
|
139
|
+
# status *in HEAD* is already a locked state (`accepted` or `active`).
|
|
140
|
+
# Editing a still-`proposed` decision is allowed: promoting it to
|
|
141
|
+
# accepted/active is the sanctioned lifecycle step, not a violation, and
|
|
142
|
+
# proposed decisions are in-flight by design. New decision files are
|
|
143
|
+
# additions (excluded by --diff-filter=M), so a decision born `accepted`
|
|
144
|
+
# is never blocked on creation. The locked-status check uses the HEAD
|
|
145
|
+
# pre-image, so flipping an accepted decision back to `proposed` to sneak
|
|
146
|
+
# a substantive edit is still caught.
|
|
147
|
+
candidate_decisions=$(git diff --cached --name-only --diff-filter=M \
|
|
138
148
|
| grep -E '^docs/build/decisions/D[0-9]+.*\.md$' \
|
|
139
149
|
| grep -v '/superseded/' \
|
|
140
150
|
|| true)
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
locked_decisions=""
|
|
153
|
+
if [[ -n "$candidate_decisions" ]]; then
|
|
154
|
+
while IFS= read -r f; do
|
|
155
|
+
[[ -z "$f" ]] && continue
|
|
156
|
+
head_status=$(git show "HEAD:$f" 2>/dev/null \
|
|
157
|
+
| grep -m1 -E '^\*\*Status:\*\*' \
|
|
158
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*//' \
|
|
159
|
+
| awk '{print tolower($1)}')
|
|
160
|
+
case "$head_status" in
|
|
161
|
+
accepted|active) locked_decisions+="${f}"$'\n' ;;
|
|
162
|
+
esac
|
|
163
|
+
done <<< "$candidate_decisions"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
if [[ -n "$locked_decisions" ]]; then
|
|
143
167
|
red "✖ active-decision modification — BLOCKED (WU 111 enforcement):"
|
|
144
|
-
while IFS= read -r f; do echo " — $f"; done <<< "$
|
|
168
|
+
while IFS= read -r f; do [[ -n "$f" ]] && echo " — $f"; done <<< "$locked_decisions"
|
|
145
169
|
red " Decisions are immutable once accepted. The discipline is to write a"
|
|
146
170
|
red " superseding D-NNN+1 and link forward. Use:"
|
|
147
171
|
red " nuos-catalogue decision supersede <target> --by=<new-D> --reason=\"...\""
|
|
@@ -149,10 +173,53 @@ if [[ -n "$modified_decisions" ]]; then
|
|
|
149
173
|
red " If this edit is a non-substantive typo fix or link cleanup that does"
|
|
150
174
|
red " not change the decision's meaning, you may bypass this block with"
|
|
151
175
|
red " --no-verify. CLAUDE.md prohibits --no-verify for substantive changes."
|
|
152
|
-
log_event "active-decision-block" "$(echo "$
|
|
176
|
+
log_event "active-decision-block" "$(echo "$locked_decisions" | tr '\n' ',')"
|
|
153
177
|
EXIT_CODE=1
|
|
154
178
|
fi
|
|
155
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
|
+
|
|
156
223
|
# ---------- Result ------------------------------------------------------
|
|
157
224
|
|
|
158
225
|
if [[ $EXIT_CODE -eq 0 ]]; then
|