@ikon85/agent-workflow-kit 0.43.0 → 0.44.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/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- package/scripts/worktree-lifecycle/session.py +0 -1857
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Issue-claim-on-pickup contract (#231).
|
|
3
|
-
|
|
4
|
-
A skill that ACCEPTS a tracked issue for building must leave a claim on the
|
|
5
|
-
issue itself before it builds — the local worktree/branch/PR guard only sees
|
|
6
|
-
the same machine, and only until someone pushes. The claim is what a second
|
|
7
|
-
session, a second machine, or a cloud agent can actually see.
|
|
8
|
-
|
|
9
|
-
Enforced here:
|
|
10
|
-
|
|
11
|
-
- `implement` and `diagnose` carry one byte-identical claim block on both
|
|
12
|
-
surfaces, and it sits before their first build instruction.
|
|
13
|
-
- The block covers all three legs: check for a foreign claim (STOP), plant a
|
|
14
|
-
claim naming branch + worktree, release it.
|
|
15
|
-
- `orchestrate-wave` claims each slice issue at builder-launch time and
|
|
16
|
-
releases only its own slice claims.
|
|
17
|
-
- Every tracker seed and this repo's own tracker layer document the concrete
|
|
18
|
-
claim / check / release operations, so the tracker-neutral skill prose
|
|
19
|
-
resolves to real commands.
|
|
20
|
-
|
|
21
|
-
Run: python3 scripts/test_issue_claim_contract.py
|
|
22
|
-
"""
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
import unittest
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
29
|
-
SURFACES = (".claude/skills", ".agents/skills")
|
|
30
|
-
PICKUP_SKILLS = ("implement", "diagnose")
|
|
31
|
-
START = "<!-- issue-claim:start -->"
|
|
32
|
-
END = "<!-- issue-claim:end -->"
|
|
33
|
-
MARKER = "<!-- agent-claim:"
|
|
34
|
-
TRACKER_DOC = "docs/agents/issue-tracker.md"
|
|
35
|
-
TRACKER_SEEDS = (
|
|
36
|
-
"setup-workflow/issue-tracker-github.md",
|
|
37
|
-
"setup-workflow/issue-tracker-gitlab.md",
|
|
38
|
-
"setup-workflow/issue-tracker-local.md",
|
|
39
|
-
)
|
|
40
|
-
# The first instruction that already assumes the build has started.
|
|
41
|
-
FIRST_BUILD_ANCHOR = {
|
|
42
|
-
"implement": "Use /tdd where possible",
|
|
43
|
-
"diagnose": "## Phase 1 — Build a feedback loop",
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def skill_body(surface: str, skill: str) -> str:
|
|
48
|
-
return (ROOT / surface / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def claim_block(body: str) -> str:
|
|
52
|
-
return body.split(START, 1)[1].split(END, 1)[0]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def section(body: str, heading: str) -> str:
|
|
56
|
-
"""Return one second-level Markdown section, excluding the next one."""
|
|
57
|
-
start = body.index(heading) + len(heading)
|
|
58
|
-
end = body.find("\n## ", start)
|
|
59
|
-
return body[start:] if end == -1 else body[start:end]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class ClaimBlockContract(unittest.TestCase):
|
|
63
|
-
def test_pickup_skills_carry_exactly_one_claim_block_on_both_surfaces(self):
|
|
64
|
-
for skill in PICKUP_SKILLS:
|
|
65
|
-
for surface in SURFACES:
|
|
66
|
-
with self.subTest(skill=skill, surface=surface):
|
|
67
|
-
body = skill_body(surface, skill)
|
|
68
|
-
self.assertEqual(body.count(START), 1)
|
|
69
|
-
self.assertEqual(body.count(END), 1)
|
|
70
|
-
self.assertLess(body.index(START), body.index(END))
|
|
71
|
-
|
|
72
|
-
def test_claim_block_is_byte_identical_across_skills_and_surfaces(self):
|
|
73
|
-
blocks = {
|
|
74
|
-
(skill, surface): claim_block(skill_body(surface, skill))
|
|
75
|
-
for skill in PICKUP_SKILLS
|
|
76
|
-
for surface in SURFACES
|
|
77
|
-
}
|
|
78
|
-
first = blocks[(PICKUP_SKILLS[0], SURFACES[0])]
|
|
79
|
-
for key, block in blocks.items():
|
|
80
|
-
with self.subTest(key=key):
|
|
81
|
-
self.assertEqual(block, first)
|
|
82
|
-
|
|
83
|
-
def test_claim_block_covers_check_plant_and_release(self):
|
|
84
|
-
block = claim_block(skill_body(SURFACES[0], PICKUP_SKILLS[0]))
|
|
85
|
-
# Check leg — a foreign claim stops the pickup, and is never removed.
|
|
86
|
-
self.assertIn("foreign claim", block)
|
|
87
|
-
self.assertIn("STOP", block)
|
|
88
|
-
self.assertIn("never delete a foreign claim", block)
|
|
89
|
-
# Plant leg — the marker must carry branch AND worktree, or a colliding
|
|
90
|
-
# session cannot find the work in progress.
|
|
91
|
-
self.assertIn(MARKER, block)
|
|
92
|
-
self.assertIn("branch=", block)
|
|
93
|
-
self.assertIn("worktree=", block)
|
|
94
|
-
# Release leg — the PR supersedes it; an abandoned claim is removed.
|
|
95
|
-
self.assertIn("supersedes", block)
|
|
96
|
-
self.assertIn("abandon", block)
|
|
97
|
-
|
|
98
|
-
def test_claim_block_resolves_through_the_tracker_layer_with_a_fallback(self):
|
|
99
|
-
block = claim_block(skill_body(SURFACES[0], PICKUP_SKILLS[0]))
|
|
100
|
-
self.assertIn(TRACKER_DOC, block)
|
|
101
|
-
self.assertIn("fall back", block)
|
|
102
|
-
|
|
103
|
-
def test_claim_is_planted_before_the_first_build_instruction(self):
|
|
104
|
-
for skill in PICKUP_SKILLS:
|
|
105
|
-
for surface in SURFACES:
|
|
106
|
-
with self.subTest(skill=skill, surface=surface):
|
|
107
|
-
body = skill_body(surface, skill)
|
|
108
|
-
self.assertLess(
|
|
109
|
-
body.index(END), body.index(FIRST_BUILD_ANCHOR[skill])
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class OrchestrateWaveSliceClaimContract(unittest.TestCase):
|
|
114
|
-
def test_dispatch_claims_each_slice_issue_at_builder_launch(self):
|
|
115
|
-
for surface in SURFACES:
|
|
116
|
-
with self.subTest(surface=surface):
|
|
117
|
-
dispatch = section(
|
|
118
|
-
skill_body(surface, "orchestrate-wave"),
|
|
119
|
-
"## Phase 2 — Dispatch one wave in parallel",
|
|
120
|
-
)
|
|
121
|
-
self.assertIn(MARKER, dispatch)
|
|
122
|
-
self.assertIn("slice issue", dispatch)
|
|
123
|
-
self.assertIn("branch=", dispatch)
|
|
124
|
-
self.assertIn("worktree=", dispatch)
|
|
125
|
-
|
|
126
|
-
def test_wave_claim_is_not_treated_as_a_slice_claim(self):
|
|
127
|
-
for surface in SURFACES:
|
|
128
|
-
with self.subTest(surface=surface):
|
|
129
|
-
dispatch = section(
|
|
130
|
-
skill_body(surface, "orchestrate-wave"),
|
|
131
|
-
"## Phase 2 — Dispatch one wave in parallel",
|
|
132
|
-
)
|
|
133
|
-
self.assertIn("wave claim", dispatch)
|
|
134
|
-
|
|
135
|
-
def test_cleanup_releases_only_this_runs_slice_claims(self):
|
|
136
|
-
for surface in SURFACES:
|
|
137
|
-
with self.subTest(surface=surface):
|
|
138
|
-
cleanup = section(
|
|
139
|
-
skill_body(surface, "orchestrate-wave"),
|
|
140
|
-
"## Phase 6 — Cleanup + close",
|
|
141
|
-
)
|
|
142
|
-
self.assertIn("slice claim", cleanup)
|
|
143
|
-
self.assertIn("this run", cleanup)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class TrackerLayerClaimOperations(unittest.TestCase):
|
|
147
|
-
def documents(self) -> dict[str, str]:
|
|
148
|
-
docs = {TRACKER_DOC: (ROOT / TRACKER_DOC).read_text(encoding="utf-8")}
|
|
149
|
-
for surface in SURFACES:
|
|
150
|
-
for seed in TRACKER_SEEDS:
|
|
151
|
-
path = ROOT / surface / seed
|
|
152
|
-
docs[str(path.relative_to(ROOT))] = path.read_text(encoding="utf-8")
|
|
153
|
-
return docs
|
|
154
|
-
|
|
155
|
-
def test_every_tracker_layer_defines_claim_check_and_release(self):
|
|
156
|
-
for name, body in self.documents().items():
|
|
157
|
-
with self.subTest(document=name):
|
|
158
|
-
self.assertIn("## Pickup claim", body)
|
|
159
|
-
claim = section(body, "## Pickup claim")
|
|
160
|
-
self.assertIn(MARKER, claim)
|
|
161
|
-
self.assertIn("branch=", claim)
|
|
162
|
-
self.assertIn("worktree=", claim)
|
|
163
|
-
# a read side (detect a foreign claim) and a release side
|
|
164
|
-
self.assertIn("Check", claim)
|
|
165
|
-
self.assertIn("Release", claim)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if __name__ == "__main__":
|
|
169
|
-
unittest.main()
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""dist-kit doc language census (#1953) — a coarse net over EVERY shipped
|
|
3
|
-
`.md` file (docs/templates, README, SKILL.md bodies alike), not just
|
|
4
|
-
`publish: true` skill bodies (that zero-tolerance census already exists in
|
|
5
|
-
`test_skill_language_census.py`). This guard is a percentage smoke test: a
|
|
6
|
-
kit doc is red once ≥10% of its lines carry German prose, so an
|
|
7
|
-
English-speaking consumer never again silently gets a majority-German
|
|
8
|
-
template the way `docs/agents/wave-anchor-template.md` shipped before this
|
|
9
|
-
slice (23/100 lines German — the census #1953 was written to catch).
|
|
10
|
-
|
|
11
|
-
Reuses `test_skill_language_census.py`'s wordlist + line-classification
|
|
12
|
-
helpers (`_line_hits`, `strip_frontmatter`, the bilingual
|
|
13
|
-
`<user-story-example lang="de">` block skip) rather than a parallel
|
|
14
|
-
heuristic — same STRONG/WEAK wordlist, same allowlist exemptions (contract
|
|
15
|
-
literals, `Welle <N>` title format, the inline `language-census: ok`
|
|
16
|
-
marker).
|
|
17
|
-
|
|
18
|
-
Two test classes:
|
|
19
|
-
- `RatioFixtures` — pure-function unit tests, no build. Proves the ratio
|
|
20
|
-
computation goes RED on an artificially-German fixture and GREEN on an
|
|
21
|
-
all-English one, independent of the real dist-kit build.
|
|
22
|
-
- `DistKitDocsCensus` — builds the REAL dist-kit (subprocess, like
|
|
23
|
-
`test_dist_kit_smoke.py`) and asserts every shipped `.md` file is under
|
|
24
|
-
the threshold.
|
|
25
|
-
|
|
26
|
-
Run: python3 scripts/test_kit_docs_language_census.py
|
|
27
|
-
"""
|
|
28
|
-
import subprocess
|
|
29
|
-
import sys
|
|
30
|
-
import unittest
|
|
31
|
-
from pathlib import Path
|
|
32
|
-
|
|
33
|
-
from test_skill_language_census import (
|
|
34
|
-
BILINGUAL_BLOCK_END,
|
|
35
|
-
BILINGUAL_BLOCK_START,
|
|
36
|
-
_line_hits,
|
|
37
|
-
strip_frontmatter,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
REPO = Path(__file__).resolve().parent.parent
|
|
41
|
-
DIST = REPO / "dist-kit"
|
|
42
|
-
|
|
43
|
-
GERMAN_LINE_THRESHOLD_PCT = 10.0
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def german_line_ratio(text: str) -> tuple[int, int]:
|
|
47
|
-
"""(german_hit_lines, total_lines) for a doc body — frontmatter stripped,
|
|
48
|
-
bilingual `<user-story-example lang="de">...</user-story-example>` blocks
|
|
49
|
-
skipped (same exemptions as the publish:true skill census)."""
|
|
50
|
-
body = strip_frontmatter(text)
|
|
51
|
-
lines = body.splitlines()
|
|
52
|
-
total = len(lines)
|
|
53
|
-
hits = 0
|
|
54
|
-
in_bilingual_de_block = False
|
|
55
|
-
for line in lines:
|
|
56
|
-
if BILINGUAL_BLOCK_START.search(line):
|
|
57
|
-
in_bilingual_de_block = True
|
|
58
|
-
if in_bilingual_de_block:
|
|
59
|
-
if BILINGUAL_BLOCK_END.search(line):
|
|
60
|
-
in_bilingual_de_block = False
|
|
61
|
-
continue
|
|
62
|
-
if _line_hits(line):
|
|
63
|
-
hits += 1
|
|
64
|
-
return hits, total
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def is_below_threshold(text: str) -> tuple[bool, float]:
|
|
68
|
-
"""(clean, pct) — clean is False once pct >= GERMAN_LINE_THRESHOLD_PCT."""
|
|
69
|
-
hits, total = german_line_ratio(text)
|
|
70
|
-
if total == 0:
|
|
71
|
-
return True, 0.0
|
|
72
|
-
pct = hits / total * 100
|
|
73
|
-
return pct < GERMAN_LINE_THRESHOLD_PCT, pct
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class RatioFixtures(unittest.TestCase):
|
|
77
|
-
"""Pure-function proof: the census heuristic actually catches German
|
|
78
|
-
prose (RED) and does not false-positive on clean English (GREEN) —
|
|
79
|
-
independent of whether dist-kit can be built in this environment."""
|
|
80
|
-
|
|
81
|
-
def test_mostly_german_doc_is_red(self):
|
|
82
|
-
# Mirrors the pre-translation wave-anchor-template.md shape: a title
|
|
83
|
-
# line + a handful of English structural lines + a majority of dense
|
|
84
|
-
# German prose paragraphs — comfortably over the 10% line threshold.
|
|
85
|
-
german_doc = "\n".join(
|
|
86
|
-
["# Title", "", "## Section", ""]
|
|
87
|
-
+ ["Diese Zeile beschreibt eine Änderung und ist auf Deutsch geschrieben."] * 3
|
|
88
|
-
+ ["Der Anker wird für die Wellen-Planung genutzt und muss gepflegt werden."] * 3
|
|
89
|
-
+ [""] * 14 # padding so German lines are a clear >10% share, not 100%
|
|
90
|
-
)
|
|
91
|
-
clean, pct = is_below_threshold(german_doc)
|
|
92
|
-
self.assertFalse(clean, f"expected RED (>= {GERMAN_LINE_THRESHOLD_PCT}%), got {pct:.1f}%")
|
|
93
|
-
self.assertGreaterEqual(pct, GERMAN_LINE_THRESHOLD_PCT)
|
|
94
|
-
|
|
95
|
-
def test_all_english_doc_is_green(self):
|
|
96
|
-
english_doc = "\n".join(
|
|
97
|
-
["# Title", "", "## Section", "",
|
|
98
|
-
"This line describes a change and is written in English.",
|
|
99
|
-
"The anchor is used for wave planning and must be maintained.",
|
|
100
|
-
""]
|
|
101
|
-
)
|
|
102
|
-
clean, pct = is_below_threshold(english_doc)
|
|
103
|
-
self.assertTrue(clean, f"expected GREEN (< {GERMAN_LINE_THRESHOLD_PCT}%), got {pct:.1f}%")
|
|
104
|
-
|
|
105
|
-
def test_frontmatter_excluded_from_ratio(self):
|
|
106
|
-
# A German `description:` inside frontmatter (legitimate — quotes a
|
|
107
|
-
# German trigger phrase) must not count toward the body ratio.
|
|
108
|
-
text = (
|
|
109
|
-
"---\n"
|
|
110
|
-
"name: x\n"
|
|
111
|
-
'description: Use when "lass uns das Board durchgehen und die Wellen planen".\n'
|
|
112
|
-
"---\n\n"
|
|
113
|
-
"This body is English only, nothing else in here at all.\n"
|
|
114
|
-
)
|
|
115
|
-
clean, pct = is_below_threshold(text)
|
|
116
|
-
self.assertTrue(clean, f"frontmatter leaked into ratio: {pct:.1f}%")
|
|
117
|
-
|
|
118
|
-
def test_exempt_marker_line_does_not_count_as_a_hit(self):
|
|
119
|
-
text = "\n".join(
|
|
120
|
-
["# Title", "Dies ist deutsch und bleibt es. <!-- language-census: ok -->"]
|
|
121
|
-
+ ["English line."] * 10
|
|
122
|
-
)
|
|
123
|
-
clean, pct = is_below_threshold(text)
|
|
124
|
-
self.assertTrue(clean, f"exempt marker line still counted: {pct:.1f}%")
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _build_dist_kit():
|
|
128
|
-
"""Run the real build (subprocess), mirroring test_dist_kit_smoke.py.
|
|
129
|
-
Returns (CompletedProcess, skip_reason); skip_reason set only when node
|
|
130
|
-
itself is unavailable in this environment."""
|
|
131
|
-
build = REPO / "scripts/build-kit.mjs"
|
|
132
|
-
try:
|
|
133
|
-
r = subprocess.run(["node", str(build)], cwd=REPO,
|
|
134
|
-
capture_output=True, text=True, timeout=120)
|
|
135
|
-
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
136
|
-
return None, str(e)
|
|
137
|
-
return r, None
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class DistKitDocsCensus(unittest.TestCase):
|
|
141
|
-
"""Smoke test over the actually-shipped dist-kit/**/*.md — the ONLY
|
|
142
|
-
thing a consumer ever reads, so this is what must stay clean."""
|
|
143
|
-
|
|
144
|
-
@classmethod
|
|
145
|
-
def setUpClass(cls):
|
|
146
|
-
r, skip_reason = _build_dist_kit()
|
|
147
|
-
if skip_reason:
|
|
148
|
-
raise unittest.SkipTest(f"node/build unavailable: {skip_reason}")
|
|
149
|
-
if r.returncode != 0:
|
|
150
|
-
raise AssertionError(f"build-kit.mjs failed: {r.stderr}")
|
|
151
|
-
if not DIST.is_dir():
|
|
152
|
-
raise AssertionError("dist-kit/ missing after build")
|
|
153
|
-
|
|
154
|
-
def test_every_shipped_doc_is_under_the_german_line_threshold(self):
|
|
155
|
-
violations = []
|
|
156
|
-
checked = 0
|
|
157
|
-
for path in sorted(DIST.rglob("*.md")):
|
|
158
|
-
if not path.is_file():
|
|
159
|
-
continue
|
|
160
|
-
checked += 1
|
|
161
|
-
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
162
|
-
clean, pct = is_below_threshold(text)
|
|
163
|
-
if not clean:
|
|
164
|
-
violations.append((str(path.relative_to(DIST)), pct))
|
|
165
|
-
|
|
166
|
-
self.assertGreater(checked, 0, "no dist-kit/**/*.md found — build produced nothing?")
|
|
167
|
-
msg = "\n".join(f" {p}: {pct:.1f}% German lines" for p, pct in violations)
|
|
168
|
-
self.assertEqual(
|
|
169
|
-
violations, [],
|
|
170
|
-
f"Census: {checked - len(violations)} of {checked} shipped docs under "
|
|
171
|
-
f"{GERMAN_LINE_THRESHOLD_PCT}% German lines.\n\n"
|
|
172
|
-
"Translate the residual German prose (or, for a genuine intentional "
|
|
173
|
-
"literal, mark the line `<!-- language-census: ok -->`):\n" + msg,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if __name__ == "__main__":
|
|
178
|
-
unittest.main(verbosity=2)
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Behavior tests for marker identity lookup and reconciliation."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
import importlib.util
|
|
8
|
-
import io
|
|
9
|
-
import subprocess
|
|
10
|
-
import unittest
|
|
11
|
-
from contextlib import redirect_stderr, redirect_stdout
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from unittest.mock import patch
|
|
14
|
-
|
|
15
|
-
from scripts.marker_lib import find_by_marker, reconcile_after_create
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
CLI_SPEC = importlib.util.spec_from_file_location(
|
|
19
|
-
"find_by_marker_cli_test", Path(__file__).with_name("find-by-marker.py"))
|
|
20
|
-
cli = importlib.util.module_from_spec(CLI_SPEC)
|
|
21
|
-
assert CLI_SPEC.loader is not None
|
|
22
|
-
CLI_SPEC.loader.exec_module(cli)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class MarkerLookupTest(unittest.TestCase):
|
|
26
|
-
def test_pagination_joins_all_pages(self):
|
|
27
|
-
calls: list[list[str]] = []
|
|
28
|
-
|
|
29
|
-
def fake_gh(args: list[str]) -> str:
|
|
30
|
-
calls.append(args)
|
|
31
|
-
return json.dumps([
|
|
32
|
-
[{"number": 11, "state": "open", "body":
|
|
33
|
-
"<!-- prd-source-id: alpha -->"}],
|
|
34
|
-
[{"number": 12, "state": "closed", "body":
|
|
35
|
-
"<!-- prd-source-id: alpha -->"}],
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
result = find_by_marker("example/repo", "prd-source-id", "alpha", fake_gh)
|
|
39
|
-
|
|
40
|
-
self.assertEqual(result["count"], 2)
|
|
41
|
-
self.assertEqual(result["issues"], [
|
|
42
|
-
{"number": 11, "state": "open"},
|
|
43
|
-
{"number": 12, "state": "closed"},
|
|
44
|
-
])
|
|
45
|
-
self.assertIn("--paginate", calls[0])
|
|
46
|
-
self.assertIn("state=all", calls[0])
|
|
47
|
-
|
|
48
|
-
def test_pull_requests_are_discarded_before_matching(self):
|
|
49
|
-
marker = "<!-- wave-stub-source: same -->"
|
|
50
|
-
payload = [[
|
|
51
|
-
{"number": 21, "state": "open", "body": marker,
|
|
52
|
-
"pull_request": {"url": "https://api.example/pr/21"}},
|
|
53
|
-
{"number": 22, "state": "open", "body": marker},
|
|
54
|
-
]]
|
|
55
|
-
|
|
56
|
-
result = find_by_marker(
|
|
57
|
-
"example/repo", "wave-stub-source", "same",
|
|
58
|
-
lambda _args: json.dumps(payload),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
self.assertEqual(result["issues"], [{"number": 22, "state": "open"}])
|
|
62
|
-
|
|
63
|
-
def test_closed_only_match_requires_user_decision(self):
|
|
64
|
-
payload = [[{"number": 31, "state": "closed", "body":
|
|
65
|
-
"<!-- prd-source-id: retired -->"}]]
|
|
66
|
-
|
|
67
|
-
result = find_by_marker(
|
|
68
|
-
"example/repo", "prd-source-id", "retired",
|
|
69
|
-
lambda _args: json.dumps(payload),
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
self.assertEqual(result["verdict"], "user-decision")
|
|
73
|
-
|
|
74
|
-
def test_marker_value_is_exact_not_a_prefix_match(self):
|
|
75
|
-
payload = [[
|
|
76
|
-
{"number": 41, "state": "open", "body":
|
|
77
|
-
"<!-- prd-source-id: alpha-long -->"},
|
|
78
|
-
{"number": 42, "state": "open", "body":
|
|
79
|
-
"<!-- prd-source-id: alpha -->"},
|
|
80
|
-
]]
|
|
81
|
-
|
|
82
|
-
result = find_by_marker(
|
|
83
|
-
"example/repo", "prd-source-id", "alpha",
|
|
84
|
-
lambda _args: json.dumps(payload),
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
self.assertEqual(result["issues"], [{"number": 42, "state": "open"}])
|
|
88
|
-
self.assertEqual(result["verdict"], "update")
|
|
89
|
-
|
|
90
|
-
def test_post_create_rescan_stops_and_reports_both_duplicate_numbers(self):
|
|
91
|
-
marker = "<!-- program-leaf-source: program-x/1a -->"
|
|
92
|
-
responses = iter([
|
|
93
|
-
[[]],
|
|
94
|
-
[[
|
|
95
|
-
{"number": 101, "state": "open", "body": marker},
|
|
96
|
-
{"number": 102, "state": "open", "body": marker},
|
|
97
|
-
]],
|
|
98
|
-
])
|
|
99
|
-
calls: list[list[str]] = []
|
|
100
|
-
|
|
101
|
-
def fake_gh(args: list[str]) -> str:
|
|
102
|
-
calls.append(args)
|
|
103
|
-
return json.dumps(next(responses))
|
|
104
|
-
|
|
105
|
-
before = find_by_marker(
|
|
106
|
-
"example/repo", "program-leaf-source", "program-x/1a",
|
|
107
|
-
fake_gh,
|
|
108
|
-
)
|
|
109
|
-
after = reconcile_after_create(
|
|
110
|
-
"example/repo", "program-leaf-source", "program-x/1a", 101,
|
|
111
|
-
fake_gh,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
self.assertEqual(before["verdict"], "create")
|
|
115
|
-
self.assertEqual(after["verdict"], "STOP")
|
|
116
|
-
self.assertEqual([issue["number"] for issue in after["issues"]], [101, 102])
|
|
117
|
-
self.assertEqual(len(calls), 2)
|
|
118
|
-
|
|
119
|
-
def test_post_create_rescan_rejects_a_different_single_issue(self):
|
|
120
|
-
payload = [[{"number": 102, "state": "open", "body":
|
|
121
|
-
"<!-- prd-source-id: alpha -->"}]]
|
|
122
|
-
|
|
123
|
-
result = reconcile_after_create(
|
|
124
|
-
"example/repo", "prd-source-id", "alpha", 101,
|
|
125
|
-
lambda _args: json.dumps(payload),
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
self.assertEqual(result["verdict"], "STOP")
|
|
129
|
-
self.assertEqual(result["issues"], [{"number": 102, "state": "open"}])
|
|
130
|
-
|
|
131
|
-
def test_unknown_marker_kind_has_distinct_cli_error(self):
|
|
132
|
-
out, err = io.StringIO(), io.StringIO()
|
|
133
|
-
with redirect_stdout(out), redirect_stderr(err):
|
|
134
|
-
code = cli.main(
|
|
135
|
-
["--kind", "unknown-source", "--slug", "x"],
|
|
136
|
-
repo="example/repo",
|
|
137
|
-
gh=lambda _args: self.fail("unknown kinds must fail before scanning"),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
self.assertEqual(code, 2)
|
|
141
|
-
self.assertEqual(out.getvalue(), "")
|
|
142
|
-
self.assertIn("unknown marker kind", err.getvalue())
|
|
143
|
-
|
|
144
|
-
def test_cli_bounds_a_hanging_github_call(self):
|
|
145
|
-
with patch.object(
|
|
146
|
-
cli.subprocess, "run",
|
|
147
|
-
side_effect=subprocess.TimeoutExpired("gh", 30),
|
|
148
|
-
):
|
|
149
|
-
with self.assertRaisesRegex(RuntimeError, "timed out after 30s"):
|
|
150
|
-
cli._gh(["api", "--paginate", "repos/example/repo/issues"])
|
|
151
|
-
|
|
152
|
-
def test_cli_classifies_a_missing_github_binary(self):
|
|
153
|
-
with patch.object(
|
|
154
|
-
cli.subprocess, "run",
|
|
155
|
-
side_effect=FileNotFoundError(2, "No such file or directory", "gh"),
|
|
156
|
-
):
|
|
157
|
-
with self.assertRaisesRegex(RuntimeError, "gh CLI unavailable"):
|
|
158
|
-
cli._gh(["api", "--paginate", "repos/example/repo/issues"])
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if __name__ == "__main__":
|
|
162
|
-
unittest.main()
|