@femtomc/mu-core 26.2.89 → 26.2.91
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/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/node/index.d.ts +0 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +0 -1
- package/dist/node/memory_runtime.js +3 -3
- package/package.json +3 -4
- package/dist/dag.d.ts +0 -46
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js +0 -212
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/node/index.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export { findRepoRoot, getMuHomeDir, getStorePaths, workspaceIdForRepoRoot } fro
|
|
|
6
6
|
import { EventLog } from "../events.js";
|
|
7
7
|
export declare function fsEventLog(path: string): EventLog;
|
|
8
8
|
export declare function fsEventLogFromRepoRoot(repoRoot: string): EventLog;
|
|
9
|
-
export * from "../dag.js";
|
|
10
9
|
export { EVENT_VERSION, type EventEnvelope, EventLog, type EventSink, JsonlEventSink, NullEventSink, type RunIdProvider, } from "../events.js";
|
|
11
10
|
export { newRunId, nowTs, nowTsMs, randomHex, shortId } from "../ids.js";
|
|
12
11
|
export { InMemoryJsonlStore, type JsonlStore } from "../persistence.js";
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,WAAW,EACX,YAAY,EACZ,eAAe,EACf,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC5D,cAAc,qBAAqB,CAAC;AACpC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE/F,OAAO,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAKxD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAEjD;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAEjE;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,WAAW,EACX,YAAY,EACZ,eAAe,EACf,yBAAyB,EACzB,SAAS,EACT,WAAW,EACX,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC5D,cAAc,qBAAqB,CAAC;AACpC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE/F,OAAO,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAKxD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAEjD;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAEjE;AAGD,OAAO,EACN,aAAa,EACb,KAAK,aAAa,EAClB,QAAQ,EACR,KAAK,SAAS,EACd,cAAc,EACd,aAAa,EACb,KAAK,aAAa,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACxE,cAAc,YAAY,CAAC"}
|
package/dist/node/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export function fsEventLog(path) {
|
|
|
12
12
|
export function fsEventLogFromRepoRoot(repoRoot) {
|
|
13
13
|
return fsEventLog(getStorePaths(repoRoot).eventsPath);
|
|
14
14
|
}
|
|
15
|
-
export * from "../dag.js";
|
|
16
15
|
// Re-export the node-free surface so node code can import from a single place.
|
|
17
16
|
export { EVENT_VERSION, EventLog, JsonlEventSink, NullEventSink, } from "../events.js";
|
|
18
17
|
export { newRunId, nowTs, nowTsMs, randomHex, shortId } from "../ids.js";
|
|
@@ -623,7 +623,7 @@ async function collectCommandJournal(repoRoot, path) {
|
|
|
623
623
|
const channelTenantId = command ? nonEmptyString(command.channel_tenant_id) : null;
|
|
624
624
|
const channelConversationId = command ? nonEmptyString(command.channel_conversation_id) : null;
|
|
625
625
|
const actorBindingId = command ? nonEmptyString(command.actor_binding_id) : null;
|
|
626
|
-
const runId =
|
|
626
|
+
const runId = null;
|
|
627
627
|
const sessionId = command
|
|
628
628
|
? nonEmptyString(command.operator_session_id) ?? nonEmptyString(command.meta_session_id)
|
|
629
629
|
: null;
|
|
@@ -674,7 +674,7 @@ async function collectCommandJournal(repoRoot, path) {
|
|
|
674
674
|
const channelTenantId = correlation ? nonEmptyString(correlation.channel_tenant_id) : null;
|
|
675
675
|
const channelConversationId = correlation ? nonEmptyString(correlation.channel_conversation_id) : null;
|
|
676
676
|
const actorBindingId = correlation ? nonEmptyString(correlation.actor_binding_id) : null;
|
|
677
|
-
const runId =
|
|
677
|
+
const runId = null;
|
|
678
678
|
const sessionId = correlation
|
|
679
679
|
? nonEmptyString(correlation.operator_session_id) ?? nonEmptyString(correlation.meta_session_id)
|
|
680
680
|
: null;
|
|
@@ -735,7 +735,7 @@ async function collectOutbox(repoRoot, path) {
|
|
|
735
735
|
conversationId: channelConversationId,
|
|
736
736
|
bindingId: actorBindingId,
|
|
737
737
|
});
|
|
738
|
-
const runId =
|
|
738
|
+
const runId = null;
|
|
739
739
|
const sessionId = correlation
|
|
740
740
|
? nonEmptyString(correlation.operator_session_id) ?? nonEmptyString(correlation.meta_session_id)
|
|
741
741
|
: null;
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-core",
|
|
3
|
-
"version": "26.2.
|
|
4
|
-
"description": "Core primitives for mu: IDs, events,
|
|
3
|
+
"version": "26.2.91",
|
|
4
|
+
"description": "Core primitives for mu: IDs, events, schemas, and persistence interfaces.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
7
7
|
"agent",
|
|
8
8
|
"core",
|
|
9
|
-
"events"
|
|
10
|
-
"dag"
|
|
9
|
+
"events"
|
|
11
10
|
],
|
|
12
11
|
"type": "module",
|
|
13
12
|
"main": "./dist/index.js",
|
package/dist/dag.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { Issue } from "./spec.js";
|
|
2
|
-
export type ValidationResult = {
|
|
3
|
-
is_final: boolean;
|
|
4
|
-
reason: string;
|
|
5
|
-
};
|
|
6
|
-
export type RetryableDagCandidate = {
|
|
7
|
-
issue: Issue;
|
|
8
|
-
reason: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Deterministic DAG reconcile primitives used by orchestrator reconciliation.
|
|
12
|
-
*
|
|
13
|
-
* Contract: these helpers are pure functions over the provided issue snapshot and must remain
|
|
14
|
-
* side-effect free so reconcile passes are replayable/idempotent.
|
|
15
|
-
*/
|
|
16
|
-
export declare const DAG_RECONCILE_PRIMITIVE_INVARIANTS: readonly ["DAG-RECON-001: `readyLeaves` only returns open, unblocked, leaf issues within the requested subtree scope.", "DAG-RECON-002: `readyLeaves` ordering is deterministic for a fixed input snapshot (priority-ordered candidate set).", "DAG-RECON-003: `validateDag(...).is_final=true` implies no remaining non-expanded open work in the subtree.", "DAG-RECON-004: closed(failure|needs_work) and expanded-without-children are non-final and require reconcile action.", "DAG-RECON-005: `retryableDagCandidates` selection is deterministic and side-effect free for a fixed snapshot + attempts map."];
|
|
17
|
-
export declare function subtreeIds(issues: readonly Issue[], rootId: string): string[];
|
|
18
|
-
/**
|
|
19
|
-
* Reconcile selection primitive: the orchestrator must only dispatch from this ready set (or an
|
|
20
|
-
* equivalent deterministic adapter) to preserve replayability.
|
|
21
|
-
*/
|
|
22
|
-
export declare function readyLeaves(issues: readonly Issue[], opts?: {
|
|
23
|
-
root_id?: string;
|
|
24
|
-
tags?: readonly string[];
|
|
25
|
-
}): Issue[];
|
|
26
|
-
/**
|
|
27
|
-
* Reconcile retry primitive.
|
|
28
|
-
*
|
|
29
|
-
* Produces a deterministic list of closed nodes that are eligible to be reopened for orchestration.
|
|
30
|
-
* The orchestrator decides whether/when to apply the reopen side effect.
|
|
31
|
-
*/
|
|
32
|
-
export declare function retryableDagCandidates(issues: readonly Issue[], opts: {
|
|
33
|
-
root_id: string;
|
|
34
|
-
retry_outcomes?: readonly string[];
|
|
35
|
-
attempts_by_issue_id?: ReadonlyMap<string, number>;
|
|
36
|
-
max_attempts?: number;
|
|
37
|
-
}): RetryableDagCandidate[];
|
|
38
|
-
export declare function collapsible(issues: readonly Issue[], rootId: string): Issue[];
|
|
39
|
-
/**
|
|
40
|
-
* Reconcile termination primitive.
|
|
41
|
-
*
|
|
42
|
-
* `is_final=false` is a hard signal that orchestrator must continue reconciling (or repair invalid
|
|
43
|
-
* expanded state) before the root run can be considered terminal.
|
|
44
|
-
*/
|
|
45
|
-
export declare function validateDag(issues: readonly Issue[], rootId: string): ValidationResult;
|
|
46
|
-
//# sourceMappingURL=dag.d.ts.map
|
package/dist/dag.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dag.d.ts","sourceRoot":"","sources":["../src/dag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,MAAM,gBAAgB,GAAG;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IACnC,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,slBAMrC,CAAC;AA0BX,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAuB7E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GACvD,KAAK,EAAE,CAuCT;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,IAAI,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oBAAoB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,GACC,qBAAqB,EAAE,CAsCzB;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,CA6B7E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAqDtF"}
|
package/dist/dag.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic DAG reconcile primitives used by orchestrator reconciliation.
|
|
3
|
-
*
|
|
4
|
-
* Contract: these helpers are pure functions over the provided issue snapshot and must remain
|
|
5
|
-
* side-effect free so reconcile passes are replayable/idempotent.
|
|
6
|
-
*/
|
|
7
|
-
export const DAG_RECONCILE_PRIMITIVE_INVARIANTS = [
|
|
8
|
-
"DAG-RECON-001: `readyLeaves` only returns open, unblocked, leaf issues within the requested subtree scope.",
|
|
9
|
-
"DAG-RECON-002: `readyLeaves` ordering is deterministic for a fixed input snapshot (priority-ordered candidate set).",
|
|
10
|
-
"DAG-RECON-003: `validateDag(...).is_final=true` implies no remaining non-expanded open work in the subtree.",
|
|
11
|
-
"DAG-RECON-004: closed(failure|needs_work) and expanded-without-children are non-final and require reconcile action.",
|
|
12
|
-
"DAG-RECON-005: `retryableDagCandidates` selection is deterministic and side-effect free for a fixed snapshot + attempts map.",
|
|
13
|
-
];
|
|
14
|
-
function childrenByParent(issues) {
|
|
15
|
-
const byParent = new Map();
|
|
16
|
-
for (const issue of issues) {
|
|
17
|
-
for (const dep of issue.deps) {
|
|
18
|
-
if (dep.type !== "parent") {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const list = byParent.get(dep.target) ?? [];
|
|
22
|
-
list.push(issue);
|
|
23
|
-
byParent.set(dep.target, list);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return byParent;
|
|
27
|
-
}
|
|
28
|
-
function compareByPriorityThenId(a, b) {
|
|
29
|
-
const pa = a.priority ?? 3;
|
|
30
|
-
const pb = b.priority ?? 3;
|
|
31
|
-
if (pa !== pb) {
|
|
32
|
-
return pa - pb;
|
|
33
|
-
}
|
|
34
|
-
return a.id.localeCompare(b.id);
|
|
35
|
-
}
|
|
36
|
-
export function subtreeIds(issues, rootId) {
|
|
37
|
-
const children = childrenByParent(issues);
|
|
38
|
-
const result = [];
|
|
39
|
-
const q = [rootId];
|
|
40
|
-
const seen = new Set();
|
|
41
|
-
while (q.length > 0) {
|
|
42
|
-
const nodeId = q.shift();
|
|
43
|
-
if (!nodeId) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (seen.has(nodeId)) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
seen.add(nodeId);
|
|
50
|
-
result.push(nodeId);
|
|
51
|
-
for (const child of children.get(nodeId) ?? []) {
|
|
52
|
-
q.push(child.id);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Reconcile selection primitive: the orchestrator must only dispatch from this ready set (or an
|
|
59
|
-
* equivalent deterministic adapter) to preserve replayability.
|
|
60
|
-
*/
|
|
61
|
-
export function readyLeaves(issues, opts = {}) {
|
|
62
|
-
const byId = new Map(issues.map((i) => [i.id, i]));
|
|
63
|
-
const idsInScope = new Set(opts.root_id ? subtreeIds(issues, opts.root_id) : byId.keys());
|
|
64
|
-
const blocked = new Set();
|
|
65
|
-
for (const issue of issues) {
|
|
66
|
-
for (const dep of issue.deps) {
|
|
67
|
-
if (dep.type !== "blocks") {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (issue.status !== "closed" || issue.outcome === "expanded") {
|
|
71
|
-
blocked.add(dep.target);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const children = childrenByParent(issues);
|
|
76
|
-
const result = [];
|
|
77
|
-
for (const issueId of idsInScope) {
|
|
78
|
-
const issue = byId.get(issueId);
|
|
79
|
-
if (!issue || issue.status !== "open") {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (blocked.has(issueId)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const kids = children.get(issueId) ?? [];
|
|
86
|
-
if (kids.some((kid) => kid.status !== "closed")) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (opts.tags && !opts.tags.every((tag) => issue.tags.includes(tag))) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
result.push(issue);
|
|
93
|
-
}
|
|
94
|
-
result.sort(compareByPriorityThenId);
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Reconcile retry primitive.
|
|
99
|
-
*
|
|
100
|
-
* Produces a deterministic list of closed nodes that are eligible to be reopened for orchestration.
|
|
101
|
-
* The orchestrator decides whether/when to apply the reopen side effect.
|
|
102
|
-
*/
|
|
103
|
-
export function retryableDagCandidates(issues, opts) {
|
|
104
|
-
const idsInScope = new Set(subtreeIds(issues, opts.root_id));
|
|
105
|
-
const children = childrenByParent(issues);
|
|
106
|
-
const retryOutcomes = new Set(opts.retry_outcomes ?? ["failure", "needs_work"]);
|
|
107
|
-
const maxAttempts = typeof opts.max_attempts === "number" && Number.isFinite(opts.max_attempts)
|
|
108
|
-
? Math.max(1, Math.trunc(opts.max_attempts))
|
|
109
|
-
: Number.POSITIVE_INFINITY;
|
|
110
|
-
const out = [];
|
|
111
|
-
for (const issue of issues) {
|
|
112
|
-
if (!idsInScope.has(issue.id)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (issue.status !== "closed") {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const attempts = opts.attempts_by_issue_id?.get(issue.id) ?? 0;
|
|
119
|
-
if (attempts >= maxAttempts) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (issue.outcome && retryOutcomes.has(issue.outcome)) {
|
|
123
|
-
const hasOpenChildren = (children.get(issue.id) ?? []).some((child) => child.status !== "closed");
|
|
124
|
-
if (!hasOpenChildren) {
|
|
125
|
-
out.push({ issue, reason: `outcome=${issue.outcome}` });
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (issue.outcome === "expanded" && (children.get(issue.id)?.length ?? 0) === 0) {
|
|
130
|
-
out.push({ issue, reason: "outcome=expanded_without_children" });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
out.sort((a, b) => compareByPriorityThenId(a.issue, b.issue));
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
export function collapsible(issues, rootId) {
|
|
137
|
-
const byId = new Map(issues.map((i) => [i.id, i]));
|
|
138
|
-
const idsInScope = new Set(subtreeIds(issues, rootId));
|
|
139
|
-
const children = childrenByParent(issues);
|
|
140
|
-
// `refine` is terminal for a closed reviewer node; refinement itself is
|
|
141
|
-
// orchestrated by root-phase reconcile transitions.
|
|
142
|
-
const terminalOutcomes = new Set(["success", "skipped", "refine"]);
|
|
143
|
-
const result = [];
|
|
144
|
-
for (const issueId of idsInScope) {
|
|
145
|
-
const node = byId.get(issueId);
|
|
146
|
-
if (!node) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (node.status !== "closed" || node.outcome !== "expanded") {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const kids = children.get(issueId) ?? [];
|
|
153
|
-
if (kids.length === 0) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (kids.every((kid) => kid.status === "closed" && kid.outcome != null && terminalOutcomes.has(kid.outcome))) {
|
|
157
|
-
result.push(node);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
result.sort((a, b) => a.id.localeCompare(b.id));
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Reconcile termination primitive.
|
|
165
|
-
*
|
|
166
|
-
* `is_final=false` is a hard signal that orchestrator must continue reconciling (or repair invalid
|
|
167
|
-
* expanded state) before the root run can be considered terminal.
|
|
168
|
-
*/
|
|
169
|
-
export function validateDag(issues, rootId) {
|
|
170
|
-
const byId = new Map(issues.map((i) => [i.id, i]));
|
|
171
|
-
const ids = new Set(subtreeIds(issues, rootId));
|
|
172
|
-
const root = byId.get(rootId);
|
|
173
|
-
if (!root) {
|
|
174
|
-
return { is_final: true, reason: "root not found" };
|
|
175
|
-
}
|
|
176
|
-
const children = childrenByParent(issues);
|
|
177
|
-
const needsReorch = [...ids]
|
|
178
|
-
.filter((issueId) => {
|
|
179
|
-
const issue = byId.get(issueId);
|
|
180
|
-
return issue?.status === "closed" && (issue.outcome === "failure" || issue.outcome === "needs_work");
|
|
181
|
-
})
|
|
182
|
-
.sort();
|
|
183
|
-
if (needsReorch.length > 0) {
|
|
184
|
-
return { is_final: false, reason: `needs work: ${needsReorch.join(",")}` };
|
|
185
|
-
}
|
|
186
|
-
const badExpanded = [...ids]
|
|
187
|
-
.filter((issueId) => {
|
|
188
|
-
const issue = byId.get(issueId);
|
|
189
|
-
return (issue?.status === "closed" && issue.outcome === "expanded" && (children.get(issueId)?.length ?? 0) === 0);
|
|
190
|
-
})
|
|
191
|
-
.sort();
|
|
192
|
-
if (badExpanded.length > 0) {
|
|
193
|
-
return { is_final: false, reason: `expanded without children: ${badExpanded.join(",")}` };
|
|
194
|
-
}
|
|
195
|
-
const pending = [...ids].filter((issueId) => {
|
|
196
|
-
const issue = byId.get(issueId);
|
|
197
|
-
if (!issue) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
if (issue.status === "closed" && issue.outcome === "expanded") {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
return issue.status !== "closed";
|
|
204
|
-
});
|
|
205
|
-
if (pending.length === 0) {
|
|
206
|
-
return { is_final: true, reason: "all work completed" };
|
|
207
|
-
}
|
|
208
|
-
if (pending.length === 1 && pending[0] === rootId && ids.size > 1) {
|
|
209
|
-
return { is_final: false, reason: "all children closed, root still open" };
|
|
210
|
-
}
|
|
211
|
-
return { is_final: false, reason: "in progress" };
|
|
212
|
-
}
|