@cairnkeep/cli 2.3.1 → 2.5.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/CHANGELOG.md +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -7,13 +7,22 @@ cd "$ROOT"
|
|
|
7
7
|
version=$(node -p "require('./package.json').version")
|
|
8
8
|
landing=docs/learning/README.md
|
|
9
9
|
production=docs/learning/PRODUCTION-PLAN.md
|
|
10
|
+
coverage=docs/learning/CURRICULUM-MAP.md
|
|
11
|
+
feature_guide=docs/learning/FEATURE-GUIDE.md
|
|
10
12
|
|
|
11
|
-
[[ -f "$landing" && -f "$production" ]]
|
|
13
|
+
[[ -f "$landing" && -f "$production" && -f "$coverage" && -f "$feature_guide" ]]
|
|
12
14
|
grep -qF 'docs/learning/README.md' README.md
|
|
15
|
+
grep -qF "**Baseline:** Cairnkeep $version" "$coverage"
|
|
16
|
+
grep -qF "**Baseline:** Cairnkeep $version" "$feature_guide"
|
|
17
|
+
grep -qF 'https://github.com/cairnkeep/cairnkeep-course-labs' "$landing"
|
|
18
|
+
for checkpoint in course-00-app course-01-bootstrap course-02-memory course-03-quality \
|
|
19
|
+
course-04-operation course-05-evidence course-06-governance course-07-evaluation; do
|
|
20
|
+
grep -qF "\`$checkpoint\`" "$coverage"
|
|
21
|
+
done
|
|
13
22
|
|
|
14
23
|
ready=0
|
|
15
24
|
brief=0
|
|
16
|
-
for number in $(seq -w 0
|
|
25
|
+
for number in $(seq -w 0 17); do
|
|
17
26
|
matches=(docs/learning/lessons/L"$number"-*.md)
|
|
18
27
|
[[ ${#matches[@]} -eq 1 && -f ${matches[0]} ]] || {
|
|
19
28
|
echo "learning path must contain exactly one L$number lesson" >&2
|
|
@@ -41,17 +50,24 @@ for number in $(seq -w 0 12); do
|
|
|
41
50
|
fi
|
|
42
51
|
done
|
|
43
52
|
|
|
44
|
-
[[ $ready -eq 4 && $brief -eq
|
|
53
|
+
[[ $ready -eq 4 && $brief -eq 14 ]] || {
|
|
45
54
|
echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
|
|
46
55
|
exit 1
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
for track in quickstart practitioner operator; do
|
|
58
|
+
for track in quickstart practitioner evidence-and-evaluation operator; do
|
|
50
59
|
file=docs/learning/tracks/$track.md
|
|
51
60
|
[[ -f "$file" ]]
|
|
52
61
|
grep -q '^# ' "$file"
|
|
53
62
|
done
|
|
54
63
|
|
|
64
|
+
while read -r command; do
|
|
65
|
+
grep -qF "\`cairn $command\`" "$coverage" || {
|
|
66
|
+
echo "top-level command has no curriculum owner: cairn $command" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
}
|
|
69
|
+
done < <(bin/cairn help | sed -n 's/^ cairn \([a-z-]*\).*/\1/p' | sort -u)
|
|
70
|
+
|
|
55
71
|
node <<'NODE'
|
|
56
72
|
const { existsSync, readdirSync, readFileSync } = require("node:fs");
|
|
57
73
|
const { dirname, resolve } = require("node:path");
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Public CLI contract for opt-in deterministic note distillation and lookup.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
6
|
+
tmp=$(mktemp -d)
|
|
7
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
8
|
+
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
9
|
+
|
|
10
|
+
cairn="$ROOT/bin/cairn"
|
|
11
|
+
fixture="$ROOT/mcp-memory-server/scripts/fixtures/notes/lifecycle-sessions.json"
|
|
12
|
+
project_a="$tmp/PARA/Projects/project-a"
|
|
13
|
+
project_b="$tmp/PARA/Projects/project-b"
|
|
14
|
+
project_c="$tmp/PARA/Projects/project-c"
|
|
15
|
+
store="$tmp/store"
|
|
16
|
+
mkdir -p "$project_a" "$project_b" "$project_c"
|
|
17
|
+
|
|
18
|
+
put_session() {
|
|
19
|
+
local name=$1 project=$2 session_id=${3:-}
|
|
20
|
+
(cd "$ROOT/mcp-memory-server" && PROJECT="$project" NAME="$name" SESSION_ID="$session_id" FIXTURE="$fixture" node --input-type=module - <<'EOF'
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { getTrajectoryLimits, trajectorySessionSchema } from "./dist/trajectory-schema.js";
|
|
23
|
+
import { putTrajectory } from "./dist/trajectory-store.js";
|
|
24
|
+
const all = JSON.parse(readFileSync(process.env.FIXTURE, "utf8"));
|
|
25
|
+
const raw = JSON.parse(JSON.stringify(all[process.env.NAME]).replaceAll("$PROJECT_ROOT", process.env.PROJECT));
|
|
26
|
+
if (process.env.SESSION_ID) raw.session_id = process.env.SESSION_ID;
|
|
27
|
+
await putTrajectory(process.env.PROJECT, trajectorySessionSchema.parse(raw), getTrajectoryLimits());
|
|
28
|
+
EOF
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
"$cairn" help | grep -q "cairn notes" || fail "help missing notes"
|
|
33
|
+
|
|
34
|
+
# Disabled means no notes root, lock, manifest, or trajectory mutation.
|
|
35
|
+
put_session failure "$project_a"
|
|
36
|
+
before=$(sha256sum "$project_a/.agentfs/trajectory.db" | cut -d' ' -f1)
|
|
37
|
+
CAIRN_AGENTFS_BASE_DIR="$store" env -u CAIRN_NOTE_DISTILLATION \
|
|
38
|
+
"$cairn" notes distill --project "$project_a" --json >"$tmp/disabled.json"
|
|
39
|
+
node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(j.enabled!==false)process.exit(1)' "$tmp/disabled.json" \
|
|
40
|
+
|| fail "disabled result is not explicit"
|
|
41
|
+
[[ ! -e "$store/notes" ]] || fail "disabled command created notes data"
|
|
42
|
+
after=$(sha256sum "$project_a/.agentfs/trajectory.db" | cut -d' ' -f1)
|
|
43
|
+
[[ "$before" == "$after" ]] || fail "disabled command changed trajectory bytes"
|
|
44
|
+
|
|
45
|
+
export CAIRN_AGENTFS_BASE_DIR="$store"
|
|
46
|
+
export CAIRN_NOTE_DISTILLATION=1
|
|
47
|
+
unset CAIRN_NOTE_ENRICHMENT CAIRN_LLM_API_KEY CAIRN_LLM_API_URL CAIRN_NOTE_ENRICHMENT_MODEL
|
|
48
|
+
|
|
49
|
+
"$cairn" notes distill --project "$project_a" --session note-failure-001 --json >"$tmp/created.json"
|
|
50
|
+
note_a=$(node -e 'const j=require(process.argv[1]);if(j.created.length!==1||j.created[0].status!=="unresolved")process.exit(1);process.stdout.write(j.created[0].id)' "$tmp/created.json") \
|
|
51
|
+
|| fail "exact-session distillation did not create one unresolved note"
|
|
52
|
+
path_a=$(node -e 'process.stdout.write(require(process.argv[1]).created[0].path)' "$tmp/created.json")
|
|
53
|
+
printf '\n## Maintainer notes\n\nPreserve this operator sentence.\n' >>"$path_a"
|
|
54
|
+
|
|
55
|
+
put_session fix "$project_a"
|
|
56
|
+
"$cairn" notes distill --project "$project_a" --json >"$tmp/fixed.json"
|
|
57
|
+
node -e 'const j=require(process.argv[1]);if(!j.updated.some(x=>x.status==="resolved"))process.exit(1)' "$tmp/fixed.json" \
|
|
58
|
+
|| fail "incremental current-project run did not resolve the note"
|
|
59
|
+
grep -q 'Preserve this operator sentence.' "$path_a" || fail "managed update removed manual content"
|
|
60
|
+
|
|
61
|
+
printf '%s\n' "TypeError: Cannot read properties of undefined (reading 'name')" \
|
|
62
|
+
" at loadUser (/another/root/src/user.ts:999:1)" \
|
|
63
|
+
| "$cairn" notes search-error --project "$project_a" --json >"$tmp/search.json"
|
|
64
|
+
node -e 'const j=require(process.argv[1]);if(j.results[0]?.id!==process.argv[2]||j.results[0]?.status!=="resolved")process.exit(1)' "$tmp/search.json" "$note_a" \
|
|
65
|
+
|| fail "fresh public search did not return resolved note first"
|
|
66
|
+
|
|
67
|
+
"$cairn" notes distill --project "$project_a" --session note-fix-001 --json >"$tmp/repeat.json"
|
|
68
|
+
node -e 'const j=require(process.argv[1]);if(j.already_processed.length!==1)process.exit(1)' "$tmp/repeat.json" \
|
|
69
|
+
|| fail "exact reprocessing is not idempotent"
|
|
70
|
+
|
|
71
|
+
put_session failure "$project_b" note-failure-project-b
|
|
72
|
+
"$cairn" notes distill --all-projects --para-root "$tmp/PARA" --json >"$tmp/all.json"
|
|
73
|
+
node -e 'const j=require(process.argv[1]);if(j.projects_scanned<2)process.exit(1)' "$tmp/all.json" \
|
|
74
|
+
|| fail "all-project mode did not scan both trajectory projects"
|
|
75
|
+
note_b=$(node -e 'const j=require(process.argv[1]);const c=j.results.flatMap(x=>x.created||[]).find(x=>x.project_id!==process.argv[2]);if(!c)process.exit(1);process.stdout.write(c.id)' "$tmp/all.json" "$(basename "$(dirname "$(dirname "$path_a")")")") \
|
|
76
|
+
|| fail "all-project mode did not create the corroborating note"
|
|
77
|
+
|
|
78
|
+
if "$cairn" notes promote "$note_a" --with "$note_b" --json >"$tmp/unconfirmed.out" 2>&1; then
|
|
79
|
+
fail "promotion succeeded without --confirm"
|
|
80
|
+
fi
|
|
81
|
+
if "$cairn" notes promote "$note_a" --with "$note_a" --confirm --json >"$tmp/same.out" 2>&1; then
|
|
82
|
+
fail "promotion accepted one-project evidence"
|
|
83
|
+
fi
|
|
84
|
+
"$cairn" notes promote "$note_a" --with "$note_b" --confirm --json >"$tmp/promoted.json"
|
|
85
|
+
node -e 'const j=require(process.argv[1]);if(j.status!=="promoted"||!j.shared_path)process.exit(1)' "$tmp/promoted.json" \
|
|
86
|
+
|| fail "confirmed cross-project promotion failed"
|
|
87
|
+
[[ $(find "$store/notes/shared" -type f -name '*.md' | wc -l | tr -d ' ') == 1 ]] \
|
|
88
|
+
|| fail "promotion did not leave exactly one shared canonical note"
|
|
89
|
+
grep -q 'node_type: provenance' "$path_a" || fail "project note did not become provenance"
|
|
90
|
+
|
|
91
|
+
# A live lock and an unmanaged replacement both fail without changing bytes.
|
|
92
|
+
put_session failure "$project_c" note-failure-project-c
|
|
93
|
+
"$cairn" notes distill --project "$project_c" --json >"$tmp/project-c.json"
|
|
94
|
+
path_c=$(node -e 'process.stdout.write(require(process.argv[1]).created[0].path)' "$tmp/project-c.json")
|
|
95
|
+
project_id_c=$(basename "$(dirname "$(dirname "$path_c")")")
|
|
96
|
+
mkdir -p "$store/notes/.cairnkeep/locks/$project_id_c.lock"
|
|
97
|
+
if "$cairn" notes distill --project "$project_c" --json >"$tmp/locked.out" 2>&1; then
|
|
98
|
+
fail "distillation ignored a live project lock"
|
|
99
|
+
fi
|
|
100
|
+
rm -rf "$store/notes/.cairnkeep/locks/$project_id_c.lock"
|
|
101
|
+
printf 'manual unmanaged bytes\n' >"$path_c"
|
|
102
|
+
collision_before=$(sha256sum "$path_c" | cut -d' ' -f1)
|
|
103
|
+
put_session recurrence "$project_c" note-recurrence-project-c
|
|
104
|
+
if "$cairn" notes distill --project "$project_c" --json >"$tmp/collision.out" 2>&1; then
|
|
105
|
+
fail "distillation overwrote an unmanaged collision"
|
|
106
|
+
fi
|
|
107
|
+
collision_after=$(sha256sum "$path_c" | cut -d' ' -f1)
|
|
108
|
+
[[ "$collision_before" == "$collision_after" ]] || fail "collision changed unmanaged bytes"
|
|
109
|
+
|
|
110
|
+
"$cairn" notes doctor --json >"$tmp/doctor.json"
|
|
111
|
+
node -e 'const j=require(process.argv[1]);if(j.schema_version!==1||j.ok!==true)process.exit(1)' "$tmp/doctor.json" \
|
|
112
|
+
|| fail "notes doctor did not validate the generated tree"
|
|
113
|
+
|
|
114
|
+
echo "PASS: cairn notes CLI, disabled path, promotion, lock and collision contracts"
|
|
@@ -8,6 +8,23 @@ tmp=$(mktemp -d)
|
|
|
8
8
|
trap 'rm -rf "$tmp"' EXIT
|
|
9
9
|
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
10
10
|
|
|
11
|
+
manifest_field() {
|
|
12
|
+
node -e 'const value=require(process.argv[1]); process.stdout.write(JSON.stringify(value[process.argv[2]] ?? null))' "$1" "$2"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
root_version_before=$(manifest_field "$ROOT/package.json" version)
|
|
16
|
+
root_dependencies_before=$(manifest_field "$ROOT/package.json" dependencies)
|
|
17
|
+
server_dependencies_before=$(manifest_field "$ROOT/mcp-memory-server/package.json" dependencies)
|
|
18
|
+
server_dev_dependencies_before=$(manifest_field "$ROOT/mcp-memory-server/package.json" devDependencies)
|
|
19
|
+
root_lock_before=$(sha256sum "$ROOT/package-lock.json" | cut -d' ' -f1)
|
|
20
|
+
server_lock_before=$(sha256sum "$ROOT/mcp-memory-server/package-lock.json" | cut -d' ' -f1)
|
|
21
|
+
node "$ROOT/mcp-memory-server/dist/eval-cli.js" --help >"$tmp/source-eval-help"
|
|
22
|
+
env -u CAIRN_EVAL "$ROOT/bin/cairn" eval validate \
|
|
23
|
+
--task-set "$tmp/source-unread-task-set" \
|
|
24
|
+
--adapter "$tmp/source-unread-adapter" \
|
|
25
|
+
--output "$tmp/source-unwritten-output" --json >"$tmp/source-eval-disabled.json"
|
|
26
|
+
[[ ! -e "$tmp/source-unwritten-output" ]] || fail "source disabled eval created output"
|
|
27
|
+
|
|
11
28
|
cd "$ROOT"
|
|
12
29
|
npm pack --silent --pack-destination "$tmp" --dry-run=false >/dev/null
|
|
13
30
|
tarballs=("$tmp"/*.tgz)
|
|
@@ -25,12 +42,249 @@ bootstrap_output=$(cairn bootstrap "$tmp/project")
|
|
|
25
42
|
[[ "$bootstrap_output" == *"cairn memory-server"* ]] || fail "bootstrap printed invalid memory-server setup instructions"
|
|
26
43
|
[[ "$bootstrap_output" == *"cairn sync --apply"* ]] || fail "bootstrap printed invalid operating-layer setup instructions"
|
|
27
44
|
[[ -x "$tmp/project/.ai/start-claude.sh" ]] || fail "bootstrap did not install an executable Claude launcher"
|
|
45
|
+
[[ -x "$tmp/project/.ai/start-pi.sh" ]] || fail "bootstrap did not install an executable Pi launcher"
|
|
46
|
+
[[ -x "$tmp/project/.ai/start-kimi.sh" ]] || fail "bootstrap did not install an executable Kimi launcher"
|
|
28
47
|
[[ -f "$tmp/project/.planning/config.json" ]] || fail "bootstrap did not install the planning scaffold"
|
|
29
48
|
|
|
30
49
|
(cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
|
|
50
|
+
env -u CAIRN_NOTE_DISTILLATION cairn notes --help >/dev/null \
|
|
51
|
+
|| fail "installed package omitted the notes command"
|
|
31
52
|
|
|
32
53
|
installed_root="$tmp/prefix/lib/node_modules/@cairnkeep/cli"
|
|
54
|
+
cairn eval --help >"$tmp/installed-eval-help"
|
|
55
|
+
cmp -s "$tmp/source-eval-help" "$tmp/installed-eval-help" || \
|
|
56
|
+
fail "source and installed eval help differ"
|
|
57
|
+
env -u CAIRN_EVAL cairn eval validate \
|
|
58
|
+
--task-set "$tmp/installed-unread-task-set" \
|
|
59
|
+
--adapter "$tmp/installed-unread-adapter" \
|
|
60
|
+
--output "$tmp/installed-unwritten-output" --json >"$tmp/installed-eval-disabled.json"
|
|
61
|
+
cmp -s "$tmp/source-eval-disabled.json" "$tmp/installed-eval-disabled.json" || \
|
|
62
|
+
fail "source and installed disabled eval identity differ"
|
|
63
|
+
[[ ! -e "$tmp/installed-unwritten-output" ]] || fail "installed disabled eval created output"
|
|
64
|
+
|
|
65
|
+
for required in \
|
|
66
|
+
schemas/capability-contract.schema.json \
|
|
67
|
+
schemas/capability-callback.schema.json \
|
|
68
|
+
templates/capabilities.json.template \
|
|
69
|
+
mcp-memory-server/dist/capability-schema.js \
|
|
70
|
+
mcp-memory-server/dist/capability-registry.js \
|
|
71
|
+
mcp-memory-server/dist/capability-config.js \
|
|
72
|
+
mcp-memory-server/dist/capability-store.js \
|
|
73
|
+
mcp-memory-server/dist/capability-adapter.js \
|
|
74
|
+
mcp-memory-server/dist/capability-cli.js \
|
|
75
|
+
claude/capability-contract/hooks/capability-command-start.sh \
|
|
76
|
+
claude/capability-contract/hooks/capability-command-finish.sh \
|
|
77
|
+
opencode/capability-contract/plugins/capability-command.ts
|
|
78
|
+
do
|
|
79
|
+
[[ -f "$installed_root/$required" ]] || fail "npm tarball omitted $required"
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
for required in \
|
|
83
|
+
schemas/eval-task-set.schema.json \
|
|
84
|
+
schemas/eval-adapter.schema.json \
|
|
85
|
+
schemas/eval-protocol.schema.json \
|
|
86
|
+
schemas/eval-report.schema.json \
|
|
87
|
+
mcp-memory-server/dist/eval-schema.js \
|
|
88
|
+
mcp-memory-server/dist/eval-process.js \
|
|
89
|
+
mcp-memory-server/dist/eval-workspace.js \
|
|
90
|
+
mcp-memory-server/dist/eval-plan.js \
|
|
91
|
+
mcp-memory-server/dist/eval-statistics.js \
|
|
92
|
+
mcp-memory-server/dist/eval-report.js \
|
|
93
|
+
mcp-memory-server/dist/eval-runner.js \
|
|
94
|
+
mcp-memory-server/dist/eval-cli.js \
|
|
95
|
+
scripts/fake-eval-adapter.mjs \
|
|
96
|
+
examples/eval/task-set.json \
|
|
97
|
+
examples/eval/bundled-fake.json \
|
|
98
|
+
examples/eval/adapter.json \
|
|
99
|
+
examples/eval/README.md
|
|
100
|
+
do
|
|
101
|
+
[[ -f "$installed_root/$required" ]] || fail "npm tarball omitted $required"
|
|
102
|
+
done
|
|
103
|
+
[[ -x "$installed_root/scripts/fake-eval-adapter.mjs" ]] || \
|
|
104
|
+
fail "installed fake adapter is not executable"
|
|
105
|
+
for asset in \
|
|
106
|
+
schemas/eval-task-set.schema.json \
|
|
107
|
+
schemas/eval-adapter.schema.json \
|
|
108
|
+
schemas/eval-protocol.schema.json \
|
|
109
|
+
schemas/eval-report.schema.json \
|
|
110
|
+
examples/eval/task-set.json \
|
|
111
|
+
examples/eval/bundled-fake.json \
|
|
112
|
+
examples/eval/adapter.json \
|
|
113
|
+
examples/eval/README.md
|
|
114
|
+
do
|
|
115
|
+
[[ ! -x "$installed_root/$asset" ]] || \
|
|
116
|
+
fail "installed public eval data asset is unexpectedly executable: $asset"
|
|
117
|
+
done
|
|
118
|
+
eval_assets=$(cd "$installed_root/examples/eval" && find . -type f -print | LC_ALL=C sort)
|
|
119
|
+
[[ "$eval_assets" == $'./README.md\n./adapter.json\n./bundled-fake.json\n./task-set.json' ]] || \
|
|
120
|
+
fail "installed eval examples contain an unexpected path"
|
|
121
|
+
|
|
122
|
+
node - "$installed_root/examples/eval/adapter.json" <<'NODE'
|
|
123
|
+
const value = require(process.argv[2]);
|
|
124
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(["command", "id", "schema_version", "turn_semantics"])) process.exit(1);
|
|
125
|
+
if (value.id !== "cairn-offline-fake" || value.command.program !== "../../scripts/fake-eval-adapter.mjs") process.exit(1);
|
|
126
|
+
if (!Array.isArray(value.command.args) || value.command.args.length !== 0) process.exit(1);
|
|
127
|
+
NODE
|
|
128
|
+
if grep -R -E 'https?://|CAIRN_LLM_API_KEY|CAIRN_LLM_ENDPOINT' \
|
|
129
|
+
"$installed_root/examples/eval" "$installed_root/scripts/fake-eval-adapter.mjs" >/dev/null; then
|
|
130
|
+
fail "installed eval fixture contains a live endpoint or credential default"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
installed_task_set="$installed_root/examples/eval/task-set.json"
|
|
134
|
+
installed_adapter="$installed_root/examples/eval/adapter.json"
|
|
135
|
+
installed_output="$tmp/installed-eval-output"
|
|
136
|
+
CAIRN_EVAL=1 cairn eval validate \
|
|
137
|
+
--task-set "$installed_task_set" --adapter "$installed_adapter" \
|
|
138
|
+
--output "$installed_output" --seed installed-package-seed --json >"$tmp/installed-eval-validate.json"
|
|
139
|
+
[[ ! -e "$installed_output" ]] || fail "installed eval validation created output"
|
|
140
|
+
CAIRN_EVAL=1 cairn eval run \
|
|
141
|
+
--task-set "$installed_task_set" --adapter "$installed_adapter" \
|
|
142
|
+
--output "$installed_output" --seed installed-package-seed --yes --json \
|
|
143
|
+
>"$tmp/installed-eval-run.json" 2>"$tmp/installed-eval-run.err"
|
|
144
|
+
|
|
145
|
+
node - \
|
|
146
|
+
"$installed_root/mcp-memory-server/dist/eval-schema.js" \
|
|
147
|
+
"$installed_root/package.json" \
|
|
148
|
+
"$installed_task_set" \
|
|
149
|
+
"$installed_root/examples/eval/bundled-fake.json" \
|
|
150
|
+
"$tmp/installed-eval-validate.json" \
|
|
151
|
+
"$tmp/installed-eval-run.json" <<'NODE'
|
|
152
|
+
const { readFileSync } = require("node:fs");
|
|
153
|
+
const { pathToFileURL } = require("node:url");
|
|
154
|
+
(async () => {
|
|
155
|
+
const [schemaPath, packagePath, taskSetPath, bindingPath, validationPath, runPath] = process.argv.slice(2);
|
|
156
|
+
const { canonicalDigest } = await import(pathToFileURL(schemaPath).href);
|
|
157
|
+
const manifest = JSON.parse(readFileSync(taskSetPath, "utf8"));
|
|
158
|
+
const binding = JSON.parse(readFileSync(bindingPath, "utf8"));
|
|
159
|
+
const installedPackage = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
160
|
+
const validation = JSON.parse(readFileSync(validationPath, "utf8"));
|
|
161
|
+
const run = JSON.parse(readFileSync(runPath, "utf8"));
|
|
162
|
+
const digest = canonicalDigest(manifest);
|
|
163
|
+
if (binding.identifier !== "cairn-offline-fake-v1") process.exit(1);
|
|
164
|
+
if (binding.package_version !== installedPackage.version || binding.task_set_digest !== digest) process.exit(1);
|
|
165
|
+
if (validation.operation !== "validate" || validation.invocation_count !== manifest.tasks.length * 2) process.exit(1);
|
|
166
|
+
if (validation.plan.source.identifier !== binding.identifier
|
|
167
|
+
|| validation.plan.source.package_version !== installedPackage.version
|
|
168
|
+
|| validation.plan.task_set_digest !== digest) process.exit(1);
|
|
169
|
+
if (run.operation !== "run" || run.status !== "final" || run.invocation_count !== manifest.tasks.length * 2) process.exit(1);
|
|
170
|
+
const report = JSON.parse(readFileSync(run.report_path, "utf8"));
|
|
171
|
+
if (report.task_set_digest !== digest || report.evidence.task_set_digest !== digest) process.exit(1);
|
|
172
|
+
if (report.evidence.package_version !== installedPackage.version
|
|
173
|
+
|| report.evidence.evidence_scope !== "offline-framework"
|
|
174
|
+
|| report.observations.length !== manifest.tasks.length * 2) process.exit(1);
|
|
175
|
+
})().catch((error) => { console.error(error); process.exit(1); });
|
|
176
|
+
NODE
|
|
177
|
+
|
|
178
|
+
binding_path="$installed_root/examples/eval/bundled-fake.json"
|
|
179
|
+
cp "$binding_path" "$tmp/original-bundled-fake.json"
|
|
180
|
+
for field in identifier package_version task_set_digest; do
|
|
181
|
+
node - "$binding_path" "$field" <<'NODE'
|
|
182
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
183
|
+
const [path, field] = process.argv.slice(2);
|
|
184
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
185
|
+
value[field] = field === "task_set_digest" ? "0".repeat(64) : `mutated-${field}`;
|
|
186
|
+
writeFileSync(path, `${JSON.stringify(value)}\n`);
|
|
187
|
+
NODE
|
|
188
|
+
if CAIRN_EVAL=1 cairn eval validate \
|
|
189
|
+
--task-set "$installed_task_set" --adapter "$installed_adapter" \
|
|
190
|
+
--output "$tmp/rejected-eval-output" --json >/dev/null 2>&1; then
|
|
191
|
+
fail "installed eval accepted mutated $field binding"
|
|
192
|
+
fi
|
|
193
|
+
cp "$tmp/original-bundled-fake.json" "$binding_path"
|
|
194
|
+
done
|
|
195
|
+
[[ ! -e "$tmp/rejected-eval-output" ]] || fail "rejected installed eval fixture created output"
|
|
196
|
+
chmod -R u+w "$installed_output"
|
|
197
|
+
|
|
198
|
+
if find \
|
|
199
|
+
"$installed_root/claude/capability-contract/commands" \
|
|
200
|
+
"$installed_root/opencode/capability-contract/command" \
|
|
201
|
+
"$installed_root/opencode/capability-contract/workflows" \
|
|
202
|
+
-type f -print 2>/dev/null | grep -q .; then
|
|
203
|
+
fail "npm tarball included a retired capability prompt owner"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
CAIRN_CAPABILITY_CONTRACT=1 cairn capabilities status --json >"$tmp/capability-status.json" \
|
|
207
|
+
|| fail "installed package capability status failed"
|
|
208
|
+
node - "$tmp/capability-status.json" <<'NODE'
|
|
209
|
+
const fs = require("fs")
|
|
210
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"))
|
|
211
|
+
const expected = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"]
|
|
212
|
+
if (value.schema_version !== 1 || value.contract_enabled !== true) process.exit(1)
|
|
213
|
+
if (JSON.stringify(value.capabilities.map((row) => row.id)) !== JSON.stringify(expected)) process.exit(1)
|
|
214
|
+
if (!/^[0-9a-f]{64}$/.test(value.configuration_digest)) process.exit(1)
|
|
215
|
+
NODE
|
|
33
216
|
[[ -f "$installed_root/examples/anythingllm/sync_to_anythingllm.py" ]] || \
|
|
34
217
|
fail "npm tarball omitted the default AnythingLLM sync script"
|
|
218
|
+
[[ -f "$installed_root/pi/extensions/cairnkeep-trajectory.ts" ]] || \
|
|
219
|
+
fail "npm tarball omitted the Pi trajectory extension"
|
|
220
|
+
[[ -x "$installed_root/scripts/sync-pi-assets.sh" ]] || \
|
|
221
|
+
fail "npm tarball omitted the Pi sync command"
|
|
222
|
+
[[ -f "$installed_root/templates/start-pi.sh.template" ]] || \
|
|
223
|
+
fail "npm tarball omitted the Pi launcher template"
|
|
224
|
+
[[ -f "$installed_root/templates/start-kimi.sh.template" ]] || \
|
|
225
|
+
fail "npm tarball omitted the Kimi launcher template"
|
|
226
|
+
[[ -f "$installed_root/schemas/note.schema.json" ]] || \
|
|
227
|
+
fail "npm tarball omitted the note schema"
|
|
228
|
+
[[ -f "$installed_root/schemas/memory-node.schema.json" ]] || \
|
|
229
|
+
fail "npm tarball omitted the typed memory-node schema"
|
|
230
|
+
[[ -f "$installed_root/schemas/artifact.schema.json" ]] || \
|
|
231
|
+
fail "npm tarball omitted the artifact schema"
|
|
232
|
+
for module in node-schema node-store node-cli; do
|
|
233
|
+
[[ -f "$installed_root/mcp-memory-server/dist/$module.js" ]] || \
|
|
234
|
+
fail "npm tarball omitted compiled $module"
|
|
235
|
+
done
|
|
236
|
+
[[ -f "$installed_root/mcp-memory-server/dist/note-cli.js" ]] || \
|
|
237
|
+
fail "npm tarball omitted the compiled note CLI"
|
|
238
|
+
[[ -f "$installed_root/mcp-memory-server/dist/note-enrichment.js" ]] || \
|
|
239
|
+
fail "npm tarball omitted optional note enrichment"
|
|
240
|
+
[[ -f "$installed_root/mcp-memory-server/dist/note-store.js" ]] || \
|
|
241
|
+
fail "npm tarball omitted the deterministic note store"
|
|
242
|
+
for module in artifact-schema artifact-store compaction-normalize artifact-cli; do
|
|
243
|
+
[[ -f "$installed_root/mcp-memory-server/dist/$module.js" ]] || \
|
|
244
|
+
fail "npm tarball omitted compiled $module"
|
|
245
|
+
done
|
|
246
|
+
[[ -x "$installed_root/claude/hooks/compaction-capture.sh" ]] || \
|
|
247
|
+
fail "npm tarball omitted the executable Claude compaction hook"
|
|
248
|
+
[[ -f "$installed_root/opencode/plugins/memory-capture.ts" ]] || \
|
|
249
|
+
fail "npm tarball omitted the OpenCode capture plugin"
|
|
250
|
+
[[ -f "$installed_root/opencode/plugins/memory-wakeup.ts" ]] || \
|
|
251
|
+
fail "npm tarball omitted the OpenCode recovery plugin"
|
|
252
|
+
for fixture in \
|
|
253
|
+
compaction-claude-code-2.1.219.json \
|
|
254
|
+
compaction-claude-code-2.1.220.json \
|
|
255
|
+
compaction-opencode-1.17.20-event.json \
|
|
256
|
+
compaction-opencode-1.17.20-messages.json
|
|
257
|
+
do
|
|
258
|
+
[[ -f "$installed_root/mcp-memory-server/scripts/fixtures/$fixture" ]] || \
|
|
259
|
+
fail "npm tarball omitted versioned fixture $fixture"
|
|
260
|
+
done
|
|
261
|
+
if find "$installed_root" -type f \( \
|
|
262
|
+
-path '*/.ai/capabilities.json' \
|
|
263
|
+
-o -name 'manifest-v1.json' \
|
|
264
|
+
-o -name '*.db' -o -name '*.db-wal' -o -name '*.db-shm' \
|
|
265
|
+
-o -path '*/notes/projects/*' -o -path '*/transactions/*' -o -path '*/backups/*' \
|
|
266
|
+
-o -path '*/runtime-evidence/*' -o -path '*/generated-artifacts/*' \
|
|
267
|
+
-o -path '*/artifacts/projects/*' -o -path '*/artifacts/sessions/*' -o -path '*/artifacts/data/*' \
|
|
268
|
+
-o -path '*/capability-callback/*' -o -path '*/test-tmp/*' \
|
|
269
|
+
-o -path '*/.agentfs/*' -o -path '*/eval/experiments/*' \
|
|
270
|
+
-o -path '*/eval/reports/*' -o -path '*/eval/snapshots/*' \
|
|
271
|
+
-o -path '*/eval/runtime-evidence/*' -o -path '*/eval/evidence/*' \
|
|
272
|
+
-o -path '*/eval/user-data/*' \
|
|
273
|
+
\) | grep -q .; then
|
|
274
|
+
fail "npm tarball included generated capability, callback, eval evidence, or user data"
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
if find "$installed_root" -type f \( -name 'token_miser' -o -name 'token-miser' -o -name 'token_miser.*' -o -name 'token-miser.*' \) | grep -q .; then
|
|
278
|
+
fail "npm tarball included a token-miser implementation instead of the existing delegate boundary"
|
|
279
|
+
fi
|
|
280
|
+
[[ -f "$installed_root/mcp-memory-server/dist/index.js" ]] || fail "npm tarball omitted the existing MCP delegate owner"
|
|
281
|
+
[[ -x "$installed_root/scripts/sync-opencode-explore-assets.sh" ]] || fail "npm tarball omitted the existing explore asset delegate"
|
|
282
|
+
|
|
283
|
+
[[ "$(manifest_field "$ROOT/package.json" version)" == "$root_version_before" ]] || fail "package version changed during pack/install"
|
|
284
|
+
[[ "$(manifest_field "$ROOT/package.json" dependencies)" == "$root_dependencies_before" ]] || fail "root dependencies changed during pack/install"
|
|
285
|
+
[[ "$(manifest_field "$ROOT/mcp-memory-server/package.json" dependencies)" == "$server_dependencies_before" ]] || fail "server dependencies changed during pack/install"
|
|
286
|
+
[[ "$(manifest_field "$ROOT/mcp-memory-server/package.json" devDependencies)" == "$server_dev_dependencies_before" ]] || fail "server devDependencies changed during pack/install"
|
|
287
|
+
[[ "$(sha256sum "$ROOT/package-lock.json" | cut -d' ' -f1)" == "$root_lock_before" ]] || fail "root lockfile changed during pack/install"
|
|
288
|
+
[[ "$(sha256sum "$ROOT/mcp-memory-server/package-lock.json" | cut -d' ' -f1)" == "$server_lock_before" ]] || fail "server lockfile changed during pack/install"
|
|
35
289
|
|
|
36
290
|
echo "PASS: npm tarball installs a self-contained CLI and MCP server"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
MARKER="PHASE16_RED:PACKAGE_UNINSTALL_CONTRACT_MISSING"
|
|
6
|
+
|
|
7
|
+
case "${1:-}" in
|
|
8
|
+
"")
|
|
9
|
+
test "$#" -eq 0 || exit 2
|
|
10
|
+
"$ROOT/scripts/test-package-install.sh" || exit 1
|
|
11
|
+
"$ROOT/scripts/test-uninstall.sh" || exit 1
|
|
12
|
+
echo "PASS: Phase 16 package/uninstall baseline"
|
|
13
|
+
;;
|
|
14
|
+
--expect-red)
|
|
15
|
+
test "$#" -eq 1 || exit 2
|
|
16
|
+
tmp=$(mktemp -d)
|
|
17
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
18
|
+
|
|
19
|
+
mkdir -p "$tmp/expected/schemas" "$tmp/expected/mcp-memory-server/dist"
|
|
20
|
+
mkdir -p "$tmp/store/notes/.cairnkeep/transactions/txn-prepared/history"
|
|
21
|
+
printf '%s\n' '{"schema_version":1,"node_type":"knowledge","tags":["release"]}' >"$tmp/store/identity-node-metadata.json"
|
|
22
|
+
printf '%s\n' '{"schema_version":1,"import_id":"package-fixture","batch_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' >"$tmp/store/identity-import-replay.json"
|
|
23
|
+
printf '%s\n' '{"schema_version":1,"state":"prepared"}' >"$tmp/store/notes/.cairnkeep/transactions/txn-prepared/journal.json"
|
|
24
|
+
printf '%s\n' 'history bytes' >"$tmp/store/notes/.cairnkeep/transactions/txn-prepared/history/note.json"
|
|
25
|
+
|
|
26
|
+
expected="schemas/memory-node.schema.json mcp-memory-server/dist/node-schema.js mcp-memory-server/dist/node-store.js mcp-memory-server/dist/node-cli.js"
|
|
27
|
+
listing=$(cd "$ROOT" && npm pack --dry-run --json --ignore-scripts 2>/dev/null) || exit 1
|
|
28
|
+
missing=0
|
|
29
|
+
for path in $expected; do
|
|
30
|
+
if ! printf '%s\n' "$listing" | grep -Fq "\"path\": \"$path\""; then
|
|
31
|
+
missing=$((missing + 1))
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
test "$missing" -gt 0 || {
|
|
35
|
+
echo "Phase 16 package artifacts are already present; expected RED is stale" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
grep -q -- '--purge-memory' "$ROOT/scripts/uninstall.sh" || exit 1
|
|
39
|
+
if grep -q 'identity-node-metadata\|txn-prepared' "$ROOT/scripts/test-uninstall.sh"; then
|
|
40
|
+
echo "Phase 16 uninstall fixtures are already covered; expected RED is stale" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
printf '%s\n' "$MARKER" >&2
|
|
44
|
+
exit 86
|
|
45
|
+
;;
|
|
46
|
+
*)
|
|
47
|
+
echo "Usage: $0 [--expect-red]" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
;;
|
|
50
|
+
esac
|