@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.
- package/cli/commands/init.sh +2 -1
- package/cli/commands/registry.sh +1 -1
- package/cli/commands/source.sh +1 -1
- package/cli/commands/sync.sh +78 -6
- package/cli/intelligence +15 -2
- package/cli/internal/check.sh +39 -0
- package/cli/internal/convert-legacy.sh +1 -1
- package/cli/internal/package-search.sh +1 -1
- package/cli/internal/package-update.sh +2 -2
- package/cli/internal/restore.sh +3 -2
- package/cli/lib/cli-common.sh +145 -33
- package/cli/lib/gitignore.sh +19 -2
- package/cli/lib/lockfile.sh +14 -4
- package/cli/lib/manifest.sh +147 -1
- package/cli/lib/qmap.awk +97 -32
- package/cli/lib/registry.sh +1 -1
- package/cli/lib/sync-cache.sh +596 -142
- package/cli/lib/sync-lock.sh +19 -2
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/_template.sh +32 -8
- package/engine/adapters/agents.sh +47 -52
- package/engine/adapters/antigravity.sh +71 -53
- package/engine/adapters/claude.sh +42 -57
- package/engine/adapters/codex.sh +77 -16
- package/engine/adapters/copilot.sh +54 -65
- package/engine/adapters/cursor.sh +88 -78
- package/engine/adapters/opencode.sh +23 -32
- package/engine/adapters/pi.sh +140 -42
- package/engine/lib/adapter-contract.sh +36 -4
- package/engine/lib/common.sh +1388 -165
- package/engine/lib/contract.sh +5 -0
- package/engine/sync.sh +145 -38
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +48 -38
- package/packages/sync/references/conventions.md +61 -12
- package/packages/sync/references/onboarding-migration.md +4 -4
- package/packages/sync/rules/intelligence-authoring.md +2 -2
- package/packages/sync/skills/intelligence-learn-from-repository/SKILL.md +7 -5
- package/packages/sync/skills/intelligence-review-context/references/audit-checks.md +1 -1
- package/packages/sync/skills/intelligence-update-context/references/rules.md +5 -0
package/cli/lib/sync-cache.sh
CHANGED
|
@@ -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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# on
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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="$
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
[ "$
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
|
|
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
|
-
/^
|
|
454
|
+
/^WARNING:/ { warning=1; print; next }
|
|
159
455
|
warning && /^ / { print; next }
|
|
160
456
|
{ warning=0 }
|
|
161
|
-
/^(CONTEXT:|IS_STATUS=ok|=== Done
|
|
457
|
+
/^(CONTEXT:|IS_STATUS=ok|=== Done:)/ { print }
|
|
162
458
|
' "$1"
|
|
163
459
|
}
|
|
164
460
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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 "$
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|