bb-harness 4.0.1__py3-none-any.whl
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.
- bb_harness/__init__.py +3 -0
- bb_harness/__main__.py +8 -0
- bb_harness/artifact_migration.py +141 -0
- bb_harness/batched_generation.py +272 -0
- bb_harness/cli.py +120 -0
- bb_harness/commands/__init__.py +25 -0
- bb_harness/commands/_invoke.py +26 -0
- bb_harness/commands/coverage.py +86 -0
- bb_harness/commands/evaluate.py +72 -0
- bb_harness/commands/export.py +145 -0
- bb_harness/commands/gate.py +69 -0
- bb_harness/commands/heatmap.py +61 -0
- bb_harness/commands/import_results.py +127 -0
- bb_harness/commands/ingest.py +64 -0
- bb_harness/commands/regression_graph.py +54 -0
- bb_harness/commands/run.py +222 -0
- bb_harness/commands/state_diagram.py +46 -0
- bb_harness/commands/validate.py +36 -0
- bb_harness/coverage_engine.py +553 -0
- bb_harness/efficiency_benchmark.py +194 -0
- bb_harness/efficient_generation.py +138 -0
- bb_harness/evidence_revisions.py +79 -0
- bb_harness/gate_engine.py +790 -0
- bb_harness/local_pipeline.py +1577 -0
- bb_harness/local_profiles.yaml +46 -0
- bb_harness/local_runtime.py +310 -0
- bb_harness/requirements_confidence.py +580 -0
- bb_harness/schema_validation.py +81 -0
- bb_harness/schemas/__init__.py +1 -0
- bb_harness/schemas/automation_evidence.schema.json +72 -0
- bb_harness/schemas/case_review_patch.schema.json +157 -0
- bb_harness/schemas/coverage_report.schema.json +257 -0
- bb_harness/schemas/effort_plan.schema.json +114 -0
- bb_harness/schemas/execution_evidence.schema.json +220 -0
- bb_harness/schemas/feature_spec.schema.json +97 -0
- bb_harness/schemas/gate_decision.schema.json +150 -0
- bb_harness/schemas/local_run_manifest.schema.json +339 -0
- bb_harness/schemas/manual_case_set.schema.json +503 -0
- bb_harness/schemas/observation_set.schema.json +166 -0
- bb_harness/schemas/phase_contract.schema.json +350 -0
- bb_harness/schemas/release_brief.schema.json +150 -0
- bb_harness/schemas/requirements_confidence.schema.json +500 -0
- bb_harness/schemas/requirements_review.schema.json +177 -0
- bb_harness/schemas/risk_register.schema.json +107 -0
- bb_harness/schemas/shared_defs.schema.json +1943 -0
- bb_harness/schemas/spec-source.schema.json +62 -0
- bb_harness/schemas/technique_plan.schema.json +133 -0
- bb_harness/schemas/test_model.schema.json +187 -0
- bb_harness/schemas/testrail-export.schema.json +39 -0
- bb_harness/schemas/waiver_set.schema.json +78 -0
- bb_harness/schemas/xray-export.schema.json +54 -0
- bb_harness/techniques/__init__.py +1 -0
- bb_harness/techniques/common.py +200 -0
- bb_harness/techniques/domain.py +120 -0
- bb_harness/techniques/extended.py +100 -0
- bb_harness/techniques/finite.py +242 -0
- bb_harness/token_budget.py +109 -0
- bb_harness/tools/__init__.py +1 -0
- bb_harness/tools/_shared/__init__.py +1 -0
- bb_harness/tools/_shared/import_common.py +104 -0
- bb_harness/tools/_shared/io_common.py +28 -0
- bb_harness/tools/_shared/spec_ingest_confluence.py +221 -0
- bb_harness/tools/_shared/spec_ingest_jira.py +149 -0
- bb_harness/tools/_shared/spec_ingest_markdown.py +154 -0
- bb_harness/tools/check_utf8.py +77 -0
- bb_harness/tools/export_notion.py +457 -0
- bb_harness/tools/export_testrail.py +171 -0
- bb_harness/tools/export_xray.py +160 -0
- bb_harness/tools/import_testrail.py +349 -0
- bb_harness/tools/import_xray.py +285 -0
- bb_harness/tools/quick_validate_skill.py +305 -0
- bb_harness/tools/regression_graph.py +416 -0
- bb_harness/tools/risk_heatmap.py +324 -0
- bb_harness/tools/spec_ingest.py +119 -0
- bb_harness/tools/state_diagram.py +148 -0
- bb_harness/tools/validate_artifact.py +326 -0
- bb_harness/tools/validate_release_bundle.py +433 -0
- bb_harness/tools/validate_spec.py +367 -0
- bb_harness/tools/verify_local_benchmark.py +169 -0
- bb_harness-4.0.1.dist-info/METADATA +308 -0
- bb_harness-4.0.1.dist-info/RECORD +90 -0
- bb_harness-4.0.1.dist-info/WHEEL +5 -0
- bb_harness-4.0.1.dist-info/entry_points.txt +2 -0
- bb_harness-4.0.1.dist-info/licenses/COMMERCIAL-LICENSE.md +14 -0
- bb_harness-4.0.1.dist-info/licenses/LICENSE +217 -0
- bb_harness-4.0.1.dist-info/licenses/LICENSE.ja.md +190 -0
- bb_harness-4.0.1.dist-info/licenses/LICENSING.md +55 -0
- bb_harness-4.0.1.dist-info/licenses/NOTICE +29 -0
- bb_harness-4.0.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +11 -0
- bb_harness-4.0.1.dist-info/top_level.txt +1 -0
bb_harness/__init__.py
ADDED
bb_harness/__main__.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""旧artifactを上書きせず構造情報だけを移行する。"""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from bb_harness.coverage_engine import ALIASES, TECHNIQUES, VERSION
|
|
6
|
+
from bb_harness.schema_validation import validate_artifact
|
|
7
|
+
from bb_harness.techniques.common import ModelError
|
|
8
|
+
|
|
9
|
+
ROOT_ADDITIONS = {"schema_version", "generation", "migration", "evidence_binding"}
|
|
10
|
+
CASE_ADDITIONS = {
|
|
11
|
+
"technique_refs",
|
|
12
|
+
"coverage_obligation_ids",
|
|
13
|
+
"coverage_inputs",
|
|
14
|
+
"test_data",
|
|
15
|
+
"case_revision",
|
|
16
|
+
}
|
|
17
|
+
CHARTER_ADDITIONS = {
|
|
18
|
+
"case_revision",
|
|
19
|
+
"technique_refs",
|
|
20
|
+
"basis_refs",
|
|
21
|
+
"mission",
|
|
22
|
+
"resources",
|
|
23
|
+
"entry_criteria",
|
|
24
|
+
"exit_criteria",
|
|
25
|
+
"environment",
|
|
26
|
+
"limitations",
|
|
27
|
+
"history_refs",
|
|
28
|
+
}
|
|
29
|
+
OBS_ADDITIONS = {"technique_refs", "model_refs", "coverage_obligation_ids", "basis_refs"}
|
|
30
|
+
EVIDENCE_ADDITIONS = {
|
|
31
|
+
"case_revision",
|
|
32
|
+
"model_hash",
|
|
33
|
+
"coverage_obligation_ids",
|
|
34
|
+
"relation_evaluation",
|
|
35
|
+
"session_log",
|
|
36
|
+
"model_ref",
|
|
37
|
+
"sample_id",
|
|
38
|
+
"observed_inputs",
|
|
39
|
+
"observed_outputs",
|
|
40
|
+
}
|
|
41
|
+
GATE_ADDITIONS = {
|
|
42
|
+
"evidence_binding_mode",
|
|
43
|
+
"required_obligation_design_rate",
|
|
44
|
+
"required_obligation_execution_rate",
|
|
45
|
+
"coverage_exit_criteria_met",
|
|
46
|
+
"coverage_report_id",
|
|
47
|
+
"coverage_mode",
|
|
48
|
+
"coverage_unknown_count",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def migrate_artifact(
|
|
53
|
+
value: dict, artifact_type: str, *, artifact_version: str = "enhanced"
|
|
54
|
+
) -> dict:
|
|
55
|
+
schema = f"{artifact_type}.schema.json"
|
|
56
|
+
validate_artifact(value, schema)
|
|
57
|
+
result = copy.deepcopy(value)
|
|
58
|
+
if artifact_version == "legacy":
|
|
59
|
+
if artifact_type in {"technique_plan", "coverage_report"}:
|
|
60
|
+
raise ModelError("legacyには被覆artifactの表現がない")
|
|
61
|
+
for key in ROOT_ADDITIONS:
|
|
62
|
+
result.pop(key, None)
|
|
63
|
+
if artifact_type == "test_model":
|
|
64
|
+
for key in {*TECHNIQUES, "parameters", "integration_paths"}:
|
|
65
|
+
result.pop(key, None)
|
|
66
|
+
if artifact_type == "manual_case_set":
|
|
67
|
+
for item in result["manual_cases"]:
|
|
68
|
+
for key in CASE_ADDITIONS:
|
|
69
|
+
item.pop(key, None)
|
|
70
|
+
for item in result.get("exploratory_charters", []):
|
|
71
|
+
for key in CHARTER_ADDITIONS:
|
|
72
|
+
item.pop(key, None)
|
|
73
|
+
if artifact_type == "observation_set":
|
|
74
|
+
allowed = {
|
|
75
|
+
"equivalence_partitioning",
|
|
76
|
+
"boundary_value",
|
|
77
|
+
"decision_table",
|
|
78
|
+
"state_transition",
|
|
79
|
+
"exploratory",
|
|
80
|
+
"error_guessing",
|
|
81
|
+
"use_case",
|
|
82
|
+
"user_scenario",
|
|
83
|
+
}
|
|
84
|
+
for item in result["observations"]:
|
|
85
|
+
if set(item["techniques"]) - allowed:
|
|
86
|
+
raise ModelError("新技法を旧observation enumへ損失なく変換できない")
|
|
87
|
+
for key in OBS_ADDITIONS:
|
|
88
|
+
item.pop(key, None)
|
|
89
|
+
if artifact_type == "execution_evidence":
|
|
90
|
+
for key in EVIDENCE_ADDITIONS:
|
|
91
|
+
result.pop(key, None)
|
|
92
|
+
if artifact_type == "gate_decision":
|
|
93
|
+
for key in GATE_ADDITIONS:
|
|
94
|
+
result.get("evidence_summary", {}).pop(key, None)
|
|
95
|
+
validate_artifact(result, schema)
|
|
96
|
+
return result
|
|
97
|
+
if artifact_version != "enhanced":
|
|
98
|
+
raise ModelError("unknown artifact version")
|
|
99
|
+
if result.get("schema_version") == VERSION:
|
|
100
|
+
return result
|
|
101
|
+
result["schema_version"] = VERSION
|
|
102
|
+
references = {
|
|
103
|
+
name: ("ISTQB CTAL-TA v4.0", reference) for name, reference, _ in TECHNIQUES.values()
|
|
104
|
+
}
|
|
105
|
+
references.update(
|
|
106
|
+
{
|
|
107
|
+
"equivalence_partitioning": ("ISTQB CTFL v4.0.1", "4.2.1"),
|
|
108
|
+
"boundary_value_analysis": ("ISTQB CTFL v4.0.1", "4.2.2"),
|
|
109
|
+
"error_guessing": ("ISTQB CTFL v4.0.1", "4.4.1"),
|
|
110
|
+
"exploratory_testing": ("ISTQB CTFL v4.0.1", "4.4.2"),
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
for item in result.get("observations", []) + result.get("manual_cases", []):
|
|
114
|
+
refs = []
|
|
115
|
+
for alias in item.get("techniques", []):
|
|
116
|
+
key = ALIASES.get(alias, alias)
|
|
117
|
+
if key in references:
|
|
118
|
+
standard, reference = references[key]
|
|
119
|
+
refs.append(
|
|
120
|
+
{
|
|
121
|
+
"key": key,
|
|
122
|
+
"standard": standard,
|
|
123
|
+
"reference": reference,
|
|
124
|
+
"legacy_alias": alias,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
if refs:
|
|
128
|
+
item["technique_refs"] = refs
|
|
129
|
+
status = (
|
|
130
|
+
"unmapped"
|
|
131
|
+
if artifact_type == "manual_case_set"
|
|
132
|
+
else "needs_review"
|
|
133
|
+
if artifact_type == "test_model"
|
|
134
|
+
else "migrated"
|
|
135
|
+
)
|
|
136
|
+
result["migration"] = {
|
|
137
|
+
"status": status,
|
|
138
|
+
"reason": "旧IDと本文を保持。文字列から精度・条件式・正式被覆を推測しない",
|
|
139
|
+
}
|
|
140
|
+
validate_artifact(result, schema)
|
|
141
|
+
return result
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""完全JSON単位の有限な分割生成。途中文字列の継ぎ足しはしない。"""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from bb_harness.coverage_engine import TECHNIQUES, validate_case_coverage
|
|
7
|
+
from bb_harness.efficient_generation import apply_review_patch, remaining_work, review_patch_prompt
|
|
8
|
+
from bb_harness.schema_validation import validate_artifact
|
|
9
|
+
from bb_harness.techniques.common import ModelError
|
|
10
|
+
|
|
11
|
+
MAX_CASE_BATCHES = 24
|
|
12
|
+
BATCH_SIZE = 2
|
|
13
|
+
CORE_FIELDS = (
|
|
14
|
+
"feature_id",
|
|
15
|
+
"flows",
|
|
16
|
+
"data_partitions",
|
|
17
|
+
"boundaries",
|
|
18
|
+
"rule_columns",
|
|
19
|
+
"states",
|
|
20
|
+
"valid_transitions",
|
|
21
|
+
"invalid_transitions",
|
|
22
|
+
"role_matrix",
|
|
23
|
+
"regression_edges",
|
|
24
|
+
"quality_lenses",
|
|
25
|
+
"parameters",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def select_schema(schema: dict, properties: list | tuple) -> dict:
|
|
30
|
+
"""必要なpropertyと参照先のdefsだけを、再帰参照も保持して選択する。"""
|
|
31
|
+
result = {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": False,
|
|
34
|
+
"properties": {key: copy.deepcopy(schema["properties"][key]) for key in properties},
|
|
35
|
+
"required": list(properties),
|
|
36
|
+
}
|
|
37
|
+
pending = []
|
|
38
|
+
|
|
39
|
+
def visit(node):
|
|
40
|
+
if isinstance(node, dict):
|
|
41
|
+
if "$ref" in node:
|
|
42
|
+
pending.append(node["$ref"])
|
|
43
|
+
for value in node.values():
|
|
44
|
+
visit(value)
|
|
45
|
+
elif isinstance(node, list):
|
|
46
|
+
for value in node:
|
|
47
|
+
visit(value)
|
|
48
|
+
|
|
49
|
+
visit(result)
|
|
50
|
+
seen = set()
|
|
51
|
+
while pending:
|
|
52
|
+
ref = pending.pop()
|
|
53
|
+
if ref in seen:
|
|
54
|
+
continue
|
|
55
|
+
if not ref.startswith("#/$defs/"):
|
|
56
|
+
raise ModelError("partition schema requires portable local references")
|
|
57
|
+
seen.add(ref)
|
|
58
|
+
parts = [part.replace("~1", "/").replace("~0", "~") for part in ref[2:].split("/")]
|
|
59
|
+
source, target = schema, result
|
|
60
|
+
for part in parts:
|
|
61
|
+
source = source[part]
|
|
62
|
+
for part in parts[:-1]:
|
|
63
|
+
target = target.setdefault(part, {})
|
|
64
|
+
target[parts[-1]] = copy.deepcopy(source)
|
|
65
|
+
visit(source)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _checkpoint(pipeline, name: str, value: dict) -> None:
|
|
70
|
+
from bb_harness.local_pipeline import _write_json
|
|
71
|
+
|
|
72
|
+
pipeline.checkpoint_dir.mkdir(exist_ok=True)
|
|
73
|
+
_write_json(pipeline.checkpoint_dir / f"{name}.json", value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def model_core_request(feature: dict) -> tuple[dict, str]:
|
|
77
|
+
from bb_harness.local_pipeline import (
|
|
78
|
+
_test_model_prompt,
|
|
79
|
+
portable_schema,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
whole = portable_schema("test_model.schema.json")
|
|
83
|
+
schema = select_schema(whole, CORE_FIELDS)
|
|
84
|
+
for field in ("boundaries", "data_partitions", "invalid_transitions"):
|
|
85
|
+
schema["properties"][field]["minItems"] = 1
|
|
86
|
+
schema["properties"]["selected_techniques"] = {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"uniqueItems": True,
|
|
89
|
+
"items": {"enum": list(TECHNIQUES)},
|
|
90
|
+
}
|
|
91
|
+
schema["required"].append("selected_techniques")
|
|
92
|
+
prompt = (
|
|
93
|
+
"モデルの概要と共有parameterだけを返します。型付きモデル本文は次の呼出で生成します。\n"
|
|
94
|
+
"selected_techniquesには、この仕様で根拠を持って適用する技法だけを列挙します。\n"
|
|
95
|
+
"不要な技法を選択せず、不明な値を発明しません。JSONは余分な空白を省いてください。\n"
|
|
96
|
+
"boundariesには数値境界と状態境界を含めます。仕様の許可状態/拒否状態の切替点も境界です。\n"
|
|
97
|
+
+ _test_model_prompt(feature)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return schema, prompt
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def generate_model(pipeline, feature: dict) -> dict:
|
|
104
|
+
from bb_harness.local_pipeline import (
|
|
105
|
+
_normalize_test_model,
|
|
106
|
+
_validate_test_model_semantics,
|
|
107
|
+
portable_schema,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
whole = portable_schema("test_model.schema.json")
|
|
111
|
+
schema, prompt = model_core_request(feature)
|
|
112
|
+
|
|
113
|
+
def validate_core(value):
|
|
114
|
+
base = {key: item for key, item in value.items() if key != "selected_techniques"}
|
|
115
|
+
validate_artifact(base, "test_model.schema.json")
|
|
116
|
+
_validate_test_model_semantics(base, feature=None)
|
|
117
|
+
|
|
118
|
+
core = pipeline._generate_custom(
|
|
119
|
+
"test_model__core",
|
|
120
|
+
schema,
|
|
121
|
+
prompt,
|
|
122
|
+
normalize=lambda value: _normalize_test_model(value, feature),
|
|
123
|
+
semantic_validate=validate_core,
|
|
124
|
+
)
|
|
125
|
+
_checkpoint(pipeline, "test_model-core", core)
|
|
126
|
+
selected = core.pop("selected_techniques")
|
|
127
|
+
result = copy.deepcopy(core)
|
|
128
|
+
for field in selected:
|
|
129
|
+
part_schema = select_schema(whole, [field])
|
|
130
|
+
prompt = (
|
|
131
|
+
f"{field}だけを完全なJSONで返してください。他技法や概要の再出力は不要です。\n"
|
|
132
|
+
"共有parameterを使い、根拠source_refsはfeature_specの実在オブジェクトを保持します。\n"
|
|
133
|
+
"まだ存在しないobservation/risk IDを参照しません。\n"
|
|
134
|
+
"モデルIDは技法名を含め一意にします。\n"
|
|
135
|
+
"条件や精度が不明なら発明せず空配列にしてください。JSONの余分な空白は省きます。\n"
|
|
136
|
+
+ json.dumps({"feature_spec": feature, "model_core": core}, ensure_ascii=False)
|
|
137
|
+
)
|
|
138
|
+
value = pipeline._generate_custom(f"test_model__{field}", part_schema, prompt)
|
|
139
|
+
result[field] = value[field]
|
|
140
|
+
_checkpoint(pipeline, f"test_model-{field}", value)
|
|
141
|
+
identifiers = [item["id"] for field in TECHNIQUES for item in result.get(field, [])]
|
|
142
|
+
if len(set(identifiers)) != len(identifiers):
|
|
143
|
+
raise ModelError("duplicate model IDs across generated partitions")
|
|
144
|
+
parameter_ids = [item["id"] for item in result.get("parameters", [])]
|
|
145
|
+
if len(set(parameter_ids)) != len(parameter_ids):
|
|
146
|
+
raise ModelError("duplicate shared parameter IDs")
|
|
147
|
+
validate_artifact(result, "test_model.schema.json")
|
|
148
|
+
_validate_test_model_semantics(result, feature=None)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def pending_keys(pending: dict) -> set[str]:
|
|
153
|
+
return (
|
|
154
|
+
{"obligation:" + item["id"] for item in pending["obligations"]}
|
|
155
|
+
| {"risk:" + key for key in pending["risk_ids"]}
|
|
156
|
+
| {"observation:" + key for key in pending["observation_ids"]}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def generate_cases(pipeline, feature: dict, model: dict, observations: dict, risks: dict) -> dict:
|
|
161
|
+
from bb_harness.local_pipeline import (
|
|
162
|
+
_merge_case_sets,
|
|
163
|
+
_normalize_cases,
|
|
164
|
+
_validate_source_grounding,
|
|
165
|
+
portable_schema,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
cases = {"feature_id": feature["feature_id"], "manual_cases": [], "exploratory_charters": []}
|
|
169
|
+
schema = portable_schema("manual_case_set.schema.json")
|
|
170
|
+
schema["properties"]["manual_cases"].update(minItems=0, maxItems=BATCH_SIZE)
|
|
171
|
+
schema["properties"]["exploratory_charters"].update(maxItems=BATCH_SIZE)
|
|
172
|
+
for number in range(MAX_CASE_BATCHES):
|
|
173
|
+
pending = remaining_work(feature, model, observations, risks, cases)
|
|
174
|
+
before = pending_keys(pending)
|
|
175
|
+
if not before:
|
|
176
|
+
break
|
|
177
|
+
focused = {
|
|
178
|
+
"obligations": pending["obligations"][:BATCH_SIZE],
|
|
179
|
+
"risk_ids": pending["risk_ids"][:BATCH_SIZE],
|
|
180
|
+
"observation_ids": pending["observation_ids"][:BATCH_SIZE],
|
|
181
|
+
}
|
|
182
|
+
prompt = (
|
|
183
|
+
"追加ケースを最大2件、探索チャーターを最大2件、完全JSONで返します。\n"
|
|
184
|
+
"focusの入力・経路をcoverage_inputsへ保持し、step_refs/expected_result_refsは1始まり。\n"
|
|
185
|
+
"指定したrisk/観点の未被覆を優先し、既存ケースの再出力は不要です。\n"
|
|
186
|
+
"oracle/source_refとtrace_toには実在する根拠/OBS/RISK IDだけを使います。\n"
|
|
187
|
+
"仕様にない表示文言・内部実装を発明しません。期待値は具体的に観測可能にします。\n"
|
|
188
|
+
"必要な追加がなければ空配列。JSONは余分な空白を省きます。\n"
|
|
189
|
+
+ json.dumps(
|
|
190
|
+
{
|
|
191
|
+
"feature_spec": feature,
|
|
192
|
+
"test_model": model,
|
|
193
|
+
"observations": observations,
|
|
194
|
+
"risks": risks,
|
|
195
|
+
"focus": focused,
|
|
196
|
+
"existing": [
|
|
197
|
+
{"tc_id": c["tc_id"], "title": c["title"], "trace_to": c["trace_to"]}
|
|
198
|
+
for c in cases["manual_cases"]
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
ensure_ascii=False,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def validate(value):
|
|
206
|
+
validate_artifact(value, "manual_case_set.schema.json")
|
|
207
|
+
_validate_source_grounding(value, pipeline.source_ids)
|
|
208
|
+
checked = validate_case_coverage(value, model, pipeline.technique_plan)
|
|
209
|
+
if checked["errors"]:
|
|
210
|
+
raise ModelError("; ".join(checked["errors"][:5]))
|
|
211
|
+
|
|
212
|
+
batch = pipeline._generate_custom(
|
|
213
|
+
f"manual_case_set__{number + 1}",
|
|
214
|
+
schema,
|
|
215
|
+
prompt,
|
|
216
|
+
normalize=lambda value: _normalize_cases(value, feature, observations, risks),
|
|
217
|
+
semantic_validate=validate,
|
|
218
|
+
)
|
|
219
|
+
cases = _merge_case_sets(cases, batch, feature, observations, risks)
|
|
220
|
+
_checkpoint(pipeline, f"cases-{number + 1:02d}", cases)
|
|
221
|
+
after = pending_keys(remaining_work(feature, model, observations, risks, cases))
|
|
222
|
+
if not before - after:
|
|
223
|
+
break
|
|
224
|
+
return cases
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def review_cases(
|
|
228
|
+
pipeline, feature: dict, model: dict, observations: dict, risks: dict, cases: dict
|
|
229
|
+
) -> dict:
|
|
230
|
+
from bb_harness.local_pipeline import _validate_source_grounding, portable_schema
|
|
231
|
+
|
|
232
|
+
result = copy.deepcopy(cases)
|
|
233
|
+
size = max(len(cases["manual_cases"]), len(cases.get("exploratory_charters", [])))
|
|
234
|
+
for offset in range(0, size, BATCH_SIZE):
|
|
235
|
+
subset = {
|
|
236
|
+
"feature_id": feature["feature_id"],
|
|
237
|
+
"manual_cases": result["manual_cases"][offset : offset + BATCH_SIZE],
|
|
238
|
+
"exploratory_charters": result.get("exploratory_charters", [])[
|
|
239
|
+
offset : offset + BATCH_SIZE
|
|
240
|
+
],
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def validate(patch, subset=subset, result=result):
|
|
244
|
+
apply_review_patch(subset, patch) # 対象外IDもここで拒否する。
|
|
245
|
+
updated = apply_review_patch(result, patch)
|
|
246
|
+
_validate_source_grounding(updated, pipeline.source_ids)
|
|
247
|
+
checked = validate_case_coverage(updated, model, pipeline.technique_plan)
|
|
248
|
+
if checked["errors"]:
|
|
249
|
+
raise ModelError("; ".join(checked["errors"][:5]))
|
|
250
|
+
|
|
251
|
+
patch = pipeline._generate_custom(
|
|
252
|
+
f"manual_case_review__{offset // BATCH_SIZE + 1}",
|
|
253
|
+
portable_schema("case_review_patch.schema.json"),
|
|
254
|
+
review_patch_prompt(feature, model, observations, risks, subset),
|
|
255
|
+
semantic_validate=validate,
|
|
256
|
+
)
|
|
257
|
+
result = apply_review_patch(result, patch)
|
|
258
|
+
_checkpoint(pipeline, f"review-{offset // BATCH_SIZE + 1:02d}", patch)
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def design_status(lint: dict, coverage: dict) -> str:
|
|
263
|
+
if lint["errors"] or coverage["errors"]:
|
|
264
|
+
return "blocked"
|
|
265
|
+
if (
|
|
266
|
+
coverage["blocked_selections"]
|
|
267
|
+
or coverage["unknown_ids"]
|
|
268
|
+
or coverage["design"]["uncovered_ids"]
|
|
269
|
+
or coverage["design"]["rate"] is None
|
|
270
|
+
):
|
|
271
|
+
return "degraded"
|
|
272
|
+
return "ready"
|
bb_harness/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""CLI entry point for bb-harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from bb_harness import __version__
|
|
9
|
+
from bb_harness.commands import (
|
|
10
|
+
coverage,
|
|
11
|
+
evaluate,
|
|
12
|
+
export,
|
|
13
|
+
gate,
|
|
14
|
+
heatmap,
|
|
15
|
+
import_results,
|
|
16
|
+
ingest,
|
|
17
|
+
regression_graph,
|
|
18
|
+
run,
|
|
19
|
+
state_diagram,
|
|
20
|
+
validate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
25
|
+
"""Create main CLI parser with subcommands."""
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="bb-harness",
|
|
28
|
+
description="Manual black-box test harness CLI tool",
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version",
|
|
34
|
+
action="version",
|
|
35
|
+
version=f"bb-harness {__version__}",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--verbose",
|
|
40
|
+
"-v",
|
|
41
|
+
action="store_true",
|
|
42
|
+
dest="global_verbose",
|
|
43
|
+
help="Enable verbose output",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--dry-run",
|
|
48
|
+
action="store_true",
|
|
49
|
+
dest="global_dry_run",
|
|
50
|
+
help="Run without making changes (for API operations)",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create subparsers
|
|
54
|
+
subparsers = parser.add_subparsers(
|
|
55
|
+
title="subcommands",
|
|
56
|
+
dest="command",
|
|
57
|
+
help="Available commands",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Add subcommands
|
|
61
|
+
validate.add_subparser(subparsers)
|
|
62
|
+
ingest.add_subparser(subparsers)
|
|
63
|
+
state_diagram.add_subparser(subparsers)
|
|
64
|
+
regression_graph.add_subparser(subparsers)
|
|
65
|
+
heatmap.add_subparser(subparsers)
|
|
66
|
+
gate.add_subparser(subparsers)
|
|
67
|
+
export.add_subparser(subparsers)
|
|
68
|
+
import_results.add_subparser(subparsers)
|
|
69
|
+
run.add_subparser(subparsers)
|
|
70
|
+
coverage.add_subparser(subparsers)
|
|
71
|
+
evaluate.add_subparser(subparsers)
|
|
72
|
+
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: list[str] | None = None) -> int:
|
|
77
|
+
"""Main entry point."""
|
|
78
|
+
parser = create_parser()
|
|
79
|
+
args = parser.parse_args(argv)
|
|
80
|
+
|
|
81
|
+
if args.command is None:
|
|
82
|
+
parser.print_help()
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
# Propagate top-level --dry-run and --verbose to subcommands
|
|
86
|
+
# This allows both `bb-harness --dry-run export ...` and `bb-harness export ... --dry-run`
|
|
87
|
+
top_dry_run = getattr(args, "global_dry_run", False)
|
|
88
|
+
top_verbose = getattr(args, "global_verbose", False)
|
|
89
|
+
|
|
90
|
+
# Dispatch to subcommand
|
|
91
|
+
dispatch_map = {
|
|
92
|
+
"validate": validate.run,
|
|
93
|
+
"ingest": ingest.run,
|
|
94
|
+
"state-diagram": state_diagram.run,
|
|
95
|
+
"regression-graph": regression_graph.run,
|
|
96
|
+
"heatmap": heatmap.run,
|
|
97
|
+
"gate": gate.run,
|
|
98
|
+
"export": export.run,
|
|
99
|
+
"import": import_results.run,
|
|
100
|
+
"run": run.run,
|
|
101
|
+
"coverage": coverage.run,
|
|
102
|
+
"evaluate": evaluate.run,
|
|
103
|
+
"migrate": coverage.run,
|
|
104
|
+
"bind-cases": coverage.run,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
handler = dispatch_map.get(args.command)
|
|
108
|
+
if handler:
|
|
109
|
+
# Ensure subcommand sees top-level flags (logical OR with subcommand's own flags)
|
|
110
|
+
if top_dry_run:
|
|
111
|
+
args.dry_run = True
|
|
112
|
+
if top_verbose:
|
|
113
|
+
args.verbose = True
|
|
114
|
+
return handler(args)
|
|
115
|
+
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""bb-harness command modules."""
|
|
2
|
+
|
|
3
|
+
from bb_harness.commands import (
|
|
4
|
+
export,
|
|
5
|
+
gate,
|
|
6
|
+
heatmap,
|
|
7
|
+
import_results,
|
|
8
|
+
ingest,
|
|
9
|
+
regression_graph,
|
|
10
|
+
run,
|
|
11
|
+
state_diagram,
|
|
12
|
+
validate,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"export",
|
|
17
|
+
"gate",
|
|
18
|
+
"heatmap",
|
|
19
|
+
"import_results",
|
|
20
|
+
"ingest",
|
|
21
|
+
"regression_graph",
|
|
22
|
+
"run",
|
|
23
|
+
"state_diagram",
|
|
24
|
+
"validate",
|
|
25
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Invoke argparse-based packaged tools without spawning a subprocess."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def invoke_tool(
|
|
11
|
+
main: Callable[[], int],
|
|
12
|
+
argv: list[str],
|
|
13
|
+
args: argparse.Namespace | None = None,
|
|
14
|
+
) -> int:
|
|
15
|
+
effective = list(argv)
|
|
16
|
+
if args is not None and getattr(args, "dry_run", False):
|
|
17
|
+
effective.append("--dry-run")
|
|
18
|
+
if args is not None and getattr(args, "verbose", False):
|
|
19
|
+
command = " ".join(effective)
|
|
20
|
+
print(f"[verbose] Running: {main.__module__} {command}", file=sys.stderr)
|
|
21
|
+
previous = sys.argv
|
|
22
|
+
sys.argv = [main.__module__, *effective]
|
|
23
|
+
try:
|
|
24
|
+
return main()
|
|
25
|
+
finally:
|
|
26
|
+
sys.argv = previous
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""被覆検証と非破壊artifact移行のCLI。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from bb_harness.artifact_migration import migrate_artifact
|
|
9
|
+
from bb_harness.coverage_engine import build_coverage_report, build_technique_plan
|
|
10
|
+
from bb_harness.evidence_revisions import bind_case_set
|
|
11
|
+
from bb_harness.gate_engine import load_evidence_files
|
|
12
|
+
from bb_harness.tools.validate_artifact import ARTIFACT_SCHEMA_MAP
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
16
|
+
parser = subparsers.add_parser("coverage", help="型付きモデルの設計・実施被覆を検証する")
|
|
17
|
+
for name in ("feature", "test-model", "observations", "risk", "cases"):
|
|
18
|
+
parser.add_argument("--" + name, type=Path, required=True)
|
|
19
|
+
parser.add_argument("--evidence", type=Path)
|
|
20
|
+
parser.add_argument("--build-id", default="unexecuted")
|
|
21
|
+
parser.add_argument("--output", type=Path, required=True, help="新規出力ディレクトリ")
|
|
22
|
+
parser = subparsers.add_parser("migrate", help="artifactを別ファイルへ非破壊移行する")
|
|
23
|
+
parser.add_argument("--input", type=Path, required=True)
|
|
24
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
25
|
+
parser.add_argument("--type", choices=sorted(ARTIFACT_SCHEMA_MAP), required=True)
|
|
26
|
+
parser.add_argument("--artifact-version", choices=["legacy", "enhanced"], default="enhanced")
|
|
27
|
+
parser = subparsers.add_parser(
|
|
28
|
+
"bind-cases", help="現在のケースとモデルの版を別ファイルへ固定する"
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--input", type=Path, required=True)
|
|
31
|
+
parser.add_argument("--test-model", type=Path, required=True)
|
|
32
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read(path: Path) -> dict:
|
|
36
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write(path: Path, value: dict) -> None:
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
with path.open("x", encoding="utf-8") as handle:
|
|
42
|
+
json.dump(value, handle, ensure_ascii=False, indent=2, allow_nan=False)
|
|
43
|
+
handle.write("\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run(args: argparse.Namespace) -> int:
|
|
47
|
+
try:
|
|
48
|
+
if args.command == "bind-cases":
|
|
49
|
+
if args.output.resolve() in {args.input.resolve(), args.test_model.resolve()}:
|
|
50
|
+
raise ValueError("入力ファイルの上書きは禁止")
|
|
51
|
+
_write(args.output, bind_case_set(_read(args.input), _read(args.test_model)))
|
|
52
|
+
elif args.command == "migrate":
|
|
53
|
+
if args.input.resolve() == args.output.resolve():
|
|
54
|
+
raise ValueError("移行元の上書きは禁止")
|
|
55
|
+
result = migrate_artifact(
|
|
56
|
+
_read(args.input), args.type, artifact_version=args.artifact_version
|
|
57
|
+
)
|
|
58
|
+
_write(args.output, result)
|
|
59
|
+
else:
|
|
60
|
+
feature, model, observations, risks, cases = (
|
|
61
|
+
_read(path)
|
|
62
|
+
for path in (
|
|
63
|
+
args.feature,
|
|
64
|
+
args.test_model,
|
|
65
|
+
args.observations,
|
|
66
|
+
args.risk,
|
|
67
|
+
args.cases,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
plan = build_technique_plan(feature, model, observations, risks)
|
|
71
|
+
report = build_coverage_report(
|
|
72
|
+
model,
|
|
73
|
+
plan,
|
|
74
|
+
cases,
|
|
75
|
+
load_evidence_files(args.evidence) if args.evidence else [],
|
|
76
|
+
build_id=args.build_id,
|
|
77
|
+
)
|
|
78
|
+
if args.output.exists():
|
|
79
|
+
raise ValueError("被覆出力には未存在のディレクトリを指定する")
|
|
80
|
+
_write(args.output / "technique_plan.json", plan)
|
|
81
|
+
_write(args.output / "coverage_report.json", report)
|
|
82
|
+
print(f"Generated: {args.output}")
|
|
83
|
+
return 0
|
|
84
|
+
except (OSError, ValueError, TypeError, KeyError) as exc:
|
|
85
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
86
|
+
return 1
|