@ainova-systems/intelligence 0.19.0 → 0.21.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.
Files changed (41) hide show
  1. package/cli/commands/init.sh +2 -1
  2. package/cli/commands/registry.sh +1 -1
  3. package/cli/commands/source.sh +1 -1
  4. package/cli/commands/sync.sh +78 -6
  5. package/cli/intelligence +15 -2
  6. package/cli/internal/check.sh +39 -0
  7. package/cli/internal/convert-legacy.sh +1 -1
  8. package/cli/internal/package-search.sh +1 -1
  9. package/cli/internal/package-update.sh +2 -2
  10. package/cli/internal/restore.sh +3 -2
  11. package/cli/lib/cli-common.sh +145 -33
  12. package/cli/lib/gitignore.sh +19 -2
  13. package/cli/lib/lockfile.sh +14 -4
  14. package/cli/lib/manifest.sh +147 -1
  15. package/cli/lib/qmap.awk +97 -32
  16. package/cli/lib/registry.sh +1 -1
  17. package/cli/lib/sync-cache.sh +596 -142
  18. package/cli/lib/sync-lock.sh +19 -2
  19. package/engine/ENGINE_SHA +1 -1
  20. package/engine/VERSION +1 -1
  21. package/engine/adapters/_template.sh +32 -8
  22. package/engine/adapters/agents.sh +47 -52
  23. package/engine/adapters/antigravity.sh +71 -53
  24. package/engine/adapters/claude.sh +42 -57
  25. package/engine/adapters/codex.sh +77 -16
  26. package/engine/adapters/copilot.sh +54 -65
  27. package/engine/adapters/cursor.sh +88 -78
  28. package/engine/adapters/opencode.sh +23 -32
  29. package/engine/adapters/pi.sh +140 -42
  30. package/engine/lib/adapter-contract.sh +36 -4
  31. package/engine/lib/common.sh +1388 -165
  32. package/engine/lib/contract.sh +5 -0
  33. package/engine/sync.sh +145 -38
  34. package/package.json +1 -1
  35. package/packages/sync/references/adapters.md +48 -38
  36. package/packages/sync/references/conventions.md +61 -12
  37. package/packages/sync/references/onboarding-migration.md +4 -4
  38. package/packages/sync/rules/intelligence-authoring.md +2 -2
  39. package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
  40. package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
  41. package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
@@ -312,10 +312,26 @@ ensure_gitignore_header() {
312
312
  fi
313
313
  }
314
314
 
315
+ # local_artifact_ignore_patterns <content-dir> — the patterns that make a rule,
316
+ # agent or skill local (decision 0021): a file or skill directory with one of
317
+ # these names never reaches committed output, and Git never commits it. The
318
+ # engine recognizes the same names, so the two cannot disagree; `init` writes
319
+ # them and `status --check` reports one missing.
320
+ local_artifact_ignore_patterns() {
321
+ local dir="${1#./}"
322
+ while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
323
+ printf '%s\n' "$dir/rules/*.local.md" "$dir/agents/*.local.md" "$dir/skills/*.local/"
324
+ }
325
+
326
+ # ensure_base_gitignore <root> <content-dir> — the package store and the local
327
+ # artifact patterns of the project's content directory.
315
328
  ensure_base_gitignore() {
316
- local root="$1"
329
+ local root="$1" content_dir="$2" line
317
330
  ensure_gitignore_header "$root"
318
331
  gitignore_add_line "$root" '.intelligence/'
332
+ while IFS= read -r line; do
333
+ gitignore_add_line "$root" "$line"
334
+ done < <(local_artifact_ignore_patterns "$content_dir")
319
335
  }
320
336
 
321
337
  ensure_target_gitignore() {
@@ -343,7 +359,7 @@ ensure_target_gitignore() {
343
359
 
344
360
  ensure_manifest_gitignore() {
345
361
  local root="$1" manifest="$2" target
346
- ensure_base_gitignore "$root"
362
+ ensure_base_gitignore "$root" "$(manifest_intelligence_dir "$manifest")"
347
363
  while IFS= read -r target; do
348
364
  [ -n "$target" ] || continue
349
365
  if [ "$(is_target_enabled "$manifest" "$target")" = "1" ]; then
@@ -420,6 +436,7 @@ managed_gitignore_patterns() {
420
436
  local root="$1" manifest="$2" content_dir target output records kind value
421
437
  content_dir="$(manifest_intelligence_dir "$manifest")"
422
438
  printf '%s\n' '.intelligence/' "$content_dir/_backup/"
439
+ local_artifact_ignore_patterns "$content_dir"
423
440
  while IFS= read -r target; do
424
441
  [ -n "$target" ] || continue
425
442
  [ "$(is_target_enabled "$manifest" "$target")" = "1" ] || continue
@@ -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"
@@ -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")"