@deftai/directive-content 0.55.2 → 0.56.1
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/.githooks/pre-commit +143 -0
- package/.githooks/pre-push +121 -0
- package/QUICK-START.md +2 -2
- package/Taskfile.yml +934 -0
- package/UPGRADING.md +47 -1
- package/events/README.md +3 -3
- package/package.json +5 -4
- package/scripts/_agents_md.py +494 -0
- package/scripts/_cache_fetch.py +635 -0
- package/scripts/_cache_quota.py +529 -0
- package/scripts/_cache_refresh.py +163 -0
- package/scripts/_cache_validate.py +209 -0
- package/scripts/_content_root.py +42 -0
- package/scripts/_doctor_state.py +277 -0
- package/scripts/_event_detect.py +305 -0
- package/scripts/_events.py +514 -0
- package/scripts/_lifecycle_hygiene.py +568 -0
- package/scripts/_pathspec.py +91 -0
- package/scripts/_policy_show_cli.py +266 -0
- package/scripts/_precutover.py +92 -0
- package/scripts/_project_context.py +224 -0
- package/scripts/_project_definition_io.py +164 -0
- package/scripts/_relocate_snapshot.py +209 -0
- package/scripts/_relocate_states.py +343 -0
- package/scripts/_resolve_preflight_path.py +152 -0
- package/scripts/_safe_subprocess.py +167 -0
- package/scripts/_session_start_hook.py +205 -0
- package/scripts/_sor_gate_diff.py +365 -0
- package/scripts/_stdio_utf8.py +59 -0
- package/scripts/_triage_bootstrap_gitignore.py +904 -0
- package/scripts/_triage_classify_cli.py +122 -0
- package/scripts/_triage_queue_cli.py +625 -0
- package/scripts/_triage_scope_cli.py +343 -0
- package/scripts/_triage_scope_drift_cli.py +121 -0
- package/scripts/_triage_scope_ignores.py +286 -0
- package/scripts/_triage_scope_milestone.py +432 -0
- package/scripts/_triage_scope_mutations.py +337 -0
- package/scripts/_triage_scope_renderers.py +207 -0
- package/scripts/_triage_smoketest_stages.py +674 -0
- package/scripts/_triage_subscribe_cli.py +140 -0
- package/scripts/_triage_welcome_cli.py +421 -0
- package/scripts/_vbrief_build.py +239 -0
- package/scripts/_vbrief_fidelity.py +479 -0
- package/scripts/_vbrief_legacy.py +589 -0
- package/scripts/_vbrief_reconciliation.py +883 -0
- package/scripts/_vbrief_routing.py +277 -0
- package/scripts/_vbrief_safety.py +778 -0
- package/scripts/_vbrief_sources.py +312 -0
- package/scripts/_vbrief_speckit.py +262 -0
- package/scripts/_vbrief_story_quality.py +353 -0
- package/scripts/_vbrief_validation.py +299 -0
- package/scripts/build_dist.py +412 -0
- package/scripts/cache.py +1078 -0
- package/scripts/cache_scanner.py +745 -0
- package/scripts/candidates_log.py +432 -0
- package/scripts/capacity_backfill.py +680 -0
- package/scripts/capacity_show.py +653 -0
- package/scripts/ci_local.py +689 -0
- package/scripts/code_structure_validate.py +765 -0
- package/scripts/codebase_default_extractor.py +495 -0
- package/scripts/codebase_map.py +304 -0
- package/scripts/codebase_map_fresh.py +104 -0
- package/scripts/codebase_projection_registry.py +94 -0
- package/scripts/codebase_provider.py +582 -0
- package/scripts/doctor.py +2257 -0
- package/scripts/framework_commands.py +505 -0
- package/scripts/gh_rest.py +882 -0
- package/scripts/github_auth_modes.py +437 -0
- package/scripts/github_body.py +292 -0
- package/scripts/ip_risk.py +531 -0
- package/scripts/issue_emit.py +670 -0
- package/scripts/issue_ingest.py +1064 -0
- package/scripts/migrate_preflight.py +418 -0
- package/scripts/migrate_vbrief.py +2677 -0
- package/scripts/monitor_pr.py +401 -0
- package/scripts/pack_migrate_lessons.py +336 -0
- package/scripts/pack_migrate_patterns.py +254 -0
- package/scripts/pack_migrate_rules.py +350 -0
- package/scripts/pack_migrate_skills.py +423 -0
- package/scripts/pack_migrate_strategies.py +311 -0
- package/scripts/pack_migrate_swarm_spec.py +250 -0
- package/scripts/pack_render.py +434 -0
- package/scripts/packs_slice.py +712 -0
- package/scripts/platform_capabilities.py +336 -0
- package/scripts/policy.py +2826 -0
- package/scripts/policy_set.py +324 -0
- package/scripts/pr_check_closing_keywords.py +524 -0
- package/scripts/pr_check_protected_issues.py +267 -0
- package/scripts/pr_merge_readiness.py +1004 -0
- package/scripts/pr_wait_mergeable.py +669 -0
- package/scripts/prd_render.py +159 -0
- package/scripts/preflight_architecture_sor.py +974 -0
- package/scripts/preflight_branch.py +289 -0
- package/scripts/preflight_cache.py +974 -0
- package/scripts/preflight_gh.py +721 -0
- package/scripts/preflight_implementation.py +272 -0
- package/scripts/preflight_story_start.py +838 -0
- package/scripts/preflight_wip_cap.py +149 -0
- package/scripts/probe_session.py +545 -0
- package/scripts/project_render.py +293 -0
- package/scripts/quarantine_ext.py +237 -0
- package/scripts/reconcile_issues.py +1442 -0
- package/scripts/refresh-path.ps1 +107 -0
- package/scripts/release.py +2030 -0
- package/scripts/release_e2e.py +1011 -0
- package/scripts/release_publish.py +486 -0
- package/scripts/release_rollback.py +980 -0
- package/scripts/relocate.py +1034 -0
- package/scripts/resolve_changelog_unreleased.py +667 -0
- package/scripts/resolve_version.py +490 -0
- package/scripts/resume_conditions.py +706 -0
- package/scripts/ritual_sentinel.py +609 -0
- package/scripts/roadmap_render.py +635 -0
- package/scripts/rule_ownership_lint.py +325 -0
- package/scripts/scm.py +591 -0
- package/scripts/scope_audit_log.py +387 -0
- package/scripts/scope_decompose.py +654 -0
- package/scripts/scope_demote.py +509 -0
- package/scripts/scope_lifecycle.py +1126 -0
- package/scripts/scope_undo.py +772 -0
- package/scripts/session_start.py +406 -0
- package/scripts/setup_ghx.py +339 -0
- package/scripts/setup_windows.ps1 +220 -0
- package/scripts/slice_audit.py +585 -0
- package/scripts/slice_record.py +530 -0
- package/scripts/slice_record_existing.py +692 -0
- package/scripts/slug_normalize.py +178 -0
- package/scripts/spec_render.py +477 -0
- package/scripts/spec_validate.py +238 -0
- package/scripts/subagent_monitor.py +658 -0
- package/scripts/swarm_complete_cohort.py +644 -0
- package/scripts/swarm_launch.py +1206 -0
- package/scripts/swarm_readiness.py +554 -0
- package/scripts/swarm_verify_review_clean.py +438 -0
- package/scripts/swarm_worktrees.py +497 -0
- package/scripts/toolchain-check.py +52 -0
- package/scripts/triage_actions.py +871 -0
- package/scripts/triage_bootstrap.py +1153 -0
- package/scripts/triage_bulk.py +630 -0
- package/scripts/triage_classify.py +932 -0
- package/scripts/triage_help.py +1685 -0
- package/scripts/triage_queue.py +1944 -0
- package/scripts/triage_reconcile.py +581 -0
- package/scripts/triage_refresh.py +643 -0
- package/scripts/triage_scope.py +999 -0
- package/scripts/triage_scope_drift.py +575 -0
- package/scripts/triage_smoketest.py +396 -0
- package/scripts/triage_subscribe.py +399 -0
- package/scripts/triage_summary.py +1011 -0
- package/scripts/triage_welcome.py +1178 -0
- package/scripts/ts_check_lane.py +86 -0
- package/scripts/validate-links.py +64 -0
- package/scripts/validate_strategy_output.py +212 -0
- package/scripts/vbrief_activate.py +228 -0
- package/scripts/vbrief_migrate_conformance.py +368 -0
- package/scripts/vbrief_reconcile_graph.py +306 -0
- package/scripts/vbrief_reconcile_labels.py +460 -0
- package/scripts/vbrief_reconcile_umbrellas.py +741 -0
- package/scripts/vbrief_validate.py +1195 -0
- package/scripts/verify-stubs.py +61 -0
- package/scripts/verify_capacity.py +160 -0
- package/scripts/verify_encoding.py +699 -0
- package/scripts/verify_hooks_installed.py +206 -0
- package/scripts/verify_investigation.py +360 -0
- package/scripts/verify_judgment_gates.py +827 -0
- package/scripts/verify_no_task_runtime.py +171 -0
- package/scripts/verify_scm_boundary.py +509 -0
- package/scripts/verify_session_ritual.py +389 -0
- package/scripts/verify_tools.py +426 -0
- package/scripts/verify_vbrief_conformance.py +478 -0
- package/tasks/architecture.yml +13 -0
- package/tasks/cache.yml +69 -0
- package/tasks/capacity.yml +38 -0
- package/tasks/change.yml +46 -0
- package/tasks/changelog.yml +24 -0
- package/tasks/ci.yml +49 -0
- package/tasks/codebase.yml +47 -0
- package/tasks/commit.yml +30 -0
- package/tasks/core.yml +126 -0
- package/tasks/deployments.yml +54 -0
- package/tasks/framework.yml +74 -0
- package/tasks/install.yml +60 -0
- package/tasks/issue.yml +50 -0
- package/tasks/migrate.yml +73 -0
- package/tasks/packs.yml +92 -0
- package/tasks/policy.yml +75 -0
- package/tasks/pr.yml +89 -0
- package/tasks/prd.yml +39 -0
- package/tasks/project.yml +27 -0
- package/tasks/reconcile.yml +32 -0
- package/tasks/relocate.yml +56 -0
- package/tasks/roadmap.yml +28 -0
- package/tasks/scm.yml +126 -0
- package/tasks/scope-undo.yml +36 -0
- package/tasks/scope.yml +141 -0
- package/tasks/session.yml +19 -0
- package/tasks/setup.yml +37 -0
- package/tasks/slice.yml +69 -0
- package/tasks/spec.yml +41 -0
- package/tasks/swarm.yml +85 -0
- package/tasks/toolchain.yml +13 -0
- package/tasks/triage-actions.yml +94 -0
- package/tasks/triage-bootstrap.yml +43 -0
- package/tasks/triage-bulk.yml +75 -0
- package/tasks/triage-classify.yml +30 -0
- package/tasks/triage-queue.yml +50 -0
- package/tasks/triage-reconcile.yml +29 -0
- package/tasks/triage-scope-drift.yml +29 -0
- package/tasks/triage-scope.yml +31 -0
- package/tasks/triage-smoketest.yml +33 -0
- package/tasks/triage-subscribe.yml +36 -0
- package/tasks/triage-summary.yml +29 -0
- package/tasks/triage-welcome.yml +32 -0
- package/tasks/ts.yml +328 -0
- package/tasks/vbrief.yml +206 -0
- package/tasks/verify.yml +292 -0
- package/templates/agents-entry.md +1 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""``plan.policy.triageScopeIgnores[]`` validator + resolver (D14 / #1133).
|
|
2
|
+
|
|
3
|
+
Extracted from ``scripts/triage_scope.py`` so the parent module stays
|
|
4
|
+
under the 1000-line MUST cap from ``coding/coding.md`` after D14
|
|
5
|
+
landed the milestone rule type AND this ignore-list foundation.
|
|
6
|
+
|
|
7
|
+
The public surface is re-exported by ``triage_scope`` so existing
|
|
8
|
+
call sites (``triage_scope.validate_scope_ignores``,
|
|
9
|
+
``triage_scope.resolve_scope_ignores``,
|
|
10
|
+
``triage_scope.validate_triage_scope_ignores_on_plan``) keep working
|
|
11
|
+
unchanged.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
#: Index regex used by :func:`validate_triage_scope_ignores_on_plan` to
|
|
22
|
+
#: associate each error message back to the raw entry that produced it.
|
|
23
|
+
#: The validator prefixes every error with
|
|
24
|
+
#: ``plan.policy.triageScopeIgnores[<i>]`` so the wrapper can look up the
|
|
25
|
+
#: raw entry by integer index and decide the pointer (#1133 vs #1182)
|
|
26
|
+
#: from the entry's *shape* rather than from a substring match on the
|
|
27
|
+
#: error text -- shape inspection is robust to future error-wording
|
|
28
|
+
#: edits, substring matching is not.
|
|
29
|
+
_INDEXED_ERROR_RE: re.Pattern[str] = re.compile(
|
|
30
|
+
r"^plan\.policy\.triageScopeIgnores\[(\d+)\]"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
#: Recognised single-key ignore-entry discriminator values (D14 / #1133).
|
|
34
|
+
#: Each legacy entry on ``plan.policy.triageScopeIgnores[]`` is a single-key
|
|
35
|
+
#: object: either ``{label: <name>}`` or ``{milestone: <name>}``.
|
|
36
|
+
#: D14c / #1182 adds the discriminated rule-shape entry ``{rule: <kind>,
|
|
37
|
+
#: ...}`` for kinds that cannot collapse to a single-name string -- see
|
|
38
|
+
#: :data:`VALID_IGNORE_RULES` below.
|
|
39
|
+
VALID_IGNORE_KEYS: frozenset[str] = frozenset({"label", "milestone"})
|
|
40
|
+
|
|
41
|
+
#: Recognised ``rule`` discriminator values on the D14c / #1182
|
|
42
|
+
#: rule-shaped ignore entry ``{rule: <kind>, any-of: [<str>, ...]}``.
|
|
43
|
+
#: v1 ships ``author`` only; future variants (``sunset-on``,
|
|
44
|
+
#: ``body-text``, ...) extend this set.
|
|
45
|
+
VALID_IGNORE_RULES: frozenset[str] = frozenset({"author"})
|
|
46
|
+
|
|
47
|
+
_PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_scope_ignores(ignores: Any) -> tuple[list[str], list[str]]:
|
|
51
|
+
"""Validate a ``plan.policy.triageScopeIgnores`` payload.
|
|
52
|
+
|
|
53
|
+
Returns ``(errors, warnings)``. ``errors`` is empty on success.
|
|
54
|
+
|
|
55
|
+
Two entry shapes are accepted:
|
|
56
|
+
|
|
57
|
+
* Single-key entries (D14 / #1133): ``{label: <name>}`` or
|
|
58
|
+
``{milestone: <name>}``. Value MUST be a non-empty string.
|
|
59
|
+
* Rule-shaped entries (D14c / #1182): ``{rule: <kind>, any-of:
|
|
60
|
+
[<str>, ...]}``. v1 supports ``rule: author`` only; ``any-of``
|
|
61
|
+
MUST be a non-empty list of non-empty strings.
|
|
62
|
+
|
|
63
|
+
Unrecognised top-level keys on a single-key entry, or unknown
|
|
64
|
+
``rule`` discriminators on a rule-shaped entry, surface as
|
|
65
|
+
warnings rather than errors so a forward-compat consumer's config
|
|
66
|
+
does not break on rollback.
|
|
67
|
+
"""
|
|
68
|
+
errors: list[str] = []
|
|
69
|
+
warnings: list[str] = []
|
|
70
|
+
if ignores is None:
|
|
71
|
+
return errors, warnings
|
|
72
|
+
if not isinstance(ignores, list):
|
|
73
|
+
errors.append(
|
|
74
|
+
"plan.policy.triageScopeIgnores must be a list of "
|
|
75
|
+
"{label|milestone: <name>} or {rule: <kind>, any-of: [...]} "
|
|
76
|
+
f"objects; got {type(ignores).__name__}"
|
|
77
|
+
)
|
|
78
|
+
return errors, warnings
|
|
79
|
+
for i, entry in enumerate(ignores):
|
|
80
|
+
prefix = f"plan.policy.triageScopeIgnores[{i}]"
|
|
81
|
+
if not isinstance(entry, dict):
|
|
82
|
+
errors.append(f"{prefix} must be an object, got {type(entry).__name__}")
|
|
83
|
+
continue
|
|
84
|
+
if "rule" in entry:
|
|
85
|
+
_validate_rule_ignore(entry, prefix, errors, warnings)
|
|
86
|
+
continue
|
|
87
|
+
_validate_single_key_ignore(entry, prefix, errors, warnings)
|
|
88
|
+
return errors, warnings
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _validate_single_key_ignore(
|
|
92
|
+
entry: dict[str, Any],
|
|
93
|
+
prefix: str,
|
|
94
|
+
errors: list[str],
|
|
95
|
+
warnings: list[str],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Validate a D14-era single-key ignore entry."""
|
|
98
|
+
known = sorted(k for k in entry if k in VALID_IGNORE_KEYS)
|
|
99
|
+
unknown = sorted(k for k in entry if k not in VALID_IGNORE_KEYS)
|
|
100
|
+
if not known:
|
|
101
|
+
errors.append(
|
|
102
|
+
f"{prefix} must have a 'label' / 'milestone' key OR a "
|
|
103
|
+
f"'rule' discriminator (v1 single-key keys: {sorted(VALID_IGNORE_KEYS)}; "
|
|
104
|
+
f"v1 rule kinds: {sorted(VALID_IGNORE_RULES)})"
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
if len(known) > 1:
|
|
108
|
+
errors.append(
|
|
109
|
+
f"{prefix}: 'label' and 'milestone' are mutually exclusive"
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
if unknown:
|
|
113
|
+
warnings.append(
|
|
114
|
+
f"{prefix}: ignoring unrecognised keys {unknown} "
|
|
115
|
+
"(forward-compat: future ignore-entry variants will surface here)"
|
|
116
|
+
)
|
|
117
|
+
key = known[0]
|
|
118
|
+
value = entry.get(key)
|
|
119
|
+
if not isinstance(value, str) or not value.strip():
|
|
120
|
+
errors.append(f"{prefix}.{key} must be a non-empty string")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _validate_rule_ignore(
|
|
124
|
+
entry: dict[str, Any],
|
|
125
|
+
prefix: str,
|
|
126
|
+
errors: list[str],
|
|
127
|
+
warnings: list[str],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Validate a D14c / #1182 rule-shaped ignore entry."""
|
|
130
|
+
kind = entry.get("rule")
|
|
131
|
+
if not isinstance(kind, str) or not kind.strip():
|
|
132
|
+
errors.append(f"{prefix}.rule must be a non-empty string")
|
|
133
|
+
return
|
|
134
|
+
if kind not in VALID_IGNORE_RULES:
|
|
135
|
+
# The wrapper :func:`validate_triage_scope_ignores_on_plan` appends
|
|
136
|
+
# the canonical ``(#1182)`` pointer; do NOT inline it here or the
|
|
137
|
+
# error renders with two conflicting pointers on one line.
|
|
138
|
+
errors.append(
|
|
139
|
+
f"{prefix}.rule {kind!r} is not a recognised ignore-rule "
|
|
140
|
+
f"kind; expected one of {sorted(VALID_IGNORE_RULES)}"
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
# Per-kind body shape. v1 ships ``author`` only; the ``any-of``
|
|
144
|
+
# contract is shared so future kinds can re-use this validator.
|
|
145
|
+
if kind == "author":
|
|
146
|
+
any_of = entry.get("any-of")
|
|
147
|
+
if not isinstance(any_of, list) or not any_of:
|
|
148
|
+
errors.append(
|
|
149
|
+
f"{prefix}.author requires 'any-of' as a non-empty list "
|
|
150
|
+
"of GitHub login strings (e.g. ['dependabot[bot]'])"
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
for j, name in enumerate(any_of):
|
|
154
|
+
if not isinstance(name, str) or not name.strip():
|
|
155
|
+
errors.append(
|
|
156
|
+
f"{prefix}.author.any-of[{j}] must be a non-empty string"
|
|
157
|
+
)
|
|
158
|
+
extra = sorted(k for k in entry if k not in {"rule", "any-of"})
|
|
159
|
+
if extra:
|
|
160
|
+
warnings.append(
|
|
161
|
+
f"{prefix}.author: ignoring unrecognised keys {extra} "
|
|
162
|
+
"(forward-compat: future author-rule variants will surface here)"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_project_definition(project_root: Path | None) -> dict[str, Any] | None:
|
|
167
|
+
"""Load PROJECT-DEFINITION.vbrief.json (None on missing/malformed)."""
|
|
168
|
+
root = project_root or Path.cwd()
|
|
169
|
+
path = root / _PROJECT_DEFINITION_REL_PATH
|
|
170
|
+
if not path.is_file():
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
174
|
+
except (json.JSONDecodeError, OSError):
|
|
175
|
+
return None
|
|
176
|
+
return data if isinstance(data, dict) else None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_scope_ignores(
|
|
180
|
+
project_root: Path | None = None,
|
|
181
|
+
*,
|
|
182
|
+
project_definition: dict[str, Any] | None = None,
|
|
183
|
+
) -> dict[str, set[str]]:
|
|
184
|
+
"""Return ``{'labels', 'milestones', 'authors'}`` sets from PROJECT-DEFINITION.
|
|
185
|
+
|
|
186
|
+
Used by the drift detector to suppress label / milestone / author
|
|
187
|
+
signals the operator explicitly chose to ignore. Unset / missing /
|
|
188
|
+
non-list yields empty sets (the framework default is to surface
|
|
189
|
+
every drift signal until the operator opts out).
|
|
190
|
+
|
|
191
|
+
The ``authors`` key was added in D14c (#1182) when the rule-shaped
|
|
192
|
+
``{rule: author, any-of: [<login>, ...]}`` ignore entry shipped;
|
|
193
|
+
callers MUST tolerate the new key even when they only consume
|
|
194
|
+
labels / milestones.
|
|
195
|
+
"""
|
|
196
|
+
data = (
|
|
197
|
+
project_definition
|
|
198
|
+
if project_definition is not None
|
|
199
|
+
else _load_project_definition(project_root)
|
|
200
|
+
)
|
|
201
|
+
out: dict[str, set[str]] = {
|
|
202
|
+
"labels": set(),
|
|
203
|
+
"milestones": set(),
|
|
204
|
+
"authors": set(),
|
|
205
|
+
}
|
|
206
|
+
if not isinstance(data, dict):
|
|
207
|
+
return out
|
|
208
|
+
plan = data.get("plan")
|
|
209
|
+
if not isinstance(plan, dict):
|
|
210
|
+
return out
|
|
211
|
+
policy = plan.get("policy")
|
|
212
|
+
if not isinstance(policy, dict):
|
|
213
|
+
return out
|
|
214
|
+
raw = policy.get("triageScopeIgnores")
|
|
215
|
+
if not isinstance(raw, list):
|
|
216
|
+
return out
|
|
217
|
+
for entry in raw:
|
|
218
|
+
if not isinstance(entry, dict):
|
|
219
|
+
continue
|
|
220
|
+
# D14 single-key shape.
|
|
221
|
+
label = entry.get("label")
|
|
222
|
+
if isinstance(label, str) and label.strip():
|
|
223
|
+
out["labels"].add(label)
|
|
224
|
+
milestone = entry.get("milestone")
|
|
225
|
+
if isinstance(milestone, str) and milestone.strip():
|
|
226
|
+
out["milestones"].add(milestone)
|
|
227
|
+
# D14c rule-shaped entries.
|
|
228
|
+
rule = entry.get("rule")
|
|
229
|
+
if rule == "author":
|
|
230
|
+
any_of = entry.get("any-of")
|
|
231
|
+
if isinstance(any_of, list):
|
|
232
|
+
for name in any_of:
|
|
233
|
+
if isinstance(name, str) and name.strip():
|
|
234
|
+
out["authors"].add(name)
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def validate_triage_scope_ignores_on_plan(plan: Any, filepath: Any) -> list[str]:
|
|
239
|
+
"""vbrief_validate hook: validate ``plan.policy.triageScopeIgnores`` (#1133 / #1182).
|
|
240
|
+
|
|
241
|
+
Returns formatted error strings prefixed with ``<filepath>:`` so
|
|
242
|
+
``vbrief_validate.validate_project_definition`` can splice them in.
|
|
243
|
+
Unset / missing payload returns an empty list. Errors carry the
|
|
244
|
+
``(#1133)`` pointer for D14 single-key shape errors and ``(#1182)``
|
|
245
|
+
for any error on a D14c rule-shape entry (``{rule: <kind>, ...}``).
|
|
246
|
+
|
|
247
|
+
The pointer is resolved by inspecting each entry's *shape* (presence
|
|
248
|
+
of a top-level ``rule`` key) rather than substring-matching the
|
|
249
|
+
error text -- substring heuristics were fragile against rule-key
|
|
250
|
+
error messages that did not happen to mention ``author`` (e.g.
|
|
251
|
+
``{rule: ""}`` -> ``...rule must be a non-empty string``,
|
|
252
|
+
``{rule: "sunset-on"}`` -> ``...rule 'sunset-on' is not a
|
|
253
|
+
recognised ignore-rule kind...``). The shape check is robust to
|
|
254
|
+
future error-wording edits AND covers the entire rule-shape error
|
|
255
|
+
surface uniformly.
|
|
256
|
+
"""
|
|
257
|
+
out: list[str] = []
|
|
258
|
+
policy = plan.get("policy") if isinstance(plan, dict) else None
|
|
259
|
+
raw = policy.get("triageScopeIgnores") if isinstance(policy, dict) else None
|
|
260
|
+
if raw is None:
|
|
261
|
+
return out
|
|
262
|
+
errors, _warnings = validate_scope_ignores(raw)
|
|
263
|
+
raw_list = raw if isinstance(raw, list) else []
|
|
264
|
+
for err in errors:
|
|
265
|
+
out.append(f"{filepath}: {err} ({_pointer_for_error(err, raw_list)})")
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _pointer_for_error(err: str, raw_list: list[Any]) -> str:
|
|
270
|
+
"""Resolve the issue-tracker pointer for a single validator error.
|
|
271
|
+
|
|
272
|
+
Strategy: extract the entry index ``[i]`` from the canonical error
|
|
273
|
+
prefix and look the entry up in ``raw_list``. An entry carrying a
|
|
274
|
+
top-level ``rule`` key is a D14c rule-shape entry (#1182); anything
|
|
275
|
+
else is a D14 single-key entry (#1133). Errors with no parseable
|
|
276
|
+
index (e.g. the top-level ``must be a list`` error) fall through to
|
|
277
|
+
#1133.
|
|
278
|
+
"""
|
|
279
|
+
match = _INDEXED_ERROR_RE.match(err)
|
|
280
|
+
if match is not None:
|
|
281
|
+
idx = int(match.group(1))
|
|
282
|
+
if 0 <= idx < len(raw_list):
|
|
283
|
+
entry = raw_list[idx]
|
|
284
|
+
if isinstance(entry, dict) and "rule" in entry:
|
|
285
|
+
return "#1182"
|
|
286
|
+
return "#1133"
|