mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -464,6 +464,156 @@ function renderDiscardedItem(item) {
|
|
|
464
464
|
return parts.join(' — ');
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Normalize the two loosely-typed inputs into the four buckets every section
|
|
469
|
+
* renderer reads. Absorbs the optional-chain / nullish-coalesce cluster that
|
|
470
|
+
* otherwise all lands on `buildFollowUpsCommentBody` (Story #4926).
|
|
471
|
+
*
|
|
472
|
+
* @param {object|null|undefined} proposals
|
|
473
|
+
* @param {object|null|undefined} graduated
|
|
474
|
+
* @returns {{ filed: object[], framework: object[], consumer: object[],
|
|
475
|
+
* discarded: object[], filingErrors: string[] }}
|
|
476
|
+
*/
|
|
477
|
+
function normalizeRollupBuckets(proposals, graduated) {
|
|
478
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
479
|
+
return {
|
|
480
|
+
filed: asArray(graduated?.filed),
|
|
481
|
+
framework: asArray(proposals?.framework),
|
|
482
|
+
consumer: asArray(proposals?.consumer),
|
|
483
|
+
discarded: asArray(proposals?.discarded),
|
|
484
|
+
filingErrors: asArray(graduated?.errors),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Render the "**Filed**" section, or nothing when no proposal was filed.
|
|
490
|
+
*
|
|
491
|
+
* @param {object[]} filed
|
|
492
|
+
* @returns {string[]}
|
|
493
|
+
*/
|
|
494
|
+
function renderFiledSection(filed) {
|
|
495
|
+
if (filed.length === 0) return [];
|
|
496
|
+
return [
|
|
497
|
+
'**Filed**',
|
|
498
|
+
...filed.map(
|
|
499
|
+
(item) =>
|
|
500
|
+
`- ${item.source}: ${item.title}${item.url ? ` — ${item.url}` : ''}`,
|
|
501
|
+
),
|
|
502
|
+
'',
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Render the "**Actionable (not auto-filed)**" fallback — the command stanzas
|
|
508
|
+
* an operator runs by hand when auto-filing produced nothing.
|
|
509
|
+
*
|
|
510
|
+
* @param {object[]} actionable framework + consumer, in that order
|
|
511
|
+
* @param {object[]} filed
|
|
512
|
+
* @returns {string[]}
|
|
513
|
+
*/
|
|
514
|
+
function renderActionableSection(actionable, filed) {
|
|
515
|
+
if (actionable.length === 0 || filed.length > 0) return [];
|
|
516
|
+
const lines = ['**Actionable (not auto-filed)**'];
|
|
517
|
+
for (const item of actionable) {
|
|
518
|
+
lines.push(`- ${item.source}: ${item.title}`, '', '```bash', item.command);
|
|
519
|
+
lines.push('```');
|
|
520
|
+
}
|
|
521
|
+
lines.push('');
|
|
522
|
+
return lines;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Render the "**Below threshold (not filed)**" section.
|
|
527
|
+
*
|
|
528
|
+
* @param {object[]} discarded
|
|
529
|
+
* @returns {string[]}
|
|
530
|
+
*/
|
|
531
|
+
function renderDiscardedSection(discarded) {
|
|
532
|
+
if (discarded.length === 0) return [];
|
|
533
|
+
return [
|
|
534
|
+
'**Below threshold (not filed)**',
|
|
535
|
+
...discarded.map(
|
|
536
|
+
(item) => `- ${item.source}: ${renderDiscardedItem(item)}`,
|
|
537
|
+
),
|
|
538
|
+
'',
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Render the all-empty branch. Story #4828 — "no proposals" has two readings,
|
|
544
|
+
* and only one of them is a quiet run. Signals gathered but nothing routed is
|
|
545
|
+
* the third instance of the silence Stories #4578 and #4824 each fixed once.
|
|
546
|
+
*
|
|
547
|
+
* @param {{ empty: boolean, outcome: {zeroProposals: boolean},
|
|
548
|
+
* signalCount: number, categories: object[], storyCount: number }} args
|
|
549
|
+
* @returns {string[]}
|
|
550
|
+
*/
|
|
551
|
+
function renderEmptySection({
|
|
552
|
+
empty,
|
|
553
|
+
outcome,
|
|
554
|
+
signalCount,
|
|
555
|
+
categories,
|
|
556
|
+
storyCount,
|
|
557
|
+
}) {
|
|
558
|
+
if (!empty) return [];
|
|
559
|
+
return [
|
|
560
|
+
...(outcome.zeroProposals
|
|
561
|
+
? renderZeroProposalLines(signalCount, categories)
|
|
562
|
+
: renderEmptyRollupLines(storyCount)),
|
|
563
|
+
'',
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Build the machine-readable twin of the rendered prose. Every suspect flag
|
|
569
|
+
* above has a field here so a caller never has to regex the body.
|
|
570
|
+
*
|
|
571
|
+
* @param {object} args
|
|
572
|
+
* @returns {object}
|
|
573
|
+
*/
|
|
574
|
+
function buildRollupPayload({
|
|
575
|
+
storyId,
|
|
576
|
+
storyCount,
|
|
577
|
+
signalCount,
|
|
578
|
+
categories,
|
|
579
|
+
buckets,
|
|
580
|
+
empty,
|
|
581
|
+
outcome,
|
|
582
|
+
}) {
|
|
583
|
+
const { filed, framework, consumer, discarded, filingErrors } = buckets;
|
|
584
|
+
return {
|
|
585
|
+
storyId,
|
|
586
|
+
storyCount,
|
|
587
|
+
// Story #4828 — the corpus the roll-up actually read. Without it a
|
|
588
|
+
// reader cannot tell "0 proposals because nothing recurred" from
|
|
589
|
+
// "0 proposals because routing broke".
|
|
590
|
+
signalCount,
|
|
591
|
+
categories,
|
|
592
|
+
framework: framework.map((i) => i.category),
|
|
593
|
+
consumer: consumer.map((i) => i.category),
|
|
594
|
+
// Story #4824 — the machine-readable twin of the row above. A bare
|
|
595
|
+
// category list could not distinguish a genuine one-off from a
|
|
596
|
+
// recurrence the window was too narrow to see, so the count, the
|
|
597
|
+
// cross-Story span, and the shape fingerprint ride along.
|
|
598
|
+
discarded: discarded.map((i) => ({
|
|
599
|
+
category: i.category,
|
|
600
|
+
occurrences: i.occurrences,
|
|
601
|
+
storyCount: i.storyCount ?? null,
|
|
602
|
+
fingerprint: i.fingerprint ?? null,
|
|
603
|
+
})),
|
|
604
|
+
filed: filed.map((i) => ({ category: i.category, url: i.url ?? null })),
|
|
605
|
+
// Story #4578 — an empty roll-up over N>1 Stories is a claim worth
|
|
606
|
+
// flagging, not a success. Machine-readable twin of the warning
|
|
607
|
+
// prose so a caller need not regex the body.
|
|
608
|
+
emptyRollupSuspect: storyCount > 1 && empty && signalCount === 0,
|
|
609
|
+
// Story #4828 — the two remaining shapes that used to render as
|
|
610
|
+
// success. Machine-readable twins of the warning prose above.
|
|
611
|
+
zeroProposalSuspect: outcome.zeroProposals,
|
|
612
|
+
unfiledProposalSuspect: outcome.unfiledProposals,
|
|
613
|
+
filingErrors,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
467
617
|
/**
|
|
468
618
|
* @param {{
|
|
469
619
|
* storyId: number,
|
|
@@ -487,13 +637,14 @@ export function buildFollowUpsCommentBody({
|
|
|
487
637
|
signalCount = 0,
|
|
488
638
|
categories = [],
|
|
489
639
|
}) {
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const
|
|
640
|
+
const buckets = normalizeRollupBuckets(proposals, graduated);
|
|
641
|
+
const { filed, framework, consumer, discarded } = buckets;
|
|
642
|
+
const actionable = [...framework, ...consumer];
|
|
643
|
+
const empty =
|
|
644
|
+
filed.length === 0 && actionable.length === 0 && discarded.length === 0;
|
|
494
645
|
const outcome = assessRollupOutcome({
|
|
495
646
|
signalCount,
|
|
496
|
-
proposalCount:
|
|
647
|
+
proposalCount: actionable.length,
|
|
497
648
|
discardedCount: discarded.length,
|
|
498
649
|
filedCount: filed.length,
|
|
499
650
|
filingErrors: graduated?.errors,
|
|
@@ -504,103 +655,39 @@ export function buildFollowUpsCommentBody({
|
|
|
504
655
|
'',
|
|
505
656
|
`Actionable follow-ups captured from Story #${storyId} after merge.`,
|
|
506
657
|
'',
|
|
658
|
+
...(outcome.unfiledProposals
|
|
659
|
+
? [
|
|
660
|
+
...renderUnfiledProposalLines(
|
|
661
|
+
actionable.length,
|
|
662
|
+
buckets.filingErrors,
|
|
663
|
+
outcome.blockingSkipReasons,
|
|
664
|
+
),
|
|
665
|
+
'',
|
|
666
|
+
]
|
|
667
|
+
: []),
|
|
668
|
+
...renderFiledSection(filed),
|
|
669
|
+
...renderActionableSection(actionable, filed),
|
|
670
|
+
...renderDiscardedSection(discarded),
|
|
671
|
+
...renderEmptySection({
|
|
672
|
+
empty,
|
|
673
|
+
outcome,
|
|
674
|
+
signalCount,
|
|
675
|
+
categories,
|
|
676
|
+
storyCount,
|
|
677
|
+
}),
|
|
678
|
+
'```json',
|
|
507
679
|
];
|
|
508
|
-
if (outcome.unfiledProposals) {
|
|
509
|
-
lines.push(
|
|
510
|
-
...renderUnfiledProposalLines(
|
|
511
|
-
framework.length + consumer.length,
|
|
512
|
-
Array.isArray(graduated?.errors) ? graduated.errors : [],
|
|
513
|
-
outcome.blockingSkipReasons,
|
|
514
|
-
),
|
|
515
|
-
'',
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
if (filed.length > 0) {
|
|
519
|
-
lines.push('**Filed**');
|
|
520
|
-
for (const item of filed) {
|
|
521
|
-
lines.push(
|
|
522
|
-
`- ${item.source}: ${item.title}${item.url ? ` — ${item.url}` : ''}`,
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
lines.push('');
|
|
526
|
-
}
|
|
527
|
-
if (framework.length + consumer.length > 0 && filed.length === 0) {
|
|
528
|
-
lines.push('**Actionable (not auto-filed)**');
|
|
529
|
-
for (const item of [...framework, ...consumer]) {
|
|
530
|
-
lines.push(`- ${item.source}: ${item.title}`);
|
|
531
|
-
lines.push('');
|
|
532
|
-
lines.push('```bash');
|
|
533
|
-
lines.push(item.command);
|
|
534
|
-
lines.push('```');
|
|
535
|
-
}
|
|
536
|
-
lines.push('');
|
|
537
|
-
}
|
|
538
|
-
if (discarded.length > 0) {
|
|
539
|
-
lines.push('**Below threshold (not filed)**');
|
|
540
|
-
for (const item of discarded) {
|
|
541
|
-
lines.push(`- ${item.source}: ${renderDiscardedItem(item)}`);
|
|
542
|
-
}
|
|
543
|
-
lines.push('');
|
|
544
|
-
}
|
|
545
|
-
if (
|
|
546
|
-
filed.length === 0 &&
|
|
547
|
-
framework.length === 0 &&
|
|
548
|
-
consumer.length === 0 &&
|
|
549
|
-
discarded.length === 0
|
|
550
|
-
) {
|
|
551
|
-
// Story #4828 — "no proposals" has two readings, and only one of them is
|
|
552
|
-
// a quiet run. Signals gathered but nothing routed is the third instance
|
|
553
|
-
// of the silence Stories #4578 and #4824 each fixed once.
|
|
554
|
-
lines.push(
|
|
555
|
-
...(outcome.zeroProposals
|
|
556
|
-
? renderZeroProposalLines(signalCount, categories)
|
|
557
|
-
: renderEmptyRollupLines(storyCount)),
|
|
558
|
-
);
|
|
559
|
-
lines.push('');
|
|
560
|
-
}
|
|
561
|
-
lines.push('```json');
|
|
562
680
|
lines.push(
|
|
563
681
|
JSON.stringify(
|
|
564
|
-
{
|
|
682
|
+
buildRollupPayload({
|
|
565
683
|
storyId,
|
|
566
684
|
storyCount,
|
|
567
|
-
// Story #4828 — the corpus the roll-up actually read. Without it a
|
|
568
|
-
// reader cannot tell "0 proposals because nothing recurred" from
|
|
569
|
-
// "0 proposals because routing broke".
|
|
570
685
|
signalCount,
|
|
571
686
|
categories,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// recurrence the window was too narrow to see, so the count, the
|
|
577
|
-
// cross-Story span, and the shape fingerprint ride along.
|
|
578
|
-
discarded: discarded.map((i) => ({
|
|
579
|
-
category: i.category,
|
|
580
|
-
occurrences: i.occurrences,
|
|
581
|
-
storyCount: i.storyCount ?? null,
|
|
582
|
-
fingerprint: i.fingerprint ?? null,
|
|
583
|
-
})),
|
|
584
|
-
filed: filed.map((i) => ({
|
|
585
|
-
category: i.category,
|
|
586
|
-
url: i.url ?? null,
|
|
587
|
-
})),
|
|
588
|
-
// Story #4578 — an empty roll-up over N>1 Stories is a claim worth
|
|
589
|
-
// flagging, not a success. Machine-readable twin of the warning
|
|
590
|
-
// prose so a caller need not regex the body.
|
|
591
|
-
emptyRollupSuspect:
|
|
592
|
-
storyCount > 1 &&
|
|
593
|
-
filed.length === 0 &&
|
|
594
|
-
framework.length === 0 &&
|
|
595
|
-
consumer.length === 0 &&
|
|
596
|
-
discarded.length === 0 &&
|
|
597
|
-
signalCount === 0,
|
|
598
|
-
// Story #4828 — the two remaining shapes that used to render as
|
|
599
|
-
// success. Machine-readable twins of the warning prose above.
|
|
600
|
-
zeroProposalSuspect: outcome.zeroProposals,
|
|
601
|
-
unfiledProposalSuspect: outcome.unfiledProposals,
|
|
602
|
-
filingErrors: Array.isArray(graduated?.errors) ? graduated.errors : [],
|
|
603
|
-
},
|
|
687
|
+
buckets,
|
|
688
|
+
empty,
|
|
689
|
+
outcome,
|
|
690
|
+
}),
|
|
604
691
|
null,
|
|
605
692
|
2,
|
|
606
693
|
),
|
|
@@ -816,6 +816,28 @@ export function renderFanOutRemedy(finding) {
|
|
|
816
816
|
);
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
/**
|
|
820
|
+
* The finding kinds that are genuinely **cross-Story conflicts** — the SSOT
|
|
821
|
+
* for that question (Story #4907).
|
|
822
|
+
*
|
|
823
|
+
* Two readers need it and must not disagree: the validator, which renders
|
|
824
|
+
* these through {@link renderHardConflictError} when policy upgrades them to
|
|
825
|
+
* `errors[]`, and the persist soft-finding surface, which announces a
|
|
826
|
+
* conflict as a conflict and every other soft kind (`spec-word-budget`,
|
|
827
|
+
* `merge-candidate`, `unanchored-constant`, `missing-reason-to-exist`) as the
|
|
828
|
+
* advisory it is. A second copy of this list is how the two drift back apart,
|
|
829
|
+
* so it is defined exactly once and imported.
|
|
830
|
+
*/
|
|
831
|
+
export const CONFLICT_KINDS = Object.freeze(
|
|
832
|
+
new Set([
|
|
833
|
+
'shared-editor',
|
|
834
|
+
'implicit-cross-story-dep',
|
|
835
|
+
'cross-cutting-registries',
|
|
836
|
+
'fan-out-warning',
|
|
837
|
+
'missing-bdd-scaffold',
|
|
838
|
+
]),
|
|
839
|
+
);
|
|
840
|
+
|
|
819
841
|
/**
|
|
820
842
|
* Render a `'hard'`-severity conflict finding as a human-readable error
|
|
821
843
|
* message. Used by the validator when policy flags upgrade a finding to
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseStoryBodyOrThrow,
|
|
11
11
|
} from './story-body-gate.js';
|
|
12
12
|
import {
|
|
13
|
+
CONFLICT_KINDS,
|
|
13
14
|
computeConflictFindings,
|
|
14
15
|
renderHardConflictError,
|
|
15
16
|
} from './ticket-validator-conflicts.js';
|
|
@@ -666,25 +667,18 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
666
667
|
policy: opts.conflictPolicy,
|
|
667
668
|
});
|
|
668
669
|
// Advisory `## Spec` word-budget pass (Story #4723) — soft findings only,
|
|
669
|
-
// surfaced as warnings here and via the persist soft-finding channel;
|
|
670
670
|
// never promoted to `errors[]`, so an over-budget Spec cannot fail the
|
|
671
671
|
// persist. Runs after `assertStoryBodiesParse`, so string bodies parse.
|
|
672
|
+
// This pass computes but does not report: the sole production caller always
|
|
673
|
+
// runs the persist soft-finding surface, which reports every soft kind
|
|
674
|
+
// uniformly. Warning here too made `spec-word-budget` the only kind logged
|
|
675
|
+
// twice per run (Story #4907).
|
|
672
676
|
const specBudgetFindings = computeSpecBudgetFindings({ stories });
|
|
673
|
-
for (const finding of specBudgetFindings) {
|
|
674
|
-
Logger.warn(`[ticket-validator] spec-word-budget: ${finding.message}`);
|
|
675
|
-
}
|
|
676
677
|
const findings = [
|
|
677
678
|
...sizingFindings,
|
|
678
679
|
...conflictFindings,
|
|
679
680
|
...specBudgetFindings,
|
|
680
681
|
];
|
|
681
|
-
const CONFLICT_KINDS = new Set([
|
|
682
|
-
'shared-editor',
|
|
683
|
-
'implicit-cross-story-dep',
|
|
684
|
-
'cross-cutting-registries',
|
|
685
|
-
'fan-out-warning',
|
|
686
|
-
'missing-bdd-scaffold',
|
|
687
|
-
]);
|
|
688
682
|
const errors = findings
|
|
689
683
|
.filter((f) => f.severity === 'hard')
|
|
690
684
|
.map((f) =>
|
|
@@ -180,7 +180,7 @@ export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
|
|
|
180
180
|
* authoritative entry — the label set is the actual race-detection signal).
|
|
181
181
|
* Bounded to 1-9 digits to mirror the wave-marker safety margin.
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
const CLAIM_TYPE_PATTERN = /^claim-([0-9]{1,9})$/;
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Lifecycle-listener marker pattern (Story #2239 / #2241 / #2242). The
|
|
@@ -189,7 +189,7 @@ export const CLAIM_TYPE_PATTERN = /^claim-([0-9]{1,9})$/;
|
|
|
189
189
|
* `lifecycle-epic-unblocked`). Treated as a generic prefix so future
|
|
190
190
|
* listener-owned events can mint new markers without touching this enum.
|
|
191
191
|
*/
|
|
192
|
-
|
|
192
|
+
const LIFECYCLE_TYPE_PATTERN = /^lifecycle-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
195
|
* @param {string} type
|
|
@@ -273,7 +273,7 @@ export function structuredCommentMarker(type, attrs = null) {
|
|
|
273
273
|
* cannot leak across boundaries. Tests reset via the exported
|
|
274
274
|
* `_resetStructuredCommentCache()` seam.
|
|
275
275
|
*/
|
|
276
|
-
|
|
276
|
+
const _structuredCommentCache = new WeakMap();
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* Build a well-formed ticket snapshot for a Story that has zero child
|
|
@@ -359,7 +359,7 @@ export function structuredCommentCacheKey(ticketId, type, attrs) {
|
|
|
359
359
|
* - Scoped per-provider via WeakMap so test fakes never share state
|
|
360
360
|
* with the real GitHubProvider.
|
|
361
361
|
*/
|
|
362
|
-
|
|
362
|
+
const _rawCommentsCache = new WeakMap();
|
|
363
363
|
|
|
364
364
|
/**
|
|
365
365
|
* Lookup (or lazily create) the per-provider raw-comments cache.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/reserved-test-ids.js — the framework's one declaration of which
|
|
3
|
+
* Epic / Story ids are **synthetic** (Story #4892).
|
|
4
|
+
*
|
|
5
|
+
* The band `999000–999999` has been reserved for test fixtures since the
|
|
6
|
+
* post-test temp reaper shipped, but it lived as a private regexp inside
|
|
7
|
+
* `cleanup-repo-test-temp.js` (`/^epic-999\d{3}$/`) and nothing else could
|
|
8
|
+
* consult it. Two independent surfaces need the same rule, and spelling it
|
|
9
|
+
* twice is how they would drift:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Write side.** A test that spawns a real CLI at the repository root
|
|
12
|
+
* inherits the real state directory, so its fixture telemetry can land
|
|
13
|
+
* in the operator's live `temp/` signals tree. The post-run guard in
|
|
14
|
+
* `check-test-temp-hygiene.js` fails the run when a stream file owned by
|
|
15
|
+
* a reserved id survives there.
|
|
16
|
+
* 2. **Read side.** The retro composer renders the distinct Stories a
|
|
17
|
+
* friction bucket spans as recurrence evidence in a body that is filed
|
|
18
|
+
* as a real GitHub issue. A reserved id is synthetic by construction, so
|
|
19
|
+
* it can never be resolved to a real issue and must never be published
|
|
20
|
+
* as evidence (issue #4870 named `#999999` as a contributing Story).
|
|
21
|
+
*
|
|
22
|
+
* Deliberately dependency-free: the write side is a CLI guard and the read
|
|
23
|
+
* side is a pure composer, so the shared rule must not drag config, fs, or
|
|
24
|
+
* git resolution into either.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** First id in the reserved test-fixture band. */
|
|
28
|
+
const RESERVED_TEST_ID_MIN = 999000;
|
|
29
|
+
|
|
30
|
+
/** Last id in the reserved test-fixture band. */
|
|
31
|
+
const RESERVED_TEST_ID_MAX = 999999;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable band, for guard failure messages that have to tell an
|
|
35
|
+
* operator which ids they may not use for real work.
|
|
36
|
+
*/
|
|
37
|
+
export const RESERVED_TEST_ID_BAND = `${RESERVED_TEST_ID_MIN}–${RESERVED_TEST_ID_MAX}`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Is `id` inside the reserved test-fixture band?
|
|
41
|
+
*
|
|
42
|
+
* Exact-band membership, because this is the predicate the temp reaper and
|
|
43
|
+
* the pollution guard share: both classify an on-disk directory that a test
|
|
44
|
+
* created, and over-reaching (treating every large id as reserved) would let
|
|
45
|
+
* one of them delete or condemn a directory the band never claimed.
|
|
46
|
+
*
|
|
47
|
+
* @param {unknown} id
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function isReservedTestId(id) {
|
|
51
|
+
return (
|
|
52
|
+
Number.isInteger(id) &&
|
|
53
|
+
id >= RESERVED_TEST_ID_MIN &&
|
|
54
|
+
id <= RESERVED_TEST_ID_MAX
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* May `id` be published as ticket evidence in operator-visible output?
|
|
60
|
+
*
|
|
61
|
+
* The floor is the reserved band, not the band itself: an id at or above
|
|
62
|
+
* `RESERVED_TEST_ID_MIN` is synthetic by construction (the framework reserves
|
|
63
|
+
* that space for fixtures, and no repository has minted that many issues), so
|
|
64
|
+
* nothing at or beyond the floor can be resolved to a real issue. A
|
|
65
|
+
* non-integer or non-positive id is not a ticket reference at all.
|
|
66
|
+
*
|
|
67
|
+
* This is a plausibility bound, not an existence probe: it is the strongest
|
|
68
|
+
* statement a caller with no ticket-provider access can make, and it is what
|
|
69
|
+
* keeps the invariant true at every call site instead of only where somebody
|
|
70
|
+
* remembered to wire a probe.
|
|
71
|
+
*
|
|
72
|
+
* @param {unknown} id
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function isPublishableTicketId(id) {
|
|
76
|
+
return Number.isInteger(id) && id > 0 && id < RESERVED_TEST_ID_MIN;
|
|
77
|
+
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* records into the `Map<storyId, number[]>` adjacency the kernel consumes.
|
|
8
8
|
* This module is now the one home for that step; the live consumer is:
|
|
9
9
|
*
|
|
10
|
-
* - `lib/wave-runner/ready-set.js` (`
|
|
10
|
+
* - `lib/wave-runner/ready-set.js` (`planReadySet`, the path-agnostic
|
|
11
11
|
* continuous scheduler the `stories-wave-tick.js` adapter dispatches through)
|
|
12
12
|
* - `stories-wave-tick.js` (for cycle detection, before delegating
|
|
13
|
-
* selection to `
|
|
13
|
+
* selection to `planReadySet`)
|
|
14
14
|
*
|
|
15
15
|
* (Pre-v2: `epic-runner/phases/build-wave-dag.js` and `dispatch-pipeline.js`
|
|
16
16
|
* also called here; both entry seams were deleted.)
|
|
@@ -44,7 +44,7 @@ import { parseBlockedBy } from './dependency-parser.js';
|
|
|
44
44
|
* @param {object} [opts]
|
|
45
45
|
* @param {boolean} [opts.dropForeign=false] When `false` (the v2 default,
|
|
46
46
|
* matching the `/deliver` path — `stories-wave-tick.js` and the
|
|
47
|
-
* `
|
|
47
|
+
* `planReadySet` core), the operator-DAG contract is preserved: a
|
|
48
48
|
* dependency on an id absent from the input is treated as not-yet-done
|
|
49
49
|
* and withholds the dependent until it completes. When `true` (the
|
|
50
50
|
* pre-v2 Epic-scoped-wrapper semantics), edges pointing at ids outside
|