alissa-tools-github-orcloop 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- {alissa_tools_github_orcloop-0.3.8/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.9}/PKG-INFO +1 -1
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/ghclient.py +18 -1
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/loop.py +384 -1
- alissa_tools_github_orcloop-0.3.9/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_orcloop-0.3.8/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/README.md +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/alissa_client.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/config.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/state.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.3.8 → alissa_tools_github_orcloop-0.3.9}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -127,11 +127,20 @@ class PullRequest:
|
|
|
127
127
|
body: str
|
|
128
128
|
merged: bool
|
|
129
129
|
url: str
|
|
130
|
+
# The merge commit's SHA (`merge_commit_sha`), for anchoring the close
|
|
131
|
+
# evidence to a point in history. Defaulted rather than required: it is
|
|
132
|
+
# meaningful only on a merged PR, and a payload that omits it must degrade
|
|
133
|
+
# to "unknown" in the evidence rather than fail the close walk.
|
|
134
|
+
merge_sha: str = ""
|
|
130
135
|
|
|
131
136
|
@property
|
|
132
137
|
def full_name(self) -> str:
|
|
133
138
|
return f"{self.owner}/{self.repo}"
|
|
134
139
|
|
|
140
|
+
@property
|
|
141
|
+
def pr_slug(self) -> str:
|
|
142
|
+
return f"{self.owner}/{self.repo}#{self.number}"
|
|
143
|
+
|
|
135
144
|
|
|
136
145
|
class RateLimited(RuntimeError):
|
|
137
146
|
"""A `gh` rate-limit signal -- a retry-later condition, distinct from a
|
|
@@ -377,7 +386,14 @@ class GitHub:
|
|
|
377
386
|
"""One PR's close-loop-relevant fields, or None if it cannot be read.
|
|
378
387
|
A malformed/empty payload degrades to None rather than a fabricated PR
|
|
379
388
|
-- the close-loop treats "unreadable" as "not the closer, keep
|
|
380
|
-
looking", never as a merge.
|
|
389
|
+
looking", never as a merge.
|
|
390
|
+
|
|
391
|
+
`body` and `merge_sha` come from this ONE payload, which is why the
|
|
392
|
+
close edge's delivery-record evidence (O15) costs no extra round-trip:
|
|
393
|
+
the PR the close walk already resolved carries everything the evidence
|
|
394
|
+
needs. A field the payload omits degrades to the empty string here and
|
|
395
|
+
is reported as missing at the render site -- never fabricated, never
|
|
396
|
+
fetched a second time."""
|
|
381
397
|
data = self._api(f"repos/{owner}/{repo}/pulls/{number}")
|
|
382
398
|
if not isinstance(data, dict) or not data:
|
|
383
399
|
return None
|
|
@@ -390,6 +406,7 @@ class GitHub:
|
|
|
390
406
|
body=data.get("body") or "",
|
|
391
407
|
merged=bool(data.get("merged") or data.get("merged_at")),
|
|
392
408
|
url=data.get("html_url") or "",
|
|
409
|
+
merge_sha=str(data.get("merge_commit_sha") or ""),
|
|
393
410
|
)
|
|
394
411
|
|
|
395
412
|
def _referenced_pr_numbers(self, owner: str, repo: str, number: int) -> list[int]:
|
|
@@ -219,6 +219,13 @@ released before a feed switch):
|
|
|
219
219
|
merged-PR evidence, and move the task `committed -> in_progress ->
|
|
220
220
|
pending_validation` (the two-step transition the status matrix requires;
|
|
221
221
|
orcloop hands off at pending_validation, never validates for the operator).
|
|
222
|
+
That evidence carries the merged PR's DELIVERY RECORD -- its `## Delivery
|
|
223
|
+
notes` section, or the whole body when there is none, anchored by PR title
|
|
224
|
+
and merge SHA (O15, `render_close_evidence`). A task arriving at
|
|
225
|
+
`pending_validation` has to be validatable from what is attached to it; a
|
|
226
|
+
bare "closed by <url>" makes the reader re-derive every criterion from
|
|
227
|
+
GitHub. An unreadable write-up degrades loudly and never blocks the walk --
|
|
228
|
+
the merge already happened, so holding the close would only strand the task.
|
|
222
229
|
* Closed with NO merged closing PR -> the `closed-unmerged` escalation only;
|
|
223
230
|
the task is NOT mutated (a human closed it, or the work was abandoned).
|
|
224
231
|
* The origin task already moved on (validated/cancelled/blocked/... -- the
|
|
@@ -637,6 +644,116 @@ CLOSE_EVIDENCE_BODY = (
|
|
|
637
644
|
"operator validation."
|
|
638
645
|
)
|
|
639
646
|
|
|
647
|
+
# O15: the confirmation sentence above is the CLAIM; everything below is the
|
|
648
|
+
# delivery record that lets a validator check it without leaving the task. The
|
|
649
|
+
# two anchor lines are always present, on every path -- a bare slug and URL is
|
|
650
|
+
# what the operator's soft validator rejected on studio TASK-1156925553.
|
|
651
|
+
CLOSE_EVIDENCE_PR_LINE = "**Pull request:** {pr_slug} — {title}"
|
|
652
|
+
CLOSE_EVIDENCE_SHA_LINE = "**Merge commit:** `{sha}`"
|
|
653
|
+
CLOSE_EVIDENCE_TITLE_MISSING = "_(the pull request payload carried no title)_"
|
|
654
|
+
CLOSE_EVIDENCE_SHA_MISSING = "unknown"
|
|
655
|
+
|
|
656
|
+
# The heading over the included record, in its two shapes. Each names WHICH of
|
|
657
|
+
# the two it is, so the reader never has to guess whether they are looking at a
|
|
658
|
+
# curated section or the whole body.
|
|
659
|
+
CLOSE_EVIDENCE_NOTES_HEADING = "## Delivery notes (from the merged pull request)"
|
|
660
|
+
# "no write-up" rather than "no section": a heading with nothing under it is one
|
|
661
|
+
# of the shapes that lands here, and this heading must not deny it exists when
|
|
662
|
+
# the body below plainly shows it.
|
|
663
|
+
CLOSE_EVIDENCE_FULL_HEADING = (
|
|
664
|
+
"## Pull request body (no `## Delivery notes` write-up — full body included)"
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# The heading the delivery record is written under. Matched line-anchored and
|
|
668
|
+
# case-insensitively, but otherwise EXACTLY: see `extract_delivery_notes` for
|
|
669
|
+
# why this one is strict where O14's context anchors are lenient.
|
|
670
|
+
#
|
|
671
|
+
# WHO EMITS IT: nothing in THIS repo. The producer is the develop daemon's
|
|
672
|
+
# worker directive -- a companion task in the develop-daemon lane, fed the same
|
|
673
|
+
# day as issue #36 and explicitly out of scope for it -- which tells an
|
|
674
|
+
# implementing session to write this section into its PR body. So until that
|
|
675
|
+
# lands, every close legitimately takes the full-body branch; the fallback is
|
|
676
|
+
# the norm today and the section is the arriving case, not the other way round.
|
|
677
|
+
# Both branches are load-bearing and neither is dead code: a PR authored by a
|
|
678
|
+
# human, or by any daemon that never adopts the directive, will always want the
|
|
679
|
+
# fallback.
|
|
680
|
+
DELIVERY_NOTES_HEADING = "## Delivery notes"
|
|
681
|
+
|
|
682
|
+
# The cap on the included record. Generous on purpose -- a delivery write-up is
|
|
683
|
+
# thousands of chars, and evidence is markdown on a task rather than a GitHub
|
|
684
|
+
# body, so there is no 65,536-char cliff to fit under. It exists so one
|
|
685
|
+
# pathological PR body cannot turn every close into a megabyte write, not to
|
|
686
|
+
# shape the common case. A constant, not a config key, for the reason O14's
|
|
687
|
+
# context caps are: documented caps are a promise, tunable ones are a
|
|
688
|
+
# compatibility commitment.
|
|
689
|
+
#
|
|
690
|
+
# THE HEADROOM IS MEASURED, not assumed, because the size of this write is the
|
|
691
|
+
# one way this feature could newly BLOCK a close: `add_evidence` is not wrapped
|
|
692
|
+
# in `_mutate_close`, `_close_loop` routes its `AlissaError` to
|
|
693
|
+
# `_closeloop_failed`, and `set_status(pending_validation)` comes after it -- so
|
|
694
|
+
# a rejection that is deterministic rather than transient would strand the
|
|
695
|
+
# origin task at `in_progress` on every future poll. `AddEvidenceRequest`
|
|
696
|
+
# declares `markdownContent` with a `minLength` and NO `maxLength`, so the
|
|
697
|
+
# ceiling was probed directly against the live endpoint (2026-07-28, disposable
|
|
698
|
+
# task, since cancelled):
|
|
699
|
+
#
|
|
700
|
+
# 32,300 -> 201 131,072 -> 201 524,288 -> 201
|
|
701
|
+
# 65,536 -> 201 262,144 -> 201 1,048,576 -> 500
|
|
702
|
+
#
|
|
703
|
+
# Worst case here is this cap plus the confirmation line and the two anchors,
|
|
704
|
+
# ~32.3k -- sixteen times under the largest write the endpoint accepted. Raising
|
|
705
|
+
# this constant is therefore NOT a free edit: past ~512k the write starts
|
|
706
|
+
# failing, and it fails in the one direction this feature promises never to
|
|
707
|
+
# fail. Re-probe before widening it.
|
|
708
|
+
CLOSE_EVIDENCE_RECORD_MAX_CHARS = 32000
|
|
709
|
+
# Appended verbatim (issue #36's own wording). No silent caps.
|
|
710
|
+
#
|
|
711
|
+
# The marker ALONE is not the "was anything cut" signal, and the counterexample
|
|
712
|
+
# is this repo's own PR #37: the record is the PR body verbatim, that body
|
|
713
|
+
# mentions the marker inside the very section that gets extracted, and so the
|
|
714
|
+
# first evidence item this feature ever produced would answer a bare-literal
|
|
715
|
+
# grep wrong. `CLOSE_EVIDENCE_TRUNCATED_RE` below is the signal -- deliberately
|
|
716
|
+
# the only place the pattern is written down, because a comment restating it is
|
|
717
|
+
# a second copy to drift from (and, found the hard way, a second copy for a
|
|
718
|
+
# counterfactual to patch instead of the code).
|
|
719
|
+
CLOSE_EVIDENCE_TRUNCATED_MARKER = "[PR body truncated — read the PR]"
|
|
720
|
+
# The signal as a PATTERN rather than as prose: whoever greps an evidence item
|
|
721
|
+
# matches the same thing `_truncate_record` emits, and tests pin both directions
|
|
722
|
+
# so the two cannot drift apart. A promise about a diagnostic is worth exactly as
|
|
723
|
+
# much as the mechanism that keeps it true.
|
|
724
|
+
#
|
|
725
|
+
# ANCHORED at the end, which is what makes the ceiling below exactly true rather
|
|
726
|
+
# than merely nearly so. A real truncation is always LAST in the evidence:
|
|
727
|
+
# `_truncate_record` appends the marker after the record it cut, and
|
|
728
|
+
# `render_close_evidence` appends that record after everything else. Unanchored,
|
|
729
|
+
# a forged line ANYWHERE in a body would read as truncated; anchored, only a body
|
|
730
|
+
# whose own end forges it can. `\s*` before `\Z` so a consumer that normalises
|
|
731
|
+
# trailing whitespace still matches -- trailing blanks cannot forge anything.
|
|
732
|
+
# The end-position invariant this depends on is pinned by a test, so a future
|
|
733
|
+
# change that appends anything after the record fails loudly instead of silently
|
|
734
|
+
# turning a real truncation undetectable. A false NEGATIVE is the worse failure
|
|
735
|
+
# here, exactly as it is for O13's warning.
|
|
736
|
+
#
|
|
737
|
+
# THE CEILING, stated so nobody over-trusts it: a body whose end is a forged
|
|
738
|
+
# marker-and-counts line is indistinguishable from a real truncation. That needs
|
|
739
|
+
# merge access to the target repo, which is the same trust boundary
|
|
740
|
+
# `render_close_evidence` scopes for the included text as a whole -- so it bounds
|
|
741
|
+
# the diagnostic rather than opening a hole to escape.
|
|
742
|
+
CLOSE_EVIDENCE_TRUNCATED_RE = re.compile(
|
|
743
|
+
re.escape(CLOSE_EVIDENCE_TRUNCATED_MARKER) + r" \(\d+ of \d+ chars included\)\s*\Z"
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# The loud degradation. The close walk NEVER stops for this: the merge already
|
|
747
|
+
# happened, and holding the close would strand the origin task in `in_progress`
|
|
748
|
+
# for a missing write-up. (The opposite trade from O14's context hold, where
|
|
749
|
+
# nothing has happened yet and the release can safely wait.)
|
|
750
|
+
CLOSE_EVIDENCE_UNRETRIEVABLE = (
|
|
751
|
+
"**The pull request body could not be retrieved**, so this evidence carries "
|
|
752
|
+
"the merge confirmation only — open the pull request for the delivery "
|
|
753
|
+
"record. The close-loop hand-off proceeded regardless: the merge is a fact "
|
|
754
|
+
"whether or not its write-up can be read."
|
|
755
|
+
)
|
|
756
|
+
|
|
640
757
|
|
|
641
758
|
class Action(str, Enum):
|
|
642
759
|
RELEASED = "released"
|
|
@@ -2589,6 +2706,12 @@ class Orchestrator:
|
|
|
2589
2706
|
record). A retry after the write succeeded finds the marker and skips,
|
|
2590
2707
|
so evidence is never duplicated.
|
|
2591
2708
|
|
|
2709
|
+
The evidence body itself is the merged PR's DELIVERY RECORD, not a bare
|
|
2710
|
+
merge confirmation -- see `render_close_evidence`. It is rendered from
|
|
2711
|
+
the `pr` object this method was handed, so it adds no call and no
|
|
2712
|
+
failure mode to the walk: a PR whose write-up cannot be read still
|
|
2713
|
+
reaches `pending_validation`, carrying an explicit line saying so.
|
|
2714
|
+
|
|
2592
2715
|
Criteria satisfaction is naturally idempotent (already-satisfied ones are
|
|
2593
2716
|
skipped), so it needs no gate."""
|
|
2594
2717
|
if task.status == TASK_TODO_STATE:
|
|
@@ -2600,7 +2723,7 @@ class Orchestrator:
|
|
|
2600
2723
|
self.alissa.add_evidence(
|
|
2601
2724
|
ref,
|
|
2602
2725
|
CLOSE_EVIDENCE_TITLE,
|
|
2603
|
-
|
|
2726
|
+
render_close_evidence(slug, pr),
|
|
2604
2727
|
)
|
|
2605
2728
|
self.state.record_escalation(task_number, CLOSELOOP_EVIDENCE_DONE)
|
|
2606
2729
|
self.alissa.set_status(ref, "pending_validation")
|
|
@@ -3117,3 +3240,263 @@ def render_issue_body(
|
|
|
3117
3240
|
"""`render_issue(...).body` -- the body alone, for callers with nothing to
|
|
3118
3241
|
report about how it was rendered."""
|
|
3119
3242
|
return render_issue(task, target, scope, config, context).body
|
|
3243
|
+
|
|
3244
|
+
|
|
3245
|
+
# --- the close edge's delivery-record evidence (O15) -----------------------
|
|
3246
|
+
|
|
3247
|
+
# POSSESSIVE quantifiers (`*+`, python >= 3.11, which setup.py already requires)
|
|
3248
|
+
# on all three whitespace runs. Greedy `*` makes the tail CUBIC on a line that
|
|
3249
|
+
# starts with the heading and then fails: the engine tries every way to split
|
|
3250
|
+
# the trailing whitespace across `[ \t]* … [ \t]* … [ \t]*`. Measured on
|
|
3251
|
+
# `"## Delivery notes" + " "*n + "X"`: 20ms at n=200, 2.7s at n=800, 24.7s at
|
|
3252
|
+
# n=1600 -- and this regex runs inside `_mutate_close`, on a single-threaded
|
|
3253
|
+
# poll loop with no timeout, so a crafted body would stall the WHOLE daemon,
|
|
3254
|
+
# release edge included, on every poll forever. Possessive quantifiers cannot
|
|
3255
|
+
# backtrack, which takes the same n=65,000 input from days to 0.045ms. They are
|
|
3256
|
+
# safe here precisely because the three runs are DISJOINT character classes
|
|
3257
|
+
# (whitespace / `:` / `#`), so refusing to give text back can never starve a
|
|
3258
|
+
# later token: every tolerance case below still matches and every rejection
|
|
3259
|
+
# still fails.
|
|
3260
|
+
_DELIVERY_NOTES_RE = re.compile(
|
|
3261
|
+
rf"^[ ]{{0,3}}{re.escape(DELIVERY_NOTES_HEADING)}[ \t]*+:?[ \t]*+#*+[ \t]*+\r?$",
|
|
3262
|
+
re.IGNORECASE,
|
|
3263
|
+
)
|
|
3264
|
+
# A sibling-or-parent heading: `# ` or `## `. A `### ` subsection belongs to the
|
|
3265
|
+
# delivery notes and must NOT end them.
|
|
3266
|
+
_SECTION_END_RE = re.compile(r"^[ ]{0,3}#{1,2}[ \t]")
|
|
3267
|
+
# The delimiter and whatever follows it on the line (the "info string"), which
|
|
3268
|
+
# decides whether a fence line can CLOSE a block.
|
|
3269
|
+
_FENCE_RE = re.compile(r"^[ ]{0,3}(`{3,}|~{3,})(.*)$")
|
|
3270
|
+
|
|
3271
|
+
|
|
3272
|
+
def _fence_scan(lines: Sequence[str]) -> tuple[list[bool], str]:
|
|
3273
|
+
"""`(mask, open_fence)` for a markdown document: which lines sit in (or
|
|
3274
|
+
delimit) a fenced code block, and the delimiter still open at the end.
|
|
3275
|
+
|
|
3276
|
+
Both halves come from ONE pass on purpose. A caller that needs to close a
|
|
3277
|
+
block must close it with the delimiter that opened it, and re-deriving that
|
|
3278
|
+
by scanning backwards for "a fence line" is a guess -- the nearest fence
|
|
3279
|
+
line may be an inner one of the other character, or a shorter one, neither
|
|
3280
|
+
of which opened anything. The scan already knows; it hands the answer back
|
|
3281
|
+
rather than making the caller reconstruct it.
|
|
3282
|
+
|
|
3283
|
+
CommonMark's three closing rules, all of which matter here:
|
|
3284
|
+
|
|
3285
|
+
* **Same character.** A `~~~` inside a ``` block is content. Conflating the
|
|
3286
|
+
two ends the block early and hands the code after it back to the heading
|
|
3287
|
+
scan -- the failure the fence tracking exists to prevent.
|
|
3288
|
+
* **At least as long.** ``` cannot close a ```` block, so an inner shorter
|
|
3289
|
+
fence is content too.
|
|
3290
|
+
* **No info string.** ```` ```python ```` opens a block; it can never close
|
|
3291
|
+
one. Without this rule a `## Delivery notes` heading sitting inside an
|
|
3292
|
+
unclosed block reads as a real heading, and because the scan takes the
|
|
3293
|
+
FIRST candidate with content, a fake section early in a body would outrank
|
|
3294
|
+
the real one later -- the one direction this module promises not to err.
|
|
3295
|
+
|
|
3296
|
+
An unclosed fence runs to the end of the document, which errs toward
|
|
3297
|
+
treating more text as code and so toward including more of the body: the
|
|
3298
|
+
same direction as every other tie-break here.
|
|
3299
|
+
"""
|
|
3300
|
+
fenced = [False] * len(lines)
|
|
3301
|
+
open_fence = ""
|
|
3302
|
+
for index, line in enumerate(lines):
|
|
3303
|
+
match = _FENCE_RE.match(line)
|
|
3304
|
+
if match:
|
|
3305
|
+
delimiter, info = match.group(1), match.group(2).strip()
|
|
3306
|
+
closes = (
|
|
3307
|
+
open_fence
|
|
3308
|
+
and delimiter[0] == open_fence[0]
|
|
3309
|
+
and len(delimiter) >= len(open_fence)
|
|
3310
|
+
and not info
|
|
3311
|
+
)
|
|
3312
|
+
if closes or not open_fence:
|
|
3313
|
+
# A delimiter line is not content either way; marking it keeps
|
|
3314
|
+
# the line that OPENS a block from reading as a heading.
|
|
3315
|
+
fenced[index] = True
|
|
3316
|
+
open_fence = "" if closes else delimiter
|
|
3317
|
+
continue
|
|
3318
|
+
# A fence line that cannot close the open block IS content.
|
|
3319
|
+
fenced[index] = bool(open_fence)
|
|
3320
|
+
return fenced, open_fence
|
|
3321
|
+
|
|
3322
|
+
|
|
3323
|
+
def extract_delivery_notes(body: str) -> str | None:
|
|
3324
|
+
"""The `## Delivery notes` section of a pull-request body, or None when the
|
|
3325
|
+
body has no such section with anything under it.
|
|
3326
|
+
|
|
3327
|
+
THE TIE-BREAK, and why it is the opposite of O14's. Both this and
|
|
3328
|
+
`strip_context_section` locate a section by an ambiguous heading, but the
|
|
3329
|
+
costs are mirrored. There, a wrong START anchor SUPPRESSED a diagnostic, so
|
|
3330
|
+
every tie broke toward doing nothing. Here, "do nothing" means falling back
|
|
3331
|
+
to the FULL body -- a strict superset of any section this could have found.
|
|
3332
|
+
Guessing wrong and matching a section costs the reader the rest of the
|
|
3333
|
+
write-up; guessing wrong and matching nothing costs them some scrolling. So
|
|
3334
|
+
the heading match is STRICT: the line, stripped of up to three leading
|
|
3335
|
+
spaces and an optional trailing colon, must be exactly the heading, differing
|
|
3336
|
+
at most in case. `## Delivery notes are still missing` is prose about the
|
|
3337
|
+
notes, not the notes, and reads as absent.
|
|
3338
|
+
|
|
3339
|
+
Strict about the PROSE, tolerant about the TYPOGRAPHY, and the CRLF case is
|
|
3340
|
+
why the distinction matters. This is the only end-of-line-anchored regex in
|
|
3341
|
+
the package -- every other line-anchored parser (`TASK_TRAILER_RE`, the
|
|
3342
|
+
`markers.py` templates, `_SECTION_END_RE`, `_FENCE_RE`) is a prefix match and
|
|
3343
|
+
so already tolerates a trailing `\r`. A PR body edited in GitHub's web UI
|
|
3344
|
+
comes back CRLF-normalised, which would have made every heading read as
|
|
3345
|
+
absent and sent a PR that plainly HAS delivery notes down the full-body
|
|
3346
|
+
branch, under a heading asserting it has none. So `\r` is admitted, as is a
|
|
3347
|
+
closed ATX heading (`## Delivery notes ##`) -- both are the same class of
|
|
3348
|
+
concession as the leading spaces and the case-fold, and neither can turn
|
|
3349
|
+
prose into a match. The scan still splits on `"\n"` and nothing else, for the
|
|
3350
|
+
reason `escape_control_lines` documents: `str.splitlines()` breaks on
|
|
3351
|
+
characters `^` does not recognise and this function JOINS its result back
|
|
3352
|
+
together, so it must not be able to invent a line boundary.
|
|
3353
|
+
|
|
3354
|
+
THE TERMINATOR is the next `# ` or `## ` -- a sibling or parent section.
|
|
3355
|
+
`### ` subsections are kept: a write-up structured as `### What shipped` /
|
|
3356
|
+
`### Verification` under the notes heading is the shape the devloop
|
|
3357
|
+
directive asks for, and ending on the first `###` would take the first
|
|
3358
|
+
paragraph and drop the substance.
|
|
3359
|
+
|
|
3360
|
+
FENCED CODE IS SKIPPED, for both the heading scan and the terminator scan
|
|
3361
|
+
(see `_fence_scan`). A delivery write-up quotes shell and diffs, where a
|
|
3362
|
+
line can legitimately begin with `## `, and a code fence is the one context
|
|
3363
|
+
in markdown where a hash is not a heading.
|
|
3364
|
+
|
|
3365
|
+
EMPTY SECTIONS DO NOT COUNT. A heading with nothing under it is not a
|
|
3366
|
+
delivery record, and returning it would suppress the full-body fallback --
|
|
3367
|
+
the one failure here that actually loses information. The scan therefore
|
|
3368
|
+
continues past an empty candidate to a later one, and answers None only if
|
|
3369
|
+
no candidate has content.
|
|
3370
|
+
"""
|
|
3371
|
+
lines = (body or "").split("\n")
|
|
3372
|
+
fenced, _ = _fence_scan(lines)
|
|
3373
|
+
|
|
3374
|
+
for start, line in enumerate(lines):
|
|
3375
|
+
if fenced[start] or not _DELIVERY_NOTES_RE.match(line):
|
|
3376
|
+
continue
|
|
3377
|
+
end = len(lines)
|
|
3378
|
+
for index in range(start + 1, len(lines)):
|
|
3379
|
+
if not fenced[index] and _SECTION_END_RE.match(lines[index]):
|
|
3380
|
+
end = index
|
|
3381
|
+
break
|
|
3382
|
+
section = "\n".join(lines[start + 1:end]).strip()
|
|
3383
|
+
if section:
|
|
3384
|
+
return section
|
|
3385
|
+
return None
|
|
3386
|
+
|
|
3387
|
+
|
|
3388
|
+
def _open_fence(text: str) -> str:
|
|
3389
|
+
"""The code fence `text` leaves open, or `""` -- the delimiter itself, so
|
|
3390
|
+
the caller can close it with the characters that opened it. Taken from
|
|
3391
|
+
`_fence_scan`'s own tracking rather than re-derived: see there."""
|
|
3392
|
+
return _fence_scan(text.split("\n"))[1]
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
def _truncate_record(record: str) -> str:
|
|
3396
|
+
"""`record` capped at `CLOSE_EVIDENCE_RECORD_MAX_CHARS`, with the cut marked
|
|
3397
|
+
where it happened. The counts are appended after the marker rather than
|
|
3398
|
+
inside it so the marker stays greppable as the literal string issue #36
|
|
3399
|
+
named -- and `CLOSE_EVIDENCE_TRUNCATED_RE` is the actual "something was cut"
|
|
3400
|
+
signal, since a PR body may quote the bare literal (see the constant).
|
|
3401
|
+
|
|
3402
|
+
The marker is appended LAST, after the record and after any fence this
|
|
3403
|
+
function had to close. `CLOSE_EVIDENCE_TRUNCATED_RE` is anchored to the end
|
|
3404
|
+
of the evidence on the strength of that, so anything appended after the
|
|
3405
|
+
record -- here or in `render_close_evidence` -- would make a real truncation
|
|
3406
|
+
undetectable. `test_a_real_cut_ends_the_evidence_item` pins it.
|
|
3407
|
+
|
|
3408
|
+
The counts describe what is REALLY there. The slice is `.rstrip()`ed, so
|
|
3409
|
+
reporting the cap would overstate by the whitespace dropped -- a record cut
|
|
3410
|
+
mid-whitespace-run could claim 32,000 chars included and carry seven. Only
|
|
3411
|
+
whitespace is ever lost that way, so this is about the number being exactly
|
|
3412
|
+
true rather than about content. The re-added fence below is NOT counted: it
|
|
3413
|
+
is this function's own text, not the record's.
|
|
3414
|
+
|
|
3415
|
+
A cut that lands INSIDE a fenced code block closes the fence first. The cut
|
|
3416
|
+
is a character slice, so a record whose fence opens before the cap and
|
|
3417
|
+
closes after it would otherwise leave the block open -- and the marker,
|
|
3418
|
+
appended after the cut, would render as program output inside that block.
|
|
3419
|
+
The text is present and greppable either way, so this is about the reader
|
|
3420
|
+
NOTICING, which is the whole claim "no silent caps" makes.
|
|
3421
|
+
"""
|
|
3422
|
+
original = len(record)
|
|
3423
|
+
if original <= CLOSE_EVIDENCE_RECORD_MAX_CHARS:
|
|
3424
|
+
return record
|
|
3425
|
+
kept = record[:CLOSE_EVIDENCE_RECORD_MAX_CHARS].rstrip()
|
|
3426
|
+
cut = kept
|
|
3427
|
+
fence = _open_fence(kept)
|
|
3428
|
+
if fence:
|
|
3429
|
+
cut += f"\n{fence}"
|
|
3430
|
+
return (
|
|
3431
|
+
cut
|
|
3432
|
+
+ f"\n\n{CLOSE_EVIDENCE_TRUNCATED_MARKER} "
|
|
3433
|
+
+ f"({len(kept)} of {original} chars included)"
|
|
3434
|
+
)
|
|
3435
|
+
|
|
3436
|
+
|
|
3437
|
+
def render_close_evidence(slug: str, pr: PullRequest) -> str:
|
|
3438
|
+
"""The close edge's evidence markdown: the merge confirmation, the two
|
|
3439
|
+
anchor lines, and the merged PR's delivery record.
|
|
3440
|
+
|
|
3441
|
+
Costs no round-trip. `GitHub.closing_merged_pr` already re-fetched the PR to
|
|
3442
|
+
decide it was the closer, and that payload carries the body and the merge
|
|
3443
|
+
SHA -- so the record is assembled from the object the close walk is holding,
|
|
3444
|
+
not fetched again. A second fetch would add a failure mode to a path whose
|
|
3445
|
+
whole point is that it must not acquire one.
|
|
3446
|
+
|
|
3447
|
+
DEGRADE, NEVER BLOCK. Every field is rendered from what the PR object
|
|
3448
|
+
actually has: a missing title and a missing merge SHA each say so in place,
|
|
3449
|
+
and a missing BODY drops the record for
|
|
3450
|
+
`CLOSE_EVIDENCE_UNRETRIEVABLE` -- today's evidence plus an explicit line.
|
|
3451
|
+
None of the three raises, because the caller is mid-`_mutate_close`: the
|
|
3452
|
+
merge has happened, the criteria are satisfied, and an exception here would
|
|
3453
|
+
strand the origin task short of `pending_validation` over a write-up.
|
|
3454
|
+
|
|
3455
|
+
THE INCLUDED BODY IS NOT ESCAPED, and the reason is narrower than "nothing
|
|
3456
|
+
parses evidence". O14 blockquotes control lines because an ISSUE body is
|
|
3457
|
+
read back by orcloop's own line-anchored trailer parsers; task evidence is
|
|
3458
|
+
not -- `add_evidence` is write-only and nothing in this package reads
|
|
3459
|
+
evidence back, so no text arriving here can reach a parser that decides
|
|
3460
|
+
anything. That is the claim, and it is about ORCLOOP. The evidence IS read
|
|
3461
|
+
by the operator's soft validator, which is an LLM, so this text is parsed --
|
|
3462
|
+
just not by anything whose answer moves a task. Anyone adding a machine
|
|
3463
|
+
consumer of evidence inherits no guarantee from this function. Note also
|
|
3464
|
+
what reaching this text costs an attacker: the close edge only fires on
|
|
3465
|
+
issues orcloop itself released, and the body must belong to a PR MERGED into
|
|
3466
|
+
that repo.
|
|
3467
|
+
|
|
3468
|
+
On the empty-body branch being defensive: through today's resolution path a
|
|
3469
|
+
resolved closer cannot have an empty body, because `closing_merged_pr`
|
|
3470
|
+
matched a closing keyword IN that body. That is a guarantee held by another
|
|
3471
|
+
function, and this one's contract -- evidence that never lies about what it
|
|
3472
|
+
contains -- must not depend on it. `GitHub.pull_request` degrades an absent
|
|
3473
|
+
`body` field to `""`, so a payload that omits it, or any future resolution
|
|
3474
|
+
rule that does not read the body, lands here and says so rather than
|
|
3475
|
+
emitting a heading with nothing under it.
|
|
3476
|
+
"""
|
|
3477
|
+
parts = [
|
|
3478
|
+
CLOSE_EVIDENCE_BODY.format(slug=slug, pr_url=pr.url),
|
|
3479
|
+
CLOSE_EVIDENCE_PR_LINE.format(
|
|
3480
|
+
pr_slug=pr.pr_slug, title=pr.title.strip() or CLOSE_EVIDENCE_TITLE_MISSING
|
|
3481
|
+
),
|
|
3482
|
+
CLOSE_EVIDENCE_SHA_LINE.format(
|
|
3483
|
+
sha=pr.merge_sha.strip() or CLOSE_EVIDENCE_SHA_MISSING
|
|
3484
|
+
),
|
|
3485
|
+
]
|
|
3486
|
+
body = (pr.body or "").strip()
|
|
3487
|
+
if not body:
|
|
3488
|
+
log.warning(
|
|
3489
|
+
"%s: the merged pull request %s carries no readable body — close "
|
|
3490
|
+
"evidence attached WITHOUT a delivery record (the close-loop "
|
|
3491
|
+
"hand-off is unaffected)",
|
|
3492
|
+
slug, pr.url or pr.pr_slug,
|
|
3493
|
+
)
|
|
3494
|
+
parts.append(CLOSE_EVIDENCE_UNRETRIEVABLE)
|
|
3495
|
+
return "\n\n".join(parts)
|
|
3496
|
+
|
|
3497
|
+
notes = extract_delivery_notes(body)
|
|
3498
|
+
heading = (
|
|
3499
|
+
CLOSE_EVIDENCE_NOTES_HEADING if notes is not None else CLOSE_EVIDENCE_FULL_HEADING
|
|
3500
|
+
)
|
|
3501
|
+
parts.append(f"{heading}\n\n{_truncate_record(notes if notes is not None else body)}")
|
|
3502
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.9
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.8
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|