@event4u/agent-config 16.0.0 → 16.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +297 -137
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +85 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +1832 -1007
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2007 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +398 -39
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gate: a major that ships BREAKING CHANGES owes `docs/MIGRATION.md` a section.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `lint_scheduled_deprecations` is the FORWARD half of the
|
|
6
|
+
* release pre-flight's MIGRATION.md obligation — commitments due at a future
|
|
7
|
+
* major. The backward half had nothing at all behind it, and it failed exactly
|
|
8
|
+
* the way an unchecked obligation does: 15.0.0 and 16.0.0 both shipped
|
|
9
|
+
* BREAKING CHANGES, neither had a migration section, and the gap survived
|
|
10
|
+
* seventeen separate reports before anything in the tree could see it.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT REQUIRES, AND WHAT IT DELIBERATELY DOES NOT. A major section in a
|
|
13
|
+
* changelog carrying at least one `### BREAKING CHANGES` entry must have a
|
|
14
|
+
* `## ` heading in `docs/MIGRATION.md` naming that version. That is the whole
|
|
15
|
+
* requirement: a STATED ANSWER, never a non-empty procedure. Not every
|
|
16
|
+
* breaking change asks something of a consumer, and a gate that could not
|
|
17
|
+
* express "this asks nothing of you" would turn into a ritual heading with no
|
|
18
|
+
* content — which is worse than no gate, because the heading would be
|
|
19
|
+
* believed. The 16.0.0 section this gate ships beside says exactly that, and
|
|
20
|
+
* passes.
|
|
21
|
+
*
|
|
22
|
+
* THE FLOOR, AND WHY IT IS A CONSTANT RATHER THAN `package.json`. Majors below
|
|
23
|
+
* {@link FLOOR_MAJOR} are out of scope. The obvious alternative — grandfather
|
|
24
|
+
* everything at or below the shipped version, the cutoff `lint_changelog_rollback`
|
|
25
|
+
* uses — would leave this gate comparing ZERO majors on the day it lands, which
|
|
26
|
+
* is the "a gate that scans nothing exits green" shape. A constant floor set at
|
|
27
|
+
* the oldest major this repository actually carries a section for means the gate
|
|
28
|
+
* does real arithmetic from its first run (two majors today, both passing) and
|
|
29
|
+
* a reader can see which historical majors were forgiven and why.
|
|
30
|
+
*
|
|
31
|
+
* EXIT CONTRACT
|
|
32
|
+
* 0 every in-scope major has a section, or none is in scope for a reason
|
|
33
|
+
* the output names
|
|
34
|
+
* 1 an in-scope major carries BREAKING CHANGES and has no section, or the
|
|
35
|
+
* scan root is dead
|
|
36
|
+
* 2 usage error
|
|
37
|
+
*
|
|
38
|
+
* `--root <dir>` drives the whole check against a fixture tree, which is the
|
|
39
|
+
* seam `--self-test` and the unit suite use. `--self-test` proves the rule
|
|
40
|
+
* against the real CLI on inline fixtures.
|
|
41
|
+
*/
|
|
42
|
+
import * as fs from 'node:fs';
|
|
43
|
+
import * as os from 'node:os';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
48
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
49
|
+
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
50
|
+
|
|
51
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
52
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
53
|
+
|
|
54
|
+
export const GATE = 'lint_major_migration_sections';
|
|
55
|
+
export const MIGRATION_PATH = path.join('docs', 'MIGRATION.md');
|
|
56
|
+
const CHANGELOG_PATH = 'CHANGELOG.md';
|
|
57
|
+
const ARCHIVE_DIR = path.join('docs', 'archive');
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Oldest major this gate holds to the requirement.
|
|
61
|
+
*
|
|
62
|
+
* 15 because 15.0.0 is the oldest major `docs/MIGRATION.md` carries a section
|
|
63
|
+
* for. Everything below it shipped before the obligation existed and is
|
|
64
|
+
* forgiven on the record rather than silently: retro-failing eight majors
|
|
65
|
+
* nobody can now write an honest section for is the gate-lands-as-N-blockers
|
|
66
|
+
* failure this repository has refused before. Raise it only by deleting a
|
|
67
|
+
* section, which is not a move this gate should make cheap.
|
|
68
|
+
*/
|
|
69
|
+
export const FLOOR_MAJOR = 15;
|
|
70
|
+
|
|
71
|
+
const MAJOR_HEADING_RE = /^## \[(\d+)\.(\d+)\.(\d+)\]/;
|
|
72
|
+
const ANY_SECTION_RE = /^#{1,2} /;
|
|
73
|
+
const BREAKING_HEADING_RE = /^### BREAKING CHANGES\s*$/;
|
|
74
|
+
const SUB_HEADING_RE = /^### /;
|
|
75
|
+
const BULLET_RE = /^\*\s+\S/;
|
|
76
|
+
|
|
77
|
+
/** One major section found in a changelog file. */
|
|
78
|
+
export interface MajorSection {
|
|
79
|
+
/** Bare `X.0.0`. */
|
|
80
|
+
readonly version: string;
|
|
81
|
+
readonly major: number;
|
|
82
|
+
/** Repo-relative file the heading was read from. */
|
|
83
|
+
readonly source: string;
|
|
84
|
+
/** 1-based heading line. */
|
|
85
|
+
readonly line: number;
|
|
86
|
+
/** BREAKING CHANGES bullets under the heading. */
|
|
87
|
+
readonly breaking: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Collect every `## [X.0.0](…)` section and how many BREAKING bullets it has.
|
|
92
|
+
*
|
|
93
|
+
* Minor and patch sections are not returned at all: the obligation is stated
|
|
94
|
+
* per MAJOR, because that is the boundary a consumer plans an upgrade around
|
|
95
|
+
* and the boundary the release runbook already stops at.
|
|
96
|
+
*/
|
|
97
|
+
export function findMajorSections(text: string, source: string): MajorSection[] {
|
|
98
|
+
const lines = text.split('\n');
|
|
99
|
+
const out: MajorSection[] = [];
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const m = MAJOR_HEADING_RE.exec(lines[i] as string);
|
|
102
|
+
if (m === null) continue;
|
|
103
|
+
if (m[2] !== '0' || m[3] !== '0') continue;
|
|
104
|
+
let breaking = 0;
|
|
105
|
+
let inBreaking = false;
|
|
106
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
107
|
+
const line = lines[j] as string;
|
|
108
|
+
if (ANY_SECTION_RE.test(line)) break;
|
|
109
|
+
if (BREAKING_HEADING_RE.test(line)) {
|
|
110
|
+
inBreaking = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (inBreaking && SUB_HEADING_RE.test(line)) {
|
|
114
|
+
inBreaking = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (inBreaking && BULLET_RE.test(line)) breaking += 1;
|
|
118
|
+
}
|
|
119
|
+
out.push({
|
|
120
|
+
version: `${m[1] as string}.0.0`,
|
|
121
|
+
major: Number(m[1]),
|
|
122
|
+
source,
|
|
123
|
+
line: i + 1,
|
|
124
|
+
breaking,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Versions named by a `## ` heading in `docs/MIGRATION.md`.
|
|
132
|
+
*
|
|
133
|
+
* The heading text is matched on a whole version token, so both shapes the
|
|
134
|
+
* file already uses are accepted — `## 16.0.0 — …` and `## 8.x → 9.0.0 — …` —
|
|
135
|
+
* without this gate dictating a heading grammar the file did not choose.
|
|
136
|
+
*/
|
|
137
|
+
export function migrationVersions(text: string): Set<string> {
|
|
138
|
+
const out = new Set<string>();
|
|
139
|
+
for (const line of text.split('\n')) {
|
|
140
|
+
if (!line.startsWith('## ')) continue;
|
|
141
|
+
for (const m of line.matchAll(/(?<![\d.])(\d+)\.0\.0(?![\d.])/g)) {
|
|
142
|
+
out.add(`${m[1] as string}.0.0`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface Finding {
|
|
149
|
+
readonly section: MajorSection;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read every changelog file under `root`, newest-era file first. */
|
|
153
|
+
export function changelogSources(root: string): string[] {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
if (fs.existsSync(path.join(root, CHANGELOG_PATH))) out.push(CHANGELOG_PATH);
|
|
156
|
+
const archive = path.join(root, ARCHIVE_DIR);
|
|
157
|
+
if (fs.existsSync(archive)) {
|
|
158
|
+
for (const name of fs.readdirSync(archive).sort()) {
|
|
159
|
+
if (name.startsWith('CHANGELOG-') && name.endsWith('.md')) {
|
|
160
|
+
out.push(path.join(ARCHIVE_DIR, name));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface EvaluateResult {
|
|
168
|
+
readonly findings: Finding[];
|
|
169
|
+
readonly sections: MajorSection[];
|
|
170
|
+
readonly inScope: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compare the majors found against the sections MIGRATION.md carries.
|
|
175
|
+
*
|
|
176
|
+
* Every section reaches exactly one ledger outcome: out of scope below the
|
|
177
|
+
* floor, out of scope with no BREAKING entries, completed when a heading
|
|
178
|
+
* exists, failed when it does not.
|
|
179
|
+
*/
|
|
180
|
+
export function evaluate(
|
|
181
|
+
sections: readonly MajorSection[],
|
|
182
|
+
migration: ReadonlySet<string>,
|
|
183
|
+
ledger: GateLedger,
|
|
184
|
+
): EvaluateResult {
|
|
185
|
+
const findings: Finding[] = [];
|
|
186
|
+
let inScope = 0;
|
|
187
|
+
for (const s of sections) {
|
|
188
|
+
const target = `${s.source}:${s.version}`;
|
|
189
|
+
ledger.plan(target);
|
|
190
|
+
if (s.major < FLOOR_MAJOR) {
|
|
191
|
+
ledger.outOfScope(target, 'declared_exemption');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (s.breaking === 0) {
|
|
195
|
+
ledger.outOfScope(target, 'not_applicable_kind');
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
inScope += 1;
|
|
199
|
+
if (migration.has(s.version)) {
|
|
200
|
+
ledger.complete(target);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
ledger.fail(target, `no docs/MIGRATION.md section for ${s.version}`);
|
|
204
|
+
findings.push({ section: s });
|
|
205
|
+
}
|
|
206
|
+
return { findings, sections: [...sections], inScope };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The cut-time question, answered against an entry that is not on disk yet.
|
|
211
|
+
*
|
|
212
|
+
* `release.ts` renders the changelog entry before prepending it, so at the
|
|
213
|
+
* moment the cut can still be refused the section exists only in memory. This
|
|
214
|
+
* takes that text directly rather than re-reading `CHANGELOG.md`, which at that
|
|
215
|
+
* point still describes the PREVIOUS release.
|
|
216
|
+
*
|
|
217
|
+
* @returns a refusal reason naming the version, or `null` when the cut is clear.
|
|
218
|
+
*/
|
|
219
|
+
export function pendingMajorFinding(
|
|
220
|
+
target: string,
|
|
221
|
+
changelogEntry: string,
|
|
222
|
+
migrationText: string,
|
|
223
|
+
): string | null {
|
|
224
|
+
const m = /^(\d+)\.0\.0$/.exec(target.trim());
|
|
225
|
+
if (m === null) return null;
|
|
226
|
+
const sections = findMajorSections(changelogEntry, '(pending entry)');
|
|
227
|
+
const breaking = sections.reduce((n, s) => n + s.breaking, 0);
|
|
228
|
+
if (breaking === 0) return null;
|
|
229
|
+
if (Number(m[1]) < FLOOR_MAJOR) return null;
|
|
230
|
+
if (migrationVersions(migrationText).has(target)) return null;
|
|
231
|
+
return (
|
|
232
|
+
`the ${target} entry carries ${String(breaking)} BREAKING CHANGES entr(y/ies) and ` +
|
|
233
|
+
`docs/MIGRATION.md has no "## " heading naming ${target}. Add one before the cut — ` +
|
|
234
|
+
'a section stating that the change asks nothing of a consumer satisfies this, and is ' +
|
|
235
|
+
'the right answer when it is true. What is not acceptable is silence.'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function repoRoot(): string {
|
|
240
|
+
return REPO_ROOT;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const SELF_TEST_MIN_CASES = 6;
|
|
244
|
+
const SELF_TEST_MIN_REJECT = 3;
|
|
245
|
+
|
|
246
|
+
/** Prove the rejections still fire against the real CLI, not against imports. */
|
|
247
|
+
function selfTest(): number {
|
|
248
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lmms-selftest-'));
|
|
249
|
+
const repo = repoRoot();
|
|
250
|
+
const script = path.join('src', 'scripts', 'lint_major_migration_sections.ts');
|
|
251
|
+
|
|
252
|
+
const changelog = (version: string, breakingBullets: readonly string[]): string =>
|
|
253
|
+
[
|
|
254
|
+
'# Changelog',
|
|
255
|
+
'',
|
|
256
|
+
`## [${version}](https://example.invalid/compare) (2026-01-01)`,
|
|
257
|
+
'',
|
|
258
|
+
...(breakingBullets.length > 0
|
|
259
|
+
? ['### BREAKING CHANGES', '', ...breakingBullets.map((b) => `* ${b}`), '']
|
|
260
|
+
: []),
|
|
261
|
+
'### Features',
|
|
262
|
+
'',
|
|
263
|
+
'* something additive',
|
|
264
|
+
'',
|
|
265
|
+
].join('\n');
|
|
266
|
+
|
|
267
|
+
const fixture = (
|
|
268
|
+
name: string,
|
|
269
|
+
changelogText: string,
|
|
270
|
+
migrationHeadings: readonly string[],
|
|
271
|
+
): number => {
|
|
272
|
+
const dir = path.join(root, name);
|
|
273
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
274
|
+
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), changelogText, 'utf8');
|
|
275
|
+
fs.writeFileSync(
|
|
276
|
+
path.join(dir, 'docs', 'MIGRATION.md'),
|
|
277
|
+
['# Migration Guide', '', ...migrationHeadings.flatMap((h) => [h, '', 'body', ''])].join(
|
|
278
|
+
'\n',
|
|
279
|
+
),
|
|
280
|
+
'utf8',
|
|
281
|
+
);
|
|
282
|
+
return runGateCli(repo, script, ['--root', dir, '--quiet'], repo);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const cases: SelfTestCase[] = [
|
|
286
|
+
{
|
|
287
|
+
name: 'a major with BREAKING and no MIGRATION heading is refused',
|
|
288
|
+
expect: 'reject',
|
|
289
|
+
run: () => fixture('missing', changelog('17.0.0', ['**x:** drop y']), []),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
// The same fixture with the heading added. This pair IS the gate:
|
|
293
|
+
// one half proves the refusal fires, the other that the heading is
|
|
294
|
+
// what clears it, so neither can be satisfied by a gate that always
|
|
295
|
+
// reds or always passes.
|
|
296
|
+
name: 'the same major passes once the heading exists',
|
|
297
|
+
expect: 'accept',
|
|
298
|
+
run: () =>
|
|
299
|
+
fixture('present', changelog('17.0.0', ['**x:** drop y']), [
|
|
300
|
+
'## 17.0.0 — what to do',
|
|
301
|
+
]),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'a heading naming a DIFFERENT major does not satisfy the requirement',
|
|
305
|
+
expect: 'reject',
|
|
306
|
+
run: () =>
|
|
307
|
+
fixture('wrong-version', changelog('17.0.0', ['**x:** drop y']), [
|
|
308
|
+
'## 16.0.0 — what to do',
|
|
309
|
+
]),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'an arrow-form heading naming the version satisfies it',
|
|
313
|
+
expect: 'accept',
|
|
314
|
+
run: () =>
|
|
315
|
+
fixture('arrow', changelog('17.0.0', ['**x:** drop y']), [
|
|
316
|
+
'## 16.x → 17.0.0 — what to do',
|
|
317
|
+
]),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'a major BELOW the floor is forgiven',
|
|
321
|
+
expect: 'accept',
|
|
322
|
+
run: () => fixture('below-floor', changelog('9.0.0', ['**x:** drop y']), []),
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'a major with no BREAKING entries owes nothing',
|
|
326
|
+
expect: 'accept',
|
|
327
|
+
run: () => fixture('no-breaking', changelog('17.0.0', []), []),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
// The anti-vacuity half. A tree with no changelog at all must not
|
|
331
|
+
// read as "every major has its section"; a gate that greens over an
|
|
332
|
+
// empty corpus is the failure `_lib/scan_scope.ts` exists for.
|
|
333
|
+
name: 'a tree with no changelog is a dead scan root, not a pass',
|
|
334
|
+
expect: 'reject',
|
|
335
|
+
run: () => {
|
|
336
|
+
const dir = path.join(root, 'no-changelog');
|
|
337
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
338
|
+
fs.writeFileSync(path.join(dir, 'docs', 'MIGRATION.md'), '# Migration Guide\n', 'utf8');
|
|
339
|
+
return runGateCli(repo, script, ['--root', dir, '--quiet'], repo);
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
return runSelfTest({
|
|
346
|
+
gate: GATE,
|
|
347
|
+
cases,
|
|
348
|
+
minCases: SELF_TEST_MIN_CASES,
|
|
349
|
+
minRejectCases: SELF_TEST_MIN_REJECT,
|
|
350
|
+
});
|
|
351
|
+
} finally {
|
|
352
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function main(argv: readonly string[]): number {
|
|
357
|
+
let root = REPO_ROOT;
|
|
358
|
+
let quiet = false;
|
|
359
|
+
for (let i = 0; i < argv.length; i++) {
|
|
360
|
+
const a = argv[i];
|
|
361
|
+
if (a === '--quiet') quiet = true;
|
|
362
|
+
else if (a === '--self-test') return selfTest();
|
|
363
|
+
else if (a === '--root') {
|
|
364
|
+
const next = argv[++i];
|
|
365
|
+
if (next === undefined || next.startsWith('--')) {
|
|
366
|
+
process.stderr.write('usage: --root expects a directory\n');
|
|
367
|
+
return 2;
|
|
368
|
+
}
|
|
369
|
+
root = path.resolve(next);
|
|
370
|
+
} else {
|
|
371
|
+
process.stderr.write(
|
|
372
|
+
`usage: ${GATE} [--root <dir>] [--quiet] [--self-test]\nunknown argument: ${String(a)}\n`,
|
|
373
|
+
);
|
|
374
|
+
return 2;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const migrationFile = path.join(root, MIGRATION_PATH);
|
|
379
|
+
if (!fs.existsSync(migrationFile)) {
|
|
380
|
+
process.stderr.write(`❌ ${GATE}: ${MIGRATION_PATH} not found under ${root}\n`);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
const migration = migrationVersions(fs.readFileSync(migrationFile, 'utf-8'));
|
|
384
|
+
|
|
385
|
+
const sources = changelogSources(root);
|
|
386
|
+
const sections: MajorSection[] = [];
|
|
387
|
+
for (const rel of sources) {
|
|
388
|
+
sections.push(...findMajorSections(fs.readFileSync(path.join(root, rel), 'utf-8'), rel));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const ledger = new GateLedger(GATE);
|
|
392
|
+
const result = evaluate(sections, migration, ledger);
|
|
393
|
+
const tally = ledger.finalize();
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// No `allowEmpty`. Zero major sections means the heading shape drifted
|
|
397
|
+
// or the changelog moved — blindness, not cleanliness, and this
|
|
398
|
+
// repository has shipped majors continuously since 1.x.
|
|
399
|
+
reportScanned({
|
|
400
|
+
gate: GATE,
|
|
401
|
+
scanned: tally.planned,
|
|
402
|
+
units: 'major changelog section(s)',
|
|
403
|
+
roots: [CHANGELOG_PATH, ARCHIVE_DIR],
|
|
404
|
+
});
|
|
405
|
+
} catch (exc) {
|
|
406
|
+
if (exc instanceof DeadScopeError) {
|
|
407
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
throw exc;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (result.findings.length > 0) {
|
|
414
|
+
for (const f of result.findings) {
|
|
415
|
+
process.stderr.write(
|
|
416
|
+
`❌ ${GATE}: ${f.section.version} ships ${String(f.section.breaking)} BREAKING ` +
|
|
417
|
+
`CHANGES entr(y/ies) (${f.section.source}:${String(f.section.line)}) and ` +
|
|
418
|
+
`${MIGRATION_PATH} carries no "## " heading naming it.\n` +
|
|
419
|
+
` Add a section for ${f.section.version}. A section stating that the change\n` +
|
|
420
|
+
' asks nothing of a consumer satisfies this and is the right answer when it\n' +
|
|
421
|
+
' is true — the requirement is a stated answer, never a procedure.\n',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!quiet) {
|
|
428
|
+
ledger.report();
|
|
429
|
+
process.stdout.write(
|
|
430
|
+
`✅ ${GATE}: ${String(tally.planned)} major section(s) read, ` +
|
|
431
|
+
`${String(result.inScope)} in scope (major >= ${String(FLOOR_MAJOR)} with BREAKING ` +
|
|
432
|
+
'CHANGES), each with a migration section' +
|
|
433
|
+
(result.inScope === 0
|
|
434
|
+
? ' — NONE in scope this run, so no comparison ran. Green here means\n' +
|
|
435
|
+
' "nothing to compare", not "the comparison passed".\n'
|
|
436
|
+
: '.\n'),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function _isCliEntry(): boolean {
|
|
443
|
+
const entry = process.argv[1];
|
|
444
|
+
if (entry === undefined) return false;
|
|
445
|
+
return import.meta.url === pathToFileURL(path.resolve(entry)).href;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (_isCliEntry()) {
|
|
449
|
+
process.exit(main(process.argv.slice(2)));
|
|
450
|
+
}
|
|
@@ -588,6 +588,46 @@ function _blobAt(absPath: string, sha: string): string | null {
|
|
|
588
588
|
return _git(root, ['show', `${sha}:${rel}`]);
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
+
/** The `reviewed:` value carried by `text`, or null when it has no valid marker. */
|
|
592
|
+
function _reviewedIn(text: string): string | null {
|
|
593
|
+
for (const line of text.split('\n')) {
|
|
594
|
+
const m = parseMarker(line);
|
|
595
|
+
if (m !== null) return m.reviewed;
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The commit that INTRODUCED the current `reviewed:` value — the oldest of the
|
|
602
|
+
* unbroken run of commits, counting back from the newest, that all carry it.
|
|
603
|
+
*
|
|
604
|
+
* Merge-date lag is why this exists. A review stamped on the day the work was
|
|
605
|
+
* authored but committed (or squash-merged) the next day puts the stamp and the
|
|
606
|
+
* content it reviewed in ONE commit whose date is later than the stamp itself.
|
|
607
|
+
* A baseline of "newest commit dated <= reviewed" then skips that very commit,
|
|
608
|
+
* falls back to the pre-work state, and reports stale against a plan that was
|
|
609
|
+
* reviewed exactly as the contract asks — every author-today/merge-tomorrow PR
|
|
610
|
+
* reproduces it.
|
|
611
|
+
*
|
|
612
|
+
* The oldest commit of the run is the honest baseline, and the age matters:
|
|
613
|
+
* comparing against the NEWEST commit carrying the stamp would compare the
|
|
614
|
+
* current content with itself, making every committed file trivially fresh and
|
|
615
|
+
* emptying the gate.
|
|
616
|
+
*/
|
|
617
|
+
function _stampIntroducedAt(
|
|
618
|
+
absPath: string,
|
|
619
|
+
history: HistoryEntry[],
|
|
620
|
+
reviewed: string,
|
|
621
|
+
): HistoryEntry | undefined {
|
|
622
|
+
let introduced: HistoryEntry | undefined;
|
|
623
|
+
for (const entry of history) {
|
|
624
|
+
const blob = _blobAt(absPath, entry.sha);
|
|
625
|
+
if (blob === null || _reviewedIn(blob) !== reviewed) break;
|
|
626
|
+
introduced = entry;
|
|
627
|
+
}
|
|
628
|
+
return introduced;
|
|
629
|
+
}
|
|
630
|
+
|
|
591
631
|
export type FileStatus = 'ok' | 'draft-exempt' | 'grandfathered' | 'fail';
|
|
592
632
|
|
|
593
633
|
export interface FileResult {
|
|
@@ -653,10 +693,14 @@ export function checkFile(absPath: string): FileResult {
|
|
|
653
693
|
// current content, so staleness is not judgeable — see
|
|
654
694
|
// hasUncommittedChanges. The committed-history case below is unchanged.
|
|
655
695
|
if (content.reviewed !== null && !hasUncommittedChanges(absPath)) {
|
|
656
|
-
// Staleness (contract §§ 1.1 + 3): compare features of the
|
|
657
|
-
//
|
|
696
|
+
// Staleness (contract §§ 1.1 + 3): compare the features of the version
|
|
697
|
+
// the review was stamped on against the current content. That version
|
|
698
|
+
// is the commit which INTRODUCED the current `reviewed:` value; the
|
|
699
|
+
// date lookup is the fallback for a stamp no commit carries yet.
|
|
658
700
|
const history = fileHistory(absPath);
|
|
659
|
-
const base =
|
|
701
|
+
const base =
|
|
702
|
+
_stampIntroducedAt(absPath, history, content.reviewed) ??
|
|
703
|
+
history.find((h) => h.date <= (content.reviewed as string));
|
|
660
704
|
if (base !== undefined) {
|
|
661
705
|
const blob = _blobAt(absPath, base.sha);
|
|
662
706
|
if (blob !== null && !featuresEqual(extractFeatures(blob), extractFeatures(text))) {
|
|
@@ -28,6 +28,19 @@
|
|
|
28
28
|
* contract above makes. `later/` and `archive/` are outside this gate's
|
|
29
29
|
* glob and are untouched by it.
|
|
30
30
|
*
|
|
31
|
+
* 5. An entry that declares `- **Ownership:**` declares one of the three
|
|
32
|
+
* OWNER-OWNED classes (`product-owned`, `business-owned`,
|
|
33
|
+
* `destructive-owned`). A technical class there is a judgement call
|
|
34
|
+
* parked in a file instead of closed, which is the shape ADR-268 § 10
|
|
35
|
+
* retires: a technical decision does not become owner-owned because it is
|
|
36
|
+
* hard, so it routes back through the closure pass rather than into
|
|
37
|
+
* `## Blockers`.
|
|
38
|
+
*
|
|
39
|
+
* HARD rather than ratcheted, on the same "no backlog to grandfather"
|
|
40
|
+
* argument the `Class:` contract makes: `Ownership:` is a new opt-in
|
|
41
|
+
* field, so on the day it ships no entry in the tree declares one and the
|
|
42
|
+
* rule fires on nothing.
|
|
43
|
+
*
|
|
31
44
|
* Fenced code blocks are stripped before scanning so a roadmap that shows
|
|
32
45
|
* the `## Blockers` shape as a documentation example is not flagged.
|
|
33
46
|
*
|
|
@@ -107,6 +120,29 @@ const KNOWN_CLASSES: ReadonlySet<string> = new Set(['0', '1', '2', '3']);
|
|
|
107
120
|
/** The classes whose whole claim is that an agent can execute them. */
|
|
108
121
|
const RUNNABLE_CLASSES: ReadonlySet<string> = new Set(['0', '1']);
|
|
109
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The ownership axis, as a blocker may declare it.
|
|
125
|
+
*
|
|
126
|
+
* Only the three owner-owned classes are legal here. A blocker IS the record
|
|
127
|
+
* of a decision the agent correctly did not own; a technical class in that
|
|
128
|
+
* field says the opposite — that something the ownership ladder can close was
|
|
129
|
+
* filed instead. The remaining five ownership classes are deliberately absent
|
|
130
|
+
* rather than listed as "also accepted".
|
|
131
|
+
*/
|
|
132
|
+
const OWNERSHIP_FIELD_RE = /^-[ \t]*\*\*Ownership:\*\*[ \t]*`?([a-z-]+)`?/im;
|
|
133
|
+
const OWNER_OWNED_CLASSES: ReadonlySet<string> = new Set([
|
|
134
|
+
'product-owned',
|
|
135
|
+
'business-owned',
|
|
136
|
+
'destructive-owned',
|
|
137
|
+
]);
|
|
138
|
+
const TECHNICAL_CLASSES: ReadonlySet<string> = new Set([
|
|
139
|
+
'deterministic',
|
|
140
|
+
'reversible-technical',
|
|
141
|
+
'contested-technical',
|
|
142
|
+
'critical-technical',
|
|
143
|
+
'spend-exhaustion',
|
|
144
|
+
]);
|
|
145
|
+
|
|
110
146
|
/**
|
|
111
147
|
* The authored class, or `''` when the entry declares none.
|
|
112
148
|
*
|
|
@@ -248,6 +284,28 @@ function _scanBoth(rawText: string): ScanResult {
|
|
|
248
284
|
});
|
|
249
285
|
}
|
|
250
286
|
}
|
|
287
|
+
const ownership = OWNERSHIP_FIELD_RE.exec(body);
|
|
288
|
+
if (ownership !== null) {
|
|
289
|
+
const declared = ownership[1] as string;
|
|
290
|
+
if (TECHNICAL_CLASSES.has(declared)) {
|
|
291
|
+
violations.push({
|
|
292
|
+
line: _lineAt(text, sectionStart + cur.start),
|
|
293
|
+
message:
|
|
294
|
+
`blocker '${cur.id}' declares ownership '${declared}', which is ` +
|
|
295
|
+
'a technical class — a technical decision does not become ' +
|
|
296
|
+
'owner-owned because it is hard (ADR-268 § 10). Route it back ' +
|
|
297
|
+
'through the closure pass and record the answer as a ' +
|
|
298
|
+
'`## Decisions` row instead of parking it here',
|
|
299
|
+
});
|
|
300
|
+
} else if (!OWNER_OWNED_CLASSES.has(declared)) {
|
|
301
|
+
violations.push({
|
|
302
|
+
line: _lineAt(text, sectionStart + cur.start),
|
|
303
|
+
message:
|
|
304
|
+
`blocker '${cur.id}' declares unknown ownership '${declared}' ` +
|
|
305
|
+
'(expected product-owned, business-owned or destructive-owned)',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
251
309
|
const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
|
|
252
310
|
([name]) => name,
|
|
253
311
|
);
|
|
@@ -628,10 +686,13 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
628
686
|
export {
|
|
629
687
|
REPO_ROOT,
|
|
630
688
|
ROADMAP_GLOB,
|
|
689
|
+
_stripFencedCode,
|
|
631
690
|
REQUIRED_FIELDS,
|
|
632
691
|
DECIDABILITY_FIELDS,
|
|
633
692
|
KNOWN_CLASSES,
|
|
634
693
|
RUNNABLE_CLASSES,
|
|
694
|
+
OWNER_OWNED_CLASSES,
|
|
695
|
+
TECHNICAL_CLASSES,
|
|
635
696
|
_blockerClass,
|
|
636
697
|
_hasExecutableSubstance,
|
|
637
698
|
_scan,
|