@ikon85/agent-workflow-kit 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ask-matt/SKILL.md +139 -0
- package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/audit-skills/SKILL.md +145 -0
- package/.agents/skills/board-to-waves/SKILL.md +135 -0
- package/.agents/skills/code-review/SKILL.md +64 -0
- package/.agents/skills/codebase-design/DEEPENING.md +37 -0
- package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.agents/skills/codebase-design/SKILL.md +114 -0
- package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
- package/.agents/skills/decision-gate/SKILL.md +67 -0
- package/.agents/skills/diagnose/SKILL.md +136 -0
- package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +76 -0
- package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
- package/.agents/skills/grill-me/SKILL.md +54 -0
- package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/grill-with-docs/SKILL.md +137 -0
- package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/implement/SKILL.md +15 -0
- package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/kit-release/SKILL.md +56 -0
- package/.agents/skills/local-ci/SKILL.md +94 -0
- package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
- package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.agents/skills/prototype/LOGIC.md +79 -0
- package/.agents/skills/prototype/SKILL.md +30 -0
- package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.agents/skills/research/SKILL.md +12 -0
- package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/retro/SKILL.md +209 -0
- package/.agents/skills/scale-check/SKILL.md +141 -0
- package/.agents/skills/security-audit/SKILL.md +67 -0
- package/.agents/skills/setup-workflow/SKILL.md +187 -0
- package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/setup-workflow/board-sync.md +111 -0
- package/.agents/skills/setup-workflow/code-review.md +26 -0
- package/.agents/skills/setup-workflow/domain.md +51 -0
- package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.agents/skills/setup-workflow/triage-labels.md +18 -0
- package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
- package/.agents/skills/spec-self-critique/SKILL.md +152 -0
- package/.agents/skills/spec-self-critique/scenarios.md +82 -0
- package/.agents/skills/tdd/SKILL.md +120 -0
- package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/tdd/deep-modules.md +33 -0
- package/.agents/skills/tdd/interface-design.md +31 -0
- package/.agents/skills/tdd/mocking.md +59 -0
- package/.agents/skills/tdd/refactoring.md +10 -0
- package/.agents/skills/tdd/tests.md +61 -0
- package/.agents/skills/to-issues/SKILL.md +325 -0
- package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.agents/skills/to-prd/SKILL.md +180 -0
- package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/to-waves/SKILL.md +260 -0
- package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
- package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.agents/skills/triage/SKILL.md +122 -0
- package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/verify-spike/SKILL.md +39 -0
- package/.agents/skills/wayfinder/SKILL.md +127 -0
- package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.agents/skills/wrapup/SKILL.md +71 -0
- package/.agents/skills/wrapup/SOURCES.txt +6 -0
- package/.agents/skills/zoom-out/SKILL.md +7 -0
- package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/hooks/_hook_utils.py +184 -0
- package/.claude/hooks/drift-guard.py +176 -0
- package/.claude/hooks/skill-drift-hint.py +122 -0
- package/.claude/hooks/sync-board-status.py +191 -0
- package/.claude/skills/ask-matt/SKILL.md +139 -0
- package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/audit-skills/SKILL.md +145 -0
- package/.claude/skills/board-to-waves/SKILL.md +148 -0
- package/.claude/skills/code-review/SKILL.md +64 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/codex-build/SKILL.md +126 -0
- package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
- package/.claude/skills/codex-review/SKILL.md +145 -0
- package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
- package/.claude/skills/decision-gate/SKILL.md +67 -0
- package/.claude/skills/diagnose/SKILL.md +136 -0
- package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/domain-modeling/SKILL.md +76 -0
- package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
- package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
- package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
- package/.claude/skills/grill-me/SKILL.md +54 -0
- package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-me-codex/SKILL.md +158 -0
- package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
- package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs/SKILL.md +137 -0
- package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
- package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
- package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
- package/.claude/skills/implement/SKILL.md +15 -0
- package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
- package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
- package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
- package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
- package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/kit-release/SKILL.md +56 -0
- package/.claude/skills/local-ci/SKILL.md +94 -0
- package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
- package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
- package/.claude/skills/prototype/LOGIC.md +79 -0
- package/.claude/skills/prototype/SKILL.md +30 -0
- package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/prototype/UI.md +112 -0
- package/.claude/skills/research/SKILL.md +12 -0
- package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/retro/SKILL.md +209 -0
- package/.claude/skills/scale-check/SKILL.md +141 -0
- package/.claude/skills/security-audit/SKILL.md +67 -0
- package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
- package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
- package/.claude/skills/setup-workflow/SKILL.md +187 -0
- package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/setup-workflow/board-sync.md +111 -0
- package/.claude/skills/setup-workflow/code-review.md +26 -0
- package/.claude/skills/setup-workflow/domain.md +51 -0
- package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
- package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
- package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
- package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
- package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
- package/.claude/skills/setup-workflow/triage-labels.md +18 -0
- package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
- package/.claude/skills/skill-manifest.json +389 -0
- package/.claude/skills/spec-self-critique/SKILL.md +152 -0
- package/.claude/skills/spec-self-critique/scenarios.md +82 -0
- package/.claude/skills/tdd/SKILL.md +120 -0
- package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/tdd/deep-modules.md +33 -0
- package/.claude/skills/tdd/interface-design.md +31 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/refactoring.md +10 -0
- package/.claude/skills/tdd/tests.md +61 -0
- package/.claude/skills/to-issues/SKILL.md +325 -0
- package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
- package/.claude/skills/to-prd/SKILL.md +180 -0
- package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/to-waves/SKILL.md +260 -0
- package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
- package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
- package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
- package/.claude/skills/triage/SKILL.md +122 -0
- package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/verify-spike/SKILL.md +39 -0
- package/.claude/skills/wayfinder/SKILL.md +127 -0
- package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/wrapup/SKILL.md +81 -0
- package/.claude/skills/wrapup/SOURCES.txt +6 -0
- package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
- package/.claude/skills/write-a-skill/SKILL.md +88 -0
- package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
- package/.claude/skills/zoom-out/SKILL.md +7 -0
- package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
- package/LICENSE +28 -0
- package/PROVENANCE.md +39 -0
- package/README.md +619 -0
- package/agent-workflow-kit.package.json +1939 -0
- package/docs/agents/board-sync.md +71 -0
- package/docs/agents/code-review.md +14 -0
- package/docs/agents/domain.md +7 -0
- package/docs/agents/issue-tracker.md +12 -0
- package/docs/agents/security-audit-runbook-template.md +205 -0
- package/docs/agents/skills/orchestrate-wave.md +13 -0
- package/docs/agents/skills/spec-self-critique.md +18 -0
- package/docs/agents/triage-labels.md +10 -0
- package/docs/agents/wave-anchor-template.md +88 -0
- package/docs/conventions/spec-completeness.md +23 -0
- package/docs/index.html +56 -0
- package/docs/methodology.html +488 -0
- package/docs/methodology.svg +93 -0
- package/docs/workflow.png +0 -0
- package/package.json +40 -0
- package/scripts/anchor_table.py +343 -0
- package/scripts/board-sync.py +1232 -0
- package/scripts/board_config.py +147 -0
- package/scripts/board_fields.py +192 -0
- package/scripts/build-kit.mjs +116 -0
- package/scripts/build-kit.test.mjs +59 -0
- package/scripts/check-kit-staleness.mjs +45 -0
- package/scripts/check-kit-staleness.test.mjs +19 -0
- package/scripts/execute-ready-check.py +516 -0
- package/scripts/issue_deps.py +79 -0
- package/scripts/kit-release.mjs +100 -0
- package/scripts/kit-release.test.mjs +67 -0
- package/scripts/lib/audit-refs.mjs +146 -0
- package/scripts/lib/scrub.mjs +113 -0
- package/scripts/lib/scrub.test.mjs +187 -0
- package/scripts/loc_offender_core.py +196 -0
- package/scripts/loc_offender_gate.py +182 -0
- package/scripts/node_kind.py +68 -0
- package/scripts/portability_profile_scan.py +53 -0
- package/scripts/pr-body-check.py +263 -0
- package/scripts/program_graph.py +125 -0
- package/scripts/program_graph_parse.py +275 -0
- package/scripts/program_graph_validate.py +256 -0
- package/scripts/program_sync.py +160 -0
- package/scripts/release-delta-guard.mjs +96 -0
- package/scripts/release-delta-guard.test.mjs +53 -0
- package/scripts/test_board_sync_create_idempotency.py +104 -0
- package/scripts/test_dist_kit_smoke.py +199 -0
- package/scripts/test_kit_docs_language_census.py +178 -0
- package/scripts/test_skill_code_review_seed.py +89 -0
- package/scripts/test_skill_frontmatter_lint.py +226 -0
- package/scripts/test_skill_gh_lint.py +124 -0
- package/scripts/test_skill_language_census.py +254 -0
- package/scripts/test_skill_portability_lint.py +773 -0
- package/scripts/test_skill_publish_audit.py +300 -0
- package/scripts/test_skill_selfcontainment_lint.py +255 -0
- package/scripts/test_skill_setup_workflow_seeds.py +200 -0
- package/scripts/test_skill_stale_name_lint.py +126 -0
- package/scripts/test_skill_surface_refs.py +300 -0
- package/scripts/test_skill_trailing_artifact_lint.py +71 -0
- package/scripts/wrapup-land.py +573 -0
- package/src/cli.mjs +70 -0
- package/src/commands/diff.mjs +6 -0
- package/src/commands/init.mjs +59 -0
- package/src/commands/uninstall.mjs +43 -0
- package/src/commands/update.mjs +95 -0
- package/src/lib/atomicWrite.mjs +49 -0
- package/src/lib/bundle.mjs +133 -0
- package/src/lib/hash.mjs +13 -0
- package/src/lib/manifest.mjs +58 -0
- package/src/lib/sentinel.mjs +20 -0
- package/src/lib/settings.mjs +23 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""LoC-offender drive gate — PURE decision core.
|
|
3
|
+
|
|
4
|
+
The decision logic + structured Loc-Defer parsing. No I/O. The git/subprocess
|
|
5
|
+
adapter + CLI live in loc_offender_gate.py, which imports this module. Splitting
|
|
6
|
+
keeps each file <= the 300-line size gate this very slice enforces.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
# Band thresholds (changed = added + deleted LoC of the offender file in the slice).
|
|
13
|
+
EXEMPT_LOC = 5 # <= this: trivial touch, exempt
|
|
14
|
+
SUBSTANTIAL_LOC = 30 # >= this: substantial → shrink-or-defer owed; 6..29 = warn
|
|
15
|
+
|
|
16
|
+
DEFER_REASONS = {"grossfile-partial", "risky-split", "vendored-adjacent", "hotfix"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class FileChange:
|
|
21
|
+
"""One file's change in the slice diff (base..head)."""
|
|
22
|
+
|
|
23
|
+
path: str # current (post-rename) repo-root POSIX path
|
|
24
|
+
old_path: str | None # pre-rename path, or None if not renamed
|
|
25
|
+
changed_loc: int # added + deleted
|
|
26
|
+
head_lines: int | None # current line count; None if the file was deleted
|
|
27
|
+
base_lines: int # line count at the slice base
|
|
28
|
+
binary: bool = False # numstat reported '-'/'-' (unparseable)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Defer:
|
|
33
|
+
"""A parsed, well-formed Loc-Defer trailer."""
|
|
34
|
+
|
|
35
|
+
file: str
|
|
36
|
+
reason: str
|
|
37
|
+
followup: str | None # e.g. ''
|
|
38
|
+
reduced_net: int | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Verdict:
|
|
43
|
+
ok: bool = True
|
|
44
|
+
reds: list[str] = field(default_factory=list)
|
|
45
|
+
warnings: list[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def exit_code(self) -> int:
|
|
49
|
+
return 0 if self.ok else 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalize_defer_path(raw: str):
|
|
53
|
+
"""Return (path, error). repo-root POSIX path, leading './' stripped.
|
|
54
|
+
Absolute paths and parent traversal ('..') are rejected (defer must name a
|
|
55
|
+
repo-relative offender, never escape the tree)."""
|
|
56
|
+
p = (raw or "").strip().replace("\\", "/")
|
|
57
|
+
if not p:
|
|
58
|
+
return None, "leerer file-Pfad"
|
|
59
|
+
if p.startswith("/"):
|
|
60
|
+
return None, f"absoluter Pfad nicht erlaubt: {p}"
|
|
61
|
+
while p.startswith("./"):
|
|
62
|
+
p = p[2:]
|
|
63
|
+
if p == ".." or p.startswith("../") or "/../" in p or p.endswith("/.."):
|
|
64
|
+
return None, f"Pfad-Traversal ('..') nicht erlaubt: {p}"
|
|
65
|
+
return p, None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_one(value: str):
|
|
69
|
+
"""Parse one `file=…; reason=…; followup=…; reduced_net=…` trailer value.
|
|
70
|
+
Return (Defer, error). Syntactic validity only (enum, required-field rule,
|
|
71
|
+
path); the reduced_net-vs-reality check is semantic → done in evaluate()."""
|
|
72
|
+
fields: dict[str, str] = {}
|
|
73
|
+
for part in value.split(";"):
|
|
74
|
+
part = part.strip()
|
|
75
|
+
if not part:
|
|
76
|
+
continue
|
|
77
|
+
if "=" not in part:
|
|
78
|
+
return None, f"malformter Trailer-Teil (kein key=value): {part!r}"
|
|
79
|
+
k, _, val = part.partition("=")
|
|
80
|
+
fields[k.strip()] = val.strip()
|
|
81
|
+
|
|
82
|
+
raw_file = fields.get("file")
|
|
83
|
+
if not raw_file:
|
|
84
|
+
return None, f"Loc-Defer ohne file=: {value!r}"
|
|
85
|
+
path, perr = normalize_defer_path(raw_file)
|
|
86
|
+
if perr:
|
|
87
|
+
return None, perr
|
|
88
|
+
|
|
89
|
+
reason = fields.get("reason")
|
|
90
|
+
if reason not in DEFER_REASONS:
|
|
91
|
+
return None, f"unbekannter reason={reason!r} (erlaubt: {sorted(DEFER_REASONS)})"
|
|
92
|
+
|
|
93
|
+
followup = fields.get("followup") or None
|
|
94
|
+
reduced_raw = fields.get("reduced_net")
|
|
95
|
+
reduced_net = None
|
|
96
|
+
if reduced_raw is not None:
|
|
97
|
+
if not reduced_raw.lstrip("#").isdigit() or int(reduced_raw.lstrip("#")) <= 0:
|
|
98
|
+
return None, f"reduced_net muss positive Ganzzahl sein, war {reduced_raw!r}"
|
|
99
|
+
reduced_net = int(reduced_raw)
|
|
100
|
+
|
|
101
|
+
if followup is None and reduced_net is None:
|
|
102
|
+
return None, f"Loc-Defer für {path} braucht followup=#n ODER reduced_net>0"
|
|
103
|
+
|
|
104
|
+
return Defer(file=path, reason=reason, followup=followup, reduced_net=reduced_net), None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_loc_defer_trailers(values):
|
|
108
|
+
"""Parse all `Loc-Defer:` trailer values from the slice commits.
|
|
109
|
+
Return (defers, errors). A duplicate `file=` (same offender twice) is an
|
|
110
|
+
error — ambiguous intent → fail-closed."""
|
|
111
|
+
defers: list[Defer] = []
|
|
112
|
+
errors: list[str] = []
|
|
113
|
+
seen: set[str] = set()
|
|
114
|
+
for value in values:
|
|
115
|
+
defer, err = _parse_one(value)
|
|
116
|
+
if err:
|
|
117
|
+
errors.append(err)
|
|
118
|
+
continue
|
|
119
|
+
if defer.file in seen:
|
|
120
|
+
errors.append(f"doppelter Loc-Defer für {defer.file} — mehrdeutig.")
|
|
121
|
+
continue
|
|
122
|
+
seen.add(defer.file)
|
|
123
|
+
defers.append(defer)
|
|
124
|
+
return defers, errors
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _defer_covers(defer: Defer, change: FileChange) -> tuple[bool, str | None]:
|
|
128
|
+
"""Semantic check: does this (already syntactically-valid) defer actually cover
|
|
129
|
+
the change? Returns (ok, error). reduced_net may not over-declare the real
|
|
130
|
+
reduction; grossfile-partial needs reduced_net >= changed OR a followup."""
|
|
131
|
+
actual_reduction = (change.base_lines - change.head_lines) if change.head_lines is not None else change.base_lines
|
|
132
|
+
if defer.reduced_net is not None and defer.reduced_net > actual_reduction:
|
|
133
|
+
return False, (
|
|
134
|
+
f"{defer.file}: reduced_net={defer.reduced_net} über-deklariert "
|
|
135
|
+
f"(reale Reduktion {actual_reduction}).")
|
|
136
|
+
if defer.reason == "grossfile-partial":
|
|
137
|
+
meets_reduced = defer.reduced_net is not None and defer.reduced_net >= change.changed_loc
|
|
138
|
+
if not meets_reduced and not defer.followup:
|
|
139
|
+
return False, (
|
|
140
|
+
f"{defer.file}: grossfile-partial verlangt reduced_net >= "
|
|
141
|
+
f"{change.changed_loc} (changed) ODER followup.")
|
|
142
|
+
return True, None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def offender_key(change: FileChange) -> str:
|
|
146
|
+
"""The baseline path a change is matched against: the OLD path on a rename
|
|
147
|
+
(the offender was listed under its pre-rename path), else the current path."""
|
|
148
|
+
return change.old_path or change.path
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def evaluate(changes, offenders, defers, max_lines, defer_errors=None) -> Verdict:
|
|
152
|
+
"""Pure decision over the slice diff. Red if any substantial offender touch
|
|
153
|
+
is neither shrunk nor validly deferred. A non-empty `defer_errors` (malformed/
|
|
154
|
+
duplicate trailers from parse_loc_defer_trailers) is a global fail-closed red."""
|
|
155
|
+
v = Verdict()
|
|
156
|
+
for err in defer_errors or []:
|
|
157
|
+
v.ok = False
|
|
158
|
+
v.reds.append(f"malformter Loc-Defer-Trailer → fail-closed: {err}")
|
|
159
|
+
by_key = {d.file: d for d in defers}
|
|
160
|
+
for ch in changes:
|
|
161
|
+
key = offender_key(ch)
|
|
162
|
+
if key not in offenders:
|
|
163
|
+
continue
|
|
164
|
+
# Binary/unparseable numstat is checked FIRST: collect_changes reports
|
|
165
|
+
# changed_loc=0 for a binary file, so a band check would exempt it before
|
|
166
|
+
# this defensive fail-closed could fire. (Should never hit for .ts/.tsx.)
|
|
167
|
+
if ch.binary:
|
|
168
|
+
v.ok = False
|
|
169
|
+
v.reds.append(f"{key}: binäres/unparsebares numstat für einen Offender → fail-closed.")
|
|
170
|
+
continue
|
|
171
|
+
if ch.changed_loc <= EXEMPT_LOC:
|
|
172
|
+
continue
|
|
173
|
+
if ch.changed_loc < SUBSTANTIAL_LOC:
|
|
174
|
+
v.warnings.append(
|
|
175
|
+
f"{key}: {ch.changed_loc} LoC geändert (6–{SUBSTANTIAL_LOC - 1} = "
|
|
176
|
+
"Warnung) — beim nächsten substanziellen Touch schrumpfen.")
|
|
177
|
+
continue
|
|
178
|
+
# Substantial touch (>= SUBSTANTIAL_LOC).
|
|
179
|
+
if ch.head_lines is None: # deleted = best possible shrink
|
|
180
|
+
continue
|
|
181
|
+
if ch.head_lines <= max_lines or ch.head_lines < ch.base_lines:
|
|
182
|
+
continue # under the limit, or net-shrunk → green (prune enforced by)
|
|
183
|
+
# Still > max_lines, no net shrink → needs a valid defer.
|
|
184
|
+
defer = by_key.get(key)
|
|
185
|
+
if defer is not None:
|
|
186
|
+
covers, derr = _defer_covers(defer, ch)
|
|
187
|
+
if covers:
|
|
188
|
+
continue
|
|
189
|
+
v.ok = False
|
|
190
|
+
v.reds.append(derr)
|
|
191
|
+
continue
|
|
192
|
+
v.ok = False
|
|
193
|
+
v.reds.append(
|
|
194
|
+
f"{key} ({ch.head_lines}>{max_lines}) ≥{SUBSTANTIAL_LOC} LoC angefasst, "
|
|
195
|
+
"nicht geschrumpft, kein Loc-Defer.")
|
|
196
|
+
return v
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""LoC-offender drive gate — git/subprocess adapter + CLI.
|
|
3
|
+
|
|
4
|
+
Thin shell over the pure core in loc_offender_core.py. Invoked by .githooks/pre-push
|
|
5
|
+
as `loc_offender_gate.py --check --head <local_sha> [--base-ref <ref>]`, once per
|
|
6
|
+
non-delete pushed branch ref. Exit 1 = red (push blocked). Pure logic + its tests
|
|
7
|
+
live in loc_offender_core.py.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from loc_offender_core import (
|
|
18
|
+
FileChange,
|
|
19
|
+
Verdict,
|
|
20
|
+
evaluate,
|
|
21
|
+
parse_loc_defer_trailers,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class GateError(RuntimeError):
|
|
25
|
+
"""A condition the gate cannot evaluate → fail-closed (exit 1)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _git(args, cwd=None) -> str:
|
|
29
|
+
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
|
30
|
+
if r.returncode != 0:
|
|
31
|
+
raise GateError(f"git {' '.join(args)}: {r.stderr.strip()}")
|
|
32
|
+
return r.stdout
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _ref_exists(ref: str, cwd=None) -> bool:
|
|
36
|
+
return subprocess.run(["git", "rev-parse", "--verify", "--quiet", ref],
|
|
37
|
+
cwd=cwd, capture_output=True).returncode == 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_base_ref(explicit, cwd=None) -> str:
|
|
41
|
+
"""origin/HEAD → origin/main → fail-closed. An explicit --base-ref wins if valid."""
|
|
42
|
+
if explicit:
|
|
43
|
+
if _ref_exists(explicit, cwd):
|
|
44
|
+
return explicit
|
|
45
|
+
raise GateError(f"--base-ref {explicit!r} nicht auflösbar.")
|
|
46
|
+
head = subprocess.run(["git", "rev-parse", "--abbrev-ref", "origin/HEAD"],
|
|
47
|
+
cwd=cwd, capture_output=True, text=True)
|
|
48
|
+
if head.returncode == 0 and head.stdout.strip():
|
|
49
|
+
return head.stdout.strip()
|
|
50
|
+
if _ref_exists("origin/main", cwd):
|
|
51
|
+
return "origin/main"
|
|
52
|
+
raise GateError("keine base-ref auflösbar (origin/HEAD, origin/main fehlen) → fail-closed.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_max_lines(path=None):
|
|
56
|
+
"""Read maxLines + offenders set from max-lines-allowlist.json (the single SSOT).
|
|
57
|
+
|
|
58
|
+
Missing file / malformed JSON / missing keys → GateError, not a bare
|
|
59
|
+
exception — run_gate's `except GateError` already turns that into a normal
|
|
60
|
+
red gate message (with the SKIP_CI_GUARDS escape hatch) instead of a raw
|
|
61
|
+
traceback escaping the gate.
|
|
62
|
+
"""
|
|
63
|
+
p = Path(path) if path else _repo_root() / "max-lines-allowlist.json"
|
|
64
|
+
try:
|
|
65
|
+
raw = json.loads(p.read_text(encoding="utf-8"))
|
|
66
|
+
return int(raw["maxLines"]), set(raw["offenders"])
|
|
67
|
+
except GateError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise GateError(f"max-lines-allowlist.json unlesbar/ungültig ({p}): {exc}") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _repo_root() -> Path:
|
|
74
|
+
return Path(_git(["rev-parse", "--show-toplevel"]).strip())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _count_lines_at(rev: str, path: str, cwd=None):
|
|
78
|
+
"""wc -l style count of <rev>:<path>; None if the path does not exist there."""
|
|
79
|
+
r = subprocess.run(["git", "show", f"{rev}:{path}"], cwd=cwd, capture_output=True, text=True)
|
|
80
|
+
if r.returncode != 0:
|
|
81
|
+
return None
|
|
82
|
+
body = r.stdout
|
|
83
|
+
if body == "":
|
|
84
|
+
return 0
|
|
85
|
+
return len(body[:-1].split("\n")) if body.endswith("\n") else len(body.split("\n"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_numstat_z(raw: str):
|
|
89
|
+
"""Parse `git diff --numstat -z --find-renames` into (added, deleted, old_path, path).
|
|
90
|
+
-z record: `A\\tD\\tpath\\0` normally; `A\\tD\\t\\0from\\0to\\0` for renames;
|
|
91
|
+
`-` for added/deleted on binary files."""
|
|
92
|
+
toks = raw.split("\0")
|
|
93
|
+
i, out = 0, []
|
|
94
|
+
while i < len(toks):
|
|
95
|
+
field = toks[i]
|
|
96
|
+
if field == "":
|
|
97
|
+
i += 1
|
|
98
|
+
continue
|
|
99
|
+
added, deleted, inline = (field.split("\t") + ["", "", ""])[:3]
|
|
100
|
+
if inline == "": # rename → next two tokens are from, to
|
|
101
|
+
old_path, path = toks[i + 1], toks[i + 2]
|
|
102
|
+
i += 3
|
|
103
|
+
else:
|
|
104
|
+
old_path, path = None, inline
|
|
105
|
+
i += 1
|
|
106
|
+
out.append((added, deleted, old_path, path))
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def collect_changes(base: str, head: str, offenders, cwd=None):
|
|
111
|
+
"""Build FileChange entries for diff files that touch an offender (matched by
|
|
112
|
+
old path on rename). Non-offender files are skipped (no git line-count calls)."""
|
|
113
|
+
raw = _git(["diff", "--numstat", "-z", "--find-renames", base, head], cwd=cwd)
|
|
114
|
+
changes = []
|
|
115
|
+
for added, deleted, old_path, path in parse_numstat_z(raw):
|
|
116
|
+
key = old_path or path
|
|
117
|
+
if key not in offenders:
|
|
118
|
+
continue
|
|
119
|
+
binary = added == "-" or deleted == "-"
|
|
120
|
+
changed = 0 if binary else int(added) + int(deleted)
|
|
121
|
+
base_lines = _count_lines_at(base, key, cwd) or 0
|
|
122
|
+
head_lines = _count_lines_at(head, path, cwd)
|
|
123
|
+
changes.append(FileChange(path=path, old_path=old_path, changed_loc=changed,
|
|
124
|
+
head_lines=head_lines, base_lines=base_lines, binary=binary))
|
|
125
|
+
return changes
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def collect_defer_values(base: str, head: str, cwd=None):
|
|
129
|
+
"""All `Loc-Defer:` trailer values across the slice commits (real trailer block,
|
|
130
|
+
via git's own parser — not a %B body grep)."""
|
|
131
|
+
out = _git(["log", f"{base}..{head}",
|
|
132
|
+
"--format=%(trailers:key=Loc-Defer,valueonly,unfold)"], cwd=cwd)
|
|
133
|
+
return [ln.strip() for ln in out.splitlines() if ln.strip()]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
DEFAULT_MAX_LINES = 300 # only the fallback when the allowlist itself is unreadable
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_gate(head: str, base_ref=None, cwd=None):
|
|
140
|
+
"""Resolve base, collect the slice diff + defers, evaluate. GateError → red.
|
|
141
|
+
Returns (Verdict, max_lines) so the caller's repair message needs no re-read."""
|
|
142
|
+
max_lines = DEFAULT_MAX_LINES
|
|
143
|
+
try:
|
|
144
|
+
ref = resolve_base_ref(base_ref, cwd)
|
|
145
|
+
base = _git(["merge-base", ref, head], cwd=cwd).strip()
|
|
146
|
+
max_lines, offenders = load_max_lines()
|
|
147
|
+
changes = collect_changes(base, head, offenders, cwd)
|
|
148
|
+
defers, errors = parse_loc_defer_trailers(collect_defer_values(base, head, cwd))
|
|
149
|
+
return evaluate(changes, offenders, defers, max_lines, defer_errors=errors), max_lines
|
|
150
|
+
except GateError as exc:
|
|
151
|
+
v = Verdict(ok=False)
|
|
152
|
+
v.reds.append(f"{exc} → fail-closed (Notausgang: SKIP_CI_GUARDS=1 git push).")
|
|
153
|
+
return v, max_lines
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
_REPAIR = (
|
|
157
|
+
"→ schrumpfen (≤{max}) ODER Aufschub:\n"
|
|
158
|
+
' git commit --amend --trailer "Loc-Defer: file=<pfad>; reason=grossfile-partial; followup=#NNN"\n'
|
|
159
|
+
"→ Notausgang: SKIP_CI_GUARDS=1 git push")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main(argv=None) -> int:
|
|
163
|
+
ap = argparse.ArgumentParser(prog="loc_offender_gate.py", description=__doc__.splitlines()[0])
|
|
164
|
+
ap.add_argument("--check", action="store_true", help="run the gate (exit 1 = red)")
|
|
165
|
+
ap.add_argument("--head", required=True, help="pushed commit SHA (the slice head)")
|
|
166
|
+
ap.add_argument("--base-ref", dest="base_ref", help="integration base ref (default: origin/HEAD→origin/main)")
|
|
167
|
+
args = ap.parse_args(argv)
|
|
168
|
+
|
|
169
|
+
v, max_lines = run_gate(args.head, args.base_ref)
|
|
170
|
+
for w in v.warnings:
|
|
171
|
+
print(f"⚠ LoC-Offender: {w}")
|
|
172
|
+
if v.ok:
|
|
173
|
+
return 0
|
|
174
|
+
print("🔴 LoC-Offender-Gate — Push blockiert:")
|
|
175
|
+
for r in v.reds:
|
|
176
|
+
print(f" {r}")
|
|
177
|
+
print(_REPAIR.format(max=max_lines))
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
sys.exit(main())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""node_kind.py — pure node-kind classifier shared by execute-ready-check.py
|
|
3
|
+
.
|
|
4
|
+
|
|
5
|
+
execute-ready-check.py used to infer "is this an anchor?" structurally (no
|
|
6
|
+
native parent + has children). That heuristic breaks the moment an anchor gets
|
|
7
|
+
a NATIVE parent of its own — a Welle-Anker promoted under a Programm-PRD, or
|
|
8
|
+
the Programm-PRD itself: the checker would climb to the parent and audit the
|
|
9
|
+
anchor as if it were a plain leaf, forcing a bucket + rev-match on itself and
|
|
10
|
+
on its sibling anchors (which don't carry a bucket by design).
|
|
11
|
+
|
|
12
|
+
This module replaces that heuristic with an EXPLICIT classification, read off
|
|
13
|
+
the node's own labels/body — never off its position in the graph:
|
|
14
|
+
|
|
15
|
+
program — `<!-- prd: program -->` body marker. Highest precedence (a
|
|
16
|
+
distinct object, wins over any label a Programm-PRD might also
|
|
17
|
+
carry). Deliberately marker-only — the `type:program` label is a
|
|
18
|
+
LATER slice's addition; this module needs no profile key
|
|
19
|
+
for it and must keep working before that label exists.
|
|
20
|
+
anchor — `type:cluster` label. Wins over `wave-stub` when a node somehow
|
|
21
|
+
carries both (a drift/transition state) — mirrors the
|
|
22
|
+
to-prd/to-issues precedence ("type:cluster stops always; Wave
|
|
23
|
+
stops unless wave-stub").
|
|
24
|
+
wave_stub — `wave-stub` label (a board-to-waves candidate stub, OR an
|
|
25
|
+
un-promoted Stufe-1p program stub — same label, two
|
|
26
|
+
provenances, both structurally "not yet an anchor").
|
|
27
|
+
leaf — none of the above; an ordinary slice/child issue.
|
|
28
|
+
|
|
29
|
+
ROOT_KINDS = {program, anchor}: execute-ready-check.py evaluates these at
|
|
30
|
+
their OWN local root (their own children as siblings) and never lifts them to
|
|
31
|
+
a real parent, even when one exists. The multi-level, programme-wide graph
|
|
32
|
+
validation is validate-graph's job — this stays
|
|
33
|
+
rooted-local (target + at most one level of children).
|
|
34
|
+
|
|
35
|
+
Pure + I/O-free by design (mirrors anchor_table.py) — directly unit-tested
|
|
36
|
+
with plain dicts, no gh/board_config coupling.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
PROGRAM = "program"
|
|
43
|
+
ANCHOR = "anchor"
|
|
44
|
+
WAVE_STUB = "wave_stub"
|
|
45
|
+
LEAF = "leaf"
|
|
46
|
+
|
|
47
|
+
ROOT_KINDS = frozenset({PROGRAM, ANCHOR})
|
|
48
|
+
|
|
49
|
+
PROGRAM_MARKER_RE = re.compile(r"<!--\s*prd:\s*program\s*-->")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def classify_node(node: dict, *, cluster_type_label: str | None,
|
|
53
|
+
wave_stub_label: str | None) -> str:
|
|
54
|
+
"""program | anchor | wave_stub | leaf for `node` ({body, labels, ...}).
|
|
55
|
+
|
|
56
|
+
`cluster_type_label`/`wave_stub_label` come from the board profile
|
|
57
|
+
(`_CFG["labels"].get(...)`) — passed in rather than read here so this
|
|
58
|
+
module stays board-profile-free and directly testable.
|
|
59
|
+
"""
|
|
60
|
+
body = node.get("body") or ""
|
|
61
|
+
labels = node.get("labels") or []
|
|
62
|
+
if PROGRAM_MARKER_RE.search(body):
|
|
63
|
+
return PROGRAM
|
|
64
|
+
if cluster_type_label and cluster_type_label in labels:
|
|
65
|
+
return ANCHOR
|
|
66
|
+
if wave_stub_label and wave_stub_label in labels:
|
|
67
|
+
return WAVE_STUB
|
|
68
|
+
return LEAF
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""portability_profile_scan.py — profile-VALUE literal scan (#1878, Welle 49
|
|
3
|
+
Slice 7, extends the portability lint in test_skill_portability_lint.py).
|
|
4
|
+
|
|
5
|
+
A published (generic/vendored) skill body must reference the board profile
|
|
6
|
+
(`docs/agents/board-sync.md`, loaded by `board_config.py`) by KEY —
|
|
7
|
+
`prMarkers.retroValues`, `headings.vorBau` — never MANDATE the literal VALUE
|
|
8
|
+
a profile happens to configure (e.g. testreporter's `gefahren`/`übersprungen`,
|
|
9
|
+
`Vor Bau zu klären`). A consumer whose profile configures different words
|
|
10
|
+
silently fails a skill that hardcoded testreporter's wording as required —
|
|
11
|
+
the same drift class `test_skill_portability_lint.py`'s CONSTANT_PATTERNS
|
|
12
|
+
scan already closes for opaque board node/option ids; this closes it for the
|
|
13
|
+
human-facing convention words instead.
|
|
14
|
+
|
|
15
|
+
A line that also names the profile key is a reference, not a hardcode —
|
|
16
|
+
exempt (mirrors CONSTANT_PATTERNS' own exempt rule). A line carrying
|
|
17
|
+
`portability-lint: ok` is exempt too (deliberate doc example, e.g. a
|
|
18
|
+
copy-paste-ready marker/heading example where inlining the key would corrupt
|
|
19
|
+
the example).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
EXEMPT = "portability-lint: ok"
|
|
26
|
+
|
|
27
|
+
# (label, literal pattern, profile-key substring whose presence on the same
|
|
28
|
+
# line exempts the match — a reference, not a hardcode).
|
|
29
|
+
PROFILE_VALUE_PATTERNS = [
|
|
30
|
+
# prMarkers.retroValues — the two configured PR "**Retro:**"-line words.
|
|
31
|
+
# Backtick-quoted only: bare "gefahren"/"übersprungen" are ordinary German
|
|
32
|
+
# words (past participles of fahren/überspringen) outside this
|
|
33
|
+
# convention — unquoted would false-positive on normal prose (e.g. "Retro
|
|
34
|
+
# schon gefahren?").
|
|
35
|
+
("prMarkers.retroValues literal", re.compile(r"`(?:gefahren|übersprungen)`"), "retroValues"),
|
|
36
|
+
# headings.vorBau — the configured HITL heading text. The phrase is
|
|
37
|
+
# specific enough that no quoting requirement is needed.
|
|
38
|
+
("headings.vorBau literal", re.compile(r"Vor Bau zu klären"), "vorBau"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_profile_value_literals(text: str) -> list[tuple[int, str, str]]:
|
|
43
|
+
"""Return (line_no, label, line) for every hardcoded MANDATORY profile-value
|
|
44
|
+
literal, minus lines that also reference the profile key (a profile-driven
|
|
45
|
+
reference, not a hardcode) or carry the exempt marker."""
|
|
46
|
+
out = []
|
|
47
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
48
|
+
if EXEMPT in line:
|
|
49
|
+
continue
|
|
50
|
+
for label, pattern, key in PROFILE_VALUE_PATTERNS:
|
|
51
|
+
if pattern.search(line) and key not in line:
|
|
52
|
+
out.append((n, label, line.strip()))
|
|
53
|
+
return out
|