@event4u/agent-config 11.0.0 → 12.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +173 -229
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/agent-handoff.md +0 -1
- package/dist/agent-src/commands/agent-status.md +0 -1
- package/dist/agent-src/commands/agents/audit.md +0 -1
- package/dist/agent-src/commands/agents/init.md +0 -1
- package/dist/agent-src/commands/agents/optimize.md +0 -1
- package/dist/agent-src/commands/agents/user/accept.md +0 -1
- package/dist/agent-src/commands/agents/user/delete.md +0 -1
- package/dist/agent-src/commands/agents/user/init.md +0 -1
- package/dist/agent-src/commands/agents/user/review.md +0 -1
- package/dist/agent-src/commands/agents/user/show.md +0 -1
- package/dist/agent-src/commands/agents/user/update.md +0 -1
- package/dist/agent-src/commands/agents/user.md +0 -1
- package/dist/agent-src/commands/agents.md +0 -1
- package/dist/agent-src/commands/analytics/prune.md +0 -1
- package/dist/agent-src/commands/analytics/show.md +0 -1
- package/dist/agent-src/commands/analytics.md +0 -1
- package/dist/agent-src/commands/analyze/conformance.md +0 -1
- package/dist/agent-src/commands/analyze/decision.md +0 -1
- package/dist/agent-src/commands/analyze/inbox.md +0 -1
- package/dist/agent-src/commands/analyze/incident.md +0 -1
- package/dist/agent-src/commands/analyze/near-miss.md +0 -1
- package/dist/agent-src/commands/analyze/postmortem.md +0 -1
- package/dist/agent-src/commands/analyze/premortem.md +0 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
- package/dist/agent-src/commands/analyze.md +0 -1
- package/dist/agent-src/commands/brand/identity.md +0 -1
- package/dist/agent-src/commands/brand/review.md +0 -1
- package/dist/agent-src/commands/brand/strategy.md +0 -1
- package/dist/agent-src/commands/brand/tokens.md +0 -1
- package/dist/agent-src/commands/brand/voice.md +0 -1
- package/dist/agent-src/commands/brand.md +0 -1
- package/dist/agent-src/commands/bug/fix.md +0 -1
- package/dist/agent-src/commands/bug/investigate.md +0 -1
- package/dist/agent-src/commands/bug.md +0 -1
- package/dist/agent-src/commands/challenge-me/vision.md +0 -1
- package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
- package/dist/agent-src/commands/challenge-me.md +0 -1
- package/dist/agent-src/commands/chat-history/import.md +0 -1
- package/dist/agent-src/commands/chat-history.md +0 -1
- package/dist/agent-src/commands/check-current-md.md +0 -1
- package/dist/agent-src/commands/commit/in-chunks.md +0 -1
- package/dist/agent-src/commands/commit.md +0 -1
- package/dist/agent-src/commands/condense.md +0 -1
- package/dist/agent-src/commands/context/create.md +0 -1
- package/dist/agent-src/commands/context/refactor.md +0 -1
- package/dist/agent-src/commands/context.md +0 -1
- package/dist/agent-src/commands/contribution-precheck.md +0 -1
- package/dist/agent-src/commands/cost/profile.md +0 -1
- package/dist/agent-src/commands/cost/report.md +0 -1
- package/dist/agent-src/commands/cost.md +0 -1
- package/dist/agent-src/commands/council/analysis.md +0 -1
- package/dist/agent-src/commands/council/debate.md +0 -1
- package/dist/agent-src/commands/council/default.md +0 -1
- package/dist/agent-src/commands/council/design.md +0 -1
- package/dist/agent-src/commands/council/optimize.md +0 -1
- package/dist/agent-src/commands/council/pr.md +0 -1
- package/dist/agent-src/commands/council.md +0 -1
- package/dist/agent-src/commands/design-system/capture.md +0 -1
- package/dist/agent-src/commands/design-system/generate.md +0 -1
- package/dist/agent-src/commands/design-system/import.md +0 -1
- package/dist/agent-src/commands/design-system.md +0 -1
- package/dist/agent-src/commands/estimate-ticket.md +0 -1
- package/dist/agent-src/commands/explain-run.md +0 -1
- package/dist/agent-src/commands/feature/dev.md +0 -1
- package/dist/agent-src/commands/feature/explore.md +0 -1
- package/dist/agent-src/commands/feature/plan.md +0 -1
- package/dist/agent-src/commands/feature/refactor.md +0 -1
- package/dist/agent-src/commands/feature/roadmap.md +0 -1
- package/dist/agent-src/commands/feature.md +0 -1
- package/dist/agent-src/commands/fix/ci.md +0 -1
- package/dist/agent-src/commands/fix/comments.md +0 -1
- package/dist/agent-src/commands/fix/portability.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments.md +0 -1
- package/dist/agent-src/commands/fix/quality.md +0 -1
- package/dist/agent-src/commands/fix/refs.md +0 -1
- package/dist/agent-src/commands/fix/route.md +0 -1
- package/dist/agent-src/commands/fix/seeder.md +0 -1
- package/dist/agent-src/commands/fix.md +0 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
- package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
- package/dist/agent-src/commands/ghostwriter/list.md +0 -1
- package/dist/agent-src/commands/ghostwriter/show.md +0 -1
- package/dist/agent-src/commands/ghostwriter/write.md +0 -1
- package/dist/agent-src/commands/ghostwriter.md +0 -1
- package/dist/agent-src/commands/grill-me.md +0 -1
- package/dist/agent-src/commands/humanize.md +0 -1
- package/dist/agent-src/commands/image/analyse.md +0 -1
- package/dist/agent-src/commands/image/create.md +0 -1
- package/dist/agent-src/commands/image/verify.md +0 -1
- package/dist/agent-src/commands/image.md +0 -1
- package/dist/agent-src/commands/implement-ticket.md +0 -1
- package/dist/agent-src/commands/jira-ticket.md +0 -1
- package/dist/agent-src/commands/judge/on-diff.md +0 -1
- package/dist/agent-src/commands/judge/solo.md +0 -1
- package/dist/agent-src/commands/judge/steps.md +0 -1
- package/dist/agent-src/commands/judge.md +0 -1
- package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
- package/dist/agent-src/commands/knowledge/forget.md +0 -1
- package/dist/agent-src/commands/knowledge/ingest.md +0 -1
- package/dist/agent-src/commands/knowledge/list.md +0 -1
- package/dist/agent-src/commands/knowledge.md +0 -1
- package/dist/agent-src/commands/memory/add.md +0 -1
- package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
- package/dist/agent-src/commands/memory/load.md +0 -1
- package/dist/agent-src/commands/memory/mine-session.md +0 -1
- package/dist/agent-src/commands/memory/promote.md +0 -1
- package/dist/agent-src/commands/memory/propose.md +0 -1
- package/dist/agent-src/commands/memory.md +0 -1
- package/dist/agent-src/commands/mission/upgrade.md +0 -1
- package/dist/agent-src/commands/mode.md +0 -1
- package/dist/agent-src/commands/module/create.md +0 -1
- package/dist/agent-src/commands/module/explore.md +0 -1
- package/dist/agent-src/commands/module.md +0 -1
- package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
- package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
- package/dist/agent-src/commands/optimize/deep.md +0 -1
- package/dist/agent-src/commands/optimize/project.md +0 -1
- package/dist/agent-src/commands/optimize/prompt.md +0 -1
- package/dist/agent-src/commands/optimize/rtk.md +0 -1
- package/dist/agent-src/commands/optimize/skills.md +0 -1
- package/dist/agent-src/commands/optimize.md +0 -1
- package/dist/agent-src/commands/orchestrate.md +0 -1
- package/dist/agent-src/commands/override/create.md +0 -1
- package/dist/agent-src/commands/override/manage.md +0 -1
- package/dist/agent-src/commands/override.md +0 -1
- package/dist/agent-src/commands/package/reset.md +0 -1
- package/dist/agent-src/commands/package/test.md +0 -1
- package/dist/agent-src/commands/package.md +0 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
- package/dist/agent-src/commands/post-as/me.md +0 -1
- package/dist/agent-src/commands/post-as.md +0 -1
- package/dist/agent-src/commands/pr/create/description-only.md +0 -1
- package/dist/agent-src/commands/pr/create.md +23 -9
- package/dist/agent-src/commands/prediction-pool.md +0 -1
- package/dist/agent-src/commands/prepare-for-review.md +0 -1
- package/dist/agent-src/commands/profile/activate.md +0 -1
- package/dist/agent-src/commands/profile/deactivate.md +0 -1
- package/dist/agent-src/commands/profile/show.md +0 -1
- package/dist/agent-src/commands/profile.md +0 -1
- package/dist/agent-src/commands/project/analyze.md +0 -1
- package/dist/agent-src/commands/project/health.md +0 -1
- package/dist/agent-src/commands/project.md +0 -1
- package/dist/agent-src/commands/refine-ticket.md +0 -1
- package/dist/agent-src/commands/research/deep.md +0 -1
- package/dist/agent-src/commands/research/report.md +0 -1
- package/dist/agent-src/commands/research.md +0 -1
- package/dist/agent-src/commands/review/changes.md +0 -1
- package/dist/agent-src/commands/review/routing.md +0 -1
- package/dist/agent-src/commands/review.md +0 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
- package/dist/agent-src/commands/roadmap/create.md +0 -1
- package/dist/agent-src/commands/roadmap/materialize.md +0 -1
- package/dist/agent-src/commands/roadmap/next.md +0 -1
- package/dist/agent-src/commands/roadmap/process-full.md +0 -1
- package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
- package/dist/agent-src/commands/roadmap/process-step.md +0 -1
- package/dist/agent-src/commands/roadmap.md +0 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
- package/dist/agent-src/commands/security-audit-config.md +0 -1
- package/dist/agent-src/commands/skill/preview.md +0 -1
- package/dist/agent-src/commands/skill.md +0 -1
- package/dist/agent-src/commands/skills/discover.md +0 -1
- package/dist/agent-src/commands/skills.md +0 -1
- package/dist/agent-src/commands/sync/agent-settings.md +0 -1
- package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
- package/dist/agent-src/commands/sync/gitignore.md +0 -1
- package/dist/agent-src/commands/sync.md +0 -1
- package/dist/agent-src/commands/tdd/green.md +0 -1
- package/dist/agent-src/commands/tdd/red.md +0 -1
- package/dist/agent-src/commands/tdd/refactor.md +0 -1
- package/dist/agent-src/commands/tdd.md +0 -1
- package/dist/agent-src/commands/team/adversarial.md +0 -1
- package/dist/agent-src/commands/team/delegate.md +0 -1
- package/dist/agent-src/commands/team/review.md +0 -1
- package/dist/agent-src/commands/team/status.md +0 -1
- package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
- package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
- package/dist/agent-src/commands/team-knowledge.md +0 -1
- package/dist/agent-src/commands/team.md +0 -1
- package/dist/agent-src/commands/tests/create.md +0 -1
- package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
- package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
- package/dist/agent-src/commands/tests/execute.md +0 -1
- package/dist/agent-src/commands/tests.md +0 -1
- package/dist/agent-src/commands/threat-model.md +0 -1
- package/dist/agent-src/commands/update-form-request-messages.md +0 -1
- package/dist/agent-src/commands/upstream-contribute.md +0 -1
- package/dist/agent-src/commands/video/from-script.md +0 -1
- package/dist/agent-src/commands/video/from-song.md +0 -1
- package/dist/agent-src/commands/video/scene.md +0 -1
- package/dist/agent-src/commands/video/stitch.md +0 -1
- package/dist/agent-src/commands/video/storyboard.md +0 -1
- package/dist/agent-src/commands/video.md +0 -1
- package/dist/agent-src/commands/work.md +0 -1
- package/dist/agent-src/commands/worktree/cleanup.md +0 -1
- package/dist/agent-src/commands/worktree/create.md +0 -1
- package/dist/agent-src/commands/worktree/status.md +0 -1
- package/dist/agent-src/commands/worktree/verify.md +0 -1
- package/dist/agent-src/commands/worktree.md +0 -1
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +9 -1
- package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -7
- package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/commands/commands.candidates.test.js +1 -1
- package/dist/cli/commands/commands.candidates.test.js.map +1 -1
- package/dist/cli/commands/commands.js +9 -15
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/commands/profiles.test.js +7 -7
- package/dist/cli/commands/profiles.test.js.map +1 -1
- package/dist/cli/discovery/loadManifest.js.map +1 -1
- package/dist/cli/main.js +2 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-K34QTSD5.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-LKZ5DOYW.js} +557 -297
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-O52B4F5S.js} +123 -2338
- package/dist/cli-delegate/cmd_conformance.js +18 -6
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +213 -412
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +2588 -1082
- package/dist/install/install.mjs +744 -482
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +159 -15
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +21 -1
- package/docs/contracts/settings-classes.md +41 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/src/config/agent-settings.template.yml +34 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +39 -9
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +7 -1
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +52 -5
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +862 -0
- package/src/scripts/ai_council/clients.ts +182 -3
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/audit_command_surface.ts +21 -15
- package/src/scripts/build_catalog_index.ts +1 -1
- package/src/scripts/build_discovery_manifest.ts +30 -34
- package/src/scripts/capture_skill_catalogue.ts +185 -328
- package/src/scripts/check_branch_freshness.ts +217 -62
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/gen_discovery_baseline.ts +3 -3
- package/src/scripts/hook_manifest.yaml +67 -13
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_command_tiers.ts +29 -75
- package/src/scripts/lint_originality.ts +101 -14
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +95 -1
- package/src/scripts/schemas/command.schema.json +6 -11
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/skill_linter.ts +82 -2
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +113 -8
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gate: the scheduled-deprecations table in `docs/MIGRATION.md` is checked by
|
|
4
|
+
* arithmetic, not by somebody remembering to read it.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. The table records commitments due at a *future* major, and
|
|
7
|
+
* the release runbook's pre-flight sends the releaser to it with a manual
|
|
8
|
+
* checkbox whose own text says "A row left unread is how a removal commitment
|
|
9
|
+
* becomes folklore". The row for the `code_graph` engine committed its removal
|
|
10
|
+
* to the major after the one after 9.x — 11.0 — and `package.json` reads
|
|
11
|
+
* 12.0.0. The row existed, was correct, and was missed anyway. So the defect is
|
|
12
|
+
* the absence of a check, not the absence of a row, and a second manual reminder
|
|
13
|
+
* would repeat the failure at a higher frequency.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IT DOES NOT DO. It never removes anything. Acting on an overdue row is a
|
|
16
|
+
* public-surface change gated by `downstream-changes` and `scope-control` and
|
|
17
|
+
* lands at a major cut; this gate reports and, at a cut, refuses. It also does
|
|
18
|
+
* not invent a due version: a cell it cannot resolve is reported as
|
|
19
|
+
* *unresolved* and reds, because a table whose format drifted silently is
|
|
20
|
+
* exactly the shape "a gate that scans nothing exits green" describes.
|
|
21
|
+
*
|
|
22
|
+
* EXIT CONTRACT
|
|
23
|
+
* 0 nothing due, or due surfaces reported on an ordinary branch
|
|
24
|
+
* 1 a row could not be parsed / resolved, the table is missing or empty, or
|
|
25
|
+
* a due surface was found while `--cutting <version>` was passed
|
|
26
|
+
* 2 usage error, or an environment failure that leaves the gate with no
|
|
27
|
+
* comparand at all (missing / malformed `package.json`)
|
|
28
|
+
*
|
|
29
|
+
* `--cutting <X.Y.Z>` is the major-cut switch: an ordinary branch gets the
|
|
30
|
+
* report, a release gets the refusal. That asymmetry is deliberate — an overdue
|
|
31
|
+
* row can be a considered deferral, and a hard refusal on every branch would
|
|
32
|
+
* turn a judgement into an outage. A deliberate deferral is expressible in the
|
|
33
|
+
* table itself (a `permanent keep` row), which is what keeps the refusal honest
|
|
34
|
+
* at the cut.
|
|
35
|
+
*
|
|
36
|
+
* **THE COMPARAND IS THE TARGET, NOT THE SHIPPED VERSION, AND THE DIFFERENCE IS
|
|
37
|
+
* THE WHOLE POINT.** An earlier version of this gate compared against
|
|
38
|
+
* `package.json` in both modes. At the cut to N, `package.json` still reads
|
|
39
|
+
* N-1, so a row committed to N resolved to "one major early" and passed — the
|
|
40
|
+
* refusal could only ever fire on a row that was ALREADY a major late, which is
|
|
41
|
+
* precisely the lateness this gate exists to prevent. `--cutting` takes the
|
|
42
|
+
* target so the cut that would CREATE the miss is the one that gets refused.
|
|
43
|
+
*
|
|
44
|
+
* SCOPE BOUND, stated because the runbook checkbox above it is wider: only the
|
|
45
|
+
* **Removal due** column is compared. The Deprecation-notice-due cell is parsed
|
|
46
|
+
* solely as the anchor for a relative removal cell and is never checked against
|
|
47
|
+
* anything, because a shipped notice is written as a date rather than a version
|
|
48
|
+
* and has no comparand. The "ship the notice" half of the pre-flight obligation
|
|
49
|
+
* therefore remains a human read.
|
|
50
|
+
*/
|
|
51
|
+
import * as fs from 'node:fs';
|
|
52
|
+
import * as os from 'node:os';
|
|
53
|
+
import * as path from 'node:path';
|
|
54
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
55
|
+
|
|
56
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
57
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
58
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
59
|
+
|
|
60
|
+
const MIGRATION_PATH = path.join('docs', 'MIGRATION.md');
|
|
61
|
+
const PACKAGE_PATH = 'package.json';
|
|
62
|
+
|
|
63
|
+
/** The heading the table lives under. Matched case-insensitively on the stem. */
|
|
64
|
+
const SECTION_MARKER = /^##\s+scheduled deprecations\b/i;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A row whose removal date is deliberately not pinned. The marker is the
|
|
68
|
+
* table's own wording, so a maintainer-owned removal date stays expressible
|
|
69
|
+
* without becoming an unresolved parse.
|
|
70
|
+
*/
|
|
71
|
+
const UNPINNED_MARKER = /not pinned/i;
|
|
72
|
+
|
|
73
|
+
/** A row explicitly recorded as staying — the "documented keep" state. */
|
|
74
|
+
const PERMANENT_KEEP_MARKER = /permanent keep|no removal scheduled/i;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A commitment WITHDRAWN with its reason recorded, rather than re-dated.
|
|
78
|
+
*
|
|
79
|
+
* The third tracked state, and it exists because the table grew one the day
|
|
80
|
+
* after this gate shipped: the `code_graph` commitment was withdrawn rather
|
|
81
|
+
* than moved, on the argument that the payload it existed for had already
|
|
82
|
+
* landed. That is a legitimate outcome — the table's own prose calls a
|
|
83
|
+
* withdrawn commitment recorded "not the folklore this table exists to
|
|
84
|
+
* prevent" — so the gate must read it as tracked, not red it as unresolvable.
|
|
85
|
+
* The table is the contract; a gate that forces the table into its own grammar
|
|
86
|
+
* has the dependency backwards.
|
|
87
|
+
*/
|
|
88
|
+
const WITHDRAWN_MARKER = /no removal date|commitment withdrawn|withdrawn \d{4}-\d{2}-\d{2}/i;
|
|
89
|
+
|
|
90
|
+
export interface DeprecationRow {
|
|
91
|
+
/** Raw first cell — the surface, as written. */
|
|
92
|
+
surface: string;
|
|
93
|
+
/** Short name for reporting: the first backticked token, else a truncation. */
|
|
94
|
+
name: string;
|
|
95
|
+
/** Raw "Deprecation notice due" cell. */
|
|
96
|
+
noticeDueRaw: string;
|
|
97
|
+
/** Raw "Removal due" cell. */
|
|
98
|
+
removalDueRaw: string;
|
|
99
|
+
/** Path-shaped tokens found in the surface cell (contain a `/`). */
|
|
100
|
+
paths: string[];
|
|
101
|
+
/** 1-based row number within the table, for error messages. */
|
|
102
|
+
index: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type Resolution =
|
|
106
|
+
| { kind: 'major'; major: number }
|
|
107
|
+
| { kind: 'unpinned' }
|
|
108
|
+
| { kind: 'keep' }
|
|
109
|
+
| { kind: 'withdrawn' }
|
|
110
|
+
| { kind: 'unresolved'; cell: string };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a due-version cell to a concrete major.
|
|
114
|
+
*
|
|
115
|
+
* The grammar is deliberately small, and every form below appears in the table
|
|
116
|
+
* today. A form outside it resolves to `unresolved` and reds rather than being
|
|
117
|
+
* guessed at — an invented due version is worse than a parse failure, because
|
|
118
|
+
* it would silently move a commitment.
|
|
119
|
+
*
|
|
120
|
+
* `11.0` / `11.0.0` / `11` → 11
|
|
121
|
+
* `next major after 9.x` → 10
|
|
122
|
+
* `the major after that` → (notice major) + 1
|
|
123
|
+
* `next major after the notice` → (notice major) + 1
|
|
124
|
+
* `… not pinned …` → unpinned (maintainer-owned date)
|
|
125
|
+
* `… permanent keep …` → keep (documented, not scheduled)
|
|
126
|
+
* `… no removal date …` → withdrawn (commitment withdrawn, reason recorded)
|
|
127
|
+
*
|
|
128
|
+
* @param cell the raw table cell
|
|
129
|
+
* @param noticeMajor the already-resolved notice major, when one exists — the
|
|
130
|
+
* relative forms are only resolvable against it
|
|
131
|
+
*/
|
|
132
|
+
export function resolveDueMajor(cell: string, noticeMajor?: number): Resolution {
|
|
133
|
+
const text = cell.trim();
|
|
134
|
+
if (text === '') return { kind: 'unresolved', cell };
|
|
135
|
+
|
|
136
|
+
// Checked before the relative forms on purpose: the unpinned row also
|
|
137
|
+
// contains the words "next major after the notice", and the explicit
|
|
138
|
+
// not-pinned statement is the load-bearing half of that cell.
|
|
139
|
+
if (UNPINNED_MARKER.test(text)) return { kind: 'unpinned' };
|
|
140
|
+
if (PERMANENT_KEEP_MARKER.test(text)) return { kind: 'keep' };
|
|
141
|
+
if (WITHDRAWN_MARKER.test(text)) return { kind: 'withdrawn' };
|
|
142
|
+
|
|
143
|
+
const relativeAfterVersion = /next major after\s+(?:v)?(\d+)(?:\.(?:x|\d+))?/i.exec(text);
|
|
144
|
+
const relativeToNotice = /(?:the major after that|next major after the notice)/i.test(text);
|
|
145
|
+
|
|
146
|
+
if (relativeAfterVersion) {
|
|
147
|
+
return { kind: 'major', major: Number(relativeAfterVersion[1]) + 1 };
|
|
148
|
+
}
|
|
149
|
+
if (relativeToNotice) {
|
|
150
|
+
if (noticeMajor === undefined) return { kind: 'unresolved', cell };
|
|
151
|
+
return { kind: 'major', major: noticeMajor + 1 };
|
|
152
|
+
}
|
|
153
|
+
// ANCHORED, not "a digit run somewhere in the cell". The loose form matched
|
|
154
|
+
// any number and so resolved `shipped 2026-07-29 — dormant by default` to
|
|
155
|
+
// major 2026 — a confidently wrong commitment, which is the one outcome the
|
|
156
|
+
// contract at the top of this file rules out. The whole cell (bare of
|
|
157
|
+
// markdown emphasis) must BE a version, or the cell is unresolved.
|
|
158
|
+
const explicit = /^\**\s*v?(\d+)(?:\.(?:\d+|x)){0,2}\s*\**$/.exec(text);
|
|
159
|
+
if (explicit) {
|
|
160
|
+
return { kind: 'major', major: Number(explicit[1]) };
|
|
161
|
+
}
|
|
162
|
+
return { kind: 'unresolved', cell };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The table's header no longer carries a `Removal due` column.
|
|
167
|
+
*
|
|
168
|
+
* Its own error, rather than N per-row "cell not resolvable" findings: a
|
|
169
|
+
* renamed column has exactly one cause and one fix, and reporting it per row
|
|
170
|
+
* buries that under noise proportional to the table's length.
|
|
171
|
+
*/
|
|
172
|
+
export class HeaderMismatchError extends Error {
|
|
173
|
+
constructor(readonly header: readonly string[]) {
|
|
174
|
+
super(
|
|
175
|
+
'lint_scheduled_deprecations: the scheduled-deprecations table has no ' +
|
|
176
|
+
`"Removal due" column — header reads [${header.join(' | ')}]. Either the ` +
|
|
177
|
+
'column was renamed (update this gate in the same change) or the heading ' +
|
|
178
|
+
'now sits above a different table.',
|
|
179
|
+
);
|
|
180
|
+
this.name = 'HeaderMismatchError';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Split one markdown table line into trimmed cells, dropping the outer pipes. */
|
|
185
|
+
function splitRow(line: string): string[] {
|
|
186
|
+
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
|
|
187
|
+
return trimmed.split('|').map((c) => c.trim());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A separator line — `|---|---|` and its alignment variants. */
|
|
191
|
+
function isSeparator(line: string): boolean {
|
|
192
|
+
return /^\|?[\s:|-]+\|[\s:|-]*$/.test(line.trim()) && line.includes('-');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract the scheduled-deprecations table from a `MIGRATION.md` body.
|
|
197
|
+
*
|
|
198
|
+
* Reads only the first table under the section heading and stops at the next
|
|
199
|
+
* `##`, so the version sections below it — every one of which records a change
|
|
200
|
+
* that already shipped — can never be parsed as commitments.
|
|
201
|
+
*/
|
|
202
|
+
export function parseRows(markdown: string): DeprecationRow[] {
|
|
203
|
+
const lines = markdown.split('\n');
|
|
204
|
+
let inSection = false;
|
|
205
|
+
let header: string[] | undefined;
|
|
206
|
+
// Resolved ONCE when the header row is read, not per data row. Recomputing
|
|
207
|
+
// them inside the loop made a renamed column red every row independently
|
|
208
|
+
// with an empty cell quoted, burying the one true cause — a header that no
|
|
209
|
+
// longer matches — under N identical findings.
|
|
210
|
+
let idxNotice = -1;
|
|
211
|
+
let idxRemoval = -1;
|
|
212
|
+
const rows: DeprecationRow[] = [];
|
|
213
|
+
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
// ANY heading closes the span, not just a sibling `## `. The table now
|
|
216
|
+
// has a `### Row status` subsection under it; with a `^##\s` close the
|
|
217
|
+
// span ran past it, so a table added there would have been appended as
|
|
218
|
+
// data rows with its header parsed as one.
|
|
219
|
+
if (/^#{1,6}\s/.test(line)) {
|
|
220
|
+
if (inSection) break;
|
|
221
|
+
inSection = SECTION_MARKER.test(line);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!inSection) continue;
|
|
225
|
+
if (!line.trim().startsWith('|')) continue;
|
|
226
|
+
if (isSeparator(line)) continue;
|
|
227
|
+
|
|
228
|
+
const cells = splitRow(line);
|
|
229
|
+
if (header === undefined) {
|
|
230
|
+
header = cells.map((c) => c.toLowerCase());
|
|
231
|
+
idxNotice = header.findIndex((h) => h.includes('notice due'));
|
|
232
|
+
idxRemoval = header.findIndex((h) => h.includes('removal due'));
|
|
233
|
+
if (idxRemoval < 0) {
|
|
234
|
+
throw new HeaderMismatchError(header);
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const surface = cells[0] ?? '';
|
|
240
|
+
const backticked = [...surface.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? '');
|
|
241
|
+
|
|
242
|
+
rows.push({
|
|
243
|
+
surface,
|
|
244
|
+
name: backticked[0] ?? surface.slice(0, 60),
|
|
245
|
+
noticeDueRaw: idxNotice >= 0 ? (cells[idxNotice] ?? '') : '',
|
|
246
|
+
removalDueRaw: idxRemoval >= 0 ? (cells[idxRemoval] ?? '') : '',
|
|
247
|
+
paths: backticked.filter((t) => t.includes('/')),
|
|
248
|
+
index: rows.length + 1,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return rows;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface Finding {
|
|
255
|
+
row: DeprecationRow;
|
|
256
|
+
/**
|
|
257
|
+
* `overdue` — the removal is due at or before the comparand major.
|
|
258
|
+
* `unresolved` — the removal-due cell could not be resolved to a version.
|
|
259
|
+
*/
|
|
260
|
+
kind: 'overdue' | 'unresolved';
|
|
261
|
+
/**
|
|
262
|
+
* `comparand − dueMajor`. Positive = late by that many majors; **zero =
|
|
263
|
+
* due exactly at the major being cut**, which is a refusal at a cut and
|
|
264
|
+
* silent on an ordinary branch, since a row due at the shipped major is
|
|
265
|
+
* being acted on right now rather than missed.
|
|
266
|
+
*/
|
|
267
|
+
overdueBy: number;
|
|
268
|
+
/** Path tokens from the row that still exist on disk. */
|
|
269
|
+
livePaths: string[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compare every row's removal-due major against `comparand`.
|
|
274
|
+
*
|
|
275
|
+
* `comparand` is the SHIPPED major on an ordinary branch and the TARGET major
|
|
276
|
+
* at a cut — the distinction the gate's header paragraph explains, and the
|
|
277
|
+
* reason this parameter is not simply read from `package.json` in here.
|
|
278
|
+
*/
|
|
279
|
+
export function evaluate(
|
|
280
|
+
rows: readonly DeprecationRow[],
|
|
281
|
+
comparand: number,
|
|
282
|
+
root: string,
|
|
283
|
+
ledger?: GateLedger,
|
|
284
|
+
/**
|
|
285
|
+
* `>= 0` at a cut (a row due AT the target must be acted on now), `> 0` on
|
|
286
|
+
* an ordinary branch (a row due at the shipped major is being worked, not
|
|
287
|
+
* missed). Without the split, a row committed to the current major warned
|
|
288
|
+
* on every branch and every CI run for a whole release cycle — a standing
|
|
289
|
+
* notice for something nobody is late on, which is the habituation this
|
|
290
|
+
* gate exists to avoid producing.
|
|
291
|
+
*/
|
|
292
|
+
dueAtComparandCounts = false,
|
|
293
|
+
): Finding[] {
|
|
294
|
+
const findings: Finding[] = [];
|
|
295
|
+
ledger?.plan(rows.map((r) => `row-${String(r.index)}:${r.name}`));
|
|
296
|
+
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
const key = `row-${String(row.index)}:${row.name}`;
|
|
299
|
+
const notice = resolveDueMajor(row.noticeDueRaw);
|
|
300
|
+
const noticeMajor = notice.kind === 'major' ? notice.major : undefined;
|
|
301
|
+
const removal = resolveDueMajor(row.removalDueRaw, noticeMajor);
|
|
302
|
+
|
|
303
|
+
if (removal.kind === 'unpinned' || removal.kind === 'keep' || removal.kind === 'withdrawn') {
|
|
304
|
+
// A tracked state carries no comparable version, so the arithmetic
|
|
305
|
+
// never runs on it. Recorded as SKIPPED rather than completed: a row
|
|
306
|
+
// that skipped the comparison must not read the same as one that
|
|
307
|
+
// passed it, which is the false-green shape this whole gate is about
|
|
308
|
+
// — and today all three shipped rows are tracked states, so a
|
|
309
|
+
// `complete()` here meant the gate printed green over zero
|
|
310
|
+
// comparisons.
|
|
311
|
+
ledger?.skip(key, 'not_applicable_kind');
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (removal.kind === 'unresolved') {
|
|
315
|
+
findings.push({ row, kind: 'unresolved', overdueBy: 0, livePaths: [] });
|
|
316
|
+
ledger?.fail(key, `removal-due cell not resolvable: ${removal.cell}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const overdueBy = comparand - removal.major;
|
|
321
|
+
if (overdueBy > 0 || (overdueBy === 0 && dueAtComparandCounts)) {
|
|
322
|
+
const livePaths = row.paths.filter((p) => fs.existsSync(path.join(root, p)));
|
|
323
|
+
findings.push({ row, kind: 'overdue', overdueBy, livePaths });
|
|
324
|
+
ledger?.fail(
|
|
325
|
+
key,
|
|
326
|
+
`removal due at ${String(removal.major)}.0, comparand major is ${String(comparand)}`,
|
|
327
|
+
);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
ledger?.complete(key);
|
|
331
|
+
}
|
|
332
|
+
return findings;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function _isDir(p: string): boolean {
|
|
336
|
+
try {
|
|
337
|
+
return fs.statSync(p).isDirectory();
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Walk up from CWD until a dir containing `docs/MIGRATION.md` is found. */
|
|
344
|
+
function repoRoot(): string {
|
|
345
|
+
let cur = process.cwd();
|
|
346
|
+
const chain = [cur];
|
|
347
|
+
for (;;) {
|
|
348
|
+
const parent = path.dirname(cur);
|
|
349
|
+
if (parent === cur) break;
|
|
350
|
+
chain.push(parent);
|
|
351
|
+
cur = parent;
|
|
352
|
+
}
|
|
353
|
+
for (const candidate of chain) {
|
|
354
|
+
if (_isDir(path.join(candidate, 'docs')) && fs.existsSync(path.join(candidate, MIGRATION_PATH))) {
|
|
355
|
+
return candidate;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return process.cwd();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The major from `package.json`'s `version`. */
|
|
362
|
+
export function shippedMajor(root: string): number {
|
|
363
|
+
const raw = fs.readFileSync(path.join(root, PACKAGE_PATH), 'utf-8');
|
|
364
|
+
const parsed: unknown = JSON.parse(raw);
|
|
365
|
+
const version =
|
|
366
|
+
typeof parsed === 'object' && parsed !== null && 'version' in parsed
|
|
367
|
+
? (parsed as { version?: unknown }).version
|
|
368
|
+
: undefined;
|
|
369
|
+
if (typeof version !== 'string') {
|
|
370
|
+
throw new Error(`${PACKAGE_PATH}: no string \`version\` field`);
|
|
371
|
+
}
|
|
372
|
+
const major = /^(\d+)\./.exec(version);
|
|
373
|
+
if (!major) {
|
|
374
|
+
throw new Error(`${PACKAGE_PATH}: version "${version}" has no leading major`);
|
|
375
|
+
}
|
|
376
|
+
return Number(major[1]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Floors for `--self-test`, declared here so a truncation is a visible diff. */
|
|
380
|
+
const SELF_TEST_MIN_CASES = 10;
|
|
381
|
+
const SELF_TEST_MIN_REJECT = 6;
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Prove, on demand, that this gate's rejections still fire against its own CLI.
|
|
385
|
+
*
|
|
386
|
+
* The unit suite covers the same ground against imported functions; this covers
|
|
387
|
+
* the binary a contributor actually runs, including argv parsing and the entry
|
|
388
|
+
* guard — the layers that have silently no-opped in this repository before.
|
|
389
|
+
*/
|
|
390
|
+
function selfTest(): number {
|
|
391
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lsd-selftest-'));
|
|
392
|
+
const repo = repoRoot();
|
|
393
|
+
const script = path.join('src', 'scripts', 'lint_scheduled_deprecations.ts');
|
|
394
|
+
|
|
395
|
+
const header = [
|
|
396
|
+
'# Migration Guide',
|
|
397
|
+
'',
|
|
398
|
+
'## Scheduled deprecations (forward-looking)',
|
|
399
|
+
'',
|
|
400
|
+
'| Surface | Committed | Deprecation notice due | Removal due | Reversal condition |',
|
|
401
|
+
'|---|---|---|---|---|',
|
|
402
|
+
].join('\n');
|
|
403
|
+
|
|
404
|
+
/** Build a fixture tree and run the real CLI against it. */
|
|
405
|
+
const fixture = (name: string, major: number, table: string, args: string[] = []): number => {
|
|
406
|
+
const dir = path.join(root, name);
|
|
407
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
408
|
+
fs.writeFileSync(
|
|
409
|
+
path.join(dir, 'package.json'),
|
|
410
|
+
JSON.stringify({ name: 'fixture', version: `${String(major)}.0.0` }),
|
|
411
|
+
'utf8',
|
|
412
|
+
);
|
|
413
|
+
fs.writeFileSync(path.join(dir, 'docs', 'MIGRATION.md'), `${header}\n${table}\n`, 'utf8');
|
|
414
|
+
return runGateCli(repo, script, args, dir);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const cases: SelfTestCase[] = [
|
|
418
|
+
{
|
|
419
|
+
name: 'overdue row at a major cut is refused',
|
|
420
|
+
expect: 'reject',
|
|
421
|
+
run: () =>
|
|
422
|
+
fixture(
|
|
423
|
+
'overdue',
|
|
424
|
+
12,
|
|
425
|
+
'| `legacy` | 2026-01-01 | 10.0 | 11.0 | none |',
|
|
426
|
+
['--cutting', '13.0.0'],
|
|
427
|
+
),
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
// THE CASE THE FIRST VERSION OF THIS GATE MISSED. A row committed
|
|
431
|
+
// to the major being cut is due NOW; measured against the shipped
|
|
432
|
+
// version it reads as one major early and passes, so the cut that
|
|
433
|
+
// creates the miss goes through. This case is what pins the
|
|
434
|
+
// comparand to the target.
|
|
435
|
+
name: 'a row due AT the major being cut is refused, not waved through',
|
|
436
|
+
expect: 'reject',
|
|
437
|
+
run: () =>
|
|
438
|
+
fixture(
|
|
439
|
+
'due-at-cut',
|
|
440
|
+
12,
|
|
441
|
+
'| `due_now` | 2026-01-01 | 12.0 | 13.0 | none |',
|
|
442
|
+
['--cutting', '13.0.0'],
|
|
443
|
+
),
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'the same row is silent on an ordinary branch — it is not late yet',
|
|
447
|
+
expect: 'accept',
|
|
448
|
+
run: () => fixture('due-at-cut-branch', 12, '| `due_now` | 2026-01-01 | 12.0 | 13.0 | none |'),
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
// due == SHIPPED, which is the case the contract calls silent on a
|
|
452
|
+
// branch and neither the earlier fixtures nor the self-test covered
|
|
453
|
+
// (both used due 13 against shipped 12, i.e. due == TARGET).
|
|
454
|
+
name: 'a row due at the SHIPPED major is silent on an ordinary branch',
|
|
455
|
+
expect: 'accept',
|
|
456
|
+
run: () => fixture('due-at-shipped', 12, '| `due_at_shipped` | 2026-01-01 | 11.0 | 12.0 | none |'),
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: 'the same row IS refused at the very next cut',
|
|
460
|
+
expect: 'reject',
|
|
461
|
+
run: () =>
|
|
462
|
+
fixture(
|
|
463
|
+
'due-at-shipped-cut',
|
|
464
|
+
12,
|
|
465
|
+
'| `due_at_shipped` | 2026-01-01 | 11.0 | 12.0 | none |',
|
|
466
|
+
['--cutting', '13.0.0'],
|
|
467
|
+
),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: 'a withdrawn commitment is tracked, not an unresolvable cell',
|
|
471
|
+
expect: 'accept',
|
|
472
|
+
run: () =>
|
|
473
|
+
fixture(
|
|
474
|
+
'withdrawn',
|
|
475
|
+
12,
|
|
476
|
+
'| `withdrawn_thing` | 2026-01-01 | 10.0 | **REVISED 2026-08-15 — no removal date; see below** | none |',
|
|
477
|
+
['--cutting', '13.0.0'],
|
|
478
|
+
),
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: 'a header with no Removal due column reds once, naming the header',
|
|
482
|
+
expect: 'reject',
|
|
483
|
+
run: () => {
|
|
484
|
+
const dir = path.join(root, 'renamed-header');
|
|
485
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
486
|
+
fs.writeFileSync(
|
|
487
|
+
path.join(dir, 'package.json'),
|
|
488
|
+
JSON.stringify({ name: 'fixture', version: '12.0.0' }),
|
|
489
|
+
'utf8',
|
|
490
|
+
);
|
|
491
|
+
fs.writeFileSync(
|
|
492
|
+
path.join(dir, 'docs', 'MIGRATION.md'),
|
|
493
|
+
`${header.replace('| Removal due ', '| Retirement date ')}\n` +
|
|
494
|
+
'| `a` | 2026-01-01 | 10.0 | 11.0 | none |\n' +
|
|
495
|
+
'| `b` | 2026-01-01 | 10.0 | 11.0 | none |\n',
|
|
496
|
+
'utf8',
|
|
497
|
+
);
|
|
498
|
+
return runGateCli(repo, script, [], dir);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: 'unresolvable removal-due cell reds on an ordinary branch',
|
|
503
|
+
expect: 'reject',
|
|
504
|
+
run: () => fixture('vague', 12, '| `vague` | 2026-01-01 | soon | eventually | none |'),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: 'a table parsing to zero rows reds rather than exiting green',
|
|
508
|
+
expect: 'reject',
|
|
509
|
+
run: () => {
|
|
510
|
+
const dir = path.join(root, 'dead');
|
|
511
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
512
|
+
fs.writeFileSync(
|
|
513
|
+
path.join(dir, 'package.json'),
|
|
514
|
+
JSON.stringify({ name: 'fixture', version: '12.0.0' }),
|
|
515
|
+
'utf8',
|
|
516
|
+
);
|
|
517
|
+
fs.writeFileSync(
|
|
518
|
+
path.join(dir, 'docs', 'MIGRATION.md'),
|
|
519
|
+
'# Migration Guide\n\n## Planned removals\n\n| Surface | Removal due |\n|---|---|\n| `x` | 1.0 |\n',
|
|
520
|
+
'utf8',
|
|
521
|
+
);
|
|
522
|
+
return runGateCli(repo, script, [], dir);
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: 'a future-dated row passes at a major cut',
|
|
527
|
+
expect: 'accept',
|
|
528
|
+
run: () =>
|
|
529
|
+
fixture(
|
|
530
|
+
'future',
|
|
531
|
+
12,
|
|
532
|
+
'| `later` | 2026-01-01 | 13.0 | 14.0 | none |',
|
|
533
|
+
['--cutting', '13.0.0'],
|
|
534
|
+
),
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
return runSelfTest({
|
|
540
|
+
gate: 'lint_scheduled_deprecations',
|
|
541
|
+
cases,
|
|
542
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
543
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
544
|
+
});
|
|
545
|
+
} finally {
|
|
546
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function main(argv: readonly string[]): number {
|
|
551
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
552
|
+
const quiet = argv.includes('--quiet');
|
|
553
|
+
const root = repoRoot();
|
|
554
|
+
|
|
555
|
+
// `--cutting <X.Y.Z>` — the version being released. Present = refusal mode.
|
|
556
|
+
const cutIdx = argv.indexOf('--cutting');
|
|
557
|
+
let cutting: string | undefined;
|
|
558
|
+
if (cutIdx >= 0) {
|
|
559
|
+
cutting = argv[cutIdx + 1];
|
|
560
|
+
if (cutting === undefined || cutting.startsWith('--')) {
|
|
561
|
+
process.stderr.write('usage: lint_scheduled_deprecations [--cutting <X.Y.Z>] [--quiet]\n');
|
|
562
|
+
return 2;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Shape-checked BEFORE any file work: a usage error cannot need the tree,
|
|
567
|
+
// and validating it after the parse let a HeaderMismatchError mask it with
|
|
568
|
+
// exit 1 where the caller had simply mistyped the version.
|
|
569
|
+
let cutMajor: number | undefined;
|
|
570
|
+
if (cutting !== undefined) {
|
|
571
|
+
const m = /^v?(\d+)\./.exec(cutting.trim());
|
|
572
|
+
if (!m) {
|
|
573
|
+
process.stderr.write(
|
|
574
|
+
`usage: --cutting expects a bare version (X.Y.Z), got "${cutting}"\n`,
|
|
575
|
+
);
|
|
576
|
+
return 2;
|
|
577
|
+
}
|
|
578
|
+
cutMajor = Number(m[1]);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const migration = path.join(root, MIGRATION_PATH);
|
|
582
|
+
if (!fs.existsSync(migration)) {
|
|
583
|
+
process.stderr.write(`❌ lint_scheduled_deprecations: ${MIGRATION_PATH} not found under ${root}\n`);
|
|
584
|
+
return 1;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let rows: DeprecationRow[];
|
|
588
|
+
try {
|
|
589
|
+
rows = parseRows(fs.readFileSync(migration, 'utf-8'));
|
|
590
|
+
} catch (exc) {
|
|
591
|
+
if (exc instanceof HeaderMismatchError) {
|
|
592
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
593
|
+
return 1;
|
|
594
|
+
}
|
|
595
|
+
throw exc;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// A missing or malformed package.json is an environment error, not a
|
|
599
|
+
// finding: without it the gate has no comparand at all. Reported as exit 2
|
|
600
|
+
// per the contract at the top of this file, rather than as an unlisted
|
|
601
|
+
// code and a stack trace.
|
|
602
|
+
let shipped: number;
|
|
603
|
+
try {
|
|
604
|
+
shipped = shippedMajor(root);
|
|
605
|
+
} catch (exc) {
|
|
606
|
+
process.stderr.write(
|
|
607
|
+
`❌ lint_scheduled_deprecations: cannot read the shipped major (environment, not a finding) — ` +
|
|
608
|
+
`${exc instanceof Error ? exc.message : String(exc)}\n`,
|
|
609
|
+
);
|
|
610
|
+
return 2;
|
|
611
|
+
}
|
|
612
|
+
const comparand = cutMajor ?? shipped;
|
|
613
|
+
|
|
614
|
+
const ledger = new GateLedger('lint_scheduled_deprecations');
|
|
615
|
+
const findings = evaluate(rows, comparand, root, ledger, cutting !== undefined);
|
|
616
|
+
const tally = ledger.finalize();
|
|
617
|
+
|
|
618
|
+
// Anti-vacuity. Zero rows means the heading moved or the table's format
|
|
619
|
+
// drifted — the failure this gate is about, so it is never a pass. No
|
|
620
|
+
// `allowEmpty`: an empty scheduled-deprecations table is not a success
|
|
621
|
+
// state while `MIGRATION.md` itself instructs that a promise without a row
|
|
622
|
+
// is untracked.
|
|
623
|
+
const compared = tally.completed + tally.failed;
|
|
624
|
+
try {
|
|
625
|
+
// The SCOPE assertion is on rows parsed: zero means the heading moved or
|
|
626
|
+
// the table's format drifted, which is the dead-scan-root case. How many
|
|
627
|
+
// of those rows the arithmetic actually reached is a different number,
|
|
628
|
+
// published beside it below — conflating the two is what let this gate
|
|
629
|
+
// report green over zero comparisons.
|
|
630
|
+
reportScanned({
|
|
631
|
+
gate: 'lint_scheduled_deprecations',
|
|
632
|
+
scanned: tally.planned,
|
|
633
|
+
units: 'scheduled-deprecation row(s)',
|
|
634
|
+
roots: [MIGRATION_PATH],
|
|
635
|
+
});
|
|
636
|
+
} catch (exc) {
|
|
637
|
+
if (exc instanceof DeadScopeError) {
|
|
638
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
639
|
+
return 1;
|
|
640
|
+
}
|
|
641
|
+
throw exc;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const unresolved = findings.filter((f) => f.kind === 'unresolved');
|
|
645
|
+
const overdue = findings.filter((f) => f.kind === 'overdue');
|
|
646
|
+
|
|
647
|
+
for (const f of unresolved) {
|
|
648
|
+
process.stderr.write(
|
|
649
|
+
`❌ lint_scheduled_deprecations: row ${String(f.row.index)} (${f.row.name}) — ` +
|
|
650
|
+
`removal-due cell not resolvable: "${f.row.removalDueRaw}"\n` +
|
|
651
|
+
' The grammar is documented at the top of this gate. Either write a\n' +
|
|
652
|
+
' concrete major, or record the row as not pinned, a permanent keep, or a\n' +
|
|
653
|
+
' withdrawn commitment with its reason.\n' +
|
|
654
|
+
' NOTE: the relative form ("the major after that") resolves against the\n' +
|
|
655
|
+
' NOTICE cell, and a notice is written as a date once it ships — so a row\n' +
|
|
656
|
+
' using it must be rewritten to a concrete major on notice day.\n',
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const against =
|
|
661
|
+
cutting === undefined
|
|
662
|
+
? `shipped ${String(shipped)}.x`
|
|
663
|
+
: `the ${String(comparand)}.0 cut (shipped is ${String(shipped)}.x)`;
|
|
664
|
+
|
|
665
|
+
for (const f of overdue) {
|
|
666
|
+
const late =
|
|
667
|
+
f.overdueBy === 0
|
|
668
|
+
? 'comes due AT'
|
|
669
|
+
: `is ${String(f.overdueBy)} major(s) overdue against`;
|
|
670
|
+
process.stdout.write(
|
|
671
|
+
`⚠️ lint_scheduled_deprecations: ${f.row.name} — removal ${late} ${against}\n` +
|
|
672
|
+
` removal due: ${f.row.removalDueRaw}\n`,
|
|
673
|
+
);
|
|
674
|
+
if (f.livePaths.length > 0) {
|
|
675
|
+
for (const p of f.livePaths) {
|
|
676
|
+
process.stdout.write(` still on disk: ${p}\n`);
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
process.stdout.write(
|
|
680
|
+
' no path token in the row — the surface is named but its runtime\n' +
|
|
681
|
+
' paths are not, so this gate cannot say whether they still ship.\n',
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (unresolved.length > 0) return 1;
|
|
687
|
+
|
|
688
|
+
if (overdue.length > 0) {
|
|
689
|
+
if (cutting !== undefined) {
|
|
690
|
+
process.stderr.write(
|
|
691
|
+
`\n❌ lint_scheduled_deprecations: ${String(overdue.length)} surface(s) due at or before ` +
|
|
692
|
+
`the ${String(comparand)}.0 cut.\n` +
|
|
693
|
+
' Act on each row: perform the removal in its own change, or revise the\n' +
|
|
694
|
+
" row's commitment and record why the surface stays. A deliberate\n" +
|
|
695
|
+
' deferral belongs in the table, not in a release decision nobody can read.\n',
|
|
696
|
+
);
|
|
697
|
+
return 1;
|
|
698
|
+
}
|
|
699
|
+
if (!quiet) {
|
|
700
|
+
process.stdout.write(
|
|
701
|
+
'\n Reported, not refused — this is an ordinary branch. `--cutting <X.Y.Z>`\n' +
|
|
702
|
+
' turns the same finding into a refusal, measured against the TARGET major.\n',
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return 0;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!quiet) {
|
|
709
|
+
ledger.report();
|
|
710
|
+
process.stdout.write(
|
|
711
|
+
`✅ lint_scheduled_deprecations: ${String(tally.planned)} row(s) parsed, ` +
|
|
712
|
+
`${String(compared)} compared against ${against}, none due` +
|
|
713
|
+
(compared === 0
|
|
714
|
+
? ' — every row is a tracked state (not pinned / permanent keep / withdrawn),\n' +
|
|
715
|
+
' so NO arithmetic ran this time. Green here means "nothing to compare",\n' +
|
|
716
|
+
' not "the comparison passed".\n'
|
|
717
|
+
: '.\n'),
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function isCliEntry(): boolean {
|
|
724
|
+
if (process.argv[1] === undefined) return false;
|
|
725
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
726
|
+
if (import.meta.url === argvUrl) return true;
|
|
727
|
+
try {
|
|
728
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
729
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
730
|
+
return here === argv;
|
|
731
|
+
} catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (isCliEntry()) {
|
|
737
|
+
process.exitCode = main(process.argv.slice(2));
|
|
738
|
+
}
|