alissa-tools-github-orcloop 0.3.3__tar.gz → 0.3.4__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.3/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.4}/PKG-INFO +1 -1
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/alissa_client.py +71 -10
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/loop.py +49 -15
- alissa_tools_github_orcloop-0.3.4/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_orcloop-0.3.3/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/README.md +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/config.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/state.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -95,6 +95,38 @@ Body of Work (otherwise 403 ``FORBIDDEN`` on attach/detach) -- an operator setup
|
|
|
95
95
|
step, documented in the README. orcloop never attaches: attaching is the
|
|
96
96
|
operator's trigger, detaching is orcloop's terminal release act.
|
|
97
97
|
|
|
98
|
+
THE DEPENDENCY GATE'S DECLARED COUNT (O10)
|
|
99
|
+
==========================================
|
|
100
|
+
A task detail declares its dependencies in **two** arrays, not one, and the
|
|
101
|
+
split is by dependency TYPE -- verified live against ``api.alissa.app`` while
|
|
102
|
+
implementing issue #26:
|
|
103
|
+
|
|
104
|
+
* ``dependsOn`` -- the ids of the **soft**-typed dependencies.
|
|
105
|
+
* ``hardDependsOn`` -- the ids of the **hard**-typed ones (key ABSENT, not
|
|
106
|
+
empty, when there are none).
|
|
107
|
+
* ``upstreamTasks`` -- the RESOLVED view of both, deduplicated, soft rows
|
|
108
|
+
first. There is no ``softDependsOn``.
|
|
109
|
+
|
|
110
|
+
Reading only ``dependsOn`` -- which is what shipped from O2 through O9 -- sized
|
|
111
|
+
the gate off the soft dependencies alone, so a task blocked purely by hard ones
|
|
112
|
+
declared zero and released (the O4 dry-run finding, repo issue #11 -> #26). The
|
|
113
|
+
count is now the size of the **union of the two id sets**.
|
|
114
|
+
|
|
115
|
+
Why the union of ids and not ``len(upstreamTasks)``, the other option the issue
|
|
116
|
+
offered: ``upstreamTasks`` is the resolved list, so counting it makes
|
|
117
|
+
``loop.dependencies_clear``'s visibility guard (`resolved != declared`)
|
|
118
|
+
tautologically true and an upstream this actor cannot see disappears from the
|
|
119
|
+
gate entirely -- trading one silent release for another. The declared ids are
|
|
120
|
+
the only count that comes from a source the actor's visibility cannot shrink.
|
|
121
|
+
|
|
122
|
+
Why a SET union and not the sum of the two lengths: the same upstream can be
|
|
123
|
+
linked BOTH ways (verified live -- the API accepts a second link of the other
|
|
124
|
+
type and lists that one id in both arrays), while ``upstreamTasks`` reports it
|
|
125
|
+
once. Summing would declare 2 against 1 resolved, and the conservative guard
|
|
126
|
+
would then hold a task whose only dependency is fully validated -- forever,
|
|
127
|
+
eventually paging ``release-blocked``. Deduplicating matches what the resolved
|
|
128
|
+
view actually does.
|
|
129
|
+
|
|
98
130
|
WHO AM I (O8)
|
|
99
131
|
=============
|
|
100
132
|
``GET /v1/ping`` -- ``{"pong", "timestamp", "userId", "actorId",
|
|
@@ -254,10 +286,12 @@ class Task:
|
|
|
254
286
|
criteria: tuple[Criterion, ...]
|
|
255
287
|
priority: str = ""
|
|
256
288
|
created_at: int = 0 # ms since epoch; 0 when the payload omits createdAt
|
|
257
|
-
# The resolved upstream tasks (statuses the
|
|
289
|
+
# The resolved upstream tasks (statuses the dependency gate reads).
|
|
258
290
|
dependencies: tuple[Dependency, ...] = ()
|
|
259
|
-
#
|
|
260
|
-
#
|
|
291
|
+
# How many dependencies the payload DECLARES: |dependsOn ∪ hardDependsOn|,
|
|
292
|
+
# both types, deduplicated (see the module docstring, O10). May EXCEED
|
|
293
|
+
# len(dependencies) when an upstream is invisible to this actor -- the gate
|
|
294
|
+
# treats that mismatch as "not provably clear".
|
|
261
295
|
depends_on_count: int = 0
|
|
262
296
|
|
|
263
297
|
|
|
@@ -542,11 +576,14 @@ class AlissaClient:
|
|
|
542
576
|
)
|
|
543
577
|
)
|
|
544
578
|
|
|
545
|
-
# The
|
|
546
|
-
#
|
|
547
|
-
# the
|
|
548
|
-
#
|
|
549
|
-
# absent from upstreamTasks) reads as "not
|
|
579
|
+
# The dependency gate reads upstream statuses. `dependsOn` (soft) and
|
|
580
|
+
# `hardDependsOn` (hard) are the declared id lists; `upstreamTasks` is
|
|
581
|
+
# the resolved view of BOTH (id/title/status). We key the gate on the
|
|
582
|
+
# resolved statuses but remember the declared count so an upstream this
|
|
583
|
+
# actor cannot see (declared, absent from upstreamTasks) reads as "not
|
|
584
|
+
# provably clear". Both arrays are counted, deduplicated -- see the
|
|
585
|
+
# module docstring for why neither `len(upstreamTasks)` nor a plain sum
|
|
586
|
+
# of the two lengths is the right size.
|
|
550
587
|
dependencies: list[Dependency] = []
|
|
551
588
|
for up in task.get("upstreamTasks") or []:
|
|
552
589
|
if not isinstance(up, dict):
|
|
@@ -558,8 +595,32 @@ class AlissaClient:
|
|
|
558
595
|
else str(up.get("_id") or "")
|
|
559
596
|
)
|
|
560
597
|
dependencies.append(Dependency(ref=up_ref, status=up.get("status") or ""))
|
|
561
|
-
|
|
562
|
-
|
|
598
|
+
declared: set[str] = set()
|
|
599
|
+
unrecognised = 0
|
|
600
|
+
for raw in (task.get("dependsOn"), task.get("hardDependsOn")):
|
|
601
|
+
if not isinstance(raw, list):
|
|
602
|
+
continue # absent (the common case for hardDependsOn) or junk
|
|
603
|
+
for entry in raw:
|
|
604
|
+
if isinstance(entry, str):
|
|
605
|
+
declared.add(entry)
|
|
606
|
+
continue
|
|
607
|
+
# These arrays hold bare id strings today, but they could grow
|
|
608
|
+
# into objects the way `upstreamTasks` already is -- so take the
|
|
609
|
+
# id when there is one, using the same extraction as the loop
|
|
610
|
+
# above. Counting an id we DID parse as unknown would inflate
|
|
611
|
+
# the declared count past the resolved one and hold a finished
|
|
612
|
+
# dependency forever, which is the very outcome the module
|
|
613
|
+
# docstring gives for rejecting a plain sum of the two lengths.
|
|
614
|
+
entry_id = entry.get("_id") if isinstance(entry, dict) else None
|
|
615
|
+
if isinstance(entry_id, str) and entry_id:
|
|
616
|
+
declared.add(entry_id)
|
|
617
|
+
else:
|
|
618
|
+
# Nothing parseable. DROPPING it would shrink the declared
|
|
619
|
+
# count -- exactly the failure this whole change is about --
|
|
620
|
+
# so count it: over-counting only holds the release, and it
|
|
621
|
+
# cannot be deduplicated against anything anyway.
|
|
622
|
+
unrecognised += 1
|
|
623
|
+
depends_on_count = len(declared) + unrecognised
|
|
563
624
|
|
|
564
625
|
created_raw = task.get("createdAt")
|
|
565
626
|
created_at = int(created_raw) if isinstance(created_raw, (int, float)) else 0
|
|
@@ -31,10 +31,12 @@ issues the develop daemon can pick up:
|
|
|
31
31
|
open unlabeled trailer match is ADOPTED (ledger row + label, never a
|
|
32
32
|
second issue), an open labeled one is skipped, a closed one follows the
|
|
33
33
|
closed-match rule below. A check that cannot answer holds the release.
|
|
34
|
-
* **The
|
|
34
|
+
* **The dependency gate**: a candidate is releasable only once every upstream
|
|
35
35
|
task it depends on is `pending_validation` or `validated` (an unfinished
|
|
36
|
-
dependency holds the release).
|
|
37
|
-
|
|
36
|
+
dependency holds the release). BOTH dependency types count -- soft
|
|
37
|
+
(`dependsOn`) and hard (`hardDependsOn`); see `dependencies_clear` and the
|
|
38
|
+
declared-count rationale in `alissa_client`. An upstream this actor cannot
|
|
39
|
+
even see counts as not-clear -- the gate is conservative.
|
|
38
40
|
* **Per-repo lane occupancy** bounds how much autonomous work runs at once in
|
|
39
41
|
one repo: at most `repo_parallelism` in-flight released issues, AND -- when
|
|
40
42
|
parallelism > 1 -- their `Autonomous-Scope` globs must be pairwise disjoint
|
|
@@ -245,8 +247,9 @@ from .state import State
|
|
|
245
247
|
|
|
246
248
|
log = logging.getLogger(__name__)
|
|
247
249
|
|
|
248
|
-
# The
|
|
249
|
-
# release) once it has reached validation. Anything earlier holds.
|
|
250
|
+
# The dependency gate: an upstream task is "clear" (no longer holding the
|
|
251
|
+
# release) once it has reached validation. Anything earlier holds. The type of
|
|
252
|
+
# the dependency (soft/hard) does not enter here -- both hold identically.
|
|
250
253
|
DEP_CLEAR_STATES = frozenset({"pending_validation", "validated"})
|
|
251
254
|
|
|
252
255
|
# The close edge only mutates a task it left `committed` (or one mid-transition
|
|
@@ -497,12 +500,27 @@ def priority_rank(priority: str) -> int:
|
|
|
497
500
|
def dependencies_clear(task: Task) -> bool:
|
|
498
501
|
"""Whether every dependency of `task` has reached validation.
|
|
499
502
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
503
|
+
Two conditions, in this order, and the order is the whole point:
|
|
504
|
+
|
|
505
|
+
1. The declared count must match what was RESOLVED. `depends_on_count` is
|
|
506
|
+
what the payload declared (soft + hard, deduplicated); `dependencies` is
|
|
507
|
+
what came back resolved. Fewer resolved means an upstream is invisible to
|
|
508
|
+
this actor, and the gate must NOT pass on the ones it can see -- an
|
|
509
|
+
unfinished-but-hidden dependency would otherwise leak.
|
|
510
|
+
2. Every resolved upstream must have reached validation.
|
|
511
|
+
|
|
512
|
+
Vacuously true with no dependencies: 0 declared and 0 resolved match, and
|
|
513
|
+
`all()` over an empty tuple is True. That is deliberately reached THROUGH
|
|
514
|
+
the visibility guard rather than by an `if depends_on_count == 0: return
|
|
515
|
+
True` shortcut ahead of it, which is what made this function release a task
|
|
516
|
+
whose only blockers were hard dependencies (issue #26): `dependsOn` held the
|
|
517
|
+
soft ids only, so a hard-only task declared zero and took the shortcut --
|
|
518
|
+
while `upstreamTasks`, and therefore `dependencies`, listed the blocker the
|
|
519
|
+
whole time. The declared count is fixed in `alissa_client.get_task`; the
|
|
520
|
+
shortcut is gone from here so that a count that ever again understates
|
|
521
|
+
reality is HELD by the mismatch instead of waved through. Do not reintroduce
|
|
522
|
+
an early return above the guard.
|
|
523
|
+
"""
|
|
506
524
|
if len(task.dependencies) != task.depends_on_count:
|
|
507
525
|
return False
|
|
508
526
|
return all(dep.status in DEP_CLEAR_STATES for dep in task.dependencies)
|
|
@@ -1299,14 +1317,30 @@ class Orchestrator:
|
|
|
1299
1317
|
return self._release(task, target, scope, occupancy, bow=bow)
|
|
1300
1318
|
|
|
1301
1319
|
def _release_blocked(self, task: Task) -> tuple[str, Decision]:
|
|
1302
|
-
"""The
|
|
1320
|
+
"""The dependency gate holds. Below the floor this is a quiet per-poll
|
|
1303
1321
|
hold; past `release_blocked_floor_days` (measured on the task's own age)
|
|
1304
1322
|
it pages the operator once. `created_at` is ms since epoch; a task with
|
|
1305
1323
|
no timestamp (created_at 0) is treated as old enough to page, so a stuck
|
|
1306
1324
|
release is never silent forever."""
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1325
|
+
parts = [f"{d.ref}={d.status or '?'}" for d in task.dependencies]
|
|
1326
|
+
hidden = task.depends_on_count - len(task.dependencies)
|
|
1327
|
+
if hidden > 0:
|
|
1328
|
+
# Report the invisible remainder ALONGSIDE the resolved ones: a
|
|
1329
|
+
# partially-visible task holds on something the operator cannot see
|
|
1330
|
+
# in the list, and naming only the resolved deps reads as "these are
|
|
1331
|
+
# all of them" when it is precisely the missing one doing the hold.
|
|
1332
|
+
# When NOTHING resolved there is no list to be a remainder OF, and
|
|
1333
|
+
# "3 more" would be more than what -- so that shape keeps its own
|
|
1334
|
+
# count-only wording. This string goes onto the `release-blocked`
|
|
1335
|
+
# page an operator reads to decide what to unblock.
|
|
1336
|
+
parts.append(
|
|
1337
|
+
f"{hidden} more not visible to this actor" if parts
|
|
1338
|
+
else f"{hidden} dependency(ies) not visible to this actor"
|
|
1339
|
+
)
|
|
1340
|
+
# `parts` is never empty here: this runs only after `dependencies_clear`
|
|
1341
|
+
# returned False, which needs either a count mismatch (hidden > 0,
|
|
1342
|
+
# handled just above) or a resolved dep that has not validated.
|
|
1343
|
+
deps = ", ".join(parts)
|
|
1310
1344
|
floor_seconds = self.config.release_blocked_floor_days * 86400
|
|
1311
1345
|
age_seconds = self._now() - (task.created_at / 1000.0 if task.created_at else 0.0)
|
|
1312
1346
|
if age_seconds >= floor_seconds:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.4
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.3
|
|
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
|