@christang/keel 5.51.0 → 5.52.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.52.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.52.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -37,8 +37,8 @@ REQUIRED_SCRIPTS = [
|
|
|
37
37
|
"scripts/validate_plugin.py",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
PACKAGE_VERSION = "5.
|
|
41
|
-
PROTOCOL_VERSION = "5.
|
|
40
|
+
PACKAGE_VERSION = "5.52.0"
|
|
41
|
+
PROTOCOL_VERSION = "5.52.0"
|
|
42
42
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
43
43
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
44
44
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -25401,6 +25401,121 @@ def validate_quoted_marker_is_not_a_disposition_scenario() -> int:
|
|
|
25401
25401
|
return 0
|
|
25402
25402
|
|
|
25403
25403
|
|
|
25404
|
+
# A drift is a hard stop, so whatever the message says is what the author acts
|
|
25405
|
+
# on. Saying only that a value moved produced a wrong record in this repository:
|
|
25406
|
+
# the cause was recorded as a Review edit, which is measurably not covered by
|
|
25407
|
+
# the fingerprint at all.
|
|
25408
|
+
def validate_drift_names_where_to_look_scenario() -> int:
|
|
25409
|
+
label = "drift-names-where-to-look"
|
|
25410
|
+
|
|
25411
|
+
def fixture(root: Path, name: str, anchor_value: str) -> Path:
|
|
25412
|
+
repo = root / name
|
|
25413
|
+
task = strategy_probe_task(strategy="vertical-tdd")
|
|
25414
|
+
# Cite a design statement so the capsule holds two distinct sources.
|
|
25415
|
+
task = task.replace(
|
|
25416
|
+
" - E1: the task proves its own behavior",
|
|
25417
|
+
" - E1: the task proves its own behavior\n - D1",
|
|
25418
|
+
)
|
|
25419
|
+
task = task.replace(
|
|
25420
|
+
" - Contract: pending",
|
|
25421
|
+
f" - Contract: keel-task-capsule/v1 sha256:{anchor_value}",
|
|
25422
|
+
)
|
|
25423
|
+
write_gate_fixture(
|
|
25424
|
+
repo,
|
|
25425
|
+
tasks=task,
|
|
25426
|
+
design=(
|
|
25427
|
+
"## Context\n\nfixture\n\n## Decisions\n\n"
|
|
25428
|
+
"- D1 — the public behavior is proven through the CLI. "
|
|
25429
|
+
"Basis: fixture.\n"
|
|
25430
|
+
),
|
|
25431
|
+
)
|
|
25432
|
+
return repo
|
|
25433
|
+
|
|
25434
|
+
with tempfile.TemporaryDirectory(prefix="keel-drift-") as raw:
|
|
25435
|
+
root = Path(raw)
|
|
25436
|
+
|
|
25437
|
+
# The true anchor, so the matching case can be compared against it.
|
|
25438
|
+
settled = fixture(root, "settled", "0" * 64)
|
|
25439
|
+
started = run_keel(
|
|
25440
|
+
settled, "gate", "task-start", "--change", "demo", "--task", "1.1",
|
|
25441
|
+
"--json", "--no-guard",
|
|
25442
|
+
)
|
|
25443
|
+
true_value = json.loads(started.stdout)["contract"]["fingerprint"]["value"]
|
|
25444
|
+
|
|
25445
|
+
drifted = fixture(root, "drifted", "0" * 64)
|
|
25446
|
+
result = run_keel(drifted, "context", "--change", "demo", "--json")
|
|
25447
|
+
payload = json.loads(result.stdout)
|
|
25448
|
+
if payload.get("status") != "blocked":
|
|
25449
|
+
report(
|
|
25450
|
+
f"{label}: a task whose anchor does not match was not blocked; "
|
|
25451
|
+
f"status {payload.get('status')!r}."
|
|
25452
|
+
)
|
|
25453
|
+
return 1
|
|
25454
|
+
reason = " ".join(str(x) for x in (payload.get("reasons") or []))
|
|
25455
|
+
|
|
25456
|
+
for needed in ("0" * 8, true_value[:8]):
|
|
25457
|
+
if needed not in reason:
|
|
25458
|
+
report(
|
|
25459
|
+
f"{label}: the drift reason no longer reports both "
|
|
25460
|
+
f"fingerprints; {reason!r}."
|
|
25461
|
+
)
|
|
25462
|
+
return 1
|
|
25463
|
+
for source in (
|
|
25464
|
+
"openspec/changes/demo/tasks.md",
|
|
25465
|
+
"openspec/changes/demo/design.md",
|
|
25466
|
+
):
|
|
25467
|
+
if source not in reason:
|
|
25468
|
+
report(
|
|
25469
|
+
f"{label}: the drift reason does not name the authority "
|
|
25470
|
+
f"source {source!r}; {reason!r}."
|
|
25471
|
+
)
|
|
25472
|
+
return 1
|
|
25473
|
+
# The list is the authority set, not the change directory.
|
|
25474
|
+
if "proposal.md" in reason:
|
|
25475
|
+
report(
|
|
25476
|
+
f"{label}: the drift reason names a file the capsule resolved "
|
|
25477
|
+
f"no authority text from; {reason!r}."
|
|
25478
|
+
)
|
|
25479
|
+
return 1
|
|
25480
|
+
lowered = reason.lower()
|
|
25481
|
+
for word in ("evidence", "review", "checkbox"):
|
|
25482
|
+
if word not in lowered:
|
|
25483
|
+
report(
|
|
25484
|
+
f"{label}: the drift reason does not say that {word} is "
|
|
25485
|
+
f"outside the fingerprint; {reason!r}."
|
|
25486
|
+
)
|
|
25487
|
+
return 1
|
|
25488
|
+
# D3: it names a search set, never a culprit.
|
|
25489
|
+
for guess in ("changed field", "the field that changed", "caused by"):
|
|
25490
|
+
if guess in lowered:
|
|
25491
|
+
report(
|
|
25492
|
+
f"{label}: the drift reason claims which field moved, which "
|
|
25493
|
+
f"it cannot know; {reason!r}."
|
|
25494
|
+
)
|
|
25495
|
+
return 1
|
|
25496
|
+
|
|
25497
|
+
matching = fixture(root, "matching", true_value)
|
|
25498
|
+
ok = json.loads(
|
|
25499
|
+
run_keel(matching, "context", "--change", "demo", "--json").stdout
|
|
25500
|
+
)
|
|
25501
|
+
if ok.get("status") != "ready":
|
|
25502
|
+
report(
|
|
25503
|
+
f"{label}: a task whose anchor matches was not ready; "
|
|
25504
|
+
f"{ok.get('status')!r} {ok.get('reasons')!r}."
|
|
25505
|
+
)
|
|
25506
|
+
return 1
|
|
25507
|
+
ok_reason = " ".join(str(x) for x in (ok.get("reasons") or []))
|
|
25508
|
+
if "fingerprint" in ok_reason.lower():
|
|
25509
|
+
report(
|
|
25510
|
+
f"{label}: a matching anchor now reports drift wording; "
|
|
25511
|
+
f"{ok_reason!r}."
|
|
25512
|
+
)
|
|
25513
|
+
return 1
|
|
25514
|
+
|
|
25515
|
+
report(f"{label} scenario passed.")
|
|
25516
|
+
return 0
|
|
25517
|
+
|
|
25518
|
+
|
|
25404
25519
|
# A scenario name, as the registry spells one. Two registered names carry no
|
|
25405
25520
|
# hyphen — `cli` and `uninstall` — so requiring one would leave exactly those
|
|
25406
25521
|
# two unchecked, and allowing single words was measured to add no false
|
|
@@ -25635,6 +25750,7 @@ SCENARIOS: tuple = (
|
|
|
25635
25750
|
("a-strategy-is-declared", validate_strategy_is_declared_scenario),
|
|
25636
25751
|
("the-weakest-strategy-states-its-reason", validate_weakest_strategy_states_its_reason_scenario),
|
|
25637
25752
|
("a-quoted-marker-is-not-a-disposition", validate_quoted_marker_is_not_a_disposition_scenario),
|
|
25753
|
+
("drift-names-where-to-look", validate_drift_names_where_to_look_scenario),
|
|
25638
25754
|
(
|
|
25639
25755
|
"authored-scenario-names-are-registered",
|
|
25640
25756
|
validate_authored_scenario_names_scenario,
|
package/src/core/context.js
CHANGED
|
@@ -85,6 +85,32 @@ function taskHasCompletionEvidence(record, contract) {
|
|
|
85
85
|
);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Two hashes and nothing to search was the whole message, and it produced a
|
|
89
|
+
// wrong record here: an anchor that moved was written up as a Review edit,
|
|
90
|
+
// which is measurably not covered at all. The capsule already knows the answer
|
|
91
|
+
// — every authority entry carries the source its text was resolved from — so
|
|
92
|
+
// the search set is free. What is not free is the field that moved: only the
|
|
93
|
+
// previous fingerprint is retained, not the capsule behind it, so this names
|
|
94
|
+
// where to look and never what changed.
|
|
95
|
+
function driftSearchSet(contract) {
|
|
96
|
+
const sources = [
|
|
97
|
+
...new Set(
|
|
98
|
+
(contract.capsule.authority || [])
|
|
99
|
+
.map((entry) => String(entry.source || "").split("#")[0])
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
),
|
|
102
|
+
];
|
|
103
|
+
const where = sources.length > 0
|
|
104
|
+
? `The fingerprint covers text resolved from: ${sources.join(", ")}.`
|
|
105
|
+
: "The fingerprint covers this task's own resolved authority text.";
|
|
106
|
+
return (
|
|
107
|
+
`${where} Evidence, Review, and the task checkbox are not covered — `
|
|
108
|
+
+ "editing them does not move it. Reauthorize by re-running "
|
|
109
|
+
+ "`keel gate task-start` and recording the new anchor, after confirming "
|
|
110
|
+
+ "the change to the authority above was intended."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
function taskSelection(repo, change, record, source, requestedAction = null) {
|
|
89
115
|
const tasksPath = path.join(repo, "openspec", "changes", change, "tasks.md");
|
|
90
116
|
const contract = compileTaskContract(repo, change, record);
|
|
@@ -100,7 +126,8 @@ function taskSelection(repo, change, record, source, requestedAction = null) {
|
|
|
100
126
|
if (anchor && anchor !== contract.fingerprint.value) {
|
|
101
127
|
return blocked(
|
|
102
128
|
`Task contract fingerprint drift for ${change}#${record.id}: recorded `
|
|
103
|
-
+ `sha256:${anchor}, current sha256:${contract.fingerprint.value}
|
|
129
|
+
+ `sha256:${anchor}, current sha256:${contract.fingerprint.value}. `
|
|
130
|
+
+ `${driftSearchSet(contract)}`,
|
|
104
131
|
[relativePath(repo, tasksPath)]
|
|
105
132
|
);
|
|
106
133
|
}
|