mandrel 2.0.0 โ 2.1.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 +9 -7
- package/.agents/agents/story-worker.md +41 -46
- package/.agents/audit-checklists/performance.md +1 -1
- package/.agents/docs/SDLC.md +51 -44
- package/.agents/docs/agentrc-reference.json +8 -13
- package/.agents/docs/configuration.md +32 -56
- package/.agents/docs/execution-reference.md +39 -10
- package/.agents/docs/quality-gates.md +14 -16
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +64 -79
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/git-conventions-reference.md +42 -51
- package/.agents/schemas/agentrc.schema.json +34 -45
- 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 +22 -66
- 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/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 +31 -13
- 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/code-review.js +58 -168
- 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 +114 -24
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
- 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 -75
- 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/planning/authoring-context.js +41 -40
- 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 +3 -12
- 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 +15 -32
- 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 +6 -5
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
- 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/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 -6
- 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/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 +79 -4
- 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 +98 -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 +21 -13
- package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
- package/.agents/workflows/helpers/code-review.md +12 -10
- package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
- package/.agents/workflows/helpers/deliver-story.md +193 -118
- package/.agents/workflows/helpers/parallel-tooling.md +2 -2
- package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
- package/.agents/workflows/plan.md +184 -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 +40 -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 +12 -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/package.json +1 -1
- 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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dead-exports-knip.js โ the knip driver behind the dead-export ratchet.
|
|
3
|
+
*
|
|
4
|
+
* Owns everything about talking to knip and normalising what comes back:
|
|
5
|
+
* spawning it, reading a pre-captured report, and flattening its report into
|
|
6
|
+
* `{ file, symbol }` rows. `check-dead-exports.js` stays a thin CLI over this.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/dead-exports-knip
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Spawn `npx knip --reporter json --no-progress` and return the parsed
|
|
17
|
+
* envelope. Never throws โ the caller logs the error and treats current rows as
|
|
18
|
+
* empty, which surfaces every baseline row as "removed": loud, but safe.
|
|
19
|
+
*
|
|
20
|
+
* `production` adds knip's `--production` flag, which restricts analysis to
|
|
21
|
+
* entry/project patterns carrying the `!` suffix in `knip.json`. The test globs
|
|
22
|
+
* deliberately lack that suffix, so production mode drops them as entry points
|
|
23
|
+
* and an export reachable only from a test reads as dead. Without those
|
|
24
|
+
* suffixes production mode has no entry patterns at all and reports nothing โ
|
|
25
|
+
* `knip.json` and this flag are a matched pair.
|
|
26
|
+
*
|
|
27
|
+
* Exported as a hook so tests can stub the spawn without a working knip
|
|
28
|
+
* workspace.
|
|
29
|
+
*
|
|
30
|
+
* @param {{ cwd?: string, spawn?: typeof spawnSync, production?: boolean }} [opts]
|
|
31
|
+
* @returns {{ ok: true, envelope: unknown } | { ok: false, error: string }}
|
|
32
|
+
*/
|
|
33
|
+
export function runKnip({
|
|
34
|
+
cwd = process.cwd(),
|
|
35
|
+
spawn = spawnSync,
|
|
36
|
+
production = false,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const args = ['knip', '--reporter', 'json', '--no-progress'];
|
|
39
|
+
if (production) args.push('--production');
|
|
40
|
+
const result = spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx', args, {
|
|
41
|
+
cwd,
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
shell: process.platform === 'win32',
|
|
45
|
+
});
|
|
46
|
+
if (result.error) {
|
|
47
|
+
return { ok: false, error: `spawn failed: ${result.error.message}` };
|
|
48
|
+
}
|
|
49
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
50
|
+
if (stdout.trim().length === 0) {
|
|
51
|
+
return { ok: false, error: 'knip produced empty stdout' };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return { ok: true, envelope: JSON.parse(stdout) };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `knip JSON parse failed: ${err?.message ?? err}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read a pre-captured knip JSON envelope from disk (the `--knip-output` test
|
|
65
|
+
* seam). Returns the parsed envelope or `null` on failure.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath
|
|
68
|
+
* @returns {unknown}
|
|
69
|
+
*/
|
|
70
|
+
export function readKnipOutput(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Flatten knip's `--reporter json` output into `{ file, symbol }` rows. Knip
|
|
80
|
+
* emits `{ issues: [{ file, exports: [{ name, ... }], ... }, ...] }`. Only
|
|
81
|
+
* `exports` rows are mapped โ the ratchet ignores file-, dependency- and
|
|
82
|
+
* duplicate-level issues, which knip surfaces under separate `rules` keys.
|
|
83
|
+
*
|
|
84
|
+
* @param {unknown} knipEnvelope The parsed knip JSON report.
|
|
85
|
+
* @returns {Array<{ file: string, symbol: string }>}
|
|
86
|
+
*/
|
|
87
|
+
export function extractRowsFromKnip(knipEnvelope) {
|
|
88
|
+
const rows = [];
|
|
89
|
+
if (!knipEnvelope || typeof knipEnvelope !== 'object') return rows;
|
|
90
|
+
const issues = Array.isArray(knipEnvelope.issues) ? knipEnvelope.issues : [];
|
|
91
|
+
for (const issue of issues) {
|
|
92
|
+
const file = issue?.file;
|
|
93
|
+
if (typeof file !== 'string' || file.length === 0) continue;
|
|
94
|
+
const exports_ = Array.isArray(issue.exports) ? issue.exports : [];
|
|
95
|
+
for (const e of exports_) {
|
|
96
|
+
const symbol =
|
|
97
|
+
(e && typeof e.name === 'string' && e.name) ||
|
|
98
|
+
(e && typeof e.symbol === 'string' && e.symbol) ||
|
|
99
|
+
null;
|
|
100
|
+
if (!symbol) continue;
|
|
101
|
+
rows.push({ file, symbol });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dead-exports-mode.js โ the two passes of the dead-export ratchet.
|
|
3
|
+
*
|
|
4
|
+
* The gate runs twice with different reachability assumptions, and each pass
|
|
5
|
+
* needs a matched triple: which knip invocation to make, which baseline to
|
|
6
|
+
* ratchet against, and how to label its output. Resolving that triple in one
|
|
7
|
+
* place keeps the three from drifting apart โ a pass that ran production knip
|
|
8
|
+
* against the default baseline would report every test-only export as newly
|
|
9
|
+
* dead and fail the build for no reason.
|
|
10
|
+
*
|
|
11
|
+
* - **default** โ `tests/**` are knip entry points, so an export imported only
|
|
12
|
+
* by a test reads as used. This is the historical gate.
|
|
13
|
+
* - **production** โ knip's `--production` drops the test entries, so an export
|
|
14
|
+
* no production code reaches reads as dead. Depends on the `!` pattern
|
|
15
|
+
* suffixes in `knip.json`; without them production mode has no entry points
|
|
16
|
+
* and silently reports nothing.
|
|
17
|
+
*
|
|
18
|
+
* The two ratchet against separate baselines on purpose. This repo sanctions
|
|
19
|
+
* exporting purely for a test (`.agents/rules/test-seams.md`), so the
|
|
20
|
+
* production row set is large and mostly intentional; merging it into the
|
|
21
|
+
* default baseline would destroy that baseline's meaning.
|
|
22
|
+
*
|
|
23
|
+
* @module lib/dead-exports-mode
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
|
|
28
|
+
/** Baseline for the default pass (test entry points included). */
|
|
29
|
+
const DEFAULT_BASELINE = path.join('baselines', 'dead-exports.json');
|
|
30
|
+
|
|
31
|
+
/** Baseline for the `--production` pass (test-only-importer discount). */
|
|
32
|
+
const PRODUCTION_BASELINE = path.join(
|
|
33
|
+
'baselines',
|
|
34
|
+
'dead-exports-production.json',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the baseline / label / mode-tag triple for a pass.
|
|
39
|
+
*
|
|
40
|
+
* @param {boolean} production
|
|
41
|
+
* @returns {{ mode: 'production'|'default', label: string, baseline: string }}
|
|
42
|
+
*/
|
|
43
|
+
export function resolveDeadExportsMode(production) {
|
|
44
|
+
return production
|
|
45
|
+
? {
|
|
46
|
+
mode: 'production',
|
|
47
|
+
label: 'dead-exports:production',
|
|
48
|
+
baseline: PRODUCTION_BASELINE,
|
|
49
|
+
}
|
|
50
|
+
: { mode: 'default', label: 'dead-exports', baseline: DEFAULT_BASELINE };
|
|
51
|
+
}
|
|
@@ -10,8 +10,17 @@
|
|
|
10
10
|
* - Prefer `provider.searchIssues` to narrow open Stories server-side
|
|
11
11
|
* (`label:"type::story" state:open` + top seed tokens), capped at
|
|
12
12
|
* ~100 hits, then rank that set. Fall back to `listIssuesByLabel`
|
|
13
|
-
* when search errors
|
|
14
|
-
* `TicketGateway.getTickets`).
|
|
13
|
+
* when search errors, is unavailable, or returns **nothing** (same
|
|
14
|
+
* try/catch pattern as `TicketGateway.getTickets`).
|
|
15
|
+
* - The narrowing query is deliberately weak (Story #4541). GitHub ANDs
|
|
16
|
+
* free-text terms, so the former 8-token query demanded that a candidate
|
|
17
|
+
* contain all eight of a multi-sentence seed's longest words โ which no
|
|
18
|
+
* real Story does. Gate #1 duplicate triage therefore reported "no
|
|
19
|
+
* candidates" on every realistic seed while looking perfectly healthy.
|
|
20
|
+
* Search is now a cheap best-case narrowing over a handful of tokens; an
|
|
21
|
+
* empty result is treated as "search did not help", not "no duplicates",
|
|
22
|
+
* and falls through to the label listing whose Jaccard ranker has real
|
|
23
|
+
* recall.
|
|
15
24
|
* - Scoring is intentionally simple (token Jaccard over title + body).
|
|
16
25
|
* It is a triage signal, not a semantic-search replacement.
|
|
17
26
|
* - Provider errors on the list fallback propagate verbatim โ the
|
|
@@ -79,8 +88,17 @@ const STOPWORDS = new Set([
|
|
|
79
88
|
|
|
80
89
|
const DEFAULT_MIN_SCORE = 0.15;
|
|
81
90
|
const DEFAULT_MAX_RESULTS = 5;
|
|
82
|
-
/**
|
|
83
|
-
|
|
91
|
+
/**
|
|
92
|
+
* How many seed tokens to pass as free-text search terms.
|
|
93
|
+
*
|
|
94
|
+
* GitHub ANDs these, so the cap is a precision/recall dial pointing the
|
|
95
|
+
* wrong way: every extra token shrinks the candidate set. Three is a
|
|
96
|
+
* best-case narrowing that can plausibly hit on a focused seed; anything
|
|
97
|
+
* beyond that reliably matched nothing on a multi-sentence one (Story
|
|
98
|
+
* #4541). Recall does not rest on this number โ an empty result falls back
|
|
99
|
+
* to label-listing plus client-side ranking.
|
|
100
|
+
*/
|
|
101
|
+
const DEFAULT_SEARCH_TOKEN_CAP = 3;
|
|
84
102
|
/** Hard cap on search hits ranked client-side (~one Search API page). */
|
|
85
103
|
const SEARCH_RESULT_CAP = 100;
|
|
86
104
|
|
|
@@ -269,8 +287,16 @@ async function fetchOpenStoriesViaList(provider) {
|
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
/**
|
|
272
|
-
* Fetch open-Story candidates:
|
|
273
|
-
* `listIssuesByLabel` when search errors or
|
|
290
|
+
* Fetch open-Story candidates: try Search API narrowing first, fall back to
|
|
291
|
+
* `listIssuesByLabel` when search errors, is unavailable, or **returns no
|
|
292
|
+
* hits**.
|
|
293
|
+
*
|
|
294
|
+
* The empty-result fallback is the load-bearing one (Story #4541). The
|
|
295
|
+
* narrowing query ANDs seed tokens, so on any real multi-sentence seed it
|
|
296
|
+
* matches nothing โ and treating that as an authoritative "no duplicates
|
|
297
|
+
* exist" is what made Gate #1 triage report a clean bill of health while
|
|
298
|
+
* never actually looking. An empty search is evidence about the *query*, not
|
|
299
|
+
* about the backlog.
|
|
274
300
|
*
|
|
275
301
|
* @param {object} provider
|
|
276
302
|
* @param {string} seed
|
|
@@ -291,7 +317,12 @@ async function fetchOpenStoryCandidates(provider, seed) {
|
|
|
291
317
|
|
|
292
318
|
if (hasSearch) {
|
|
293
319
|
try {
|
|
294
|
-
|
|
320
|
+
const hits = await fetchOpenStoriesViaSearch(provider, seed);
|
|
321
|
+
if (hits.length > 0 || !hasList) return hits;
|
|
322
|
+
Logger.info(
|
|
323
|
+
'[duplicate-search] narrowed search matched no open Stories; ' +
|
|
324
|
+
'falling back to listIssuesByLabel + client-side ranking',
|
|
325
|
+
);
|
|
295
326
|
} catch (err) {
|
|
296
327
|
if (!hasList) throw err;
|
|
297
328
|
const msg = typeof err?.message === 'string' ? err.message : String(err);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* session's ledger items (see `.agents/schemas/qa-ledger.schema.json` and
|
|
7
7
|
* `lib/qa/qa-session.js`), the still-untriaged backlog is clustered and each
|
|
8
8
|
* cluster is promoted to a follow-up ticket โ a single Story (via `/plan`)
|
|
9
|
-
* for a tight, one-deliverable cluster, or
|
|
9
|
+
* for a tight, one-deliverable cluster, or a multi-Story plan-seed (via `/plan --seed`) for
|
|
10
10
|
* a broad cluster that spans multiple coverage surfaces. Each contributing
|
|
11
11
|
* ledger item then has the resulting `routedTo` issue link written back onto it
|
|
12
12
|
* so a resume run sees the item as filed rather than re-promoting it.
|
|
@@ -32,15 +32,24 @@ import { highestSeverity as highestSeverityOf } from './severity.js';
|
|
|
32
32
|
/** Triaged dispositions, mirrored from the `disposition` enum in the schema. */
|
|
33
33
|
const TRIAGED_DISPOSITIONS = Object.freeze(['file', 'defer', 'dismiss']);
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The two promotion targets a cluster routes to.
|
|
37
|
+
*
|
|
38
|
+
* `PLAN_SEED` promotes a broad cluster by seeding a multi-Story `/plan`
|
|
39
|
+
* run (`/plan --seed`). Its persisted wire token is the legacy string
|
|
40
|
+
* `'epic'`: archived qa-ledger records (and the distributed
|
|
41
|
+
* `qa-ledger.schema.json` `routedTo.kind` enum) still carry `'epic'`, so
|
|
42
|
+
* the value is kept read-compatible while the write-side constant name is
|
|
43
|
+
* modernised.
|
|
44
|
+
*/
|
|
36
45
|
export const PROMOTION_TARGETS = Object.freeze({
|
|
37
46
|
STORY: 'story',
|
|
38
|
-
|
|
47
|
+
PLAN_SEED: 'epic',
|
|
39
48
|
});
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
51
|
* A cluster of more than this many distinct coverage surfaces is broad enough
|
|
43
|
-
* to warrant
|
|
52
|
+
* to warrant a plan-seed (`/plan --seed`) rather than a single Story
|
|
44
53
|
* (`/plan`). One or two surfaces is a tight, single-deliverable cluster.
|
|
45
54
|
*/
|
|
46
55
|
const EPIC_COVERAGE_THRESHOLD = 2;
|
|
@@ -70,7 +79,7 @@ export function isPromotable(item) {
|
|
|
70
79
|
* Stable cluster key for a ledger item: its `class`. Items sharing a class
|
|
71
80
|
* describe the same kind of signal (a product bug, a tooling-DX gap, โฆ) and
|
|
72
81
|
* merge into one follow-up ticket. A class whose items span many distinct
|
|
73
|
-
* coverage surfaces is broad enough to promote to
|
|
82
|
+
* coverage surfaces is broad enough to promote to a plan-seed (see
|
|
74
83
|
* {@link targetForCluster}); a class confined to one or two surfaces is a
|
|
75
84
|
* single-deliverable Story. Coverage is therefore a *secondary* signal that
|
|
76
85
|
* sizes the cluster rather than splitting it.
|
|
@@ -159,7 +168,7 @@ export function clusterLedgerItems(items) {
|
|
|
159
168
|
/**
|
|
160
169
|
* Decide a cluster's promotion target. A cluster that spans more than
|
|
161
170
|
* {@link EPIC_COVERAGE_THRESHOLD} distinct coverage surfaces is broad enough to
|
|
162
|
-
* warrant
|
|
171
|
+
* warrant a plan-seed (`/plan --seed`); otherwise it is a single-deliverable
|
|
163
172
|
* Story (`/plan`).
|
|
164
173
|
*
|
|
165
174
|
* @param {{ coverages: string[] }} cluster
|
|
@@ -170,7 +179,7 @@ export function targetForCluster(cluster) {
|
|
|
170
179
|
? cluster.coverages.length
|
|
171
180
|
: 0;
|
|
172
181
|
return surfaces > EPIC_COVERAGE_THRESHOLD
|
|
173
|
-
? PROMOTION_TARGETS.
|
|
182
|
+
? PROMOTION_TARGETS.PLAN_SEED
|
|
174
183
|
: PROMOTION_TARGETS.STORY;
|
|
175
184
|
}
|
|
176
185
|
|
|
@@ -224,7 +233,7 @@ function routedToLink(issue, kind) {
|
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
/**
|
|
227
|
-
* Promote the clustered untriaged ledger items into Stories /
|
|
236
|
+
* Promote the clustered untriaged ledger items into Stories / plan-seeds via the
|
|
228
237
|
* shared findings/route logic, then write the resulting `routedTo` issue link
|
|
229
238
|
* back onto each contributing ledger item.
|
|
230
239
|
*
|
|
@@ -233,7 +242,7 @@ function routedToLink(issue, kind) {
|
|
|
233
242
|
* shared `routeFinding` against existing Issues (via the injected search
|
|
234
243
|
* port). This dedups against work already filed.
|
|
235
244
|
* 2. On a `new` decision, open the follow-up ticket through the injected
|
|
236
|
-
* `createStory` (`/plan`) or `
|
|
245
|
+
* `createStory` (`/plan`) or `createPlanSeed` (`/plan --seed`) port,
|
|
237
246
|
* chosen by {@link targetForCluster}. On any other decision, link back to
|
|
238
247
|
* the matched Issue rather than creating a duplicate.
|
|
239
248
|
* 3. Stamp the resolved `routedTo` link onto every contributing ledger item
|
|
@@ -251,8 +260,8 @@ function routedToLink(issue, kind) {
|
|
|
251
260
|
* Optional semantic candidate search, forwarded to `routeFinding`.
|
|
252
261
|
* @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.createStory
|
|
253
262
|
* Opens a single Story (`/plan`) for a tight cluster.
|
|
254
|
-
* @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.
|
|
255
|
-
* Opens
|
|
263
|
+
* @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.createPlanSeed
|
|
264
|
+
* Opens a plan-seed (`/plan --seed`) run for a broad cluster.
|
|
256
265
|
* @returns {Promise<{
|
|
257
266
|
* promotions: Array<{
|
|
258
267
|
* clusterKey: string,
|
|
@@ -268,7 +277,7 @@ function routedToLink(issue, kind) {
|
|
|
268
277
|
* @throws {Error} when a required create port is missing for a routed cluster.
|
|
269
278
|
*/
|
|
270
279
|
export async function promoteFindings(ledgerItems, ports = {}) {
|
|
271
|
-
const { searchIssues, searchCandidates, createStory,
|
|
280
|
+
const { searchIssues, searchCandidates, createStory, createPlanSeed } = ports;
|
|
272
281
|
if (
|
|
273
282
|
typeof searchCandidates !== 'function' &&
|
|
274
283
|
typeof searchIssues !== 'function'
|
|
@@ -294,10 +303,10 @@ export async function promoteFindings(ledgerItems, ports = {}) {
|
|
|
294
303
|
|
|
295
304
|
if (route.decision === 'new') {
|
|
296
305
|
const createPort =
|
|
297
|
-
target === PROMOTION_TARGETS.
|
|
306
|
+
target === PROMOTION_TARGETS.PLAN_SEED ? createPlanSeed : createStory;
|
|
298
307
|
if (typeof createPort !== 'function') {
|
|
299
308
|
throw new Error(
|
|
300
|
-
`promoteFindings: a ${target === PROMOTION_TARGETS.
|
|
309
|
+
`promoteFindings: a ${target === PROMOTION_TARGETS.PLAN_SEED ? 'createPlanSeed' : 'createStory'} port is required to promote cluster ${cluster.key}`,
|
|
301
310
|
);
|
|
302
311
|
}
|
|
303
312
|
issue = await createPort(cluster);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* format-generated-json.js โ run generated JSON through the project
|
|
3
|
+
* formatter (Biome) so a generator's output matches what the repo's
|
|
4
|
+
* commit-time formatting would have produced anyway.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists. Generators serialize with `JSON.stringify`, which
|
|
7
|
+
* expands every array across multiple lines. Biome collapses short ones
|
|
8
|
+
* that fit inside `lineWidth` (`"allowedTools": ["Read", "Bash"]`), and
|
|
9
|
+
* lint-staged runs `biome format --write` over staged JSON at commit
|
|
10
|
+
* time โ so the committed artifact is Biome-shaped while a fresh
|
|
11
|
+
* generator run is not. The gap means regenerating on a clean tree
|
|
12
|
+
* always leaves format drift, and a `<generate>` โ `lint` sequence fails
|
|
13
|
+
* on `biome ci` even when the generator's own `--check` reports the
|
|
14
|
+
* artifact semantically fresh (Story #4546).
|
|
15
|
+
*
|
|
16
|
+
* Running the real formatter, rather than hand-matching its array
|
|
17
|
+
* collapsing, keeps this correct by construction across future formatter
|
|
18
|
+
* and `lineWidth` changes.
|
|
19
|
+
*
|
|
20
|
+
* Stdin mode is deliberate: Biome's configured `formatWrite` command is
|
|
21
|
+
* whole-tree (`biome format --write .`), which is a far broader side
|
|
22
|
+
* effect than a generator should have. `--stdin-file-path` makes this a
|
|
23
|
+
* pure content transform with no filesystem writes.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import process from 'node:process';
|
|
28
|
+
import { Logger } from './Logger.js';
|
|
29
|
+
|
|
30
|
+
/** Wall-clock ceiling for the formatter spawn, so a hung child cannot wedge a generator run. */
|
|
31
|
+
const FORMATTER_TIMEOUT_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warn once about a fallback and return null for the caller to act on.
|
|
35
|
+
* Owning the message here keeps every caller's write path a single
|
|
36
|
+
* `formatGeneratedJson(...) ?? serialized` expression.
|
|
37
|
+
*/
|
|
38
|
+
function fallback(filename) {
|
|
39
|
+
Logger.warn(
|
|
40
|
+
`project formatter (biome) unavailable โ writing unformatted ${filename}; ` +
|
|
41
|
+
'run your formatter over it if a format gate rejects it',
|
|
42
|
+
);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format `source` as JSON using the project formatter.
|
|
48
|
+
*
|
|
49
|
+
* Best-effort by design: `.agents/` is materialized into consumer
|
|
50
|
+
* projects that need not have Biome installed, so an unavailable or
|
|
51
|
+
* failing formatter warns and returns `null` for the caller to fall back
|
|
52
|
+
* on its own serialization rather than failing the generator. That is
|
|
53
|
+
* safe wherever the artifact's freshness check compares parsed objects
|
|
54
|
+
* rather than bytes โ formatting carries no semantic content.
|
|
55
|
+
*
|
|
56
|
+
* `--no` keeps npx from reaching the network to install a missing Biome.
|
|
57
|
+
* `filename` is passed as a bare basename: Biome only needs it to infer
|
|
58
|
+
* the language and match config overrides, and a basename cannot carry
|
|
59
|
+
* the spaces that would break arg quoting under the Windows `shell:
|
|
60
|
+
* true` spawn.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} source Text to format.
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {string} opts.cwd Directory to resolve the formatter and its config from.
|
|
65
|
+
* @param {string} [opts.filename] Basename Biome attributes the stdin text to.
|
|
66
|
+
* @param {typeof spawnSync} [opts.spawn] Injection seam for tests.
|
|
67
|
+
* @returns {string|null} Formatted text with a trailing newline, or null to fall back.
|
|
68
|
+
*/
|
|
69
|
+
export function formatGeneratedJson(
|
|
70
|
+
source,
|
|
71
|
+
{ cwd, filename = 'generated.json', spawn = spawnSync },
|
|
72
|
+
) {
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = spawn(
|
|
76
|
+
'npx',
|
|
77
|
+
['--no', 'biome', 'format', `--stdin-file-path=${filename}`],
|
|
78
|
+
{
|
|
79
|
+
cwd,
|
|
80
|
+
input: source,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
// npm/npx ship as `.cmd` shims on Windows, which Node refuses to
|
|
83
|
+
// spawn without a shell since CVE-2024-27980.
|
|
84
|
+
shell: process.platform === 'win32',
|
|
85
|
+
timeout: FORMATTER_TIMEOUT_MS,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
return fallback(filename);
|
|
90
|
+
}
|
|
91
|
+
if (!result || result.error || result.status !== 0) return fallback(filename);
|
|
92
|
+
const stdout = result.stdout;
|
|
93
|
+
if (typeof stdout !== 'string' || stdout.trim() === '') {
|
|
94
|
+
return fallback(filename);
|
|
95
|
+
}
|
|
96
|
+
return stdout.endsWith('\n') ? stdout : `${stdout}\n`;
|
|
97
|
+
}
|
|
@@ -1,107 +1,35 @@
|
|
|
1
1
|
// .agents/scripts/lib/framework-version.js
|
|
2
2
|
/**
|
|
3
|
-
* framework-version.js โ
|
|
4
|
-
*
|
|
3
|
+
* framework-version.js โ the visible authoring-marker surface for the legacy
|
|
4
|
+
* ticket-body provenance stamp.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Tickets authored under v1 carry a hybrid stamp: a hidden
|
|
7
|
+
* `<!-- meta: {"mandrel_version":"โฆ","authored_at":"โฆ"} -->` block plus a
|
|
8
|
+
* single visible footer line
|
|
9
|
+
* `> ๐ท๏ธ Authored with Mandrel v<version> ยท <YYYY-MM-DD>`. The producer of
|
|
10
|
+
* new stamps was retired with the Epic write surface (#4574) โ nothing stamps
|
|
11
|
+
* a new ticket โ but bodies already stamped are live in the backlog, so the
|
|
12
|
+
* Story-body serializer/parser must keep round-tripping them:
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* producers delegate here (DRY โ one manifest reader).
|
|
14
|
+
* - {@link AUTHORED_MARKER_LINE_RE} lets the parser skip the marker line during
|
|
15
|
+
* section parsing so it never pollutes the last structured section.
|
|
16
|
+
* - {@link authoredMarkerLine} lets the serializer re-emit a byte-identical
|
|
17
|
+
* marker line for a stamp it parsed, preserving provenance verbatim.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* surface:
|
|
17
|
-
* - a hidden machine-readable field in the trailing
|
|
18
|
-
* `<!-- meta: {"mandrel_version":"โฆ","authored_at":"โฆ"} -->` block
|
|
19
|
-
* (the source of truth, queryable by tooling), and
|
|
20
|
-
* - a single visible footer line
|
|
21
|
-
* `> ๐ท๏ธ Authored with Mandrel v<version> ยท <YYYY-MM-DD>` so a human
|
|
22
|
-
* reading the raw GitHub issue sees the provenance without any tooling.
|
|
23
|
-
*
|
|
24
|
-
* The stamp is **immutable**: {@link stampFrameworkVersion} is a no-op when
|
|
25
|
-
* the body already carries a `mandrel_version`, so a later re-render or
|
|
26
|
-
* Epic-body edit preserves the originally-authored version verbatim rather
|
|
27
|
-
* than bumping it to whatever version happens to be running.
|
|
28
|
-
*
|
|
29
|
-
* This module imports only Node builtins so it can be pulled in from the
|
|
30
|
-
* story-body serializer, the ticket provider, and the Epic ideation renderer
|
|
31
|
-
* without risking an import cycle.
|
|
19
|
+
* This module imports nothing so it can be pulled in from the story-body
|
|
20
|
+
* serializer without risking an import cycle.
|
|
32
21
|
*/
|
|
33
22
|
|
|
34
|
-
import fs from 'node:fs';
|
|
35
|
-
import path from 'node:path';
|
|
36
|
-
import { fileURLToPath } from 'node:url';
|
|
37
|
-
|
|
38
|
-
/** Returned when the package manifest is absent or unreadable. */
|
|
39
|
-
export const FALLBACK_VERSION = 'unknown';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Trailing machine-metadata comment block: `<!-- meta: {...} -->`. Mirrors the
|
|
43
|
-
* regex the Story-body parser uses so both surfaces recognise the same block.
|
|
44
|
-
*/
|
|
45
|
-
const META_BLOCK_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
|
|
46
|
-
|
|
47
23
|
/**
|
|
48
24
|
* The visible authoring marker line. A blockquote so GitHub renders it as a
|
|
49
|
-
* callout. Used
|
|
50
|
-
*
|
|
51
|
-
* never pollutes the last structured section.
|
|
25
|
+
* callout. Used in the Story-body parser to skip the line during section
|
|
26
|
+
* parsing so it never pollutes the last structured section.
|
|
52
27
|
*/
|
|
53
28
|
export const AUTHORED_MARKER_LINE_RE = /^\s*>\s*๐ท๏ธ\s+Authored with Mandrel\b/;
|
|
54
29
|
|
|
55
30
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* so the manifest sits three directories up โ the same layout in the dev repo
|
|
59
|
-
* and the published tarball.
|
|
60
|
-
*
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
function defaultPkgPath() {
|
|
64
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
65
|
-
return path.resolve(moduleDir, '../../..', 'package.json');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Resolve the running framework version from the root `package.json`. Degrades
|
|
70
|
-
* to {@link FALLBACK_VERSION} (never throws) on any read or parse failure so a
|
|
71
|
-
* missing/unreadable manifest can never crash an authoring or hydration path.
|
|
72
|
-
*
|
|
73
|
-
* @param {{ pkgPath?: string }} [opts] - `pkgPath` override (test seam).
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
export function resolveFrameworkVersion({ pkgPath } = {}) {
|
|
77
|
-
try {
|
|
78
|
-
const resolved = typeof pkgPath === 'string' ? pkgPath : defaultPkgPath();
|
|
79
|
-
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
80
|
-
return typeof parsed.version === 'string' && parsed.version.trim()
|
|
81
|
-
? parsed.version.trim()
|
|
82
|
-
: FALLBACK_VERSION;
|
|
83
|
-
} catch {
|
|
84
|
-
return FALLBACK_VERSION;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Format an authoring date as `YYYY-MM-DD` (UTC). Matches the date shape the
|
|
90
|
-
* rest of the authoring path uses (e.g. `qa-session`).
|
|
91
|
-
*
|
|
92
|
-
* @param {Date} [date=new Date()]
|
|
93
|
-
* @returns {string}
|
|
94
|
-
*/
|
|
95
|
-
export function formatAuthoredDate(date = new Date()) {
|
|
96
|
-
const d =
|
|
97
|
-
date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
|
|
98
|
-
return d.toISOString().slice(0, 10);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Build the visible authoring marker line for a given stamp. Centralised so
|
|
103
|
-
* the string is byte-identical across the two producers (the Story-body
|
|
104
|
-
* serializer and {@link stampFrameworkVersion}).
|
|
31
|
+
* Build the visible authoring marker line for a given stamp. The Story-body
|
|
32
|
+
* serializer uses this to re-emit a legacy stamp it parsed, byte-identically.
|
|
105
33
|
*
|
|
106
34
|
* @param {{ version: string, authoredAt: string }} stamp
|
|
107
35
|
* @returns {string}
|
|
@@ -109,101 +37,3 @@ export function formatAuthoredDate(date = new Date()) {
|
|
|
109
37
|
export function authoredMarkerLine({ version, authoredAt }) {
|
|
110
38
|
return `> ๐ท๏ธ Authored with Mandrel v${version} ยท ${authoredAt}`;
|
|
111
39
|
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Read the framework stamp from a body's trailing meta block. Returns
|
|
115
|
-
* `{ version, authoredAt }` when a non-empty `mandrel_version` is present, or
|
|
116
|
-
* `null` when the body carries no stamp (or the meta block is malformed).
|
|
117
|
-
* `authoredAt` is `null` when the version is present but the date is absent.
|
|
118
|
-
*
|
|
119
|
-
* @param {string} markdown
|
|
120
|
-
* @returns {{ version: string, authoredAt: string|null }|null}
|
|
121
|
-
*/
|
|
122
|
-
export function extractFrameworkStamp(markdown) {
|
|
123
|
-
if (typeof markdown !== 'string') return null;
|
|
124
|
-
const match = markdown.match(META_BLOCK_RE);
|
|
125
|
-
if (!match) return null;
|
|
126
|
-
let parsed;
|
|
127
|
-
try {
|
|
128
|
-
parsed = JSON.parse(match[1]);
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
const version =
|
|
136
|
-
typeof parsed.mandrel_version === 'string' && parsed.mandrel_version.trim()
|
|
137
|
-
? parsed.mandrel_version.trim()
|
|
138
|
-
: null;
|
|
139
|
-
if (version === null) return null;
|
|
140
|
-
const authoredAt =
|
|
141
|
-
typeof parsed.authored_at === 'string' && parsed.authored_at.trim()
|
|
142
|
-
? parsed.authored_at.trim()
|
|
143
|
-
: null;
|
|
144
|
-
return { version, authoredAt };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Stamp a ticket body (Epic or Story markdown) with the framework version and
|
|
149
|
-
* authoring date โ **once**. When the body already carries a `mandrel_version`
|
|
150
|
-
* the body is returned verbatim (immutability: never re-derive or bump an
|
|
151
|
-
* already-authored stamp). Otherwise the version keys are merged into (or
|
|
152
|
-
* create) the trailing `<!-- meta -->` block โ appended **last** so the key
|
|
153
|
-
* order stays stable with the Story-body serializer โ and the visible marker
|
|
154
|
-
* line is (re)emitted just above it.
|
|
155
|
-
*
|
|
156
|
-
* The `version` / `authoredAt` overrides let a caller (e.g. the Epic edit path)
|
|
157
|
-
* preserve a previously-authored stamp; both default to the running version and
|
|
158
|
-
* today's date when omitted.
|
|
159
|
-
*
|
|
160
|
-
* @param {string} markdown
|
|
161
|
-
* @param {{ version?: string, authoredAt?: string }} [stamp]
|
|
162
|
-
* @returns {string}
|
|
163
|
-
*/
|
|
164
|
-
export function stampFrameworkVersion(markdown, stamp = {}) {
|
|
165
|
-
const body = typeof markdown === 'string' ? markdown : '';
|
|
166
|
-
|
|
167
|
-
// Immutability: a body that already carries a version is preserved verbatim.
|
|
168
|
-
if (extractFrameworkStamp(body) !== null) return body;
|
|
169
|
-
|
|
170
|
-
const version =
|
|
171
|
-
typeof stamp?.version === 'string' && stamp.version.trim()
|
|
172
|
-
? stamp.version.trim()
|
|
173
|
-
: resolveFrameworkVersion();
|
|
174
|
-
const authoredAt =
|
|
175
|
-
typeof stamp?.authoredAt === 'string' && stamp.authoredAt.trim()
|
|
176
|
-
? stamp.authoredAt.trim()
|
|
177
|
-
: formatAuthoredDate();
|
|
178
|
-
|
|
179
|
-
// Merge into any existing (version-less) meta block, appending the version
|
|
180
|
-
// keys last for stable key order.
|
|
181
|
-
const metaMatch = body.match(META_BLOCK_RE);
|
|
182
|
-
const meta = {};
|
|
183
|
-
if (metaMatch) {
|
|
184
|
-
try {
|
|
185
|
-
const parsed = JSON.parse(metaMatch[1]);
|
|
186
|
-
if (
|
|
187
|
-
parsed !== null &&
|
|
188
|
-
typeof parsed === 'object' &&
|
|
189
|
-
!Array.isArray(parsed)
|
|
190
|
-
) {
|
|
191
|
-
Object.assign(meta, parsed);
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
// Malformed meta comment โ drop it and re-emit a clean block.
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
meta.mandrel_version = version;
|
|
198
|
-
meta.authored_at = authoredAt;
|
|
199
|
-
|
|
200
|
-
// Strip any existing meta block / marker so both re-append canonically.
|
|
201
|
-
const head = body
|
|
202
|
-
.replace(META_BLOCK_RE, '')
|
|
203
|
-
.replace(new RegExp(AUTHORED_MARKER_LINE_RE.source, 'm'), '')
|
|
204
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
205
|
-
.trimEnd();
|
|
206
|
-
|
|
207
|
-
const marker = authoredMarkerLine({ version, authoredAt });
|
|
208
|
-
return `${head}\n\n${marker}\n\n<!-- meta: ${JSON.stringify(meta)} -->`;
|
|
209
|
-
}
|