mandrel 2.0.0 → 2.2.0
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/.agents/README.md +59 -28
- package/.agents/agents/acceptance-critic.md +20 -9
- package/.agents/agents/story-worker.md +45 -48
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +60 -46
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +33 -57
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +17 -19
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +35 -46
- package/.agents/schemas/audit-rules.json +59 -1
- package/.agents/schemas/audit-rules.schema.json +33 -1
- package/.agents/schemas/lifecycle/README.md +1 -2
- package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
- package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
- package/.agents/schemas/signal-event.schema.json +3 -3
- package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +24 -68
- package/.agents/scripts/agents-bootstrap-github.js +1 -1
- package/.agents/scripts/bootstrap.js +3 -3
- package/.agents/scripts/check-dead-exports.js +43 -104
- package/.agents/scripts/check-doc-links.js +2 -2
- package/.agents/scripts/check-lifecycle-lint.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +91 -0
- package/.agents/scripts/deliver-recover.js +122 -0
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/evidence-gate.js +20 -50
- package/.agents/scripts/generate-skills-index.js +17 -1
- package/.agents/scripts/generate-workflows-doc.js +4 -4
- package/.agents/scripts/lib/ITicketingProvider.js +1 -19
- package/.agents/scripts/lib/audit-suite/selector.js +323 -23
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
- package/.agents/scripts/lib/checks/index.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +12 -11
- package/.agents/scripts/lib/checks/state.js +17 -248
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
- package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
- package/.agents/scripts/lib/cli-args.js +23 -2
- package/.agents/scripts/lib/close-validation/gates.js +13 -13
- package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
- package/.agents/scripts/lib/close-validation/runner.js +13 -21
- package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/delivery-routing.js +7 -6
- package/.agents/scripts/lib/config/explain.js +10 -16
- package/.agents/scripts/lib/config/github.js +7 -5
- package/.agents/scripts/lib/config/limits.js +15 -25
- package/.agents/scripts/lib/config/quality.js +11 -14
- package/.agents/scripts/lib/config/runners.js +8 -21
- package/.agents/scripts/lib/config/temp-paths.js +18 -56
- package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
- package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
- package/.agents/scripts/lib/config-settings-schema.js +48 -22
- package/.agents/scripts/lib/dead-exports-knip.js +105 -0
- package/.agents/scripts/lib/dead-exports-mode.js +51 -0
- package/.agents/scripts/lib/duplicate-search.js +38 -7
- package/.agents/scripts/lib/findings/promote-finding.js +23 -14
- package/.agents/scripts/lib/format-generated-json.js +97 -0
- package/.agents/scripts/lib/framework-version.js +19 -189
- package/.agents/scripts/lib/gh-exec.js +8 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
- package/.agents/scripts/lib/git-utils.js +0 -14
- package/.agents/scripts/lib/json-utils.js +1 -2
- package/.agents/scripts/lib/label-constants.js +0 -15
- package/.agents/scripts/lib/label-taxonomy.js +1 -12
- package/.agents/scripts/lib/observability/active-story-env.js +42 -163
- package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
- package/.agents/scripts/lib/observability/signal-validator.js +4 -4
- package/.agents/scripts/lib/observability/signals-writer.js +6 -82
- package/.agents/scripts/lib/observability/source-classifier.js +2 -2
- package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +70 -191
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
- package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
- package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
- package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
- package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
- package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
- package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
- package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
- package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
- package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
- package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
- package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
- package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
- package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
- package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
- package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
- package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
- package/.agents/scripts/lib/planning-corpus.js +12 -286
- package/.agents/scripts/lib/preflight-runner.js +2 -2
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
- package/.agents/scripts/lib/signals/index.js +4 -17
- package/.agents/scripts/lib/signals/read.js +35 -35
- package/.agents/scripts/lib/signals/schema.js +8 -11
- package/.agents/scripts/lib/signals/span-tree.js +7 -7
- package/.agents/scripts/lib/signals/write.js +0 -1
- package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
- package/.agents/scripts/lib/skills/parse-skill.js +16 -3
- package/.agents/scripts/lib/story-adjacency.js +8 -7
- package/.agents/scripts/lib/story-body/story-body.js +81 -13
- package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
- package/.agents/scripts/lib/test-env.js +14 -1
- package/.agents/scripts/lib/test-tiers.js +0 -3
- package/.agents/scripts/lib/ticket-body-sections.js +0 -14
- package/.agents/scripts/lib/validation-evidence.js +31 -59
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
- package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
- package/.agents/scripts/plan-context.js +38 -7
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/plan-persist.js +145 -35
- package/.agents/scripts/plan-run-epilogue.js +83 -38
- package/.agents/scripts/post-structured-comment.js +0 -38
- package/.agents/scripts/pr-watch-with-update.js +43 -22
- package/.agents/scripts/providers/github/compose.js +0 -1
- package/.agents/scripts/providers/github/errors.js +0 -19
- package/.agents/scripts/providers/github/issues.js +1 -11
- package/.agents/scripts/providers/github/mappers.js +5 -0
- package/.agents/scripts/providers/github/sub-issues.js +0 -47
- package/.agents/scripts/providers/github/tickets.js +33 -153
- package/.agents/scripts/providers/github.js +17 -6
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/resolve-stories.js +236 -0
- package/.agents/scripts/run-coverage.js +4 -1
- package/.agents/scripts/run-lint.js +2 -2
- package/.agents/scripts/run-verify.js +31 -2
- package/.agents/scripts/signals-view.js +9 -10
- package/.agents/scripts/single-story-close.js +173 -18
- package/.agents/scripts/single-story-confirm-merge.js +288 -15
- package/.agents/scripts/single-story-init.js +6 -10
- package/.agents/scripts/stories-wave-tick.js +380 -53
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/update-ticket-state.js +8 -50
- package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
- package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
- package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
- package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
- package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
- package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
- package/.agents/skills/core/scope-triage/SKILL.md +3 -0
- package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
- package/.agents/skills/core/security-and-hardening/reference.md +375 -0
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
- package/.agents/workflows/audit-architecture.md +3 -4
- package/.agents/workflows/audit-clean-code.md +4 -4
- package/.agents/workflows/audit-documentation.md +4 -5
- package/.agents/workflows/audit-lighthouse.md +8 -0
- package/.agents/workflows/audit-navigability.md +10 -0
- package/.agents/workflows/audit-performance.md +2 -3
- package/.agents/workflows/audit-quality.md +8 -9
- package/.agents/workflows/audit-security.md +1 -2
- package/.agents/workflows/audit-seo.md +10 -0
- package/.agents/workflows/audit-ux-ui.md +7 -0
- package/.agents/workflows/deliver.md +133 -45
- package/.agents/workflows/git-cleanup.md +2 -2
- package/.agents/workflows/git-deliver.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
- package/.agents/workflows/helpers/code-review.md +14 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +209 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +239 -19
- package/.agents/workflows/qa-assist.md +6 -6
- package/.agents/workflows/qa-explore.md +3 -3
- package/.agents/workflows/qa-run.md +1 -5
- package/bin/mandrel.js +12 -1
- package/docs/CHANGELOG.md +62 -0
- package/lib/cli/registry.js +262 -19
- package/lib/cli/sync-agents.js +157 -0
- package/lib/cli/sync-commands.js +115 -6
- package/lib/cli/sync.js +168 -6
- package/lib/cli/update.js +105 -8
- package/lib/cli/version-helpers.js +131 -0
- package/lib/migrations/README.md +7 -5
- package/lib/migrations/index.js +17 -9
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
- package/.agents/schemas/epic-perf-report.schema.json +0 -89
- package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
- package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
- package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
- package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
- package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
- package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
- package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
- package/.agents/schemas/risk-verdict.schema.json +0 -53
- package/.agents/schemas/story-perf-summary.schema.json +0 -73
- package/.agents/scripts/analyze-execution.js +0 -444
- package/.agents/scripts/check-prepush-recovery.js +0 -90
- package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
- package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
- package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
- package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
- package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
- package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
- package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
- package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
- package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
- package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
- package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
- package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
- package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
- package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
- package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
- package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
- package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
- package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
- package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
- package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
- package/.agents/scripts/resolve-plan-run.js +0 -117
- package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* preflight-cache.js — Epic #3019 / Story #3027.
|
|
3
|
-
*
|
|
4
|
-
* Legacy cache adapter for the snapshot/DAG envelope the deleted
|
|
5
|
-
* `epic-deliver-preflight.js` / `epic-deliver-prepare.js` pair produced
|
|
6
|
-
* (pre-v2 Epic `/deliver`). Retained so fingerprint helpers and tests stay
|
|
7
|
-
* stable; no live v2 entry seam reads or writes this cache.
|
|
8
|
-
*
|
|
9
|
-
* Contract:
|
|
10
|
-
* - Cache file lives at `<cwd>/temp/epic-<epicId>/preflight-snapshot.json`.
|
|
11
|
-
* - Envelope shape:
|
|
12
|
-
* {
|
|
13
|
-
* epicId: number,
|
|
14
|
-
* baseSha: string, // deterministic fingerprint of the Epic
|
|
15
|
-
* // snapshot returned by provider.getTicket
|
|
16
|
-
* capturedAt: string, // ISO-8601 timestamp
|
|
17
|
-
* epic: object, // the snapshot from runSnapshotPhase
|
|
18
|
-
* stories: object[], // the open Story tickets from runBuildWaveDagPhase
|
|
19
|
-
* waves: Array<Array<object>>, // wave DAG (ids + labels preserved)
|
|
20
|
-
* }
|
|
21
|
-
* - `computeBaseSha(epic)` derives the cache key from the same provider
|
|
22
|
-
* call (`getTicket(epicId)`) used to walk the hierarchy. The hash
|
|
23
|
-
* covers the Epic's id, body, labels (sorted), and (when present)
|
|
24
|
-
* `updatedAt` — the same fields that drive Story discovery and the
|
|
25
|
-
* acceptance start gate (the Epic body's ## Acceptance Table).
|
|
26
|
-
* - Cache miss is signalled by `readCache` returning `null`. Callers
|
|
27
|
-
* MUST fall back to a fresh snapshot/DAG pass.
|
|
28
|
-
* - `writeCache` creates the parent directory recursively and writes
|
|
29
|
-
* atomically (write to `<path>.tmp` then rename).
|
|
30
|
-
*
|
|
31
|
-
* The module is pure JS / node:fs — no provider dependency — so tests
|
|
32
|
-
* can exercise it without spinning up a GitHub mock.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { createHash } from 'node:crypto';
|
|
36
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
37
|
-
import path from 'node:path';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolve the on-disk path for an Epic's preflight cache.
|
|
41
|
-
*
|
|
42
|
-
* @param {{ epicId: number, cwd?: string }} args
|
|
43
|
-
* @returns {string}
|
|
44
|
-
*/
|
|
45
|
-
export function preflightCachePath({ epicId, cwd }) {
|
|
46
|
-
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
47
|
-
throw new TypeError(
|
|
48
|
-
'preflightCachePath: epicId must be a positive integer',
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
const root = cwd ?? process.cwd();
|
|
52
|
-
return path.join(root, 'temp', `epic-${epicId}`, 'preflight-snapshot.json');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Per-ticket fingerprint fields shared by the Epic and each Story in the
|
|
57
|
-
* cache key: id, body, sorted labels, and updatedAt. Story bodies carry
|
|
58
|
-
* the dependency edges, so hashing them means a Story-dependency edit
|
|
59
|
-
* forces a cache miss (Story #4019 — the previous Epic-only key let
|
|
60
|
-
* dependency edits slip through unnoticed).
|
|
61
|
-
*
|
|
62
|
-
* @param {{ id?: number|string, number?: number|string, body?: string, labels?: string[], updatedAt?: string }} ticket
|
|
63
|
-
* @returns {{ id: number|string|null, body: string, labels: string[], updatedAt: string|null }}
|
|
64
|
-
*/
|
|
65
|
-
function ticketFingerprint(ticket) {
|
|
66
|
-
const t = ticket && typeof ticket === 'object' ? ticket : {};
|
|
67
|
-
return {
|
|
68
|
-
id: t.id ?? t.number ?? null,
|
|
69
|
-
body: typeof t.body === 'string' ? t.body : '',
|
|
70
|
-
labels: Array.isArray(t.labels) ? [...t.labels].map(String).sort() : [],
|
|
71
|
-
updatedAt: typeof t.updatedAt === 'string' ? t.updatedAt : null,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Stable string fingerprint of an Epic snapshot **plus its Story
|
|
77
|
-
* dependency state**. The hash is keyed on the exact fields
|
|
78
|
-
* `runSnapshotPhase` reads (`getTicket(epicId)` return value) and on each
|
|
79
|
-
* Story's id/body/labels/updatedAt, so that any drift the snapshot or
|
|
80
|
-
* wave-DAG phases would observe — including a Story-dependency edit —
|
|
81
|
-
* forces a cache miss (Story #4019).
|
|
82
|
-
*
|
|
83
|
-
* Labels are sorted to absorb GitHub's non-deterministic label order
|
|
84
|
-
* across responses; stories are sorted by id so enumeration order never
|
|
85
|
-
* perturbs the key. The hash is sha256; we return the full hex digest so
|
|
86
|
-
* the cache key is collision-resistant for the lifetime of a delivery.
|
|
87
|
-
*
|
|
88
|
-
* @param {{ id?: number|string, number?: number|string, body?: string, labels?: string[], updatedAt?: string }} epic
|
|
89
|
-
* @param {Array<{ id?: number|string, number?: number|string, body?: string, labels?: string[], updatedAt?: string }>} [stories]
|
|
90
|
-
* @returns {string}
|
|
91
|
-
*/
|
|
92
|
-
export function computeBaseSha(epic, stories = []) {
|
|
93
|
-
if (!epic || typeof epic !== 'object') {
|
|
94
|
-
throw new TypeError('computeBaseSha: epic snapshot must be an object');
|
|
95
|
-
}
|
|
96
|
-
const epicPrint = ticketFingerprint(epic);
|
|
97
|
-
const storyPrints = (Array.isArray(stories) ? stories : [])
|
|
98
|
-
.map(ticketFingerprint)
|
|
99
|
-
.sort((a, b) => Number(a.id ?? 0) - Number(b.id ?? 0));
|
|
100
|
-
const payload = JSON.stringify({ ...epicPrint, stories: storyPrints });
|
|
101
|
-
return createHash('sha256').update(payload).digest('hex');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Read the cached preflight envelope from disk. Returns `null` when the
|
|
106
|
-
* file is missing or unreadable — callers MUST treat that as a cache
|
|
107
|
-
* miss and fall back to a fresh snapshot/DAG pass.
|
|
108
|
-
*
|
|
109
|
-
* The function intentionally swallows `ENOENT` and JSON-parse errors so
|
|
110
|
-
* a stale or partially-written file does not poison legacy Epic prepare callers.
|
|
111
|
-
*
|
|
112
|
-
* @param {{ epicId: number, cwd?: string }} args
|
|
113
|
-
* @returns {Promise<object | null>}
|
|
114
|
-
*/
|
|
115
|
-
export async function readPreflightCache({ epicId, cwd }) {
|
|
116
|
-
const cachePath = preflightCachePath({ epicId, cwd });
|
|
117
|
-
let raw;
|
|
118
|
-
try {
|
|
119
|
-
raw = await readFile(cachePath, 'utf8');
|
|
120
|
-
} catch (err) {
|
|
121
|
-
if (err && err.code === 'ENOENT') return null;
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
const parsed = JSON.parse(raw);
|
|
126
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
127
|
-
if (parsed.epicId !== epicId) return null;
|
|
128
|
-
if (typeof parsed.baseSha !== 'string' || !parsed.baseSha) return null;
|
|
129
|
-
if (!parsed.epic || typeof parsed.epic !== 'object') return null;
|
|
130
|
-
if (!Array.isArray(parsed.stories)) return null;
|
|
131
|
-
if (!Array.isArray(parsed.waves)) return null;
|
|
132
|
-
return parsed;
|
|
133
|
-
} catch {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Persist the preflight envelope to disk. Writes atomically via tmp +
|
|
140
|
-
* rename so concurrent readers never observe a partial file.
|
|
141
|
-
*
|
|
142
|
-
* @param {{ epicId: number, baseSha: string, epic: object, stories: object[], waves: Array<Array<object>>, cwd?: string, capturedAt?: string }} args
|
|
143
|
-
* @returns {Promise<{ cachePath: string, baseSha: string }>}
|
|
144
|
-
*/
|
|
145
|
-
export async function writePreflightCache({
|
|
146
|
-
epicId,
|
|
147
|
-
baseSha,
|
|
148
|
-
epic,
|
|
149
|
-
stories,
|
|
150
|
-
waves,
|
|
151
|
-
cwd,
|
|
152
|
-
capturedAt,
|
|
153
|
-
}) {
|
|
154
|
-
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
155
|
-
throw new TypeError(
|
|
156
|
-
'writePreflightCache: epicId must be a positive integer',
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
if (typeof baseSha !== 'string' || !baseSha) {
|
|
160
|
-
throw new TypeError(
|
|
161
|
-
'writePreflightCache: baseSha must be a non-empty string',
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
if (!epic || typeof epic !== 'object') {
|
|
165
|
-
throw new TypeError('writePreflightCache: epic must be an object');
|
|
166
|
-
}
|
|
167
|
-
if (!Array.isArray(stories)) {
|
|
168
|
-
throw new TypeError('writePreflightCache: stories must be an array');
|
|
169
|
-
}
|
|
170
|
-
if (!Array.isArray(waves)) {
|
|
171
|
-
throw new TypeError('writePreflightCache: waves must be an array');
|
|
172
|
-
}
|
|
173
|
-
const cachePath = preflightCachePath({ epicId, cwd });
|
|
174
|
-
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
175
|
-
const envelope = {
|
|
176
|
-
epicId,
|
|
177
|
-
baseSha,
|
|
178
|
-
capturedAt: capturedAt ?? new Date().toISOString(),
|
|
179
|
-
epic,
|
|
180
|
-
stories,
|
|
181
|
-
waves,
|
|
182
|
-
};
|
|
183
|
-
const tmp = `${cachePath}.tmp`;
|
|
184
|
-
await writeFile(tmp, `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
|
|
185
|
-
await rename(tmp, cachePath);
|
|
186
|
-
return { cachePath, baseSha };
|
|
187
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for resolve-plan-run.js — keep the CLI entry thin so new-file
|
|
3
|
-
* CRAP stays under the quality-preview ceiling.
|
|
4
|
-
*
|
|
5
|
-
* @module lib/orchestration/resolve-plan-run
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { TYPE_LABELS } from '../label-constants.js';
|
|
9
|
-
import { buildStoryAdjacency } from '../story-adjacency.js';
|
|
10
|
-
import {
|
|
11
|
-
normalizePlanRunId,
|
|
12
|
-
PLAN_RUN_LABEL_PREFIX,
|
|
13
|
-
} from './plan-persist/story-ops.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a plan-run id into the canonical label `plan-run::<token>`.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} raw
|
|
19
|
-
* @returns {string}
|
|
20
|
-
*/
|
|
21
|
-
export function normalizePlanRunLabel(raw) {
|
|
22
|
-
const token = normalizePlanRunId(raw);
|
|
23
|
-
return `${PLAN_RUN_LABEL_PREFIX}${token}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extract a bare plan-run id from a label or raw token.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} label
|
|
30
|
-
* @returns {string}
|
|
31
|
-
*/
|
|
32
|
-
export function planRunIdFromLabel(label) {
|
|
33
|
-
return String(label ?? '')
|
|
34
|
-
.trim()
|
|
35
|
-
.replace(new RegExp(`^${PLAN_RUN_LABEL_PREFIX}`), '');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {object} issue
|
|
40
|
-
* @returns {string[]}
|
|
41
|
-
*/
|
|
42
|
-
export function normalizeIssueLabels(issue) {
|
|
43
|
-
const raw = issue?.labels;
|
|
44
|
-
if (!Array.isArray(raw)) return [];
|
|
45
|
-
return raw
|
|
46
|
-
.map((l) => (typeof l === 'string' ? l : l?.name))
|
|
47
|
-
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {object} issue
|
|
52
|
-
* @returns {{ id: number, title: string, body: string, url: string|null, labels: string[], state: string }|null}
|
|
53
|
-
*/
|
|
54
|
-
export function toStoryRecord(issue) {
|
|
55
|
-
const id = Number(issue?.number ?? issue?.id);
|
|
56
|
-
if (!Number.isInteger(id) || id <= 0) return null;
|
|
57
|
-
const labels = normalizeIssueLabels(issue);
|
|
58
|
-
if (!labels.includes(TYPE_LABELS.STORY)) return null;
|
|
59
|
-
return {
|
|
60
|
-
id,
|
|
61
|
-
title: String(issue?.title ?? ''),
|
|
62
|
-
body: String(issue?.body ?? ''),
|
|
63
|
-
url: issue?.html_url ?? issue?.url ?? null,
|
|
64
|
-
labels,
|
|
65
|
-
state: String(issue?.state ?? 'open').toLowerCase(),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {object[]} stories
|
|
71
|
-
* @returns {{ id: number, dependsOn: number[] }[]}
|
|
72
|
-
*/
|
|
73
|
-
export function storiesToDag(stories) {
|
|
74
|
-
const adjacency = buildStoryAdjacency(stories, { dropForeign: false });
|
|
75
|
-
return stories.map((s) => ({
|
|
76
|
-
id: s.id,
|
|
77
|
-
dependsOn: adjacency.get(s.id) ?? [],
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Map provider issues into the deliver-router story list + DAG nodes.
|
|
83
|
-
*
|
|
84
|
-
* @param {object[]} issues
|
|
85
|
-
* @param {{ planRunId: string, planRunLabel: string }} meta
|
|
86
|
-
* @returns {{ kind: string, planRunId: string, planRunLabel: string, stories: object[], dag: object[] }}
|
|
87
|
-
*/
|
|
88
|
-
export function buildPlanRunEnvelope(issues, { planRunId, planRunLabel }) {
|
|
89
|
-
const stories = (Array.isArray(issues) ? issues : [])
|
|
90
|
-
.map(toStoryRecord)
|
|
91
|
-
.filter(Boolean)
|
|
92
|
-
.sort((a, b) => a.id - b.id);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
kind: 'plan-run',
|
|
96
|
-
planRunId,
|
|
97
|
-
planRunLabel,
|
|
98
|
-
stories: stories.map(({ id, title, url, labels, state }) => ({
|
|
99
|
-
id,
|
|
100
|
-
title,
|
|
101
|
-
url,
|
|
102
|
-
labels,
|
|
103
|
-
state,
|
|
104
|
-
})),
|
|
105
|
-
dag: storiesToDag(stories),
|
|
106
|
-
done: stories
|
|
107
|
-
.filter(
|
|
108
|
-
(story) =>
|
|
109
|
-
story.state === 'closed' || story.labels.includes('agent::done'),
|
|
110
|
-
)
|
|
111
|
-
.map((story) => story.id),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Pure core used by tests: given issues + meta, build the envelope.
|
|
117
|
-
*
|
|
118
|
-
* @param {{ run: string, issues: object[] }} args
|
|
119
|
-
* @returns {ReturnType<typeof buildPlanRunEnvelope>}
|
|
120
|
-
*/
|
|
121
|
-
export function resolvePlanRunFromIssues({ run, issues }) {
|
|
122
|
-
const planRunLabel = normalizePlanRunLabel(run);
|
|
123
|
-
const planRunId = planRunIdFromLabel(planRunLabel);
|
|
124
|
-
return buildPlanRunEnvelope(issues, { planRunId, planRunLabel });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Fetch issues for a plan-run label via the ticketing provider.
|
|
129
|
-
*
|
|
130
|
-
* @param {object} provider
|
|
131
|
-
* @param {{ planRunLabel: string, state: string }} args
|
|
132
|
-
* @returns {Promise<object[]>}
|
|
133
|
-
*/
|
|
134
|
-
export async function fetchPlanRunIssues(provider, { planRunLabel, state }) {
|
|
135
|
-
if (typeof provider?.listIssuesByLabel !== 'function') {
|
|
136
|
-
throw new Error('resolve-plan-run: provider.listIssuesByLabel is required');
|
|
137
|
-
}
|
|
138
|
-
const labels = `${planRunLabel},${TYPE_LABELS.STORY}`;
|
|
139
|
-
const issues = await provider.listIssuesByLabel({ state, labels });
|
|
140
|
-
return Array.isArray(issues) ? issues : [];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* @param {string|undefined} state
|
|
145
|
-
* @returns {string}
|
|
146
|
-
*/
|
|
147
|
-
export function assertIssueState(state) {
|
|
148
|
-
const value = state ?? 'open';
|
|
149
|
-
if (!['open', 'closed', 'all'].includes(value)) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`resolve-plan-run: --state must be open|closed|all (got ${value})`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/orchestration/retro-perf-heuristics.js — classify perf signals.
|
|
3
|
-
*
|
|
4
|
-
* Story #3042 / Task #3045 (Epic #3019). Pure functions that turn the
|
|
5
|
-
* `epic-perf-report.waveParallelism` row shape (Story #3025) into a flat
|
|
6
|
-
* list of perf signals. The retro renderer (`./retro/phases/compose-body.js`)
|
|
7
|
-
* consumes the list to surface a `## Performance Signals` section and a
|
|
8
|
-
* `## Recommended Follow-Ons` paste-ready stanza per signal.
|
|
9
|
-
*
|
|
10
|
-
* Signals (kinds):
|
|
11
|
-
*
|
|
12
|
-
* - `low-utilisation` — any wave whose `utilisation < thresholds.utilisation`.
|
|
13
|
-
* One signal per offending wave. The signal carries the wave index and
|
|
14
|
-
* the observed utilisation so the renderer can name the offender.
|
|
15
|
-
*
|
|
16
|
-
* - `high-bootstrap-share` — emitted when the summed `story-init` /
|
|
17
|
-
* bootstrap phase time across all waves exceeds
|
|
18
|
-
* `thresholds.bootstrapShare` of the cumulative `summedStoryMs`. This is
|
|
19
|
-
* a single signal across the whole report (not one per wave) because the
|
|
20
|
-
* framework remediation is an Epic-wide bootstrap-cost story, not a
|
|
21
|
-
* per-wave tweak. The signal payload carries the observed share.
|
|
22
|
-
*
|
|
23
|
-
* - `cap-binding-run` — emitted when `>= thresholds.capBindingRunLength`
|
|
24
|
-
* consecutive waves report `capBinding: true`. One signal per maximal
|
|
25
|
-
* run, carrying the run's first/last wave index and the run length so
|
|
26
|
-
* the renderer can attribute the run-length back to the deliver-runner
|
|
27
|
-
* concurrency cap.
|
|
28
|
-
*
|
|
29
|
-
* Threshold contract (code constants only — not operator-tunable via
|
|
30
|
-
* `.agentrc.json`; the former `delivery.retro.perfThresholds` key was
|
|
31
|
-
* retired):
|
|
32
|
-
*
|
|
33
|
-
* - `utilisation`: number in [0, 1]. Default 0.6.
|
|
34
|
-
* - `bootstrapShare`: number in [0, 1]. Default 0.4.
|
|
35
|
-
* - `capBindingRunLength`: positive integer. Default 2.
|
|
36
|
-
*
|
|
37
|
-
* The function is total: it never throws on malformed input. A missing or
|
|
38
|
-
* malformed `report` returns `[]`. A missing `thresholds` falls back to
|
|
39
|
-
* `DEFAULT_RETRO_PERF_THRESHOLDS` below.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Framework-fixed defaults for `classifyPerfSignals`. Callers (and tests)
|
|
44
|
-
* can omit `thresholds` and get these values. Not sourced from agentrc —
|
|
45
|
-
* edit this constant (and keep tests in lockstep) to change the gates.
|
|
46
|
-
*/
|
|
47
|
-
export const DEFAULT_RETRO_PERF_THRESHOLDS = Object.freeze({
|
|
48
|
-
utilisation: 0.6,
|
|
49
|
-
bootstrapShare: 0.4,
|
|
50
|
-
capBindingRunLength: 2,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
function isObject(v) {
|
|
54
|
-
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function clampUnit(n, fallback) {
|
|
58
|
-
if (typeof n !== 'number' || !Number.isFinite(n)) return fallback;
|
|
59
|
-
// Out-of-range values fall back to the documented default rather than
|
|
60
|
-
// being clamped — operators wiring nonsensical thresholds should see the
|
|
61
|
-
// safe default applied, not a silent 0/1 substitute that subtly changes
|
|
62
|
-
// the heuristic.
|
|
63
|
-
if (n < 0 || n > 1) return fallback;
|
|
64
|
-
return n;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function positiveInt(n, fallback) {
|
|
68
|
-
if (typeof n !== 'number' || !Number.isFinite(n)) return fallback;
|
|
69
|
-
if (!Number.isInteger(n) || n < 1) return fallback;
|
|
70
|
-
return n;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Resolve the threshold trio against the documented defaults. Exported for
|
|
75
|
-
* the test surface so callers can verify the fallback semantics in
|
|
76
|
-
* isolation.
|
|
77
|
-
*
|
|
78
|
-
* @param {object|null|undefined} thresholds
|
|
79
|
-
* @returns {{ utilisation: number, bootstrapShare: number, capBindingRunLength: number }}
|
|
80
|
-
*/
|
|
81
|
-
export function resolvePerfThresholds(thresholds) {
|
|
82
|
-
const src = isObject(thresholds) ? thresholds : {};
|
|
83
|
-
return {
|
|
84
|
-
utilisation: clampUnit(
|
|
85
|
-
src.utilisation,
|
|
86
|
-
DEFAULT_RETRO_PERF_THRESHOLDS.utilisation,
|
|
87
|
-
),
|
|
88
|
-
bootstrapShare: clampUnit(
|
|
89
|
-
src.bootstrapShare,
|
|
90
|
-
DEFAULT_RETRO_PERF_THRESHOLDS.bootstrapShare,
|
|
91
|
-
),
|
|
92
|
-
capBindingRunLength: positiveInt(
|
|
93
|
-
src.capBindingRunLength,
|
|
94
|
-
DEFAULT_RETRO_PERF_THRESHOLDS.capBindingRunLength,
|
|
95
|
-
),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Extract the validated `waveParallelism` rows from the report. Rows whose
|
|
101
|
-
* `waveIndex` is not a non-negative integer are dropped (the schema requires
|
|
102
|
-
* one, but the heuristics module must tolerate malformed historical reports).
|
|
103
|
-
*
|
|
104
|
-
* @param {object|null|undefined} report
|
|
105
|
-
* @returns {Array<{ waveIndex: number, wallClockMs: number, summedStoryMs: number, utilisation: number, capBinding: boolean }>}
|
|
106
|
-
*/
|
|
107
|
-
function extractRows(report) {
|
|
108
|
-
if (!isObject(report)) return [];
|
|
109
|
-
const arr = Array.isArray(report.waveParallelism)
|
|
110
|
-
? report.waveParallelism
|
|
111
|
-
: [];
|
|
112
|
-
return arr
|
|
113
|
-
.filter(
|
|
114
|
-
(r) => isObject(r) && Number.isInteger(r.waveIndex) && r.waveIndex >= 0,
|
|
115
|
-
)
|
|
116
|
-
.map((r) => ({
|
|
117
|
-
waveIndex: r.waveIndex,
|
|
118
|
-
wallClockMs:
|
|
119
|
-
typeof r.wallClockMs === 'number' && r.wallClockMs > 0
|
|
120
|
-
? r.wallClockMs
|
|
121
|
-
: 0,
|
|
122
|
-
summedStoryMs:
|
|
123
|
-
typeof r.summedStoryMs === 'number' && r.summedStoryMs > 0
|
|
124
|
-
? r.summedStoryMs
|
|
125
|
-
: 0,
|
|
126
|
-
utilisation:
|
|
127
|
-
typeof r.utilisation === 'number' && Number.isFinite(r.utilisation)
|
|
128
|
-
? r.utilisation
|
|
129
|
-
: 0,
|
|
130
|
-
capBinding: Boolean(r.capBinding),
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Build the `low-utilisation` signals — one per wave whose `utilisation` is
|
|
136
|
-
* strictly below the threshold. Returns `[]` when no row crosses the gate.
|
|
137
|
-
*
|
|
138
|
-
* Waves with `summedStoryMs <= 0` are skipped unconditionally: when no
|
|
139
|
-
* per-Story timing windows are present the computed utilisation is 0.0%
|
|
140
|
-
* because the numerator is 0, not because the wave was genuinely idle.
|
|
141
|
-
* Scoring unknown timing as low would flood the retro with non-actionable
|
|
142
|
-
* follow-ons (Story #3850).
|
|
143
|
-
*/
|
|
144
|
-
function detectLowUtilisation(rows, threshold) {
|
|
145
|
-
const out = [];
|
|
146
|
-
for (const row of rows) {
|
|
147
|
-
// Skip waves with no timing data — utilisation is unknown, not low.
|
|
148
|
-
if (row.summedStoryMs <= 0) continue;
|
|
149
|
-
if (row.utilisation < threshold) {
|
|
150
|
-
out.push({
|
|
151
|
-
kind: 'low-utilisation',
|
|
152
|
-
waveIndex: row.waveIndex,
|
|
153
|
-
utilisation: row.utilisation,
|
|
154
|
-
threshold,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Sum the bootstrap (story-init) phase time across per-Story summaries on the
|
|
163
|
-
* report. Returns the absolute milliseconds. Falls back to 0 when no Story
|
|
164
|
-
* summary in the report carries a `phaseTimingsMs['story-init']` entry — in
|
|
165
|
-
* that case the share check necessarily returns no signal.
|
|
166
|
-
*/
|
|
167
|
-
function sumBootstrapMs(report) {
|
|
168
|
-
if (!isObject(report)) return 0;
|
|
169
|
-
const stories = Array.isArray(report.storyPerfSummaries)
|
|
170
|
-
? report.storyPerfSummaries
|
|
171
|
-
: [];
|
|
172
|
-
let total = 0;
|
|
173
|
-
for (const s of stories) {
|
|
174
|
-
if (!isObject(s)) continue;
|
|
175
|
-
const timings = isObject(s.phaseTimingsMs) ? s.phaseTimingsMs : null;
|
|
176
|
-
if (!timings) continue;
|
|
177
|
-
const v = timings['story-init'];
|
|
178
|
-
if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
|
|
179
|
-
total += v;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return total;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Build the (at-most-one) `high-bootstrap-share` signal. Returns `[]` when
|
|
187
|
-
* either the cumulative `summedStoryMs` is zero (no wave timing) or the
|
|
188
|
-
* observed bootstrap share is at-or-below the threshold.
|
|
189
|
-
*/
|
|
190
|
-
function detectHighBootstrapShare(rows, report, threshold) {
|
|
191
|
-
const totalStoryMs = rows.reduce((acc, r) => acc + r.summedStoryMs, 0);
|
|
192
|
-
if (totalStoryMs <= 0) return [];
|
|
193
|
-
const bootstrapMs = sumBootstrapMs(report);
|
|
194
|
-
if (bootstrapMs <= 0) return [];
|
|
195
|
-
const share = bootstrapMs / totalStoryMs;
|
|
196
|
-
if (share <= threshold) return [];
|
|
197
|
-
return [
|
|
198
|
-
{
|
|
199
|
-
kind: 'high-bootstrap-share',
|
|
200
|
-
share,
|
|
201
|
-
bootstrapMs,
|
|
202
|
-
summedStoryMs: totalStoryMs,
|
|
203
|
-
threshold,
|
|
204
|
-
},
|
|
205
|
-
];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Walk the rows in `waveIndex` order and emit one `cap-binding-run` signal
|
|
210
|
-
* per maximal run of consecutive `capBinding: true` waves whose length is at
|
|
211
|
-
* least `threshold`.
|
|
212
|
-
*/
|
|
213
|
-
function detectCapBindingRuns(rows, threshold) {
|
|
214
|
-
const ordered = [...rows].sort((a, b) => a.waveIndex - b.waveIndex);
|
|
215
|
-
const out = [];
|
|
216
|
-
let runStart = null;
|
|
217
|
-
let runLen = 0;
|
|
218
|
-
for (let i = 0; i < ordered.length; i += 1) {
|
|
219
|
-
const row = ordered[i];
|
|
220
|
-
if (row.capBinding) {
|
|
221
|
-
if (runStart === null) runStart = row.waveIndex;
|
|
222
|
-
runLen += 1;
|
|
223
|
-
// End of array? Flush the run.
|
|
224
|
-
if (i === ordered.length - 1 && runLen >= threshold) {
|
|
225
|
-
out.push({
|
|
226
|
-
kind: 'cap-binding-run',
|
|
227
|
-
fromWaveIndex: runStart,
|
|
228
|
-
toWaveIndex: row.waveIndex,
|
|
229
|
-
runLength: runLen,
|
|
230
|
-
threshold,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
if (runLen >= threshold && runStart !== null) {
|
|
235
|
-
out.push({
|
|
236
|
-
kind: 'cap-binding-run',
|
|
237
|
-
fromWaveIndex: runStart,
|
|
238
|
-
toWaveIndex: ordered[i - 1].waveIndex,
|
|
239
|
-
runLength: runLen,
|
|
240
|
-
threshold,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
runStart = null;
|
|
244
|
-
runLen = 0;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Classify perf signals from an `epic-perf-report` payload.
|
|
252
|
-
*
|
|
253
|
-
* Always returns an array of signal objects. The array is empty when no
|
|
254
|
-
* signal trips. The caller (retro renderer) is responsible for suppressing
|
|
255
|
-
* the entire `## Performance Signals` and `## Recommended Follow-Ons`
|
|
256
|
-
* sections when this returns `[]`.
|
|
257
|
-
*
|
|
258
|
-
* @param {object|null|undefined} report `epic-perf-report` payload.
|
|
259
|
-
* @param {object|null|undefined} thresholds Optional override of
|
|
260
|
-
* `{ utilisation, bootstrapShare, capBindingRunLength }`. Defaults
|
|
261
|
-
* resolve via `DEFAULT_RETRO_PERF_THRESHOLDS`.
|
|
262
|
-
* @returns {Array<object>} Signal list.
|
|
263
|
-
*/
|
|
264
|
-
export function classifyPerfSignals(report, thresholds) {
|
|
265
|
-
const resolved = resolvePerfThresholds(thresholds);
|
|
266
|
-
const rows = extractRows(report);
|
|
267
|
-
if (rows.length === 0) return [];
|
|
268
|
-
const signals = [];
|
|
269
|
-
signals.push(...detectLowUtilisation(rows, resolved.utilisation));
|
|
270
|
-
signals.push(
|
|
271
|
-
...detectHighBootstrapShare(rows, report, resolved.bootstrapShare),
|
|
272
|
-
);
|
|
273
|
-
signals.push(...detectCapBindingRuns(rows, resolved.capBindingRunLength));
|
|
274
|
-
return signals;
|
|
275
|
-
}
|