@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
@@ -1,82 +1,119 @@
1
1
  #!/bin/bash
2
2
  # Optional CLI optimization. Cache data never supplies paths or executable code:
3
3
  # every dependency is derived again from the manifest and built-in contracts.
4
+ #
5
+ # The record keeps three fingerprints apart (decision 0018), so a check can say
6
+ # which side moved:
7
+ # tooling the CLI and engine code, plus the invocation and environment that
8
+ # shape rendering — target filter, root, layout, bash, locale, PATH,
9
+ # umask. A change here is not the project's: a check renders rather
10
+ # than answer "sync needed" from it.
11
+ # inputs the manifest, the lock, every configured source including skill
12
+ # resources and installed packages, and the project adapter folder.
13
+ # outputs every path the enabled built-in adapters own or manage.
14
+ # Each fingerprint lists names, entry kinds and executable state beside content
15
+ # hashes; timestamps never count. `.intelligence/sync-cache/state` holds the
16
+ # record and the report a hit replays; `seal` holds the state file's own hash,
17
+ # so a record is trusted only whole.
18
+
19
+ SC_FORMAT='intelligence-sync-cache-v2'
20
+ SC_SAFE_SEEN=$'\n'
4
21
 
5
22
  sync_cache_safe_path() {
6
23
  local path="$1" probe="$1"
7
24
  case "$path" in *[[:cntrl:]]*|*\\*|*\"*) return 1 ;; esac
8
25
  # Include ancestors: find does not report a link above its starting path.
26
+ # Start points share most ancestors, so each one is probed once per run.
9
27
  while [ -n "$probe" ] && [ "$probe" != / ]; do
10
28
  case "$probe" in "$REPO_ROOT"|"$CLI_DIR"|"$IS_ENGINE_DIR") break ;; esac
29
+ case "$SC_SAFE_SEEN" in *$'\n'"$probe"$'\n'*) break ;; esac
11
30
  [ ! -L "$probe" ] || return 1
31
+ SC_SAFE_SEEN="$SC_SAFE_SEEN$probe"$'\n'
12
32
  probe="${probe%/*}"
13
33
  done
14
34
  }
15
35
 
16
- # One checked enumeration and one batch hash, including names, directory/file
17
- # types and executable bits. Timestamps cannot prove content is unchanged.
18
- # Unsupported paths/types are a cache miss, never an incomplete fingerprint.
19
- # Keep list descriptors open across the walk; reopening each line is expensive
20
- # on Windows and provides no additional content verification.
21
- sync_cache_fingerprint() (
22
- local work="$1" path kind executable seen=$'\n'
23
- local -a existing
24
- existing=()
25
- shift
26
- : > "$work/entries" || return 1
27
- : > "$work/files" || return 1
28
- : > "$work/list" || return 1
29
- for path in "$@"; do
30
- sync_cache_safe_path "$path" || return 1
31
- # Shared managed destinations are declared by several adapters. Walking
32
- # the same tree again only multiplies filesystem probes on Windows.
33
- case "$seen" in *$'\n'"$path"$'\n'*) continue ;; esac
34
- seen="$seen$path"$'\n'
35
- if [ -e "$path" ]; then
36
- existing+=("$path")
37
- else
38
- printf 'missing\t%s\n' "$path" >> "$work/entries" || return 1
39
- fi
36
+ # --- Native paths for Git for Windows --------------------------------------
37
+ # Git for Windows does not translate POSIX paths it reads from stdin. cygpath
38
+ # would, at the price of a process; the mount table it consults is readable in
39
+ # this shell. Only paths under a listed mount other than `/` are translated
40
+ # here — `/` also stands for drives mounted on demand — and any other path sends
41
+ # the whole list through cygpath. The same table answers two more questions:
42
+ # - whether executable bits are real: on a `noacl` mount Cygwin derives them
43
+ # from a file's name and first bytes, which the content hash already covers,
44
+ # so asking find for them would only add a stat per file;
45
+ # - whether find's order is already stable: NTFS lists a directory from its
46
+ # name-collated index, so the same tree always walks in the same order and
47
+ # the sort that other filesystems need can be skipped.
48
+ SC_MOUNTS_READ=0
49
+ SC_MOUNT_POSIX=()
50
+ SC_MOUNT_NATIVE=()
51
+ SC_MOUNT_TYPE=()
52
+ SC_EXEC_PROBE=1
53
+ SC_TRANSLATE=0
54
+
55
+ sync_cache_mounts() {
56
+ [ "$SC_MOUNTS_READ" = 0 ] || return 0
57
+ SC_MOUNTS_READ=1
58
+ case "${OSTYPE:-}" in
59
+ msys*|cygwin*) ;;
60
+ *)
61
+ command -v cygpath >/dev/null 2>&1 && SC_TRANSLATE=2
62
+ return 0
63
+ ;;
64
+ esac
65
+ SC_TRANSLATE=2
66
+ local native posix type options _rest acl=0 lines=0
67
+ [ -r /proc/mounts ] || return 0
68
+ while read -r native posix type options _rest; do
69
+ lines=$((lines + 1))
70
+ case "$options" in *noacl*) ;; *) acl=1 ;; esac
71
+ case "$native$posix" in *\\*)
72
+ native="${native//\\040/ }" posix="${posix//\\040/ }"
73
+ case "$native$posix" in *\\*) continue ;; esac
74
+ ;;
75
+ esac
76
+ [ "$posix" != / ] || continue
77
+ SC_MOUNT_POSIX+=("$posix")
78
+ SC_MOUNT_NATIVE+=("$native")
79
+ SC_MOUNT_TYPE+=("$type")
80
+ done < /proc/mounts
81
+ [ "$lines" -gt 0 ] || return 0
82
+ [ "$acl" = 1 ] || SC_EXEC_PROBE=0
83
+ [ "${#SC_MOUNT_POSIX[@]}" -eq 0 ] || SC_TRANSLATE=1
84
+ }
85
+
86
+ # sync_cache_mount_var <absolute-path> — IS_SC_MOUNT, the index of the longest
87
+ # listed mount holding the path; return 1 when none does.
88
+ sync_cache_mount_var() {
89
+ local i=0 length=0 mount
90
+ IS_SC_MOUNT=-1
91
+ while [ "$i" -lt "${#SC_MOUNT_POSIX[@]}" ]; do
92
+ mount="${SC_MOUNT_POSIX[$i]}"
93
+ case "$1" in
94
+ "$mount"|"$mount"/*)
95
+ if [ "${#mount}" -gt "$length" ]; then IS_SC_MOUNT="$i"; length="${#mount}"; fi
96
+ ;;
97
+ esac
98
+ i=$((i + 1))
40
99
  done
41
- if [ "${#existing[@]}" -gt 0 ]; then
42
- find "${existing[@]}" -print0 > "$work/list" || return 1
43
- fi
44
- while IFS= read -r -d '' path; do
45
- # Starting-path ancestors were checked above. find does not follow
46
- # links, so checking each returned entry once covers the entire tree.
47
- case "$path" in *[[:cntrl:]]*|*\\*|*\"*) return 1 ;; esac
48
- [ ! -L "$path" ] || return 1
49
- executable=0
50
- if [ -f "$path" ]; then
51
- kind='file'
52
- [ ! -x "$path" ] || executable=1
53
- printf '%s\n' "$path" >&4 || return 1
54
- elif [ -d "$path" ]; then
55
- kind=directory
56
- else
57
- return 1
58
- fi
59
- printf '%s\t%s\t%s\n' "$kind" "$executable" "$path" >&3 || return 1
60
- done < "$work/list" 3>> "$work/entries" 4> "$work/files" || return 1
61
- # find order need not be stable. Sort both streams; preserve path-to-hash
62
- # association by feeding the sorted file list into a single git process.
63
- LC_ALL=C sort -u "$work/entries" > "$work/sorted" || return 1
64
- LC_ALL=C sort -u "$work/files" > "$work/paths" || return 1
65
- if command -v cygpath >/dev/null 2>&1; then
66
- # Git for Windows does not translate POSIX paths received on stdin.
67
- cygpath -m -f "$work/paths" > "$work/native" || return 1
68
- else
69
- cp "$work/paths" "$work/native" || return 1
70
- fi
71
- git hash-object --no-filters --stdin-paths < "$work/native" > "$work/hashes" || return 1
72
- cat "$work/sorted" "$work/paths" "$work/hashes" | git hash-object --stdin || return 1
73
- )
100
+ [ "$IS_SC_MOUNT" -ge 0 ]
101
+ }
74
102
 
103
+ # sync_cache_native_var <absolute-path> — IS_SC_NATIVE, or return 1 when only
104
+ # cygpath can say.
105
+ sync_cache_native_var() {
106
+ sync_cache_mount_var "$1" || return 1
107
+ IS_SC_NATIVE="${SC_MOUNT_NATIVE[$IS_SC_MOUNT]}${1:${#SC_MOUNT_POSIX[$IS_SC_MOUNT]}}"
108
+ }
109
+
110
+ # --- Dependencies ------------------------------------------------------------
75
111
  sync_cache_dependencies() {
76
- local src section adapter file output records kind value
112
+ local src section adapter file output _adapter kind value
113
+ local -a batch=()
114
+ SC_TOOLING=("$CLI_DIR/intelligence" "$CLI_DIR/engine-package.yaml"
115
+ "$CLI_DIR/commands" "$CLI_DIR/internal" "$CLI_DIR/lib" "$IS_ENGINE_DIR")
77
116
  SC_INPUTS=("$CONFIG_FILE" "$REPO_ROOT/intelligence.lock"
78
- "$CLI_DIR/intelligence" "$CLI_DIR/engine-package.yaml"
79
- "$CLI_DIR/commands" "$CLI_DIR/internal" "$CLI_DIR/lib" "$IS_ENGINE_DIR"
80
117
  "$REPO_ROOT/$IS_CONTENT_REL/adapters")
81
118
  SC_OUTPUTS=()
82
119
  # Project adapters can read arbitrary files or environment and execute any
@@ -84,13 +121,16 @@ sync_cache_dependencies() {
84
121
  for file in "$REPO_ROOT/$IS_CONTENT_REL/adapters"/*.sh; do
85
122
  [ ! -e "$file" ] && [ ! -L "$file" ] || return 1
86
123
  done
124
+ # The engine's list parser hands a package reference (decision 0019) over
125
+ # as the store path sync renders from, and leaves out one that resolves to
126
+ # nothing; only the manifest, an input itself, decides which.
87
127
  for section in rules agents skills; do
88
128
  load_yaml_list "$CONFIG_FILE" "$section"
89
129
  while IFS= read -r src; do
90
130
  [ -n "$src" ] || continue
91
131
  adapter_contract_safe_concrete_path "$src" || return 1
92
132
  case "$src" in .|./|.intelligence|.intelligence/|.intelligence/sync-cache*) return 1 ;; esac
93
- SC_INPUTS+=("$REPO_ROOT/$src")
133
+ SC_INPUTS+=("$REPO_ROOT/${src%/}")
94
134
  done <<< "$IS_YAML_LIST"
95
135
  done
96
136
  load_targets_cache "$CONFIG_FILE"
@@ -102,96 +142,355 @@ sync_cache_dependencies() {
102
142
  case "$adapter" in agents|antigravity|claude|codex|copilot|cursor|opencode|pi) ;; *) return 1 ;; esac
103
143
  target_output_var "$CONFIG_FILE" "$adapter"
104
144
  output="${IS_TGT_OUTPUT:-.$adapter}"
105
- records="$(adapter_contract_records "$adapter" "$file" "$output")" || return 1
106
- while IFS=$'\t' read -r kind value; do
107
- case "$kind" in owned|managed) SC_OUTPUTS+=("$REPO_ROOT/$value") ;; esac
108
- done <<< "$records"
145
+ batch+=("$adapter" "$file" "$output")
109
146
  done
147
+ [ "${#batch[@]}" -gt 0 ] || return 1
148
+ # Every enabled contract in one subshell: two forks per adapter were most of
149
+ # this step on Windows.
150
+ adapter_contract_records_batch "$SC_SCRATCH.contract" "${batch[@]}" > "$SC_SCRATCH.records" || return 1
151
+ while IFS=$'\t' read -r _adapter kind value; do
152
+ case "$kind" in owned|managed) SC_OUTPUTS+=("$REPO_ROOT/$value") ;; esac
153
+ done < "$SC_SCRATCH.records"
110
154
  [ "${#SC_OUTPUTS[@]}" -gt 0 ]
111
155
  }
112
156
 
113
- sync_cache_input_hash() {
114
- local fingerprint payload
115
- fingerprint="$(sync_cache_fingerprint "$1" "${SC_INPUTS[@]}")" || return 1
116
- [ -n "${SC_UMASK:-}" ] || SC_UMASK="$(umask)"
117
- # Explicit environment values that affect rendering, discovery or parsing.
118
- # No secrets or inherited environment dump are persisted.
119
- printf -v payload '%s\n' "$fingerprint" "$2" "$REPO_ROOT" "$CONFIG_FILE" \
120
- "$IS_CONTENT_REL" "$IS_MODULE_REL" "$IS_SYNC_CMD" "$IS_MANIFEST_NAME" \
121
- "$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" "${LANG:-}" \
122
- "${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$SC_UMASK"
123
- # A here-string appends the final newline the payload already ends with.
124
- git hash-object --stdin <<< "${payload%$'\n'}"
157
+ # --- Fingerprint ---------------------------------------------------------------
158
+ # _sync_cache_starts <group> <path>... — queue a group's start points. A missing
159
+ # one is recorded as missing; a repeated one is walked once.
160
+ _sync_cache_starts() {
161
+ local group="$1" path
162
+ shift
163
+ for path in "$@"; do
164
+ sync_cache_safe_path "$path" || return 1
165
+ case "$SC_START_SEEN" in
166
+ *$'\n'"$group $path"$'\n'*) continue ;;
167
+ *$'\n'?" $path"$'\n'*) return 1 ;;
168
+ esac
169
+ SC_START_SEEN="$SC_START_SEEN$group $path"$'\n'
170
+ if [ -e "$path" ]; then
171
+ SC_STARTS+=("$path")
172
+ SC_START_GROUPS+=("$group")
173
+ else
174
+ SC_MISSING+=("$group missing $path")
175
+ fi
176
+ done
177
+ }
178
+
179
+ # The listing stage of the fingerprint pipeline. It reads find's output, tells
180
+ # kinds from repeats, files every entry under its start point's group and
181
+ # writes the three listings, each on one line, plus the file counts, to
182
+ # SC_AWK_LISTING. On stdout it prints the paths to hash for git, grouped
183
+ # tooling, inputs, outputs, then the state file: one hash per path comes back
184
+ # in that order. Any entry it cannot place fails the whole pipeline.
185
+ # When the walk itself arrives grouped (SC_AWK_STREAM=1: unsorted, start points
186
+ # in group order) each path goes to git as soon as it is placed, so hashing runs
187
+ # beside the walk; a group that comes back after a later one fails the run.
188
+ _SC_AWK_LISTING='
189
+ BEGIN {
190
+ starts = split(ENVIRON["SC_AWK_STARTS"], rows, "\n")
191
+ for (i = 1; i <= starts; i++) {
192
+ split(rows[i], field, "\t")
193
+ group[i] = field[1]; posix[i] = field[2]; native[i] = field[3]
194
+ }
195
+ probe = ENVIRON["SC_AWK_EXEC_PROBE"]
196
+ stream = (ENVIRON["SC_AWK_STREAM"] == 1)
197
+ rank["T"] = 1; rank["I"] = 2; rank["O"] = 3
198
+ }
199
+ function place( kind, j, g, k, path) {
200
+ if (count == 1) kind = "f"
201
+ else if (count == 2) kind = "d"
202
+ else if (count == 3 && probe == 1) kind = "x"
203
+ else { bad = 1; return }
204
+ if (!last || (entry != posix[last] && index(entry, posix[last] "/") != 1)) {
205
+ last = 0
206
+ for (j = 1; j <= starts; j++) {
207
+ if (entry == posix[j] || index(entry, posix[j] "/") == 1) { last = j; break }
208
+ }
209
+ if (!last) { bad = 1; return }
210
+ }
211
+ g = group[last]
212
+ listing[g] = listing[g] kind " " entry "\037"
213
+ if (kind == "d") return
214
+ k = ++files[g]
215
+ path = native[last] substr(entry, length(posix[last]) + 1)
216
+ if (!stream) { file[g, k] = path; return }
217
+ if (rank[g] < current) { bad = 1; return }
218
+ current = rank[g]
219
+ print path
220
+ fflush()
221
+ }
222
+ bad { next }
223
+ $0 == entry { count++; next }
224
+ entry != "" { place() }
225
+ { entry = $0; count = 1 }
226
+ END {
227
+ if (entry != "" && !bad) place()
228
+ if (bad) exit 1
229
+ out = ENVIRON["SC_AWK_LISTING"]
230
+ printf "T %s\nI %s\nO %s\nN %d %d %d\n", listing["T"], listing["I"], listing["O"], files["T"], files["I"], files["O"] > out
231
+ n = split("T I O", order, " ")
232
+ if (!stream) for (i = 1; i <= n; i++) for (k = 1; k <= files[order[i]]; k++) print file[order[i], k]
233
+ if (ENVIRON["SC_AWK_STATE"] != "") print ENVIRON["SC_AWK_STATE"]
234
+ }
235
+ '
236
+
237
+ # sync_cache_fingerprint <target> — one walk and one batch hash over every
238
+ # tooling, input and output path, into SC_PRINT_T, SC_PRINT_I and SC_PRINT_O.
239
+ # The cache state file joins the batch, so SC_STATE_HASH can be checked against
240
+ # its seal without another process. Unsupported paths or entry kinds return 1:
241
+ # an incomplete fingerprint must never be compared.
242
+ #
243
+ # One pipeline does the work: find lists every entry and refuses what cannot be
244
+ # fingerprinted — links and other special files, and names holding a control
245
+ # character, a backslash or a double quote (every component of a walked path is
246
+ # a start point, checked above, or an entry find visits). Directories print
247
+ # twice and, where executable bits are real, executable files three times, so
248
+ # the listing stage tells kinds without a stat per entry. git hashes the files
249
+ # while the walk is still running.
250
+ sync_cache_fingerprint() {
251
+ local target="$1" hashes i j n value ordered=0 starts_env="" state="" umask_value
252
+ local tooling inputs outputs listing saved_ifs="$IFS"
253
+ local -a hash_lines=() expression=() keep=() starts=() groups=() natives=() parts=() count=()
254
+ SC_STARTS=() SC_START_GROUPS=() SC_MISSING=() SC_START_SEEN=$'\n'
255
+ _sync_cache_starts T "${SC_TOOLING[@]}" || return 1
256
+ _sync_cache_starts I "${SC_INPUTS[@]}" || return 1
257
+ _sync_cache_starts O "${SC_OUTPUTS[@]}" || return 1
258
+ # Nested start points would list one entry twice. Within a group the outer
259
+ # one covers the inner; across groups the run is not fingerprinted.
260
+ n="${#SC_STARTS[@]}"
261
+ for ((i = 0; i < n; i++)); do
262
+ keep[i]=1
263
+ for ((j = 0; j < n; j++)); do
264
+ [ "$i" != "$j" ] || continue
265
+ case "${SC_STARTS[i]}" in
266
+ "${SC_STARTS[j]}"/*)
267
+ [ "${SC_START_GROUPS[i]}" = "${SC_START_GROUPS[j]}" ] || return 1
268
+ keep[i]=0
269
+ ;;
270
+ esac
271
+ done
272
+ done
273
+ for ((i = 0; i < n; i++)); do
274
+ [ "${keep[i]}" = 1 ] || continue
275
+ starts+=("${SC_STARTS[i]}")
276
+ groups+=("${SC_START_GROUPS[i]}")
277
+ done
278
+ SC_STATE_HASH=""
279
+ if [ -n "${SC_DIRECTORY:-}" ] && [ -f "$SC_DIRECTORY/state" ] && [ ! -L "$SC_DIRECTORY/state" ]; then
280
+ state="$SC_DIRECTORY/state"
281
+ fi
282
+ sync_cache_mounts
283
+ # Translate each start point once; its entries share the prefix.
284
+ if [ "$SC_TRANSLATE" = 1 ]; then
285
+ ordered=1
286
+ for ((j = 0; j < ${#starts[@]}; j++)); do
287
+ if ! sync_cache_native_var "${starts[j]}"; then
288
+ SC_TRANSLATE=2
289
+ break
290
+ fi
291
+ natives[j]="$IS_SC_NATIVE"
292
+ [ "${SC_MOUNT_TYPE[$IS_SC_MOUNT]}" = ntfs ] || ordered=0
293
+ done
294
+ if [ "$SC_TRANSLATE" = 1 ] && [ -n "$state" ]; then
295
+ if sync_cache_native_var "$state"; then state="$IS_SC_NATIVE"; else SC_TRANSLATE=2; fi
296
+ fi
297
+ fi
298
+ [ "$SC_TRANSLATE" = 1 ] || ordered=0
299
+ for ((j = 0; j < ${#starts[@]}; j++)); do
300
+ value="${starts[j]}"
301
+ [ "$SC_TRANSLATE" != 1 ] || value="${natives[j]}"
302
+ starts_env="$starts_env${groups[j]}"$'\t'"${starts[j]}"$'\t'"$value"$'\n'
303
+ done
304
+ starts_env="${starts_env%$'\n'}"
305
+ expression=(\( ! -type d ! -type f -exec false {} + \)
306
+ -o \( \( -name '*[[:cntrl:]]*' -o -name '*\\*' -o -name '*"*' \) -exec false {} + \)
307
+ -o -type d -print -print)
308
+ [ "$SC_EXEC_PROBE" = 0 ] || expression+=(-o -type f -perm -100 -print -print -print)
309
+ expression+=(-o -print)
310
+ if [ "${#starts[@]}" -gt 0 ]; then
311
+ # The pipeline's stages run side by side, and pipefail reports any one
312
+ # that refused; the listing stage rewrites its file whenever it succeeds.
313
+ if [ "$ordered" = 1 ]; then
314
+ # Start points are listed tooling, inputs, outputs, and an unsorted
315
+ # walk visits them in that order: stream each file to git.
316
+ hashes="$(LC_ALL=C find "${starts[@]}" "${expression[@]}" \
317
+ | SC_AWK_STARTS="$starts_env" SC_AWK_EXEC_PROBE="$SC_EXEC_PROBE" SC_AWK_STREAM=1 \
318
+ SC_AWK_LISTING="$SC_SCRATCH.listing" SC_AWK_STATE="$state" LC_ALL=C awk "$_SC_AWK_LISTING" \
319
+ | git hash-object --no-filters --stdin-paths)" || return 1
320
+ elif [ "$SC_TRANSLATE" = 2 ]; then
321
+ hashes="$(LC_ALL=C find "${starts[@]}" "${expression[@]}" | LC_ALL=C sort \
322
+ | SC_AWK_STARTS="$starts_env" SC_AWK_EXEC_PROBE="$SC_EXEC_PROBE" \
323
+ SC_AWK_LISTING="$SC_SCRATCH.listing" SC_AWK_STATE="$state" LC_ALL=C awk "$_SC_AWK_LISTING" \
324
+ | cygpath -m -f - | git hash-object --no-filters --stdin-paths)" || return 1
325
+ else
326
+ hashes="$(LC_ALL=C find "${starts[@]}" "${expression[@]}" | LC_ALL=C sort \
327
+ | SC_AWK_STARTS="$starts_env" SC_AWK_EXEC_PROBE="$SC_EXEC_PROBE" \
328
+ SC_AWK_LISTING="$SC_SCRATCH.listing" SC_AWK_STATE="$state" LC_ALL=C awk "$_SC_AWK_LISTING" \
329
+ | git hash-object --no-filters --stdin-paths)" || return 1
330
+ fi
331
+ [ -f "$SC_SCRATCH.listing" ] || return 1
332
+ listing="$(< "$SC_SCRATCH.listing")"
333
+ else
334
+ listing=$'T \nI \nO \nN 0 0 0'
335
+ fi
336
+ IFS=$'\n'
337
+ set -f
338
+ # shellcheck disable=SC2206 # word splitting on newlines is the point
339
+ parts=($listing)
340
+ # shellcheck disable=SC2206
341
+ hash_lines=($hashes)
342
+ set +f
343
+ IFS="$saved_ifs"
344
+ [ "${#parts[@]}" -eq 4 ] || return 1
345
+ case "${parts[3]}" in "N "*) ;; *) return 1 ;; esac
346
+ read -r -a count <<< "${parts[3]#N }"
347
+ n=$(( count[0] + count[1] + count[2] ))
348
+ [ -z "$state" ] || n=$((n + 1))
349
+ [ "${#hash_lines[@]}" -eq "$n" ] || return 1
350
+ [ -z "$state" ] || SC_STATE_HASH="${hash_lines[n - 1]}"
351
+ tooling="${parts[0]#T }" inputs="${parts[1]#I }" outputs="${parts[2]#O }"
352
+ for value in "${SC_MISSING[@]+"${SC_MISSING[@]}"}"; do
353
+ case "${value%% *}" in
354
+ T) tooling="$tooling${value#* }"$'\037' ;;
355
+ I) inputs="$inputs${value#* }"$'\037' ;;
356
+ *) outputs="$outputs${value#* }"$'\037' ;;
357
+ esac
358
+ done
359
+ [ "${count[0]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:0:${count[0]}}"; tooling="$tooling$value"; }
360
+ [ "${count[1]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:${count[0]}:${count[1]}}"; inputs="$inputs$value"; }
361
+ [ "${count[2]}" -eq 0 ] || { printf -v value '%s\037' "${hash_lines[@]:$((count[0] + count[1])):${count[2]}}"; outputs="$outputs$value"; }
362
+ # Explicit values that affect rendering, discovery or parsing. No secrets
363
+ # or inherited environment dump are persisted. umask prints into a file so
364
+ # reading it costs no subshell.
365
+ umask > "$SC_SCRATCH.umask" || return 1
366
+ IFS= read -r umask_value < "$SC_SCRATCH.umask" || return 1
367
+ for value in "$target" "$REPO_ROOT" "$CONFIG_FILE" "$IS_CONTENT_REL" "$IS_MODULE_REL" \
368
+ "$IS_SYNC_CMD" "$IS_MANIFEST_NAME" "$IS_PROTECTED_DIRS" "$BASH_VERSION" "${OSTYPE:-}" \
369
+ "${LANG:-}" "${LC_ALL:-}" "${LC_CTYPE:-}" "${LC_COLLATE:-}" "${PATH:-}" "$umask_value" \
370
+ "exec-probe=$SC_EXEC_PROBE" "walk-ordered=$ordered"; do
371
+ case "$value" in *[[:cntrl:]]*) return 1 ;; esac
372
+ tooling="${tooling}env $value"$'\037'
373
+ done
374
+ SC_PRINT_T="T $tooling"
375
+ SC_PRINT_I="I $inputs"
376
+ SC_PRINT_O="O $outputs"
125
377
  }
126
378
 
127
379
  sync_cache_directory() {
128
380
  local path="$REPO_ROOT/.intelligence/sync-cache" physical root_physical probe expected
381
+ SC_DIRECTORY=""
129
382
  sync_cache_safe_path "$path/state" || return 1
130
- root_physical="$(cd "$REPO_ROOT" && pwd -P)" || return 1
131
- probe="$path"; expected="$root_physical/.intelligence/sync-cache"
132
- while [ ! -e "$probe" ]; do probe="${probe%/*}"; expected="${expected%/*}"; done
133
- physical="$(cd "$probe" && pwd -P)" || return 1
134
- [ "$physical" = "$expected" ] || return 1
135
- if [ "${1:-}" = create ]; then mkdir -p "$path" || return 1; fi
383
+ # Reading is safe through the lexical check above: a record is data and
384
+ # authorizes nothing unless it matches this run exactly. Writing must not
385
+ # land outside the repository, so it also compares physical spellings.
386
+ if [ "${1:-}" = create ]; then
387
+ probe="$path"; expected="/.intelligence/sync-cache"
388
+ while [ ! -e "$probe" ]; do probe="${probe%/*}"; expected="${expected%/*}"; done
389
+ # Both physical spellings from one subshell.
390
+ physical="$(cd "$REPO_ROOT" && pwd -P && cd "$probe" && pwd -P)" || return 1
391
+ root_physical="${physical%%$'\n'*}"
392
+ physical="${physical#*$'\n'}"
393
+ [ "$physical" = "$root_physical$expected" ] || return 1
394
+ mkdir -p "$path" || return 1
395
+ fi
136
396
  SC_DIRECTORY="$path"
137
397
  }
138
398
 
399
+ # sync_cache_read — load the sealed record into SC_RECORD_T / _I / _O and
400
+ # SC_RECORD_REPORT. Anything missing, unsealed, malformed or from another
401
+ # format is no record at all.
139
402
  sync_cache_read() {
140
- local file="$1" input="$2" output="$3" work="$4" header report_hash actual
141
- [ -f "$file" ] && [ ! -L "$file" ] || return 1
142
- IFS= read -r header < "$file" || return 1
143
- case "$header" in "intelligence-sync-cache-v1 $input $output "*) ;; *) return 1 ;; esac
144
- report_hash="${header##* }"
145
- case "$report_hash" in ''|*[!a-f0-9]*) return 1 ;; esac
146
- [ "${#report_hash}" -eq 40 ] || [ "${#report_hash}" -eq 64 ] || return 1
147
- tail -n +2 "$file" > "$work/report" || return 1
148
- actual="$(git hash-object --stdin < "$work/report")" || return 1
149
- [ "$actual" = "$report_hash" ] || return 1
150
- grep -q '^IS_STATUS=ok\($\| \)' "$work/report" || return 1
151
- grep -q '^=== Done:' "$work/report" || return 1
152
- }
153
-
154
- # Keep actionable diagnostics, including the multi-line unsynced-directory
155
- # report that compact mode intentionally omits. Never replay renderer progress.
403
+ local file="$SC_DIRECTORY/state" seal="" stored offset saved_ifs="$IFS"
404
+ local -a fields=()
405
+ SC_RECORD_T="" SC_RECORD_I="" SC_RECORD_O="" SC_RECORD_REPORT=""
406
+ [ -n "$SC_STATE_HASH" ] || return 1
407
+ [ -f "$SC_DIRECTORY/seal" ] && [ ! -L "$SC_DIRECTORY/seal" ] || return 1
408
+ IFS= read -r seal < "$SC_DIRECTORY/seal" || return 1
409
+ [ "$seal" = "$SC_STATE_HASH" ] || return 1
410
+ stored="$(< "$file")" || return 1
411
+ IFS=$'\n'
412
+ set -f
413
+ # shellcheck disable=SC2206
414
+ fields=($stored)
415
+ set +f
416
+ IFS="$saved_ifs"
417
+ [ "${#fields[@]}" -ge 6 ] || return 1
418
+ [ "${fields[0]}" = "$SC_FORMAT" ] || return 1
419
+ case "${fields[1]}" in "T "*) ;; *) return 1 ;; esac
420
+ case "${fields[2]}" in "I "*) ;; *) return 1 ;; esac
421
+ case "${fields[3]}" in "O "*) ;; *) return 1 ;; esac
422
+ offset=$(( ${#fields[0]} + ${#fields[1]} + ${#fields[2]} + ${#fields[3]} + 4 ))
423
+ SC_RECORD_T="${fields[1]}"
424
+ SC_RECORD_I="${fields[2]}"
425
+ SC_RECORD_O="${fields[3]}"
426
+ SC_RECORD_REPORT="${stored:$offset}"
427
+ case $'\n'"$SC_RECORD_REPORT"$'\n' in *$'\n'IS_STATUS=ok[$' \n']*) ;; *) return 1 ;; esac
428
+ case $'\n'"$SC_RECORD_REPORT" in *$'\n''=== Done:'*) ;; *) return 1 ;; esac
429
+ }
430
+
431
+ # sync_cache_publish <report-file> — record the fingerprints taken after a
432
+ # successful render. Callers have checked that inputs and tooling did not move.
433
+ # mktemp+rename avoids following a pre-existing state-file link, and readers
434
+ # see only complete records; the seal follows the state it describes.
435
+ sync_cache_publish() {
436
+ local report="$1" staged seal_staged hash
437
+ sync_cache_directory create || return 0
438
+ staged="$(mktemp "$SC_DIRECTORY/state-XXXXXX")" || return 0
439
+ if ! { printf '%s\n' "$SC_FORMAT" "$SC_PRINT_T" "$SC_PRINT_I" "$SC_PRINT_O"; cat "$report"; } > "$staged"; then
440
+ rm -f "$staged"
441
+ return 0
442
+ fi
443
+ hash="$(git hash-object --no-filters "$staged")" || { rm -f "$staged"; return 0; }
444
+ seal_staged="$staged.seal"
445
+ printf '%s\n' "$hash" > "$seal_staged" || { rm -f "$staged" "$seal_staged"; return 0; }
446
+ mv -f "$staged" "$SC_DIRECTORY/state" || { rm -f "$staged" "$seal_staged"; return 0; }
447
+ mv -f "$seal_staged" "$SC_DIRECTORY/seal" || rm -f "$seal_staged"
448
+ }
449
+
450
+ # Keep actionable diagnostics in the same form compact mode keeps; never replay
451
+ # renderer progress.
156
452
  sync_cache_report() {
157
453
  awk '
158
- /^(WARNING:| WARN:)/ { warning=1; print; next }
454
+ /^WARNING:/ { warning=1; print; next }
159
455
  warning && /^ / { print; next }
160
456
  { warning=0 }
161
- /^(CONTEXT:|IS_STATUS=ok|=== Done:|=== WARNING:| NOT SYNCED:| Wire one in:)/ { print }
457
+ /^(CONTEXT:|IS_STATUS=ok|=== Done:)/ { print }
162
458
  ' "$1"
163
459
  }
164
460
 
165
- sync_with_cache() (
166
- local target="$1" force="$2" work before after outputs report_hash rc=0 cacheable=0
167
- work="$(mktemp -d -t intelligence-cache-XXXXXX)" || return 1
168
- trap 'rm -rf "$work"' EXIT
461
+ # sync_cache_prepare <target> — dependencies, cache directory and fingerprint;
462
+ # returns 1 when this run cannot be fingerprinted.
463
+ sync_cache_prepare() {
464
+ sync_cache_dependencies && sync_cache_directory && sync_cache_fingerprint "$1"
465
+ }
466
+
467
+ sync_with_cache() {
468
+ local target="$1" force="$2" rc=0 cacheable=0 before_t before_i
469
+ project_lock_scratch "$REPO_ROOT" || return 1
470
+ SC_SCRATCH="$IS_LOCK_SCRATCH"
169
471
  echo "Checking source files..."
170
- # Read once here so both input hashes, one in a background job, reuse it.
171
- SC_UMASK="$(umask)"
172
- if sync_cache_dependencies && sync_cache_directory; then
173
- before="$(sync_cache_input_hash "$work" "$target")" || before=""
174
- [ -z "$before" ] || cacheable=1
472
+ if sync_cache_prepare "$target"; then
473
+ cacheable=1
175
474
  fi
176
475
  if [ "$cacheable" = 1 ] && [ "$force" = 0 ]; then
177
476
  echo "Checking generated files..."
178
- outputs="$(sync_cache_fingerprint "$work" "${SC_OUTPUTS[@]}")" || outputs=""
179
- if [ -n "$outputs" ] && sync_cache_read "$SC_DIRECTORY/state" "$before" "$outputs" "$work"; then
477
+ if sync_cache_read && [ "$SC_RECORD_T" = "$SC_PRINT_T" ] && [ "$SC_RECORD_I" = "$SC_PRINT_I" ] \
478
+ && [ "$SC_RECORD_O" = "$SC_PRINT_O" ]; then
180
479
  echo 'Unchanged: generated files are up to date.'
181
- printf '\n'
182
- cat "$work/report"
480
+ printf '\n%s\n' "$SC_RECORD_REPORT"
183
481
  printf '\n=== Unchanged: no files needed updating ===\n'
184
482
  return 0
185
483
  fi
186
484
  fi
187
485
  # A failed or interrupted attempt must not leave a success cache reusable.
188
486
  if [ "$cacheable" = 1 ]; then
189
- rm -f "$SC_DIRECTORY/state" || cacheable=0
487
+ rm -f "$SC_DIRECTORY/state" "$SC_DIRECTORY/seal" || cacheable=0
190
488
  fi
489
+ before_t="${SC_PRINT_T:-}" before_i="${SC_PRINT_I:-}"
191
490
  # Stream progress while retaining diagnostics for the next cache hit. Check
192
491
  # both processes: a failed capture must never publish a success record.
193
492
  local -a pipeline_status
194
- if bash "$IS_ENGINE_DIR/sync.sh" "$target" 2>&1 | tee "$work/log"; then
493
+ if bash "$IS_ENGINE_DIR/sync.sh" "$target" 2>&1 | tee "$SC_SCRATCH.log"; then
195
494
  pipeline_status=("${PIPESTATUS[@]}")
196
495
  else
197
496
  pipeline_status=("${PIPESTATUS[@]}")
@@ -200,33 +499,188 @@ sync_with_cache() (
200
499
  [ "$rc" = 0 ] || return "$rc"
201
500
  [ "${pipeline_status[1]}" = 0 ] || return "${pipeline_status[1]}"
202
501
  if [ "$cacheable" = 1 ]; then
203
- (
204
- echo "Checking sync results..."
205
- # The input recheck and the output fingerprint only read files, so
206
- # they run side by side, each in its own scratch directory.
207
- local after_pid after_rc=0
208
- mkdir "$work/after" "$work/outputs" || return 0
209
- sync_cache_input_hash "$work/after" "$target" > "$work/after/hash" &
210
- after_pid=$!
211
- outputs="$(sync_cache_fingerprint "$work/outputs" "${SC_OUTPUTS[@]}")" || outputs=""
212
- sync_cache_report "$work/log" > "$work/report" || outputs=""
213
- report_hash="$(git hash-object --stdin < "$work/report")" || outputs=""
214
- wait "$after_pid" || after_rc=$?
215
- [ "$after_rc" = 0 ] && [ -n "$outputs" ] || return 0
216
- IFS= read -r after < "$work/after/hash" || return 0
217
- [ "$before" = "$after" ] || return 0
218
- # Recheck containment before publishing. mktemp+rename avoids following a
219
- # pre-existing state-file link, and readers see only complete records.
220
- sync_cache_directory create || return 0
221
- local staged
222
- staged="$(mktemp "$SC_DIRECTORY/state-XXXXXX")" || return 0
223
- if { printf 'intelligence-sync-cache-v1 %s %s %s\n' "$after" "$outputs" "$report_hash"; cat "$work/report"; } > "$staged"; then
224
- mv -f "$staged" "$SC_DIRECTORY/state" || rm -f "$staged"
225
- else
226
- rm -f "$staged"
227
- fi
228
- )
502
+ echo "Checking sync results..."
503
+ # Recheck inputs after rendering (decision 0010): a change during the
504
+ # run must not bless the outputs as current.
505
+ if sync_cache_fingerprint "$target" && [ "$SC_PRINT_T" = "$before_t" ] \
506
+ && [ "$SC_PRINT_I" = "$before_i" ] \
507
+ && sync_cache_report "$SC_SCRATCH.log" > "$SC_SCRATCH.report"; then
508
+ sync_cache_publish "$SC_SCRATCH.report"
509
+ fi
229
510
  fi
230
511
  echo "=== Sync complete ==="
231
512
  return 0
513
+ }
514
+
515
+ # The write contract marks links whose spelling must remain unchanged: they
516
+ # are compared, never dereferenced, and are not renderer read inputs.
517
+ sync_check_write_paths() {
518
+ local filter="$1" dir file name i output adapter kind value
519
+ local -a names=() files=() batch=()
520
+ for dir in "$IS_ENGINE_DIR/adapters" "$REPO_ROOT/$IS_CONTENT_REL/adapters"; do
521
+ for file in "$dir"/*.sh; do
522
+ [ -f "$file" ] || continue
523
+ name="${file##*/}"; name="${name%.sh}"
524
+ [ "$name" != _template ] || continue
525
+ i=0
526
+ while [ "$i" -lt "${#names[@]}" ] && [ "${names[i]}" != "$name" ]; do i=$((i + 1)); done
527
+ names[i]="$name"; files[i]="$file"
528
+ done
529
+ done
530
+ i=0
531
+ while [ "$i" -lt "${#names[@]}" ]; do
532
+ name="${names[i]}"; file="${files[i]}"; i=$((i + 1))
533
+ [ -z "$filter" ] || [ "$name" = "$filter" ] || continue
534
+ target_enabled_var "$CONFIG_FILE" "$name"
535
+ [ "$IS_TGT_ENABLED" = 1 ] || continue
536
+ target_output_var "$CONFIG_FILE" "$name"
537
+ output="${IS_TGT_OUTPUT:-.$name}"
538
+ batch+=("$name" "$file" "$output")
539
+ done
540
+ SC_CHECK_OUTPUTS=()
541
+ [ "${#batch[@]}" -gt 0 ] || return 0
542
+ adapter_contract_records_batch "$SC_SCRATCH.check-contract" "${batch[@]}" > "$SC_SCRATCH.check-records" || return 1
543
+ while IFS=$'\t' read -r adapter kind value; do
544
+ case "$kind" in owned|managed) SC_CHECK_OUTPUTS+=("$value") ;; esac
545
+ done < "$SC_SCRATCH.check-records"
546
+ }
547
+
548
+ # Check renders use a private project tree. A rollback in the live tree would
549
+ # overwrite unrelated edits made while the renderer was running.
550
+ sync_check_render() (
551
+ local target="$1" verdict="$2" staging entry link target_link manifest_rel rel output is_output original_target original_root="$REPO_ROOT" rc=0
552
+ local -a SC_CHECK_OUTPUTS=()
553
+ sync_check_write_paths "$target" || return 1
554
+ manifest_rel="${CONFIG_FILE#"$REPO_ROOT/"}"
555
+ staging="$(mktemp -d -t intelligence-check-XXXXXX)" || return 1
556
+ trap 'rm -rf "$staging"' EXIT
557
+ trap 'exit 130' INT
558
+ trap 'exit 143' TERM
559
+ mkdir "$staging/project" || { rm -rf "$staging"; return 1; }
560
+ for entry in "$REPO_ROOT"/* "$REPO_ROOT"/.[!.]* "$REPO_ROOT"/..?*; do
561
+ [ -e "$entry" ] || [ -L "$entry" ] || continue
562
+ # Git history is not a renderer input. The project lock contains the
563
+ # check's scratch buffers and must stay solely in the real project.
564
+ [ "${entry##*/}" != .git ] || continue
565
+ cp -a "$entry" "$staging/project/" || rc=$?
566
+ [ "$rc" = 0 ] || break
567
+ done
568
+ if [ "$rc" = 0 ]; then
569
+ # Never remove or rewrite through a copied directory link: it may
570
+ # still point at the live project or an external source.
571
+ if [ ! -L "$staging/project/.intelligence" ]; then
572
+ rm -rf "$staging/project/.intelligence/sync.lock"
573
+ fi
574
+ find "$staging/project" -type l -print0 > "$staging/links" || rc=1
575
+ while IFS= read -r -d '' link; do
576
+ target_link="$(readlink "$link")" || { rc=1; break; }
577
+ # Output links need no relocation: comparison never follows them.
578
+ # Internal absolute links still move with their copied project.
579
+ rel="${link#"$staging/project/"}"
580
+ is_output=0
581
+ for output in ${SC_CHECK_OUTPUTS[@]+"${SC_CHECK_OUTPUTS[@]}"}; do
582
+ case "$rel" in "$output"|"$output"/*) is_output=1; break ;; esac
583
+ done
584
+ case "$target_link" in
585
+ "$REPO_ROOT")
586
+ rm "$link" && ln -s "$staging/project" "$link" || { rc=1; break; }
587
+ ;;
588
+ "$REPO_ROOT"/*)
589
+ rm "$link" && ln -s "$staging/project/${target_link#"$REPO_ROOT/"}" "$link" || { rc=1; break; }
590
+ ;;
591
+ /*) ;; # Absolute external links already retain their resolution.
592
+ *)
593
+ [ "$is_output" = 0 ] || continue
594
+ original_target="$REPO_ROOT/$rel"
595
+ normalize_path_var "${original_target%/*}/$target_link"
596
+ original_target="$IS_NORM_PATH"
597
+ case "$original_target" in
598
+ "$REPO_ROOT") original_target="$staging/project" ;;
599
+ "$REPO_ROOT"/*) original_target="$staging/project/${original_target#"$REPO_ROOT/"}" ;;
600
+ esac
601
+ normalize_path_var "${link%/*}/$target_link"
602
+ [ "$IS_NORM_PATH" != "$original_target" ] || continue
603
+ rm "$link" && ln -s "$original_target" "$link" || { rc=1; break; }
604
+ ;;
605
+ esac
606
+ done < "$staging/links"
607
+ fi
608
+ if [ "$rc" = 0 ]; then
609
+ REPO_ROOT="$staging/project" CONFIG_FILE="$staging/project/$manifest_rel" \
610
+ IS_SYNC_CHECK_ROOT="$original_root" IS_SYNC_CHECK="$verdict" bash "$IS_ENGINE_DIR/sync.sh" "$target" || rc=$?
611
+ fi
612
+ return "$rc"
232
613
  )
614
+
615
+ # Warning forms are shared by cached and freshly rendered check reports.
616
+ sync_check_warnings() {
617
+ awk '
618
+ /^(WARNING:| WARN:)/ { warning=1; print; next }
619
+ warning && /^ / { print; next }
620
+ { warning=0 }
621
+ '
622
+ }
623
+
624
+ # --- sync --check ----------------------------------------------------------------
625
+ # sync_check <target> <force> — 0 when generated files are what sync would
626
+ # leave, 2 when sync would change them (IS_CHECK_OUT_OF_DATE=1 marks that
627
+ # verdict, so the command can turn a failure that ends in 2 into 1), and the
628
+ # failure's own status otherwise. Prints one IS_STATUS line. A valid record
629
+ # from the same tooling answers without rendering; otherwise the engine renders
630
+ # in a private project copy, compares without following links, and discards
631
+ # the copy; a clean result is recorded so the next check is fast.
632
+ # shellcheck disable=SC2034 # IS_CHECK_OUT_OF_DATE is read by commands/sync.sh
633
+ sync_check() {
634
+ local target="$1" force="$2" rc=0 cacheable=0 verdict="" sources=0 generated=0
635
+ local before_t before_i before_o
636
+ project_lock_scratch "$REPO_ROOT" || return 1
637
+ SC_SCRATCH="$IS_LOCK_SCRATCH"
638
+ if sync_cache_prepare "$target"; then
639
+ cacheable=1
640
+ fi
641
+ if [ "$cacheable" = 1 ] && [ "$force" = 0 ] && sync_cache_read && [ "$SC_RECORD_T" = "$SC_PRINT_T" ]; then
642
+ [ "$SC_RECORD_I" = "$SC_PRINT_I" ] || sources=1
643
+ [ "$SC_RECORD_O" = "$SC_PRINT_O" ] || generated=1
644
+ case "$sources$generated" in
645
+ 00)
646
+ sync_check_warnings <<< "$SC_RECORD_REPORT" >&2
647
+ is_status ok "generated files are up to date"; return 0 ;;
648
+ 10) is_status out-of-date "sources changed since the last sync; run 'intelligence sync'" ;;
649
+ 01) ;; # Managed directories also contain preserved hand-written files.
650
+ *) is_status out-of-date "sources and generated files changed since the last sync; run 'intelligence sync'" ;;
651
+ esac
652
+ if [ "$sources" = 1 ]; then
653
+ IS_CHECK_OUT_OF_DATE=1
654
+ return "$IS_RC_OUT_OF_DATE"
655
+ fi
656
+ fi
657
+ before_t="${SC_PRINT_T:-}" before_i="${SC_PRINT_I:-}" before_o="${SC_PRINT_O:-}"
658
+ rm -f "$SC_SCRATCH.verdict"
659
+ sync_check_render "$target" "$SC_SCRATCH.verdict" > "$SC_SCRATCH.log" 2>&1 || rc=$?
660
+ if [ "$rc" = 0 ]; then
661
+ [ ! -f "$SC_SCRATCH.verdict" ] || IFS= read -r verdict < "$SC_SCRATCH.verdict" || verdict=""
662
+ case "$verdict" in
663
+ same|differs) ;;
664
+ *) echo "ERROR: the engine rendered without reporting a comparison" >&2; rc=1 ;;
665
+ esac
666
+ fi
667
+ if [ "$rc" != 0 ]; then
668
+ cat "$SC_SCRATCH.log" >&2
669
+ return "$rc"
670
+ fi
671
+ sync_check_warnings < "$SC_SCRATCH.log" >&2
672
+ if [ "$verdict" = differs ]; then
673
+ IS_CHECK_OUT_OF_DATE=1
674
+ is_status out-of-date "rendering would change generated files; run 'intelligence sync'"
675
+ return "$IS_RC_OUT_OF_DATE"
676
+ fi
677
+ # Publish under sync's rules: tooling and inputs unchanged by the run, and
678
+ # outputs exactly as they were before it (the engine restored them).
679
+ if [ "$cacheable" = 1 ] && sync_cache_fingerprint "$target" && [ "$SC_PRINT_T" = "$before_t" ] \
680
+ && [ "$SC_PRINT_I" = "$before_i" ] && [ "$SC_PRINT_O" = "$before_o" ] \
681
+ && sync_cache_report "$SC_SCRATCH.log" > "$SC_SCRATCH.report"; then
682
+ sync_cache_publish "$SC_SCRATCH.report"
683
+ fi
684
+ is_status ok "generated files are up to date (rendered to compare)"
685
+ return 0
686
+ }