@ccoalm/ccl-skills 0.7.0 → 0.8.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/README.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +8 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +11 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +4 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +7 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +95 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +54 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +69 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +8 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +8 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +6 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
- package/dist/assets/release.json +175 -70
- package/package.json +1 -1
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Behavior regression for the canonical review-plan intent updater.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
6
|
+
UPDATER="$SCRIPT_DIR/update_review_plan_intent.py"
|
|
7
|
+
GATE="$SCRIPT_DIR/review_gate.py"
|
|
8
|
+
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd -P)"
|
|
9
|
+
[ -f "$UPDATER" ] || { echo "FAIL: updater not found: $UPDATER" >&2; exit 1; }
|
|
10
|
+
[ -f "$GATE" ] || { echo "FAIL: review gate not found: $GATE" >&2; exit 1; }
|
|
11
|
+
|
|
12
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/review-plan-intent.XXXXXX")"
|
|
13
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
14
|
+
|
|
15
|
+
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
16
|
+
assert_rc() { [ "$1" = "$2" ] || fail "expected rc=$2 got rc=$1 ($3)"; }
|
|
17
|
+
assert_contains() { case "$2" in *"$1"*) : ;; *) fail "expected '$1' ($3): $2";; esac; }
|
|
18
|
+
digest() { python3 - "$1" <<'PY'
|
|
19
|
+
import hashlib
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest())
|
|
24
|
+
PY
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
PLAN="$TMP/review-plan.json"
|
|
28
|
+
APPEND="$TMP/append.txt"
|
|
29
|
+
CORE="$TMP/core.txt"
|
|
30
|
+
LATEST="$TMP/latest.txt"
|
|
31
|
+
OLD_INTENT="$TMP/old-intent.txt"
|
|
32
|
+
|
|
33
|
+
python3 - "$PLAN" "$APPEND" "$CORE" "$LATEST" "$OLD_INTENT" <<'PY'
|
|
34
|
+
import json
|
|
35
|
+
import hashlib
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
plan_path, append_path, core_path, latest_path, old_intent_path = map(
|
|
40
|
+
Path, sys.argv[1:]
|
|
41
|
+
)
|
|
42
|
+
old = "scope:" + ("x" * (3995 - len("scope:") - len("c27"))) + "c27"
|
|
43
|
+
latest = "latest-round:c28"
|
|
44
|
+
core = old[: 3892 - len("\n\n") - len(latest)]
|
|
45
|
+
replacement = f"{core}\n\n{latest}"
|
|
46
|
+
assert len(old) == 3995 and old.endswith("c27")
|
|
47
|
+
assert len(replacement) == 3892 and old.startswith(core)
|
|
48
|
+
plan = {
|
|
49
|
+
"intent": old,
|
|
50
|
+
"acceptance": ["The candidate behavior is verified."],
|
|
51
|
+
"self_review": [
|
|
52
|
+
{
|
|
53
|
+
"concern": concern,
|
|
54
|
+
"skill": "code-review",
|
|
55
|
+
"conclusion": conclusion,
|
|
56
|
+
"evidence_refs": ["focused-test"],
|
|
57
|
+
}
|
|
58
|
+
for concern, conclusion in (
|
|
59
|
+
("correctness", "The focused checks cover the updater's accepted state transitions."),
|
|
60
|
+
("safety", "The focused checks cover no-write failures and file integrity boundaries."),
|
|
61
|
+
("failure_paths", "The focused checks cover overflow, stale input, and malformed text paths."),
|
|
62
|
+
("tests_evidence", "The focused regression fails when bounded update guarantees are removed."),
|
|
63
|
+
("compatibility", "The focused checks preserve the plan schema and original file permissions."),
|
|
64
|
+
)
|
|
65
|
+
],
|
|
66
|
+
"evidence": [
|
|
67
|
+
{
|
|
68
|
+
"id": "focused-test",
|
|
69
|
+
"result": "The focused updater regression covers overflow and replacement behavior.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "review-plan-intent-stable-core-v1",
|
|
73
|
+
"result": (
|
|
74
|
+
f"chars={len(core)};sha256="
|
|
75
|
+
f"{hashlib.sha256(core.encode('utf-8')).hexdigest()}"
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
plan_path.write_text(json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
81
|
+
append_path.write_text("\nlatest-round:c28", encoding="utf-8")
|
|
82
|
+
core_path.write_text(core, encoding="utf-8")
|
|
83
|
+
latest_path.write_text(latest, encoding="utf-8")
|
|
84
|
+
old_intent_path.write_text(old, encoding="utf-8")
|
|
85
|
+
PY
|
|
86
|
+
chmod 600 "$PLAN"
|
|
87
|
+
|
|
88
|
+
before="$(digest "$PLAN")"
|
|
89
|
+
set +e
|
|
90
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --append-intent-file "$APPEND" 2>&1)"
|
|
91
|
+
rc=$?
|
|
92
|
+
set -e
|
|
93
|
+
assert_rc "$rc" 2 "3995-character append must fail closed"
|
|
94
|
+
assert_contains "intent_append_overflow" "$out" "stable overflow reason"
|
|
95
|
+
[ "$(digest "$PLAN")" = "$before" ] || fail "overflow changed the plan"
|
|
96
|
+
|
|
97
|
+
# Duplicate JSON keys must fail before any rewrite. Otherwise json.loads keeps
|
|
98
|
+
# only the last value and the updater permanently erases bytes while claiming
|
|
99
|
+
# that intent compaction is loss-preserving.
|
|
100
|
+
python3 - "$PLAN" "$TMP/duplicate-plan.json" "$TMP/duplicate-append.txt" <<'PY'
|
|
101
|
+
import json
|
|
102
|
+
import sys
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
source, target, append = map(Path, sys.argv[1:])
|
|
106
|
+
plan = json.loads(source.read_text(encoding="utf-8"))
|
|
107
|
+
plan["intent"] = "short stable intent"
|
|
108
|
+
rendered = json.dumps(plan, ensure_ascii=False, indent=2) + "\n"
|
|
109
|
+
rendered = rendered.replace(
|
|
110
|
+
' "acceptance":',
|
|
111
|
+
' "acceptance": ["discarded duplicate"],\n "acceptance":',
|
|
112
|
+
1,
|
|
113
|
+
)
|
|
114
|
+
target.write_text(rendered, encoding="utf-8")
|
|
115
|
+
append.write_text(" next transition", encoding="utf-8")
|
|
116
|
+
PY
|
|
117
|
+
duplicate_before="$(digest "$TMP/duplicate-plan.json")"
|
|
118
|
+
set +e
|
|
119
|
+
out="$(python3 "$UPDATER" --plan "$TMP/duplicate-plan.json" \
|
|
120
|
+
--append-intent-file "$TMP/duplicate-append.txt" \
|
|
121
|
+
--expected-sha256 "$duplicate_before" 2>&1)"
|
|
122
|
+
rc=$?
|
|
123
|
+
set -e
|
|
124
|
+
assert_rc "$rc" 2 "duplicate JSON keys must fail closed"
|
|
125
|
+
assert_contains "plan_duplicate_key" "$out" "duplicate-key reason"
|
|
126
|
+
[ "$(digest "$TMP/duplicate-plan.json")" = "$duplicate_before" ] \
|
|
127
|
+
|| fail "duplicate-key refusal changed the plan"
|
|
128
|
+
|
|
129
|
+
set +e
|
|
130
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$before" 2>&1)"
|
|
131
|
+
rc=$?
|
|
132
|
+
set -e
|
|
133
|
+
assert_rc "$rc" 0 "compact core+latest update should succeed"
|
|
134
|
+
assert_contains "review_plan_intent_updated" "$out" "success token"
|
|
135
|
+
[ "$(digest "$PLAN")" != "$before" ] || fail "replacement did not change the plan digest"
|
|
136
|
+
# GNU-first probe (mirrors opencode_review.sh): GNU stat echoes unknown BSD
|
|
137
|
+
# directives verbatim with exit 0, so a BSD-first fallback never fires on CI.
|
|
138
|
+
[ "$(stat -c '%a' "$PLAN" 2>/dev/null || stat -f '%Lp' "$PLAN")" = 600 ] || fail "plan mode was not preserved"
|
|
139
|
+
python3 - "$PLAN" "$OLD_INTENT" <<'PY'
|
|
140
|
+
import base64
|
|
141
|
+
import hashlib
|
|
142
|
+
import json
|
|
143
|
+
import sys
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
147
|
+
core = Path(sys.argv[1]).with_name("core.txt").read_text(encoding="utf-8")
|
|
148
|
+
latest = Path(sys.argv[1]).with_name("latest.txt").read_text(encoding="utf-8")
|
|
149
|
+
old_intent = Path(sys.argv[2]).read_text(encoding="utf-8")
|
|
150
|
+
assert plan["intent"] == f"{core}\n\n{latest}"
|
|
151
|
+
assert len(plan["intent"]) == 3892
|
|
152
|
+
assert set(plan) == {"intent", "acceptance", "self_review", "evidence"}
|
|
153
|
+
|
|
154
|
+
history_prefix = "review-plan-intent-history-v1-"
|
|
155
|
+
manifests = [
|
|
156
|
+
row
|
|
157
|
+
for row in plan["evidence"]
|
|
158
|
+
if row.get("id", "").startswith(history_prefix)
|
|
159
|
+
and row["id"].endswith("-manifest")
|
|
160
|
+
]
|
|
161
|
+
assert len(manifests) == 1, plan["evidence"]
|
|
162
|
+
manifest = manifests[0]
|
|
163
|
+
manifest_fields = dict(
|
|
164
|
+
field.split("=", 1) for field in manifest["result"].split(";")
|
|
165
|
+
)
|
|
166
|
+
group = manifest["id"][len(history_prefix) : -len("-manifest")]
|
|
167
|
+
parts = sorted(
|
|
168
|
+
(
|
|
169
|
+
row
|
|
170
|
+
for row in plan["evidence"]
|
|
171
|
+
if row.get("id", "").startswith(f"{history_prefix}{group}-part-")
|
|
172
|
+
),
|
|
173
|
+
key=lambda row: row["id"],
|
|
174
|
+
)
|
|
175
|
+
assert len(parts) == int(manifest_fields["parts"]), parts
|
|
176
|
+
encoded_suffix = "".join(
|
|
177
|
+
row["result"].split(";data=", 1)[1] for row in parts
|
|
178
|
+
)
|
|
179
|
+
suffix = base64.b64decode(encoded_suffix, validate=True).decode("utf-8")
|
|
180
|
+
preserved_old_intent = core + suffix
|
|
181
|
+
assert preserved_old_intent == old_intent
|
|
182
|
+
assert manifest_fields["format"] == "review-plan-intent-history-v1"
|
|
183
|
+
assert manifest_fields["encoding"] == "base64-utf8"
|
|
184
|
+
assert int(manifest_fields["old_chars"]) == len(old_intent)
|
|
185
|
+
assert int(manifest_fields["core_chars"]) == len(core)
|
|
186
|
+
assert manifest_fields["old_sha256"] == hashlib.sha256(
|
|
187
|
+
old_intent.encode("utf-8")
|
|
188
|
+
).hexdigest()
|
|
189
|
+
assert int(manifest_fields["suffix_bytes"]) == len(suffix.encode("utf-8"))
|
|
190
|
+
assert manifest_fields["suffix_sha256"] == hashlib.sha256(
|
|
191
|
+
suffix.encode("utf-8")
|
|
192
|
+
).hexdigest()
|
|
193
|
+
PY
|
|
194
|
+
|
|
195
|
+
# Feed the helper's output through the real plan validator. Restrict the client
|
|
196
|
+
# order to the implementer's own family so this proves validation reached the
|
|
197
|
+
# provider-selection boundary without launching an external reviewer.
|
|
198
|
+
printf '%s\n' \
|
|
199
|
+
'diff --git a/skills/code-review/references/staged-review-contract.md b/skills/code-review/references/staged-review-contract.md' \
|
|
200
|
+
'--- a/skills/code-review/references/staged-review-contract.md' \
|
|
201
|
+
'+++ b/skills/code-review/references/staged-review-contract.md' \
|
|
202
|
+
'@@ -1 +1 @@' \
|
|
203
|
+
'-old contract text' \
|
|
204
|
+
'+new contract text' >"$TMP/candidate.diff"
|
|
205
|
+
set +e
|
|
206
|
+
out="$(CODE_REVIEW_CLIENT_ORDER=codex python3 "$GATE" \
|
|
207
|
+
--mode review \
|
|
208
|
+
--cwd "$ROOT" \
|
|
209
|
+
--diff-file "$TMP/candidate.diff" \
|
|
210
|
+
--implementer-family openai \
|
|
211
|
+
--review-plan-file "$PLAN" \
|
|
212
|
+
--stage build \
|
|
213
|
+
--challenge-budget 0 \
|
|
214
|
+
--timeout 5 \
|
|
215
|
+
--total-timeout 5 2>&1)"
|
|
216
|
+
rc=$?
|
|
217
|
+
set -e
|
|
218
|
+
assert_rc "$rc" 2 "gate-valid plan should reach the no-independent-reviewer boundary"
|
|
219
|
+
assert_contains '"reason_code":"no_independent_reviewer_available"' "$out" "real gate accepted the updated plan"
|
|
220
|
+
|
|
221
|
+
# Compaction is the recovery path when another producer has already written an
|
|
222
|
+
# over-limit intent. It must validate the replacement, not reject the old state
|
|
223
|
+
# before mode dispatch and force an identity-bypassing hand edit.
|
|
224
|
+
python3 - "$PLAN" "$TMP/overlong-plan.json" "$CORE" "$TMP/overlong-latest.txt" <<'PY'
|
|
225
|
+
import json
|
|
226
|
+
import sys
|
|
227
|
+
from pathlib import Path
|
|
228
|
+
|
|
229
|
+
source, target, core_path, latest_path = map(Path, sys.argv[1:])
|
|
230
|
+
plan = json.loads(source.read_text(encoding="utf-8"))
|
|
231
|
+
core = core_path.read_text(encoding="utf-8")
|
|
232
|
+
plan["intent"] = core + ("x" * (4001 - len(core)))
|
|
233
|
+
target.write_text(json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
234
|
+
latest_path.write_text("recover the newest over-limit transition", encoding="utf-8")
|
|
235
|
+
PY
|
|
236
|
+
overlong_before="$(digest "$TMP/overlong-plan.json")"
|
|
237
|
+
set +e
|
|
238
|
+
out="$(python3 "$UPDATER" --plan "$TMP/overlong-plan.json" --compact-core-intent-file "$CORE" --latest-intent-file "$TMP/overlong-latest.txt" --expected-sha256 "$overlong_before" 2>&1)"
|
|
239
|
+
rc=$?
|
|
240
|
+
set -e
|
|
241
|
+
assert_rc "$rc" 0 "an over-limit existing intent must remain compactable"
|
|
242
|
+
assert_contains "review_plan_intent_updated mode=compact" "$out" "over-limit recovery token"
|
|
243
|
+
python3 - "$TMP/overlong-plan.json" "$CORE" "$TMP/overlong-latest.txt" <<'PY'
|
|
244
|
+
import json
|
|
245
|
+
import sys
|
|
246
|
+
from pathlib import Path
|
|
247
|
+
|
|
248
|
+
plan_path, core_path, latest_path = map(Path, sys.argv[1:])
|
|
249
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
250
|
+
assert plan["intent"] == f"{core_path.read_text(encoding='utf-8')}\n\n{latest_path.read_text(encoding='utf-8')}"
|
|
251
|
+
assert len(plan["intent"]) <= 4000
|
|
252
|
+
PY
|
|
253
|
+
|
|
254
|
+
# Existing plans without a persisted stable-core identity remain appendable,
|
|
255
|
+
# but must not compact an arbitrary prefix into a new history boundary.
|
|
256
|
+
python3 - "$PLAN" "$TMP/legacy-plan.json" <<'PY'
|
|
257
|
+
import json
|
|
258
|
+
import sys
|
|
259
|
+
from pathlib import Path
|
|
260
|
+
|
|
261
|
+
source, target = map(Path, sys.argv[1:])
|
|
262
|
+
plan = json.loads(source.read_text(encoding="utf-8"))
|
|
263
|
+
plan["evidence"] = [
|
|
264
|
+
row
|
|
265
|
+
for row in plan["evidence"]
|
|
266
|
+
if row.get("id") != "review-plan-intent-stable-core-v1"
|
|
267
|
+
]
|
|
268
|
+
target.write_text(json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
269
|
+
PY
|
|
270
|
+
printf ' legacy-append' >"$TMP/legacy-append.txt"
|
|
271
|
+
legacy_before="$(digest "$TMP/legacy-plan.json")"
|
|
272
|
+
set +e
|
|
273
|
+
out="$(python3 "$UPDATER" --plan "$TMP/legacy-plan.json" --append-intent-file "$TMP/legacy-append.txt" --expected-sha256 "$legacy_before" 2>&1)"
|
|
274
|
+
rc=$?
|
|
275
|
+
set -e
|
|
276
|
+
assert_rc "$rc" 0 "legacy plan append must remain compatible"
|
|
277
|
+
legacy_appended="$(digest "$TMP/legacy-plan.json")"
|
|
278
|
+
python3 - "$TMP/legacy-plan.json" "$TMP/legacy-core.txt" "$TMP/legacy-latest.txt" <<'PY'
|
|
279
|
+
import json
|
|
280
|
+
import sys
|
|
281
|
+
from pathlib import Path
|
|
282
|
+
|
|
283
|
+
plan_path, core_path, latest_path = map(Path, sys.argv[1:])
|
|
284
|
+
intent = json.loads(plan_path.read_text(encoding="utf-8"))["intent"]
|
|
285
|
+
core_path.write_text(intent[:100], encoding="utf-8")
|
|
286
|
+
latest_path.write_text("a genuinely new legacy transition", encoding="utf-8")
|
|
287
|
+
PY
|
|
288
|
+
set +e
|
|
289
|
+
out="$(python3 "$UPDATER" --plan "$TMP/legacy-plan.json" --compact-core-intent-file "$TMP/legacy-core.txt" --latest-intent-file "$TMP/legacy-latest.txt" --expected-sha256 "$legacy_appended" 2>&1)"
|
|
290
|
+
rc=$?
|
|
291
|
+
set -e
|
|
292
|
+
assert_rc "$rc" 2 "legacy plan compaction without identity must fail closed"
|
|
293
|
+
assert_contains "intent_core_identity_missing" "$out" "missing stable-core identity reason"
|
|
294
|
+
[ "$(digest "$TMP/legacy-plan.json")" = "$legacy_appended" ] || fail "identity-free compaction changed the legacy plan"
|
|
295
|
+
|
|
296
|
+
# Pin the exact documented character boundary. A 4001-character append must not
|
|
297
|
+
# change the last valid 4000-character plan.
|
|
298
|
+
python3 - "$APPEND" <<'PY'
|
|
299
|
+
import sys
|
|
300
|
+
from pathlib import Path
|
|
301
|
+
|
|
302
|
+
Path(sys.argv[1]).write_text("z" * 108, encoding="utf-8")
|
|
303
|
+
PY
|
|
304
|
+
expected="$(digest "$PLAN")"
|
|
305
|
+
python3 "$UPDATER" --plan "$PLAN" --append-intent-file "$APPEND" --expected-sha256 "$expected" >/dev/null
|
|
306
|
+
valid_4000="$(digest "$PLAN")"
|
|
307
|
+
printf 'q' >"$APPEND"
|
|
308
|
+
set +e
|
|
309
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --append-intent-file "$APPEND" --expected-sha256 "$valid_4000" 2>&1)"
|
|
310
|
+
rc=$?
|
|
311
|
+
set -e
|
|
312
|
+
assert_rc "$rc" 2 "4001-character append must fail closed"
|
|
313
|
+
assert_contains "intent_append_overflow" "$out" "append overflow reason"
|
|
314
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "append overflow changed the plan"
|
|
315
|
+
|
|
316
|
+
# A prefix is not a stable core merely because it happens to match. The plan's
|
|
317
|
+
# persisted identity binds compaction to the exact core chosen before history
|
|
318
|
+
# accumulated.
|
|
319
|
+
cp "$PLAN" "$TMP/arbitrary-prefix-plan.json"
|
|
320
|
+
python3 - "$TMP/arbitrary-prefix-plan.json" "$TMP/arbitrary-prefix-core.txt" "$TMP/arbitrary-prefix-latest.txt" <<'PY'
|
|
321
|
+
import json
|
|
322
|
+
import sys
|
|
323
|
+
from pathlib import Path
|
|
324
|
+
|
|
325
|
+
plan_path, core_path, latest_path = map(Path, sys.argv[1:])
|
|
326
|
+
intent = json.loads(plan_path.read_text(encoding="utf-8"))["intent"]
|
|
327
|
+
core_path.write_text(intent[:100], encoding="utf-8")
|
|
328
|
+
latest_path.write_text("a novel transition absent from the old intent", encoding="utf-8")
|
|
329
|
+
PY
|
|
330
|
+
prefix_before="$(digest "$TMP/arbitrary-prefix-plan.json")"
|
|
331
|
+
set +e
|
|
332
|
+
out="$(python3 "$UPDATER" --plan "$TMP/arbitrary-prefix-plan.json" --compact-core-intent-file "$TMP/arbitrary-prefix-core.txt" --latest-intent-file "$TMP/arbitrary-prefix-latest.txt" --expected-sha256 "$prefix_before" 2>&1)"
|
|
333
|
+
rc=$?
|
|
334
|
+
set -e
|
|
335
|
+
assert_rc "$rc" 2 "an arbitrary matching prefix must not redefine stable core"
|
|
336
|
+
assert_contains "intent_core_identity_mismatch" "$out" "stable-core identity mismatch reason"
|
|
337
|
+
[ "$(digest "$TMP/arbitrary-prefix-plan.json")" = "$prefix_before" ] || fail "arbitrary-prefix refusal changed the plan"
|
|
338
|
+
|
|
339
|
+
# A digest that was already stale when opened must fail before replacement.
|
|
340
|
+
python3 - "$LATEST" <<'PY'
|
|
341
|
+
import sys
|
|
342
|
+
from pathlib import Path
|
|
343
|
+
|
|
344
|
+
Path(sys.argv[1]).write_text("new compact intent", encoding="utf-8")
|
|
345
|
+
PY
|
|
346
|
+
set +e
|
|
347
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$(printf '0%.0s' {1..64})" 2>&1)"
|
|
348
|
+
rc=$?
|
|
349
|
+
set -e
|
|
350
|
+
assert_rc "$rc" 2 "stale expected digest must fail closed"
|
|
351
|
+
assert_contains "plan_digest_mismatch" "$out" "stale-input mismatch reason"
|
|
352
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "stale-input mismatch changed the plan"
|
|
353
|
+
|
|
354
|
+
# An intent hand-rewritten outside the helper still starts with the supplied
|
|
355
|
+
# core, but its first core_chars characters no longer hash to the persisted
|
|
356
|
+
# identity; compaction must refuse rather than adopt the rewritten boundary.
|
|
357
|
+
cp "$PLAN" "$TMP/identity-drift-plan.json"
|
|
358
|
+
python3 - "$TMP/identity-drift-plan.json" "$TMP/identity-drift-core.txt" <<'PY'
|
|
359
|
+
import json
|
|
360
|
+
import re
|
|
361
|
+
import sys
|
|
362
|
+
from pathlib import Path
|
|
363
|
+
|
|
364
|
+
plan_path, core_path = map(Path, sys.argv[1:])
|
|
365
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
366
|
+
identity = next(
|
|
367
|
+
row["result"]
|
|
368
|
+
for row in plan["evidence"]
|
|
369
|
+
if row.get("id") == "review-plan-intent-stable-core-v1"
|
|
370
|
+
)
|
|
371
|
+
chars = int(re.search(r"chars=([0-9]+)", identity).group(1))
|
|
372
|
+
rewritten = "x" * chars + " freshly rewritten tail"
|
|
373
|
+
assert len(rewritten) <= 4000
|
|
374
|
+
plan["intent"] = rewritten
|
|
375
|
+
core_path.write_text(rewritten[:chars], encoding="utf-8")
|
|
376
|
+
plan_path.write_text(
|
|
377
|
+
json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
378
|
+
)
|
|
379
|
+
PY
|
|
380
|
+
printf 'a novel transition for identity drift' >"$TMP/identity-drift-latest.txt"
|
|
381
|
+
drift_before="$(digest "$TMP/identity-drift-plan.json")"
|
|
382
|
+
set +e
|
|
383
|
+
out="$(python3 "$UPDATER" --plan "$TMP/identity-drift-plan.json" --compact-core-intent-file "$TMP/identity-drift-core.txt" --latest-intent-file "$TMP/identity-drift-latest.txt" --expected-sha256 "$drift_before" 2>&1)"
|
|
384
|
+
rc=$?
|
|
385
|
+
set -e
|
|
386
|
+
assert_rc "$rc" 2 "a rewritten intent must not satisfy the persisted identity"
|
|
387
|
+
assert_contains "plan_core_identity_mismatch" "$out" "persisted-identity drift reason"
|
|
388
|
+
[ "$(digest "$TMP/identity-drift-plan.json")" = "$drift_before" ] || fail "identity drift refusal changed the plan"
|
|
389
|
+
|
|
390
|
+
# Zero-loss compaction refuses to overflow the evidence-row cap and leaves the
|
|
391
|
+
# plan byte-identical.
|
|
392
|
+
cp "$PLAN" "$TMP/overflow-rows-plan.json"
|
|
393
|
+
python3 - "$TMP/overflow-rows-plan.json" <<'PY'
|
|
394
|
+
import json
|
|
395
|
+
import sys
|
|
396
|
+
from pathlib import Path
|
|
397
|
+
|
|
398
|
+
plan_path = Path(sys.argv[1])
|
|
399
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
400
|
+
rows = plan["evidence"]
|
|
401
|
+
index = 0
|
|
402
|
+
while len(rows) < 49:
|
|
403
|
+
rows.append(
|
|
404
|
+
{
|
|
405
|
+
"id": f"synthetic-filler-{index:04d}",
|
|
406
|
+
"result": "synthetic filler evidence row exercising the overflow cap",
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
index += 1
|
|
410
|
+
plan_path.write_text(
|
|
411
|
+
json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
412
|
+
)
|
|
413
|
+
PY
|
|
414
|
+
printf 'a novel transition for row overflow' >"$TMP/overflow-rows-latest.txt"
|
|
415
|
+
overflow_rows_before="$(digest "$TMP/overflow-rows-plan.json")"
|
|
416
|
+
set +e
|
|
417
|
+
out="$(python3 "$UPDATER" --plan "$TMP/overflow-rows-plan.json" --compact-core-intent-file "$CORE" --latest-intent-file "$TMP/overflow-rows-latest.txt" --expected-sha256 "$overflow_rows_before" 2>&1)"
|
|
418
|
+
rc=$?
|
|
419
|
+
set -e
|
|
420
|
+
assert_rc "$rc" 2 "history rows beyond the evidence cap must fail closed"
|
|
421
|
+
assert_contains "intent_history_evidence_overflow" "$out" "history-row overflow reason"
|
|
422
|
+
[ "$(digest "$TMP/overflow-rows-plan.json")" = "$overflow_rows_before" ] || fail "row overflow refusal changed the plan"
|
|
423
|
+
|
|
424
|
+
# File transport may contribute one final newline, but arbitrary outer
|
|
425
|
+
# whitespace or repeated blank lines must not be silently stripped into shape.
|
|
426
|
+
python3 - "$PLAN" "$CORE" <<'PY'
|
|
427
|
+
import json
|
|
428
|
+
import sys
|
|
429
|
+
from pathlib import Path
|
|
430
|
+
|
|
431
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
432
|
+
Path(sys.argv[2]).write_text(plan["intent"][:100], encoding="utf-8")
|
|
433
|
+
PY
|
|
434
|
+
printf ' leading intent\n' >"$LATEST"
|
|
435
|
+
set +e
|
|
436
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$valid_4000" 2>&1)"
|
|
437
|
+
rc=$?
|
|
438
|
+
set -e
|
|
439
|
+
assert_rc "$rc" 2 "outer whitespace must be rejected instead of normalized away"
|
|
440
|
+
assert_contains "intent_latest_not_normalized" "$out" "outer-whitespace reason"
|
|
441
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "outer-whitespace refusal changed the plan"
|
|
442
|
+
|
|
443
|
+
printf 'replacement intent\n\n' >"$LATEST"
|
|
444
|
+
set +e
|
|
445
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$valid_4000" 2>&1)"
|
|
446
|
+
rc=$?
|
|
447
|
+
set -e
|
|
448
|
+
assert_rc "$rc" 2 "multiple trailing newlines must not be collapsed silently"
|
|
449
|
+
assert_contains "intent_latest_not_normalized" "$out" "repeated-newline reason"
|
|
450
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "repeated-newline refusal changed the plan"
|
|
451
|
+
|
|
452
|
+
# Structured compaction requires both components, preserves an exact old prefix,
|
|
453
|
+
# and refuses an already-present transition.
|
|
454
|
+
set +e
|
|
455
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --expected-sha256 "$valid_4000" 2>&1)"
|
|
456
|
+
rc=$?
|
|
457
|
+
set -e
|
|
458
|
+
assert_rc "$rc" 2 "core-only compaction must fail closed"
|
|
459
|
+
assert_contains "intent_update_mode_invalid" "$out" "core-only reason"
|
|
460
|
+
|
|
461
|
+
set +e
|
|
462
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --latest-intent-file "$LATEST" --expected-sha256 "$valid_4000" 2>&1)"
|
|
463
|
+
rc=$?
|
|
464
|
+
set -e
|
|
465
|
+
assert_rc "$rc" 2 "latest-only compaction must fail closed"
|
|
466
|
+
assert_contains "intent_update_mode_invalid" "$out" "latest-only reason"
|
|
467
|
+
|
|
468
|
+
printf 'unrelated stable core' >"$CORE"
|
|
469
|
+
printf 'genuinely new transition' >"$LATEST"
|
|
470
|
+
set +e
|
|
471
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$valid_4000" 2>&1)"
|
|
472
|
+
rc=$?
|
|
473
|
+
set -e
|
|
474
|
+
assert_rc "$rc" 2 "unrelated core must fail closed"
|
|
475
|
+
assert_contains "intent_core_not_preserved" "$out" "core-preservation reason"
|
|
476
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "bad-core refusal changed the plan"
|
|
477
|
+
|
|
478
|
+
python3 - "$PLAN" "$CORE" "$LATEST" <<'PY'
|
|
479
|
+
import json
|
|
480
|
+
import sys
|
|
481
|
+
from pathlib import Path
|
|
482
|
+
|
|
483
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
484
|
+
Path(sys.argv[2]).write_text(plan["intent"][:100], encoding="utf-8")
|
|
485
|
+
Path(sys.argv[3]).write_text("latest-round:c28", encoding="utf-8")
|
|
486
|
+
PY
|
|
487
|
+
set +e
|
|
488
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --compact-core-intent-file "$CORE" --latest-intent-file "$LATEST" --expected-sha256 "$valid_4000" 2>&1)"
|
|
489
|
+
rc=$?
|
|
490
|
+
set -e
|
|
491
|
+
assert_rc "$rc" 2 "an already-present transition must fail closed"
|
|
492
|
+
assert_contains "intent_latest_not_new" "$out" "old-transition reason"
|
|
493
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "old-transition refusal changed the plan"
|
|
494
|
+
|
|
495
|
+
# Empty transport after removing one file delimiter is not a state transition.
|
|
496
|
+
for empty_kind in zero lf crlf; do
|
|
497
|
+
case "$empty_kind" in
|
|
498
|
+
zero) : >"$APPEND" ;;
|
|
499
|
+
lf) printf '\n' >"$APPEND" ;;
|
|
500
|
+
crlf) printf '\r\n' >"$APPEND" ;;
|
|
501
|
+
esac
|
|
502
|
+
set +e
|
|
503
|
+
out="$(python3 "$UPDATER" --plan "$PLAN" --append-intent-file "$APPEND" --expected-sha256 "$valid_4000" 2>&1)"
|
|
504
|
+
rc=$?
|
|
505
|
+
set -e
|
|
506
|
+
assert_rc "$rc" 2 "$empty_kind append must fail closed"
|
|
507
|
+
assert_contains "intent_append_empty" "$out" "$empty_kind empty reason"
|
|
508
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "$empty_kind append changed the plan"
|
|
509
|
+
done
|
|
510
|
+
|
|
511
|
+
# The plan itself is opened without following links, and multiply-linked files
|
|
512
|
+
# are rejected before any mutation.
|
|
513
|
+
ln -s "$PLAN" "$TMP/plan-link.json"
|
|
514
|
+
printf 'x' >"$APPEND"
|
|
515
|
+
set +e
|
|
516
|
+
out="$(python3 "$UPDATER" --plan "$TMP/plan-link.json" --append-intent-file "$APPEND" 2>&1)"
|
|
517
|
+
rc=$?
|
|
518
|
+
set -e
|
|
519
|
+
assert_rc "$rc" 2 "symlink plan must fail closed"
|
|
520
|
+
assert_contains "plan_unreadable" "$out" "symlink refusal reason"
|
|
521
|
+
[ "$(digest "$PLAN")" = "$valid_4000" ] || fail "symlink refusal changed the target"
|
|
522
|
+
|
|
523
|
+
cp "$PLAN" "$TMP/hard-plan.json"
|
|
524
|
+
ln "$TMP/hard-plan.json" "$TMP/hard-plan-alias.json"
|
|
525
|
+
set +e
|
|
526
|
+
out="$(python3 "$UPDATER" --plan "$TMP/hard-plan.json" --append-intent-file "$APPEND" 2>&1)"
|
|
527
|
+
rc=$?
|
|
528
|
+
set -e
|
|
529
|
+
assert_rc "$rc" 2 "hardlinked plan must fail closed"
|
|
530
|
+
assert_contains "plan_hardlinked" "$out" "hardlink refusal reason"
|
|
531
|
+
|
|
532
|
+
# Opening a FIFO for reading must not block before the regular-file check.
|
|
533
|
+
mkfifo "$TMP/fifo-plan.json"
|
|
534
|
+
set +e
|
|
535
|
+
out="$(python3 - "$UPDATER" "$TMP/fifo-plan.json" "$APPEND" <<'PY'
|
|
536
|
+
import subprocess
|
|
537
|
+
import sys
|
|
538
|
+
|
|
539
|
+
updater, plan, append = sys.argv[1:]
|
|
540
|
+
try:
|
|
541
|
+
completed = subprocess.run(
|
|
542
|
+
[sys.executable, updater, "--plan", plan, "--append-intent-file", append],
|
|
543
|
+
text=True,
|
|
544
|
+
stdout=subprocess.PIPE,
|
|
545
|
+
stderr=subprocess.STDOUT,
|
|
546
|
+
timeout=1,
|
|
547
|
+
check=False,
|
|
548
|
+
)
|
|
549
|
+
except subprocess.TimeoutExpired as exc:
|
|
550
|
+
raise AssertionError("FIFO plan open blocked") from exc
|
|
551
|
+
assert completed.returncode == 2, completed
|
|
552
|
+
assert "plan_not_regular" in completed.stdout, completed.stdout
|
|
553
|
+
print(completed.stdout, end="")
|
|
554
|
+
PY
|
|
555
|
+
)"
|
|
556
|
+
rc=$?
|
|
557
|
+
set -e
|
|
558
|
+
assert_rc "$rc" 0 "FIFO plan must fail quickly"
|
|
559
|
+
assert_contains "plan_not_regular" "$out" "FIFO regular-file reason"
|
|
560
|
+
|
|
561
|
+
# Character limits count Unicode code points, while the whole plan remains
|
|
562
|
+
# byte-bounded.
|
|
563
|
+
python3 - "$PLAN" "$TMP/unicode-plan.json" <<'PY'
|
|
564
|
+
import json
|
|
565
|
+
import sys
|
|
566
|
+
from pathlib import Path
|
|
567
|
+
|
|
568
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
569
|
+
plan["intent"] = "界" * 3999
|
|
570
|
+
Path(sys.argv[2]).write_text(
|
|
571
|
+
json.dumps(plan, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
572
|
+
)
|
|
573
|
+
PY
|
|
574
|
+
printf '新' >"$APPEND"
|
|
575
|
+
unicode_before="$(digest "$TMP/unicode-plan.json")"
|
|
576
|
+
python3 "$UPDATER" --plan "$TMP/unicode-plan.json" --append-intent-file "$APPEND" --expected-sha256 "$unicode_before" >/dev/null
|
|
577
|
+
python3 - "$TMP/unicode-plan.json" <<'PY'
|
|
578
|
+
import json
|
|
579
|
+
import sys
|
|
580
|
+
from pathlib import Path
|
|
581
|
+
|
|
582
|
+
assert len(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["intent"]) == 4000
|
|
583
|
+
PY
|
|
584
|
+
unicode_4000="$(digest "$TMP/unicode-plan.json")"
|
|
585
|
+
set +e
|
|
586
|
+
out="$(python3 "$UPDATER" --plan "$TMP/unicode-plan.json" --append-intent-file "$APPEND" --expected-sha256 "$unicode_4000" 2>&1)"
|
|
587
|
+
rc=$?
|
|
588
|
+
set -e
|
|
589
|
+
assert_rc "$rc" 2 "4001 Unicode characters must fail closed"
|
|
590
|
+
assert_contains "intent_append_overflow" "$out" "Unicode overflow reason"
|
|
591
|
+
[ "$(digest "$TMP/unicode-plan.json")" = "$unicode_4000" ] || fail "Unicode overflow changed the plan"
|
|
592
|
+
|
|
593
|
+
# A character-valid update that would cross the serialized 32,000-byte plan
|
|
594
|
+
# limit is also a no-write failure.
|
|
595
|
+
python3 - "$PLAN" "$TMP/byte-plan.json" "$APPEND" <<'PY'
|
|
596
|
+
import json
|
|
597
|
+
import sys
|
|
598
|
+
from pathlib import Path
|
|
599
|
+
|
|
600
|
+
source, target, append = map(Path, sys.argv[1:])
|
|
601
|
+
base = json.loads(source.read_text(encoding="utf-8"))
|
|
602
|
+
base["intent"] = "small intent"
|
|
603
|
+
append.write_text("a" * 3000, encoding="utf-8")
|
|
604
|
+
for count in range(1, 51):
|
|
605
|
+
base["evidence"] = [
|
|
606
|
+
{"id": f"evidence-{index}", "result": "r" * 600}
|
|
607
|
+
for index in range(count)
|
|
608
|
+
]
|
|
609
|
+
before = (json.dumps(base, ensure_ascii=False, indent=2) + "\n").encode()
|
|
610
|
+
after_plan = dict(base)
|
|
611
|
+
after_plan["intent"] += "a" * 3000
|
|
612
|
+
after = (json.dumps(after_plan, ensure_ascii=False, indent=2) + "\n").encode()
|
|
613
|
+
if len(before) <= 32_000 < len(after):
|
|
614
|
+
target.write_bytes(before)
|
|
615
|
+
break
|
|
616
|
+
else:
|
|
617
|
+
raise AssertionError("could not construct the 32,000-byte boundary fixture")
|
|
618
|
+
PY
|
|
619
|
+
byte_before="$(digest "$TMP/byte-plan.json")"
|
|
620
|
+
set +e
|
|
621
|
+
out="$(python3 "$UPDATER" --plan "$TMP/byte-plan.json" --append-intent-file "$APPEND" --expected-sha256 "$byte_before" 2>&1)"
|
|
622
|
+
rc=$?
|
|
623
|
+
set -e
|
|
624
|
+
assert_rc "$rc" 2 "serialized plan overflow must fail closed"
|
|
625
|
+
assert_contains "updated_plan_too_large" "$out" "serialized byte-limit reason"
|
|
626
|
+
[ "$(digest "$TMP/byte-plan.json")" = "$byte_before" ] || fail "serialized overflow changed the plan"
|
|
627
|
+
|
|
628
|
+
# A JSON escape may decode to a lone surrogate that cannot be emitted as UTF-8.
|
|
629
|
+
# That is a stable refusal, not a traceback, and leaves the file unchanged.
|
|
630
|
+
python3 - "$PLAN" "$TMP/surrogate-plan.json" <<'PY'
|
|
631
|
+
import json
|
|
632
|
+
import sys
|
|
633
|
+
from pathlib import Path
|
|
634
|
+
|
|
635
|
+
plan = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
636
|
+
plan["intent"] = "valid intent"
|
|
637
|
+
plan["evidence"][0]["result"] = "\ud800"
|
|
638
|
+
Path(sys.argv[2]).write_text(json.dumps(plan, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
|
639
|
+
PY
|
|
640
|
+
printf ' next' >"$APPEND"
|
|
641
|
+
surrogate_before="$(digest "$TMP/surrogate-plan.json")"
|
|
642
|
+
set +e
|
|
643
|
+
out="$(python3 "$UPDATER" --plan "$TMP/surrogate-plan.json" --append-intent-file "$APPEND" --expected-sha256 "$surrogate_before" 2>&1)"
|
|
644
|
+
rc=$?
|
|
645
|
+
set -e
|
|
646
|
+
assert_rc "$rc" 2 "unencodable plan content must fail closed"
|
|
647
|
+
assert_contains "updated_plan_invalid_unicode" "$out" "surrogate reason"
|
|
648
|
+
[ "$(digest "$TMP/surrogate-plan.json")" = "$surrogate_before" ] || fail "surrogate refusal changed the plan"
|
|
649
|
+
|
|
650
|
+
# Pre-commit file-sync and replace faults leave the original target intact and
|
|
651
|
+
# remove every temporary file; no sensitive plan copy survives the failed call.
|
|
652
|
+
python3 - "$UPDATER" "$TMP" <<'PY'
|
|
653
|
+
import importlib.util
|
|
654
|
+
import json
|
|
655
|
+
import os
|
|
656
|
+
import stat
|
|
657
|
+
import sys
|
|
658
|
+
from pathlib import Path
|
|
659
|
+
|
|
660
|
+
updater_path, root = Path(sys.argv[1]), Path(sys.argv[2])
|
|
661
|
+
spec = importlib.util.spec_from_file_location("intent_updater_faults", updater_path)
|
|
662
|
+
module = importlib.util.module_from_spec(spec)
|
|
663
|
+
assert spec.loader is not None
|
|
664
|
+
spec.loader.exec_module(module)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def exercise(kind: str) -> None:
|
|
668
|
+
plan_path = root / f"{kind}-fault-plan.json"
|
|
669
|
+
append_path = root / f"{kind}-fault-append.txt"
|
|
670
|
+
plan = {
|
|
671
|
+
"intent": "initial fault-test intent",
|
|
672
|
+
"acceptance": ["The original target survives a pre-commit fault."],
|
|
673
|
+
"self_review": [],
|
|
674
|
+
"evidence": [{"id": "focused-test", "result": "A detailed pre-commit fault-injection result."}],
|
|
675
|
+
}
|
|
676
|
+
plan_path.write_text(json.dumps(plan, indent=2) + "\n", encoding="utf-8")
|
|
677
|
+
append_path.write_text(" next", encoding="utf-8")
|
|
678
|
+
before = plan_path.read_bytes()
|
|
679
|
+
real_fsync = module.os.fsync
|
|
680
|
+
real_replace = module.os.replace
|
|
681
|
+
if kind == "fsync":
|
|
682
|
+
def fail_regular_sync(fd: int) -> None:
|
|
683
|
+
if stat.S_ISREG(os.fstat(fd).st_mode):
|
|
684
|
+
raise OSError("synthetic file fsync failure")
|
|
685
|
+
real_fsync(fd)
|
|
686
|
+
|
|
687
|
+
module.os.fsync = fail_regular_sync
|
|
688
|
+
else:
|
|
689
|
+
def fail_replace(source: object, target: object) -> None:
|
|
690
|
+
raise OSError("synthetic replace failure")
|
|
691
|
+
|
|
692
|
+
module.os.replace = fail_replace
|
|
693
|
+
try:
|
|
694
|
+
module.run(["--plan", str(plan_path), "--append-intent-file", str(append_path)])
|
|
695
|
+
except module.UpdateError as exc:
|
|
696
|
+
assert exc.reason == "plan_write_failed", exc.reason
|
|
697
|
+
else:
|
|
698
|
+
raise AssertionError(f"{kind} fault did not fail")
|
|
699
|
+
finally:
|
|
700
|
+
module.os.fsync = real_fsync
|
|
701
|
+
module.os.replace = real_replace
|
|
702
|
+
assert plan_path.read_bytes() == before, f"{kind} fault changed the target"
|
|
703
|
+
assert not list(root.glob(f".{plan_path.name}.*")), f"{kind} fault leaked a temporary plan"
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
exercise("fsync")
|
|
707
|
+
exercise("replace")
|
|
708
|
+
PY
|
|
709
|
+
|
|
710
|
+
# Once rename succeeds, a directory-sync failure is a committed-but-unknown
|
|
711
|
+
# durability outcome. It carries the new digest and forbids a blind retry.
|
|
712
|
+
python3 - "$UPDATER" "$TMP/durability-plan.json" "$TMP/durability-append.txt" <<'PY'
|
|
713
|
+
import importlib.util
|
|
714
|
+
import json
|
|
715
|
+
import os
|
|
716
|
+
import stat
|
|
717
|
+
import sys
|
|
718
|
+
from pathlib import Path
|
|
719
|
+
|
|
720
|
+
updater_path, plan_path, append_path = map(Path, sys.argv[1:])
|
|
721
|
+
spec = importlib.util.spec_from_file_location("intent_updater_under_test", updater_path)
|
|
722
|
+
module = importlib.util.module_from_spec(spec)
|
|
723
|
+
assert spec.loader is not None
|
|
724
|
+
spec.loader.exec_module(module)
|
|
725
|
+
plan = {
|
|
726
|
+
"intent": "initial intent",
|
|
727
|
+
"acceptance": ["The candidate behavior is verified."],
|
|
728
|
+
"self_review": [],
|
|
729
|
+
"evidence": [{"id": "focused-test", "result": "A sufficiently detailed result for this fault injection."}],
|
|
730
|
+
}
|
|
731
|
+
plan_path.write_text(json.dumps(plan, indent=2) + "\n", encoding="utf-8")
|
|
732
|
+
append_path.write_text(" next", encoding="utf-8")
|
|
733
|
+
real_fsync = module.os.fsync
|
|
734
|
+
|
|
735
|
+
def fail_directory_sync(fd):
|
|
736
|
+
if stat.S_ISDIR(os.fstat(fd).st_mode):
|
|
737
|
+
raise OSError("synthetic directory fsync failure")
|
|
738
|
+
return real_fsync(fd)
|
|
739
|
+
|
|
740
|
+
module.os.fsync = fail_directory_sync
|
|
741
|
+
try:
|
|
742
|
+
module.run(["--plan", str(plan_path), "--append-intent-file", str(append_path)])
|
|
743
|
+
except module.UpdateError as exc:
|
|
744
|
+
assert exc.reason == "plan_committed_durability_unknown", exc.reason
|
|
745
|
+
assert "sha256=" in exc.detail and "do not retry blindly" in exc.detail
|
|
746
|
+
else:
|
|
747
|
+
raise AssertionError("directory fsync fault did not fail")
|
|
748
|
+
updated = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
749
|
+
assert updated["intent"] == "initial intent next"
|
|
750
|
+
PY
|
|
751
|
+
|
|
752
|
+
# A stdout pipe closed before the flushed success receipt must exit nonzero
|
|
753
|
+
# with plan_committed_receipt_lost while the plan file keeps the committed
|
|
754
|
+
# update; rc 0 with a lost receipt reads as "no update happened".
|
|
755
|
+
python3 - "$UPDATER" "$TMP/closed-pipe-plan.json" "$TMP/closed-pipe-append.txt" <<'PY'
|
|
756
|
+
import json
|
|
757
|
+
import os
|
|
758
|
+
import subprocess
|
|
759
|
+
import sys
|
|
760
|
+
from pathlib import Path
|
|
761
|
+
|
|
762
|
+
updater_path, plan_path, append_path = map(Path, sys.argv[1:])
|
|
763
|
+
plan = {
|
|
764
|
+
"intent": "initial intent",
|
|
765
|
+
"acceptance": ["The candidate behavior is verified."],
|
|
766
|
+
"self_review": [],
|
|
767
|
+
"evidence": [
|
|
768
|
+
{"id": "focused-test", "result": "A sufficiently detailed result for closed-pipe coverage."}
|
|
769
|
+
],
|
|
770
|
+
}
|
|
771
|
+
plan_path.write_text(json.dumps(plan, indent=2) + "\n", encoding="utf-8")
|
|
772
|
+
append_path.write_text(" next", encoding="utf-8")
|
|
773
|
+
read_fd, write_fd = os.pipe()
|
|
774
|
+
os.close(read_fd)
|
|
775
|
+
result = subprocess.run(
|
|
776
|
+
[sys.executable, str(updater_path), "--plan", str(plan_path), "--append-intent-file", str(append_path)],
|
|
777
|
+
stdout=write_fd,
|
|
778
|
+
stderr=subprocess.PIPE,
|
|
779
|
+
text=True,
|
|
780
|
+
)
|
|
781
|
+
os.close(write_fd)
|
|
782
|
+
assert result.returncode == 2, result.returncode
|
|
783
|
+
assert "plan_committed_receipt_lost" in result.stderr, result.stderr
|
|
784
|
+
assert "re-read the plan" in result.stderr, result.stderr
|
|
785
|
+
committed = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
786
|
+
assert committed["intent"] == "initial intent next", committed["intent"]
|
|
787
|
+
PY
|
|
788
|
+
|
|
789
|
+
echo "test_update_review_plan_intent: ok"
|