@house365/comate 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/bin/cli.mjs +45 -0
- package/bin/init-run.mjs +99 -0
- package/bin/init.mjs +159 -0
- package/docs/COMATE_MANUAL.md +612 -0
- package/package.json +52 -0
- package/skills/comate/SKILL.md +213 -0
- package/skills/comate/reference/decision-points.md +54 -0
- package/skills/comate/reference/dirty-worktree.md +58 -0
- package/skills/comate/reference/prerequisites.md +88 -0
- package/skills/comate/scripts/comate-lib.sh +430 -0
- package/skills/comate/scripts/comate.sh +1681 -0
- package/skills/comate/tests/comate.bats +449 -0
- package/skills/comate/tests/run.sh +101 -0
- package/skills/comate-archive/SKILL.md +63 -0
- package/skills/comate-build/SKILL.md +152 -0
- package/skills/comate-design/SKILL.md +106 -0
- package/skills/comate-hotfix/SKILL.md +115 -0
- package/skills/comate-open/SKILL.md +109 -0
- package/skills/comate-tweak/SKILL.md +109 -0
- package/skills/comate-verify/SKILL.md +130 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# comate-lib.sh — shared library for comate (sourced, never executed directly)
|
|
3
|
+
#
|
|
4
|
+
# Single source of truth for: YAML flat read/write, field-order template + enums,
|
|
5
|
+
# hashing, logging, exit-code contract. Other scripts `source` this file.
|
|
6
|
+
#
|
|
7
|
+
# - all YAML logic lives here (Comet duplicated it across 4 scripts).
|
|
8
|
+
# - .comate.yaml is a declared flat `key: value` subset; validate_flat_yaml enforces it.
|
|
9
|
+
# - yaml_set does full-rewrite + atomic mv + write-back verification.
|
|
10
|
+
# - stable exit codes — 0 OK | 1 BLOCK | 2 USAGE | 3 ENV | 4 CORRUPT.
|
|
11
|
+
#
|
|
12
|
+
# This file MUST NOT set `set -e` / `set -u`: it is sourced into hosts that
|
|
13
|
+
# manage their own shell options.
|
|
14
|
+
|
|
15
|
+
# --- Exit-code contract ---
|
|
16
|
+
readonly COMATE_OK=0
|
|
17
|
+
readonly COMATE_BLOCK=1
|
|
18
|
+
readonly COMATE_USAGE=2
|
|
19
|
+
readonly COMATE_ENV=3
|
|
20
|
+
readonly COMATE_CORRUPT=4
|
|
21
|
+
|
|
22
|
+
# --- Color logging on stderr ---
|
|
23
|
+
_comate_color() {
|
|
24
|
+
# $1 = ansi code, $2 = message
|
|
25
|
+
if [ -t 2 ]; then
|
|
26
|
+
printf '\033[%sm%s\033[0m\n' "$1" "$2" >&2
|
|
27
|
+
else
|
|
28
|
+
printf '%s\n' "$2" >&2
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
log_pass() { _comate_color "32" "$1"; }
|
|
32
|
+
log_fail() { _comate_color "31" "$1"; }
|
|
33
|
+
log_warn() { _comate_color "33" "$1"; }
|
|
34
|
+
log_info() { _comate_color "36" "$1"; }
|
|
35
|
+
|
|
36
|
+
# die <code> <msg> — print red error to stderr and exit with the given code.
|
|
37
|
+
die() {
|
|
38
|
+
local code="$1"
|
|
39
|
+
shift
|
|
40
|
+
_comate_color "31" "$*"
|
|
41
|
+
exit "$code"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# --- Canonical field order ---
|
|
45
|
+
# Every yaml_set rewrites the whole file in this order, so diffs stay stable
|
|
46
|
+
# and duplicate keys are impossible.
|
|
47
|
+
# Three-place sync rule: a new field must be added here, in ENUMS (if enum),
|
|
48
|
+
# and in the bats yaml fixtures.
|
|
49
|
+
FIELD_ORDER=(
|
|
50
|
+
schema_version
|
|
51
|
+
workflow
|
|
52
|
+
phase
|
|
53
|
+
build_mode
|
|
54
|
+
tdd_mode
|
|
55
|
+
isolation
|
|
56
|
+
verify_mode
|
|
57
|
+
verify_result
|
|
58
|
+
design_doc
|
|
59
|
+
plan
|
|
60
|
+
verification_report
|
|
61
|
+
branch_status
|
|
62
|
+
handoff_context
|
|
63
|
+
handoff_hash
|
|
64
|
+
impl_context
|
|
65
|
+
impl_hash
|
|
66
|
+
direct_override
|
|
67
|
+
base_ref
|
|
68
|
+
base_branch
|
|
69
|
+
source_kind
|
|
70
|
+
source_manifest
|
|
71
|
+
build_command
|
|
72
|
+
verify_command
|
|
73
|
+
created_at
|
|
74
|
+
verified_at
|
|
75
|
+
archived
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Current schema version written by `state init`.
|
|
79
|
+
readonly COMATE_SCHEMA_VERSION=1
|
|
80
|
+
|
|
81
|
+
# --- Enum table: key -> space-separated legal values ---
|
|
82
|
+
declare -A ENUMS=(
|
|
83
|
+
[schema_version]="1"
|
|
84
|
+
[workflow]="full hotfix tweak"
|
|
85
|
+
[phase]="open design build verify archive"
|
|
86
|
+
[build_mode]="subagent-driven-development executing-plans direct"
|
|
87
|
+
[tdd_mode]="tdd direct"
|
|
88
|
+
[isolation]="branch worktree"
|
|
89
|
+
[verify_mode]="light full"
|
|
90
|
+
[verify_result]="pending pass fail"
|
|
91
|
+
[branch_status]="pending handled"
|
|
92
|
+
[direct_override]="true false"
|
|
93
|
+
[archived]="true false"
|
|
94
|
+
[source_kind]="conversation file"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Keys whose values are free-form (paths, dates, commands, hashes, refs).
|
|
98
|
+
# Listed so validate_change/validate can flag genuinely unknown keys.
|
|
99
|
+
declare -A FREEFORM_KEYS=(
|
|
100
|
+
[design_doc]=1
|
|
101
|
+
[plan]=1
|
|
102
|
+
[verification_report]=1
|
|
103
|
+
[handoff_context]=1
|
|
104
|
+
[handoff_hash]=1
|
|
105
|
+
[impl_context]=1
|
|
106
|
+
[impl_hash]=1
|
|
107
|
+
[base_ref]=1
|
|
108
|
+
[base_branch]=1
|
|
109
|
+
[source_manifest]=1
|
|
110
|
+
[build_command]=1
|
|
111
|
+
[verify_command]=1
|
|
112
|
+
[created_at]=1
|
|
113
|
+
[verified_at]=1
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# --- Path location ---
|
|
117
|
+
|
|
118
|
+
# comate_change_dir <name> — echo the change directory, preferring the active
|
|
119
|
+
# location but falling back to archive/ if that is where it lives.
|
|
120
|
+
comate_change_dir() {
|
|
121
|
+
local name="$1"
|
|
122
|
+
if [ -d "openspec/changes/$name" ]; then
|
|
123
|
+
echo "openspec/changes/$name"
|
|
124
|
+
elif [ -d "openspec/changes/archive/$name" ]; then
|
|
125
|
+
echo "openspec/changes/archive/$name"
|
|
126
|
+
else
|
|
127
|
+
echo "openspec/changes/$name"
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# comate_yaml_for <name> — echo path to that change's .comate.yaml.
|
|
132
|
+
comate_yaml_for() {
|
|
133
|
+
echo "$(comate_change_dir "$1")/.comate.yaml"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# --- Input validation ---
|
|
137
|
+
|
|
138
|
+
# validate_change_name <name> — only [A-Za-z0-9_-]; reject empty and `..`.
|
|
139
|
+
validate_change_name() {
|
|
140
|
+
local name="$1"
|
|
141
|
+
if [ -z "$name" ]; then
|
|
142
|
+
die "$COMATE_USAGE" "ERROR: change name cannot be empty"
|
|
143
|
+
fi
|
|
144
|
+
if [[ "$name" == *..* ]]; then
|
|
145
|
+
die "$COMATE_USAGE" "ERROR: change name cannot contain '..' (path traversal not allowed): '$name'"
|
|
146
|
+
fi
|
|
147
|
+
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
148
|
+
die "$COMATE_USAGE" "ERROR: invalid change name '$name' (allowed: a-z A-Z 0-9 - _)"
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# validate_enum <key> <value> — die if value not legal for an enum key.
|
|
153
|
+
# Free-form keys and unlisted keys pass through. null/empty always allowed.
|
|
154
|
+
validate_enum() {
|
|
155
|
+
local key="$1" value="$2"
|
|
156
|
+
local legal="${ENUMS[$key]:-}"
|
|
157
|
+
[ -z "$legal" ] && return 0
|
|
158
|
+
[ -z "$value" ] && return 0
|
|
159
|
+
[ "$value" = "null" ] && return 0
|
|
160
|
+
local v
|
|
161
|
+
for v in $legal; do
|
|
162
|
+
[ "$value" = "$v" ] && return 0
|
|
163
|
+
done
|
|
164
|
+
die "$COMATE_USAGE" "ERROR: invalid value '$value' for '$key' (expected: $legal)"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# --- YAML read (single implementation) ---
|
|
168
|
+
|
|
169
|
+
# _strip_inline_comment — drop a ` #...` comment that is not inside quotes.
|
|
170
|
+
_strip_inline_comment() {
|
|
171
|
+
printf '%s\n' "$1" | awk -v squote="'" '
|
|
172
|
+
{
|
|
173
|
+
out = ""; quote = ""
|
|
174
|
+
for (i = 1; i <= length($0); i++) {
|
|
175
|
+
c = substr($0, i, 1)
|
|
176
|
+
if (quote == "") {
|
|
177
|
+
if (c == "\"" || c == squote) { quote = c }
|
|
178
|
+
else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
|
|
179
|
+
sub(/[[:space:]]+$/, "", out); print out; next
|
|
180
|
+
}
|
|
181
|
+
} else if (c == quote) { quote = "" }
|
|
182
|
+
out = out c
|
|
183
|
+
}
|
|
184
|
+
print out
|
|
185
|
+
}'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# _strip_wrapping_quotes — remove a single pair of wrapping quotes.
|
|
189
|
+
_strip_wrapping_quotes() {
|
|
190
|
+
local value="$1"
|
|
191
|
+
case "$value" in
|
|
192
|
+
\"*\") printf '%s\n' "${value:1:${#value}-2}" ;;
|
|
193
|
+
\'*\') printf '%s\n' "${value:1:${#value}-2}" ;;
|
|
194
|
+
*) printf '%s\n' "$value" ;;
|
|
195
|
+
esac
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# yaml_get <file> <key> — echo the value (comment/quote-stripped), empty if absent.
|
|
199
|
+
yaml_get() {
|
|
200
|
+
local file="$1" key="$2"
|
|
201
|
+
[ -f "$file" ] || return 0
|
|
202
|
+
local value
|
|
203
|
+
value=$(grep "^${key}:" "$file" 2>/dev/null | head -1 | sed "s/^${key}:[[:space:]]*//" || true)
|
|
204
|
+
value=$(_strip_inline_comment "$value")
|
|
205
|
+
_strip_wrapping_quotes "$value"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# yaml_keys <file> — list all top-level keys, one per line.
|
|
209
|
+
yaml_keys() {
|
|
210
|
+
local file="$1"
|
|
211
|
+
[ -f "$file" ] || return 0
|
|
212
|
+
grep -E '^[A-Za-z0-9_-]+:' "$file" 2>/dev/null | sed 's/:.*//' || true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# _yaml_read_all <file> <assoc-array-name> — load every known field into the
|
|
216
|
+
# named associative array, defaulting missing fields to "null".
|
|
217
|
+
_yaml_read_all() {
|
|
218
|
+
local file="$1" arrname="$2" k
|
|
219
|
+
for k in "${FIELD_ORDER[@]}"; do
|
|
220
|
+
local v
|
|
221
|
+
v=$(yaml_get "$file" "$k")
|
|
222
|
+
[ -z "$v" ] && v="null"
|
|
223
|
+
printf -v "${arrname}[$k]" '%s' "$v"
|
|
224
|
+
done
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# validate_flat_yaml <file> — enforce that .comate.yaml is the
|
|
228
|
+
# declared subset: top-level `key: value` scalars plus `#` comments only.
|
|
229
|
+
# Reject indented lines, list items, block/flow collections. die(CORRUPT) on violation.
|
|
230
|
+
validate_flat_yaml() {
|
|
231
|
+
local file="$1"
|
|
232
|
+
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: state file not found: $file"
|
|
233
|
+
local lineno=0 line
|
|
234
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
235
|
+
lineno=$((lineno + 1))
|
|
236
|
+
# Allow blank lines and full-line comments.
|
|
237
|
+
case "$line" in
|
|
238
|
+
"" ) continue ;;
|
|
239
|
+
esac
|
|
240
|
+
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
|
241
|
+
continue
|
|
242
|
+
fi
|
|
243
|
+
# Reject leading whitespace (nesting / continuation / list under a key).
|
|
244
|
+
if [[ "$line" =~ ^[[:space:]]+ ]]; then
|
|
245
|
+
die "$COMATE_CORRUPT" "ERROR: $file:$lineno indented line not allowed (flat key:value only): $line"
|
|
246
|
+
fi
|
|
247
|
+
# Reject top-level list items.
|
|
248
|
+
if [[ "$line" =~ ^- ]]; then
|
|
249
|
+
die "$COMATE_CORRUPT" "ERROR: $file:$lineno list item not allowed (flat key:value only): $line"
|
|
250
|
+
fi
|
|
251
|
+
# Require a `key: value` shape.
|
|
252
|
+
if [[ ! "$line" =~ ^[A-Za-z0-9_-]+: ]]; then
|
|
253
|
+
die "$COMATE_CORRUPT" "ERROR: $file:$lineno not a 'key: value' line: $line"
|
|
254
|
+
fi
|
|
255
|
+
# Reject values that open a block scalar (| or >).
|
|
256
|
+
local val
|
|
257
|
+
val=$(printf '%s\n' "$line" | sed 's/^[A-Za-z0-9_-]*:[[:space:]]*//')
|
|
258
|
+
val=$(_strip_inline_comment "$val")
|
|
259
|
+
case "$val" in
|
|
260
|
+
"|"|">"|"|-"|">-"|"|+"|">+")
|
|
261
|
+
die "$COMATE_CORRUPT" "ERROR: $file:$lineno block scalar not allowed (flat key:value only): $line" ;;
|
|
262
|
+
esac
|
|
263
|
+
done < "$file"
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# --- YAML write (full rewrite + atomic mv + write-back check) ---
|
|
268
|
+
|
|
269
|
+
# yaml_set <file> <key> <value> — validate enum, read whole file, set key in
|
|
270
|
+
# memory, rewrite every field in FIELD_ORDER to a temp file, atomically mv into
|
|
271
|
+
# place, then read back and confirm the value matches.
|
|
272
|
+
yaml_set() {
|
|
273
|
+
local file="$1" key="$2" value="$3"
|
|
274
|
+
[ -f "$file" ] || die "$COMATE_CORRUPT" "ERROR: state file not found: $file"
|
|
275
|
+
|
|
276
|
+
# Reject keys we do not know about (prevents silent typo fields).
|
|
277
|
+
if [ -z "${ENUMS[$key]:-}" ] && [ -z "${FREEFORM_KEYS[$key]:-}" ] && [ "$key" != "schema_version" ]; then
|
|
278
|
+
die "$COMATE_USAGE" "ERROR: unknown field '$key'"
|
|
279
|
+
fi
|
|
280
|
+
validate_enum "$key" "$value"
|
|
281
|
+
|
|
282
|
+
declare -A _cur
|
|
283
|
+
_yaml_read_all "$file" _cur
|
|
284
|
+
_cur[$key]="$value"
|
|
285
|
+
|
|
286
|
+
local tmp
|
|
287
|
+
tmp=$(mktemp "${TMPDIR:-/tmp}/comate.XXXXXX") || die "$COMATE_ENV" "ERROR: mktemp failed"
|
|
288
|
+
{
|
|
289
|
+
printf '# comate state — flat key:value only, no nesting\n'
|
|
290
|
+
local k
|
|
291
|
+
for k in "${FIELD_ORDER[@]}"; do
|
|
292
|
+
printf '%s: %s\n' "$k" "${_cur[$k]:-null}"
|
|
293
|
+
done
|
|
294
|
+
} > "$tmp"
|
|
295
|
+
|
|
296
|
+
mv "$tmp" "$file" || { rm -f "$tmp"; die "$COMATE_ENV" "ERROR: failed to replace $file"; }
|
|
297
|
+
|
|
298
|
+
# Write-back verification.
|
|
299
|
+
local readback
|
|
300
|
+
readback=$(yaml_get "$file" "$key")
|
|
301
|
+
if [ "$readback" != "$value" ]; then
|
|
302
|
+
die "$COMATE_CORRUPT" "ERROR: write-back mismatch for '$key': wrote '$value', read '$readback'"
|
|
303
|
+
fi
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# --- Hashing (cross-platform) ---
|
|
307
|
+
|
|
308
|
+
# _hash_stream — sha256 of stdin, hex only.
|
|
309
|
+
_hash_stream() {
|
|
310
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
311
|
+
sha256sum | awk '{print $1}'
|
|
312
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
313
|
+
shasum -a 256 | awk '{print $1}'
|
|
314
|
+
else
|
|
315
|
+
die "$COMATE_ENV" "ERROR: sha256sum or shasum is required"
|
|
316
|
+
fi
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# hash_file <file> — sha256 hex of one file.
|
|
320
|
+
hash_file() {
|
|
321
|
+
local file="$1"
|
|
322
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
323
|
+
sha256sum "$file" | awk '{print $1}'
|
|
324
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
325
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
326
|
+
else
|
|
327
|
+
die "$COMATE_ENV" "ERROR: sha256sum or shasum is required"
|
|
328
|
+
fi
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# hash_files_manifest <file...> — for each existing file emit `path:`/`sha256:`
|
|
332
|
+
# lines, then hash the whole manifest. Order-stable, missing files skipped.
|
|
333
|
+
hash_files_manifest() {
|
|
334
|
+
local f
|
|
335
|
+
for f in "$@"; do
|
|
336
|
+
if [ -f "$f" ]; then
|
|
337
|
+
printf 'path:%s\n' "$f"
|
|
338
|
+
printf 'sha256:%s\n' "$(hash_file "$f")"
|
|
339
|
+
fi
|
|
340
|
+
done | _hash_stream
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# file_nonempty <path>
|
|
344
|
+
file_nonempty() { [ -f "$1" ] && [ -s "$1" ]; }
|
|
345
|
+
|
|
346
|
+
# --- Source tracking (knowledge-base provenance) ---
|
|
347
|
+
|
|
348
|
+
# comate_kb_root — repo-relative root under which source files may be tracked.
|
|
349
|
+
comate_kb_root() { echo "docs/knowledge-base"; }
|
|
350
|
+
|
|
351
|
+
# _abs_path <path> — absolute, symlink-resolved path; empty + non-zero if it
|
|
352
|
+
# cannot be resolved. realpath/readlink -f preferred; dir-level fallback otherwise.
|
|
353
|
+
_abs_path() {
|
|
354
|
+
local p="$1"
|
|
355
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
356
|
+
realpath "$p" 2>/dev/null
|
|
357
|
+
elif command -v readlink >/dev/null 2>&1 && readlink -f "$p" >/dev/null 2>&1; then
|
|
358
|
+
readlink -f "$p" 2>/dev/null
|
|
359
|
+
else
|
|
360
|
+
local dir base
|
|
361
|
+
dir=$(dirname "$p"); base=$(basename "$p")
|
|
362
|
+
( cd "$dir" 2>/dev/null && printf '%s/%s\n' "$(pwd -P)" "$base" )
|
|
363
|
+
fi
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# validate_kb_path <path> — echo the repo-relative path if it resolves to an
|
|
367
|
+
# existing file under the knowledge-base root, else echo nothing and return 1.
|
|
368
|
+
# Rejects '..' traversal, absolute/repo-external paths, and symlink escapes.
|
|
369
|
+
validate_kb_path() {
|
|
370
|
+
local raw="$1" root_abs target_abs cwd
|
|
371
|
+
[ -n "$raw" ] || return 1
|
|
372
|
+
case "$raw" in *..*) return 1 ;; esac
|
|
373
|
+
[ -f "$raw" ] || return 1
|
|
374
|
+
root_abs=$(_abs_path "$(comate_kb_root)") || return 1
|
|
375
|
+
[ -n "$root_abs" ] || return 1
|
|
376
|
+
target_abs=$(_abs_path "$raw") || return 1
|
|
377
|
+
[ -n "$target_abs" ] || return 1
|
|
378
|
+
case "$target_abs" in
|
|
379
|
+
"$root_abs"/*) ;;
|
|
380
|
+
*) return 1 ;;
|
|
381
|
+
esac
|
|
382
|
+
cwd=$(_abs_path .) || return 1
|
|
383
|
+
printf '%s\n' "${target_abs#"$cwd"/}"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# sources_manifest_for <name> — echo path to a change's source manifest.
|
|
387
|
+
sources_manifest_for() {
|
|
388
|
+
echo "$(comate_change_dir "$1")/.comate/sources.manifest"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# sources_manifest_paths <manifest> — list recorded `path:` values, one per line.
|
|
392
|
+
sources_manifest_paths() {
|
|
393
|
+
local m="$1"
|
|
394
|
+
[ -f "$m" ] || return 0
|
|
395
|
+
grep '^path:' "$m" 2>/dev/null | sed 's/^path://' || true
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# sources_drift <manifest> — recompute sha256 for each recorded path and compare
|
|
399
|
+
# against the manifest. Echo human-readable detail; return COMATE_OK if all
|
|
400
|
+
# match, COMATE_BLOCK on drift/missing file, COMATE_CORRUPT on malformed entries.
|
|
401
|
+
sources_drift() {
|
|
402
|
+
local m="$1" path="" recorded="" rc=0 line actual
|
|
403
|
+
[ -f "$m" ] || { echo "manifest not found: $m"; return "$COMATE_CORRUPT"; }
|
|
404
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
405
|
+
case "$line" in
|
|
406
|
+
path:*) path="${line#path:}" ;;
|
|
407
|
+
sha256:*)
|
|
408
|
+
recorded="${line#sha256:}"
|
|
409
|
+
if [ -z "$path" ]; then echo "manifest corrupt: sha256 with no preceding path"; return "$COMATE_CORRUPT"; fi
|
|
410
|
+
if [ ! -f "$path" ]; then
|
|
411
|
+
echo "source file missing: $path"; rc=1
|
|
412
|
+
else
|
|
413
|
+
actual=$(hash_file "$path")
|
|
414
|
+
if [ "$actual" != "$recorded" ]; then
|
|
415
|
+
echo "source drifted: $path"
|
|
416
|
+
echo " recorded: $recorded"
|
|
417
|
+
echo " actual: $actual"
|
|
418
|
+
rc=1
|
|
419
|
+
fi
|
|
420
|
+
fi
|
|
421
|
+
;;
|
|
422
|
+
---|"") path=""; recorded="" ;;
|
|
423
|
+
esac
|
|
424
|
+
done < "$m"
|
|
425
|
+
[ "$rc" -eq 0 ] && return "$COMATE_OK"
|
|
426
|
+
return "$COMATE_BLOCK"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# json_escape <string> — escape backslash and double-quote for JSON values.
|
|
430
|
+
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|