@christang/keel 5.64.0 → 5.66.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.66.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.66.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",
|
|
@@ -38,8 +38,8 @@ REQUIRED_SCRIPTS = [
|
|
|
38
38
|
"scripts/validate_plugin.py",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
-
PACKAGE_VERSION = "5.
|
|
42
|
-
PROTOCOL_VERSION = "5.
|
|
41
|
+
PACKAGE_VERSION = "5.66.0"
|
|
42
|
+
PROTOCOL_VERSION = "5.66.0"
|
|
43
43
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
44
44
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
45
45
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -17027,6 +17027,42 @@ STANDING_AUTHORIZATION_ACTIONS = (
|
|
|
17027
17027
|
)
|
|
17028
17028
|
|
|
17029
17029
|
|
|
17030
|
+
def run_node_expression(root: Path, expression: str) -> str:
|
|
17031
|
+
"""Evaluate a Node expression with `root` as the working directory.
|
|
17032
|
+
|
|
17033
|
+
The declaration set is read from the module that defines it rather than by
|
|
17034
|
+
regexing its source: a regex over the source would be a second copy of the
|
|
17035
|
+
same literal the check exists to remove, one level down (issue #143).
|
|
17036
|
+
"""
|
|
17037
|
+
result = subprocess.run(
|
|
17038
|
+
["node", "-e", expression],
|
|
17039
|
+
cwd=root,
|
|
17040
|
+
text=True,
|
|
17041
|
+
encoding="utf-8",
|
|
17042
|
+
errors="replace",
|
|
17043
|
+
capture_output=True,
|
|
17044
|
+
check=False,
|
|
17045
|
+
)
|
|
17046
|
+
return result.stdout
|
|
17047
|
+
|
|
17048
|
+
|
|
17049
|
+
def config_header_problem(names: list, flat_header: str) -> str:
|
|
17050
|
+
"""The one rule for "the header names every declaration", or "" when it does.
|
|
17051
|
+
|
|
17052
|
+
Returns the diagnostic naming the missing declaration. Deliberately reports
|
|
17053
|
+
no count: a count tells an author that two numbers differ, and the name tells
|
|
17054
|
+
them which line to write.
|
|
17055
|
+
"""
|
|
17056
|
+
for name in names:
|
|
17057
|
+
if name not in flat_header:
|
|
17058
|
+
return (
|
|
17059
|
+
f"keel/config.yaml's header does not name the `{name}` "
|
|
17060
|
+
"declaration, so a project reading it would never learn it may "
|
|
17061
|
+
"write one. Add it to the header's list."
|
|
17062
|
+
)
|
|
17063
|
+
return ""
|
|
17064
|
+
|
|
17065
|
+
|
|
17030
17066
|
def write_authorize_config(repo: Path, body: str) -> None:
|
|
17031
17067
|
(repo / "keel").mkdir(parents=True, exist_ok=True)
|
|
17032
17068
|
(repo / "keel" / "config.yaml").write_text(body, encoding="utf-8")
|
|
@@ -18117,11 +18153,27 @@ def validate_delegation_resident_text_scenario() -> int:
|
|
|
18117
18153
|
# M1 — the config header counts its declarations correctly.
|
|
18118
18154
|
config = ROOT / "keel/config.yaml"
|
|
18119
18155
|
cfg = flat(config)
|
|
18120
|
-
|
|
18121
|
-
|
|
18156
|
+
# Derived from the module that reads the declarations, not restated here. The
|
|
18157
|
+
# literal this replaces had been hand-bumped twice, and the second bump was
|
|
18158
|
+
# found by a red suite run in a scenario named for something else (#143).
|
|
18159
|
+
declarations = [
|
|
18160
|
+
name
|
|
18161
|
+
for name in run_node_expression(
|
|
18162
|
+
ROOT,
|
|
18163
|
+
"process.stdout.write("
|
|
18164
|
+
"require('./src/core/config.js').CONFIG_DECLARATIONS.join(','))",
|
|
18165
|
+
).strip().split(",")
|
|
18166
|
+
if name
|
|
18167
|
+
]
|
|
18168
|
+
if not declarations:
|
|
18169
|
+
report(
|
|
18170
|
+
"delegation-resident-text: src/core/config.js exports no declaration "
|
|
18171
|
+
"list, so the header cannot be checked against what Keel reads."
|
|
18172
|
+
)
|
|
18122
18173
|
return 1
|
|
18123
|
-
|
|
18124
|
-
|
|
18174
|
+
header_problem = config_header_problem(declarations, cfg)
|
|
18175
|
+
if header_problem:
|
|
18176
|
+
report(f"delegation-resident-text: {header_problem}")
|
|
18125
18177
|
return 1
|
|
18126
18178
|
if "delegation" not in cfg:
|
|
18127
18179
|
report("delegation-resident-text: the config header does not document delegation.")
|
|
@@ -28459,6 +28511,236 @@ def validate_a_claim_names_what_would_falsify_it_scenario() -> int:
|
|
|
28459
28511
|
return 0
|
|
28460
28512
|
|
|
28461
28513
|
|
|
28514
|
+
def validate_a_negation_is_not_a_marker_scenario() -> int:
|
|
28515
|
+
"""Issue #144: `Not resolved here:` was read as `Resolved here:`.
|
|
28516
|
+
|
|
28517
|
+
The marker scan's `\\b` sits at the space inside the negation, so a finding
|
|
28518
|
+
that said in as many words it was *not* resolved here had the next word read
|
|
28519
|
+
as its resolution evidence — a dismissal read as a repair, then refused for
|
|
28520
|
+
lacking repair evidence. 5.42.0 established that a quoted marker is a
|
|
28521
|
+
quotation; a negated one is the same class and was not covered.
|
|
28522
|
+
"""
|
|
28523
|
+
label = "a-negation-is-not-a-marker"
|
|
28524
|
+
|
|
28525
|
+
def findings_task(findings: str) -> str:
|
|
28526
|
+
return "\n".join(
|
|
28527
|
+
[
|
|
28528
|
+
"- [x] 1.1 Findings probe",
|
|
28529
|
+
" - Owner: claude",
|
|
28530
|
+
" - Mode: implementation",
|
|
28531
|
+
" - Covers:",
|
|
28532
|
+
" - E1: the task proves its own behavior",
|
|
28533
|
+
" - Read:",
|
|
28534
|
+
" - README.md",
|
|
28535
|
+
" - Touch:",
|
|
28536
|
+
" - src/example.js",
|
|
28537
|
+
" - Verify:",
|
|
28538
|
+
" - Strategy: vertical-tdd",
|
|
28539
|
+
" - M1: node test.js asserts the public behavior",
|
|
28540
|
+
" - Evidence:",
|
|
28541
|
+
" - Contract: pending",
|
|
28542
|
+
" - M1: pass. node test.js reported the behavior.",
|
|
28543
|
+
" - M1.red: fail. the behavior did not exist.",
|
|
28544
|
+
" - M1.green: pass.",
|
|
28545
|
+
" - Review:",
|
|
28546
|
+
" - Status: pass",
|
|
28547
|
+
" - Acceptance check: the behavior is proven.",
|
|
28548
|
+
" - Scope check: only Touch changed.",
|
|
28549
|
+
f" - Findings: {findings}",
|
|
28550
|
+
" - Blocker: none",
|
|
28551
|
+
" - Reauthorizations: none",
|
|
28552
|
+
]
|
|
28553
|
+
)
|
|
28554
|
+
|
|
28555
|
+
with tempfile.TemporaryDirectory(prefix="keel-negation-") as raw:
|
|
28556
|
+
root = Path(raw)
|
|
28557
|
+
|
|
28558
|
+
def complete(name: str, findings: str) -> dict:
|
|
28559
|
+
repo = root / name
|
|
28560
|
+
write_gate_fixture(repo, tasks=findings_task(findings))
|
|
28561
|
+
run_keel(
|
|
28562
|
+
repo, "gate", "task-start", "--change", "demo", "--task", "1.1",
|
|
28563
|
+
"--record", "--json",
|
|
28564
|
+
)
|
|
28565
|
+
done = run_keel(
|
|
28566
|
+
repo, "gate", "task-complete", "--change", "demo", "--task", "1.1",
|
|
28567
|
+
"--json",
|
|
28568
|
+
)
|
|
28569
|
+
try:
|
|
28570
|
+
return json.loads(done.stdout)
|
|
28571
|
+
except json.JSONDecodeError:
|
|
28572
|
+
return {"status": "unparsed", "problems": [{"message": done.stdout[:400]}]}
|
|
28573
|
+
|
|
28574
|
+
# M1 — the text that produced #144, verbatim in shape.
|
|
28575
|
+
negated = complete(
|
|
28576
|
+
"negated",
|
|
28577
|
+
"one, still open. The count is derivable from the declarations the "
|
|
28578
|
+
"module reads. Not resolved here: it is a different module and would "
|
|
28579
|
+
"be scope expansion. Durable owner: https://example.com/issues/1",
|
|
28580
|
+
)
|
|
28581
|
+
if negated.get("status") != "pass":
|
|
28582
|
+
report(
|
|
28583
|
+
f"{label}: names neither a check nor a path — a finding saying it "
|
|
28584
|
+
"was *not* resolved here, closing with a durable owner, was "
|
|
28585
|
+
f"refused; {problem_codes(negated)!r} {problem_text(negated)!r}."
|
|
28586
|
+
)
|
|
28587
|
+
return 1
|
|
28588
|
+
if "resolution evidence" in problem_text(negated).lower():
|
|
28589
|
+
report(
|
|
28590
|
+
f"{label}: the negated phrase still produced a resolution-evidence "
|
|
28591
|
+
f"diagnostic; {problem_text(negated)!r}."
|
|
28592
|
+
)
|
|
28593
|
+
return 1
|
|
28594
|
+
|
|
28595
|
+
# M2 — the positive control, in all three opening positions. Without it
|
|
28596
|
+
# the narrowing could pass by recognizing no marker anywhere.
|
|
28597
|
+
for name, findings in (
|
|
28598
|
+
("opens-value", "Resolved here: M9"),
|
|
28599
|
+
("after-break", "one, and it was fixed.\n Resolved here: M9"),
|
|
28600
|
+
("after-stop", "one, and it was fixed. Resolved here: M9"),
|
|
28601
|
+
):
|
|
28602
|
+
payload = complete(name, findings)
|
|
28603
|
+
if payload.get("status") == "pass":
|
|
28604
|
+
report(
|
|
28605
|
+
f"{label}: stopped recognizing a real marker ({name}) — a "
|
|
28606
|
+
"`Resolved here:` naming a check this task does not declare "
|
|
28607
|
+
"completed cleanly."
|
|
28608
|
+
)
|
|
28609
|
+
return 1
|
|
28610
|
+
if "M9" not in problem_text(payload):
|
|
28611
|
+
report(
|
|
28612
|
+
f"{label}: stopped recognizing a real marker ({name}) — the "
|
|
28613
|
+
f"refusal does not name the cited check; {problem_text(payload)!r}."
|
|
28614
|
+
)
|
|
28615
|
+
return 1
|
|
28616
|
+
|
|
28617
|
+
# M3 — a marker that opens no clause is refused, not ignored, and the
|
|
28618
|
+
# refusal names the rule so the author is not told to add what they wrote.
|
|
28619
|
+
swallowed = complete(
|
|
28620
|
+
"swallowed", "one, and the fix was Resolved here: M1 in passing"
|
|
28621
|
+
)
|
|
28622
|
+
if swallowed.get("status") == "pass":
|
|
28623
|
+
report(
|
|
28624
|
+
f"{label}: a finding whose only marker opens no clause completed "
|
|
28625
|
+
"cleanly, so it carried no disposition and nothing said so."
|
|
28626
|
+
)
|
|
28627
|
+
return 1
|
|
28628
|
+
# Asserted on the whole phrase, not the word: `opens` alone matches
|
|
28629
|
+
# inside `openspec`, which the same diagnostic already prints, and the
|
|
28630
|
+
# assertion was vacuous until this was tightened.
|
|
28631
|
+
if "opens its clause" not in problem_text(swallowed).lower():
|
|
28632
|
+
report(
|
|
28633
|
+
f"{label}: does not name the opening requirement — an author "
|
|
28634
|
+
"whose sentence visibly contains a marker is told to add a "
|
|
28635
|
+
f"disposition; {problem_text(swallowed)!r}."
|
|
28636
|
+
)
|
|
28637
|
+
return 1
|
|
28638
|
+
|
|
28639
|
+
if label not in {name for name, _ in SCENARIOS}:
|
|
28640
|
+
report(f"{label}: the scenario registry does not include it.")
|
|
28641
|
+
return 1
|
|
28642
|
+
report(f"{label} scenario passed.")
|
|
28643
|
+
return 0
|
|
28644
|
+
|
|
28645
|
+
|
|
28646
|
+
def validate_a_count_is_derived_from_what_it_counts_scenario() -> int:
|
|
28647
|
+
"""Issue #143: the header assertion restated a set as an English numeral.
|
|
28648
|
+
|
|
28649
|
+
It had been hand-bumped twice, and the second bump was discovered by a red
|
|
28650
|
+
suite run in a scenario whose name has nothing to do with what was changed.
|
|
28651
|
+
An assertion about a set belongs to the set: read it from the module that
|
|
28652
|
+
defines it, and name the member that is missing rather than a count.
|
|
28653
|
+
"""
|
|
28654
|
+
label = "a-count-is-derived-from-what-it-counts"
|
|
28655
|
+
|
|
28656
|
+
def declared_names(root: Path) -> list[str]:
|
|
28657
|
+
result = run_node_expression(
|
|
28658
|
+
root,
|
|
28659
|
+
"process.stdout.write("
|
|
28660
|
+
"require('./src/core/config.js').CONFIG_DECLARATIONS.join(','))",
|
|
28661
|
+
)
|
|
28662
|
+
return [name for name in result.strip().split(",") if name]
|
|
28663
|
+
|
|
28664
|
+
names = declared_names(ROOT)
|
|
28665
|
+
if not names:
|
|
28666
|
+
report(
|
|
28667
|
+
f"{label}: the assertion is a literal — `src/core/config.js` exports "
|
|
28668
|
+
"no declaration list, so nothing the header is checked against comes "
|
|
28669
|
+
"from the code that reads it."
|
|
28670
|
+
)
|
|
28671
|
+
return 1
|
|
28672
|
+
for expected in (
|
|
28673
|
+
"fast_check",
|
|
28674
|
+
"authorize",
|
|
28675
|
+
"precedents",
|
|
28676
|
+
"triage",
|
|
28677
|
+
"delegation",
|
|
28678
|
+
"full_mode_paths",
|
|
28679
|
+
):
|
|
28680
|
+
if expected not in names:
|
|
28681
|
+
report(
|
|
28682
|
+
f"{label}: the exported declaration list omits {expected!r}, so "
|
|
28683
|
+
f"the header is not checked for it; got {names!r}."
|
|
28684
|
+
)
|
|
28685
|
+
return 1
|
|
28686
|
+
|
|
28687
|
+
header = (ROOT / "keel/config.yaml").read_text(encoding="utf-8")
|
|
28688
|
+
flat_header = re.sub(r"\s+", " ", header)
|
|
28689
|
+
for name in names:
|
|
28690
|
+
if name not in flat_header:
|
|
28691
|
+
report(
|
|
28692
|
+
f"{label}: keel/config.yaml's header does not name {name!r}, "
|
|
28693
|
+
"which is the declaration a new project would never learn it may "
|
|
28694
|
+
"write."
|
|
28695
|
+
)
|
|
28696
|
+
return 1
|
|
28697
|
+
|
|
28698
|
+
# The derivation is proved by feeding the check a member the header cannot
|
|
28699
|
+
# contain. Without this the whole scenario is satisfied by a list that
|
|
28700
|
+
# happens to agree with a header nobody compared it to.
|
|
28701
|
+
missing = config_header_problem(names + ["invented_declaration"], flat_header)
|
|
28702
|
+
if not missing:
|
|
28703
|
+
report(
|
|
28704
|
+
f"{label}: the assertion is a literal — a declaration absent from "
|
|
28705
|
+
"the header produced no failure, so the check is not reading the "
|
|
28706
|
+
"exported list."
|
|
28707
|
+
)
|
|
28708
|
+
return 1
|
|
28709
|
+
if "invented_declaration" not in missing:
|
|
28710
|
+
report(
|
|
28711
|
+
f"{label}: the failure does not name the missing declaration; got "
|
|
28712
|
+
f"{missing!r}."
|
|
28713
|
+
)
|
|
28714
|
+
return 1
|
|
28715
|
+
if re.search(r"\b(?:four|five|six|seven)\b", missing, re.IGNORECASE):
|
|
28716
|
+
report(
|
|
28717
|
+
f"{label}: still says six declarations — the failure reports a "
|
|
28718
|
+
f"count rather than the missing name; got {missing!r}."
|
|
28719
|
+
)
|
|
28720
|
+
return 1
|
|
28721
|
+
|
|
28722
|
+
# A header naming every declaration while miscounting them in prose passes:
|
|
28723
|
+
# membership is the checkable property, and a count is a lossy restatement.
|
|
28724
|
+
miscounted = flat_header.replace(
|
|
28725
|
+
"Six independent declarations", "Five independent declarations"
|
|
28726
|
+
)
|
|
28727
|
+
if miscounted == flat_header:
|
|
28728
|
+
report(f"{label}: the header's prose count could not be located to vary it.")
|
|
28729
|
+
return 1
|
|
28730
|
+
if config_header_problem(names, miscounted):
|
|
28731
|
+
report(
|
|
28732
|
+
f"{label}: still says six declarations — a header naming all of them "
|
|
28733
|
+
"while miscounting them in prose was refused."
|
|
28734
|
+
)
|
|
28735
|
+
return 1
|
|
28736
|
+
|
|
28737
|
+
if label not in {name for name, _ in SCENARIOS}:
|
|
28738
|
+
report(f"{label}: the scenario registry does not include it.")
|
|
28739
|
+
return 1
|
|
28740
|
+
report(f"{label} scenario passed.")
|
|
28741
|
+
return 0
|
|
28742
|
+
|
|
28743
|
+
|
|
28462
28744
|
SCENARIOS: tuple = (
|
|
28463
28745
|
("stateless-continuity", validate_stateless_continuity_scenario),
|
|
28464
28746
|
("core-gates", validate_core_gates_scenario),
|
|
@@ -28817,6 +29099,14 @@ SCENARIOS: tuple = (
|
|
|
28817
29099
|
"a-claim-names-what-would-falsify-it",
|
|
28818
29100
|
validate_a_claim_names_what_would_falsify_it_scenario,
|
|
28819
29101
|
),
|
|
29102
|
+
(
|
|
29103
|
+
"a-negation-is-not-a-marker",
|
|
29104
|
+
validate_a_negation_is_not_a_marker_scenario,
|
|
29105
|
+
),
|
|
29106
|
+
(
|
|
29107
|
+
"a-count-is-derived-from-what-it-counts",
|
|
29108
|
+
validate_a_count_is_derived_from_what_it_counts_scenario,
|
|
29109
|
+
),
|
|
28820
29110
|
)
|
|
28821
29111
|
|
|
28822
29112
|
|
package/src/core/config.js
CHANGED
|
@@ -68,6 +68,21 @@ function classifyAuthorizationEntry(entry) {
|
|
|
68
68
|
// and the declaration must still be correct after both.
|
|
69
69
|
const DELEGATION_TIERS = ["routine", "standard", "deep"];
|
|
70
70
|
|
|
71
|
+
// Every declaration this module reads, in the order the file documents them. One
|
|
72
|
+
// list, so an assertion about the set can be derived from the set instead of
|
|
73
|
+
// restating it: the header check used to pin an English numeral and had been
|
|
74
|
+
// hand-bumped twice, each time discovered by a red suite run in a scenario named
|
|
75
|
+
// for something else (issue #143). Adding a declaration now costs a header line
|
|
76
|
+
// and an entry here, and nothing has to agree about a number.
|
|
77
|
+
const CONFIG_DECLARATIONS = [
|
|
78
|
+
"fast_check",
|
|
79
|
+
"authorize",
|
|
80
|
+
"precedents",
|
|
81
|
+
"triage",
|
|
82
|
+
"delegation",
|
|
83
|
+
"full_mode_paths",
|
|
84
|
+
];
|
|
85
|
+
|
|
71
86
|
const CONFIG_RELATIVE_PATH = path.join("keel", "config.yaml");
|
|
72
87
|
|
|
73
88
|
// The declarations share keel/config.yaml with fast_check, so the reader stays
|
|
@@ -533,6 +548,7 @@ function triageIssue(repo, labels, issue = null) {
|
|
|
533
548
|
module.exports = {
|
|
534
549
|
CONFIG_RELATIVE_PATH,
|
|
535
550
|
DELEGATION_TIERS,
|
|
551
|
+
CONFIG_DECLARATIONS,
|
|
536
552
|
STANDING_AUTHORIZATION_ACTIONS,
|
|
537
553
|
SCOPED_AUTHORIZATION_ACTIONS,
|
|
538
554
|
readFullModePaths,
|
package/src/core/gates.js
CHANGED
|
@@ -672,7 +672,24 @@ function transientOwnerMessage(candidate) {
|
|
|
672
672
|
// swallow and changes nothing about why this one is narrow. The match is
|
|
673
673
|
// global because each resolved claim owes its own evidence; checking only the
|
|
674
674
|
// first would let a second one assert itself for free.
|
|
675
|
-
|
|
675
|
+
// A marker is a disposition only where it OPENS its clause: the start of the
|
|
676
|
+
// value, a line break, or sentence-ending punctuation may precede it, and a word
|
|
677
|
+
// may not. `Not resolved here:` contains the marker and means its opposite, and a
|
|
678
|
+
// `\b` boundary sat happily at the space inside it — the negation was read as a
|
|
679
|
+
// resolution claim and the next word, `it`, as its evidence, so a dismissal was
|
|
680
|
+
// read as a repair and then refused for lacking repair evidence (issue #144).
|
|
681
|
+
// Quoting was the first way a marker could appear without being one (5.42.0);
|
|
682
|
+
// negating it is the second.
|
|
683
|
+
//
|
|
684
|
+
// Written as a lookbehind so the captures keep reading from the same offsets —
|
|
685
|
+
// every rule here is positional, and a rule that shortened the text would move
|
|
686
|
+
// what they read.
|
|
687
|
+
const CLAUSE_OPENING = "(?<=^|[\\n.;:!?\u2014\u2013])[ \\t]*";
|
|
688
|
+
|
|
689
|
+
const RESOLVED_HERE = new RegExp(
|
|
690
|
+
`${CLAUSE_OPENING}resolved here\\s*:[ \\t]*(\\S*)`,
|
|
691
|
+
"gi"
|
|
692
|
+
);
|
|
676
693
|
|
|
677
694
|
// Resolution evidence is deliberately narrower than a durable owner. An
|
|
678
695
|
// `http`/`https` reference says someone else will do the work later, which is
|
|
@@ -714,8 +731,24 @@ function resolutionEvidenceVerdict(repo, value, commands, change) {
|
|
|
714
731
|
// next token, so a shortened text would move what they read. The original is
|
|
715
732
|
// kept for anything reported back, since a diagnostic quoting the blanked copy
|
|
716
733
|
// would show the author a sentence with holes in it.
|
|
717
|
-
|
|
718
|
-
|
|
734
|
+
// The same opening rule, for the same reason. Narrowing only the capture would
|
|
735
|
+
// leave a negated marker counted as a disposition being *present* while it
|
|
736
|
+
// supplied no evidence — a finding accepted as disposed with nothing behind it,
|
|
737
|
+
// which is worse than either half of the defect alone.
|
|
738
|
+
// The marker vocabulary wherever it appears, with no opening rule. Two different
|
|
739
|
+
// questions are asked of this text and they need different patterns. *Blanking* a
|
|
740
|
+
// marker inside a quoted span asks "is this marker text?" — and inside a span the
|
|
741
|
+
// character before it is a backtick, so an opening rule would refuse to blank it
|
|
742
|
+
// and the quotation would survive into the recognition scan. *Recognizing* a
|
|
743
|
+
// disposition asks "does this marker open a clause?", which is the narrower
|
|
744
|
+
// question `DISPOSITION_MARKER` below answers.
|
|
745
|
+
const MARKER_VOCABULARY =
|
|
746
|
+
/(?:resolved here|durable owner|discard (?:reason|rationale))\s*:/gi;
|
|
747
|
+
|
|
748
|
+
const DISPOSITION_MARKER = new RegExp(
|
|
749
|
+
`${CLAUSE_OPENING}(?:resolved here|durable owner|discard (?:reason|rationale))\\s*:`,
|
|
750
|
+
"gi"
|
|
751
|
+
);
|
|
719
752
|
|
|
720
753
|
function withoutQuotedMarkers(text) {
|
|
721
754
|
return String(text || "").replace(/`[^`\n]*`/g, (span) =>
|
|
@@ -725,7 +758,7 @@ function withoutQuotedMarkers(text) {
|
|
|
725
758
|
// span would destroy the very path the rule exists to read. Measured: the
|
|
726
759
|
// first draft of this function did exactly that, and both the backticked
|
|
727
760
|
// owner path and the backticked resolution path started failing.
|
|
728
|
-
span.replace(
|
|
761
|
+
span.replace(MARKER_VOCABULARY, (marker) => " ".repeat(marker.length))
|
|
729
762
|
);
|
|
730
763
|
}
|
|
731
764
|
|
|
@@ -1269,6 +1302,16 @@ function completionChecks(repo, task, contract = null, changeVerify = null, chan
|
|
|
1269
1302
|
+ "must still do is `Durable owner:` naming "
|
|
1270
1303
|
+ `${DURABLE_OWNER_FORMS}; one deliberately not being done is a `
|
|
1271
1304
|
+ "`Discard reason:`/`Discard rationale:` prefix."
|
|
1305
|
+
// Said only where a marker is visibly present and was not counted,
|
|
1306
|
+
// which is the one case where "carry a disposition" reads as a
|
|
1307
|
+
// contradiction of what the author can see they wrote.
|
|
1308
|
+
+ (mentionsUnopenedMarker(scannable)
|
|
1309
|
+
? " A marker counts only where it opens its clause — after the "
|
|
1310
|
+
+ "start of the value, a line break, or sentence-ending "
|
|
1311
|
+
+ "punctuation. This text mentions one mid-sentence, so it was "
|
|
1312
|
+
+ "read as prose; move it to the start of its own clause, or "
|
|
1313
|
+
+ "keep it as prose and add the disposition separately."
|
|
1314
|
+
: "")
|
|
1272
1315
|
)
|
|
1273
1316
|
);
|
|
1274
1317
|
}
|
|
@@ -1276,6 +1319,15 @@ function completionChecks(repo, task, contract = null, changeVerify = null, chan
|
|
|
1276
1319
|
return { problems, reviewProblems };
|
|
1277
1320
|
}
|
|
1278
1321
|
|
|
1322
|
+
// Whether the text names a disposition marker somewhere that is not a clause
|
|
1323
|
+
// opening. Used only to add a sentence to a refusal, never to accept anything:
|
|
1324
|
+
// a marker mid-sentence stays prose, and this is what tells its author why.
|
|
1325
|
+
function mentionsUnopenedMarker(text) {
|
|
1326
|
+
const body = String(text || "");
|
|
1327
|
+
if (!new RegExp(MARKER_VOCABULARY.source, "i").test(body)) return false;
|
|
1328
|
+
return !new RegExp(DISPOSITION_MARKER.source, "i").test(body);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1279
1331
|
function taskComplete(repo, options) {
|
|
1280
1332
|
const selection = loadSelection(repo, options);
|
|
1281
1333
|
const task = selection.selected[0];
|