@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.
@@ -156,10 +156,20 @@ target_set_enabled() {
156
156
  '
157
157
  }
158
158
 
159
+ _QMAP_AWK="${BASH_SOURCE[0]%/*}/qmap.awk"
160
+
159
161
  # _qmap_read <mode> <file> <block> [key] [field] [expected-lock-version]
160
162
  # All scalar reads and strict lock records share one tokenizer. Environment
161
163
  # transport preserves literal backslashes that awk -v would interpret again.
162
164
  _qmap_read() {
165
+ if _qmap_memo_find "$@"; then
166
+ _qmap_memo_replay
167
+ return
168
+ fi
169
+ _qmap_read_file "$@"
170
+ }
171
+
172
+ _qmap_read_file() {
163
173
  local mode="$1" file="$2"
164
174
  if [ ! -f "$file" ] || [ ! -r "$file" ]; then
165
175
  case "$mode" in
@@ -169,7 +179,140 @@ _qmap_read() {
169
179
  fi
170
180
  QMAP_MODE="$mode" QMAP_FILE="$file" QMAP_BLOCK="${3:-}" \
171
181
  QMAP_KEY="${4:-}" QMAP_FIELD="${5:-}" QMAP_EXPECTED_VERSION="${6:-}" \
172
- LC_ALL=C awk -f "${BASH_SOURCE[0]%/*}/qmap.awk" < "$file"
182
+ LC_ALL=C awk -f "$_QMAP_AWK" < "$file"
183
+ }
184
+
185
+ # _qmap_read_var <mode> <file> <block> [key] [field] [expected-lock-version]
186
+ # — _qmap_read into IS_QMAP_OUT, as `$(_qmap_read ...)` would capture it, and
187
+ # with its status. A preloaded answer costs no process: on Git Bash every
188
+ # `$(...)` forks, which is what made a lifecycle preflight take half a second.
189
+ # shellcheck disable=SC2034 # IS_QMAP_OUT is the return channel
190
+ _qmap_read_var() {
191
+ if _qmap_memo_find "$@"; then
192
+ IS_QMAP_OUT="${QMAP_MEMO_OUTS[$IS_QMAP_MEMO_AT]}"
193
+ while [[ "$IS_QMAP_OUT" == *$'\n' ]]; do IS_QMAP_OUT="${IS_QMAP_OUT%$'\n'}"; done
194
+ if [ "${QMAP_MEMO_RCS[$IS_QMAP_MEMO_AT]}" -ne 0 ]; then
195
+ printf '%s' "${QMAP_MEMO_OUTS[$IS_QMAP_MEMO_AT]}" >&2
196
+ IS_QMAP_OUT=""
197
+ fi
198
+ return "${QMAP_MEMO_RCS[$IS_QMAP_MEMO_AT]}"
199
+ fi
200
+ IS_QMAP_OUT="$(_qmap_read_file "$@")"
201
+ }
202
+
203
+ # --- Reads answered once per process ------------------------------------
204
+ # qmap_preload runs one awk over several requests and remembers each answer —
205
+ # output or strict error, and status — under the exact request it answers.
206
+ # Only a command that holds the project lock may preload, and anything that
207
+ # writes a qmap document forgets every answer (qmap_memo_reset): a remembered
208
+ # read must never outlive the bytes it was read from.
209
+ QMAP_MEMO_KEYS=()
210
+ QMAP_MEMO_RCS=()
211
+ QMAP_MEMO_OUTS=()
212
+
213
+ qmap_memo_reset() {
214
+ QMAP_MEMO_KEYS=()
215
+ QMAP_MEMO_RCS=()
216
+ QMAP_MEMO_OUTS=()
217
+ if declare -F manifest_view_reset >/dev/null; then manifest_view_reset; fi
218
+ }
219
+
220
+ # _qmap_memo_find <request...> — set IS_QMAP_MEMO_AT to the remembered answer.
221
+ _qmap_memo_find() {
222
+ local request="${1:-}"$'\037'"${2:-}"$'\037'"${3:-}"$'\037'"${4:-}"$'\037'"${5:-}"$'\037'"${6:-}" i=0
223
+ while [ "$i" -lt "${#QMAP_MEMO_KEYS[@]}" ]; do
224
+ if [ "${QMAP_MEMO_KEYS[$i]}" = "$request" ]; then
225
+ IS_QMAP_MEMO_AT="$i"
226
+ return 0
227
+ fi
228
+ i=$((i + 1))
229
+ done
230
+ return 1
231
+ }
232
+
233
+ # Print a remembered answer exactly as the awk reader printed it.
234
+ _qmap_memo_replay() {
235
+ if [ "${QMAP_MEMO_RCS[$IS_QMAP_MEMO_AT]}" -ne 0 ]; then
236
+ printf '%s' "${QMAP_MEMO_OUTS[$IS_QMAP_MEMO_AT]}" >&2
237
+ return "${QMAP_MEMO_RCS[$IS_QMAP_MEMO_AT]}"
238
+ fi
239
+ printf '%s' "${QMAP_MEMO_OUTS[$IS_QMAP_MEMO_AT]}"
240
+ }
241
+
242
+ # qmap_preload <request>... — each request is one string of _qmap_read's six
243
+ # arguments, unit-separated. Requests for absent or empty documents are left
244
+ # to the ordinary reader, which reports them exactly as before. A preload that
245
+ # cannot run leaves nothing remembered; reads then take the ordinary path.
246
+ # A caller that wants the awk pass to overlap other work uses the three steps
247
+ # itself: qmap_preload_plan, qmap_preload_run (prints), qmap_preload_store.
248
+ qmap_preload() {
249
+ local out
250
+ qmap_preload_plan "$@" || return 0
251
+ out="$(qmap_preload_run)" || return 0
252
+ qmap_preload_store "$out"
253
+ }
254
+
255
+ # qmap_preload_plan <request>... — QMAP_PLAN_REQUESTS, _FILES and _JOBS for the
256
+ # requests one awk pass can answer; returns 1 when there are none.
257
+ qmap_preload_plan() {
258
+ local request saved_ifs="$IFS"
259
+ local -a fields=()
260
+ QMAP_PLAN_REQUESTS=() QMAP_PLAN_FILES=() QMAP_PLAN_JOBS=""
261
+ for request in "$@"; do
262
+ IFS=$'\037'
263
+ set -f
264
+ # shellcheck disable=SC2206 # splitting on the unit separator is the point
265
+ fields=($request)
266
+ set +f
267
+ IFS="$saved_ifs"
268
+ case "${fields[1]:-}" in /*) ;; *) continue ;; esac
269
+ [ -f "${fields[1]}" ] && [ -r "${fields[1]}" ] && [ -s "${fields[1]}" ] || continue
270
+ QMAP_PLAN_FILES+=("${fields[1]}")
271
+ QMAP_PLAN_REQUESTS+=("$request")
272
+ QMAP_PLAN_JOBS="$QMAP_PLAN_JOBS${fields[0]:-}"$'\037'"${fields[2]:-}"$'\037'"${fields[3]:-}"$'\037'"${fields[4]:-}"$'\037'"${fields[5]:-}"$'\n'
273
+ done
274
+ [ "${#QMAP_PLAN_FILES[@]}" -gt 0 ]
275
+ }
276
+
277
+ # qmap_preload_run — the planned pass; a trailing `end` proves it completed.
278
+ qmap_preload_run() {
279
+ QMAP_JOBS="${QMAP_PLAN_JOBS%$'\n'}" LC_ALL=C awk -f "$_QMAP_AWK" "${QMAP_PLAN_FILES[@]}" && echo end
280
+ }
281
+
282
+ # qmap_preload_store <output> — remember every planned answer, or nothing when
283
+ # the output is not exactly one complete answer per request.
284
+ qmap_preload_store() {
285
+ local header line status count raw i=0 saved_ifs="$IFS"
286
+ local -a lines=() rcs=() outs=()
287
+ IFS=$'\n'
288
+ set -f
289
+ # shellcheck disable=SC2206 # no line is empty: rows carry a ">" prefix
290
+ lines=($1)
291
+ set +f
292
+ IFS="$saved_ifs"
293
+ while [ "$i" -lt "${#lines[@]}" ]; do
294
+ header="${lines[i]}"
295
+ i=$((i + 1))
296
+ if [ "$header" = end ]; then
297
+ [ "$i" -eq "${#lines[@]}" ] && [ "${#rcs[@]}" -eq "${#QMAP_PLAN_REQUESTS[@]}" ] || return 0
298
+ QMAP_MEMO_KEYS+=("${QMAP_PLAN_REQUESTS[@]}")
299
+ QMAP_MEMO_RCS+=("${rcs[@]}")
300
+ QMAP_MEMO_OUTS+=("${outs[@]}")
301
+ return 0
302
+ fi
303
+ case "$header" in $'\036'[01]' '[0-9]*) ;; *) return 0 ;; esac
304
+ status="${header:1:1}" count="${header#* }"
305
+ raw=""
306
+ while [ "$count" -gt 0 ]; do
307
+ line="${lines[i]-}"
308
+ case "$line" in '>'*) ;; *) return 0 ;; esac
309
+ raw="$raw${line#>}"$'\n'
310
+ i=$((i + 1))
311
+ count=$((count - 1))
312
+ done
313
+ rcs+=("$status")
314
+ outs+=("$raw")
315
+ done
173
316
  }
174
317
 
175
318
  # qmap_validate_document <file> <block> — structural validation only.
@@ -191,6 +334,7 @@ qmap_value() { _qmap_read value "$1" "$2" "$3"; }
191
334
  _qmap_stage() {
192
335
  local file="$1"; shift
193
336
  local tmp="$file.cli.tmp" rc=0
337
+ qmap_memo_reset
194
338
  awk "$@" "$file" > "$tmp" || rc=$?
195
339
  [ "$rc" -eq 0 ] || { rm -f "$tmp"; die "internal: manifest edit refused (awk exit $rc) for $file"; }
196
340
  [ -s "$tmp" ] || { rm -f "$tmp"; die "internal: manifest edit produced an empty file for $file"; }
@@ -643,6 +787,8 @@ sources_remove_package() {
643
787
  shift 2
644
788
  [ -f "$file" ] || return 0
645
789
  tmp="$file.cli.tmp"
790
+ # A write to a qmap document forgets every remembered read, as _qmap_stage does.
791
+ qmap_memo_reset
646
792
  awk -v name="$name" -v only=" $* " "$IS_PKG_REF_AWK$IS_YAML_ITEM_AWK"'
647
793
  BEGIN { store = ".intelligence/packages/" name }
648
794
  FNR == 1 { pass++ }
package/cli/lib/qmap.awk CHANGED
@@ -1,16 +1,58 @@
1
1
  # Shared tokenizer for the CLI-owned quoted-key map and scalar document shape.
2
2
  # Reader modes ignore unrelated YAML. Lock mode validates the whole document
3
3
  # before publishing rows, so a bad later entry never yields a partial result.
4
+ #
5
+ # One read per process: QMAP_MODE and friends describe it, the document
6
+ # arrives on stdin, and a strict failure exits 1 with its reason on stderr.
7
+ # Several reads per process: QMAP_JOBS holds one job per line (mode, block,
8
+ # key, field and expected lock version, unit-separated) and each job names its
9
+ # document as the matching file operand, so a command that needs the manifest,
10
+ # the lock and the CLI's package descriptor parses them in a single awk start.
11
+ # Every job then prints a header, "\036<status> <line count>", followed by that
12
+ # many lines, each behind a ">" so none is empty: its rows, or its error. Empty
13
+ # documents cannot be job operands (they yield no first line to start the job);
14
+ # the caller reads those alone.
4
15
  BEGIN {
5
- mode = ENVIRON["QMAP_MODE"]
6
- block = ENVIRON["QMAP_BLOCK"]
7
- wanted_key = ENVIRON["QMAP_KEY"]
8
- wanted_field = ENVIRON["QMAP_FIELD"]
9
- expected_version = ENVIRON["QMAP_EXPECTED_VERSION"]
10
- source_file = ENVIRON["QMAP_FILE"]
11
- strict = (mode == "lock" || mode == "validate")
12
16
  sep = sprintf("%c", 31)
13
17
  single_quote = sprintf("%c", 39)
18
+ multi = (ENVIRON["QMAP_JOBS"] != "")
19
+ if (multi) job_count = split(ENVIRON["QMAP_JOBS"], job_spec, "\n")
20
+ else configure(ENVIRON["QMAP_MODE"], ENVIRON["QMAP_BLOCK"], ENVIRON["QMAP_KEY"], ENVIRON["QMAP_FIELD"], ENVIRON["QMAP_EXPECTED_VERSION"], ENVIRON["QMAP_FILE"])
21
+ }
22
+
23
+ function configure(m, b, k, f, v, file) {
24
+ mode = m
25
+ block = b
26
+ wanted_key = k
27
+ wanted_field = f
28
+ expected_version = v
29
+ source_file = file
30
+ strict = (mode == "lock" || mode == "validate")
31
+ problem = ""; problem_line = 0
32
+ in_block = 0; package = ""
33
+ row_count = 0; found = 0; result = ""; block_seen = 0
34
+ split("", row_name); split("", row_line)
35
+ split("", package_seen); split("", field_seen); split("", field_value)
36
+ split("", top_seen); split("", top_value)
37
+ out_count = 0; failed = 0
38
+ }
39
+
40
+ # emit/fail keep the single-document output stream and exit status unchanged;
41
+ # a job buffers both so its header can state them first.
42
+ function emit(line) {
43
+ if (multi) out[++out_count] = line
44
+ else print line
45
+ }
46
+
47
+ function fail(message) {
48
+ if (multi) {
49
+ failed = 1
50
+ out_count = 0
51
+ out[++out_count] = message
52
+ return
53
+ }
54
+ print message > "/dev/stderr"
55
+ exit 1
14
56
  }
15
57
 
16
58
  function error_at(reason, line) {
@@ -104,6 +146,14 @@ function token(line, text, colon, tail) {
104
146
  return 1
105
147
  }
106
148
 
149
+ # A job starts at its document's first line and ends where the next begins.
150
+ multi && FNR == 1 {
151
+ if (job) finish()
152
+ job++
153
+ split(job_spec[job], spec, sep)
154
+ configure(spec[1], spec[2], spec[3], spec[4], spec[5], FILENAME)
155
+ }
156
+
107
157
  {
108
158
  sub(/\r$/, "")
109
159
  if ($0 ~ /^[ \t]*(#.*)?$/) next
@@ -114,7 +164,7 @@ function token(line, text, colon, tail) {
114
164
  if (kind == "field" && package != "") context = "package " package ", field " key ": "
115
165
  else if (kind == "top") context = "field " key ": "
116
166
  }
117
- error_at(context "malformed lock structure or unsupported scalar", NR)
167
+ error_at(context "malformed lock structure or unsupported scalar", FNR)
118
168
  }
119
169
  if ($0 ~ /^[^ #\t]/) { in_block = 0; package = "" }
120
170
  next
@@ -122,14 +172,14 @@ function token(line, text, colon, tail) {
122
172
  if (kind == "top") {
123
173
  in_block = (key == block && empty)
124
174
  package = ""
125
- if (strict && top_seen[key]++) error_at("duplicate top-level field " key, NR)
175
+ if (strict && top_seen[key]++) error_at("duplicate top-level field " key, FNR)
126
176
  if (!(key in top_value)) top_value[key] = value
127
177
  if (mode == "top" && key == wanted_key && !found) { result = value; found = 1 }
128
178
  if (strict) {
129
179
  if (key == block) {
130
180
  block_seen = 1
131
- if (!empty) error_at(block " must be a block map", NR)
132
- } else if (empty) error_at("expected scalar for " key, NR)
181
+ if (!empty) error_at(block " must be a block map", FNR)
182
+ } else if (empty) error_at("expected scalar for " key, FNR)
133
183
  }
134
184
  next
135
185
  }
@@ -137,46 +187,55 @@ function token(line, text, colon, tail) {
137
187
  package = key
138
188
  row_count++
139
189
  row_name[row_count] = key
140
- row_line[row_count] = NR
190
+ row_line[row_count] = FNR
141
191
  if (strict) {
142
- if (key == "") error_at("empty package key", NR)
143
- if (package_seen[key]++) error_at("duplicate package " key, NR)
144
- if (!empty) error_at("expected field map for " key, NR)
192
+ if (key == "") error_at("empty package key", FNR)
193
+ if (package_seen[key]++) error_at("duplicate package " key, FNR)
194
+ if (!empty) error_at("expected field map for " key, FNR)
145
195
  }
146
196
  if (mode == "value" && key == wanted_key && !found) { result = value; found = 1 }
147
197
  next
148
198
  }
149
199
  if (kind == "field" && in_block && package != "") {
150
200
  identity = package SUBSEP key
151
- if (strict && field_seen[identity]++) error_at("duplicate field " package "." key, NR)
201
+ if (strict && field_seen[identity]++) error_at("duplicate field " package "." key, FNR)
152
202
  if (!(identity in field_value)) field_value[identity] = value
153
- if (strict && empty) error_at("expected scalar for " package "." key, NR)
203
+ if (strict && empty) error_at("expected scalar for " package "." key, FNR)
154
204
  if (mode == "field" && package == wanted_key && key == wanted_field && !found) {
155
205
  result = value
156
206
  found = 1
157
207
  }
158
208
  next
159
209
  }
160
- if (strict) error_at("malformed lock structure: expected top-level scalar or quoted package field", NR)
210
+ if (strict) error_at("malformed lock structure: expected top-level scalar or quoted package field", FNR)
161
211
  }
162
212
 
163
- END {
213
+ # The end of one document: its rows or its error, through emit/fail.
214
+ function finish( i) {
215
+ finish_rows()
216
+ if (multi) {
217
+ printf "%c%d %d\n", 30, failed, out_count
218
+ for (i = 1; i <= out_count; i++) print ">" out[i]
219
+ }
220
+ }
221
+
222
+ function finish_rows( version, count, fields, tops, i, f, name, row) {
164
223
  # Read the format header before interpreting a future body's shape. Errors
165
224
  # were retained during the single pass so header placement does not matter.
166
225
  if (mode == "lock" && top_value["lockfile_version"] != expected_version) {
167
226
  version = top_value["lockfile_version"]
168
- print source_file " has unsupported or missing lockfile_version '" (version == "" ? "<missing>" : version) "' — expected " expected_version > "/dev/stderr"
169
- exit 1
227
+ fail(source_file " has unsupported or missing lockfile_version '" (version == "" ? "<missing>" : version) "' — expected " expected_version)
228
+ return
170
229
  }
171
230
  if (strict && problem != "") {
172
- print source_file ":" problem_line ": " problem > "/dev/stderr"
173
- exit 1
231
+ fail(source_file ":" problem_line ": " problem)
232
+ return
174
233
  }
175
234
  if (strict && !block_seen) {
176
- print source_file ": missing " block " block" > "/dev/stderr"
177
- exit 1
235
+ fail(source_file ": missing " block " block")
236
+ return
178
237
  }
179
- if (mode == "lock") print "V" sep top_value["lockfile_version"] sep top_value["engine_version"]
238
+ if (mode == "lock") emit("V" sep top_value["lockfile_version"] sep top_value["engine_version"])
180
239
  if (mode == "fieldrows") {
181
240
  # One row per package key, as `keys` lists them: the name, then each
182
241
  # field QMAP_FIELD names (space separated), as qmap_field decodes it.
@@ -185,22 +244,28 @@ END {
185
244
  name = row_name[i]
186
245
  row = name
187
246
  for (f = 1; f <= count; f++) row = row sep field_value[name SUBSEP fields[f]]
188
- print row
247
+ emit(row)
189
248
  }
190
249
  } else if (mode == "rows" || mode == "lock" || mode == "keys") {
191
250
  for (i = 1; i <= row_count; i++) {
192
251
  name = row_name[i]
193
- if (mode == "keys") print name
252
+ if (mode == "keys") emit(name)
194
253
  else {
195
254
  row = name sep field_value[name SUBSEP "requested"] sep field_value[name SUBSEP "url"] sep field_value[name SUBSEP "path"] sep field_value[name SUBSEP "resolved"] sep field_value[name SUBSEP "sha"]
196
- if (mode == "lock") print "R" sep row sep row_line[i]
197
- else print row
255
+ if (mode == "lock") emit("R" sep row sep row_line[i])
256
+ else emit(row)
198
257
  }
199
258
  }
200
259
  } else if (mode == "tops") {
201
260
  # Several top-level scalars in one pass: QMAP_KEY lists them, space
202
261
  # separated; each present one prints as key<US>value.
203
262
  count = split(wanted_key, tops, " ")
204
- for (i = 1; i <= count; i++) if (tops[i] in top_value) print tops[i] sep top_value[tops[i]]
205
- } else if (found) print result
263
+ for (i = 1; i <= count; i++) if (tops[i] in top_value) emit(tops[i] sep top_value[tops[i]])
264
+ } else if (found) emit(result)
265
+ }
266
+
267
+ END {
268
+ if (multi) {
269
+ if (job) finish()
270
+ } else finish()
206
271
  }
@@ -61,7 +61,7 @@ resolve_package_source() {
61
61
  [ -n "$reg_url" ] || continue
62
62
  index="$(_fetch_index "${reg_url#git+}")"
63
63
  if [ -z "$index" ]; then
64
- echo " WARN: registry unreachable or missing index.yaml, skipped: $reg_url" >&2
64
+ echo "WARNING: registry unreachable or missing index.yaml, skipped: $reg_url" >&2
65
65
  continue
66
66
  fi
67
67
  RES_URL="$(qmap_field "$index" "packages" "$name" "url")"