@sdd-method/sdd-cli 0.110.2 → 0.111.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.
@@ -1,660 +0,0 @@
1
- #!/bin/bash
2
- #
3
- # sync-method-baseline.sh
4
- # Apply a built method baseline bundle to a target repository.
5
- #
6
- # Usage:
7
- # ./orchestration/scripts/sync-method-baseline.sh --bundle dist/platform-baseline/platform-baseline-v0.4.0.tar.gz
8
- # ./orchestration/scripts/sync-method-baseline.sh --bundle ... --target-repo /path/to/repo --dry-run
9
- #
10
- # Multi-bundle lock model (lock_version: 3):
11
- # The target repo holds one method-baseline.lock file with a `bundles:`
12
- # list. Each entry records a synced bundle (forward base or additive
13
- # extension) with its own managed_paths. Re-syncing a bundle replaces
14
- # that bundle's entry; other bundles' entries are preserved. Removal
15
- # logic is scoped per-bundle so a forward-base sync never deletes
16
- # files owned by an extension bundle, and vice versa.
17
- #
18
- # Forward bases (platform-baseline, app-baseline, integration-baseline)
19
- # set .sdd-repo-kind. Additive layers (reverse-extension-baseline) do
20
- # not, and require a forward base entry already in the lock. See ADR 0135.
21
-
22
- set -euo pipefail
23
-
24
- RED='\033[0;31m'
25
- GREEN='\033[0;32m'
26
- BLUE='\033[0;34m'
27
- NC='\033[0m'
28
- REPO_KIND_FILE=".sdd-repo-kind"
29
- PROTECTED_PREFIXES=("content/")
30
- # lock_version 3 adds a per-path sha256 to each managed_paths entry
31
- # (`- <digest> <path>`), so a consumer can tell "pristine but older" from
32
- # "locally edited" without needing the previously-applied bundle. Bumped in
33
- # lockstep with sdd-cli: a script still writing version 2 would DOWNGRADE a v3
34
- # lock on its next run and destroy those digests.
35
- LOCK_VERSION=3
36
-
37
- log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
38
- log_fail() { echo -e "${RED}[FAIL]${NC} $1"; }
39
- log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
40
- log_section() {
41
- echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}"
42
- echo -e "${BLUE} $1${NC}"
43
- echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n"
44
- }
45
-
46
- usage() {
47
- cat <<USAGE
48
- Usage: $(basename "$0") --bundle <path.tar.gz> [--target-repo <dir>] [--dry-run] [--allow-overwrite] [--no-delete]
49
-
50
- Options:
51
- --bundle Required bundle archive generated by build-method-baseline-bundle.sh
52
- --target-repo Target repository root (default: current working directory)
53
- --dry-run Report plan only; do not modify files
54
- --allow-overwrite Overwrite conflicting managed files (default: fail on conflict)
55
- --no-delete Do not delete previously managed files removed from the incoming bundle
56
- USAGE
57
- }
58
-
59
- BUNDLE=""
60
- TARGET_REPO="$(pwd)"
61
- DRY_RUN=0
62
- ALLOW_OVERWRITE=0
63
- NO_DELETE=0
64
-
65
- while [[ $# -gt 0 ]]; do
66
- case "$1" in
67
- --bundle)
68
- BUNDLE="${2:-}"
69
- shift 2
70
- ;;
71
- --target-repo)
72
- TARGET_REPO="${2:-}"
73
- shift 2
74
- ;;
75
- --dry-run)
76
- DRY_RUN=1
77
- shift
78
- ;;
79
- --allow-overwrite)
80
- ALLOW_OVERWRITE=1
81
- shift
82
- ;;
83
- --no-delete)
84
- NO_DELETE=1
85
- shift
86
- ;;
87
- -h|--help)
88
- usage
89
- exit 0
90
- ;;
91
- *)
92
- log_fail "Unknown argument: $1"
93
- usage
94
- exit 1
95
- ;;
96
- esac
97
- done
98
-
99
- if [[ -z "$BUNDLE" ]]; then
100
- log_fail "--bundle is required"
101
- usage
102
- exit 1
103
- fi
104
-
105
- if [[ ! -f "$BUNDLE" ]]; then
106
- log_fail "Bundle not found: $BUNDLE"
107
- exit 1
108
- fi
109
-
110
- if [[ ! -d "$TARGET_REPO" ]]; then
111
- log_fail "Target repo not found: $TARGET_REPO"
112
- exit 1
113
- fi
114
-
115
- log_section "Sync Method Baseline"
116
-
117
- TMP_DIR="$(mktemp -d)"
118
- trap 'rm -rf "$TMP_DIR"' EXIT
119
-
120
- tar -xzf "$BUNDLE" -C "$TMP_DIR"
121
- BUNDLE_ROOT="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
122
- if [[ -z "$BUNDLE_ROOT" ]]; then
123
- log_fail "Could not find extracted bundle root"
124
- exit 1
125
- fi
126
-
127
- MANAGED_PATHS_FILE="$BUNDLE_ROOT/MANAGED_PATHS.txt"
128
- PAYLOAD_DIR="$BUNDLE_ROOT/payload"
129
- MANIFEST_FILE="$BUNDLE_ROOT/BUNDLE_MANIFEST.yaml"
130
- LOCK_FILE="$TARGET_REPO/method-baseline.lock"
131
-
132
- if [[ ! -f "$MANAGED_PATHS_FILE" || ! -d "$PAYLOAD_DIR" || ! -f "$MANIFEST_FILE" ]]; then
133
- log_fail "Bundle missing required files (MANAGED_PATHS.txt, payload/, or BUNDLE_MANIFEST.yaml)"
134
- exit 1
135
- fi
136
-
137
- BUNDLE_VERSION="$(awk -F': ' '/^bundle_version:/{gsub(/"/,"",$2); print $2}' "$MANIFEST_FILE")"
138
- SOURCE_COMMIT="$(awk -F': ' '/^source_commit:/{gsub(/"/,"",$2); print $2}' "$MANIFEST_FILE")"
139
- BUNDLE_NAME="$(awk -F': ' '/^bundle_name:/{gsub(/"/,"",$2); print $2}' "$MANIFEST_FILE")"
140
- MIN_SDD_CLI_VERSION="$(awk -F': ' '/^minimum_sdd_cli_version:/{gsub(/"/,"",$2); print $2}' "$MANIFEST_FILE")"
141
-
142
- # If the bundle declares a minimum sdd-cli version, enforce it before
143
- # applying any payload. Relevant since v0.9.0, when catalogue-schema.yaml
144
- # was relocated from forward bundles to sdd-cli — adopters whose installed
145
- # sdd-cli predates the relocation cannot read schema correctly.
146
- if [[ -n "$MIN_SDD_CLI_VERSION" ]]; then
147
- if ! command -v sdd-cli >/dev/null 2>&1; then
148
- log_fail "Bundle $BUNDLE_NAME v$BUNDLE_VERSION requires sdd-cli >= $MIN_SDD_CLI_VERSION"
149
- log_fail "but sdd-cli is not installed."
150
- log_fail "Remediation: npm install -g @sdd-method/sdd-cli@^$MIN_SDD_CLI_VERSION"
151
- exit 1
152
- fi
153
- INSTALLED_SDD_CLI="$(sdd-cli --version 2>/dev/null | awk '{print $NF}' | head -1)"
154
- if [[ -z "$INSTALLED_SDD_CLI" ]]; then
155
- log_fail "Could not determine installed sdd-cli version (sdd-cli --version returned empty)."
156
- exit 1
157
- fi
158
- # Version sort to compare: if min comes before installed (or equals),
159
- # installed satisfies the requirement.
160
- LOWEST="$(printf '%s\n%s\n' "$MIN_SDD_CLI_VERSION" "$INSTALLED_SDD_CLI" | sort -V | head -1)"
161
- if [[ "$LOWEST" != "$MIN_SDD_CLI_VERSION" ]]; then
162
- log_fail "Bundle $BUNDLE_NAME v$BUNDLE_VERSION requires sdd-cli >= $MIN_SDD_CLI_VERSION"
163
- log_fail "but installed sdd-cli is $INSTALLED_SDD_CLI."
164
- log_fail "Remediation: npm install -g @sdd-method/sdd-cli@^$MIN_SDD_CLI_VERSION"
165
- exit 1
166
- fi
167
- log_info "sdd-cli $INSTALLED_SDD_CLI satisfies bundle minimum $MIN_SDD_CLI_VERSION"
168
- fi
169
-
170
- # Bundle classification table.
171
- # IS_FORWARD_BASE=1 → sets .sdd-repo-kind, qualifies as a base for additive layers
172
- # REPO_KIND_VALUE → written to .sdd-repo-kind when IS_FORWARD_BASE=1
173
- case "$BUNDLE_NAME" in
174
- platform-baseline)
175
- REPO_KIND_VALUE="platform-product"
176
- IS_FORWARD_BASE=1
177
- ;;
178
- app-baseline)
179
- REPO_KIND_VALUE="application-product"
180
- IS_FORWARD_BASE=1
181
- ;;
182
- integration-baseline)
183
- REPO_KIND_VALUE="integration-product"
184
- IS_FORWARD_BASE=1
185
- ;;
186
- reverse-extension-baseline)
187
- REPO_KIND_VALUE=""
188
- IS_FORWARD_BASE=0
189
- ;;
190
- *)
191
- log_fail "Unknown bundle_name in manifest: $BUNDLE_NAME"
192
- exit 1
193
- ;;
194
- esac
195
-
196
- mapfile -t INCOMING_PATHS < "$MANAGED_PATHS_FILE"
197
-
198
- for p in "${INCOMING_PATHS[@]}"; do
199
- if [[ ! -f "$PAYLOAD_DIR/$p" ]]; then
200
- log_fail "Missing payload file listed in MANAGED_PATHS.txt: $p"
201
- exit 1
202
- fi
203
- for prefix in "${PROTECTED_PREFIXES[@]}"; do
204
- if [[ "$p" == "$prefix"* ]]; then
205
- log_fail "Incoming bundle attempts to manage protected product-owned path: $p"
206
- log_fail "Method baseline bundles must not manage files under: ${PROTECTED_PREFIXES[*]}"
207
- exit 1
208
- fi
209
- done
210
- done
211
-
212
- # ---------------------------------------------------------------------------
213
- # Parse existing lock file (if any) into a working directory.
214
- #
215
- # $LOCK_WORK/order ordered list of bundle names
216
- # $LOCK_WORK/<name>/meta key=value pairs (bundle metadata)
217
- # $LOCK_WORK/<name>/paths one path per line
218
- #
219
- # Supports lock_version 2 and 3 (multi-bundle) and the legacy single-bundle
220
- # shape (no lock_version, top-level bundle_version, flat managed_paths).
221
- # Legacy locks are migrated by inferring the bundle name from
222
- # .sdd-repo-kind.
223
- # ---------------------------------------------------------------------------
224
-
225
- LOCK_WORK="$TMP_DIR/lock-work"
226
- mkdir -p "$LOCK_WORK"
227
- : > "$LOCK_WORK/order"
228
-
229
- infer_legacy_bundle_name() {
230
- # Map .sdd-repo-kind (or legacy .iteraitive-repo-kind) → bundle name.
231
- local marker="$TARGET_REPO/$REPO_KIND_FILE"
232
- local legacy_marker="$TARGET_REPO/.iteraitive-repo-kind"
233
- local repo_kind=""
234
- if [[ -f "$marker" ]]; then
235
- repo_kind="$(head -n1 "$marker" | tr -d '[:space:]')"
236
- elif [[ -f "$legacy_marker" ]]; then
237
- repo_kind="$(head -n1 "$legacy_marker" | tr -d '[:space:]')"
238
- fi
239
- case "$repo_kind" in
240
- platform-product) echo "platform-baseline" ;;
241
- application-product) echo "app-baseline" ;;
242
- integration-product) echo "integration-baseline" ;;
243
- *) echo "" ;;
244
- esac
245
- }
246
-
247
- write_bundle_dir() {
248
- # write_bundle_dir <name> <version> <source_commit> <sha> <synced_at> <is_forward_base> <paths_file>
249
- local name="$1" version="$2" commit="$3" sha="$4" synced="$5" forward="$6" paths_file="$7"
250
- local dir="$LOCK_WORK/$name"
251
- mkdir -p "$dir"
252
- {
253
- echo "bundle_version=$version"
254
- echo "source_commit=$commit"
255
- echo "bundle_sha256=$sha"
256
- echo "synced_at_utc=$synced"
257
- echo "is_forward_base=$forward"
258
- } > "$dir/meta"
259
- if [[ -n "$paths_file" && -f "$paths_file" ]]; then
260
- cp "$paths_file" "$dir/paths"
261
- else
262
- : > "$dir/paths"
263
- fi
264
- # Record order if not already present.
265
- if ! grep -qFx "$name" "$LOCK_WORK/order"; then
266
- echo "$name" >> "$LOCK_WORK/order"
267
- fi
268
- }
269
-
270
- if [[ -f "$LOCK_FILE" ]]; then
271
- # Detect shape: new locks have a `lock_version:` line near the top.
272
- if grep -q '^lock_version:' "$LOCK_FILE"; then
273
- # New shape — parse bundle blocks.
274
- awk '
275
- function flush_bundle() {
276
- if (cur_name != "") {
277
- print "BUNDLE_END"
278
- }
279
- }
280
- BEGIN { in_bundles = 0; cur_name = ""; in_paths = 0 }
281
- /^bundles:[[:space:]]*$/ { in_bundles = 1; next }
282
- in_bundles && /^[^[:space:]]/ { flush_bundle(); in_bundles = 0; next }
283
- in_bundles && /^ - name:/ {
284
- flush_bundle()
285
- sub(/^ - name:[[:space:]]*/, "", $0)
286
- gsub(/"/, "", $0)
287
- cur_name = $0
288
- in_paths = 0
289
- print "BUNDLE_BEGIN " cur_name
290
- next
291
- }
292
- in_bundles && cur_name != "" && /^ managed_paths:[[:space:]]*$/ {
293
- in_paths = 1
294
- next
295
- }
296
- in_bundles && cur_name != "" && in_paths && /^ - / {
297
- sub(/^ - /, "", $0)
298
- print "PATH " $0
299
- next
300
- }
301
- in_bundles && cur_name != "" && /^ [a-z0-9_]+:/ {
302
- in_paths = 0
303
- line = $0
304
- sub(/^ /, "", line)
305
- # Strip surrounding quotes from the value portion only. Note: the
306
- # value may itself contain colons (e.g. ISO timestamps), so we
307
- # split on the FIRST colon only, not all of them.
308
- n = index(line, ":")
309
- key = substr(line, 1, n - 1)
310
- val = substr(line, n + 1)
311
- sub(/^[[:space:]]+/, "", val)
312
- gsub(/"/, "", val)
313
- print "META " key " " val
314
- next
315
- }
316
- END { flush_bundle() }
317
- ' "$LOCK_FILE" > "$TMP_DIR/lock-parsed.txt"
318
-
319
- cur_name=""
320
- cur_version=""; cur_commit=""; cur_sha=""; cur_synced=""; cur_forward=""
321
- cur_paths_file=""
322
- while IFS= read -r line; do
323
- case "$line" in
324
- BUNDLE_BEGIN\ *)
325
- cur_name="${line#BUNDLE_BEGIN }"
326
- cur_version=""; cur_commit=""; cur_sha=""; cur_synced=""; cur_forward="false"
327
- cur_paths_file="$TMP_DIR/lock-paths-$cur_name"
328
- : > "$cur_paths_file"
329
- ;;
330
- META\ bundle_version\ *) cur_version="${line#META bundle_version }" ;;
331
- META\ source_commit\ *) cur_commit="${line#META source_commit }" ;;
332
- META\ bundle_sha256\ *) cur_sha="${line#META bundle_sha256 }" ;;
333
- META\ synced_at_utc\ *) cur_synced="${line#META synced_at_utc }" ;;
334
- META\ is_forward_base\ *) cur_forward="${line#META is_forward_base }" ;;
335
- PATH\ *) echo "${line#PATH }" >> "$cur_paths_file" ;;
336
- BUNDLE_END)
337
- if [[ -n "$cur_name" ]]; then
338
- write_bundle_dir "$cur_name" "$cur_version" "$cur_commit" "$cur_sha" "$cur_synced" "$cur_forward" "$cur_paths_file"
339
- fi
340
- cur_name=""
341
- ;;
342
- esac
343
- done < "$TMP_DIR/lock-parsed.txt"
344
- else
345
- # Legacy shape — single bundle, top-level metadata.
346
- legacy_name="$(infer_legacy_bundle_name)"
347
- if [[ -z "$legacy_name" ]]; then
348
- log_fail "Legacy lock detected but cannot infer bundle name from $REPO_KIND_FILE."
349
- log_fail "Expected one of: platform-product, application-product, integration-product."
350
- exit 1
351
- fi
352
- legacy_version="$(awk -F': ' '/^bundle_version:/{gsub(/"/,"",$2); print $2}' "$LOCK_FILE")"
353
- legacy_commit="$(awk -F': ' '/^source_commit:/{gsub(/"/,"",$2); print $2}' "$LOCK_FILE")"
354
- legacy_sha="$(awk -F': ' '/^bundle_sha256:/{gsub(/"/,"",$2); print $2}' "$LOCK_FILE")"
355
- legacy_synced="$(awk -F': ' '/^synced_at_utc:/{gsub(/"/,"",$2); print $2}' "$LOCK_FILE")"
356
- legacy_paths="$TMP_DIR/legacy-paths"
357
- awk '
358
- /^managed_paths:/ {in_list=1; next}
359
- in_list && /^ - / {sub(/^ - /, ""); print; next}
360
- in_list && !/^ - / {in_list=0}
361
- ' "$LOCK_FILE" > "$legacy_paths"
362
- write_bundle_dir "$legacy_name" "$legacy_version" "$legacy_commit" "$legacy_sha" "$legacy_synced" "true" "$legacy_paths"
363
- log_info "Migrated legacy single-bundle lock → multi-bundle (lock_version: $LOCK_VERSION). Inferred bundle: $legacy_name"
364
- fi
365
- fi
366
-
367
- # ---------------------------------------------------------------------------
368
- # Validate protected prefixes against ALL previously-locked paths.
369
- # ---------------------------------------------------------------------------
370
- if [[ -d "$LOCK_WORK" && -s "$LOCK_WORK/order" ]]; then
371
- while IFS= read -r b; do
372
- if [[ -f "$LOCK_WORK/$b/paths" ]]; then
373
- while IFS= read -r p; do
374
- for prefix in "${PROTECTED_PREFIXES[@]}"; do
375
- if [[ "$p" == "$prefix"* ]]; then
376
- log_fail "Existing lock contains protected product-owned path: $p (bundle: $b)"
377
- log_fail "Resolve this manually before refreshing the method baseline."
378
- exit 1
379
- fi
380
- done
381
- done < "$LOCK_WORK/$b/paths"
382
- fi
383
- done < "$LOCK_WORK/order"
384
- fi
385
-
386
- # ---------------------------------------------------------------------------
387
- # requires_forward_base check for additive bundles.
388
- # An additive bundle (e.g. reverse-extension-baseline) requires that the
389
- # target repo already have at least one forward-base bundle in its lock.
390
- # ---------------------------------------------------------------------------
391
- if [[ "$IS_FORWARD_BASE" -eq 0 ]]; then
392
- has_forward_base=0
393
- if [[ -s "$LOCK_WORK/order" ]]; then
394
- while IFS= read -r b; do
395
- if [[ "$b" == "$BUNDLE_NAME" ]]; then continue; fi
396
- if [[ -f "$LOCK_WORK/$b/meta" ]] && grep -q '^is_forward_base=true$' "$LOCK_WORK/$b/meta"; then
397
- has_forward_base=1
398
- break
399
- fi
400
- done < "$LOCK_WORK/order"
401
- fi
402
- if [[ "$has_forward_base" -eq 0 ]]; then
403
- log_fail "$BUNDLE_NAME is an additive extension. Sync a forward base first."
404
- log_fail "Forward bases: platform-baseline, app-baseline, integration-baseline."
405
- exit 1
406
- fi
407
- fi
408
-
409
- # ---------------------------------------------------------------------------
410
- # ADR transitions (rename / withdrawal / reassignment).
411
- #
412
- # Compare the bundle's ADR_TRANSITIONS.yaml against the previously-applied
413
- # bundle_version recorded in the lock for THIS bundle. Apply transitions
414
- # whose introduced_in_version is greater than the previous version.
415
- # ---------------------------------------------------------------------------
416
- ADR_TRANSITIONS_FILE="$BUNDLE_ROOT/ADR_TRANSITIONS.yaml"
417
- RENAMES_TO_APPLY=()
418
- TRANSITION_WARNINGS=()
419
-
420
- if [[ -f "$ADR_TRANSITIONS_FILE" ]]; then
421
- if ! command -v yq >/dev/null 2>&1; then
422
- log_info "yq not available; skipping ADR transitions (install mikefarah/yq to enable)"
423
- else
424
- PREV_VERSION=""
425
- if [[ -f "$LOCK_WORK/$BUNDLE_NAME/meta" ]]; then
426
- PREV_VERSION="$(awk -F= '/^bundle_version=/{print $2}' "$LOCK_WORK/$BUNDLE_NAME/meta")"
427
- fi
428
-
429
- semver_lt() {
430
- # semver_lt A B → exit 0 if A < B. Normalise a leading 'v': the lock
431
- # records bundle_version as "v1.2.0" but ADR_TRANSITIONS.yaml records
432
- # introduced_in_version as "1.3.0"; without this, sort -V mis-orders the
433
- # v-prefixed operand and every rename transition is silently skipped.
434
- local a="${1#v}" b="${2#v}"
435
- [[ "$a" == "$b" ]] && return 1
436
- [[ "$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -n1)" == "$a" ]]
437
- }
438
-
439
- TRANSITION_COUNT="$(yq e '.transitions | length' "$ADR_TRANSITIONS_FILE")"
440
- for ((i=0; i<TRANSITION_COUNT; i++)); do
441
- kind="$(yq e ".transitions[$i].kind" "$ADR_TRANSITIONS_FILE")"
442
- adr_id="$(yq e ".transitions[$i].adr_id" "$ADR_TRANSITIONS_FILE")"
443
- introduced="$(yq e ".transitions[$i].introduced_in_version" "$ADR_TRANSITIONS_FILE")"
444
-
445
- # Only apply transitions newer than the previously synced version.
446
- if [[ -n "$PREV_VERSION" ]] && ! semver_lt "$PREV_VERSION" "$introduced"; then
447
- continue
448
- fi
449
-
450
- case "$kind" in
451
- rename)
452
- from_path="$(yq e ".transitions[$i].from_path" "$ADR_TRANSITIONS_FILE")"
453
- to_path="$(yq e ".transitions[$i].to_path" "$ADR_TRANSITIONS_FILE")"
454
- if [[ -f "$TARGET_REPO/$from_path" && -f "$TARGET_REPO/$to_path" ]]; then
455
- TRANSITION_WARNINGS+=("[ADR $adr_id rename] both paths exist in adopter repo; manual review needed: $from_path → $to_path")
456
- elif [[ -f "$TARGET_REPO/$from_path" ]]; then
457
- RENAMES_TO_APPLY+=("$from_path|$to_path|$adr_id")
458
- fi
459
- ;;
460
- withdrawal)
461
- withdrawn_path="$(yq e ".transitions[$i].withdrawn_path" "$ADR_TRANSITIONS_FILE")"
462
- adopter_action="$(yq e ".transitions[$i].adopter_action" "$ADR_TRANSITIONS_FILE")"
463
- if [[ -f "$TARGET_REPO/$withdrawn_path" ]]; then
464
- TRANSITION_WARNINGS+=("[ADR $adr_id withdrawal] $withdrawn_path is still present. $adopter_action")
465
- fi
466
- ;;
467
- reassignment)
468
- new_decision="$(yq e ".transitions[$i].new_decision_path" "$ADR_TRANSITIONS_FILE")"
469
- TRANSITION_WARNINGS+=("[ADR $adr_id reassignment] slot now points to a different decision: $new_decision")
470
- ;;
471
- esac
472
- done
473
- fi
474
- fi
475
-
476
- # ---------------------------------------------------------------------------
477
- # Conflicts: incoming files that exist in the target with different content.
478
- # ---------------------------------------------------------------------------
479
- CONFLICTS=()
480
- for p in "${INCOMING_PATHS[@]}"; do
481
- target_file="$TARGET_REPO/$p"
482
- incoming_file="$PAYLOAD_DIR/$p"
483
- if [[ -f "$target_file" ]] && ! cmp -s "$incoming_file" "$target_file"; then
484
- CONFLICTS+=("$p")
485
- fi
486
- done
487
-
488
- if [[ ${#CONFLICTS[@]} -gt 0 && "$ALLOW_OVERWRITE" -ne 1 ]]; then
489
- log_fail "Found ${#CONFLICTS[@]} conflicting managed file(s). Re-run with --allow-overwrite to apply."
490
- printf '%s\n' "${CONFLICTS[@]}" | sed 's/^/ - /'
491
- exit 1
492
- fi
493
-
494
- # Cross-bundle ownership conflict: a path managed by another bundle in the
495
- # lock cannot be claimed by this bundle. Sync must remain federated.
496
- if [[ -s "$LOCK_WORK/order" ]]; then
497
- while IFS= read -r b; do
498
- if [[ "$b" == "$BUNDLE_NAME" ]]; then continue; fi
499
- if [[ -f "$LOCK_WORK/$b/paths" ]]; then
500
- for p in "${INCOMING_PATHS[@]}"; do
501
- if grep -qFx "$p" "$LOCK_WORK/$b/paths"; then
502
- log_fail "Path '$p' is already managed by bundle '$b'; cannot also be claimed by '$BUNDLE_NAME'."
503
- log_fail "Method baselines must not overlap. Fix upstream and rebuild."
504
- exit 1
505
- fi
506
- done
507
- fi
508
- done < "$LOCK_WORK/order"
509
- fi
510
-
511
- # ---------------------------------------------------------------------------
512
- # Removals: paths previously managed by THIS bundle that are no longer in
513
- # the incoming list.
514
- # ---------------------------------------------------------------------------
515
- REMOVE_PATHS=()
516
- PREV_BUNDLE_PATHS_FILE=""
517
- if [[ -f "$LOCK_WORK/$BUNDLE_NAME/paths" ]]; then
518
- PREV_BUNDLE_PATHS_FILE="$LOCK_WORK/$BUNDLE_NAME/paths"
519
- fi
520
-
521
- if [[ "$NO_DELETE" -ne 1 && -n "$PREV_BUNDLE_PATHS_FILE" && -s "$PREV_BUNDLE_PATHS_FILE" ]]; then
522
- while IFS= read -r prev; do
523
- keep=0
524
- for cur in "${INCOMING_PATHS[@]}"; do
525
- if [[ "$prev" == "$cur" ]]; then
526
- keep=1
527
- break
528
- fi
529
- done
530
- if [[ "$keep" -eq 0 ]]; then
531
- REMOVE_PATHS+=("$prev")
532
- fi
533
- done < "$PREV_BUNDLE_PATHS_FILE"
534
- fi
535
-
536
- log_info "Bundle: $BUNDLE_NAME (version: $BUNDLE_VERSION)"
537
- log_info "Incoming managed files: ${#INCOMING_PATHS[@]}"
538
- log_info "Conflicts: ${#CONFLICTS[@]}"
539
- log_info "Removals from previous sync of this bundle: ${#REMOVE_PATHS[@]}"
540
- log_info "ADR renames to apply: ${#RENAMES_TO_APPLY[@]}"
541
- log_info "ADR transition warnings: ${#TRANSITION_WARNINGS[@]}"
542
- log_info "Protected ownership prefixes: ${PROTECTED_PREFIXES[*]}"
543
-
544
- if [[ ${#TRANSITION_WARNINGS[@]} -gt 0 ]]; then
545
- log_info "ADR transition notices:"
546
- printf '%s\n' "${TRANSITION_WARNINGS[@]}" | sed 's/^/ ! /'
547
- fi
548
-
549
- if [[ "$DRY_RUN" -eq 1 ]]; then
550
- log_info "Dry run enabled. No files modified."
551
- if [[ ${#CONFLICTS[@]} -gt 0 ]]; then
552
- log_info "Conflicting files:"
553
- printf '%s\n' "${CONFLICTS[@]}" | sed 's/^/ - /'
554
- fi
555
- if [[ ${#REMOVE_PATHS[@]} -gt 0 ]]; then
556
- log_info "Files that would be removed:"
557
- printf '%s\n' "${REMOVE_PATHS[@]}" | sed 's/^/ - /'
558
- fi
559
- if [[ ${#RENAMES_TO_APPLY[@]} -gt 0 ]]; then
560
- log_info "ADR renames that would be applied:"
561
- for r in "${RENAMES_TO_APPLY[@]}"; do
562
- from="${r%%|*}"; rest="${r#*|}"; to="${rest%%|*}"; aid="${rest##*|}"
563
- echo " - [ADR $aid] $from → $to"
564
- done
565
- fi
566
- exit 0
567
- fi
568
-
569
- # ---------------------------------------------------------------------------
570
- # Apply ADR renames BEFORE the file copy phase so the new path doesn't
571
- # collide with the old one when both are tracked in git.
572
- # ---------------------------------------------------------------------------
573
- for r in "${RENAMES_TO_APPLY[@]}"; do
574
- from="${r%%|*}"; rest="${r#*|}"; to="${rest%%|*}"; aid="${rest##*|}"
575
- mkdir -p "$TARGET_REPO/$(dirname "$to")"
576
- if [[ -d "$TARGET_REPO/.git" ]] && git -C "$TARGET_REPO" ls-files --error-unmatch "$from" >/dev/null 2>&1; then
577
- git -C "$TARGET_REPO" mv "$from" "$to"
578
- log_info "Renamed (git mv) [ADR $aid]: $from → $to"
579
- else
580
- mv "$TARGET_REPO/$from" "$TARGET_REPO/$to"
581
- log_info "Renamed [ADR $aid]: $from → $to"
582
- fi
583
- done
584
-
585
- # ---------------------------------------------------------------------------
586
- # Apply: copy incoming files, remove stale ones.
587
- # ---------------------------------------------------------------------------
588
- for p in "${INCOMING_PATHS[@]}"; do
589
- mkdir -p "$TARGET_REPO/$(dirname "$p")"
590
- cp "$PAYLOAD_DIR/$p" "$TARGET_REPO/$p"
591
- done
592
-
593
- for p in "${REMOVE_PATHS[@]}"; do
594
- rm -f "$TARGET_REPO/$p"
595
- done
596
-
597
- # Forward bases write the repo-kind marker; additive layers don't touch it.
598
- if [[ "$IS_FORWARD_BASE" -eq 1 ]]; then
599
- printf '%s\n' "$REPO_KIND_VALUE" > "$TARGET_REPO/$REPO_KIND_FILE"
600
- fi
601
-
602
- # ---------------------------------------------------------------------------
603
- # Update lock work-dir for this bundle, then emit merged lock file.
604
- # ---------------------------------------------------------------------------
605
- SYNCED_AT_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
606
- BUNDLE_SHA="$(sha256sum "$BUNDLE" | awk '{print $1}')"
607
- INCOMING_PATHS_FILE="$TMP_DIR/incoming-paths.txt"
608
- printf '%s\n' "${INCOMING_PATHS[@]}" > "$INCOMING_PATHS_FILE"
609
- if [[ "$IS_FORWARD_BASE" -eq 1 ]]; then
610
- forward_str="true"
611
- else
612
- forward_str="false"
613
- fi
614
- write_bundle_dir "$BUNDLE_NAME" "$BUNDLE_VERSION" "$SOURCE_COMMIT" "$BUNDLE_SHA" "$SYNCED_AT_UTC" "$forward_str" "$INCOMING_PATHS_FILE"
615
-
616
- {
617
- echo "method_repo: sdd-method"
618
- echo "lock_version: $LOCK_VERSION"
619
- echo "bundles:"
620
- while IFS= read -r b; do
621
- [[ -z "$b" ]] && continue
622
- meta="$LOCK_WORK/$b/meta"
623
- paths="$LOCK_WORK/$b/paths"
624
- bv="$(grep '^bundle_version=' "$meta" | cut -d= -f2-)"
625
- sc="$(grep '^source_commit=' "$meta" | cut -d= -f2-)"
626
- bsh="$(grep '^bundle_sha256=' "$meta" | cut -d= -f2-)"
627
- sa="$(grep '^synced_at_utc=' "$meta" | cut -d= -f2-)"
628
- fb="$(grep '^is_forward_base=' "$meta" | cut -d= -f2-)"
629
- echo " - name: $b"
630
- echo " bundle_version: \"$bv\""
631
- echo " source_commit: \"$sc\""
632
- echo " bundle_sha256: \"$bsh\""
633
- echo " synced_at_utc: \"$sa\""
634
- echo " is_forward_base: $fb"
635
- echo " managed_paths:"
636
- if [[ -s "$paths" ]]; then
637
- while IFS= read -r p; do
638
- # lock_version 3: entries are `<sha256> <path>`. For the bundle being
639
- # synced the digest is computed from the PAYLOAD — what this run applies.
640
- # Entries for OTHER bundles were parsed out of the existing lock as opaque
641
- # strings, so any digest they already carry round-trips untouched; they are
642
- # re-emitted verbatim rather than re-hashed against a payload this run does
643
- # not have.
644
- if [[ "$b" == "$BUNDLE_NAME" && "$p" != *" "* && -f "$PAYLOAD_DIR/$p" ]]; then
645
- echo " - $(sha256sum "$PAYLOAD_DIR/$p" | awk '{print $1}') $p"
646
- else
647
- echo " - $p"
648
- fi
649
- done < "$paths"
650
- fi
651
- done < "$LOCK_WORK/order"
652
- } > "$LOCK_FILE"
653
-
654
- log_pass "Method baseline synced to: $TARGET_REPO"
655
- log_info "Updated lock file: method-baseline.lock (lock_version: $LOCK_VERSION)"
656
- if [[ "$IS_FORWARD_BASE" -eq 1 ]]; then
657
- log_info "Set repo kind marker: $REPO_KIND_FILE = $REPO_KIND_VALUE"
658
- else
659
- log_info "Additive layer applied; .sdd-repo-kind unchanged."
660
- fi