@christang/keel 5.4.0 → 5.6.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 +73 -2
- package/assets/bootstrap/AGENTS.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/schema.yaml +6 -2
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +8 -4
- package/bin/keel.js +79 -1
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/scripts/session-start.js +57 -0
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +31 -0
- package/scripts/validate_plugin.py +855 -2
- package/src/core/config.js +105 -0
- package/src/core/task-contract.js +19 -0
|
@@ -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.6.0"
|
|
41
|
+
PROTOCOL_VERSION = "5.6.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
|
|
@@ -2087,6 +2087,12 @@ def assert_openspec_overlay(path: Path, action: str) -> str | None:
|
|
|
2087
2087
|
"source expectations",
|
|
2088
2088
|
"Rough future slices",
|
|
2089
2089
|
"cannot mark tasks complete",
|
|
2090
|
+
# A confirmation the owner already declared is routed to the
|
|
2091
|
+
# declaration; one they did not declare is still asked for, and
|
|
2092
|
+
# neither case touches the proof.
|
|
2093
|
+
"standing-authorized action proceeds without",
|
|
2094
|
+
"undeclared action still requires",
|
|
2095
|
+
"never substitutes for a gate",
|
|
2090
2096
|
]
|
|
2091
2097
|
)
|
|
2092
2098
|
else:
|
|
@@ -2098,6 +2104,8 @@ def assert_openspec_overlay(path: Path, action: str) -> str | None:
|
|
|
2098
2104
|
"durable follow-up owner",
|
|
2099
2105
|
"explicit discard reason",
|
|
2100
2106
|
"cannot archive, sync, change acceptance, or bypass completion gates",
|
|
2107
|
+
"standing-authorizes `archive`",
|
|
2108
|
+
"completion gate and follow-up ownership checks still run",
|
|
2101
2109
|
]
|
|
2102
2110
|
)
|
|
2103
2111
|
for snippet in required:
|
|
@@ -11111,6 +11119,829 @@ def validate_verification_layering_docs_scenario() -> int:
|
|
|
11111
11119
|
return 0
|
|
11112
11120
|
|
|
11113
11121
|
|
|
11122
|
+
STANDING_AUTHORIZATION_ACTIONS = ("commit", "push", "release", "archive")
|
|
11123
|
+
|
|
11124
|
+
|
|
11125
|
+
def write_authorize_config(repo: Path, body: str) -> None:
|
|
11126
|
+
(repo / "keel").mkdir(parents=True, exist_ok=True)
|
|
11127
|
+
(repo / "keel" / "config.yaml").write_text(body, encoding="utf-8")
|
|
11128
|
+
|
|
11129
|
+
|
|
11130
|
+
def validate_standing_authorization_declaration_scenario() -> int:
|
|
11131
|
+
with tempfile.TemporaryDirectory(prefix="keel-authorize-") as raw_tmp:
|
|
11132
|
+
root = Path(raw_tmp)
|
|
11133
|
+
|
|
11134
|
+
# M1 — a declared action is authorized; an undeclared one is not.
|
|
11135
|
+
declared = root / "declared"
|
|
11136
|
+
declared.mkdir()
|
|
11137
|
+
write_authorize_config(
|
|
11138
|
+
declared,
|
|
11139
|
+
"fast_check: echo declared-check\n"
|
|
11140
|
+
"authorize:\n"
|
|
11141
|
+
" - commit\n"
|
|
11142
|
+
" - push\n",
|
|
11143
|
+
)
|
|
11144
|
+
out = run_keel(declared, "--doctor").stdout
|
|
11145
|
+
if "Standing authorization:" not in out:
|
|
11146
|
+
report("standing-authorization: doctor has no standing authorization surface.")
|
|
11147
|
+
report(out)
|
|
11148
|
+
return 1
|
|
11149
|
+
for needle in ("commit: authorized", "push: authorized"):
|
|
11150
|
+
if needle not in out:
|
|
11151
|
+
report(f"standing-authorization: declared action not reported: {needle}")
|
|
11152
|
+
report(out)
|
|
11153
|
+
return 1
|
|
11154
|
+
for needle in ("release: not authorized", "archive: not authorized"):
|
|
11155
|
+
if needle not in out:
|
|
11156
|
+
report(f"standing-authorization: undeclared action not reported: {needle}")
|
|
11157
|
+
report(out)
|
|
11158
|
+
return 1
|
|
11159
|
+
|
|
11160
|
+
# M2 — absent, blockless, and empty declarations all authorize nothing,
|
|
11161
|
+
# and none of them disturbs the fast_check surface that shares the file.
|
|
11162
|
+
absent = root / "absent"
|
|
11163
|
+
absent.mkdir()
|
|
11164
|
+
blockless = root / "blockless"
|
|
11165
|
+
blockless.mkdir()
|
|
11166
|
+
write_authorize_config(blockless, "fast_check: echo blockless-check\n")
|
|
11167
|
+
empty = root / "empty"
|
|
11168
|
+
empty.mkdir()
|
|
11169
|
+
write_authorize_config(
|
|
11170
|
+
empty, "fast_check: echo empty-check\nauthorize:\n"
|
|
11171
|
+
)
|
|
11172
|
+
for repo, label, fast in (
|
|
11173
|
+
(absent, "absent", None),
|
|
11174
|
+
(blockless, "blockless", "echo blockless-check"),
|
|
11175
|
+
(empty, "empty", "echo empty-check"),
|
|
11176
|
+
):
|
|
11177
|
+
out = run_keel(repo, "--doctor").stdout
|
|
11178
|
+
if "authorize: none" not in out:
|
|
11179
|
+
report(
|
|
11180
|
+
f"standing-authorization: {label} repo does not report an "
|
|
11181
|
+
"undeclared authorization surface."
|
|
11182
|
+
)
|
|
11183
|
+
report(out)
|
|
11184
|
+
return 1
|
|
11185
|
+
if "authorized" in out.replace("not authorized", ""):
|
|
11186
|
+
report(
|
|
11187
|
+
f"standing-authorization: {label} repo reports an authorized action."
|
|
11188
|
+
)
|
|
11189
|
+
report(out)
|
|
11190
|
+
return 1
|
|
11191
|
+
expected_fast = f"fast_check: ok - declared in keel/config.yaml: {fast}"
|
|
11192
|
+
if fast is not None and expected_fast not in out:
|
|
11193
|
+
report(
|
|
11194
|
+
f"standing-authorization: {label} repo lost its fast_check line."
|
|
11195
|
+
)
|
|
11196
|
+
report(out)
|
|
11197
|
+
return 1
|
|
11198
|
+
if fast is None and "fast_check: none" not in out:
|
|
11199
|
+
report("standing-authorization: absent repo lost its fast_check line.")
|
|
11200
|
+
report(out)
|
|
11201
|
+
return 1
|
|
11202
|
+
|
|
11203
|
+
# M3 — an unrecognized name is reported with the accepted set, exits
|
|
11204
|
+
# non-zero, and authorizes nothing that sits beside it.
|
|
11205
|
+
unknown = root / "unknown"
|
|
11206
|
+
unknown.mkdir()
|
|
11207
|
+
write_authorize_config(
|
|
11208
|
+
unknown,
|
|
11209
|
+
"authorize:\n - commit\n - deploy\n",
|
|
11210
|
+
)
|
|
11211
|
+
result = run_keel(unknown, "--doctor")
|
|
11212
|
+
combined = result.stdout + result.stderr
|
|
11213
|
+
if result.returncode == 0:
|
|
11214
|
+
report("standing-authorization: an unrecognized action name exited zero.")
|
|
11215
|
+
report(combined)
|
|
11216
|
+
return 1
|
|
11217
|
+
if "deploy" not in combined:
|
|
11218
|
+
report("standing-authorization: the error does not name the offending entry.")
|
|
11219
|
+
report(combined)
|
|
11220
|
+
return 1
|
|
11221
|
+
for action in STANDING_AUTHORIZATION_ACTIONS:
|
|
11222
|
+
if action not in combined:
|
|
11223
|
+
report(
|
|
11224
|
+
"standing-authorization: the error does not name accepted "
|
|
11225
|
+
f"action {action}."
|
|
11226
|
+
)
|
|
11227
|
+
report(combined)
|
|
11228
|
+
return 1
|
|
11229
|
+
if "commit: authorized" in combined:
|
|
11230
|
+
report(
|
|
11231
|
+
"standing-authorization: a rejected declaration still authorized "
|
|
11232
|
+
"the entry beside the bad one."
|
|
11233
|
+
)
|
|
11234
|
+
report(combined)
|
|
11235
|
+
return 1
|
|
11236
|
+
|
|
11237
|
+
report("standing-authorization-declaration scenario passed.")
|
|
11238
|
+
return 0
|
|
11239
|
+
|
|
11240
|
+
|
|
11241
|
+
def standing_authorization_task(boundary: str = "") -> str:
|
|
11242
|
+
return (
|
|
11243
|
+
"- [ ] 1.1 Behavior\n"
|
|
11244
|
+
" - Covers:\n"
|
|
11245
|
+
" - E1: public behavior\n"
|
|
11246
|
+
" - Touch:\n"
|
|
11247
|
+
" - src/feature.js\n"
|
|
11248
|
+
" - Verify:\n"
|
|
11249
|
+
" - Strategy: evidence-first\n"
|
|
11250
|
+
" - M1: node test.js proves the public behavior\n"
|
|
11251
|
+
+ boundary
|
|
11252
|
+
+ " - Evidence:\n"
|
|
11253
|
+
" - Contract: pending\n"
|
|
11254
|
+
" - M1: pending\n"
|
|
11255
|
+
" - Review:\n"
|
|
11256
|
+
" - Status: pending\n"
|
|
11257
|
+
" - Acceptance check: pending\n"
|
|
11258
|
+
" - Scope check: pending\n"
|
|
11259
|
+
" - Findings: pending\n"
|
|
11260
|
+
" - Blocker: none\n"
|
|
11261
|
+
)
|
|
11262
|
+
|
|
11263
|
+
|
|
11264
|
+
def standing_authorization_autonomy(repo: Path) -> list[str] | None:
|
|
11265
|
+
result = run_keel(
|
|
11266
|
+
repo,
|
|
11267
|
+
"gate",
|
|
11268
|
+
"task-start",
|
|
11269
|
+
"--change",
|
|
11270
|
+
"demo",
|
|
11271
|
+
"--task",
|
|
11272
|
+
"1.1",
|
|
11273
|
+
"--json",
|
|
11274
|
+
"--no-guard",
|
|
11275
|
+
)
|
|
11276
|
+
try:
|
|
11277
|
+
payload = json.loads(result.stdout)
|
|
11278
|
+
except json.JSONDecodeError:
|
|
11279
|
+
return None
|
|
11280
|
+
contract = payload.get("contract") or {}
|
|
11281
|
+
capsule = contract.get("capsule") or {}
|
|
11282
|
+
boundaries = capsule.get("boundaries") or {}
|
|
11283
|
+
return boundaries.get("autonomy")
|
|
11284
|
+
|
|
11285
|
+
|
|
11286
|
+
def validate_standing_authorization_inheritance_scenario() -> int:
|
|
11287
|
+
with tempfile.TemporaryDirectory(prefix="keel-authinherit-") as raw_tmp:
|
|
11288
|
+
root = Path(raw_tmp)
|
|
11289
|
+
|
|
11290
|
+
# M1 — a task that authored no boundary inherits the declaration, and
|
|
11291
|
+
# the capsule says where the authorization came from.
|
|
11292
|
+
inherits = root / "inherits"
|
|
11293
|
+
inherits.mkdir()
|
|
11294
|
+
write_gate_fixture(inherits, standing_authorization_task())
|
|
11295
|
+
write_authorize_config(inherits, "authorize:\n - commit\n")
|
|
11296
|
+
autonomy = standing_authorization_autonomy(inherits)
|
|
11297
|
+
if autonomy is None:
|
|
11298
|
+
report("standing-authorization-inheritance: task-start returned no capsule autonomy.")
|
|
11299
|
+
return 1
|
|
11300
|
+
inherited = [entry for entry in autonomy if "commit" in entry]
|
|
11301
|
+
if not inherited:
|
|
11302
|
+
report(
|
|
11303
|
+
"standing-authorization-inheritance: a declared action did not "
|
|
11304
|
+
f"reach the capsule autonomy boundary: {autonomy}"
|
|
11305
|
+
)
|
|
11306
|
+
return 1
|
|
11307
|
+
if not any("keel/config.yaml" in entry for entry in inherited):
|
|
11308
|
+
report(
|
|
11309
|
+
"standing-authorization-inheritance: the inherited entry does "
|
|
11310
|
+
f"not name the repository declaration as its source: {autonomy}"
|
|
11311
|
+
)
|
|
11312
|
+
return 1
|
|
11313
|
+
|
|
11314
|
+
# M2 — an authored boundary is returned unchanged, with nothing
|
|
11315
|
+
# inherited beside it.
|
|
11316
|
+
authored = root / "authored"
|
|
11317
|
+
authored.mkdir()
|
|
11318
|
+
write_gate_fixture(
|
|
11319
|
+
authored,
|
|
11320
|
+
standing_authorization_task(
|
|
11321
|
+
" - Autonomy boundary:\n"
|
|
11322
|
+
" - Default: hard-stop\n"
|
|
11323
|
+
" - Pre-authorized fallback: revert the fixture file and record M1\n"
|
|
11324
|
+
),
|
|
11325
|
+
)
|
|
11326
|
+
write_authorize_config(authored, "authorize:\n - commit\n - push\n")
|
|
11327
|
+
autonomy = standing_authorization_autonomy(authored)
|
|
11328
|
+
if autonomy is None:
|
|
11329
|
+
report("standing-authorization-inheritance: authored-boundary task did not compile.")
|
|
11330
|
+
return 1
|
|
11331
|
+
if "Pre-authorized fallback: revert the fixture file and record M1" not in autonomy:
|
|
11332
|
+
report(
|
|
11333
|
+
"standing-authorization-inheritance: the authored boundary was "
|
|
11334
|
+
f"not preserved: {autonomy}"
|
|
11335
|
+
)
|
|
11336
|
+
return 1
|
|
11337
|
+
if any("keel/config.yaml" in entry for entry in autonomy):
|
|
11338
|
+
report(
|
|
11339
|
+
"standing-authorization-inheritance: the declaration overrode an "
|
|
11340
|
+
f"authored boundary: {autonomy}"
|
|
11341
|
+
)
|
|
11342
|
+
return 1
|
|
11343
|
+
|
|
11344
|
+
# M3 — an action the declaration does not name still hard-stops.
|
|
11345
|
+
autonomy = standing_authorization_autonomy(inherits)
|
|
11346
|
+
if autonomy is None:
|
|
11347
|
+
report("standing-authorization-inheritance: re-compilation returned no autonomy.")
|
|
11348
|
+
return 1
|
|
11349
|
+
if any("push" in entry or "release" in entry for entry in autonomy):
|
|
11350
|
+
report(
|
|
11351
|
+
"standing-authorization-inheritance: an undeclared action was "
|
|
11352
|
+
f"authorized: {autonomy}"
|
|
11353
|
+
)
|
|
11354
|
+
return 1
|
|
11355
|
+
if not any(entry.startswith("Default: hard-stop") for entry in autonomy):
|
|
11356
|
+
report(
|
|
11357
|
+
"standing-authorization-inheritance: the hard-stop default "
|
|
11358
|
+
f"disappeared for undeclared actions: {autonomy}"
|
|
11359
|
+
)
|
|
11360
|
+
return 1
|
|
11361
|
+
|
|
11362
|
+
report("standing-authorization-inheritance scenario passed.")
|
|
11363
|
+
return 0
|
|
11364
|
+
|
|
11365
|
+
|
|
11366
|
+
def write_precedent(
|
|
11367
|
+
store: Path,
|
|
11368
|
+
name: str,
|
|
11369
|
+
*,
|
|
11370
|
+
category: str = "external interface",
|
|
11371
|
+
status: str = "recorded",
|
|
11372
|
+
decision: str = "Return 404 rather than 200 with an empty body.",
|
|
11373
|
+
rationale: str | None = "A 200 teaches every caller to parse the body to learn it failed.",
|
|
11374
|
+
) -> None:
|
|
11375
|
+
store.mkdir(parents=True, exist_ok=True)
|
|
11376
|
+
body = (
|
|
11377
|
+
f"# {name}\n\n"
|
|
11378
|
+
f"Applies when: a handler must report that a resource is absent.\n\n"
|
|
11379
|
+
f"- Category: {category}\n"
|
|
11380
|
+
f"- Status: {status}\n\n"
|
|
11381
|
+
"## Decision\n\n"
|
|
11382
|
+
f"{decision}\n"
|
|
11383
|
+
)
|
|
11384
|
+
if rationale is not None:
|
|
11385
|
+
body += f"\n## Rationale\n\n{rationale}\n"
|
|
11386
|
+
(store / f"{name}.md").write_text(body, encoding="utf-8")
|
|
11387
|
+
|
|
11388
|
+
|
|
11389
|
+
def validate_precedent_store_declaration_scenario() -> int:
|
|
11390
|
+
with tempfile.TemporaryDirectory(prefix="keel-precedent-") as raw_tmp:
|
|
11391
|
+
root = Path(raw_tmp)
|
|
11392
|
+
|
|
11393
|
+
# A store deliberately placed OUTSIDE every repository that reads it.
|
|
11394
|
+
shared = root / "shared-store"
|
|
11395
|
+
write_precedent(shared, "absent-resource-status")
|
|
11396
|
+
write_precedent(shared, "irreversible-cost", status="authorized")
|
|
11397
|
+
|
|
11398
|
+
def declare(repo: Path, store: str | None) -> None:
|
|
11399
|
+
(repo / "keel").mkdir(parents=True, exist_ok=True)
|
|
11400
|
+
body = "fast_check: echo check\n"
|
|
11401
|
+
if store is not None:
|
|
11402
|
+
body += f"precedents: {store}\n"
|
|
11403
|
+
(repo / "keel" / "config.yaml").write_text(body, encoding="utf-8")
|
|
11404
|
+
|
|
11405
|
+
# M1 — a declared, existing store is reported with its counts.
|
|
11406
|
+
declared = root / "declared"
|
|
11407
|
+
declared.mkdir()
|
|
11408
|
+
declare(declared, str(shared).replace("\\", "/"))
|
|
11409
|
+
out = run_keel(declared, "--doctor").stdout
|
|
11410
|
+
if "Precedent store:" not in out:
|
|
11411
|
+
report("precedent-store: doctor has no precedent surface.")
|
|
11412
|
+
report(out)
|
|
11413
|
+
return 1
|
|
11414
|
+
for needle in ("precedents: 2", "authorized: 1"):
|
|
11415
|
+
if needle not in out:
|
|
11416
|
+
report(f"precedent-store: doctor does not report {needle}.")
|
|
11417
|
+
report(out)
|
|
11418
|
+
return 1
|
|
11419
|
+
|
|
11420
|
+
# M1 (continued) — an undeclared store leaves every surface alone.
|
|
11421
|
+
silent = root / "silent"
|
|
11422
|
+
silent.mkdir()
|
|
11423
|
+
declare(silent, None)
|
|
11424
|
+
silent_out = run_keel(silent, "--doctor").stdout
|
|
11425
|
+
if "precedents: none" not in silent_out:
|
|
11426
|
+
report("precedent-store: an undeclared store is not reported as none.")
|
|
11427
|
+
report(silent_out)
|
|
11428
|
+
return 1
|
|
11429
|
+
if "fast_check: ok - declared in keel/config.yaml: echo check" not in silent_out:
|
|
11430
|
+
report("precedent-store: the fast_check surface changed.")
|
|
11431
|
+
report(silent_out)
|
|
11432
|
+
return 1
|
|
11433
|
+
|
|
11434
|
+
# M2 — two repositories declaring the same out-of-tree path read the
|
|
11435
|
+
# same precedents, which is the whole point of a declarable path.
|
|
11436
|
+
second = root / "second"
|
|
11437
|
+
second.mkdir()
|
|
11438
|
+
declare(second, str(shared).replace("\\", "/"))
|
|
11439
|
+
second_out = run_keel(second, "--doctor").stdout
|
|
11440
|
+
if "precedents: 2" not in second_out or "authorized: 1" not in second_out:
|
|
11441
|
+
report("precedent-store: a second repo did not read the shared store.")
|
|
11442
|
+
report(second_out)
|
|
11443
|
+
return 1
|
|
11444
|
+
|
|
11445
|
+
# M2 (continued) — a declared path that does not exist degrades to the
|
|
11446
|
+
# no-store behavior. This is the state CI and every clone land in.
|
|
11447
|
+
missing = root / "missing"
|
|
11448
|
+
missing.mkdir()
|
|
11449
|
+
declare(missing, str(root / "not-here").replace("\\", "/"))
|
|
11450
|
+
missing_result = run_keel(missing, "--doctor")
|
|
11451
|
+
if "precedents: none" not in missing_result.stdout:
|
|
11452
|
+
report("precedent-store: a missing store path did not degrade to none.")
|
|
11453
|
+
report(missing_result.stdout)
|
|
11454
|
+
return 1
|
|
11455
|
+
if missing_result.returncode != run_keel(silent, "--doctor").returncode:
|
|
11456
|
+
report("precedent-store: a missing store path changed the doctor exit code.")
|
|
11457
|
+
return 1
|
|
11458
|
+
|
|
11459
|
+
# M3 — completeness is a presence check, not a judgement.
|
|
11460
|
+
incomplete_store = root / "incomplete-store"
|
|
11461
|
+
write_precedent(incomplete_store, "no-reason", rationale=None)
|
|
11462
|
+
incomplete = root / "incomplete"
|
|
11463
|
+
incomplete.mkdir()
|
|
11464
|
+
declare(incomplete, str(incomplete_store).replace("\\", "/"))
|
|
11465
|
+
out = run_keel(incomplete, "--doctor").stdout
|
|
11466
|
+
if "incomplete: 1" not in out or "no-reason" not in out:
|
|
11467
|
+
report("precedent-store: a precedent with no rationale was not named incomplete.")
|
|
11468
|
+
report(out)
|
|
11469
|
+
return 1
|
|
11470
|
+
|
|
11471
|
+
opaque_store = root / "opaque-store"
|
|
11472
|
+
write_precedent(opaque_store, "unevaluable", rationale="qqq")
|
|
11473
|
+
opaque = root / "opaque"
|
|
11474
|
+
opaque.mkdir()
|
|
11475
|
+
declare(opaque, str(opaque_store).replace("\\", "/"))
|
|
11476
|
+
out = run_keel(opaque, "--doctor").stdout
|
|
11477
|
+
if "incomplete: 0" not in out:
|
|
11478
|
+
report(
|
|
11479
|
+
"precedent-store: a rationale Keel cannot evaluate was reported "
|
|
11480
|
+
"incomplete; the check must be presence, not judgement."
|
|
11481
|
+
)
|
|
11482
|
+
report(out)
|
|
11483
|
+
return 1
|
|
11484
|
+
|
|
11485
|
+
# M4 — reading a store performs no network access.
|
|
11486
|
+
#
|
|
11487
|
+
# Proxy environment variables do NOT prove this: Node's fetch ignores
|
|
11488
|
+
# HTTP_PROXY entirely, so a run under them passes whether or not the
|
|
11489
|
+
# code reaches the network. Instead, preload a module that makes every
|
|
11490
|
+
# network primitive throw. Then a passing run is evidence that none was
|
|
11491
|
+
# called, and any added network call fails loudly.
|
|
11492
|
+
guard = root / "no-network.cjs"
|
|
11493
|
+
guard.write_text(
|
|
11494
|
+
"const fail = (what) => {\n"
|
|
11495
|
+
" throw new Error('network attempted: ' + what);\n"
|
|
11496
|
+
"};\n"
|
|
11497
|
+
"require('net').Socket.prototype.connect = () => fail('net.connect');\n"
|
|
11498
|
+
"const http = require('http');\n"
|
|
11499
|
+
"http.request = () => fail('http.request');\n"
|
|
11500
|
+
"http.get = () => fail('http.get');\n"
|
|
11501
|
+
"const https = require('https');\n"
|
|
11502
|
+
"https.request = () => fail('https.request');\n"
|
|
11503
|
+
"https.get = () => fail('https.get');\n"
|
|
11504
|
+
"const dns = require('dns');\n"
|
|
11505
|
+
"dns.lookup = () => fail('dns.lookup');\n"
|
|
11506
|
+
"dns.resolve = () => fail('dns.resolve');\n"
|
|
11507
|
+
"globalThis.fetch = () => fail('fetch');\n",
|
|
11508
|
+
encoding="utf-8",
|
|
11509
|
+
)
|
|
11510
|
+
env = dict(os.environ)
|
|
11511
|
+
env["NODE_OPTIONS"] = f"--require {str(guard).replace(chr(92), '/')}"
|
|
11512
|
+
offline = run_keel(declared, "--doctor", env=env)
|
|
11513
|
+
if "precedents: 2" not in offline.stdout:
|
|
11514
|
+
report(
|
|
11515
|
+
"precedent-store: reading the store attempted network access, "
|
|
11516
|
+
"or failed under the no-network guard."
|
|
11517
|
+
)
|
|
11518
|
+
report((offline.stderr or offline.stdout).strip())
|
|
11519
|
+
return 1
|
|
11520
|
+
|
|
11521
|
+
report("precedent-store-declaration scenario passed.")
|
|
11522
|
+
return 0
|
|
11523
|
+
|
|
11524
|
+
|
|
11525
|
+
def validate_precedent_rules_scenario() -> int:
|
|
11526
|
+
"""The three rules the owner accepted must be in the skill, not in a chat.
|
|
11527
|
+
|
|
11528
|
+
Each is asserted by the phrase that carries its distinguishing content, not
|
|
11529
|
+
by a keyword: "precedent" appearing somewhere would satisfy a keyword check
|
|
11530
|
+
while saying none of what was decided.
|
|
11531
|
+
"""
|
|
11532
|
+
|
|
11533
|
+
required = [
|
|
11534
|
+
# Citation: the trigger, and its negative half.
|
|
11535
|
+
"would otherwise have interrupted",
|
|
11536
|
+
"not cited",
|
|
11537
|
+
# Promotion: who does it, and what does not.
|
|
11538
|
+
"propose the promotion",
|
|
11539
|
+
"no usage count",
|
|
11540
|
+
# No reclassification, and the reason it is a fixed point.
|
|
11541
|
+
"never moves a decision out of",
|
|
11542
|
+
"recurrence",
|
|
11543
|
+
# Recording: the rationale is the load-bearing field.
|
|
11544
|
+
"reasoning transfers",
|
|
11545
|
+
]
|
|
11546
|
+
canonical = ROOT / "src/skills/keel-align-expectations/SKILL.md"
|
|
11547
|
+
distributed = ROOT / PLUGIN_ROOT / "skills/keel-align-expectations/SKILL.md"
|
|
11548
|
+
|
|
11549
|
+
for label, path in (("canonical", canonical), ("distributed", distributed)):
|
|
11550
|
+
if not path.is_file():
|
|
11551
|
+
report(f"precedent-rules: missing {label} skill: {path}")
|
|
11552
|
+
return 1
|
|
11553
|
+
# Collapse whitespace before matching. These are multi-word phrases and
|
|
11554
|
+
# the file is hard-wrapped, so matching raw text would assert the line
|
|
11555
|
+
# layout rather than the wording — and would fail on any later reflow
|
|
11556
|
+
# that changed nothing.
|
|
11557
|
+
content = re.sub(r"\s+", " ", path.read_text(encoding="utf-8"))
|
|
11558
|
+
for phrase in required:
|
|
11559
|
+
if phrase not in content:
|
|
11560
|
+
report(f"precedent-rules: {label} skill omits: {phrase}")
|
|
11561
|
+
return 1
|
|
11562
|
+
|
|
11563
|
+
if canonical.read_bytes() != distributed.read_bytes():
|
|
11564
|
+
report("precedent-rules: the canonical and distributed skills diverged.")
|
|
11565
|
+
return 1
|
|
11566
|
+
|
|
11567
|
+
report("precedent-rules scenario passed.")
|
|
11568
|
+
return 0
|
|
11569
|
+
|
|
11570
|
+
|
|
11571
|
+
def validate_precedent_projection_pointer_scenario() -> int:
|
|
11572
|
+
"""SessionStart may say how big the store is. It may not say what is in it.
|
|
11573
|
+
|
|
11574
|
+
The store grows monotonically while the precedents relevant to any one
|
|
11575
|
+
session are a small subset, and the hook pays its cost on every session
|
|
11576
|
+
including post-compaction reinjection. So the projection carries counts and
|
|
11577
|
+
freshness; bodies load when a decision is actually being made.
|
|
11578
|
+
"""
|
|
11579
|
+
|
|
11580
|
+
def projection(repo: Path) -> tuple[str, str]:
|
|
11581
|
+
result = run_session_start_hook(
|
|
11582
|
+
repo,
|
|
11583
|
+
{"hook_event_name": "SessionStart", "source": "startup"},
|
|
11584
|
+
keel_cli=f'node "{ROOT / "bin/keel.js"}"',
|
|
11585
|
+
)
|
|
11586
|
+
payload = json.loads(result.stdout.strip().splitlines()[-1])
|
|
11587
|
+
return (
|
|
11588
|
+
payload["hookSpecificOutput"]["additionalContext"],
|
|
11589
|
+
payload.get("systemMessage", ""),
|
|
11590
|
+
)
|
|
11591
|
+
|
|
11592
|
+
with tempfile.TemporaryDirectory(prefix="keel-precproj-") as raw_tmp:
|
|
11593
|
+
root = Path(raw_tmp)
|
|
11594
|
+
store = root / "store"
|
|
11595
|
+
# Text that must never reach the projection. If any of it appears, a
|
|
11596
|
+
# body leaked where only a pointer belongs.
|
|
11597
|
+
write_precedent(
|
|
11598
|
+
store,
|
|
11599
|
+
"leak-canary",
|
|
11600
|
+
status="authorized",
|
|
11601
|
+
decision="NEVERAPPEARSINPROJECTION-decision",
|
|
11602
|
+
rationale="NEVERAPPEARSINPROJECTION-rationale",
|
|
11603
|
+
)
|
|
11604
|
+
write_precedent(store, "second")
|
|
11605
|
+
|
|
11606
|
+
# The hook is silent outside a Keel repository, so both fixtures need
|
|
11607
|
+
# an openspec tree before the projection exists at all.
|
|
11608
|
+
declaring = root / "declaring"
|
|
11609
|
+
declaring.mkdir()
|
|
11610
|
+
write_text(declaring / "openspec/changes/demo/tasks.md", task_contract_fixture())
|
|
11611
|
+
(declaring / "keel").mkdir(parents=True)
|
|
11612
|
+
(declaring / "keel" / "config.yaml").write_text(
|
|
11613
|
+
f"precedents: {str(store).replace(chr(92), '/')}\n", encoding="utf-8"
|
|
11614
|
+
)
|
|
11615
|
+
# Two ways to declare nothing, and they reach different branches: no
|
|
11616
|
+
# config file at all, and a config file that declares other things.
|
|
11617
|
+
silent = root / "silent"
|
|
11618
|
+
silent.mkdir()
|
|
11619
|
+
write_text(silent / "openspec/changes/demo/tasks.md", task_contract_fixture())
|
|
11620
|
+
other_keys = root / "other-keys"
|
|
11621
|
+
other_keys.mkdir()
|
|
11622
|
+
write_text(
|
|
11623
|
+
other_keys / "openspec/changes/demo/tasks.md", task_contract_fixture()
|
|
11624
|
+
)
|
|
11625
|
+
(other_keys / "keel").mkdir(parents=True)
|
|
11626
|
+
(other_keys / "keel" / "config.yaml").write_text(
|
|
11627
|
+
"fast_check: echo check\nauthorize:\n - commit\n", encoding="utf-8"
|
|
11628
|
+
)
|
|
11629
|
+
|
|
11630
|
+
# M1 — counts and freshness, never a body.
|
|
11631
|
+
context, message = projection(declaring)
|
|
11632
|
+
combined = f"{context}\n{message}"
|
|
11633
|
+
if "precedents: 2" not in combined or "1 authorized" not in combined:
|
|
11634
|
+
report(
|
|
11635
|
+
"precedent-projection: the projection does not state the "
|
|
11636
|
+
f"precedent counts: {combined!r}"
|
|
11637
|
+
)
|
|
11638
|
+
return 1
|
|
11639
|
+
if "last synced" not in combined:
|
|
11640
|
+
report("precedent-projection: the projection does not state store freshness.")
|
|
11641
|
+
report(combined)
|
|
11642
|
+
return 1
|
|
11643
|
+
if "NEVERAPPEARSINPROJECTION" in combined:
|
|
11644
|
+
report(
|
|
11645
|
+
"precedent-projection: a precedent body reached the projection; "
|
|
11646
|
+
"only a pointer belongs there."
|
|
11647
|
+
)
|
|
11648
|
+
report(combined)
|
|
11649
|
+
return 1
|
|
11650
|
+
|
|
11651
|
+
# M2 — an undeclared store adds nothing at all, by either route.
|
|
11652
|
+
for repo, label in ((silent, "no config file"), (other_keys, "other keys only")):
|
|
11653
|
+
quiet_context, quiet_message = projection(repo)
|
|
11654
|
+
if "precedent" in f"{quiet_context}\n{quiet_message}".lower():
|
|
11655
|
+
report(
|
|
11656
|
+
f"precedent-projection: with {label}, an undeclared store "
|
|
11657
|
+
"still added text to the projection."
|
|
11658
|
+
)
|
|
11659
|
+
report(quiet_context)
|
|
11660
|
+
return 1
|
|
11661
|
+
|
|
11662
|
+
report("precedent-projection-pointer scenario passed.")
|
|
11663
|
+
return 0
|
|
11664
|
+
|
|
11665
|
+
|
|
11666
|
+
def validate_precedent_never_weakens_scenario() -> int:
|
|
11667
|
+
"""A precedent informs a decision. It must not stand in for a proof.
|
|
11668
|
+
|
|
11669
|
+
Same shape as the standing-authorization inertness scenario, and for the
|
|
11670
|
+
same reason: every check passes when two repositories agree, so a store
|
|
11671
|
+
that silently failed to load would make each comparison trivially true.
|
|
11672
|
+
The positive control asserts the difference exists before asserting it is
|
|
11673
|
+
inert.
|
|
11674
|
+
"""
|
|
11675
|
+
|
|
11676
|
+
complete_task = (
|
|
11677
|
+
"- [ ] 1.1 Behavior\n"
|
|
11678
|
+
" - Covers:\n"
|
|
11679
|
+
" - E1: public behavior\n"
|
|
11680
|
+
" - Touch:\n"
|
|
11681
|
+
" - src/feature.js\n"
|
|
11682
|
+
" - Verify:\n"
|
|
11683
|
+
" - Strategy: evidence-first\n"
|
|
11684
|
+
" - M1: node test.js proves the public behavior\n"
|
|
11685
|
+
" - Evidence:\n"
|
|
11686
|
+
" - Contract: pending\n"
|
|
11687
|
+
" - M1: node test.js printed ok\n"
|
|
11688
|
+
" - Review:\n"
|
|
11689
|
+
" - Status: pass\n"
|
|
11690
|
+
" - Acceptance check: reviewed\n"
|
|
11691
|
+
" - Scope check: reviewed\n"
|
|
11692
|
+
" - Findings: none\n"
|
|
11693
|
+
" - Blocker: none\n"
|
|
11694
|
+
)
|
|
11695
|
+
missing_evidence_task = complete_task.replace(
|
|
11696
|
+
" - M1: node test.js printed ok\n", " - M1: pending\n"
|
|
11697
|
+
)
|
|
11698
|
+
|
|
11699
|
+
def gate_result(repo: Path, stage: str) -> dict | None:
|
|
11700
|
+
result = run_keel(
|
|
11701
|
+
repo, "gate", stage, "--change", "demo", "--task", "1.1", "--json"
|
|
11702
|
+
)
|
|
11703
|
+
try:
|
|
11704
|
+
payload = json.loads(result.stdout)
|
|
11705
|
+
except json.JSONDecodeError:
|
|
11706
|
+
return None
|
|
11707
|
+
return {
|
|
11708
|
+
"status": payload.get("status"),
|
|
11709
|
+
"problems": sorted(
|
|
11710
|
+
(problem.get("code", ""), problem.get("message", ""))
|
|
11711
|
+
for problem in payload.get("problems") or []
|
|
11712
|
+
),
|
|
11713
|
+
}
|
|
11714
|
+
|
|
11715
|
+
with tempfile.TemporaryDirectory(prefix="keel-precinert-") as raw_tmp:
|
|
11716
|
+
root = Path(raw_tmp)
|
|
11717
|
+
store = root / "store"
|
|
11718
|
+
for name in ("first", "second", "third"):
|
|
11719
|
+
write_precedent(store, name, status="authorized")
|
|
11720
|
+
|
|
11721
|
+
def pair(name: str, tasks: str) -> tuple[Path, Path]:
|
|
11722
|
+
declaring = root / f"{name}-declaring"
|
|
11723
|
+
declaring.mkdir()
|
|
11724
|
+
write_gate_fixture(declaring, tasks)
|
|
11725
|
+
(declaring / "keel").mkdir(parents=True, exist_ok=True)
|
|
11726
|
+
(declaring / "keel" / "config.yaml").write_text(
|
|
11727
|
+
f"precedents: {str(store).replace(chr(92), '/')}\n", encoding="utf-8"
|
|
11728
|
+
)
|
|
11729
|
+
silent = root / f"{name}-silent"
|
|
11730
|
+
silent.mkdir()
|
|
11731
|
+
write_gate_fixture(silent, tasks)
|
|
11732
|
+
# Positive control: prove the two repositories actually differ
|
|
11733
|
+
# before proving the difference changes nothing.
|
|
11734
|
+
live = run_keel(declaring, "--doctor").stdout
|
|
11735
|
+
inert = run_keel(silent, "--doctor").stdout
|
|
11736
|
+
if "precedents: 3" not in live or "authorized: 3" not in live:
|
|
11737
|
+
report(
|
|
11738
|
+
f"precedent-inert: the {name} declaring fixture never loaded "
|
|
11739
|
+
"its store; every comparison below would be vacuous."
|
|
11740
|
+
)
|
|
11741
|
+
raise AssertionError("declaring fixture is not declaring")
|
|
11742
|
+
if "precedents: none" not in inert:
|
|
11743
|
+
report(f"precedent-inert: the {name} silent fixture declared a store.")
|
|
11744
|
+
raise AssertionError("silent fixture is not silent")
|
|
11745
|
+
return declaring, silent
|
|
11746
|
+
|
|
11747
|
+
# M1 — every gate stage agrees across the pair.
|
|
11748
|
+
declaring, silent = pair("complete", complete_task)
|
|
11749
|
+
for stage in ("task-start", "task-complete"):
|
|
11750
|
+
live = gate_result(declaring, stage)
|
|
11751
|
+
inert = gate_result(silent, stage)
|
|
11752
|
+
if live is None or inert is None:
|
|
11753
|
+
report(f"precedent-inert: {stage} produced no JSON.")
|
|
11754
|
+
return 1
|
|
11755
|
+
if live != inert:
|
|
11756
|
+
report(
|
|
11757
|
+
f"precedent-inert: a declared store changed the {stage} "
|
|
11758
|
+
f"result: {live} != {inert}"
|
|
11759
|
+
)
|
|
11760
|
+
return 1
|
|
11761
|
+
|
|
11762
|
+
# M2 — missing evidence still fails, with unchanged failure text.
|
|
11763
|
+
declaring, silent = pair("missing", missing_evidence_task)
|
|
11764
|
+
for repo in (declaring, silent):
|
|
11765
|
+
if gate_result(repo, "task-start") is None:
|
|
11766
|
+
report("precedent-inert: task-start produced no JSON.")
|
|
11767
|
+
return 1
|
|
11768
|
+
live = gate_result(declaring, "task-complete")
|
|
11769
|
+
inert = gate_result(silent, "task-complete")
|
|
11770
|
+
if live is None or inert is None:
|
|
11771
|
+
report("precedent-inert: task-complete produced no JSON.")
|
|
11772
|
+
return 1
|
|
11773
|
+
if live.get("status") == "pass":
|
|
11774
|
+
report(
|
|
11775
|
+
"precedent-inert: a store of authorized precedents let a task "
|
|
11776
|
+
"with missing evidence pass completion."
|
|
11777
|
+
)
|
|
11778
|
+
return 1
|
|
11779
|
+
if live != inert:
|
|
11780
|
+
report(
|
|
11781
|
+
"precedent-inert: a declared store changed the failure text: "
|
|
11782
|
+
f"{live} != {inert}"
|
|
11783
|
+
)
|
|
11784
|
+
return 1
|
|
11785
|
+
|
|
11786
|
+
report("precedent-never-weakens scenario passed.")
|
|
11787
|
+
return 0
|
|
11788
|
+
|
|
11789
|
+
|
|
11790
|
+
def validate_standing_authorization_never_weakens_scenario() -> int:
|
|
11791
|
+
"""A declaration removes a confirmation. It must not remove a proof.
|
|
11792
|
+
|
|
11793
|
+
Every check here compares an authorizing repository against an identical
|
|
11794
|
+
one that declares nothing. The declaration is proven inert on the gate
|
|
11795
|
+
result, on the failure text, and on continuity selection — the three places
|
|
11796
|
+
a reader might otherwise assume authorization had bought something.
|
|
11797
|
+
"""
|
|
11798
|
+
|
|
11799
|
+
complete_task = (
|
|
11800
|
+
"- [ ] 1.1 Behavior\n"
|
|
11801
|
+
" - Covers:\n"
|
|
11802
|
+
" - E1: public behavior\n"
|
|
11803
|
+
" - Touch:\n"
|
|
11804
|
+
" - src/feature.js\n"
|
|
11805
|
+
" - Verify:\n"
|
|
11806
|
+
" - Strategy: evidence-first\n"
|
|
11807
|
+
" - M1: node test.js proves the public behavior\n"
|
|
11808
|
+
" - Evidence:\n"
|
|
11809
|
+
" - Contract: pending\n"
|
|
11810
|
+
" - M1: node test.js printed ok\n"
|
|
11811
|
+
" - Review:\n"
|
|
11812
|
+
" - Status: pass\n"
|
|
11813
|
+
" - Acceptance check: reviewed\n"
|
|
11814
|
+
" - Scope check: reviewed\n"
|
|
11815
|
+
" - Findings: none\n"
|
|
11816
|
+
" - Blocker: none\n"
|
|
11817
|
+
)
|
|
11818
|
+
missing_evidence_task = complete_task.replace(
|
|
11819
|
+
" - M1: node test.js printed ok\n", " - M1: pending\n"
|
|
11820
|
+
)
|
|
11821
|
+
|
|
11822
|
+
def gate_result(repo: Path, stage: str) -> dict | None:
|
|
11823
|
+
result = run_keel(
|
|
11824
|
+
repo, "gate", stage, "--change", "demo", "--task", "1.1", "--json"
|
|
11825
|
+
)
|
|
11826
|
+
try:
|
|
11827
|
+
payload = json.loads(result.stdout)
|
|
11828
|
+
except json.JSONDecodeError:
|
|
11829
|
+
return None
|
|
11830
|
+
return {
|
|
11831
|
+
"status": payload.get("status"),
|
|
11832
|
+
"problems": sorted(
|
|
11833
|
+
(problem.get("code", ""), problem.get("message", ""))
|
|
11834
|
+
for problem in payload.get("problems") or []
|
|
11835
|
+
),
|
|
11836
|
+
}
|
|
11837
|
+
|
|
11838
|
+
def pair(root: Path, name: str, tasks: str) -> tuple[Path, Path]:
|
|
11839
|
+
authorizing = root / f"{name}-authorizing"
|
|
11840
|
+
authorizing.mkdir()
|
|
11841
|
+
write_gate_fixture(authorizing, tasks)
|
|
11842
|
+
write_authorize_config(
|
|
11843
|
+
authorizing,
|
|
11844
|
+
"authorize:\n - commit\n - push\n - release\n - archive\n",
|
|
11845
|
+
)
|
|
11846
|
+
silent = root / f"{name}-silent"
|
|
11847
|
+
silent.mkdir()
|
|
11848
|
+
write_gate_fixture(silent, tasks)
|
|
11849
|
+
# Positive control. Every check below compares these two repositories
|
|
11850
|
+
# and passes when they agree, so a declaration that silently failed to
|
|
11851
|
+
# reach the capsule would make each comparison trivially true and prove
|
|
11852
|
+
# nothing. Assert the difference exists before asserting it is inert.
|
|
11853
|
+
live = standing_authorization_autonomy(authorizing) or []
|
|
11854
|
+
inert = standing_authorization_autonomy(silent) or []
|
|
11855
|
+
if not any("keel/config.yaml" in entry for entry in live):
|
|
11856
|
+
report(
|
|
11857
|
+
f"standing-authorization-inert: the {name} authorizing fixture "
|
|
11858
|
+
f"never actually authorized anything: {live}"
|
|
11859
|
+
)
|
|
11860
|
+
raise AssertionError("authorizing fixture is not authorizing")
|
|
11861
|
+
if any("keel/config.yaml" in entry for entry in inert):
|
|
11862
|
+
report(
|
|
11863
|
+
f"standing-authorization-inert: the {name} silent fixture "
|
|
11864
|
+
f"declared something: {inert}"
|
|
11865
|
+
)
|
|
11866
|
+
raise AssertionError("silent fixture is not silent")
|
|
11867
|
+
return authorizing, silent
|
|
11868
|
+
|
|
11869
|
+
with tempfile.TemporaryDirectory(prefix="keel-authinert-") as raw_tmp:
|
|
11870
|
+
root = Path(raw_tmp)
|
|
11871
|
+
|
|
11872
|
+
# M1 — completion returns the same status and problem set either way.
|
|
11873
|
+
authorizing, silent = pair(root, "complete", complete_task)
|
|
11874
|
+
for repo in (authorizing, silent):
|
|
11875
|
+
if gate_result(repo, "task-start") is None:
|
|
11876
|
+
report("standing-authorization-inert: task-start produced no JSON.")
|
|
11877
|
+
return 1
|
|
11878
|
+
authorized_result = gate_result(authorizing, "task-complete")
|
|
11879
|
+
silent_result = gate_result(silent, "task-complete")
|
|
11880
|
+
if authorized_result is None or silent_result is None:
|
|
11881
|
+
report("standing-authorization-inert: task-complete produced no JSON.")
|
|
11882
|
+
return 1
|
|
11883
|
+
if authorized_result != silent_result:
|
|
11884
|
+
report(
|
|
11885
|
+
"standing-authorization-inert: a declaration changed the "
|
|
11886
|
+
f"completion gate result: {authorized_result} != {silent_result}"
|
|
11887
|
+
)
|
|
11888
|
+
return 1
|
|
11889
|
+
|
|
11890
|
+
# M2 — a repo authorizing every action still fails for missing evidence,
|
|
11891
|
+
# with unchanged failure text.
|
|
11892
|
+
authorizing, silent = pair(root, "missing", missing_evidence_task)
|
|
11893
|
+
for repo in (authorizing, silent):
|
|
11894
|
+
if gate_result(repo, "task-start") is None:
|
|
11895
|
+
report("standing-authorization-inert: task-start produced no JSON.")
|
|
11896
|
+
return 1
|
|
11897
|
+
authorized_result = gate_result(authorizing, "task-complete")
|
|
11898
|
+
silent_result = gate_result(silent, "task-complete")
|
|
11899
|
+
if authorized_result is None or silent_result is None:
|
|
11900
|
+
report("standing-authorization-inert: task-complete produced no JSON.")
|
|
11901
|
+
return 1
|
|
11902
|
+
if authorized_result.get("status") == "pass":
|
|
11903
|
+
report(
|
|
11904
|
+
"standing-authorization-inert: authorizing every action let a "
|
|
11905
|
+
"task with missing evidence pass completion."
|
|
11906
|
+
)
|
|
11907
|
+
return 1
|
|
11908
|
+
if authorized_result != silent_result:
|
|
11909
|
+
report(
|
|
11910
|
+
"standing-authorization-inert: a declaration changed the failure "
|
|
11911
|
+
f"text: {authorized_result} != {silent_result}"
|
|
11912
|
+
)
|
|
11913
|
+
return 1
|
|
11914
|
+
|
|
11915
|
+
# M3 — a declaration selects nothing and starts nothing.
|
|
11916
|
+
def continuity(repo: Path) -> dict | None:
|
|
11917
|
+
result = run_keel(repo, "context", "--json")
|
|
11918
|
+
try:
|
|
11919
|
+
payload = json.loads(result.stdout)
|
|
11920
|
+
except json.JSONDecodeError:
|
|
11921
|
+
return None
|
|
11922
|
+
return {
|
|
11923
|
+
"status": payload.get("status"),
|
|
11924
|
+
"selection": payload.get("selection"),
|
|
11925
|
+
"nextAction": payload.get("nextAction"),
|
|
11926
|
+
}
|
|
11927
|
+
|
|
11928
|
+
authorizing, silent = pair(root, "context", complete_task)
|
|
11929
|
+
authorized_context = continuity(authorizing)
|
|
11930
|
+
silent_context = continuity(silent)
|
|
11931
|
+
if authorized_context is None or silent_context is None:
|
|
11932
|
+
report("standing-authorization-inert: keel context produced no JSON.")
|
|
11933
|
+
return 1
|
|
11934
|
+
if authorized_context != silent_context:
|
|
11935
|
+
report(
|
|
11936
|
+
"standing-authorization-inert: a declaration changed continuity "
|
|
11937
|
+
f"selection: {authorized_context} != {silent_context}"
|
|
11938
|
+
)
|
|
11939
|
+
return 1
|
|
11940
|
+
|
|
11941
|
+
report("standing-authorization-never-weakens scenario passed.")
|
|
11942
|
+
return 0
|
|
11943
|
+
|
|
11944
|
+
|
|
11114
11945
|
def validate_fast_check_config_scaffold_scenario() -> int:
|
|
11115
11946
|
with tempfile.TemporaryDirectory(prefix="keel-fastcfg-") as raw_tmp:
|
|
11116
11947
|
repo = Path(raw_tmp)
|
|
@@ -14491,6 +15322,28 @@ SCENARIOS: tuple = (
|
|
|
14491
15322
|
("update-pack-install", validate_update_pack_install_scenario),
|
|
14492
15323
|
("update-default-registry", validate_update_default_registry_scenario),
|
|
14493
15324
|
("verification-layering-docs", validate_verification_layering_docs_scenario),
|
|
15325
|
+
(
|
|
15326
|
+
"standing-authorization-declaration",
|
|
15327
|
+
validate_standing_authorization_declaration_scenario,
|
|
15328
|
+
),
|
|
15329
|
+
(
|
|
15330
|
+
"standing-authorization-inheritance",
|
|
15331
|
+
validate_standing_authorization_inheritance_scenario,
|
|
15332
|
+
),
|
|
15333
|
+
(
|
|
15334
|
+
"standing-authorization-never-weakens",
|
|
15335
|
+
validate_standing_authorization_never_weakens_scenario,
|
|
15336
|
+
),
|
|
15337
|
+
(
|
|
15338
|
+
"precedent-store-declaration",
|
|
15339
|
+
validate_precedent_store_declaration_scenario,
|
|
15340
|
+
),
|
|
15341
|
+
("precedent-never-weakens", validate_precedent_never_weakens_scenario),
|
|
15342
|
+
("precedent-rules", validate_precedent_rules_scenario),
|
|
15343
|
+
(
|
|
15344
|
+
"precedent-projection-pointer",
|
|
15345
|
+
validate_precedent_projection_pointer_scenario,
|
|
15346
|
+
),
|
|
14494
15347
|
("fast-check-config-scaffold", validate_fast_check_config_scaffold_scenario),
|
|
14495
15348
|
("fast-pre-push-hooks", validate_fast_pre_push_hooks_scenario),
|
|
14496
15349
|
("fast-pre-push-doctor", validate_fast_pre_push_doctor_scenario),
|