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.
Files changed (23) hide show
  1. {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
  2. {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
  3. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/loop.py +49 -15
  4. alissa_tools_github_orcloop-0.3.4/src/main/alissa/tools/github/orcloop/version +1 -0
  5. {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
  6. alissa_tools_github_orcloop-0.3.3/src/main/alissa/tools/github/orcloop/version +0 -1
  7. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/MANIFEST.in +0 -0
  8. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/README.md +0 -0
  9. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/requirements.txt +0 -0
  10. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/setup.cfg +0 -0
  11. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/setup.py +0 -0
  12. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
  13. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
  14. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/config.py +0 -0
  15. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
  16. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
  17. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
  18. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/state.py +0 -0
  19. {alissa_tools_github_orcloop-0.3.3 → alissa_tools_github_orcloop-0.3.4}/src/main/alissa/tools/github/orcloop/version.py +0 -0
  20. {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
  21. {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
  22. {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
  23. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-orcloop
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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 dependsOn gate reads).
289
+ # The resolved upstream tasks (statuses the dependency gate reads).
258
290
  dependencies: tuple[Dependency, ...] = ()
259
- # len(dependsOn): may EXCEED len(dependencies) when an upstream is invisible
260
- # to this actor -- the gate treats that mismatch as "not provably clear".
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 dependsOn gate reads upstream statuses. `dependsOn` is the id
546
- # list; `upstreamTasks` is the resolved view (id/title/status). We key
547
- # the gate on the resolved statuses but remember the raw dependsOn
548
- # count so an upstream this actor cannot see (present in dependsOn,
549
- # absent from upstreamTasks) reads as "not provably clear".
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
- depends_on = task.get("dependsOn")
562
- depends_on_count = len(depends_on) if isinstance(depends_on, list) else 0
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 dependsOn gate**: a candidate is releasable only once every upstream
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). An upstream this actor cannot even see counts
37
- as not-clear -- the gate is conservative.
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 dependsOn gate: an upstream task is "clear" (no longer holding 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
- Vacuously true with no dependencies. Conservative on partial visibility: if
501
- the resolved `dependencies` are fewer than the raw `depends_on_count`, some
502
- upstream is invisible to this actor and the gate must NOT pass on the ones
503
- it can see -- an unfinished-but-hidden dependency would otherwise leak."""
504
- if task.depends_on_count == 0:
505
- return True
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 dependsOn gate holds. Below the floor this is a quiet per-poll
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
- deps = ", ".join(f"{d.ref}={d.status or '?'}" for d in task.dependencies) or (
1308
- f"{task.depends_on_count} dependency(ies) not visible to this actor"
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-orcloop
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera