diff --git a/docs/marketplace-polyrepo-migration/migration/00-preflight.sh b/docs/marketplace-polyrepo-migration/migration/00-preflight.sh index 77659a0..2162f7f 100644 --- a/docs/marketplace-polyrepo-migration/migration/00-preflight.sh +++ b/docs/marketplace-polyrepo-migration/migration/00-preflight.sh @@ -42,6 +42,21 @@ python3 -c "import json; json.load(open('$MAP'))" >/dev/null 2>&1 || fail "plugi TARGET_COUNT="$(python3 -c "import json; print(len(json.load(open('$MAP'))['targets']))")" [ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets in plugin-map.json, found $TARGET_COUNT" +# --- path hygiene (aeb6292): every target path must be whitespace- and glob-free, so the space-joined +# `for p in $(mappaths …)` word-splitting in 99-dryrun.sh (live_files / SC6 baseline) is sound. --- +python3 - "$MAP" <<'PY' || fail "plugin-map.json has a path with whitespace or a glob metacharacter (breaks word-split path handling in 99-dryrun.sh)" +import json, re, sys +m = json.load(open(sys.argv[1])) +bad = [] +for k, t in m["targets"].items(): + for p in t.get("paths", []): + if re.search(r"\s", p) or any(c in p for c in "*?[]"): + bad.append("%s: %r" % (k, p)) +if bad: + sys.stderr.write("offending paths:\n " + "\n ".join(bad) + "\n") + sys.exit(1) +PY + # --- repo baseline (HEAD descends from the ratified brief commit, on main, not behind remote) --- BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" [ "$BRANCH" = "main" ] || fail "not on main (HEAD on '$BRANCH')" diff --git a/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs b/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs index 651e31d..bf39a02 100644 --- a/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs +++ b/docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs @@ -11,13 +11,18 @@ const map = JSON.parse(readFileSync(join(here, 'plugin-map.json'), 'utf8')); const targets = map.targets; const keys = Object.keys(targets); -const RENAMED = ['voyage', 'llm-security', 'linkedin-studio']; +// voyage ← ultraplan-local and linkedin-studio ← linkedin-thought-leadership are TRUE renames, so each +// carries >=2 --path entries (F1). llm-security is NOT a rename: llm-security-copilot was a SEPARATE, +// COEXISTING plugin, so llm-security is SINGLE-path (corrected 2026-06-17, commit 836b8e9) — a dual --path +// there collided at the coexistence commits and dropped 87 files. The map is authoritative; this suite +// tracks it. +const RENAMED = ['voyage', 'linkedin-studio']; test('plugin-map.json parses and has exactly 11 targets (10 plugins + DS)', () => { assert.equal(keys.length, 11, `expected 11 targets, found ${keys.length}: ${keys.join(', ')}`); }); -test('the 3 renamed plugins each carry >=2 --path entries (F1)', () => { +test('the renamed plugins (voyage, linkedin-studio) each carry >=2 --path entries (F1)', () => { for (const k of RENAMED) { assert.ok(targets[k], `missing renamed target ${k}`); assert.ok( @@ -27,6 +32,15 @@ test('the 3 renamed plugins each carry >=2 --path entries (F1)', () => { } }); +test('llm-security is SINGLE-path — NOT a rename (copilot was a coexisting plugin, 836b8e9)', () => { + // Locks in the 87-file-drop correction: re-introducing a 2nd --path here would re-collide at the + // coexistence commits and silently drop files again. + assert.equal( + targets['llm-security'].paths.length, 1, + `llm-security must stay single-path (dual-path dropped 87 files), has ${targets['llm-security'].paths.length}` + ); +}); + test('every target repo_url is https:// (not ssh://) — F2 / #9740', () => { for (const k of keys) { assert.match(targets[k].repo_url, /^https:\/\/git\.fromaitochitta\.com\/open\//, `${k} repo_url not the expected HTTPS form`); diff --git a/docs/marketplace-polyrepo-migration/migration/10-extract.sh b/docs/marketplace-polyrepo-migration/migration/10-extract.sh index 0c1ba86..e447e03 100644 --- a/docs/marketplace-polyrepo-migration/migration/10-extract.sh +++ b/docs/marketplace-polyrepo-migration/migration/10-extract.sh @@ -62,6 +62,12 @@ rm -f "$ARGS_FILE" TAG="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$KEY']['tag'])")" +# Re-assert the critical extension here too: the self-heal above runs preflight ONLY on a missing mirror, +# so a present mirror + since-uninstalled git-filter-repo would otherwise die with a raw git error instead +# of this actionable message (9e588ca — RUNBOOK lists it under Preconditions). +git filter-repo --version >/dev/null 2>&1 \ + || fail "git filter-repo not available — brew install git-filter-repo (see RUNBOOK Preconditions)" + # Fresh --no-local clone from the mirror, then the single composing filter-repo pass. rm -rf "$DEST" git clone --no-local --quiet "$MIRROR" "$DEST" diff --git a/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh b/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh index 2ba6e90..622f995 100644 --- a/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh +++ b/docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh @@ -78,32 +78,45 @@ validate_target() { fi fi - # --- SC2: run the plugin's declared runner in the clean room --- - local test_cmd advisory + # --- SC2: route to the target's dedicated gate if it declares one, else run the declared runner in the + # clean room. config-audit's full `node --test 'tests/**/*.test.mjs'` FAILs at a fresh-clone path on + # the 6 machine-locked v5.0.0 byte-stability tests — the exact blocker its Step-7 gate resolves — so + # `--all` MUST delegate to that gate (mirroring 99-dryrun.sh), not run the raw test_cmd. --- + local test_cmd advisory sc2_gate test_cmd="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd',''))")" advisory="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('test_cmd_advisory',''))")" - - # The generic structure validator (Step 7) is referenced by test-less plugins — vendor it into the clean room. - case "$test_cmd" in - *validate-plugin.generic.sh*) - if [ -f "$GENERIC_VALIDATOR" ]; then cp "$GENERIC_VALIDATOR" "$cr/validate-plugin.generic.sh"; fi - ;; - esac + sc2_gate="$(python3 -c "import json; print(json.load(open('$MAP'))['targets']['$key'].get('sc2_gate',''))")" local sc2_label="standalone-safe" - local out status tests - out="$(cd "$cr" && eval "$test_cmd" 2>&1)"; status=$? - if [ $status -ne 0 ]; then - problems="$problems; SC2 runner failed (exit $status)" + + if [ -n "$sc2_gate" ]; then + if WORK="$WORK" bash "$SCRIPT_DIR/$sc2_gate" >/dev/null 2>&1; then + sc2_label="$sc2_gate, standalone-safe" + else + problems="$problems; SC2 gate failed ($sc2_gate)" + fi + else + # The generic structure validator (Step 7) is referenced by test-less plugins — vendor it into the clean room. + case "$test_cmd" in + *validate-plugin.generic.sh*) + if [ -f "$GENERIC_VALIDATOR" ]; then cp "$GENERIC_VALIDATOR" "$cr/validate-plugin.generic.sh"; fi + ;; + esac + + local out status tests + out="$(cd "$cr" && eval "$test_cmd" 2>&1)"; status=$? + if [ $status -ne 0 ]; then + problems="$problems; SC2 runner failed (exit $status)" + fi + case "$test_cmd" in + *node\ --test*) + # Node 25's default reporter prints "ℹ tests N"; older/TAP prints "# tests N". Match either. + tests="$(printf '%s\n' "$out" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)" + [ -n "$tests" ] && sc2_label="$tests tests, standalone-safe" + ;; + *validate-plugin*) sc2_label="structure, standalone-safe" ;; + esac fi - case "$test_cmd" in - *node\ --test*) - # Node 25's default reporter prints "ℹ tests N"; older/TAP prints "# tests N". Match either. - tests="$(printf '%s\n' "$out" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)" - [ -n "$tests" ] && sc2_label="$tests tests, standalone-safe" - ;; - *validate-plugin*) sc2_label="structure, standalone-safe" ;; - esac if [ -n "$problems" ]; then echo "$key: FAIL${problems}" diff --git a/docs/marketplace-polyrepo-migration/migration/99-dryrun.sh b/docs/marketplace-polyrepo-migration/migration/99-dryrun.sh index d69b27d..30ba49b 100644 --- a/docs/marketplace-polyrepo-migration/migration/99-dryrun.sh +++ b/docs/marketplace-polyrepo-migration/migration/99-dryrun.sh @@ -49,6 +49,7 @@ mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$1' mappaths() { python3 -c "import json;print(' '.join(json.load(open('$MAP'))['targets']['$1']['paths']))"; } # Live monorepo git-tracked file count across a target's source paths (SC6 baseline). +# Paths are asserted whitespace- and glob-free by 00-preflight.sh (aeb6292), so the word-split below is sound. live_files() { local key="$1" total=0 p n for p in $(mappaths "$key"); do @@ -110,7 +111,11 @@ for key in $KEYS; do commits=0; tag="(none)"; ext_files=0; sc7="clean"; sc6="?" if [ -d "$dest/.git" ]; then commits="$(git -C "$dest" rev-list --count HEAD 2>/dev/null || echo 0)" - tag="$(git -C "$dest" tag 2>/dev/null | head -1)"; tag="${tag:-(none)}" + # F5 invariant: 10-extract.sh strips all carried-over tags and re-creates EXACTLY one. Assert that — + # a partial tag-strip leaves multiple tags, and `head -1` would silently report the lexicographically + # -first wrong one (e.g. v1.0.0 instead of v5.1.1) while the dry-run still passed (1708e90). + tagn="$(git -C "$dest" tag 2>/dev/null | wc -l | tr -d '[:space:]')" + if [ "$tagn" = "1" ]; then tag="$(git -C "$dest" tag 2>/dev/null)"; else tag="(tags=$tagn!)"; ok=0; fi ext_files="$(git -C "$dest" ls-files | wc -l | tr -d '[:space:]')" leak="$(git -C "$dest" ls-files | grep -E 'STATE\.md|\.local\.md$' \ | grep -v 'templates/okr\.local\.md\.template' || true)" @@ -119,14 +124,16 @@ for key in $KEYS; do ok=0 fi - # SC6 content retention. Intentional blob-strip targets may legitimately shed >1MB blobs, so a drop - # there is reported, not failed; for every other target a drop is a hard FAIL (the llm-security class). + # SC6 content retention — delegated to sc6-check.sh (the DROP detector, unit-tested in sc-checks.test.mjs). + # Only meaningful when the extract succeeded: a failed extract leaves ext_files=0, which must NOT be + # mislabelled as a content DROP (8d649e9) — emit (extract failed) instead. An intentional >1MB blob-strip + # target may legitimately shed blobs (reported, not failed); a shortfall elsewhere is a hard DROP (ok=0). lf="$(live_files "$key")" blob="$(mapget "$key" blob_strip)" - if [ "$ext_files" -lt "$lf" ]; then - if [ "$blob" = "True" ]; then sc6="${ext_files}/${lf} (blob-strip)"; else sc6="${ext_files}/${lf} DROP"; ok=0; fi + if [ -d "$dest/.git" ]; then + if sc6="$(bash "$SCRIPT_DIR/sc6-check.sh" "$ext_files" "$lf" "$blob")"; then :; else ok=0; fi else - sc6="${ext_files}/${lf}" + sc6="(extract failed)" fi # --- SC2 (regression-relative) --- @@ -140,15 +147,27 @@ for key in $KEYS; do else case "$test_cmd" in *node\ --test*) - # Regression-relative: standalone failing set must be a subset of the in-repo failing set. - sf="$(mktemp)"; bf="$(mktemp)" - capture_fails "/tmp/claude-$key" "$test_cmd" > "$sf" 2>/dev/null - capture_fails "$REPO_ROOT/plugins/$key" "$test_cmd" > "$bf" 2>/dev/null - regr="$(comm -23 "$sf" "$bf")" - pre="$(wc -l < "$bf" | tr -d '[:space:]')" - rm -f "$sf" "$bf" - if [ -z "$regr" ]; then sc2="PASS (${pre} pre-existing)"; else - sc2="FAIL (regression: $(printf '%s' "$regr" | grep -c .))"; ok=0; fi + # Regression-relative: the standalone failing set must be a SUBSET of the in-repo failing set. + # Capture the standalone set from the dry-run's OWN prepped extract ($dest = $WORK/$key), NOT the + # 40-validate side-effect clean room /tmp/claude-$key (4e494c8 — that implicit coupling masked a + # real regression when the dir was absent). Guard mktemp: an empty capture is an ERROR, never a + # false zero-regression PASS (4044c49). The subset decision is delegated to sc2-regression.sh. + if sf="$(mktemp)" && bf="$(mktemp)"; then + capture_fails "$dest" "$test_cmd" > "$sf" 2>/dev/null + capture_fails "$REPO_ROOT/plugins/$key" "$test_cmd" > "$bf" 2>/dev/null + pre="$(wc -l < "$bf" | tr -d '[:space:]')" + if regr="$(bash "$SCRIPT_DIR/sc2-regression.sh" "$sf" "$bf")"; then + sc2="PASS (${pre} pre-existing)" + else + rc=$? + if [ "$rc" -ge 2 ]; then sc2="FAIL (sc2-regression error)"; else + sc2="FAIL (regression: $(printf '%s' "$regr" | grep -c .))"; fi + ok=0 + fi + rm -f "$sf" "$bf" + else + sc2="FAIL (mktemp)"; ok=0; rm -f "$sf" "$bf" + fi ;; *) sc2="FAIL (structure)"; ok=0 ;; # deterministic structure check genuinely failed esac diff --git a/docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs b/docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs index c62a459..b84943f 100644 --- a/docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs +++ b/docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs @@ -70,6 +70,10 @@ test('llm-security retains all content (SC6 — dual-rename file-drop defect is assert.ok(/\bPASS\b/.test(line) && !/FAIL/.test(line), `llm-security must PASS standalone: ${line}`); }); +// This asserts the HAPPY path (the live report shows no DROP / no regression). The NEGATIVE coverage — +// proving the SC6 DROP and SC2 regression-FAIL detectors actually FIRE — lives in sc-checks.test.mjs, +// which drives sc6-check.sh / sc2-regression.sh (the extracted detectors this dry-run calls) directly, +// because force-fresh re-extraction would undo any file-drop planted into $WORK/ here (5d112cb). test('no SC6 file-drop and no SC2 regression on any target', { timeout: LONG }, () => { const r = report(); assert.ok(!/\bDROP\b/.test(r), 'no target may drop content (SC6)'); diff --git a/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md b/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md index f65b5ba..b4346c5 100644 --- a/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md +++ b/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md @@ -6,6 +6,9 @@ plus the Step 11 dry-run) was authored and verified locally with **NULL push (D8 ## Preconditions +- **`git-filter-repo`** (a non-default git extension) and **`python3 >= 3.6`** on `PATH` — the extraction + (`10-extract.sh`) and every map-driven script depend on them. `00-preflight.sh` and `10-extract.sh` + both assert them, but install first: `brew install git-filter-repo python3`. - All migration scripts committed locally; the Step 11 dry-run is green (it exercises every script below). - `$FORGEJO_TOKEN` is exported (macOS Keychain → `~/.zshenv`) with `write:org` + `repo` scope. - The `pre-polyrepo-archive` tag exists locally (D2) — the rollback anchor for the whole operation. diff --git a/docs/marketplace-polyrepo-migration/migration/plugin-map.json b/docs/marketplace-polyrepo-migration/migration/plugin-map.json index 4ab5b4c..ab38437 100644 --- a/docs/marketplace-polyrepo-migration/migration/plugin-map.json +++ b/docs/marketplace-polyrepo-migration/migration/plugin-map.json @@ -93,7 +93,7 @@ "repo_url": "https://git.fromaitochitta.com/open/ms-ai-architect.git", "has_vendor_ds": true, "blob_strip": true, - "blob_strip_safe": true, + "blob_strip_safe": null, "test_cmd": "bash tests/validate-plugin.sh", "standalone_caveat": "148 MB screenshot blob-bomb (F3): single filter-repo pass strips >1MB blobs when blob_strip_safe (all under playground/screenshots/), else surgical --path-glob; vendors the design-system" }, diff --git a/docs/marketplace-polyrepo-migration/migration/sc-checks.test.mjs b/docs/marketplace-polyrepo-migration/migration/sc-checks.test.mjs new file mode 100644 index 0000000..f2a62d3 --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/sc-checks.test.mjs @@ -0,0 +1,92 @@ +// Negative + positive coverage for the two highest-stakes dry-run detectors (5d112cb). +// +// 99-dryrun.sh force-fresh re-extracts all 11 targets every run (10-extract.sh wipes $dest first), so a +// "plant a dropped file in $WORK/" negative test against the full dry-run would be silently undone by +// the re-extract. So the two load-bearing detectors are extracted into standalone, side-effect-free scripts +// — sc6-check.sh (the SC6 DROP detector, the guard against the llm-security 87-file-drop class) and +// sc2-regression.sh (the SC2 regression-FAIL detector) — which 99-dryrun.sh now calls. This suite drives +// those exact scripts directly, proving each detector FIRES (DROP / regression → label + non-zero exit) and +// does NOT false-positive (retained content / subset failures → exit 0). When the dry-run calls them and +// they exit non-zero, it sets ok=0 → exit 1, so the dry-run's "report DROP/regression + exit non-zero" +// behaviour follows by construction from this coverage. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const SC6 = path.join(here, 'sc6-check.sh'); +const SC2 = path.join(here, 'sc2-regression.sh'); + +function run(script, args) { + return spawnSync('bash', [script, ...args], { encoding: 'utf8' }); +} + +// --- sc6-check.sh — the SC6 DROP detector --------------------------------------------------------------- + +test('SC6 DROP fires: a real shortfall (llm-security 87-file class) → DROP label + non-zero exit', () => { + const r = run(SC6, ['334', '421', 'false']); // the actual dual-rename defect: 334 of 421 files retained + assert.notEqual(r.status, 0, 'a non-blob-strip shortfall must exit non-zero (the dry-run sets ok=0)'); + assert.match(r.stdout, /334\/421 DROP/, `DROP label expected: ${r.stdout}`); +}); + +test('SC6 blob-strip shortfall is allowed: intentional >1MB strip → (blob-strip) label + exit 0', () => { + const r = run(SC6, ['100', '200', 'True']); // ms-ai-architect screenshot blob-bomb: legitimate shortfall + assert.equal(r.status, 0, `blob-strip shortfall must exit 0: ${r.stderr}`); + assert.match(r.stdout, /100\/200 \(blob-strip\)/, `blob-strip label expected: ${r.stdout}`); + assert.ok(!/DROP/.test(r.stdout), 'a blob-strip shortfall must NOT be labelled DROP'); +}); + +test('SC6 retained content: ext == live → no DROP, exit 0', () => { + const r = run(SC6, ['421', '421', 'false']); + assert.equal(r.status, 0, `full retention must exit 0: ${r.stderr}`); + assert.equal(r.stdout.trim(), '421/421'); +}); + +test('SC6 more files than live (e.g. path-rename overlap) → no DROP, exit 0', () => { + const r = run(SC6, ['500', '421', 'false']); + assert.equal(r.status, 0); + assert.ok(!/DROP/.test(r.stdout), `${r.stdout}`); +}); + +// --- sc2-regression.sh — the SC2 regression-FAIL detector ----------------------------------------------- + +function fixtureFiles(standalone, baseline) { + const dir = mkdtempSync(path.join(os.tmpdir(), 'sc2-')); + const sf = path.join(dir, 'sf'); + const bf = path.join(dir, 'bf'); + writeFileSync(sf, standalone.length ? standalone.join('\n') + '\n' : ''); + writeFileSync(bf, baseline.length ? baseline.join('\n') + '\n' : ''); + return { dir, sf, bf }; +} + +test('SC2 regression fires: a standalone-only failure (not in baseline) → printed + non-zero exit', () => { + // The extraction introduced a NEW failure absent from the in-repo baseline — a real regression. + const { dir, sf, bf } = fixtureFiles(['extract regressed test', 'shared pre-existing fail'], ['shared pre-existing fail']); + try { + const r = run(SC2, [sf, bf]); + assert.equal(r.status, 1, `a regression must exit 1 (the dry-run sets ok=0): ${r.stdout}${r.stderr}`); + assert.match(r.stdout, /extract regressed test/, `the regressing test name must be reported: ${r.stdout}`); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('SC2 subset PASS: standalone failures ⊆ baseline (pre-existing red) → exit 0, nothing printed', () => { + const { dir, sf, bf } = fixtureFiles(['shared pre-existing fail'], ['shared pre-existing fail', 'other baseline-only fail']); + try { + const r = run(SC2, [sf, bf]); + assert.equal(r.status, 0, `a subset must exit 0 (no migration regression): ${r.stdout}${r.stderr}`); + assert.equal(r.stdout.trim(), '', 'no regression should be printed for a subset'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test('SC2 missing capture file is an ERROR (exit 2), never a silent zero-regression PASS (4044c49)', () => { + const r = run(SC2, ['/nonexistent/sf', '/nonexistent/bf']); + assert.equal(r.status, 2, `a missing capture file must be a hard error, not a false PASS: ${r.stdout}`); +}); diff --git a/docs/marketplace-polyrepo-migration/migration/sc2-regression.sh b/docs/marketplace-polyrepo-migration/migration/sc2-regression.sh new file mode 100755 index 0000000..aa4e2ad --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/sc2-regression.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# SC2 regression decision — extracted from 99-dryrun.sh so the regression-FAIL detector is independently +# unit-testable (5d112cb). Given two SORTED failing-test-NAME files — the standalone extract's set and the +# in-repo baseline set — it prints the regressions (standalone failures absent from the baseline) and EXITS +# NON-ZERO (1) if any exist. The contract: "standalone failing set ⊆ in-repo failing set" (the extraction +# introduced NO new failure; pre-existing in-repo red is the plugin's own concern, not a migration regr). +# A missing capture file is a hard ERROR (exit 2), never a silent zero-regression PASS (guards 4044c49). +# +# Usage: sc2-regression.sh +set -u +sf="${1:?standalone fails file}" +bf="${2:?baseline fails file}" +[ -f "$sf" ] && [ -f "$bf" ] || { echo "SC2-REGRESSION ERROR: missing capture file ($sf / $bf)" >&2; exit 2; } + +regr="$(comm -23 "$sf" "$bf")" +if [ -n "$regr" ]; then + printf '%s\n' "$regr" + exit 1 +fi +exit 0 diff --git a/docs/marketplace-polyrepo-migration/migration/sc6-check.sh b/docs/marketplace-polyrepo-migration/migration/sc6-check.sh new file mode 100755 index 0000000..7f3ecae --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/sc6-check.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# SC6 content-retention decision — extracted from 99-dryrun.sh so the DROP detector (the deterministic +# guard against the llm-security 87-file-drop class, commit 836b8e9) is independently unit-testable +# (5d112cb). Given the extract's git-tracked file count, the live monorepo's count for the same paths, and +# whether an intentional >1MB blob-strip applies to this target, it prints the SC6 report-cell label and +# EXITS NON-ZERO on a real DROP — a shortfall NOT explained by a blob-strip. A blob-strip target may +# legitimately shed >1MB blobs, so a shortfall there is reported (exit 0), never failed. +# +# Usage: sc6-check.sh +# blob_strip: True/true → intentional >1MB strip (shortfall allowed); anything else → shortfall = DROP. +set -u +ext="${1:?ext_files}" +live="${2:?live_files}" +blob="${3:-false}" + +if [ "$ext" -lt "$live" ]; then + case "$blob" in + True|true) echo "${ext}/${live} (blob-strip)"; exit 0 ;; + *) echo "${ext}/${live} DROP"; exit 1 ;; + esac +fi +echo "${ext}/${live}" +exit 0