@ainova-systems/intelligence 0.19.0 → 0.20.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.
@@ -51,7 +51,7 @@ case "$sub" in
51
51
  echo " index.yaml reachable — $n package(s):"
52
52
  qmap_keys "$index" "packages" | sed 's/^/ /'
53
53
  elif [ "$force" -eq 1 ]; then
54
- echo " WARN: no index.yaml at $url — recording anyway (--force)." >&2
54
+ echo "WARNING: no index.yaml at $url — recording anyway (--force)." >&2
55
55
  else
56
56
  echo "ERROR: no index.yaml found at $url — not adding it." >&2
57
57
  echo " A registry is a git repo with index.yaml at its root, mapping names to sources:" >&2
@@ -168,7 +168,7 @@ case "$action" in
168
168
  echo "added: $dir"
169
169
  fi
170
170
  if [ ! -d "$IP_ROOT/$dir" ]; then
171
- echo " WARN: $dir does not exist yet — sync skips a missing source and renders it once the directory appears." >&2
171
+ echo "WARNING: $dir does not exist yet — sync skips a missing source and renders it once the directory appears." >&2
172
172
  fi
173
173
  print_section "$manifest" "$IP_ROOT" "$section"
174
174
  echo "Run 'intelligence sync' to render it."
@@ -1,14 +1,27 @@
1
1
  #!/bin/bash
2
- # intelligence sync [target] [--compact] [--force] - render intelligence to enabled tools.
2
+ # intelligence sync [target] [--compact] [--force] [--check] - render intelligence
3
+ # to enabled tools, or report whether rendering would change anything.
3
4
  set -euo pipefail
4
5
 
6
+ # Only sync_check may request a comparison from the engine.
7
+ unset IS_SYNC_CHECK IS_SYNC_CHECK_ROOT
8
+
5
9
  # Print before library loading and lifecycle checks so a content verification
6
- # never looks idle. Compact output remains a buffered machine-readable report.
10
+ # never looks idle. Compact output remains a buffered machine-readable report,
11
+ # and a check prints only its verdict.
7
12
  compact=0
13
+ check=0
8
14
  for argument in "$@"; do
9
- [ "$argument" != --compact ] || compact=1
15
+ case "$argument" in
16
+ --compact) compact=1 ;;
17
+ --check) check=1 ;;
18
+ esac
10
19
  done
11
- [ "$compact" -ne 0 ] || echo 'Checking project...'
20
+ [ "$compact" -ne 0 ] || [ "$check" -ne 0 ] || echo 'Checking project...'
21
+ # The CLI's package descriptor is read in the same pass as the project's
22
+ # manifest and lock, once this command holds the project lock.
23
+ # shellcheck disable=SC2034 # read by cli-common.sh while it loads
24
+ CLI_DEFER_SYNC_PKG=1
12
25
  source "$CLI_DIR/lib/cli-common.sh"
13
26
  force=0
14
27
  target=""
@@ -16,22 +29,35 @@ while [ $# -gt 0 ]; do
16
29
  case "$1" in
17
30
  --compact) compact=1 ;;
18
31
  --force) force=1 ;;
32
+ --check) check=1 ;;
19
33
  -*) die "unknown option '$1'" ;;
20
34
  *)
21
- [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force]"
35
+ [ -z "$target" ] || die "usage: intelligence sync [adapter] [--compact] [--force] [--check]"
22
36
  target="$1"
23
37
  ;;
24
38
  esac
25
39
  shift
26
40
  done
27
41
 
42
+ # Set where a check concludes that sync is needed, so a failure that happens to
43
+ # end in the same status is never mistaken for that verdict.
44
+ IS_CHECK_OUT_OF_DATE=0
45
+
46
+ # hold_project — take the project lock, then read the package descriptor,
47
+ # manifest and lock once for the whole preflight.
48
+ hold_project() {
49
+ project_lock_hold "$IP_ROOT"
50
+ project_reads_preload "$IP_ROOT"
51
+ load_sync_package_identity
52
+ }
53
+
28
54
  run_sync() {
29
55
  detect_project || return $?
30
56
  case "$IP_MODE" in
31
57
  cli)
32
58
  # One writer per project at a time: the store restore and the
33
59
  # render below both rewrite shared state (sync-lock.sh).
34
- project_lock_hold "$IP_ROOT"
60
+ hold_project
35
61
  # `sync` is the normal fresh-clone command. A newer globally installed
36
62
  # CLI aligns the current Intelligence project first (except in CI, where a
37
63
  # tracked migration must be reviewed and committed locally), and a
@@ -42,6 +68,7 @@ run_sync() {
42
68
  sync_with_cache "$target" "$force" || return $?
43
69
  ;;
44
70
  legacy)
71
+ load_sync_package_identity
45
72
  [ "$force" -eq 0 ] || die "--force is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
46
73
  [ "$compact" -eq 0 ] || die "--compact is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
47
74
  # A vendored project syncs with its own engine: its pin is the
@@ -55,11 +82,56 @@ run_sync() {
55
82
  fi
56
83
  ;;
57
84
  *)
85
+ load_sync_package_identity
58
86
  die "no intelligence project found here - run 'intelligence init'"
59
87
  ;;
60
88
  esac
61
89
  }
62
90
 
91
+ # run_check — the same preflight as sync, except that tracked alignment is never
92
+ # applied (it is reported as needed, in CI or not) and everything but the
93
+ # verdict goes to stderr. Restoring the ignored package store from the lock is
94
+ # allowed: it is reproducible state, and the comparison needs it.
95
+ run_check() {
96
+ local stamp
97
+ detect_project || return $?
98
+ case "$IP_MODE" in
99
+ cli) ;;
100
+ legacy)
101
+ load_sync_package_identity
102
+ die "--check is unavailable for a legacy Intelligence Sync project - run 'intelligence init' to convert it first"
103
+ ;;
104
+ *)
105
+ load_sync_package_identity
106
+ die "no intelligence project found here - run 'intelligence init'"
107
+ ;;
108
+ esac
109
+ hold_project
110
+ project_preflight "$IP_ROOT" || return $?
111
+ if project_needs_upgrade "$IP_ROOT"; then
112
+ read_schema_version_var "$IP_ROOT/intelligence.yaml"
113
+ stamp="$IS_SCHEMA_VERSION"
114
+ bundled_engine_version_var
115
+ IS_CHECK_OUT_OF_DATE=1
116
+ is_status out-of-date "the project needs alignment (stamp ${stamp:-unstamped}, engine $IS_BUNDLED_ENGINE_VERSION); run 'intelligence init --apply' locally, review and commit the diff"
117
+ return "$IS_RC_OUT_OF_DATE"
118
+ fi
119
+ restore_project_store_if_missing "$IP_ROOT" keep-sources >&2 || return $?
120
+ export_engine_env "$IP_ROOT" || return $?
121
+ sync_check "$target" "$force" || return $?
122
+ }
123
+
124
+ if [ "$check" -eq 1 ]; then
125
+ # --compact changes nothing here: the verdict is already one status line,
126
+ # and a failure's diagnostics already reach stderr.
127
+ rc=0
128
+ run_check || rc=$?
129
+ if [ "$rc" = "$IS_RC_OUT_OF_DATE" ] && [ "$IS_CHECK_OUT_OF_DATE" != 1 ]; then
130
+ rc=1
131
+ fi
132
+ exit "$rc"
133
+ fi
134
+
63
135
  if [ "$compact" -eq 0 ]; then
64
136
  run_sync
65
137
  exit $?
package/cli/intelligence CHANGED
@@ -11,7 +11,11 @@ case "$0" in
11
11
  */*) CLI_DIR="${0%/*}"; [ -n "$CLI_DIR" ] || CLI_DIR="/" ;;
12
12
  *) CLI_DIR="." ;;
13
13
  esac
14
- CLI_DIR="$(cd "$CLI_DIR" && pwd)"
14
+ # An absolute spelling without `.` or `..` segments or doubled slashes is
15
+ # already what `cd && pwd` prints; on Git Bash that subshell costs a fork.
16
+ case "$CLI_DIR" in
17
+ */|*/./*|*/.|*/../*|*/..|*//*|[!/]*) CLI_DIR="$(cd "$CLI_DIR" && pwd)" ;;
18
+ esac
15
19
  export CLI_DIR
16
20
 
17
21
  # Engine discovery, by role (sync.sh + VERSION). The repo and the npm package
@@ -52,9 +56,10 @@ Usage: intelligence <command> [args]
52
56
  init [--preview|--apply] Initialize, convert, restore or align this project
53
57
  --targets a,b --dir name --bare --no-sync New-project options
54
58
  --force Accept a dirty worktree during legacy project conversion
55
- sync [adapter] [--compact] [--force] Restore locked content, then render enabled adapters
59
+ sync [adapter] [--compact] [--force] [--check] Restore locked content, then render enabled adapters
56
60
  --compact On success show context, warnings and final status; on failure show diagnostics
57
61
  --force Render again and refresh discovery of unconfigured source directories
62
+ --check Change no generated file; exit 0 when up to date, 2 when sync is needed
58
63
  update [@scope/name] Show an update plan; ask before applying it
59
64
  --preview Show the plan without asking or writing
60
65
  --apply Apply the plan without asking
@@ -109,4 +114,12 @@ if [ ! -f "$cmd_file" ]; then
109
114
  echo "ERROR: unknown command '$cmd'. Run 'intelligence help'." >&2
110
115
  exit 1
111
116
  fi
117
+ # `sync` runs on every save hook and CI check, so it runs in this process: a
118
+ # second bash start is a large share of an unchanged sync on Git Bash. It
119
+ # exits on every path and reads nothing of this script but the exports.
120
+ if [ "$cmd" = sync ]; then
121
+ # shellcheck source=commands/sync.sh
122
+ source "$cmd_file"
123
+ exit $?
124
+ fi
112
125
  exec bash "$cmd_file" "$@"
@@ -435,7 +435,7 @@ while IFS="$LOCK_SEP" read -r pack name url ref mirror; do
435
435
  # Containment before deletion: a mirror value is config input, and rm -rf
436
436
  # must never follow it out of the repository.
437
437
  case "$mirror" in
438
- ""|/*|*..*) [ -n "$mirror" ] && echo " WARN: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
438
+ ""|/*|*..*) [ -n "$mirror" ] && echo "WARNING: mirror '$mirror' not repo-contained — left in place" >&2; continue ;;
439
439
  esac
440
440
  if [ -d "$root/$mirror" ]; then
441
441
  rm -rf "${root:?}/$mirror"
@@ -63,7 +63,7 @@ if [ -n "$manifest" ]; then
63
63
  [ -n "$url" ] || continue
64
64
  index="$(_fetch_index "${url#git+}")"
65
65
  if [ -z "$index" ]; then
66
- echo " WARN: registry unreachable or missing index.yaml: $url" >&2
66
+ echo "WARNING: registry unreachable or missing index.yaml: $url" >&2
67
67
  continue
68
68
  fi
69
69
  emit_index "$index" "$url"
@@ -85,7 +85,7 @@ while IFS= read -r name; do
85
85
  if [ "$probe_rc" -ne 0 ]; then
86
86
  # No answer is not "no change": reporting up to date here would
87
87
  # freeze the pin exactly as the ref-name comparison used to.
88
- echo " WARN: $name: cannot reach $url — ref '$ref' was not checked" >&2
88
+ echo "WARNING: $name: cannot reach $url — ref '$ref' was not checked" >&2
89
89
  echo " $name: $ref (not checked — remote unreachable)"
90
90
  continue
91
91
  fi
@@ -104,7 +104,7 @@ while IFS= read -r name; do
104
104
  echo " $name: $(short_sha "$ref") (pinned commit)"
105
105
  ;;
106
106
  *)
107
- echo " WARN: $name pins ref '$ref', which $url no longer advertises" >&2
107
+ echo "WARNING: $name pins ref '$ref', which $url no longer advertises" >&2
108
108
  echo " $name: $ref (unresolvable — gone upstream)"
109
109
  ;;
110
110
  esac
@@ -7,12 +7,13 @@
7
7
  set -euo pipefail
8
8
  source "$CLI_DIR/lib/cli-common.sh"
9
9
 
10
- frozen=0 force=0 no_sync=0
10
+ frozen=0 force=0 no_sync=0 keep_sources=0
11
11
  while [ $# -gt 0 ]; do
12
12
  case "$1" in
13
13
  --frozen) frozen=1 ;;
14
14
  --force) force=1 ;;
15
15
  --no-sync) no_sync=1 ;;
16
+ --keep-sources) keep_sources=1 ;;
16
17
  *) die "unknown option '$1'" ;;
17
18
  esac
18
19
  shift || true
@@ -117,7 +118,7 @@ if [ -f "$lock" ]; then
117
118
  mkdir -p "$(dirname "$IP_ROOT/$rel")"
118
119
  mv "$staging" "$IP_ROOT/$rel"
119
120
  store_record_set "$IP_ROOT" "$name" "$url" "$path" "$resolved" "$sha"
120
- wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
121
+ [ "$keep_sources" = 1 ] || wire_package_sources "$manifest" "$name" "$rel" "$IP_ROOT"
121
122
  echo "= $name@${resolved:-HEAD}"
122
123
  done < <(lock_to_tsv "$lock")
123
124
  fi
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
- # Shared plumbing for every CLI command. Commands are separate processes (the
3
- # dispatcher execs them), so each one sources this file first.
2
+ # Shared plumbing for every CLI command. Each command is its own script (the
3
+ # dispatcher execs it, or sources `sync`), so each one sources this file first.
4
4
  #
5
5
  # All YAML reading goes through the engine's lib/common.sh — the CLI never
6
6
  # grows a parallel parser for shapes the engine can already read. The only
@@ -99,23 +99,32 @@ source "$CLI_DIR/lib/sync-lock.sh"
99
99
  # Its identity is DATA shipped with the distribution (cli/engine-package.yaml),
100
100
  # never a name compiled into cli code — a fork edits the file.
101
101
  _EPKG="$CLI_DIR/engine-package.yaml"
102
- SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH=""
102
+ _EPKG_FIELDS='name url path default_registry'
103
+ SYNC_PKG_NAME="" SYNC_PKG_URL="" SYNC_PKG_PATH="" SYNC_PKG_STORE=""
103
104
  # Read by commands (init seeds it) — per-file shellcheck cannot see that.
104
105
  # shellcheck disable=SC2034
105
106
  DEFAULT_REGISTRY_URL=""
106
- # Every command loads this file first: read the four fields in one pass.
107
+
108
+ # load_sync_package_identity — read the four fields in one pass. Every command
109
+ # loads this file first and gets them at once; `sync` sets CLI_DEFER_SYNC_PKG
110
+ # before sourcing and reads them in the same awk pass as the project's manifest
111
+ # and lock (project_reads_preload), once it holds the project lock.
107
112
  # shellcheck disable=SC2034 # DEFAULT_REGISTRY_URL is read by commands, as above
108
- while IFS="$LOCK_SEP" read -r _epkg_key _epkg_value; do
109
- case "$_epkg_key" in
110
- name) SYNC_PKG_NAME="$_epkg_value" ;;
111
- url) SYNC_PKG_URL="$_epkg_value" ;;
112
- path) SYNC_PKG_PATH="$_epkg_value" ;;
113
- default_registry) DEFAULT_REGISTRY_URL="$_epkg_value" ;;
114
- esac
115
- done <<< "$(_qmap_read tops "$_EPKG" '' 'name url path default_registry')"
116
- unset _epkg_key _epkg_value
117
- [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
118
- SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
113
+ load_sync_package_identity() {
114
+ local key value
115
+ _qmap_read_var tops "$_EPKG" '' "$_EPKG_FIELDS" || true
116
+ while IFS="$LOCK_SEP" read -r key value; do
117
+ case "$key" in
118
+ name) SYNC_PKG_NAME="$value" ;;
119
+ url) SYNC_PKG_URL="$value" ;;
120
+ path) SYNC_PKG_PATH="$value" ;;
121
+ default_registry) DEFAULT_REGISTRY_URL="$value" ;;
122
+ esac
123
+ done <<< "$IS_QMAP_OUT"
124
+ [ -n "$SYNC_PKG_NAME" ] || die "corrupt CLI installation: $_EPKG is missing or has no 'name'"
125
+ SYNC_PKG_STORE=".intelligence/packages/$SYNC_PKG_NAME"
126
+ }
127
+ [ "${CLI_DEFER_SYNC_PKG:-0}" = 1 ] || load_sync_package_identity
119
128
 
120
129
  # --- Project detection ---------------------------------------------------
121
130
  # Sets: IP_MODE (cli|legacy|none), IP_ROOT, IP_UMBRELLA, IP_MODULE_DIR.
@@ -257,9 +266,14 @@ source_entry_problem() {
257
266
  }
258
267
 
259
268
  manifest_intelligence_dir() {
260
- local manifest="$1" v
261
- v="$(get_yaml_field "$manifest" "project" "intelligence_dir")"
262
- printf '%s' "${v:-intelligence}"
269
+ manifest_intelligence_dir_var "$1"
270
+ printf '%s' "$IS_CONTENT_DIR"
271
+ }
272
+
273
+ # manifest_intelligence_dir_var <manifest> — the same answer in IS_CONTENT_DIR.
274
+ manifest_intelligence_dir_var() {
275
+ get_yaml_field_var "$1" "project" "intelligence_dir"
276
+ IS_CONTENT_DIR="${IS_YAML_FIELD:-intelligence}"
263
277
  }
264
278
 
265
279
  default_target_output() {
@@ -317,7 +331,8 @@ sync_pkg_install() {
317
331
  export_engine_env() {
318
332
  local root="$1"
319
333
  local content_rel
320
- content_rel="$(manifest_intelligence_dir "$root/intelligence.yaml")"
334
+ manifest_intelligence_dir_var "$root/intelligence.yaml"
335
+ content_rel="$IS_CONTENT_DIR"
321
336
  export IS_CLI=1
322
337
  export CONFIG_FILE="$root/intelligence.yaml"
323
338
  export REPO_ROOT="$root"
@@ -382,6 +397,7 @@ project_stamped_ahead() {
382
397
 
383
398
  project_needs_upgrade() {
384
399
  local root="$1" manifest="$1/intelligence.yaml" stamp eng pinned locked name url path rows
400
+ local lname _requested _url _path resolved _sha
385
401
  [ -f "$manifest" ] || return 1
386
402
  read_schema_version_var "$manifest"
387
403
  stamp="$IS_SCHEMA_VERSION"
@@ -392,12 +408,14 @@ project_needs_upgrade() {
392
408
  # restamp the schema and re-pin the engine content DOWNWARD, and the next
393
409
  # teammate on the current CLI would move both back: leave it as found.
394
410
  project_stamped_ahead "$root" && return 1
395
- [ -n "$(top_scalar "$manifest" "sync_version")" ] && return 0
411
+ _qmap_read_var top "$manifest" '' sync_version || true
412
+ [ -z "$IS_QMAP_OUT" ] || return 0
396
413
  _ver_gt "$eng" "$stamp" && return 0
397
414
  [ -d "$root/.intelligence/engine" ] && return 0
398
415
 
399
416
  # One manifest pass for every package's fields, not a reader per field.
400
- rows="$(_qmap_read fieldrows "$manifest" packages '' 'url path version')"
417
+ _qmap_read_var fieldrows "$manifest" packages '' 'url path version' || true
418
+ rows="$IS_QMAP_OUT"
401
419
  while IFS="$LOCK_SEP" read -r name url path pinned; do
402
420
  [ -n "$name" ] || continue
403
421
  # Early RC manifests mixed requested intent with resolved source
@@ -405,7 +423,14 @@ project_needs_upgrade() {
405
423
  [ -n "$url" ] && return 0
406
424
  [ -n "$path" ] && return 0
407
425
  if [ "$name" = "$SYNC_PKG_NAME" ]; then
408
- locked="$(qmap_field "$root/intelligence.lock" "packages" "$name" "resolved")"
426
+ # The first lock row for the name: what qmap_field would decode.
427
+ locked=""
428
+ _qmap_read_var rows "$root/intelligence.lock" packages || true
429
+ while IFS="$LOCK_SEP" read -r lname _requested _url _path resolved _sha; do
430
+ [ "$lname" = "$name" ] || continue
431
+ locked="$resolved"
432
+ break
433
+ done <<< "$IS_QMAP_OUT"
409
434
  [ "$pinned" = "$eng" ] || return 0
410
435
  if [ -f "$root/intelligence.lock" ]; then
411
436
  [ -n "$locked" ] || return 0
@@ -420,21 +445,30 @@ project_needs_upgrade() {
420
445
  # which lifecycle alignment can proceed. Never manufacture a partial lock.
421
446
  project_has_packages() {
422
447
  local root="$1" name found=1
448
+ _qmap_read_var keys "$root/intelligence.yaml" "packages" || true
423
449
  while IFS= read -r name; do
424
450
  [ -n "$name" ] || continue
425
451
  assert_valid_pkg_name "$name"
426
452
  found=0
427
- done < <(qmap_keys "$root/intelligence.yaml" "packages")
453
+ done <<< "$IS_QMAP_OUT"
428
454
  return "$found"
429
455
  }
430
456
 
431
- ensure_project_current() {
432
- local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
433
- check_version_compat "$manifest" || return $?
457
+ # project_preflight <root> — what every project-aware command checks before it
458
+ # may align or write: schema compatibility (a newer major refuses with 4), a
459
+ # valid lock, and a lock for whatever packages the manifest declares.
460
+ project_preflight() {
461
+ local root="$1"
462
+ check_version_compat "$root/intelligence.yaml" || return $?
434
463
  validate_project_lock "$root" || return $?
435
464
  if project_has_packages "$root" && [ ! -f "$root/intelligence.lock" ]; then
436
465
  die "manifest declares packages but intelligence.lock is absent — restore the committed lock before running project lifecycle commands"
437
466
  fi
467
+ }
468
+
469
+ ensure_project_current() {
470
+ local root="$1" explicit="${2:-}" manifest="$1/intelligence.yaml" stamp eng
471
+ project_preflight "$root" || return $?
438
472
  project_needs_upgrade "$root" || return 0
439
473
  read_schema_version_var "$manifest"
440
474
  stamp="$IS_SCHEMA_VERSION"
@@ -444,7 +478,8 @@ ensure_project_current() {
444
478
  die "project lifecycle requires alignment (stamp ${stamp:-unstamped}, engine $eng) — run 'intelligence init --apply' locally, review and commit the diff"
445
479
  fi
446
480
  echo " project alignment: stamp ${stamp:-unstamped}, engine $eng"
447
- bash "$CLI_DIR/internal/align-project.sh" --no-sync
481
+ bash "$CLI_DIR/internal/align-project.sh" --no-sync || return $?
482
+ project_reads_refresh "$root"
448
483
  }
449
484
 
450
485
  # A present lock is validated even when there is no missing store to restore.
@@ -452,7 +487,8 @@ ensure_project_current() {
452
487
  validate_project_lock() {
453
488
  local lock="$1/intelligence.lock" output line rc=0
454
489
  if [ -e "$lock" ] || [ -L "$lock" ]; then
455
- output="$(lock_validate "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
490
+ # The capture is the subshell that contains the guards' `die`.
491
+ output="$(lock_validate_checks "$lock" "${2---metadata}" "$1/intelligence.yaml" 2>&1)" || rc=$?
456
492
  while IFS= read -r line; do
457
493
  [ -n "$line" ] || continue
458
494
  case "$line" in
@@ -472,6 +508,74 @@ validate_project_lock() {
472
508
  fi
473
509
  }
474
510
 
511
+ # --- Reads shared by one command's preflight -----------------------------
512
+ # A sync reads the CLI's package descriptor, the manifest and the lock several
513
+ # times on its way to the engine. Once it holds the project lock it answers all
514
+ # of those reads with one qmap pass and one pass of the engine's manifest
515
+ # readers; a child that may rewrite either document (alignment, restore) is
516
+ # followed by project_reads_refresh. Without a preload every reader takes its
517
+ # ordinary path.
518
+ IS_PROJECT_READS_ROOT=""
519
+
520
+ project_reads_preload() {
521
+ local root="$1" manifest="$1/intelligence.yaml" lock="$1/intelligence.lock" s=$'\037'
522
+ local qmap=0 view=0 out qmap_pid="" view_pid=""
523
+ local -a view_args=("$manifest" project intelligence_dir rules agents skills)
524
+ project_reads_reset
525
+ qmap_preload_plan \
526
+ "tops$s$_EPKG$s$s$_EPKG_FIELDS$s$s" \
527
+ "keys$s$manifest${s}packages$s$s$s" \
528
+ "fieldrows$s$manifest${s}packages$s${s}url path version$s" \
529
+ "top$s$manifest$s${s}sync_version$s$s" \
530
+ "lock$s$lock${s}packages$s$s$s$LOCKFILE_VERSION" \
531
+ "rows$s$lock${s}packages$s$s$s" && qmap=1
532
+ [ ! -f "$manifest" ] || [ ! -r "$manifest" ] || view=1
533
+ if project_lock_scratch "$root"; then
534
+ # Both passes at once: starting their processes is most of their cost.
535
+ if [ "$qmap" = 1 ]; then
536
+ qmap_preload_run > "$IS_LOCK_SCRATCH.qmap" &
537
+ qmap_pid=$!
538
+ fi
539
+ if [ "$view" = 1 ]; then
540
+ manifest_view_run "${view_args[@]}" > "$IS_LOCK_SCRATCH.view" &
541
+ view_pid=$!
542
+ fi
543
+ if [ "$qmap" = 1 ] && wait "$qmap_pid"; then
544
+ qmap_preload_store "$(< "$IS_LOCK_SCRATCH.qmap")"
545
+ fi
546
+ if [ "$view" = 1 ]; then
547
+ if wait "$view_pid"; then
548
+ manifest_view_store "$(< "$IS_LOCK_SCRATCH.view")" "${view_args[@]}"
549
+ else
550
+ manifest_view_reset
551
+ fi
552
+ fi
553
+ else
554
+ if [ "$qmap" = 1 ] && out="$(qmap_preload_run)"; then
555
+ qmap_preload_store "$out"
556
+ fi
557
+ [ "$view" = 0 ] || load_manifest_view "${view_args[@]}" || manifest_view_reset
558
+ fi
559
+ IS_PROJECT_READS_ROOT="$root"
560
+ }
561
+
562
+ project_reads_reset() {
563
+ qmap_memo_reset
564
+ manifest_view_reset
565
+ IS_PROJECT_READS_ROOT=""
566
+ }
567
+
568
+ # project_reads_refresh <root> — after a child may have rewritten the manifest
569
+ # or the lock: read them again if this process preloaded them.
570
+ project_reads_refresh() {
571
+ [ "$IS_PROJECT_READS_ROOT" = "$1" ] || return 0
572
+ project_reads_preload "$1"
573
+ }
574
+
575
+ # CLI and engine share one list-cache format. Writers invalidate both the
576
+ # qmap answers and the engine view through qmap_memo_reset.
577
+ _yaml_list_var() { load_yaml_list "$1" "$2"; }
578
+
475
579
  # Read-only diagnosis must distinguish usable installed metadata from metadata
476
580
  # which could actually restore a missing store.
477
581
  check_project_lock() {
@@ -540,28 +644,31 @@ _store_record_write() {
540
644
  # project_store_missing <root> — a package the manifest or lock names is absent
541
645
  # from the store, or holds something other than its lock row pins.
542
646
  project_store_missing() {
543
- local root="$1" manifest="$1/intelligence.yaml" name src
647
+ local root="$1" manifest="$1/intelligence.yaml" name src section
648
+ _qmap_read_var keys "$manifest" "packages" || true
544
649
  while IFS= read -r name; do
545
650
  [ -n "$name" ] || continue
546
651
  assert_valid_pkg_name "$name"
547
652
  [ -d "$root/.intelligence/packages/$name" ] || return 0
548
- done < <(qmap_keys "$manifest" "packages")
653
+ done <<< "$IS_QMAP_OUT"
549
654
  # The parser hands a package reference over as the store directory it
550
655
  # names, so every spelling is checked here as that directory.
551
656
  for section in rules agents skills; do
657
+ _yaml_list_var "$manifest" "$section"
552
658
  while IFS= read -r src; do
553
659
  case "$src" in
554
660
  .intelligence/packages/*)
555
661
  [ -d "$root/$src" ] || return 0
556
662
  ;;
557
663
  esac
558
- done < <(read_yaml_list "$manifest" "$section")
664
+ done <<< "$IS_YAML_LIST"
559
665
  done
560
666
  # Present is not enough: a `git pull` moves the lock and leaves the ignored
561
667
  # store on the commit it held before.
562
668
  local rows _requested url path resolved sha
563
669
  [ -f "$root/intelligence.lock" ] || return 1
564
- rows="$(lock_to_tsv "$root/intelligence.lock")"
670
+ _qmap_read_var rows "$root/intelligence.lock" packages || true
671
+ rows="$IS_QMAP_OUT"
565
672
  while IFS="$LOCK_SEP" read -r name _requested url path resolved sha; do
566
673
  [ -n "$name" ] || continue
567
674
  assert_valid_pkg_name "$name"
@@ -576,5 +683,10 @@ restore_project_store_if_missing() {
576
683
  project_store_missing "$root" || return 0
577
684
  [ -f "$root/intelligence.lock" ] || die "package store is missing and intelligence.lock is absent — run 'intelligence init'"
578
685
  echo " restoring package store from intelligence.lock"
579
- bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync
686
+ if [ "${2:-}" = keep-sources ]; then
687
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync --keep-sources || return $?
688
+ else
689
+ bash "$CLI_DIR/internal/restore.sh" --frozen --no-sync || return $?
690
+ fi
691
+ project_reads_refresh "$root"
580
692
  }
@@ -13,6 +13,12 @@ LOCK_SEP=$'\x1f'
13
13
  # lock_validate <lock> [--metadata|--restore] [manifest] — read-only preflight. A subshell
14
14
  # contains the fail-fast input guards so status can report a failed verdict too.
15
15
  lock_validate() (
16
+ lock_validate_checks "$@"
17
+ )
18
+
19
+ # lock_validate_checks — lock_validate without its subshell, for a caller that
20
+ # already runs it in one (a capturing `$(...)`): the guards exit through `die`.
21
+ lock_validate_checks() {
16
22
  local lock="$1" mode="${2---metadata}" manifest="${3:-}" count=0
17
23
  local kind name requested url path resolved sha line rows current
18
24
  local -x CLI_INPUT_CONTEXT=""
@@ -23,7 +29,8 @@ lock_validate() (
23
29
  esac
24
30
  # The same tokenizer supplies ordinary lock rows and strict metadata. Buffer
25
31
  # before consumption: a failed parser must never yield a partial package set.
26
- rows="$(_qmap_read lock "$lock" packages '' '' "$LOCKFILE_VERSION")" || return $?
32
+ _qmap_read_var lock "$lock" packages '' '' "$LOCKFILE_VERSION" || return $?
33
+ rows="$IS_QMAP_OUT"
27
34
  bundled_engine_version_var
28
35
  current="$IS_BUNDLED_ENGINE_VERSION"
29
36
  while IFS="$LOCK_SEP" read -r kind name requested url path resolved sha line; do
@@ -66,10 +73,12 @@ lock_validate() (
66
73
  [[ "$sha" =~ ^[0-9a-f]{40}$ || "$sha" =~ ^[0-9a-f]{64}$ ]] \
67
74
  || die "locked restore: $name has a missing or invalid commit SHA — restore a valid intelligence.lock"
68
75
  done <<< "$rows"
69
- if [ "$count" -eq 0 ] && [ -n "$manifest" ] && [ -n "$(qmap_keys "$manifest" packages)" ]; then
70
- die "$lock: packages is empty but the manifest declares packages — restore their recorded identities"
76
+ if [ "$count" -eq 0 ] && [ -n "$manifest" ]; then
77
+ _qmap_read_var keys "$manifest" packages
78
+ [ -z "$IS_QMAP_OUT" ] \
79
+ || die "$lock: packages is empty but the manifest declares packages — restore their recorded identities"
71
80
  fi
72
- )
81
+ }
73
82
 
74
83
  # top_scalar <file> <key> — one top-level scalar through the shared tokenizer.
75
84
  top_scalar() {
@@ -86,6 +95,7 @@ lock_to_tsv() {
86
95
  lock_write_from_tsv() {
87
96
  local lock="$1" tsv="$2"
88
97
  local tmp="$lock.cli.tmp"
98
+ qmap_memo_reset
89
99
  {
90
100
  echo "# Generated by the intelligence CLI. Do not edit."
91
101
  echo "lockfile_version: $LOCKFILE_VERSION"