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,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* perf-report-readers.js — I/O readers + git-scrape helpers for the
|
|
3
|
-
* structured perf-summary comments (Epic #1030 / Story #1123, split out
|
|
4
|
-
* under Story #3350).
|
|
5
|
-
*
|
|
6
|
-
* Every export here touches the filesystem, the ticketing provider, or
|
|
7
|
-
* `git log`. The pure renderers that format the report bodies live in the
|
|
8
|
-
* sibling `perf-report-render.js`; the Story-/Epic-mode orchestrators in
|
|
9
|
-
* `analyze-execution.js` wire these readers to those renderers.
|
|
10
|
-
*
|
|
11
|
-
* @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import fs from 'node:fs/promises';
|
|
15
|
-
import { storyArtifactPath } from '../config/temp-paths.js';
|
|
16
|
-
import { gitSpawn } from '../git-utils.js';
|
|
17
|
-
import { Logger } from '../Logger.js';
|
|
18
|
-
import { read as readSignals } from '../signals/read.js';
|
|
19
|
-
import { concurrentMap } from '../util/concurrent-map.js';
|
|
20
|
-
import { extractStoryPerfSummaryFromComment } from './perf-report-render.js';
|
|
21
|
-
import { forEachLine } from './signals-writer.js';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Stream every lifecycle event under the Epic into an in-memory array
|
|
25
|
-
* (Story #3025 / Task #3030). The perf-aggregator's wave-parallelism
|
|
26
|
-
* reducer needs a single chronological iterable spanning wave-start /
|
|
27
|
-
* wave-complete / state-transition events; the canonical reader walks
|
|
28
|
-
* the epic-level signals.ndjson first, then each per-Story stream in
|
|
29
|
-
* ascending Story-ID order, so that ordering matches the wave-tick
|
|
30
|
-
* emit order at runtime.
|
|
31
|
-
*
|
|
32
|
-
* Returns `[]` on any reader failure so the analyzer keeps composing
|
|
33
|
-
* its other report sections.
|
|
34
|
-
*/
|
|
35
|
-
export async function readEpicLifecycleEvents(epicId, config, logger) {
|
|
36
|
-
const events = [];
|
|
37
|
-
try {
|
|
38
|
-
for await (const evt of readSignals({ epic: epicId, config })) {
|
|
39
|
-
if (evt && typeof evt === 'object') events.push(evt);
|
|
40
|
-
}
|
|
41
|
-
} catch (err) {
|
|
42
|
-
logger?.warn?.(
|
|
43
|
-
`[analyze-execution] readEpicLifecycleEvents(${epicId}) failed (non-fatal): ${
|
|
44
|
-
err instanceof Error ? err.message : String(err)
|
|
45
|
-
}`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
return events;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Read every NDJSON line from a Story's signals stream into an array.
|
|
53
|
-
* Missing files resolve to `[]` so callers can treat absence as
|
|
54
|
-
* "no signals yet" without a try/catch.
|
|
55
|
-
*/
|
|
56
|
-
export async function readStorySignals(epicId, storyId, config) {
|
|
57
|
-
const events = [];
|
|
58
|
-
await forEachLine(
|
|
59
|
-
epicId,
|
|
60
|
-
storyId,
|
|
61
|
-
(parsed) => {
|
|
62
|
-
if (parsed && typeof parsed === 'object') events.push(parsed);
|
|
63
|
-
},
|
|
64
|
-
config,
|
|
65
|
-
);
|
|
66
|
-
return events;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Best-effort read of the per-Story phase-timings JSON written by
|
|
71
|
-
* `post-merge-close.js`. Returns `null` when the file is missing or
|
|
72
|
-
* malformed — phaseTimingsMs in the StoryPerfSummary degrades to `{}`
|
|
73
|
-
* rather than throwing.
|
|
74
|
-
*/
|
|
75
|
-
export async function readPhaseTimings(epicId, storyId, config, overridePath) {
|
|
76
|
-
const target =
|
|
77
|
-
overridePath ??
|
|
78
|
-
storyArtifactPath(epicId, storyId, 'phase-timings.json', config);
|
|
79
|
-
try {
|
|
80
|
-
const buf = await fs.readFile(target, 'utf8');
|
|
81
|
-
const parsed = JSON.parse(buf);
|
|
82
|
-
if (parsed && typeof parsed === 'object') return parsed;
|
|
83
|
-
return null;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
if (err && err.code === 'ENOENT') return null;
|
|
86
|
-
Logger.warn(
|
|
87
|
-
`[analyze-execution] could not parse phase-timings at ${target}: ${
|
|
88
|
-
err instanceof Error ? err.message : String(err)
|
|
89
|
-
}`,
|
|
90
|
-
);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Throw-away ghSpawnCount reader (Story #1795 / Epic #1788). Looks for
|
|
97
|
-
* the `temp/epic-<eid>/stories/story-<sid>/gh-spawn-count.json` file written by
|
|
98
|
-
* `close-validation.emitGhSpawnCount` immediately before the perf-summary
|
|
99
|
-
* phase spawns this analyzer. Returns the integer count when present and
|
|
100
|
-
* well-formed; `null` when absent or unparseable. Callers treat `null` as
|
|
101
|
-
* "no signal" — the structured comment simply omits `ghSpawnCount`.
|
|
102
|
-
*/
|
|
103
|
-
export async function readGhSpawnCount(epicId, storyId, config) {
|
|
104
|
-
const target = storyArtifactPath(
|
|
105
|
-
epicId,
|
|
106
|
-
storyId,
|
|
107
|
-
'gh-spawn-count.json',
|
|
108
|
-
config,
|
|
109
|
-
);
|
|
110
|
-
try {
|
|
111
|
-
const buf = await fs.readFile(target, 'utf8');
|
|
112
|
-
const parsed = JSON.parse(buf);
|
|
113
|
-
const count = parsed?.ghSpawnCount;
|
|
114
|
-
if (Number.isInteger(count) && count >= 0) return count;
|
|
115
|
-
return null;
|
|
116
|
-
} catch (err) {
|
|
117
|
-
if (err && err.code === 'ENOENT') return null;
|
|
118
|
-
Logger.warn(
|
|
119
|
-
`[analyze-execution] could not parse gh-spawn-count at ${target}: ${
|
|
120
|
-
err instanceof Error ? err.message : String(err)
|
|
121
|
-
}`,
|
|
122
|
-
);
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Fetch every `story-perf-summary` structured comment from the Stories
|
|
129
|
-
* under an Epic. We rely on `provider.getSubTickets(epicId)` for child
|
|
130
|
-
* enumeration (Stories carry `parent: #<epicId>` either via the native
|
|
131
|
-
* sub-issue link or the body marker — `getSubTickets` reconciles both).
|
|
132
|
-
* Missing comments are skipped (the Story may have been recut and not
|
|
133
|
-
* closed yet).
|
|
134
|
-
*/
|
|
135
|
-
export async function collectStorySummaries(provider, epicId, logger) {
|
|
136
|
-
let stories;
|
|
137
|
-
try {
|
|
138
|
-
const children = await provider.getSubTickets(epicId);
|
|
139
|
-
stories = Array.isArray(children) ? children : [];
|
|
140
|
-
} catch (err) {
|
|
141
|
-
logger.warn?.(
|
|
142
|
-
`[analyze-execution] getSubTickets(${epicId}) failed: ${
|
|
143
|
-
err instanceof Error ? err.message : String(err)
|
|
144
|
-
}`,
|
|
145
|
-
);
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
// Filter to Story tickets — descendants include Tasks too. We only
|
|
149
|
-
// want the per-Story summaries, so guard on `type::story`.
|
|
150
|
-
const storyTickets = stories.filter(
|
|
151
|
-
(t) =>
|
|
152
|
-
Array.isArray(t?.labels) &&
|
|
153
|
-
t.labels.some(
|
|
154
|
-
(l) => (typeof l === 'string' ? l : l?.name) === 'type::story',
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// Bounded-parallel comment fetch (Story #3990). Each Story's comment
|
|
159
|
-
// thread is an independent paginated REST read through a fresh `gh`
|
|
160
|
-
// spawn, so a serial loop pays N sequential round-trips. concurrentMap
|
|
161
|
-
// preserves input→output index, keeping summaries in Story order; the
|
|
162
|
-
// per-Story try/catch stays inside the mapper so one failed fetch
|
|
163
|
-
// degrades to warn-and-skip without aborting the batch (mirrors
|
|
164
|
-
// verifySingleResult in lib/orchestration/wave-record-io.js, #3024).
|
|
165
|
-
const perStory = await concurrentMap(
|
|
166
|
-
storyTickets,
|
|
167
|
-
async (ticket) => {
|
|
168
|
-
const id = Number(ticket.id ?? ticket.number);
|
|
169
|
-
if (!Number.isInteger(id) || id < 1) return null;
|
|
170
|
-
let comments;
|
|
171
|
-
try {
|
|
172
|
-
comments = (await provider.getTicketComments(id)) ?? [];
|
|
173
|
-
} catch (err) {
|
|
174
|
-
logger.warn?.(
|
|
175
|
-
`[analyze-execution] getTicketComments(${id}) failed: ${
|
|
176
|
-
err instanceof Error ? err.message : String(err)
|
|
177
|
-
}`,
|
|
178
|
-
);
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
for (const c of comments) {
|
|
182
|
-
const parsed = extractStoryPerfSummaryFromComment(c?.body);
|
|
183
|
-
if (parsed) return parsed;
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
},
|
|
187
|
-
{ concurrency: 4 },
|
|
188
|
-
);
|
|
189
|
-
return perStory.filter((s) => s !== null);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Default git-log gatherer for the baseline-refresh-rate reporter
|
|
194
|
-
* (Story #1400 / Task #1427). Reads commits on `epic/<id>` over the
|
|
195
|
-
* trailing window and shapes them into the record format the pure
|
|
196
|
-
* reporter expects. Returns `[]` on any spawn failure so the retro
|
|
197
|
-
* comment keeps composing — the reporter then prints "no Story merges
|
|
198
|
-
* in window" and the operator can investigate offline.
|
|
199
|
-
*
|
|
200
|
-
* Injectable via `runEpicMode({ gatherEpicCommitsFn })` so the unit test
|
|
201
|
-
* can pin behavior without a temp git repo.
|
|
202
|
-
*/
|
|
203
|
-
export function gatherEpicCommitsFromGit({ epicId, windowDays, cwd, logger }) {
|
|
204
|
-
const ref = `epic/${epicId}`;
|
|
205
|
-
const since = `${windowDays} days ago`;
|
|
206
|
-
// Subject (%s) carries the Story-merge `(resolves #N)` and the
|
|
207
|
-
// `baseline-refresh:` prefix; ISO commit date (%cI) drives the window
|
|
208
|
-
// filter inside the reporter. Tab separator avoids subjects that
|
|
209
|
-
// contain pipes.
|
|
210
|
-
const res = gitSpawn(
|
|
211
|
-
cwd,
|
|
212
|
-
'log',
|
|
213
|
-
ref,
|
|
214
|
-
`--since=${since}`,
|
|
215
|
-
'--pretty=format:%H%x09%cI%x09%s',
|
|
216
|
-
);
|
|
217
|
-
if (res.status !== 0) {
|
|
218
|
-
logger.warn?.(
|
|
219
|
-
`[analyze-execution] git log ${ref} failed (non-fatal): ${res.stderr || res.stdout}`,
|
|
220
|
-
);
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
const lines = (res.stdout || '').split('\n').filter(Boolean);
|
|
224
|
-
return lines.map((line) => {
|
|
225
|
-
const [sha, isoDate, ...rest] = line.split('\t');
|
|
226
|
-
return {
|
|
227
|
-
sha,
|
|
228
|
-
isoDate,
|
|
229
|
-
subject: rest.join('\t'),
|
|
230
|
-
epicId,
|
|
231
|
-
};
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Default friction aggregator for the Quality-gate friction block
|
|
237
|
-
* (Story #1400 / Task #1429). Walks each child Story's signals.ndjson
|
|
238
|
-
* stream and returns counts of `baseline-refresh-regression` records
|
|
239
|
-
* plus the top offenders by file (and method, when present in the
|
|
240
|
-
* `regressedFiles` / `crapOverCap` payloads).
|
|
241
|
-
*
|
|
242
|
-
* Aggregation runs against the existing `signals.ndjson` stream — no new
|
|
243
|
-
* file format. Two upstream record shapes are walked:
|
|
244
|
-
* - check-maintainability emits a flat `regressedFiles[]` of `{ file, current, baseline, drop }`.
|
|
245
|
-
* - auto-refresh-runner emits `miOverCap[]` / `crapOverCap[]` rows
|
|
246
|
-
* carrying `path`/`file` and (for crap) `method`.
|
|
247
|
-
*
|
|
248
|
-
* Injectable via `runEpicMode({ aggregateFrictionFn })` so the unit test
|
|
249
|
-
* can pin behavior without seeding NDJSON files.
|
|
250
|
-
*/
|
|
251
|
-
export async function aggregateBaselineFrictionFromSignals({
|
|
252
|
-
epicId,
|
|
253
|
-
storyIds,
|
|
254
|
-
config,
|
|
255
|
-
windowDays,
|
|
256
|
-
now,
|
|
257
|
-
}) {
|
|
258
|
-
const cutoffMs = now().getTime() - windowDays * 24 * 60 * 60 * 1000;
|
|
259
|
-
const offenders = new Map(); // key → { file, method, occurrences }
|
|
260
|
-
const storiesAffected = new Set();
|
|
261
|
-
let totalRecords = 0;
|
|
262
|
-
|
|
263
|
-
for (const sid of storyIds) {
|
|
264
|
-
let touched = false;
|
|
265
|
-
await forEachLine(
|
|
266
|
-
epicId,
|
|
267
|
-
sid,
|
|
268
|
-
(record) => {
|
|
269
|
-
if (
|
|
270
|
-
!record ||
|
|
271
|
-
typeof record !== 'object' ||
|
|
272
|
-
record.kind !== 'friction' ||
|
|
273
|
-
record.category !== 'baseline-refresh-regression'
|
|
274
|
-
) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const ts = Date.parse(record.ts);
|
|
278
|
-
if (Number.isFinite(ts) && ts < cutoffMs) return;
|
|
279
|
-
|
|
280
|
-
totalRecords += 1;
|
|
281
|
-
touched = true;
|
|
282
|
-
|
|
283
|
-
const flatFiles = Array.isArray(record.regressedFiles)
|
|
284
|
-
? record.regressedFiles.map((r) => ({ file: r.file }))
|
|
285
|
-
: [];
|
|
286
|
-
const miFiles = Array.isArray(record.miOverCap)
|
|
287
|
-
? record.miOverCap.map((r) => ({ file: r.path ?? r.file }))
|
|
288
|
-
: [];
|
|
289
|
-
const crapMethods = Array.isArray(record.crapOverCap)
|
|
290
|
-
? record.crapOverCap.map((r) => ({
|
|
291
|
-
file: r.file,
|
|
292
|
-
method: r.method,
|
|
293
|
-
}))
|
|
294
|
-
: [];
|
|
295
|
-
|
|
296
|
-
for (const off of [...flatFiles, ...miFiles, ...crapMethods]) {
|
|
297
|
-
if (!off || typeof off.file !== 'string' || off.file.length === 0) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const key = off.method ? `${off.file}::${off.method}` : off.file;
|
|
301
|
-
const prev = offenders.get(key);
|
|
302
|
-
if (prev) {
|
|
303
|
-
prev.occurrences += 1;
|
|
304
|
-
} else {
|
|
305
|
-
offenders.set(key, {
|
|
306
|
-
file: off.file,
|
|
307
|
-
method: off.method ?? null,
|
|
308
|
-
occurrences: 1,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
config,
|
|
314
|
-
);
|
|
315
|
-
if (touched) storiesAffected.add(sid);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const topOffenders = [...offenders.values()]
|
|
319
|
-
.sort((a, b) => b.occurrences - a.occurrences)
|
|
320
|
-
.slice(0, 10);
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
totalRecords,
|
|
324
|
-
storiesAffected: storiesAffected.size,
|
|
325
|
-
topOffenders,
|
|
326
|
-
windowDays,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* perf-report-render.js — pure renderers + summary extraction for the
|
|
3
|
-
* structured perf-summary comments (Epic #1030 / Story #1123, split out
|
|
4
|
-
* under Story #3350).
|
|
5
|
-
*
|
|
6
|
-
* Every export here is a pure function: it depends only on its inputs and
|
|
7
|
-
* performs no I/O. The Story-/Epic-mode orchestrators in
|
|
8
|
-
* `analyze-execution.js` read NDJSON, phase-timings, and git-log records
|
|
9
|
-
* via the sibling `perf-report-readers.js`, then hand the resulting plain
|
|
10
|
-
* objects to these renderers. Keeping the render surface free of I/O makes
|
|
11
|
-
* the report formatting unit-testable without fixtures.
|
|
12
|
-
*
|
|
13
|
-
* `extractStoryPerfSummaryFromComment` is the one mixed read/parse helper —
|
|
14
|
-
* it parses a comment body that originated from `renderStoryBody`, so it
|
|
15
|
-
* lives next to the renderer it mirrors. It delegates the fenced-JSON
|
|
16
|
-
* extraction to `parseFencedJsonComment` (the shared structured-comment
|
|
17
|
-
* parser) rather than open-coding the fence regex.
|
|
18
|
-
*
|
|
19
|
-
* @see docs/data-dictionary.md §StoryPerfSummary, §EpicPerfReport
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { parseFencedJsonComment } from '../orchestration/structured-comment-parser.js';
|
|
23
|
-
|
|
24
|
-
export const STORY_PERF_TYPE = 'story-perf-summary';
|
|
25
|
-
export const EPIC_PERF_TYPE = 'epic-perf-report';
|
|
26
|
-
// Marker emitted by `upsertStructuredComment` for the story-perf-summary
|
|
27
|
-
// type. Mirrors `structuredCommentMarker(STORY_PERF_TYPE)` so the Epic
|
|
28
|
-
// rollup can detect summary comments without importing the renderer.
|
|
29
|
-
export const STORY_PERF_MARKER = `<!-- ap:structured-comment type="${STORY_PERF_TYPE}" -->`;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Render a small operator-facing summary block that doubles as the
|
|
33
|
-
* comment body. The fenced JSON payload is the canonical machine-
|
|
34
|
-
* readable surface; the prose lines above it give a human a reason to
|
|
35
|
-
* skim. The retro composer reads the fenced JSON, not the prose.
|
|
36
|
-
*/
|
|
37
|
-
export function renderStoryBody(payload) {
|
|
38
|
-
const friction = Object.entries(payload.frictionByCategory ?? {})
|
|
39
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
40
|
-
.join(', ');
|
|
41
|
-
const phaseRows = Object.entries(payload.phaseTimingsMs ?? {})
|
|
42
|
-
.map(([k, v]) => `- \`${k}\`: ${v}ms`)
|
|
43
|
-
.join('\n');
|
|
44
|
-
const lines = [
|
|
45
|
-
`### Story Perf Summary — Story #${payload.storyId} (Epic #${payload.epicId})`,
|
|
46
|
-
'',
|
|
47
|
-
`Closed at: \`${payload.closedAt}\``,
|
|
48
|
-
'',
|
|
49
|
-
friction.length > 0
|
|
50
|
-
? `**Friction:** ${friction}`
|
|
51
|
-
: '**Friction:** none recorded',
|
|
52
|
-
'',
|
|
53
|
-
phaseRows.length > 0
|
|
54
|
-
? `**Phase timings:**\n${phaseRows}`
|
|
55
|
-
: '**Phase timings:** none recorded',
|
|
56
|
-
'',
|
|
57
|
-
`**Rework:** ${payload.reworkScore.filesEditedBeyondThreshold} files beyond threshold`,
|
|
58
|
-
`**Retries:** ${payload.retryDensity.retries} across ${payload.retryDensity.uniqueCommands} unique command(s)`,
|
|
59
|
-
'',
|
|
60
|
-
'```json',
|
|
61
|
-
JSON.stringify(payload, null, 2),
|
|
62
|
-
'```',
|
|
63
|
-
];
|
|
64
|
-
return lines.join('\n');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Render the optional Quality-gate friction summary block (Story #1400 /
|
|
69
|
-
* Task #1429). Aggregates `baseline-refresh-regression` friction records
|
|
70
|
-
* from the Stories' signals.ndjson streams over the trailing window,
|
|
71
|
-
* counts them, and lists the top offenders by file/method.
|
|
72
|
-
*
|
|
73
|
-
* Output contract:
|
|
74
|
-
* - `friction == null` → empty string (caller injected nothing).
|
|
75
|
-
* - `friction.totalRecords === 0` → `**Quality gate friction:** none`
|
|
76
|
-
* - otherwise → count line + bulleted top-offenders list
|
|
77
|
-
*
|
|
78
|
-
* The block reads the existing `signals.ndjson` stream — no new file
|
|
79
|
-
* format. Aggregation is provided by `aggregateBaselineFrictionFromSignals`
|
|
80
|
-
* which the unit test can stub via `runEpicMode({ aggregateFrictionFn })`.
|
|
81
|
-
*/
|
|
82
|
-
export function renderQualityGateFrictionBlock(friction) {
|
|
83
|
-
if (!friction) return '';
|
|
84
|
-
const total = friction.totalRecords ?? 0;
|
|
85
|
-
if (total === 0) {
|
|
86
|
-
return ['', '**Quality gate friction:** none', ''].join('\n');
|
|
87
|
-
}
|
|
88
|
-
const offenders = (friction.topOffenders ?? [])
|
|
89
|
-
.map((o) => {
|
|
90
|
-
const where = o.method ? `${o.file} → ${o.method}` : o.file;
|
|
91
|
-
return `- \`${where}\` — ${o.occurrences} occurrence(s)`;
|
|
92
|
-
})
|
|
93
|
-
.join('\n');
|
|
94
|
-
return [
|
|
95
|
-
'',
|
|
96
|
-
`**Quality gate friction:** ${total} \`baseline-refresh-regression\` record(s) across ${friction.storiesAffected ?? 0} Story/Stories`,
|
|
97
|
-
offenders.length > 0 ? `\nTop offenders:\n${offenders}` : '',
|
|
98
|
-
'',
|
|
99
|
-
].join('\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Render the per-Epic baseline-refresh-rate row (Story #1400 / Task #1427).
|
|
104
|
-
* Returns an empty string when `refresh` is null so the caller can
|
|
105
|
-
* concatenate unconditionally.
|
|
106
|
-
*/
|
|
107
|
-
export function renderBaselineRefreshRateRow(refresh) {
|
|
108
|
-
if (!refresh) return '';
|
|
109
|
-
const row = (refresh.perEpic ?? [])[0];
|
|
110
|
-
if (!row) {
|
|
111
|
-
return [
|
|
112
|
-
'',
|
|
113
|
-
`**Baseline refresh discipline (trailing ${refresh.windowDays}d):** no Story merges in window`,
|
|
114
|
-
'',
|
|
115
|
-
].join('\n');
|
|
116
|
-
}
|
|
117
|
-
const pct = (row.cleanMergeRate * 100).toFixed(1);
|
|
118
|
-
const target = row.cleanMergeRate >= 0.9 ? '✅' : '⚠️';
|
|
119
|
-
return [
|
|
120
|
-
'',
|
|
121
|
-
`**Baseline refresh discipline (trailing ${refresh.windowDays}d):** ${target} ${pct}% clean-merge rate (${row.storyMerges - row.baselineRefreshes}/${row.storyMerges} Stories landed without follow-up refresh; ${row.baselineRefreshes} \`baseline-refresh:\` commit(s))`,
|
|
122
|
-
'',
|
|
123
|
-
].join('\n');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function renderEpicBody(payload, extras = {}) {
|
|
127
|
-
const counts = payload.signalCounts ?? {};
|
|
128
|
-
const countLine = Object.entries(counts)
|
|
129
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
130
|
-
.join(', ');
|
|
131
|
-
const hotspots = (payload.topHotspots ?? [])
|
|
132
|
-
.map(
|
|
133
|
-
(h) =>
|
|
134
|
-
`- \`${h.phase}\` — ${h.occurrences} occurrence(s), avg ratio ${h.avgRatio.toFixed(2)}`,
|
|
135
|
-
)
|
|
136
|
-
.join('\n');
|
|
137
|
-
const friction = (payload.mostFrictionStories ?? [])
|
|
138
|
-
.map((s) => `- Story #${s.storyId}: ${s.frictionCount} friction signal(s)`)
|
|
139
|
-
.join('\n');
|
|
140
|
-
const lines = [
|
|
141
|
-
`### Epic Perf Report — Epic #${payload.epicId}`,
|
|
142
|
-
'',
|
|
143
|
-
`Generated at: \`${payload.generatedAt}\``,
|
|
144
|
-
'',
|
|
145
|
-
`**Signal counts:** ${countLine.length > 0 ? countLine : 'none'}`,
|
|
146
|
-
'',
|
|
147
|
-
hotspots.length > 0
|
|
148
|
-
? `**Top hotspots:**\n${hotspots}`
|
|
149
|
-
: '**Top hotspots:** none recorded',
|
|
150
|
-
'',
|
|
151
|
-
friction.length > 0
|
|
152
|
-
? `**Most-friction Stories:**\n${friction}`
|
|
153
|
-
: '**Most-friction Stories:** none recorded',
|
|
154
|
-
renderBaselineRefreshRateRow(extras.baselineRefreshRate),
|
|
155
|
-
renderQualityGateFrictionBlock(extras.qualityGateFriction),
|
|
156
|
-
'',
|
|
157
|
-
'```json',
|
|
158
|
-
JSON.stringify(payload, null, 2),
|
|
159
|
-
'```',
|
|
160
|
-
];
|
|
161
|
-
return lines.join('\n');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Extract a story-perf-summary payload from a comment body. The body
|
|
166
|
-
* begins with the structured marker line, followed by markdown prose,
|
|
167
|
-
* followed by a fenced ```json``` block carrying the canonical payload.
|
|
168
|
-
* We delegate the fence extraction to `parseFencedJsonComment` so the
|
|
169
|
-
* fenced-JSON regex lives in exactly one place.
|
|
170
|
-
*
|
|
171
|
-
* Returns `null` when the marker is missing, no fence is found, or the
|
|
172
|
-
* fence does not parse — the caller treats absence as "no signal" and
|
|
173
|
-
* keeps walking.
|
|
174
|
-
*/
|
|
175
|
-
export function extractStoryPerfSummaryFromComment(body) {
|
|
176
|
-
if (typeof body !== 'string' || !body.includes(STORY_PERF_MARKER)) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
const parsed = parseFencedJsonComment({ body });
|
|
180
|
-
if (parsed && parsed.kind === 'story-perf-summary') return parsed;
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* audit-lens-routing.js — risk-envelope → audit-lens roster helpers.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `code-review.js` so the review runner stays focused on
|
|
5
|
-
* executing a review pass, while callers that only need risk→lens mapping
|
|
6
|
-
* (ceremony routing, plan-run epilogue consumers, unit tests) import a
|
|
7
|
-
* smaller pure module.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { selectAuditStrategy } from '../dynamic-workflow/capability.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The axes whose presence (at `high` risk) routes a specific post-delivery
|
|
14
|
-
* audit lens. Mirrors the audit-workflow names under
|
|
15
|
-
* `.agents/workflows/audit-*.md`. Story #3939 broadened the routing so every
|
|
16
|
-
* high-risk REQUIRED axis maps to a lens rather than leaving three of them
|
|
17
|
-
* (`data-migration`, `destructive-mutation`, `billing`) routing nothing:
|
|
18
|
-
*
|
|
19
|
-
* - `security` → `audit-security` (auth/secret boundary)
|
|
20
|
-
* - `public-api` → `audit-architecture` (the canonical
|
|
21
|
-
* architectural axis; a breaking public surface)
|
|
22
|
-
* - `data-migration` → `audit-quality` (migration correctness +
|
|
23
|
-
* regression coverage)
|
|
24
|
-
* - `destructive-mutation` → `audit-security` (irreversible mutation is
|
|
25
|
-
* an auth/abuse boundary; co-routes with
|
|
26
|
-
* `security`, de-duplicated)
|
|
27
|
-
* - `billing` → `audit-privacy` (money + the PII/consent
|
|
28
|
-
* surface that travels with it)
|
|
29
|
-
* - `critical-workflow` → `audit-quality` (the load-bearing path
|
|
30
|
-
* warrants the deepest coverage pass)
|
|
31
|
-
*
|
|
32
|
-
* The `visible-behavior` axis intentionally routes NO lens: it forces the
|
|
33
|
-
* acceptance-spec disposition at plan time and no audit lens maps cleanly to
|
|
34
|
-
* it. Any other axis (or a low/medium-risk axis) contributes no lens.
|
|
35
|
-
*
|
|
36
|
-
* Every key here MUST be a value in the `axis` enum of
|
|
37
|
-
* `.agents/schemas/risk-verdict.schema.json` — the verdict-derived envelope
|
|
38
|
-
* can only ever carry schema-valid axes, so a key absent from that enum is
|
|
39
|
-
* unreachable dead routing (Story #3889 removed the unreachable
|
|
40
|
-
* `architecture` key; the architectural axis is `public-api`).
|
|
41
|
-
*/
|
|
42
|
-
const AXIS_TO_LENS = Object.freeze({
|
|
43
|
-
security: 'audit-security',
|
|
44
|
-
'public-api': 'audit-architecture',
|
|
45
|
-
'data-migration': 'audit-quality',
|
|
46
|
-
'destructive-mutation': 'audit-security',
|
|
47
|
-
billing: 'audit-privacy',
|
|
48
|
-
'critical-workflow': 'audit-quality',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Stable output order for routed lenses so the lens list is deterministic
|
|
53
|
-
* regardless of axis ordering in the verdict. Every lens any axis can route
|
|
54
|
-
* appears here exactly once; `resolveAuditLenses` filters this canonical
|
|
55
|
-
* order down to the matched set (Story #3939).
|
|
56
|
-
*/
|
|
57
|
-
const LENS_ORDER = Object.freeze([
|
|
58
|
-
'audit-security',
|
|
59
|
-
'audit-architecture',
|
|
60
|
-
'audit-quality',
|
|
61
|
-
'audit-privacy',
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Resolve the set of post-delivery audit lenses a judged risk envelope routes.
|
|
66
|
-
*
|
|
67
|
-
* High-risk axes map to their audit lens via {@link AXIS_TO_LENS}; only axes
|
|
68
|
-
* judged `high` contribute (a `low`/`medium` axis carries no lens). The result
|
|
69
|
-
* is de-duplicated and stably ordered by {@link LENS_ORDER} so an envelope
|
|
70
|
-
* listing the `public-api` axis more than once routes `['audit-architecture']`
|
|
71
|
-
* once, not twice — and two distinct axes routing the same lens (e.g.
|
|
72
|
-
* `security` + `destructive-mutation` both → `audit-security`) collapse to a
|
|
73
|
-
* single entry. A low-risk envelope — or any envelope with no high-risk routed
|
|
74
|
-
* axis — resolves to an empty array (no lens beyond the existing baseline
|
|
75
|
-
* gates).
|
|
76
|
-
*
|
|
77
|
-
* Pure function — no I/O, no side effects.
|
|
78
|
-
*
|
|
79
|
-
* @param {{ axes?: Array<{ axis?: string, level?: string }> }} [envelope]
|
|
80
|
-
* @returns {string[]} Ordered, de-duplicated audit-lens identifiers.
|
|
81
|
-
*/
|
|
82
|
-
export function resolveAuditLenses(envelope = {}) {
|
|
83
|
-
const axes = Array.isArray(envelope?.axes) ? envelope.axes : [];
|
|
84
|
-
const matched = new Set();
|
|
85
|
-
for (const entry of axes) {
|
|
86
|
-
if (!entry || entry.level !== 'high') continue;
|
|
87
|
-
const lens = AXIS_TO_LENS[entry.axis];
|
|
88
|
-
if (lens) matched.add(lens);
|
|
89
|
-
}
|
|
90
|
-
return LENS_ORDER.filter((lens) => matched.has(lens));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Build the post-delivery audit-lens execution plan for a judged risk
|
|
95
|
-
* envelope. Each routed lens (see {@link resolveAuditLenses}) is paired with a
|
|
96
|
-
* strategy decision from the **existing** `selectAuditStrategy` engine — no new
|
|
97
|
-
* audit machinery is introduced. A low-risk envelope resolves to an empty
|
|
98
|
-
* `lenses` array and runs no audit beyond the baseline gates (Story #3876).
|
|
99
|
-
*
|
|
100
|
-
* Pure with respect to the injected `selectAuditStrategyFn` (default is the
|
|
101
|
-
* shared dynamic-workflow engine, which is itself pure over its snapshot).
|
|
102
|
-
*
|
|
103
|
-
* @param {{ axes?: Array<{ axis?: string, level?: string }> }} [envelope]
|
|
104
|
-
* @param {{
|
|
105
|
-
* snapshot?: object,
|
|
106
|
-
* forceStrategy?: ('orchestrated'|'sequential'|null),
|
|
107
|
-
* selectAuditStrategyFn?: typeof selectAuditStrategy,
|
|
108
|
-
* }} [opts]
|
|
109
|
-
* @returns {{ lenses: string[], plan: Array<{ lens: string, strategy: string, reason: string, forced: boolean }> }}
|
|
110
|
-
*/
|
|
111
|
-
export function planAuditLenses(envelope = {}, opts = {}) {
|
|
112
|
-
const {
|
|
113
|
-
snapshot = {},
|
|
114
|
-
forceStrategy = null,
|
|
115
|
-
selectAuditStrategyFn = selectAuditStrategy,
|
|
116
|
-
} = opts;
|
|
117
|
-
const lenses = resolveAuditLenses(envelope);
|
|
118
|
-
const plan = lenses.map((lens) => {
|
|
119
|
-
const decision = selectAuditStrategyFn({ snapshot, forceStrategy });
|
|
120
|
-
return {
|
|
121
|
-
lens,
|
|
122
|
-
strategy: decision.strategy,
|
|
123
|
-
reason: decision.reason,
|
|
124
|
-
forced: decision.forced,
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
|
-
return { lenses, plan };
|
|
128
|
-
}
|