pm-graph 2026.10.20 → 2026.10.24
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/CHANGELOG.md +37 -0
- package/README.md +176 -2
- package/dist/blocker-cuts-command.d.ts +21 -0
- package/dist/blocker-cuts-command.js +60 -0
- package/dist/blocker-cuts-command.js.map +1 -0
- package/dist/blocker-cuts.d.ts +87 -0
- package/dist/blocker-cuts.js +227 -0
- package/dist/blocker-cuts.js.map +1 -0
- package/dist/cross-workspace-command.d.ts +3 -0
- package/dist/cross-workspace-command.js +96 -0
- package/dist/cross-workspace-command.js.map +1 -0
- package/dist/cross-workspace.d.ts +91 -0
- package/dist/cross-workspace.js +120 -0
- package/dist/cross-workspace.js.map +1 -0
- package/dist/index.d.ts +46 -8
- package/dist/index.js +106 -119
- package/dist/index.js.map +1 -1
- package/dist/snapshot-impact-command.d.ts +3 -0
- package/dist/snapshot-impact-command.js +108 -0
- package/dist/snapshot-impact-command.js.map +1 -0
- package/dist/snapshot-impact.d.ts +99 -0
- package/dist/snapshot-impact.js +189 -0
- package/dist/snapshot-impact.js.map +1 -0
- package/docs/blocker-cuts-example.md +94 -0
- package/docs/cross-workspace-example.md +80 -0
- package/manifest.json +3 -2
- package/package.json +13 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.10.24 - 2026-10-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Enumerate minimal blocker frontiers for a goal with bounded completeness receipts ([pm-graph-8z44](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/features/pm-graph-8z44.toon))
|
|
8
|
+
- Compare snapshot graph impact for resuming agents ([pm-graph-snapshot-impact](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/features/pm-graph-snapshot-impact.toon))
|
|
9
|
+
|
|
10
|
+
### Other
|
|
11
|
+
|
|
12
|
+
- Correct review evidence in tracker reconciliation PR 155 ([pm-graph-review155](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/chores/pm-graph-review155.toon))
|
|
13
|
+
|
|
14
|
+
## 2026.10.23 - 2026-10-23
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Adopt pm-github 2026.10.16: default-branch-safe gated sync and daily gated closeout PRs ([pm-graph-mupl](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/chores/pm-graph-mupl.toon))
|
|
19
|
+
- Enable the gated GitHub issue sync and drop the braces chain ([pm-graph-maint1014](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/chores/pm-graph-maint1014.toon))
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- A release run bumped and committed a new version every night while publishing was impossible, because the only externally-failable step runs last ([pm-graph-wos1](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-wos1.toon))
|
|
24
|
+
- Fix release publish ordering ahead of protected main push ([pm-graph-ihhe](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-ihhe.toon))
|
|
25
|
+
- Committed CHANGELOG.md drifts from deterministic regeneration after the pm-graph-3svp type classification change, breaking changelog:check on every PR ([pm-graph-brgk](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-brgk.toon))
|
|
26
|
+
- Refresh managed pm-github extension for project-local PM CLI 2026.9.27 ([pm-graph-ipvn](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-ipvn.toon))
|
|
27
|
+
- Graph export uses quadratic item and edge scans on large trackers ([pm-graph-3a1p](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-3a1p.toon))
|
|
28
|
+
- Remove vulnerable braces dependency from certification toolchain ([pm-graph-g8uc](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-g8uc.toon))
|
|
29
|
+
- The publish-attestation gate misses a publish routed through an unquoted shell variable ([pm-graph-ifqz](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-ifqz.toon))
|
|
30
|
+
- The changelog gate stamps an untagged version with the current date, so its verdict flips every midnight with no commit ([pm-graph-4glq](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-4glq.toon))
|
|
31
|
+
- The release job authenticated with a stored npm token that expired, so publishing stopped while every other gate stayed green ([pm-graph-ybzl](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/issues/pm-graph-ybzl.toon))
|
|
32
|
+
|
|
33
|
+
### Other
|
|
34
|
+
|
|
35
|
+
- Cover the output_format decline contract with real SDK harness tests instead of a registration-only assertion ([pm-graph-vgj0](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-vgj0.toon))
|
|
36
|
+
- Bring pm-graph to real 100% line/branch/function coverage and raise the gate to 100 ([pm-graph-6zil](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-6zil.toon))
|
|
37
|
+
- Render package visuals with build-time Triplicate tokens ([pm-graph-triplicate](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/tasks/pm-graph-triplicate.toon))
|
|
38
|
+
- Certify pm-graph on PM CLI/SDK 2026.10.4 and consolidate pending dependency updates ([pm-graph-lcmc](https://github.com/unbraind/pm-graph/blob/main/.agents/pm/chores/pm-graph-lcmc.toon))
|
|
39
|
+
|
|
3
40
|
## 2026.10.14 - 2026-10-14
|
|
4
41
|
|
|
5
42
|
### Added
|
package/README.md
CHANGED
|
@@ -304,11 +304,103 @@ Example output:
|
|
|
304
304
|
}
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
+
## Without git
|
|
308
|
+
|
|
309
|
+
All offline graph commands, including `blocker-cuts`, and the graph exporter
|
|
310
|
+
work in plain initialized trackers without Git. Neo4j commands need their
|
|
311
|
+
documented database configuration and connectivity; they also do not need a
|
|
312
|
+
Git repository.
|
|
313
|
+
|
|
307
314
|
## Offline Analytics
|
|
308
315
|
|
|
309
|
-
|
|
316
|
+
### `pm pm-graph blocker-cuts <goal>`
|
|
310
317
|
|
|
311
|
-
|
|
318
|
+
Return alternative **inclusion-minimal blocker frontiers**, smallest cardinality
|
|
319
|
+
first, with separate source, graph and search completeness receipts. This is a
|
|
320
|
+
graph-view data operation; it complements core dominators/articulation/plan/impact
|
|
321
|
+
and this package's path/critical-path commands.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
pm pm-graph blocker-cuts goal-id
|
|
325
|
+
pm pm-graph blocker-cuts goal-id --max-cuts 16 --max-states 20000 --json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Every upstream unresolved prerequisite path must meet a returned cut. All
|
|
329
|
+
prerequisites are conjunctive: for `goal -> a -> {b,c}`, the alternatives are
|
|
330
|
+
`[a]` and `[b,c]`. These are **plans**: resolving `b,c` clears the upstream
|
|
331
|
+
frontier, but `a` still needs completion. Each cut's `immediatelyUnblocked` is
|
|
332
|
+
true only when that set includes every immediate unresolved prerequisite. This
|
|
333
|
+
does not predict effort, duration, automatic task completion, or approvals.
|
|
334
|
+
|
|
335
|
+
The command uses an extension-free, strict, full, unbounded SDK read (including
|
|
336
|
+
terminal items), and exact goal ids. `BLOCKED_BY`, `DEPENDS_ON`, `REQUIRES` and
|
|
337
|
+
reversed `BLOCKS` are prerequisites; hierarchy, related links and facets are
|
|
338
|
+
excluded. Terminal statuses from the tracker registry stop traversal. A resolved
|
|
339
|
+
goal or a goal with no active blockers returns the empty cut `[[]]`.
|
|
340
|
+
|
|
341
|
+
`source` certifies the read, while `receipt.graph` describes the active goal cone
|
|
342
|
+
in that supplied snapshot. It cannot prove that missing real-world relationships
|
|
343
|
+
were recorded, nor provide transaction isolation from concurrent tracker edits.
|
|
344
|
+
Unknown referenced items/statuses refuse with `INCOMPLETE_GRAPH`; a relevant
|
|
345
|
+
cycle refuses with `CYCLE` and one closed cycle witness. Unrelated cycles and
|
|
346
|
+
missing references outside the active cone do not prevent a goal proof.
|
|
347
|
+
Missing-data receipts include total counts and at most 32 example ids per kind.
|
|
348
|
+
|
|
349
|
+
`receipt.search.complete` means **all** inclusion-minimal cuts were enumerated.
|
|
350
|
+
`completeThroughSize` certifies fully searched cardinalities;
|
|
351
|
+
`minimumCardinality` is the proven smallest size, or `null` if no cut was found.
|
|
352
|
+
Partial results remain individually minimal. Never interpret an empty partial
|
|
353
|
+
list as "unblocked". To act on displayed rows, also inspect the host's
|
|
354
|
+
`read_output` receipt: output compaction is independent of search completeness.
|
|
355
|
+
|
|
356
|
+
| Budget | Default | Hard maximum |
|
|
357
|
+
| --- | ---: | ---: |
|
|
358
|
+
| `--max-states` (candidate subsets) | 20000 | 1000000 |
|
|
359
|
+
| `--max-work` (search visits, edges, subset comparisons) | 2000000 | 20000000 |
|
|
360
|
+
| `--max-cuts` (returned alternatives) | 32 | 256 |
|
|
361
|
+
| `--max-size` (frontier cardinality) | 8 | 64 |
|
|
362
|
+
|
|
363
|
+
Enumeration stops deterministically at the first cap and reports its name;
|
|
364
|
+
raising that budget and rerunning restarts the search. Preflight scans are
|
|
365
|
+
input-capped at 100000 nodes / 500000 relationships and 512 active unresolved
|
|
366
|
+
vertices. Oversized snapshots refuse with `GRAPH_LIMIT` and a scoping remedy.
|
|
367
|
+
`INVALID_OPTIONS`, `INVALID_GRAPH`, `UNKNOWN_GOAL`, and `SOURCE_INCOMPLETE` also
|
|
368
|
+
include remedies. CLI refusals exit 2 (unknown goals exit 3); successful bounded
|
|
369
|
+
searches exit 0, with partiality explicit in the receipt.
|
|
370
|
+
|
|
371
|
+
The typed SDK has no filesystem, Git or Neo4j dependency:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { minimalBlockerCuts } from "pm-graph";
|
|
375
|
+
import type { BlockerCutResult } from "pm-graph";
|
|
376
|
+
|
|
377
|
+
const result: BlockerCutResult = minimalBlockerCuts(snapshot, "goal-id", {
|
|
378
|
+
maxCuts: 16,
|
|
379
|
+
// For custom workflows, pass disjoint resolvedStatuses/unresolvedStatuses.
|
|
380
|
+
});
|
|
381
|
+
if (!result.ok) console.error(result.refusal.code, result.refusal.remedy);
|
|
382
|
+
else renderFrontiers(result.cuts, result.receipt);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
SDK callers must certify their own complete source read; the pure function only
|
|
386
|
+
certifies the supplied goal cone. Successful results contain ids and receipts,
|
|
387
|
+
excluding item bodies, titles and local paths. `SOURCE_INCOMPLETE` remedies retain
|
|
388
|
+
the original source error message to distinguish invalid roots, strict-read
|
|
389
|
+
failures and bad settings; these diagnostics can include tracker paths.
|
|
390
|
+
See [the worked example](docs/blocker-cuts-example.md).
|
|
391
|
+
|
|
392
|
+
### Other offline analytics
|
|
393
|
+
|
|
394
|
+
The following commands analyze the workspace dependency graph **entirely offline** — no Neo4j required. They operate on **structural edges only** (`BLOCKED_BY`, `CHILD_OF`, and dependency edges such as `BLOCKS`/`RELATED`) between real items; facet edges (type/status/assignee/sprint/release) and tag edges are deliberately excluded so cycle, path, and centrality results stay meaningful.
|
|
395
|
+
|
|
396
|
+
Closed/canceled items are excluded by default; pass `--include-closed` to keep them. `analyze`, `cycles`, `critical-path`, and `topo-sort` also accept `--root <id>` / `--depth <n>` to scope the analysis to a neighborhood. `analyze`, `cycles`, `critical-path`, `topo-sort`, `path`, `impact`, and `explain` accept `--filter type=...|status=...` to keep only items matching the given type/status (same-key values are ORed; distinct keys are ANDed); non-item nodes (facets/tags) are always kept. `cycles` and `critical-path` additionally accept `--format <text|mermaid|graphml|dot>` (default `text`) to render the relevant subgraph as a diagram for docs — `dot` emits a Graphviz `digraph` consumable by `dot`, `neato`, etc.
|
|
397
|
+
|
|
398
|
+
The host dependency contract stores `A --blocked_by--> B` and `B --blocks--> A`
|
|
399
|
+
as the same prerequisite: A waits for B. Offline analytics normalize `BLOCKS`
|
|
400
|
+
to item → blocker direction before computing paths, cycles, execution order,
|
|
401
|
+
impact, depth, and explanations. Graph exports and diagram relationships retain
|
|
402
|
+
their original stored endpoints and relation types. The edge-array SDK helpers
|
|
403
|
+
(such as `topoSort`) expect this normalized item → blocker direction.
|
|
312
404
|
|
|
313
405
|
### `pm pm-graph analyze`
|
|
314
406
|
|
|
@@ -458,6 +550,76 @@ pm pm-graph impact pm-vc4q --json
|
|
|
458
550
|
{ "ok": true, "id": "pm-vc4q", "count": 3, "impacted": ["pm-0wtl", "pm-8k1m", "pm-yol1"] }
|
|
459
551
|
```
|
|
460
552
|
|
|
553
|
+
## Snapshot graph change impact
|
|
554
|
+
|
|
555
|
+
`compareGraphSnapshots(before, after, goal, options?)` compares two explicit
|
|
556
|
+
`Graph` SDK values. The CLI accepts regular JSON files containing those values
|
|
557
|
+
or the existing export command's `{ graph: ... }` envelope:
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
pm pm-graph snapshot-impact before.json after.json goal-id --limit 16 --json
|
|
561
|
+
pm pm-graph snapshot-impact before.json after.json goal-id --limit 16 --offset 16
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The result reports gained/lost upstream prerequisites and downstream dependents,
|
|
565
|
+
added/removed minimal blocker frontiers, and explicit `SUPPORTS`/`EVIDENCE_FOR`
|
|
566
|
+
links into the goal or its upstream prerequisites. Removed facts carry the
|
|
567
|
+
earlier node title and status, even if that node was deleted. A deleted goal is
|
|
568
|
+
valid when it exists in the earlier snapshot. Reordering inputs, duplicate
|
|
569
|
+
edges and nonsemantic title/capture metadata edits produce no changes.
|
|
570
|
+
|
|
571
|
+
The gating policy is `BLOCKED_BY`, `DEPENDS_ON`, `REQUIRES`, and reversed
|
|
572
|
+
`BLOCKS`; hierarchy, facets and other relationships do not imply prerequisites.
|
|
573
|
+
Reachability is structural, including terminal items. Blocker frontiers stop
|
|
574
|
+
at terminal items and require all upstream paths to intersect a cut. Resolving
|
|
575
|
+
a frontier still leaves intermediate work; `immediatelyUnblocked` identifies
|
|
576
|
+
cuts containing every direct prerequisite. Both cut searches must finish before
|
|
577
|
+
differences are reported. Cycles, missing cone endpoints, unknown active statuses
|
|
578
|
+
and exhausted cut searches return typed refusals with remedies. A dangling
|
|
579
|
+
reference to a deleted node retains its earlier explanation in `missingNode`
|
|
580
|
+
while refusing to infer cut differences from incomplete input. For custom
|
|
581
|
+
workflows, pass `cutOptions.resolvedStatuses`/`unresolvedStatuses` in the SDK or
|
|
582
|
+
`--resolved-statuses done,canceled --unresolved-statuses ready,working` in the CLI.
|
|
583
|
+
|
|
584
|
+
`receipt.output` exposes the exact total, returned/omitted counts and
|
|
585
|
+
`nextOffset`. Keep both input files immutable while recovering pages.
|
|
586
|
+
Defaults are 32 rows and 2,000,000 traversal work units; `--limit` accepts
|
|
587
|
+
1..256 and `--max-work` accepts 1..20,000,000. Cut budgets have separate
|
|
588
|
+
`--cut-max-states`, `--cut-max-work`, `--cut-max-cuts` and `--cut-max-size` flags
|
|
589
|
+
(defaults: 20,000 / 2,000,000 / 32 / 8). Each snapshot is limited to 10,000 nodes,
|
|
590
|
+
50,000 relationships and, for CLI files, 16 MiB. The active blocker cone is
|
|
591
|
+
limited to 512 unresolved items including the goal; this is a preflight cap,
|
|
592
|
+
not a completion guarantee. Completion requires `--cut-max-size` (default 8,
|
|
593
|
+
hard cap 64) to be at least the number of unresolved prerequisites in the
|
|
594
|
+
active cone, excluding the goal. A chain of 9 unresolved prerequisites therefore
|
|
595
|
+
returns `CUT_SEARCH_INCOMPLETE` with defaults; more than 64 cannot complete
|
|
596
|
+
under the current search. Other cut budgets can stop even smaller searches.
|
|
597
|
+
Explanation titles/statuses are clipped to
|
|
598
|
+
160/64 Unicode code points with `textClipped: true`; ids are limited to 256
|
|
599
|
+
characters. The host's JSON/TOON projection and token budgets also apply to the
|
|
600
|
+
declared `changes` rows. Refusals exit 2 (unknown goal: 3) and preserve
|
|
601
|
+
`refusal.code`, `refusal.remedy` and the failing side.
|
|
602
|
+
|
|
603
|
+
This is a comparison over the supplied snapshots. Core owns snapshot capture
|
|
604
|
+
and storage; pm-context owns resume packets. Certify backend reads separately
|
|
605
|
+
(for example with `listAllComplete`) before `graphFromItems` projection. The
|
|
606
|
+
projection includes item relationships and facets; evidence nodes and support
|
|
607
|
+
edges must be supplied explicitly by the caller's backend. Support is a linked
|
|
608
|
+
structural assertion, not verification of a document, file, test run or URL.
|
|
609
|
+
The comparison never accesses those resources or either live backend.
|
|
610
|
+
|
|
611
|
+
See the [SDK and CLI example](docs/snapshot-impact-example.md) and the
|
|
612
|
+
[public tracker validation](docs/snapshot-impact-validation.md).
|
|
613
|
+
|
|
614
|
+
## Without git
|
|
615
|
+
|
|
616
|
+
Graph projection, consent-scoped `project`, export, offline analysis, path/impact/explain and snapshot
|
|
617
|
+
comparison work in plain trackers. Snapshot comparison reads only the two
|
|
618
|
+
explicit JSON files and also works without a tracker. Neo4j operations require
|
|
619
|
+
their configured database connection. None of these graph operations needs a
|
|
620
|
+
git repository; the no-git regression runs the installed CLI with git absent
|
|
621
|
+
from PATH.
|
|
622
|
+
|
|
461
623
|
## Graph Model
|
|
462
624
|
|
|
463
625
|
- **`PmItem`** nodes for real pm items, labelled with item type (e.g. `task`, `bug`).
|
|
@@ -558,3 +720,15 @@ form paper, carbon ink, ruled item sheets and explicit status fills. Portable fo
|
|
|
558
720
|
stacks put Archivo, Public Sans and Martian Mono first and fetch no external fonts.
|
|
559
721
|
[Design, themes, token parity and reproducible review renders](docs/triplicate-visuals.md).
|
|
560
722
|
The README banner embeds pm-brand's committed output at an immutable SHA.
|
|
723
|
+
|
|
724
|
+
## Consent-scoped cross-workspace projection
|
|
725
|
+
|
|
726
|
+
`pm pm-graph project projection.json --limit 100 --json` reads only explicitly
|
|
727
|
+
consented trackers into a read-only item graph. The typed `projectWorkspaces`
|
|
728
|
+
SDK also accepts database readers bound to existing core authorization. Qualified
|
|
729
|
+
IDs preserve workspace identity; cross-workspace evidence needs separate consent
|
|
730
|
+
and two visible endpoints. Denied or incomplete sources yield opaque boundaries,
|
|
731
|
+
with no hidden node IDs, labels or source exception text. Source completeness
|
|
732
|
+
and output omission receipts remain separate. JSON and compact TOON use the
|
|
733
|
+
host's standard output controls. See the [manifest and SDK example](docs/cross-workspace-example.md)
|
|
734
|
+
for scope, authorization, pagination, refusal remedies and limits.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { listAllComplete } from "@unbrained/pm-cli/sdk";
|
|
2
|
+
import type { PmCompleteListCertificate } from "@unbrained/pm-cli/sdk";
|
|
3
|
+
import type { BlockerCutReceipt, BlockerCutRefusalCode } from "./blocker-cuts.ts";
|
|
4
|
+
import type { CommandContext, ExtensionApi, Graph } from "./index.ts";
|
|
5
|
+
/** Structured CLI refusal; the host honors exit_code while retaining the full result. */
|
|
6
|
+
export type BlockerCutCommandRefusal = {
|
|
7
|
+
ok: false;
|
|
8
|
+
exit_code: number;
|
|
9
|
+
code: BlockerCutRefusalCode | "SOURCE_INCOMPLETE";
|
|
10
|
+
remediation: string;
|
|
11
|
+
refusal: {
|
|
12
|
+
code: BlockerCutCommandRefusal["code"];
|
|
13
|
+
remedy: string;
|
|
14
|
+
};
|
|
15
|
+
/** Declared row collection is present even when the host delivers a refusal. */
|
|
16
|
+
cuts: [];
|
|
17
|
+
receipt?: BlockerCutReceipt;
|
|
18
|
+
source?: PmCompleteListCertificate;
|
|
19
|
+
};
|
|
20
|
+
/** Register the offline command; source reads are strict, complete, and extension-free. */
|
|
21
|
+
export declare function registerBlockerCuts(api: ExtensionApi, resolveRoot: (context: CommandContext) => string, project: (items: Awaited<ReturnType<typeof listAllComplete>>["items"], root: string) => Graph): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineCommand, defineFlag, listAllComplete, readSettings, resolveRuntimeStatusRegistry } from "@unbrained/pm-cli/sdk";
|
|
2
|
+
import { minimalBlockerCuts } from "./blocker-cuts.js";
|
|
3
|
+
/** Preserve refusal evidence in CLI JSON/TOON instead of losing it in error formatting. */
|
|
4
|
+
function commandRefusal(code, remedy, receipt, source) {
|
|
5
|
+
return { ok: false, code, remediation: remedy, refusal: { code, remedy }, cuts: [],
|
|
6
|
+
exit_code: code === "UNKNOWN_GOAL" ? 3 : 2, receipt, source };
|
|
7
|
+
}
|
|
8
|
+
/** Register the offline command; source reads are strict, complete, and extension-free. */
|
|
9
|
+
export function registerBlockerCuts(api, resolveRoot, project) {
|
|
10
|
+
api.registerCommand(defineCommand({
|
|
11
|
+
name: "pm-graph blocker-cuts",
|
|
12
|
+
description: "Enumerate alternative minimal blocker frontiers for a goal with bounded completeness receipts.",
|
|
13
|
+
read_output: { row_keys: ["cuts"] },
|
|
14
|
+
arguments: [{ name: "goal", required: true, description: "Exact goal item id" }],
|
|
15
|
+
flags: [
|
|
16
|
+
defineFlag({ long: "--max-states", value_name: "integer", value_type: "number", description: "Positive integer search-state budget (default 20000)." }),
|
|
17
|
+
defineFlag({ long: "--max-work", value_name: "integer", value_type: "number", description: "Positive integer work budget (default 2000000)." }),
|
|
18
|
+
defineFlag({ long: "--max-cuts", value_name: "integer", value_type: "number", description: "Positive integer cut limit (default 32)." }),
|
|
19
|
+
defineFlag({ long: "--max-size", value_name: "integer", value_type: "number", description: "Positive integer frontier size limit (default 8)." }),
|
|
20
|
+
],
|
|
21
|
+
examples: ["pm pm-graph blocker-cuts goal-id --max-cuts 16 --json"],
|
|
22
|
+
failure_hints: ["Reload the full tracker for missing nodes; break cycles; increase search limits only when the receipt is partial."],
|
|
23
|
+
run: async (context) => {
|
|
24
|
+
const positionals = context.args ?? [];
|
|
25
|
+
const options = {};
|
|
26
|
+
for (const key of ["maxStates", "maxWork", "maxCuts", "maxSize"]) {
|
|
27
|
+
const value = context.options?.[key];
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
31
|
+
return commandRefusal("INVALID_OPTIONS", `Supply a positive integer for ${key}.`);
|
|
32
|
+
}
|
|
33
|
+
options[key] = value;
|
|
34
|
+
}
|
|
35
|
+
if (positionals.length !== 1)
|
|
36
|
+
return commandRefusal("INVALID_OPTIONS", "Use pm pm-graph blocker-cuts <exact-goal-id>; see --help for budgets.");
|
|
37
|
+
let source;
|
|
38
|
+
let snapshot;
|
|
39
|
+
let root;
|
|
40
|
+
try {
|
|
41
|
+
root = resolveRoot(context);
|
|
42
|
+
snapshot = await listAllComplete({}, { pmRoot: root, noExtensions: true });
|
|
43
|
+
source = snapshot.complete_list;
|
|
44
|
+
const registry = resolveRuntimeStatusRegistry((await readSettings(root)).schema);
|
|
45
|
+
options.resolvedStatuses = [...registry.terminal_statuses];
|
|
46
|
+
options.unresolvedStatuses = [...registry.by_id.keys()].filter((status) => !registry.terminal_statuses.has(status));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return commandRefusal("SOURCE_INCOMPLETE", `Restore a readable complete tracker; run pm list --all --full --strict-read --output-limit unbounded --output-budget unbounded, then retry. Cause: ${cause}`);
|
|
51
|
+
}
|
|
52
|
+
const graph = project(snapshot.items, root);
|
|
53
|
+
const result = minimalBlockerCuts(graph, positionals[0], options);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return { ...commandRefusal(result.refusal.code, result.refusal.remedy, result.receipt, source), goal: result.goal };
|
|
56
|
+
return { ...result, source };
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=blocker-cuts-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocker-cuts-command.js","sourceRoot":"","sources":["../src/blocker-cuts-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAE/H,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAiBvD,2FAA2F;AAC3F,SAAS,cAAc,CAAC,IAAsC,EAAE,MAAc,EAAE,OAA2B,EAAE,MAAkC;IAC7I,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE;QAChF,SAAS,EAAE,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAClE,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,mBAAmB,CACjC,GAAiB,EACjB,WAAgD,EAChD,OAA6F;IAE7F,GAAG,CAAC,eAAe,CAAC,aAAa,CAAC;QAChC,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EAAE,gGAAgG;QAC7G,WAAW,EAAE,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE;QACnC,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;QAChF,KAAK,EAAE;YACL,UAAU,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;YACvJ,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;YAC/I,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;YACxI,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;SAClJ;QACD,QAAQ,EAAE,CAAC,uDAAuD,CAAC;QACnE,aAAa,EAAE,CAAC,mHAAmH,CAAC;QACpI,GAAG,EAAE,KAAK,EAAE,OAAuB,EAAE,EAAE;YACrC,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;YACvC,MAAM,OAAO,GAAsB,EAAE,CAAC;YACtC,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAU,EAAE,CAAC;gBAC1E,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;gBACrC,IAAI,KAAK,KAAK,SAAS;oBAAE,SAAS;gBAClC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;oBAC5E,OAAO,cAAc,CAAC,iBAAiB,EAAE,iCAAiC,GAAG,GAAG,CAAC,CAAC;gBACpF,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACvB,CAAC;YACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,cAAc,CAAC,iBAAiB,EAAE,uEAAuE,CAAC,CAAC;YAChJ,IAAI,MAAiC,CAAC;YACtC,IAAI,QAAqD,CAAC;YAC1D,IAAI,IAAY,CAAC;YACjB,IAAI,CAAC;gBACH,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;gBAC5B,QAAQ,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3E,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC;gBAChC,MAAM,QAAQ,GAAG,4BAA4B,CAAC,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;gBACjF,OAAO,CAAC,gBAAgB,GAAG,CAAC,GAAG,QAAQ,CAAC,iBAAiB,CAAC,CAAC;gBAC3D,OAAO,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;YACtH,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACrE,OAAO,cAAc,CAAC,mBAAmB,EAAE,sJAAsJ,KAAK,EAAE,CAAC,CAAC;YAC5M,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YAClE,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,OAAO,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YACpI,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Graph } from "./index.ts";
|
|
2
|
+
/** Deterministic limits; input size is capped and enumeration is work-budgeted. */
|
|
3
|
+
export type BlockerCutOptions = {
|
|
4
|
+
maxStates?: number;
|
|
5
|
+
maxWork?: number;
|
|
6
|
+
maxCuts?: number;
|
|
7
|
+
maxSize?: number;
|
|
8
|
+
/** Terminal statuses stop traversal; custom registries can supply their own sets. */
|
|
9
|
+
resolvedStatuses?: readonly string[];
|
|
10
|
+
/** Missing/unrecognized statuses refuse a proof instead of guessing actionability. */
|
|
11
|
+
unresolvedStatuses?: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
/** One inclusion-minimal vertex separator, sorted by id. */
|
|
14
|
+
export type BlockerCut = {
|
|
15
|
+
blockers: string[];
|
|
16
|
+
/** True only if resolving this set also satisfies every immediate prerequisite. */
|
|
17
|
+
immediatelyUnblocked: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Independent receipts for the supplied graph and the bounded enumeration. */
|
|
20
|
+
export type BlockerCutReceipt = {
|
|
21
|
+
graph: {
|
|
22
|
+
scope: "supplied-snapshot";
|
|
23
|
+
edgePolicy: "BLOCKED_BY|DEPENDS_ON|REQUIRES|reverse:BLOCKS";
|
|
24
|
+
nodes: number;
|
|
25
|
+
relationships: number;
|
|
26
|
+
reachableUnresolved: number;
|
|
27
|
+
complete: boolean;
|
|
28
|
+
unknownNodes: string[];
|
|
29
|
+
unknownNodeCount: number;
|
|
30
|
+
unknownStatuses: string[];
|
|
31
|
+
unknownStatusCount: number;
|
|
32
|
+
cycle: string[];
|
|
33
|
+
};
|
|
34
|
+
search: {
|
|
35
|
+
complete: boolean;
|
|
36
|
+
reason: "exhausted" | "maxStates" | "maxWork" | "maxCuts" | "maxSize" | "refused";
|
|
37
|
+
states: number;
|
|
38
|
+
work: number;
|
|
39
|
+
completeThroughSize: number;
|
|
40
|
+
minimumCardinality: number | null;
|
|
41
|
+
limits: {
|
|
42
|
+
maxStates: number;
|
|
43
|
+
maxWork: number;
|
|
44
|
+
maxCuts: number;
|
|
45
|
+
maxSize: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Stable refusal codes with actionable remedies, usable by a graph view without parsing text. */
|
|
50
|
+
export type BlockerCutRefusalCode = "INVALID_OPTIONS" | "GRAPH_LIMIT" | "INVALID_GRAPH" | "UNKNOWN_GOAL" | "INCOMPLETE_GRAPH" | "CYCLE";
|
|
51
|
+
/** Compact result: frontier cuts describe plans, not automatic completion of intervening work. */
|
|
52
|
+
export type BlockerCutResult = {
|
|
53
|
+
ok: true;
|
|
54
|
+
goal: string;
|
|
55
|
+
semantics: "upstream-frontier";
|
|
56
|
+
goalResolved: boolean;
|
|
57
|
+
immediateBlockers: string[];
|
|
58
|
+
cuts: BlockerCut[];
|
|
59
|
+
receipt: BlockerCutReceipt;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
goal: string;
|
|
63
|
+
refusal: {
|
|
64
|
+
code: BlockerCutRefusalCode;
|
|
65
|
+
remedy: string;
|
|
66
|
+
};
|
|
67
|
+
receipt: BlockerCutReceipt;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Enumerate inclusion-minimal unresolved vertex cuts between a goal and its
|
|
71
|
+
* upstream unresolved leaves. Every prerequisite is conjunctive. Resolving a
|
|
72
|
+
* frontier clears all upstream blocking paths; intermediate work still needs
|
|
73
|
+
* completion. `immediatelyUnblocked` distinguishes direct eligibility.
|
|
74
|
+
*
|
|
75
|
+
* Only BLOCKED_BY, DEPENDS_ON, REQUIRES and reversed BLOCKS edges are gating;
|
|
76
|
+
* hierarchy, facets and RELATED edges never imply prerequisites. Resolved
|
|
77
|
+
* nodes stop traversal. Cycles, dangling prerequisites and unknown statuses in
|
|
78
|
+
* the active goal cone refuse a proof. Completeness covers the supplied
|
|
79
|
+
* snapshot only; callers must separately certify their source read.
|
|
80
|
+
*
|
|
81
|
+
* Cardinality-ordered combination search proves minimality before emitting a
|
|
82
|
+
* cut, including when truncated. Work counts visited vertices/edges and subset
|
|
83
|
+
* comparisons; hard caps bound both output and exponential search. Preflight
|
|
84
|
+
* scans at most 100,000 nodes and 500,000 relationships; the active cone is
|
|
85
|
+
* limited to 512 unresolved vertices. No filesystem, git or service access.
|
|
86
|
+
*/
|
|
87
|
+
export declare function minimalBlockerCuts(graph: Graph, goal: string, options?: BlockerCutOptions): BlockerCutResult;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const DEFAULT_LIMITS = { maxStates: 20_000, maxWork: 2_000_000, maxCuts: 32, maxSize: 8 };
|
|
2
|
+
const HARD_LIMITS = { maxStates: 1_000_000, maxWork: 20_000_000, maxCuts: 256, maxSize: 64 };
|
|
3
|
+
const FORWARD = new Set(["BLOCKED_BY", "DEPENDS_ON", "REQUIRES"]);
|
|
4
|
+
/** Find one closed directed cycle iteratively, without enumerating exponentially many paths. */
|
|
5
|
+
function cycleWitness(adjacency, root) {
|
|
6
|
+
const color = new Map();
|
|
7
|
+
const stack = [{ id: root, next: 0 }];
|
|
8
|
+
color.set(root, 1);
|
|
9
|
+
while (stack.length) {
|
|
10
|
+
const frame = stack[stack.length - 1];
|
|
11
|
+
const neighbors = adjacency.get(frame.id);
|
|
12
|
+
if (frame.next === neighbors.length) {
|
|
13
|
+
color.set(frame.id, 2);
|
|
14
|
+
stack.pop();
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const id = neighbors[frame.next++];
|
|
18
|
+
if (color.get(id) === 1) {
|
|
19
|
+
return [...stack.slice(stack.findIndex((entry) => entry.id === id)).map((entry) => entry.id), id];
|
|
20
|
+
}
|
|
21
|
+
if (!color.has(id)) {
|
|
22
|
+
color.set(id, 1);
|
|
23
|
+
stack.push({ id, next: 0 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Enumerate inclusion-minimal unresolved vertex cuts between a goal and its
|
|
30
|
+
* upstream unresolved leaves. Every prerequisite is conjunctive. Resolving a
|
|
31
|
+
* frontier clears all upstream blocking paths; intermediate work still needs
|
|
32
|
+
* completion. `immediatelyUnblocked` distinguishes direct eligibility.
|
|
33
|
+
*
|
|
34
|
+
* Only BLOCKED_BY, DEPENDS_ON, REQUIRES and reversed BLOCKS edges are gating;
|
|
35
|
+
* hierarchy, facets and RELATED edges never imply prerequisites. Resolved
|
|
36
|
+
* nodes stop traversal. Cycles, dangling prerequisites and unknown statuses in
|
|
37
|
+
* the active goal cone refuse a proof. Completeness covers the supplied
|
|
38
|
+
* snapshot only; callers must separately certify their source read.
|
|
39
|
+
*
|
|
40
|
+
* Cardinality-ordered combination search proves minimality before emitting a
|
|
41
|
+
* cut, including when truncated. Work counts visited vertices/edges and subset
|
|
42
|
+
* comparisons; hard caps bound both output and exponential search. Preflight
|
|
43
|
+
* scans at most 100,000 nodes and 500,000 relationships; the active cone is
|
|
44
|
+
* limited to 512 unresolved vertices. No filesystem, git or service access.
|
|
45
|
+
*/
|
|
46
|
+
export function minimalBlockerCuts(graph, goal, options = {}) {
|
|
47
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
48
|
+
const receipt = {
|
|
49
|
+
graph: { scope: "supplied-snapshot", edgePolicy: "BLOCKED_BY|DEPENDS_ON|REQUIRES|reverse:BLOCKS",
|
|
50
|
+
nodes: graph.nodes.length, relationships: graph.relationships.length,
|
|
51
|
+
reachableUnresolved: 0, complete: false, unknownNodes: [], unknownNodeCount: 0,
|
|
52
|
+
unknownStatuses: [], unknownStatusCount: 0, cycle: [] },
|
|
53
|
+
search: { complete: false, reason: "refused", states: 0, work: 0, completeThroughSize: 0,
|
|
54
|
+
minimumCardinality: null, limits },
|
|
55
|
+
};
|
|
56
|
+
const refuse = (code, remedy) => ({ ok: false, goal, refusal: { code, remedy }, receipt });
|
|
57
|
+
for (const key of Object.keys(limits)) {
|
|
58
|
+
const value = options[key] ?? limits[key];
|
|
59
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > HARD_LIMITS[key]) {
|
|
60
|
+
return refuse("INVALID_OPTIONS", `Set ${key} to an integer in 1..${HARD_LIMITS[key]}.`);
|
|
61
|
+
}
|
|
62
|
+
limits[key] = value;
|
|
63
|
+
}
|
|
64
|
+
const resolved = new Set(options.resolvedStatuses ?? ["closed", "canceled"]);
|
|
65
|
+
const unresolved = new Set(options.unresolvedStatuses ?? ["open", "in_progress", "blocked"]);
|
|
66
|
+
if ([...resolved].some((status) => unresolved.has(status))) {
|
|
67
|
+
return refuse("INVALID_OPTIONS", "Supply disjoint resolvedStatuses and unresolvedStatuses.");
|
|
68
|
+
}
|
|
69
|
+
if (graph.nodes.length > 100_000 || graph.relationships.length > 500_000) {
|
|
70
|
+
return refuse("GRAPH_LIMIT", "Supply a certified goal-scoped snapshot below 100000 nodes and 500000 relationships, retaining every prerequisite.");
|
|
71
|
+
}
|
|
72
|
+
const nodes = new Map();
|
|
73
|
+
for (const node of graph.nodes) {
|
|
74
|
+
if (nodes.has(node.id))
|
|
75
|
+
return refuse("INVALID_GRAPH", "Supply unique node ids; reload the complete tracker snapshot.");
|
|
76
|
+
nodes.set(node.id, node);
|
|
77
|
+
}
|
|
78
|
+
if (!nodes.get(goal)?.labels.includes("PmItem")) {
|
|
79
|
+
return refuse("UNKNOWN_GOAL", "Select an exact PmItem id from the complete tracker snapshot.");
|
|
80
|
+
}
|
|
81
|
+
const allEdges = new Map();
|
|
82
|
+
for (const rel of graph.relationships) {
|
|
83
|
+
if (!FORWARD.has(rel.type) && rel.type !== "BLOCKS")
|
|
84
|
+
continue;
|
|
85
|
+
const from = rel.type === "BLOCKS" ? rel.to : rel.from;
|
|
86
|
+
const to = rel.type === "BLOCKS" ? rel.from : rel.to;
|
|
87
|
+
if (!allEdges.has(from))
|
|
88
|
+
allEdges.set(from, new Set());
|
|
89
|
+
allEdges.get(from).add(to);
|
|
90
|
+
}
|
|
91
|
+
const adjacency = new Map();
|
|
92
|
+
const pending = [goal];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
while (pending.length) {
|
|
95
|
+
const id = pending.pop();
|
|
96
|
+
if (seen.has(id))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(id);
|
|
99
|
+
const node = nodes.get(id);
|
|
100
|
+
if (!node?.labels.includes("PmItem")) {
|
|
101
|
+
receipt.graph.unknownNodeCount++;
|
|
102
|
+
if (receipt.graph.unknownNodes.length < 32)
|
|
103
|
+
receipt.graph.unknownNodes.push(id);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const status = String(node.properties.status);
|
|
107
|
+
if (resolved.has(status))
|
|
108
|
+
continue;
|
|
109
|
+
if (!unresolved.has(status)) {
|
|
110
|
+
receipt.graph.unknownStatusCount++;
|
|
111
|
+
if (receipt.graph.unknownStatuses.length < 32)
|
|
112
|
+
receipt.graph.unknownStatuses.push(id);
|
|
113
|
+
}
|
|
114
|
+
adjacency.set(id, []);
|
|
115
|
+
receipt.graph.reachableUnresolved = adjacency.size;
|
|
116
|
+
if (adjacency.size > 512)
|
|
117
|
+
return refuse("GRAPH_LIMIT", "Reduce the active goal cone below 513 unresolved items, retaining every prerequisite; inspect immediate blockers first.");
|
|
118
|
+
for (const next of allEdges.get(id) ?? []) {
|
|
119
|
+
const target = nodes.get(next);
|
|
120
|
+
if (target?.labels.includes("PmItem") && resolved.has(String(target.properties.status)))
|
|
121
|
+
continue;
|
|
122
|
+
adjacency.get(id).push(next);
|
|
123
|
+
}
|
|
124
|
+
adjacency.get(id).sort();
|
|
125
|
+
for (const next of adjacency.get(id))
|
|
126
|
+
pending.push(next);
|
|
127
|
+
}
|
|
128
|
+
receipt.graph.reachableUnresolved = adjacency.size;
|
|
129
|
+
receipt.graph.unknownNodes.sort();
|
|
130
|
+
receipt.graph.unknownStatuses.sort();
|
|
131
|
+
if (receipt.graph.unknownNodeCount || receipt.graph.unknownStatusCount) {
|
|
132
|
+
return refuse("INCOMPLETE_GRAPH", "Reload all referenced items with statuses; supply registry status sets for custom workflows. Do not filter or truncate the goal cone.");
|
|
133
|
+
}
|
|
134
|
+
const goalResolved = resolved.has(String(nodes.get(goal).properties.status));
|
|
135
|
+
if (!goalResolved)
|
|
136
|
+
receipt.graph.cycle = cycleWitness(adjacency, goal);
|
|
137
|
+
receipt.graph.complete = true;
|
|
138
|
+
if (receipt.graph.cycle.length)
|
|
139
|
+
return refuse("CYCLE", "Break or explicitly resolve the reported dependency cycle, then reload and retry.");
|
|
140
|
+
const immediateBlockers = adjacency.get(goal) ?? [];
|
|
141
|
+
const cuts = [];
|
|
142
|
+
const finish = (reason) => {
|
|
143
|
+
receipt.search.reason = reason;
|
|
144
|
+
receipt.search.complete = reason === "exhausted";
|
|
145
|
+
return { ok: true, goal, semantics: "upstream-frontier", goalResolved, immediateBlockers, cuts, receipt };
|
|
146
|
+
};
|
|
147
|
+
if (!immediateBlockers.length) {
|
|
148
|
+
cuts.push({ blockers: [], immediatelyUnblocked: true });
|
|
149
|
+
receipt.search.minimumCardinality = 0;
|
|
150
|
+
return finish("exhausted");
|
|
151
|
+
}
|
|
152
|
+
const candidates = [...adjacency.keys()].filter((id) => id !== goal).sort();
|
|
153
|
+
const leaves = new Set(candidates.filter((id) => adjacency.get(id).length === 0));
|
|
154
|
+
const spend = () => {
|
|
155
|
+
if (receipt.search.work === limits.maxWork)
|
|
156
|
+
return false;
|
|
157
|
+
receipt.search.work++;
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
for (let size = 1; size <= Math.min(limits.maxSize, candidates.length); size++) {
|
|
161
|
+
const indices = Array.from({ length: size }, (_, i) => i);
|
|
162
|
+
let more = true;
|
|
163
|
+
while (more) {
|
|
164
|
+
if (receipt.search.states === limits.maxStates)
|
|
165
|
+
return finish("maxStates");
|
|
166
|
+
receipt.search.states++;
|
|
167
|
+
const selected = indices.map((i) => candidates[i]);
|
|
168
|
+
const selectedSet = new Set(selected);
|
|
169
|
+
let redundant = false;
|
|
170
|
+
for (const cut of cuts) {
|
|
171
|
+
let contains = true;
|
|
172
|
+
for (const id of cut.blockers) {
|
|
173
|
+
if (!spend())
|
|
174
|
+
return finish("maxWork");
|
|
175
|
+
if (!selectedSet.has(id)) {
|
|
176
|
+
contains = false;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (contains) {
|
|
181
|
+
redundant = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!redundant) {
|
|
186
|
+
const visit = [goal];
|
|
187
|
+
const visited = new Set();
|
|
188
|
+
let separated = true;
|
|
189
|
+
while (visit.length) {
|
|
190
|
+
if (!spend())
|
|
191
|
+
return finish("maxWork");
|
|
192
|
+
const id = visit.pop();
|
|
193
|
+
if (selectedSet.has(id) || visited.has(id))
|
|
194
|
+
continue;
|
|
195
|
+
visited.add(id);
|
|
196
|
+
if (leaves.has(id)) {
|
|
197
|
+
separated = false;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
for (const next of adjacency.get(id)) {
|
|
201
|
+
if (!spend())
|
|
202
|
+
return finish("maxWork");
|
|
203
|
+
visit.push(next);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (separated) {
|
|
207
|
+
cuts.push({ blockers: selected, immediatelyUnblocked: immediateBlockers.every((id) => selectedSet.has(id)) });
|
|
208
|
+
receipt.search.minimumCardinality ??= size;
|
|
209
|
+
if (cuts.length === limits.maxCuts)
|
|
210
|
+
return finish("maxCuts");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let position = size - 1;
|
|
214
|
+
while (position >= 0 && indices[position] === candidates.length - size + position)
|
|
215
|
+
position--;
|
|
216
|
+
more = position >= 0;
|
|
217
|
+
if (more) {
|
|
218
|
+
indices[position]++;
|
|
219
|
+
for (let next = position + 1; next < size; next++)
|
|
220
|
+
indices[next] = indices[next - 1] + 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
receipt.search.completeThroughSize = size;
|
|
224
|
+
}
|
|
225
|
+
return finish(limits.maxSize < candidates.length ? "maxSize" : "exhausted");
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=blocker-cuts.js.map
|