sourcebound 1.2.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.
- clean_docs/__init__.py +26 -0
- clean_docs/__main__.py +3 -0
- clean_docs/accessibility.py +182 -0
- clean_docs/adapters/__init__.py +1 -0
- clean_docs/adapters/event_capture.py +56 -0
- clean_docs/adapters/mdx_dependencies.json +714 -0
- clean_docs/adapters/mdx_parser.mjs +29992 -0
- clean_docs/applicability.py +330 -0
- clean_docs/audit.py +1120 -0
- clean_docs/bootstrap.py +507 -0
- clean_docs/capabilities.py +200 -0
- clean_docs/changed.py +452 -0
- clean_docs/claims.py +840 -0
- clean_docs/cli.py +1612 -0
- clean_docs/context.py +307 -0
- clean_docs/corpus.py +377 -0
- clean_docs/demo.py +369 -0
- clean_docs/doctor.py +184 -0
- clean_docs/emit/__init__.py +4 -0
- clean_docs/emit/llms_txt.py +102 -0
- clean_docs/emit/stepwise.py +168 -0
- clean_docs/engine.py +324 -0
- clean_docs/errors.py +20 -0
- clean_docs/evaluation.py +867 -0
- clean_docs/execution.py +138 -0
- clean_docs/explain.py +123 -0
- clean_docs/extractors/__init__.py +19 -0
- clean_docs/extractors/command.py +51 -0
- clean_docs/extractors/inventory.py +176 -0
- clean_docs/extractors/json_pointer.py +84 -0
- clean_docs/extractors/python_literal.py +104 -0
- clean_docs/extractors/static.py +111 -0
- clean_docs/feedback.py +1390 -0
- clean_docs/impact.py +1624 -0
- clean_docs/improvements.py +1178 -0
- clean_docs/inventory.py +474 -0
- clean_docs/isolation.py +157 -0
- clean_docs/manifest.py +898 -0
- clean_docs/mdx.py +272 -0
- clean_docs/migration.py +121 -0
- clean_docs/models.py +194 -0
- clean_docs/outcomes.py +296 -0
- clean_docs/performance.py +123 -0
- clean_docs/phrasing.py +448 -0
- clean_docs/plugins.py +249 -0
- clean_docs/policy.py +536 -0
- clean_docs/projections.py +255 -0
- clean_docs/regions.py +75 -0
- clean_docs/release.py +232 -0
- clean_docs/renderers.py +57 -0
- clean_docs/residue.py +311 -0
- clean_docs/review_contracts.py +862 -0
- clean_docs/review_ledger.py +297 -0
- clean_docs/review_limits.py +9 -0
- clean_docs/sensitivity.py +602 -0
- clean_docs/snapshot.py +212 -0
- clean_docs/standard.py +281 -0
- clean_docs/standards/default.json +309 -0
- clean_docs/standards/exemplars.md +87 -0
- clean_docs/standards/v0-migrations.json +26 -0
- clean_docs/symbols.py +47 -0
- clean_docs/templates.py +96 -0
- clean_docs/verdict.py +1397 -0
- clean_docs/visuals.py +346 -0
- clean_docs/write_gate.py +170 -0
- sourcebound-1.2.1.dist-info/LICENSE +21 -0
- sourcebound-1.2.1.dist-info/METADATA +109 -0
- sourcebound-1.2.1.dist-info/RECORD +71 -0
- sourcebound-1.2.1.dist-info/WHEEL +5 -0
- sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
- sourcebound-1.2.1.dist-info/top_level.txt +1 -0
clean_docs/cli.py
ADDED
|
@@ -0,0 +1,1612 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from clean_docs import __version__
|
|
10
|
+
from clean_docs.applicability import ROLE_DESCRIPTIONS
|
|
11
|
+
from clean_docs.audit import AUDIT_BASELINE_PATH, audit, write_audit_baseline
|
|
12
|
+
from clean_docs.bootstrap import BootstrapPlan, apply_bootstrap_plan, build_bootstrap_plan
|
|
13
|
+
from clean_docs.capabilities import CLI_REFERENCE
|
|
14
|
+
from clean_docs.changed import check_changed, render_sarif
|
|
15
|
+
from clean_docs.claims import claim_binding_results, scan_source_claims
|
|
16
|
+
from clean_docs.context import compile_context
|
|
17
|
+
from clean_docs.doctor import build_diagnostic_bundle
|
|
18
|
+
from clean_docs.emit import emit_llms_txt, emit_stepwise_skill
|
|
19
|
+
from clean_docs.engine import drive, evaluate, write_results
|
|
20
|
+
from clean_docs.evaluation import run_evaluation, write_evaluation_history
|
|
21
|
+
from clean_docs.errors import CleanDocsError, ConfigurationError
|
|
22
|
+
from clean_docs.execution import ExecutionPolicy
|
|
23
|
+
from clean_docs.explain import explain
|
|
24
|
+
from clean_docs.feedback import (
|
|
25
|
+
disable_feedback,
|
|
26
|
+
enable_feedback,
|
|
27
|
+
enqueue_feedback,
|
|
28
|
+
flush_feedback,
|
|
29
|
+
ingest_behavior_signal,
|
|
30
|
+
load_behavior_signal,
|
|
31
|
+
load_feedback_config,
|
|
32
|
+
preview_feedback,
|
|
33
|
+
prepare_behavior_signal,
|
|
34
|
+
purge_feedback,
|
|
35
|
+
rotate_feedback_identity,
|
|
36
|
+
transition_improvement_case,
|
|
37
|
+
)
|
|
38
|
+
from clean_docs.impact import build_impact_plan, render_impact_plan
|
|
39
|
+
from clean_docs.improvements import (
|
|
40
|
+
LIFECYCLE_EVIDENCE_KINDS,
|
|
41
|
+
LIFECYCLE_STATES,
|
|
42
|
+
LifecycleEvidence,
|
|
43
|
+
check_candidate_lifecycle,
|
|
44
|
+
initialize_candidate_lifecycle,
|
|
45
|
+
load_candidate_lifecycle,
|
|
46
|
+
load_review_candidates,
|
|
47
|
+
transition_candidate_lifecycle,
|
|
48
|
+
write_candidate_lifecycle,
|
|
49
|
+
write_improvement_candidates,
|
|
50
|
+
)
|
|
51
|
+
from clean_docs.inventory import scan_inventory
|
|
52
|
+
from clean_docs.plugins import scan_extended_inventory
|
|
53
|
+
from clean_docs.manifest import load_manifest
|
|
54
|
+
from clean_docs.models import BindingResult
|
|
55
|
+
from clean_docs.migration import apply_migration, build_migration_plan, rollback_migration
|
|
56
|
+
from clean_docs.outcomes import build_outcome_receipt
|
|
57
|
+
from clean_docs.performance import benchmark_changed_check
|
|
58
|
+
from clean_docs.phrasing import (
|
|
59
|
+
CommandPhrasingProvider,
|
|
60
|
+
PhrasingProvider,
|
|
61
|
+
RecordedProvider,
|
|
62
|
+
load_command_phrasing_provider,
|
|
63
|
+
prepare_command_proposer_transcript_path,
|
|
64
|
+
write_command_proposer_transcript,
|
|
65
|
+
)
|
|
66
|
+
from clean_docs.projections import evaluate_projections, write_projections
|
|
67
|
+
from clean_docs.release import (
|
|
68
|
+
build_release_report,
|
|
69
|
+
render_release_markdown,
|
|
70
|
+
validate_release_narrative,
|
|
71
|
+
)
|
|
72
|
+
from clean_docs.regions import atomic_write
|
|
73
|
+
from clean_docs.review_ledger import validate_review_event_ledger
|
|
74
|
+
from clean_docs.review_ledger import initialize_review_event_ledger, write_review_event_ledger
|
|
75
|
+
from clean_docs.residue import LOCAL_CONFIG_NAME, load_local_residue_rules
|
|
76
|
+
from clean_docs.sensitivity import (
|
|
77
|
+
decode_json_object,
|
|
78
|
+
evaluate_binding_sensitivity,
|
|
79
|
+
load_json_object,
|
|
80
|
+
)
|
|
81
|
+
from clean_docs.standard import compile_standard, pack_matches_standard, write_pack
|
|
82
|
+
from clean_docs.snapshot import RepositorySnapshot
|
|
83
|
+
from clean_docs.verdict import (
|
|
84
|
+
VERDICT_SCHEMA,
|
|
85
|
+
build_pr_verdict,
|
|
86
|
+
render_verdict_sarif,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _command_help(command: str) -> str:
|
|
91
|
+
return next(item["job"] for item in CLI_REFERENCE if item["command"] == command)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parser() -> argparse.ArgumentParser:
|
|
95
|
+
parser = argparse.ArgumentParser(prog="sourcebound")
|
|
96
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
97
|
+
parser.add_argument("--root", type=Path, default=Path.cwd(), help="repository root")
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--manifest", type=Path, default=Path(".sourcebound.yml"), help="manifest path"
|
|
100
|
+
)
|
|
101
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
102
|
+
audit_parser = sub.add_parser("audit", help=_command_help("audit"))
|
|
103
|
+
audit_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
104
|
+
audit_parser.add_argument(
|
|
105
|
+
"--update-baseline",
|
|
106
|
+
action="store_true",
|
|
107
|
+
help="replace the exact existing-debt baseline with current findings",
|
|
108
|
+
)
|
|
109
|
+
audit_parser.add_argument(
|
|
110
|
+
"--preview-policy",
|
|
111
|
+
action="store_true",
|
|
112
|
+
help="report compatible house-policy candidates without accepting them as gates",
|
|
113
|
+
)
|
|
114
|
+
residue = sub.add_parser("residue", help="manage private residue matching")
|
|
115
|
+
residue_sub = residue.add_subparsers(dest="residue_command", required=True)
|
|
116
|
+
residue_sub.add_parser("status", help="report whether private matching is active")
|
|
117
|
+
residue_sub.add_parser("init-local", help="create a permission-restricted local rule template")
|
|
118
|
+
inventory_parser = sub.add_parser("inventory", help=_command_help("inventory"))
|
|
119
|
+
inventory_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
120
|
+
inventory_parser.add_argument(
|
|
121
|
+
"--no-exec",
|
|
122
|
+
action="store_true",
|
|
123
|
+
help="skip repository-declared discoverer plugins",
|
|
124
|
+
)
|
|
125
|
+
claims_parser = sub.add_parser("claims", help=_command_help("claims"))
|
|
126
|
+
claims_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
127
|
+
binding_parser = sub.add_parser("binding", help=_command_help("binding"))
|
|
128
|
+
binding_sub = binding_parser.add_subparsers(
|
|
129
|
+
dest="binding_command",
|
|
130
|
+
required=True,
|
|
131
|
+
)
|
|
132
|
+
binding_sensitivity = binding_sub.add_parser(
|
|
133
|
+
"sensitivity",
|
|
134
|
+
help=_command_help("binding sensitivity"),
|
|
135
|
+
)
|
|
136
|
+
binding_sensitivity.add_argument(
|
|
137
|
+
"--proposal",
|
|
138
|
+
type=Path,
|
|
139
|
+
required=True,
|
|
140
|
+
help="proposal JSON path, or - for standard input",
|
|
141
|
+
)
|
|
142
|
+
binding_sensitivity.add_argument(
|
|
143
|
+
"--fact",
|
|
144
|
+
type=Path,
|
|
145
|
+
required=True,
|
|
146
|
+
help="independently frozen mutation-target JSON",
|
|
147
|
+
)
|
|
148
|
+
binding_sensitivity.add_argument(
|
|
149
|
+
"--fact-sha256",
|
|
150
|
+
required=True,
|
|
151
|
+
help="expected SHA-256 of the complete mutation-target file",
|
|
152
|
+
)
|
|
153
|
+
binding_sensitivity.add_argument(
|
|
154
|
+
"--format",
|
|
155
|
+
choices=("text", "json"),
|
|
156
|
+
default="json",
|
|
157
|
+
)
|
|
158
|
+
context_parser = sub.add_parser("context")
|
|
159
|
+
context_sub = context_parser.add_subparsers(dest="context_command", required=True)
|
|
160
|
+
context_compile = context_sub.add_parser(
|
|
161
|
+
"compile", help=_command_help("context compile")
|
|
162
|
+
)
|
|
163
|
+
context_compile.add_argument("--request", type=Path, required=True)
|
|
164
|
+
context_compile.add_argument("--format", choices=("text", "json"), default="json")
|
|
165
|
+
review_parser = sub.add_parser("review", help=_command_help("review"))
|
|
166
|
+
review_sub = review_parser.add_subparsers(dest="review_command", required=True)
|
|
167
|
+
review_candidates = review_sub.add_parser(
|
|
168
|
+
"candidates",
|
|
169
|
+
help=_command_help("review candidates"),
|
|
170
|
+
)
|
|
171
|
+
review_candidates.add_argument("--input", type=Path, required=True)
|
|
172
|
+
review_candidates.add_argument(
|
|
173
|
+
"--ledger",
|
|
174
|
+
type=Path,
|
|
175
|
+
help="append-only review-event ledger that supplies the candidate denominator",
|
|
176
|
+
)
|
|
177
|
+
review_candidates.add_argument(
|
|
178
|
+
"--prior-ledger",
|
|
179
|
+
type=Path,
|
|
180
|
+
help="immutable base ledger whose events the current ledger must preserve",
|
|
181
|
+
)
|
|
182
|
+
review_candidates.add_argument("--out", type=Path)
|
|
183
|
+
review_candidates.add_argument(
|
|
184
|
+
"--check",
|
|
185
|
+
action="store_true",
|
|
186
|
+
help="exit 1 instead of rewriting a stale --out candidate set",
|
|
187
|
+
)
|
|
188
|
+
review_candidates.add_argument(
|
|
189
|
+
"--format",
|
|
190
|
+
choices=("text", "json"),
|
|
191
|
+
default="json",
|
|
192
|
+
)
|
|
193
|
+
review_ledger = review_sub.add_parser(
|
|
194
|
+
"ledger",
|
|
195
|
+
help=_command_help("review ledger"),
|
|
196
|
+
)
|
|
197
|
+
review_ledger_sub = review_ledger.add_subparsers(
|
|
198
|
+
dest="review_ledger_command",
|
|
199
|
+
required=True,
|
|
200
|
+
)
|
|
201
|
+
ledger_init = review_ledger_sub.add_parser(
|
|
202
|
+
"init",
|
|
203
|
+
help=_command_help("review ledger init"),
|
|
204
|
+
)
|
|
205
|
+
ledger_init.add_argument("--input", type=Path, required=True)
|
|
206
|
+
ledger_init.add_argument("--out", type=Path, required=True)
|
|
207
|
+
ledger_init.add_argument(
|
|
208
|
+
"--force",
|
|
209
|
+
action="store_true",
|
|
210
|
+
help="replace an existing unpublished ledger",
|
|
211
|
+
)
|
|
212
|
+
ledger_init.add_argument("--format", choices=("text", "json"), default="json")
|
|
213
|
+
review_lifecycle = review_sub.add_parser(
|
|
214
|
+
"lifecycle",
|
|
215
|
+
help=_command_help("review lifecycle"),
|
|
216
|
+
)
|
|
217
|
+
review_lifecycle_sub = review_lifecycle.add_subparsers(
|
|
218
|
+
dest="review_lifecycle_command",
|
|
219
|
+
required=True,
|
|
220
|
+
)
|
|
221
|
+
lifecycle_init = review_lifecycle_sub.add_parser(
|
|
222
|
+
"init",
|
|
223
|
+
help=_command_help("review lifecycle init"),
|
|
224
|
+
)
|
|
225
|
+
lifecycle_init.add_argument("--input", type=Path, required=True)
|
|
226
|
+
lifecycle_init.add_argument("--out", type=Path, required=True)
|
|
227
|
+
lifecycle_init.add_argument(
|
|
228
|
+
"--force",
|
|
229
|
+
action="store_true",
|
|
230
|
+
help="replace an existing lifecycle record",
|
|
231
|
+
)
|
|
232
|
+
lifecycle_init.add_argument("--format", choices=("text", "json"), default="json")
|
|
233
|
+
lifecycle_transition = review_lifecycle_sub.add_parser(
|
|
234
|
+
"transition",
|
|
235
|
+
help=_command_help("review lifecycle transition"),
|
|
236
|
+
)
|
|
237
|
+
lifecycle_transition.add_argument("--input", type=Path, required=True)
|
|
238
|
+
lifecycle_transition.add_argument("--state", type=Path, required=True)
|
|
239
|
+
lifecycle_transition.add_argument("--observation", required=True)
|
|
240
|
+
lifecycle_transition.add_argument("--to", choices=tuple(sorted(LIFECYCLE_STATES)), required=True)
|
|
241
|
+
lifecycle_transition.add_argument(
|
|
242
|
+
"--evidence-kind",
|
|
243
|
+
choices=tuple(sorted(LIFECYCLE_EVIDENCE_KINDS)),
|
|
244
|
+
required=True,
|
|
245
|
+
)
|
|
246
|
+
lifecycle_transition.add_argument("--reference", required=True)
|
|
247
|
+
lifecycle_transition.add_argument("--detail", required=True)
|
|
248
|
+
lifecycle_transition.add_argument(
|
|
249
|
+
"--format", choices=("text", "json"), default="json"
|
|
250
|
+
)
|
|
251
|
+
lifecycle_check = review_lifecycle_sub.add_parser(
|
|
252
|
+
"check",
|
|
253
|
+
help=_command_help("review lifecycle check"),
|
|
254
|
+
)
|
|
255
|
+
lifecycle_check.add_argument("--input", type=Path, required=True)
|
|
256
|
+
lifecycle_check.add_argument("--state", type=Path, required=True)
|
|
257
|
+
lifecycle_check.add_argument("--format", choices=("text", "json"), default="json")
|
|
258
|
+
init_parser = sub.add_parser("init", help=_command_help("init"))
|
|
259
|
+
model_mode = init_parser.add_mutually_exclusive_group()
|
|
260
|
+
model_mode.add_argument(
|
|
261
|
+
"--no-model", action="store_true", help="use deterministic adapters only"
|
|
262
|
+
)
|
|
263
|
+
model_mode.add_argument(
|
|
264
|
+
"--recorded-model-response",
|
|
265
|
+
type=Path,
|
|
266
|
+
help="replay a grounded JSON provider response",
|
|
267
|
+
)
|
|
268
|
+
model_mode.add_argument(
|
|
269
|
+
"--model-config",
|
|
270
|
+
type=Path,
|
|
271
|
+
help="YAML command-provider configuration for bounded init drafting",
|
|
272
|
+
)
|
|
273
|
+
init_parser.add_argument(
|
|
274
|
+
"--model-transcript",
|
|
275
|
+
type=Path,
|
|
276
|
+
help="repository-relative transcript path for an explicit command-provider run",
|
|
277
|
+
)
|
|
278
|
+
init_parser.add_argument(
|
|
279
|
+
"--dry-run", action="store_true", help="print the content plan without writing"
|
|
280
|
+
)
|
|
281
|
+
init_parser.add_argument(
|
|
282
|
+
"--accept-hygiene-baseline",
|
|
283
|
+
action="store_true",
|
|
284
|
+
help="record exact existing findings so mature repositories can adopt the gate",
|
|
285
|
+
)
|
|
286
|
+
init_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
287
|
+
explain_parser = sub.add_parser("explain", help=_command_help("explain"))
|
|
288
|
+
explain_parser.add_argument("identifier", help="policy rule or inventory id")
|
|
289
|
+
explain_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
290
|
+
doctor_parser = sub.add_parser("doctor", help=_command_help("doctor"))
|
|
291
|
+
doctor_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
292
|
+
doctor_parser.add_argument("--bundle", type=Path)
|
|
293
|
+
verify = sub.add_parser("verify", help=_command_help("verify"))
|
|
294
|
+
verify.add_argument("--base")
|
|
295
|
+
verify.add_argument("--head")
|
|
296
|
+
verify.add_argument("--project", type=Path, default=Path("."))
|
|
297
|
+
verify.add_argument("--out", type=Path)
|
|
298
|
+
verify.add_argument(
|
|
299
|
+
"--no-exec",
|
|
300
|
+
action="store_true",
|
|
301
|
+
help="skip repository-declared commands and plugins",
|
|
302
|
+
)
|
|
303
|
+
benchmark = sub.add_parser("benchmark", help=_command_help("benchmark"))
|
|
304
|
+
benchmark.add_argument("--base", required=True)
|
|
305
|
+
benchmark.add_argument("--head", required=True)
|
|
306
|
+
benchmark.add_argument("--project", type=Path, default=Path("."))
|
|
307
|
+
benchmark.add_argument("--iterations", type=int, default=7)
|
|
308
|
+
benchmark.add_argument("--out", type=Path)
|
|
309
|
+
derive = sub.add_parser("derive", help=_command_help("derive"))
|
|
310
|
+
derive_mode = derive.add_mutually_exclusive_group()
|
|
311
|
+
derive_mode.add_argument(
|
|
312
|
+
"--write", action="store_true", help="write derived regions atomically"
|
|
313
|
+
)
|
|
314
|
+
derive_mode.add_argument(
|
|
315
|
+
"--check", action="store_true", help="exit 1 when a region would change"
|
|
316
|
+
)
|
|
317
|
+
derive.add_argument("--binding", help="evaluate one binding id")
|
|
318
|
+
derive.add_argument("--ref", help="read bound sources from an immutable git ref")
|
|
319
|
+
derive.add_argument("--format", choices=("text", "json"), default="text")
|
|
320
|
+
drive_parser = sub.add_parser("drive", help=_command_help("drive"))
|
|
321
|
+
drive_parser.add_argument("--binding", help="repair one binding id")
|
|
322
|
+
drive_parser.add_argument("--ref", help="read bound sources from an immutable git ref")
|
|
323
|
+
drive_parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
324
|
+
plan = sub.add_parser("plan", help=_command_help("plan"))
|
|
325
|
+
plan.add_argument("--base", required=True, help="target branch or base git ref")
|
|
326
|
+
plan.add_argument("--head", required=True, help="change head git ref")
|
|
327
|
+
plan.add_argument(
|
|
328
|
+
"--project", type=Path, default=Path("."), help="monorepo project path"
|
|
329
|
+
)
|
|
330
|
+
plan.add_argument("--no-cache", action="store_true", help="bypass immutable-ref cache")
|
|
331
|
+
plan.add_argument("--format", choices=("text", "json"), default="text")
|
|
332
|
+
plan.add_argument(
|
|
333
|
+
"--no-exec",
|
|
334
|
+
action="store_true",
|
|
335
|
+
help="skip repository-declared commands and plugins",
|
|
336
|
+
)
|
|
337
|
+
verdict = sub.add_parser("verdict", help=_command_help("verdict"))
|
|
338
|
+
verdict.add_argument("--base", required=True, help="target branch or base git ref")
|
|
339
|
+
verdict.add_argument("--head", required=True, help="checked-out change head git ref")
|
|
340
|
+
verdict.add_argument(
|
|
341
|
+
"--mutation-receipt",
|
|
342
|
+
type=Path,
|
|
343
|
+
action="append",
|
|
344
|
+
default=[],
|
|
345
|
+
help="optional binding-sensitivity receipt to summarize",
|
|
346
|
+
)
|
|
347
|
+
verdict.add_argument("--format", choices=("json", "sarif"), default="json")
|
|
348
|
+
check = sub.add_parser("check", help=_command_help("check"))
|
|
349
|
+
check.add_argument("--binding", help="evaluate one binding id")
|
|
350
|
+
check.add_argument("--ref", help="read bound sources from an immutable git ref")
|
|
351
|
+
check.add_argument("--changed", action="store_true", help="check base-to-head impact")
|
|
352
|
+
check.add_argument("--base", help="base git ref for --changed")
|
|
353
|
+
check.add_argument("--head", help="head git ref for --changed")
|
|
354
|
+
check.add_argument("--project", type=Path, default=Path("."), help="monorepo project path")
|
|
355
|
+
check.add_argument("--no-cache", action="store_true", help="bypass immutable-ref cache")
|
|
356
|
+
check.add_argument("--format", choices=("text", "json", "sarif"), default="text")
|
|
357
|
+
check.add_argument(
|
|
358
|
+
"--no-exec",
|
|
359
|
+
action="store_true",
|
|
360
|
+
help="skip repository-declared commands and plugins",
|
|
361
|
+
)
|
|
362
|
+
project = sub.add_parser("project", help=_command_help("project"))
|
|
363
|
+
project.add_argument(
|
|
364
|
+
"--check", action="store_true", help="exit 1 instead of writing stale projections"
|
|
365
|
+
)
|
|
366
|
+
project.add_argument("--format", choices=("text", "json"), default="text")
|
|
367
|
+
evaluate_tasks = sub.add_parser("eval", help=_command_help("eval"))
|
|
368
|
+
evaluate_tasks.add_argument(
|
|
369
|
+
"--fixtures", type=Path, default=Path(".sourcebound/eval.yml")
|
|
370
|
+
)
|
|
371
|
+
evaluate_tasks.add_argument("--mode", choices=("replay", "live"), default="replay")
|
|
372
|
+
evaluate_tasks.add_argument("--record-dir", type=Path)
|
|
373
|
+
evaluate_tasks.add_argument("--history", type=Path)
|
|
374
|
+
evaluate_tasks.add_argument("--format", choices=("text", "json"), default="text")
|
|
375
|
+
release = sub.add_parser("release", help=_command_help("release"))
|
|
376
|
+
release.add_argument("--from", dest="from_ref", required=True, help="base git ref")
|
|
377
|
+
release.add_argument("--to", dest="to_ref", required=True, help="target git ref")
|
|
378
|
+
release.add_argument("--recorded-model-response", type=Path)
|
|
379
|
+
release.add_argument("--format", choices=("markdown", "json"), default="markdown")
|
|
380
|
+
migrate = sub.add_parser("migrate", help=_command_help("migrate"))
|
|
381
|
+
migration_mode = migrate.add_mutually_exclusive_group()
|
|
382
|
+
migration_mode.add_argument("--write", action="store_true")
|
|
383
|
+
migration_mode.add_argument("--rollback", action="store_true")
|
|
384
|
+
migrate.add_argument("--format", choices=("text", "json"), default="text")
|
|
385
|
+
feedback = sub.add_parser("feedback", help=_command_help("feedback"))
|
|
386
|
+
feedback_sub = feedback.add_subparsers(dest="feedback_command", required=True)
|
|
387
|
+
feedback_enable = feedback_sub.add_parser(
|
|
388
|
+
"enable",
|
|
389
|
+
help="enable feedback with a visible sink configuration",
|
|
390
|
+
)
|
|
391
|
+
feedback_enable.add_argument("--sink", choices=("local", "connected"), required=True)
|
|
392
|
+
feedback_enable.add_argument("--target")
|
|
393
|
+
feedback_enable.add_argument("--endpoint")
|
|
394
|
+
feedback_enable.add_argument("--token-env")
|
|
395
|
+
feedback_sub.add_parser("status", help="show feedback configuration and queue counts")
|
|
396
|
+
feedback_sub.add_parser("preview", help="write exact pending envelope bytes")
|
|
397
|
+
feedback_sub.add_parser("flush", help="deliver pending envelopes explicitly")
|
|
398
|
+
feedback_sub.add_parser("disable", help="remove delivery authority immediately")
|
|
399
|
+
feedback_sub.add_parser("rotate", help="replace the pseudonymous installation identifier")
|
|
400
|
+
feedback_sub.add_parser("purge", help="delete queued, delivered, and signal state")
|
|
401
|
+
feedback_signal = feedback_sub.add_parser(
|
|
402
|
+
"signal",
|
|
403
|
+
help="validate or ingest aggregate behavior signals",
|
|
404
|
+
)
|
|
405
|
+
feedback_signal_sub = feedback_signal.add_subparsers(
|
|
406
|
+
dest="feedback_signal_command",
|
|
407
|
+
required=True,
|
|
408
|
+
)
|
|
409
|
+
for signal_command in ("prepare", "validate", "ingest"):
|
|
410
|
+
signal_parser = feedback_signal_sub.add_parser(signal_command)
|
|
411
|
+
signal_parser.add_argument("--input", type=Path, required=True)
|
|
412
|
+
feedback_case = feedback_sub.add_parser(
|
|
413
|
+
"case",
|
|
414
|
+
help="advance a verified improvement case",
|
|
415
|
+
)
|
|
416
|
+
feedback_case_sub = feedback_case.add_subparsers(
|
|
417
|
+
dest="feedback_case_command",
|
|
418
|
+
required=True,
|
|
419
|
+
)
|
|
420
|
+
feedback_transition = feedback_case_sub.add_parser("transition")
|
|
421
|
+
feedback_transition.add_argument("--case", required=True)
|
|
422
|
+
feedback_transition.add_argument(
|
|
423
|
+
"--to",
|
|
424
|
+
required=True,
|
|
425
|
+
choices=(
|
|
426
|
+
"reproduced",
|
|
427
|
+
"root-cause-classified",
|
|
428
|
+
"evaluation-proposed",
|
|
429
|
+
"regression-added",
|
|
430
|
+
"shadow-measured",
|
|
431
|
+
"candidate-change",
|
|
432
|
+
"ordinary-verified-pr",
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
feedback_transition.add_argument("--receipt", type=Path, required=True)
|
|
436
|
+
emit = sub.add_parser("emit", help=_command_help("emit"))
|
|
437
|
+
emit_sub = emit.add_subparsers(dest="target", required=True)
|
|
438
|
+
stepwise = emit_sub.add_parser(
|
|
439
|
+
"stepwise-skill", help=_command_help("emit stepwise-skill")
|
|
440
|
+
)
|
|
441
|
+
stepwise.add_argument("--out", type=Path, default=Path("dist/stepwise-skill"))
|
|
442
|
+
stepwise.add_argument("--display-name", default="Keep documentation true")
|
|
443
|
+
stepwise.add_argument("--role", choices=("skill", "command"), default="skill")
|
|
444
|
+
stepwise.add_argument("--parent-command")
|
|
445
|
+
stepwise.add_argument("--command", dest="command_name")
|
|
446
|
+
llms = emit_sub.add_parser(
|
|
447
|
+
"llms-txt", help=_command_help("emit llms-txt")
|
|
448
|
+
)
|
|
449
|
+
llms.add_argument("--out", type=Path, default=Path("llms.txt"))
|
|
450
|
+
llms.add_argument("--title", default="Repository documentation")
|
|
451
|
+
llms.add_argument("--summary")
|
|
452
|
+
standard = sub.add_parser("standard", help=_command_help("standard"))
|
|
453
|
+
standard_sub = standard.add_subparsers(dest="standard_command", required=True)
|
|
454
|
+
for command in ("build", "check"):
|
|
455
|
+
item = standard_sub.add_parser(command, help=_command_help(f"standard {command}"))
|
|
456
|
+
item.add_argument("--source", type=Path, default=Path("STANDARD.md"))
|
|
457
|
+
item.add_argument(
|
|
458
|
+
"--output", type=Path, default=Path("src/clean_docs/standards/default.json")
|
|
459
|
+
)
|
|
460
|
+
return parser
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _validate_arguments(args: argparse.Namespace) -> None:
|
|
464
|
+
if (
|
|
465
|
+
args.command == "review"
|
|
466
|
+
and args.review_command == "candidates"
|
|
467
|
+
and args.check
|
|
468
|
+
and args.out is None
|
|
469
|
+
):
|
|
470
|
+
raise ConfigurationError("review candidates --check requires --out")
|
|
471
|
+
if (
|
|
472
|
+
args.command == "review"
|
|
473
|
+
and args.review_command == "candidates"
|
|
474
|
+
and args.prior_ledger is not None
|
|
475
|
+
and args.ledger is None
|
|
476
|
+
):
|
|
477
|
+
raise ConfigurationError("review candidates --prior-ledger requires --ledger")
|
|
478
|
+
if args.command != "check":
|
|
479
|
+
return
|
|
480
|
+
changed_only = (
|
|
481
|
+
args.base is not None
|
|
482
|
+
or args.head is not None
|
|
483
|
+
or args.project != Path(".")
|
|
484
|
+
or args.no_cache
|
|
485
|
+
)
|
|
486
|
+
if changed_only and not args.changed:
|
|
487
|
+
raise ConfigurationError(
|
|
488
|
+
"--base, --head, --project, and --no-cache require check --changed"
|
|
489
|
+
)
|
|
490
|
+
if args.changed and (args.base is None or args.head is None):
|
|
491
|
+
raise ConfigurationError("check --changed requires both --base and --head")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _json(results: list[BindingResult], *, repaired: bool = False) -> str:
|
|
495
|
+
assurance = {
|
|
496
|
+
"region": {
|
|
497
|
+
"source_evidence_checked": True,
|
|
498
|
+
"document_bytes_checked": True,
|
|
499
|
+
},
|
|
500
|
+
"command-pin": {
|
|
501
|
+
"command_output_checked": True,
|
|
502
|
+
"anchor_exists": True,
|
|
503
|
+
"anchored_prose_checked": False,
|
|
504
|
+
},
|
|
505
|
+
"symbol": {
|
|
506
|
+
"source_locator_exists": True,
|
|
507
|
+
"anchored_prose_checked": False,
|
|
508
|
+
},
|
|
509
|
+
"source-claim": {
|
|
510
|
+
"document_value_checked": True,
|
|
511
|
+
"source_value_checked": True,
|
|
512
|
+
},
|
|
513
|
+
"plugin": {
|
|
514
|
+
"plugin_executed": False,
|
|
515
|
+
"result_checked": False,
|
|
516
|
+
},
|
|
517
|
+
"projection": {
|
|
518
|
+
"configured_output_checked": True,
|
|
519
|
+
"source_document_prose_certified": False,
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
return json.dumps({
|
|
523
|
+
"ok": not any(
|
|
524
|
+
result.changed
|
|
525
|
+
and result.state != "skipped-untrusted-execution"
|
|
526
|
+
and not (repaired and result.binding_type == "region")
|
|
527
|
+
for result in results
|
|
528
|
+
),
|
|
529
|
+
"complete": not any(
|
|
530
|
+
result.state == "skipped-untrusted-execution" for result in results
|
|
531
|
+
),
|
|
532
|
+
"results": [
|
|
533
|
+
{
|
|
534
|
+
"binding": result.binding_id,
|
|
535
|
+
"mechanism": result.binding_type,
|
|
536
|
+
"doc": result.doc,
|
|
537
|
+
"status": result.state or (
|
|
538
|
+
"repaired" if repaired and result.changed
|
|
539
|
+
and result.binding_type == "region" else (
|
|
540
|
+
"drift" if result.changed else "current"
|
|
541
|
+
)),
|
|
542
|
+
"diff": result.diff,
|
|
543
|
+
"provenance": {
|
|
544
|
+
"ref": result.provenance.ref,
|
|
545
|
+
"path": result.provenance.path,
|
|
546
|
+
"locator": result.provenance.locator,
|
|
547
|
+
"extractor": result.provenance.extractor,
|
|
548
|
+
"digest": result.provenance.digest,
|
|
549
|
+
},
|
|
550
|
+
"assurance": {
|
|
551
|
+
**assurance[result.binding_type],
|
|
552
|
+
**(
|
|
553
|
+
{"anchored_prose_checked": result.prose_checked}
|
|
554
|
+
if result.binding_type == "command-pin"
|
|
555
|
+
else {}
|
|
556
|
+
),
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
for result in results
|
|
560
|
+
],
|
|
561
|
+
}, indent=2) + "\n"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _text(results: list[BindingResult], *, repaired: bool = False) -> str:
|
|
565
|
+
lines: list[str] = []
|
|
566
|
+
for result in results:
|
|
567
|
+
was_repaired = repaired and result.changed and result.binding_type == "region"
|
|
568
|
+
status = result.state or ("repaired" if was_repaired else (
|
|
569
|
+
"drift" if result.changed else "current"
|
|
570
|
+
))
|
|
571
|
+
lines.append(f"[{status}] {result.binding_id}: {result.doc}")
|
|
572
|
+
if result.diff:
|
|
573
|
+
lines.append(result.diff.rstrip())
|
|
574
|
+
return "\n".join(lines) + "\n"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _main(argv: list[str] | None = None) -> int:
|
|
578
|
+
args = _parser().parse_args(argv)
|
|
579
|
+
try:
|
|
580
|
+
_validate_arguments(args)
|
|
581
|
+
except CleanDocsError as exc:
|
|
582
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
583
|
+
return exc.exit_code
|
|
584
|
+
root = args.root.resolve()
|
|
585
|
+
if args.command == "residue":
|
|
586
|
+
local = root / LOCAL_CONFIG_NAME
|
|
587
|
+
try:
|
|
588
|
+
if args.residue_command == "init-local":
|
|
589
|
+
if local.exists():
|
|
590
|
+
raise ConfigurationError("local residue config already exists")
|
|
591
|
+
atomic_write(local, "version: 1\nrules: []\n")
|
|
592
|
+
local.chmod(0o600)
|
|
593
|
+
print(f"residue: initialized {LOCAL_CONFIG_NAME}")
|
|
594
|
+
return 0
|
|
595
|
+
active = bool(load_local_residue_rules(local))
|
|
596
|
+
except CleanDocsError as exc:
|
|
597
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
598
|
+
return exc.exit_code
|
|
599
|
+
print(f"residue: private matching {'active' if active else 'inactive'}")
|
|
600
|
+
return 0
|
|
601
|
+
if args.command == "review":
|
|
602
|
+
try:
|
|
603
|
+
source = args.input if args.input.is_absolute() else root / args.input
|
|
604
|
+
candidates = load_review_candidates(source, root=root)
|
|
605
|
+
if args.review_command == "candidates":
|
|
606
|
+
if args.ledger is not None:
|
|
607
|
+
ledger = args.ledger if args.ledger.is_absolute() else root / args.ledger
|
|
608
|
+
prior_ledger = (
|
|
609
|
+
args.prior_ledger
|
|
610
|
+
if args.prior_ledger is None or args.prior_ledger.is_absolute()
|
|
611
|
+
else root / args.prior_ledger
|
|
612
|
+
)
|
|
613
|
+
validate_review_event_ledger(ledger, candidates, prior_path=prior_ledger)
|
|
614
|
+
rendered = json.dumps(
|
|
615
|
+
candidates.as_dict(),
|
|
616
|
+
indent=2,
|
|
617
|
+
ensure_ascii=False,
|
|
618
|
+
) + "\n"
|
|
619
|
+
if args.out is None:
|
|
620
|
+
print(rendered, end="")
|
|
621
|
+
return 0
|
|
622
|
+
output = args.out if args.out.is_absolute() else root / args.out
|
|
623
|
+
try:
|
|
624
|
+
relative_output = output.resolve().relative_to(root)
|
|
625
|
+
except ValueError as exc:
|
|
626
|
+
raise ConfigurationError(
|
|
627
|
+
"review candidates --out must stay inside the repository"
|
|
628
|
+
) from exc
|
|
629
|
+
if args.check:
|
|
630
|
+
try:
|
|
631
|
+
observed = output.read_text(encoding="utf-8")
|
|
632
|
+
except FileNotFoundError:
|
|
633
|
+
observed = ""
|
|
634
|
+
except OSError as exc:
|
|
635
|
+
raise ConfigurationError(
|
|
636
|
+
f"cannot read improvement candidates {output}: {exc}"
|
|
637
|
+
) from exc
|
|
638
|
+
current = observed == rendered
|
|
639
|
+
if args.format == "json":
|
|
640
|
+
print(json.dumps({
|
|
641
|
+
"schema": "sourcebound.improvement-candidate-check.v1",
|
|
642
|
+
"ok": current,
|
|
643
|
+
"output": relative_output.as_posix(),
|
|
644
|
+
"candidate_digest": candidates.digest,
|
|
645
|
+
}, indent=2))
|
|
646
|
+
else:
|
|
647
|
+
print(
|
|
648
|
+
f"[{'current' if current else 'drift'}] "
|
|
649
|
+
f"{relative_output.as_posix()}"
|
|
650
|
+
)
|
|
651
|
+
return 0 if current else 1
|
|
652
|
+
write_improvement_candidates(candidates, output)
|
|
653
|
+
if args.format == "json":
|
|
654
|
+
print(rendered, end="")
|
|
655
|
+
else:
|
|
656
|
+
print(
|
|
657
|
+
f"[written] {relative_output.as_posix()}: "
|
|
658
|
+
f"{len(candidates.candidates)} candidate(s)"
|
|
659
|
+
)
|
|
660
|
+
return 0
|
|
661
|
+
|
|
662
|
+
if args.review_command == "ledger":
|
|
663
|
+
output = args.out if args.out.is_absolute() else root / args.out
|
|
664
|
+
try:
|
|
665
|
+
relative_output = output.resolve().relative_to(root)
|
|
666
|
+
except ValueError as exc:
|
|
667
|
+
raise ConfigurationError(
|
|
668
|
+
"review ledger output must stay inside the repository"
|
|
669
|
+
) from exc
|
|
670
|
+
if output.exists() and not args.force:
|
|
671
|
+
raise ConfigurationError(
|
|
672
|
+
"review ledger init refuses to replace an existing ledger; use --force"
|
|
673
|
+
)
|
|
674
|
+
ledger = initialize_review_event_ledger(candidates)
|
|
675
|
+
write_review_event_ledger(ledger, output)
|
|
676
|
+
if args.format == "json":
|
|
677
|
+
print(json.dumps(ledger.as_dict(), indent=2))
|
|
678
|
+
else:
|
|
679
|
+
print(
|
|
680
|
+
f"[written] {relative_output.as_posix()}: "
|
|
681
|
+
f"{len(ledger.events)} event(s)"
|
|
682
|
+
)
|
|
683
|
+
return 0
|
|
684
|
+
|
|
685
|
+
state_argument = args.out if args.review_lifecycle_command == "init" else args.state
|
|
686
|
+
state_path = (
|
|
687
|
+
state_argument if state_argument.is_absolute() else root / state_argument
|
|
688
|
+
)
|
|
689
|
+
try:
|
|
690
|
+
relative_state = state_path.resolve().relative_to(root)
|
|
691
|
+
except ValueError as exc:
|
|
692
|
+
raise ConfigurationError(
|
|
693
|
+
"review lifecycle state must stay inside the repository"
|
|
694
|
+
) from exc
|
|
695
|
+
if args.review_lifecycle_command == "init":
|
|
696
|
+
if state_path.exists() and not args.force:
|
|
697
|
+
raise ConfigurationError(
|
|
698
|
+
"review lifecycle init refuses to replace an existing state; use --force"
|
|
699
|
+
)
|
|
700
|
+
lifecycle = initialize_candidate_lifecycle(candidates)
|
|
701
|
+
write_candidate_lifecycle(lifecycle, state_path)
|
|
702
|
+
if args.format == "json":
|
|
703
|
+
print(json.dumps(lifecycle.as_dict(), indent=2))
|
|
704
|
+
else:
|
|
705
|
+
print(
|
|
706
|
+
f"[written] {relative_state.as_posix()}: "
|
|
707
|
+
f"{len(lifecycle.candidates)} candidate(s)"
|
|
708
|
+
)
|
|
709
|
+
return 0
|
|
710
|
+
if args.review_lifecycle_command == "check":
|
|
711
|
+
lifecycle = load_candidate_lifecycle(state_path, candidates)
|
|
712
|
+
unknown = check_candidate_lifecycle(lifecycle, root=root)
|
|
713
|
+
if args.format == "json":
|
|
714
|
+
print(json.dumps({
|
|
715
|
+
"schema": "sourcebound.improvement-candidate-lifecycle-check.v1",
|
|
716
|
+
"ok": not unknown,
|
|
717
|
+
"status": "current" if not unknown else "unknown",
|
|
718
|
+
"state": relative_state.as_posix(),
|
|
719
|
+
"candidate_digest": lifecycle.candidate_digest,
|
|
720
|
+
"lifecycle_digest": lifecycle.digest,
|
|
721
|
+
"unknown_evidence": list(unknown),
|
|
722
|
+
}, indent=2))
|
|
723
|
+
else:
|
|
724
|
+
status = "current" if not unknown else "unknown"
|
|
725
|
+
print(f"[{status}] {relative_state.as_posix()}")
|
|
726
|
+
return 0 if not unknown else 1
|
|
727
|
+
lifecycle = load_candidate_lifecycle(state_path, candidates)
|
|
728
|
+
lifecycle = transition_candidate_lifecycle(
|
|
729
|
+
lifecycle,
|
|
730
|
+
root=root,
|
|
731
|
+
observation_id=args.observation,
|
|
732
|
+
to_state=args.to,
|
|
733
|
+
evidence=LifecycleEvidence(
|
|
734
|
+
kind=args.evidence_kind,
|
|
735
|
+
reference=args.reference,
|
|
736
|
+
detail=args.detail,
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
write_candidate_lifecycle(lifecycle, state_path)
|
|
740
|
+
unknown = check_candidate_lifecycle(lifecycle, root=root)
|
|
741
|
+
if args.format == "json":
|
|
742
|
+
print(json.dumps(lifecycle.as_dict(), indent=2))
|
|
743
|
+
else:
|
|
744
|
+
status = "transitioned" if not unknown else "unknown"
|
|
745
|
+
print(f"[{status}] {args.observation}: {args.to} in {relative_state.as_posix()}")
|
|
746
|
+
return 0 if not unknown else 1
|
|
747
|
+
except CleanDocsError as exc:
|
|
748
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
749
|
+
return exc.exit_code
|
|
750
|
+
if args.command == "feedback":
|
|
751
|
+
try:
|
|
752
|
+
if args.feedback_command == "enable":
|
|
753
|
+
config = enable_feedback(
|
|
754
|
+
root,
|
|
755
|
+
sink=args.sink,
|
|
756
|
+
target=args.target,
|
|
757
|
+
endpoint=args.endpoint,
|
|
758
|
+
token_env=args.token_env,
|
|
759
|
+
)
|
|
760
|
+
print(json.dumps(config.as_dict(), indent=2))
|
|
761
|
+
return 0
|
|
762
|
+
if args.feedback_command == "status":
|
|
763
|
+
status_config = load_feedback_config(root)
|
|
764
|
+
pending = preview_feedback(root)
|
|
765
|
+
print(json.dumps({
|
|
766
|
+
"schema": "sourcebound.feedback-status.v1",
|
|
767
|
+
"configured": status_config is not None,
|
|
768
|
+
"enabled": (
|
|
769
|
+
status_config.enabled if status_config is not None else False
|
|
770
|
+
),
|
|
771
|
+
"sink": (
|
|
772
|
+
dict(status_config.sink)
|
|
773
|
+
if status_config is not None
|
|
774
|
+
else None
|
|
775
|
+
),
|
|
776
|
+
"pending_bytes": len(pending),
|
|
777
|
+
"pending_records": pending.count(b"\n"),
|
|
778
|
+
}, indent=2))
|
|
779
|
+
return 0
|
|
780
|
+
if args.feedback_command == "preview":
|
|
781
|
+
sys.stdout.buffer.write(preview_feedback(root))
|
|
782
|
+
return 0
|
|
783
|
+
if args.feedback_command == "flush":
|
|
784
|
+
flush_result = flush_feedback(root)
|
|
785
|
+
print(json.dumps({
|
|
786
|
+
"schema": "sourcebound.feedback-flush.v1",
|
|
787
|
+
**flush_result,
|
|
788
|
+
}, indent=2))
|
|
789
|
+
return 0 if flush_result["failed"] == 0 else 1
|
|
790
|
+
if args.feedback_command == "disable":
|
|
791
|
+
config = disable_feedback(root)
|
|
792
|
+
print(json.dumps(config.as_dict(), indent=2))
|
|
793
|
+
return 0
|
|
794
|
+
if args.feedback_command == "rotate":
|
|
795
|
+
config = rotate_feedback_identity(root)
|
|
796
|
+
print(json.dumps(config.as_dict(), indent=2))
|
|
797
|
+
return 0
|
|
798
|
+
if args.feedback_command == "signal":
|
|
799
|
+
signal_path = (
|
|
800
|
+
args.input if args.input.is_absolute() else root / args.input
|
|
801
|
+
)
|
|
802
|
+
if args.feedback_signal_command == "prepare":
|
|
803
|
+
try:
|
|
804
|
+
signal_body_raw = json.loads(
|
|
805
|
+
signal_path.read_text(encoding="utf-8")
|
|
806
|
+
)
|
|
807
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
808
|
+
raise ConfigurationError(
|
|
809
|
+
f"cannot read behavior signal body {signal_path}"
|
|
810
|
+
) from exc
|
|
811
|
+
if not isinstance(signal_body_raw, dict):
|
|
812
|
+
raise ConfigurationError(
|
|
813
|
+
"behavior signal body must be an object"
|
|
814
|
+
)
|
|
815
|
+
signal = prepare_behavior_signal(signal_body_raw)
|
|
816
|
+
print(json.dumps(signal, indent=2))
|
|
817
|
+
elif args.feedback_signal_command == "validate":
|
|
818
|
+
signal, _source = load_behavior_signal(signal_path)
|
|
819
|
+
print(json.dumps(signal, indent=2))
|
|
820
|
+
else:
|
|
821
|
+
case = ingest_behavior_signal(root, signal_path)
|
|
822
|
+
print(json.dumps(case, indent=2))
|
|
823
|
+
return 0
|
|
824
|
+
if args.feedback_command == "case":
|
|
825
|
+
receipt_path = (
|
|
826
|
+
args.receipt
|
|
827
|
+
if args.receipt.is_absolute()
|
|
828
|
+
else root / args.receipt
|
|
829
|
+
)
|
|
830
|
+
case = transition_improvement_case(
|
|
831
|
+
root,
|
|
832
|
+
case_id=args.case,
|
|
833
|
+
target_state=args.to,
|
|
834
|
+
receipt_path=receipt_path,
|
|
835
|
+
)
|
|
836
|
+
print(json.dumps(case, indent=2))
|
|
837
|
+
return 0
|
|
838
|
+
purge_feedback(root)
|
|
839
|
+
print("feedback: local state purged")
|
|
840
|
+
return 0
|
|
841
|
+
except CleanDocsError as exc:
|
|
842
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
843
|
+
return exc.exit_code
|
|
844
|
+
if args.command == "audit":
|
|
845
|
+
try:
|
|
846
|
+
if args.update_baseline:
|
|
847
|
+
write_audit_baseline(root)
|
|
848
|
+
report = audit(root, preview_policy=args.preview_policy)
|
|
849
|
+
except CleanDocsError as exc:
|
|
850
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
851
|
+
return exc.exit_code
|
|
852
|
+
if args.format == "json":
|
|
853
|
+
print(json.dumps({
|
|
854
|
+
"schema": "sourcebound.audit.v1",
|
|
855
|
+
"ok": report.ok,
|
|
856
|
+
"documents": list(report.documents),
|
|
857
|
+
"ignored_documents": list(report.ignored_documents),
|
|
858
|
+
"unsupported_documents": list(report.unsupported_documents),
|
|
859
|
+
"document_profiles": {
|
|
860
|
+
profile.path: profile.role
|
|
861
|
+
for profile in report.document_profiles
|
|
862
|
+
},
|
|
863
|
+
"registered_documents": [
|
|
864
|
+
profile.path
|
|
865
|
+
for profile in report.document_profiles
|
|
866
|
+
if profile.registered
|
|
867
|
+
],
|
|
868
|
+
"enforcement": {
|
|
869
|
+
"repository_integrity": report.repository_integrity_enforced,
|
|
870
|
+
"policy_documents": [
|
|
871
|
+
profile.path
|
|
872
|
+
for profile in report.document_profiles
|
|
873
|
+
if profile.registered
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
"policy_preview": report.policy_preview,
|
|
877
|
+
"role_definitions": ROLE_DESCRIPTIONS,
|
|
878
|
+
"findings": [asdict(finding) for finding in report.findings],
|
|
879
|
+
"advisories": [asdict(finding) for finding in report.advisories],
|
|
880
|
+
"advisory_totals": dict(report.advisory_totals),
|
|
881
|
+
"baselined_findings": [
|
|
882
|
+
asdict(finding) for finding in report.baselined_findings
|
|
883
|
+
],
|
|
884
|
+
"stale_baseline": [
|
|
885
|
+
asdict(finding) for finding in report.stale_baseline
|
|
886
|
+
],
|
|
887
|
+
}, indent=2))
|
|
888
|
+
else:
|
|
889
|
+
document_roles = {
|
|
890
|
+
profile.path: profile.role
|
|
891
|
+
for profile in report.document_profiles
|
|
892
|
+
}
|
|
893
|
+
for audit_finding in report.findings:
|
|
894
|
+
print(
|
|
895
|
+
f"[{audit_finding.rule}] {audit_finding.path}:{audit_finding.line} "
|
|
896
|
+
f"{audit_finding.detail}"
|
|
897
|
+
)
|
|
898
|
+
for advisory in report.advisories:
|
|
899
|
+
print(
|
|
900
|
+
f"[advisory:{advisory.rule} "
|
|
901
|
+
f"role={document_roles.get(advisory.path, 'unknown')}] "
|
|
902
|
+
f"{advisory.path}:{advisory.line} "
|
|
903
|
+
f"{advisory.detail}"
|
|
904
|
+
)
|
|
905
|
+
for stale_finding in report.stale_baseline:
|
|
906
|
+
print(
|
|
907
|
+
f"[stale-baseline] {stale_finding.path}:{stale_finding.line} "
|
|
908
|
+
f"{stale_finding.rule}: finding was resolved; update the baseline"
|
|
909
|
+
)
|
|
910
|
+
if args.update_baseline:
|
|
911
|
+
print(f"[updated] {AUDIT_BASELINE_PATH}")
|
|
912
|
+
print(
|
|
913
|
+
f"audit: {len(report.documents)} active document(s), "
|
|
914
|
+
f"{len(report.ignored_documents)} archived, "
|
|
915
|
+
f"{len(report.findings)} finding(s); "
|
|
916
|
+
f"{sum(count for _rule, count in report.advisory_totals)} "
|
|
917
|
+
f"advisory candidate(s), {len(report.advisories)} shown; "
|
|
918
|
+
f"{len(report.baselined_findings)} baselined; "
|
|
919
|
+
f"{len(report.stale_baseline)} stale; "
|
|
920
|
+
f"{len(report.unsupported_documents)} unsupported; "
|
|
921
|
+
"integrity "
|
|
922
|
+
f"{'enforced' if report.repository_integrity_enforced else 'assessment-only'}"
|
|
923
|
+
)
|
|
924
|
+
return 0 if report.ok else 1
|
|
925
|
+
if args.command == "inventory":
|
|
926
|
+
try:
|
|
927
|
+
manifest_path = root / args.manifest
|
|
928
|
+
manifest = load_manifest(manifest_path) if manifest_path.is_file() else None
|
|
929
|
+
discoverer_ids = (
|
|
930
|
+
sorted(
|
|
931
|
+
plugin.id
|
|
932
|
+
for plugin in manifest.plugins
|
|
933
|
+
if "discoverer" in plugin.interfaces
|
|
934
|
+
)
|
|
935
|
+
if manifest is not None
|
|
936
|
+
else []
|
|
937
|
+
)
|
|
938
|
+
inventory_report = (
|
|
939
|
+
scan_inventory(root)
|
|
940
|
+
if args.no_exec
|
|
941
|
+
else scan_extended_inventory(root)
|
|
942
|
+
)
|
|
943
|
+
except CleanDocsError as exc:
|
|
944
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
945
|
+
return exc.exit_code
|
|
946
|
+
if args.format == "json":
|
|
947
|
+
payload = inventory_report.as_dict()
|
|
948
|
+
payload["execution"] = {
|
|
949
|
+
"mode": (
|
|
950
|
+
ExecutionPolicy.STATIC_ONLY.value
|
|
951
|
+
if args.no_exec
|
|
952
|
+
else ExecutionPolicy.TRUSTED.value
|
|
953
|
+
),
|
|
954
|
+
"skipped_plugin_ids": discoverer_ids if args.no_exec else [],
|
|
955
|
+
}
|
|
956
|
+
print(json.dumps(payload, indent=2))
|
|
957
|
+
else:
|
|
958
|
+
for item in inventory_report.items:
|
|
959
|
+
print(f"[{item.coverage}] {item.kind} {item.name}: {item.source}#{item.locator}")
|
|
960
|
+
print(
|
|
961
|
+
f"inventory: {len(inventory_report.items)} surface(s); "
|
|
962
|
+
f"{len(inventory_report.languages)} language(s)"
|
|
963
|
+
)
|
|
964
|
+
return 0
|
|
965
|
+
if args.command == "claims":
|
|
966
|
+
manifest = args.manifest if args.manifest.is_absolute() else root / args.manifest
|
|
967
|
+
try:
|
|
968
|
+
source_claim_checks = (
|
|
969
|
+
load_manifest(manifest).source_claim_checks
|
|
970
|
+
if manifest.is_file()
|
|
971
|
+
else ()
|
|
972
|
+
)
|
|
973
|
+
claim_report = scan_source_claims(root, source_claim_checks)
|
|
974
|
+
except CleanDocsError as exc:
|
|
975
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
976
|
+
return exc.exit_code
|
|
977
|
+
if args.format == "json":
|
|
978
|
+
print(json.dumps(claim_report.as_dict(), indent=2))
|
|
979
|
+
else:
|
|
980
|
+
for missing in claim_report.missing:
|
|
981
|
+
print(
|
|
982
|
+
f"[required:missing] {missing.id} {missing.doc}#{missing.anchor}: "
|
|
983
|
+
f"{missing.detail}"
|
|
984
|
+
)
|
|
985
|
+
for claim_result in claim_report.results:
|
|
986
|
+
print(
|
|
987
|
+
f"[{'required' if claim_result.status == 'drift' else 'current'}:"
|
|
988
|
+
f"{claim_result.kind}] {claim_result.id} "
|
|
989
|
+
f"{claim_result.doc}:{claim_result.line} "
|
|
990
|
+
f"<- {claim_result.source}#{claim_result.locator}: "
|
|
991
|
+
f"{claim_result.detail}"
|
|
992
|
+
)
|
|
993
|
+
for claim_candidate in claim_report.candidates:
|
|
994
|
+
print(
|
|
995
|
+
f"[advisory:{claim_candidate.status}:{claim_candidate.kind}] "
|
|
996
|
+
f"{claim_candidate.id} "
|
|
997
|
+
f"{claim_candidate.doc}:{claim_candidate.line} "
|
|
998
|
+
f"<- {claim_candidate.source}#{claim_candidate.locator}: "
|
|
999
|
+
f"{claim_candidate.detail}"
|
|
1000
|
+
)
|
|
1001
|
+
print(
|
|
1002
|
+
f"claims: {len(claim_report.results)} enforced relationship(s), "
|
|
1003
|
+
f"{sum(count for _status, count in claim_report.candidate_totals)} "
|
|
1004
|
+
f"ranked candidate(s); {claim_report.authority}"
|
|
1005
|
+
)
|
|
1006
|
+
return 0 if claim_report.ok else 1
|
|
1007
|
+
if args.command == "binding":
|
|
1008
|
+
fact_path = args.fact if args.fact.is_absolute() else root / args.fact
|
|
1009
|
+
try:
|
|
1010
|
+
if args.proposal == Path("-"):
|
|
1011
|
+
proposal_bytes = sys.stdin.read().encode()
|
|
1012
|
+
proposal = decode_json_object(proposal_bytes, "proposal")
|
|
1013
|
+
else:
|
|
1014
|
+
proposal_path = (
|
|
1015
|
+
args.proposal
|
|
1016
|
+
if args.proposal.is_absolute()
|
|
1017
|
+
else root / args.proposal
|
|
1018
|
+
)
|
|
1019
|
+
proposal, proposal_bytes = load_json_object(
|
|
1020
|
+
proposal_path,
|
|
1021
|
+
"proposal",
|
|
1022
|
+
)
|
|
1023
|
+
fact, fact_bytes = load_json_object(fact_path, "fact")
|
|
1024
|
+
receipt = evaluate_binding_sensitivity(
|
|
1025
|
+
root,
|
|
1026
|
+
proposal,
|
|
1027
|
+
fact,
|
|
1028
|
+
proposal_bytes=proposal_bytes,
|
|
1029
|
+
fact_bytes=fact_bytes,
|
|
1030
|
+
expected_fact_file_sha256=args.fact_sha256,
|
|
1031
|
+
)
|
|
1032
|
+
except CleanDocsError as exc:
|
|
1033
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1034
|
+
return exc.exit_code
|
|
1035
|
+
if args.format == "json":
|
|
1036
|
+
print(json.dumps(receipt, indent=2))
|
|
1037
|
+
else:
|
|
1038
|
+
inputs = receipt["inputs"]
|
|
1039
|
+
assert isinstance(inputs, dict)
|
|
1040
|
+
relationship = inputs["relationship"]
|
|
1041
|
+
assert isinstance(relationship, dict)
|
|
1042
|
+
print(
|
|
1043
|
+
f"[{receipt['state']}] {relationship['id']}: "
|
|
1044
|
+
f"{receipt['detail']}"
|
|
1045
|
+
)
|
|
1046
|
+
print(
|
|
1047
|
+
"semantic relationship authorized: "
|
|
1048
|
+
f"{str(receipt['semantic_relationship_authorized']).lower()}"
|
|
1049
|
+
)
|
|
1050
|
+
return {
|
|
1051
|
+
"sensitive": 0,
|
|
1052
|
+
"insensitive": 1,
|
|
1053
|
+
"invalid": 2,
|
|
1054
|
+
"unsupported": 3,
|
|
1055
|
+
}[str(receipt["state"])]
|
|
1056
|
+
if args.command == "context":
|
|
1057
|
+
request = args.request if args.request.is_absolute() else root / args.request
|
|
1058
|
+
try:
|
|
1059
|
+
context_bundle = compile_context(root, request)
|
|
1060
|
+
except CleanDocsError as exc:
|
|
1061
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1062
|
+
return exc.exit_code
|
|
1063
|
+
if args.format == "json":
|
|
1064
|
+
print(json.dumps(context_bundle.as_dict(), indent=2))
|
|
1065
|
+
else:
|
|
1066
|
+
for context_item in context_bundle.items:
|
|
1067
|
+
print(
|
|
1068
|
+
f"[included:{context_item.authority}] {context_item.id} "
|
|
1069
|
+
f"{context_item.path}#{context_item.locator}: "
|
|
1070
|
+
f"{context_item.inclusion_reason}"
|
|
1071
|
+
)
|
|
1072
|
+
for excluded_context in context_bundle.excluded:
|
|
1073
|
+
print(
|
|
1074
|
+
f"[excluded:{excluded_context.reason}] "
|
|
1075
|
+
f"{excluded_context.id}: {excluded_context.path}"
|
|
1076
|
+
)
|
|
1077
|
+
print(
|
|
1078
|
+
f"context: {context_bundle.status}; "
|
|
1079
|
+
f"{context_bundle.used_bytes}/"
|
|
1080
|
+
f"{context_bundle.budget_bytes} bytes"
|
|
1081
|
+
)
|
|
1082
|
+
return 0 if context_bundle.ok else 2
|
|
1083
|
+
if args.command == "release":
|
|
1084
|
+
try:
|
|
1085
|
+
release_report = build_release_report(root, args.from_ref, args.to_ref)
|
|
1086
|
+
narrative = None
|
|
1087
|
+
if args.recorded_model_response is not None:
|
|
1088
|
+
response_path = args.recorded_model_response
|
|
1089
|
+
if not response_path.is_absolute():
|
|
1090
|
+
response_path = root / response_path
|
|
1091
|
+
try:
|
|
1092
|
+
response = response_path.read_text(encoding="utf-8")
|
|
1093
|
+
except OSError as exc:
|
|
1094
|
+
raise ConfigurationError(
|
|
1095
|
+
f"cannot read recorded release narrative {response_path}"
|
|
1096
|
+
) from exc
|
|
1097
|
+
narrative = validate_release_narrative(release_report, response)
|
|
1098
|
+
except CleanDocsError as exc:
|
|
1099
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1100
|
+
return exc.exit_code
|
|
1101
|
+
if args.format == "json":
|
|
1102
|
+
payload = release_report.as_dict()
|
|
1103
|
+
if narrative is not None:
|
|
1104
|
+
payload["narrative"] = narrative.as_dict()
|
|
1105
|
+
print(json.dumps(payload, indent=2))
|
|
1106
|
+
else:
|
|
1107
|
+
sys.stdout.write(render_release_markdown(release_report, narrative))
|
|
1108
|
+
return 0 if narrative is None or narrative.ok else 1
|
|
1109
|
+
if args.command == "migrate":
|
|
1110
|
+
manifest = args.manifest if args.manifest.is_absolute() else root / args.manifest
|
|
1111
|
+
try:
|
|
1112
|
+
if args.rollback:
|
|
1113
|
+
rollback_migration(manifest)
|
|
1114
|
+
print(f"migrate: restored {manifest}")
|
|
1115
|
+
return 0
|
|
1116
|
+
migration = build_migration_plan(manifest)
|
|
1117
|
+
if args.write:
|
|
1118
|
+
backup = apply_migration(manifest, migration)
|
|
1119
|
+
else:
|
|
1120
|
+
backup = None
|
|
1121
|
+
except CleanDocsError as exc:
|
|
1122
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1123
|
+
return exc.exit_code
|
|
1124
|
+
if args.format == "json":
|
|
1125
|
+
payload = migration.as_dict()
|
|
1126
|
+
payload["applied"] = args.write
|
|
1127
|
+
print(json.dumps(payload, indent=2))
|
|
1128
|
+
else:
|
|
1129
|
+
sys.stdout.write(migration.diff)
|
|
1130
|
+
state = "applied" if args.write else "planned"
|
|
1131
|
+
suffix = f"; backup {backup}" if backup is not None else ""
|
|
1132
|
+
print(f"migrate: {state} version 0 to 1{suffix}")
|
|
1133
|
+
return 0
|
|
1134
|
+
if args.command == "explain":
|
|
1135
|
+
try:
|
|
1136
|
+
explanation = explain(root, args.identifier)
|
|
1137
|
+
except CleanDocsError as exc:
|
|
1138
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1139
|
+
return exc.exit_code
|
|
1140
|
+
if args.format == "json":
|
|
1141
|
+
print(json.dumps(explanation.as_dict(), indent=2))
|
|
1142
|
+
else:
|
|
1143
|
+
print(f"[{explanation.state}] {explanation.id}: {explanation.summary}")
|
|
1144
|
+
if explanation.evidence:
|
|
1145
|
+
print(
|
|
1146
|
+
"evidence: "
|
|
1147
|
+
f"{explanation.evidence['source']}#{explanation.evidence['locator']} "
|
|
1148
|
+
f"via {explanation.evidence['adapter']} "
|
|
1149
|
+
f"sha256:{explanation.evidence['sha256']}"
|
|
1150
|
+
)
|
|
1151
|
+
print(f"repair: {explanation.repair}")
|
|
1152
|
+
return 0
|
|
1153
|
+
if args.command == "init":
|
|
1154
|
+
provider: PhrasingProvider | None = None
|
|
1155
|
+
plan: BootstrapPlan | None = None
|
|
1156
|
+
transcript_path = None
|
|
1157
|
+
try:
|
|
1158
|
+
if args.recorded_model_response:
|
|
1159
|
+
response_path = args.recorded_model_response
|
|
1160
|
+
if not response_path.is_absolute():
|
|
1161
|
+
response_path = root / response_path
|
|
1162
|
+
try:
|
|
1163
|
+
provider = RecordedProvider(response_path.read_text(encoding="utf-8"))
|
|
1164
|
+
except OSError as exc:
|
|
1165
|
+
raise ConfigurationError(
|
|
1166
|
+
f"cannot read recorded model response {response_path}"
|
|
1167
|
+
) from exc
|
|
1168
|
+
if args.model_config:
|
|
1169
|
+
config_path = args.model_config
|
|
1170
|
+
if not config_path.is_absolute():
|
|
1171
|
+
config_path = root / config_path
|
|
1172
|
+
provider = load_command_phrasing_provider(config_path, root)
|
|
1173
|
+
candidate_transcript = (
|
|
1174
|
+
args.model_transcript
|
|
1175
|
+
if args.model_transcript is not None
|
|
1176
|
+
else Path(".sourcebound/init-proposer-transcript.json")
|
|
1177
|
+
)
|
|
1178
|
+
prepare_command_proposer_transcript_path(root, candidate_transcript)
|
|
1179
|
+
transcript_path = candidate_transcript
|
|
1180
|
+
if args.model_transcript and not isinstance(provider, CommandPhrasingProvider):
|
|
1181
|
+
raise ConfigurationError(
|
|
1182
|
+
"--model-transcript requires --model-config"
|
|
1183
|
+
)
|
|
1184
|
+
plan = build_bootstrap_plan(
|
|
1185
|
+
root,
|
|
1186
|
+
provider,
|
|
1187
|
+
accept_hygiene_baseline=args.accept_hygiene_baseline,
|
|
1188
|
+
)
|
|
1189
|
+
if transcript_path is not None and isinstance(provider, CommandPhrasingProvider):
|
|
1190
|
+
write_command_proposer_transcript(
|
|
1191
|
+
root,
|
|
1192
|
+
transcript_path,
|
|
1193
|
+
provider,
|
|
1194
|
+
state="accepted",
|
|
1195
|
+
outcome="accept",
|
|
1196
|
+
detail="deterministic parser accepted the bounded draft selections",
|
|
1197
|
+
record=plan.model,
|
|
1198
|
+
)
|
|
1199
|
+
if not args.dry_run:
|
|
1200
|
+
apply_bootstrap_plan(root, plan)
|
|
1201
|
+
except CleanDocsError as exc:
|
|
1202
|
+
if transcript_path is not None and isinstance(provider, CommandPhrasingProvider):
|
|
1203
|
+
try:
|
|
1204
|
+
write_command_proposer_transcript(
|
|
1205
|
+
root,
|
|
1206
|
+
transcript_path,
|
|
1207
|
+
provider,
|
|
1208
|
+
state=(
|
|
1209
|
+
"provider-failed"
|
|
1210
|
+
if provider.last_response is None
|
|
1211
|
+
else "rejected"
|
|
1212
|
+
if plan is None
|
|
1213
|
+
else "bootstrap-failed"
|
|
1214
|
+
),
|
|
1215
|
+
outcome=(
|
|
1216
|
+
"provider-failed"
|
|
1217
|
+
if provider.last_response is None
|
|
1218
|
+
else "parser-reject"
|
|
1219
|
+
),
|
|
1220
|
+
detail=(
|
|
1221
|
+
provider.last_error
|
|
1222
|
+
if provider.last_response is None and provider.last_error is not None
|
|
1223
|
+
else str(exc)
|
|
1224
|
+
),
|
|
1225
|
+
)
|
|
1226
|
+
except CleanDocsError as transcript_error:
|
|
1227
|
+
print(f"sourcebound: {transcript_error}", file=sys.stderr)
|
|
1228
|
+
return transcript_error.exit_code
|
|
1229
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1230
|
+
return exc.exit_code
|
|
1231
|
+
if args.format == "json":
|
|
1232
|
+
print(json.dumps(plan.as_dict(), indent=2))
|
|
1233
|
+
else:
|
|
1234
|
+
state = "planned" if args.dry_run else "applied"
|
|
1235
|
+
for write in plan.writes:
|
|
1236
|
+
print(f"[{state}] write {write.path}: {write.reason}")
|
|
1237
|
+
for move in plan.moves:
|
|
1238
|
+
print(f"[{state}] move {move.source} -> {move.path}: {move.reason}")
|
|
1239
|
+
if plan.accept_hygiene_baseline:
|
|
1240
|
+
print(
|
|
1241
|
+
f"[{state}] write {AUDIT_BASELINE_PATH}: "
|
|
1242
|
+
"record exact existing documentation debt after bootstrap"
|
|
1243
|
+
)
|
|
1244
|
+
for gap in plan.gaps:
|
|
1245
|
+
print(f"[gap] {gap}")
|
|
1246
|
+
print(
|
|
1247
|
+
f"init: {state} "
|
|
1248
|
+
f"{len(plan.writes) + len(plan.moves) + int(plan.accept_hygiene_baseline)} "
|
|
1249
|
+
"operation(s); "
|
|
1250
|
+
f"{len(plan.facts)} grounded fact(s)"
|
|
1251
|
+
)
|
|
1252
|
+
return 2 if plan.gaps else 0
|
|
1253
|
+
if args.command == "doctor":
|
|
1254
|
+
manifest = args.manifest if args.manifest.is_absolute() else root / args.manifest
|
|
1255
|
+
bundle = build_diagnostic_bundle(root, manifest)
|
|
1256
|
+
checks = bundle.checks
|
|
1257
|
+
if args.bundle is not None:
|
|
1258
|
+
bundle_path = args.bundle if args.bundle.is_absolute() else root / args.bundle
|
|
1259
|
+
atomic_write(bundle_path, json.dumps(bundle.as_dict(), indent=2) + "\n")
|
|
1260
|
+
if args.format == "json":
|
|
1261
|
+
print(json.dumps(bundle.as_dict(), indent=2))
|
|
1262
|
+
else:
|
|
1263
|
+
for check in checks:
|
|
1264
|
+
print(f"[{'ok' if check.ok else 'fail'}] {check.name}: {check.detail}")
|
|
1265
|
+
return 0 if all(check.ok for check in checks) else 1
|
|
1266
|
+
if args.command in {"verify", "benchmark"}:
|
|
1267
|
+
manifest = args.manifest if args.manifest.is_absolute() else root / args.manifest
|
|
1268
|
+
try:
|
|
1269
|
+
if args.command == "verify":
|
|
1270
|
+
outcome_report = build_outcome_receipt(
|
|
1271
|
+
root,
|
|
1272
|
+
manifest,
|
|
1273
|
+
base=args.base,
|
|
1274
|
+
head=args.head,
|
|
1275
|
+
project=args.project,
|
|
1276
|
+
execution_policy=(
|
|
1277
|
+
ExecutionPolicy.STATIC_ONLY
|
|
1278
|
+
if args.no_exec
|
|
1279
|
+
else ExecutionPolicy.TRUSTED
|
|
1280
|
+
),
|
|
1281
|
+
)
|
|
1282
|
+
payload = outcome_report.as_dict()
|
|
1283
|
+
ok = outcome_report.ok
|
|
1284
|
+
else:
|
|
1285
|
+
performance_report = benchmark_changed_check(
|
|
1286
|
+
root,
|
|
1287
|
+
manifest,
|
|
1288
|
+
base=args.base,
|
|
1289
|
+
head=args.head,
|
|
1290
|
+
project=args.project,
|
|
1291
|
+
iterations=args.iterations,
|
|
1292
|
+
)
|
|
1293
|
+
payload = performance_report.as_dict()
|
|
1294
|
+
ok = performance_report.ok
|
|
1295
|
+
except CleanDocsError as exc:
|
|
1296
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1297
|
+
return exc.exit_code
|
|
1298
|
+
rendered = json.dumps(payload, indent=2) + "\n"
|
|
1299
|
+
if args.out is not None:
|
|
1300
|
+
output = args.out if args.out.is_absolute() else root / args.out
|
|
1301
|
+
atomic_write(output, rendered)
|
|
1302
|
+
sys.stdout.write(rendered)
|
|
1303
|
+
return 0 if ok else 1
|
|
1304
|
+
if args.command == "standard":
|
|
1305
|
+
source = args.source if args.source.is_absolute() else root / args.source
|
|
1306
|
+
output = args.output if args.output.is_absolute() else root / args.output
|
|
1307
|
+
try:
|
|
1308
|
+
if args.standard_command == "build":
|
|
1309
|
+
write_pack(compile_standard(source), output)
|
|
1310
|
+
print(f"wrote {output}")
|
|
1311
|
+
return 0
|
|
1312
|
+
if pack_matches_standard(source, output):
|
|
1313
|
+
print(f"[current] {output}")
|
|
1314
|
+
return 0
|
|
1315
|
+
print(f"sourcebound: policy pack is stale: {output}", file=sys.stderr)
|
|
1316
|
+
return 1
|
|
1317
|
+
except CleanDocsError as exc:
|
|
1318
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1319
|
+
return exc.exit_code
|
|
1320
|
+
manifest = args.manifest
|
|
1321
|
+
if not manifest.is_absolute():
|
|
1322
|
+
manifest = root / manifest
|
|
1323
|
+
try:
|
|
1324
|
+
if args.command == "eval":
|
|
1325
|
+
fixtures = args.fixtures if args.fixtures.is_absolute() else root / args.fixtures
|
|
1326
|
+
record_dir = args.record_dir
|
|
1327
|
+
if record_dir is not None and not record_dir.is_absolute():
|
|
1328
|
+
record_dir = root / record_dir
|
|
1329
|
+
evaluation_report = run_evaluation(
|
|
1330
|
+
root,
|
|
1331
|
+
manifest,
|
|
1332
|
+
fixtures,
|
|
1333
|
+
mode=args.mode,
|
|
1334
|
+
record_dir=record_dir,
|
|
1335
|
+
)
|
|
1336
|
+
if args.history:
|
|
1337
|
+
history = args.history if args.history.is_absolute() else root / args.history
|
|
1338
|
+
write_evaluation_history(history, evaluation_report)
|
|
1339
|
+
if args.format == "json":
|
|
1340
|
+
print(json.dumps(evaluation_report.as_dict(), indent=2))
|
|
1341
|
+
else:
|
|
1342
|
+
for audience, task_results in (
|
|
1343
|
+
("human", evaluation_report.human_tasks),
|
|
1344
|
+
("agent", evaluation_report.agent_tasks),
|
|
1345
|
+
):
|
|
1346
|
+
passed = sum(result.ok for result in task_results)
|
|
1347
|
+
print(f"{audience}: {passed}/{len(task_results)} task(s) passed")
|
|
1348
|
+
for result in task_results:
|
|
1349
|
+
state = "pass" if result.ok else "fail"
|
|
1350
|
+
print(f"[{state}] {result.id} ({result.scorer}): {result.detail}")
|
|
1351
|
+
print(
|
|
1352
|
+
f"hygiene: {len(evaluation_report.hygiene_findings)} finding(s)"
|
|
1353
|
+
)
|
|
1354
|
+
return 0 if evaluation_report.ok else 1
|
|
1355
|
+
if args.command == "project":
|
|
1356
|
+
loaded = load_manifest(manifest)
|
|
1357
|
+
projection_results = evaluate_projections(root, loaded)
|
|
1358
|
+
if args.check:
|
|
1359
|
+
output = (
|
|
1360
|
+
_json(projection_results)
|
|
1361
|
+
if args.format == "json"
|
|
1362
|
+
else _text(projection_results)
|
|
1363
|
+
)
|
|
1364
|
+
sys.stdout.write(output)
|
|
1365
|
+
return 1 if any(result.changed for result in projection_results) else 0
|
|
1366
|
+
written = write_projections(root, loaded)
|
|
1367
|
+
if args.format == "json":
|
|
1368
|
+
print(json.dumps({
|
|
1369
|
+
"ok": True,
|
|
1370
|
+
"written": [path.as_posix() for path in written],
|
|
1371
|
+
}, indent=2))
|
|
1372
|
+
else:
|
|
1373
|
+
for path in written:
|
|
1374
|
+
print(f"[projected] {path}")
|
|
1375
|
+
print(f"project: wrote {len(written)} file(s)")
|
|
1376
|
+
return 0
|
|
1377
|
+
if args.command == "emit":
|
|
1378
|
+
loaded = load_manifest(manifest)
|
|
1379
|
+
out = args.out if args.out.is_absolute() else root / args.out
|
|
1380
|
+
|
|
1381
|
+
def _shown(path: Path) -> Path:
|
|
1382
|
+
return path.relative_to(root) if path.is_relative_to(root) else path
|
|
1383
|
+
|
|
1384
|
+
if args.target == "llms-txt":
|
|
1385
|
+
written_path = emit_llms_txt(
|
|
1386
|
+
loaded, out, title=args.title, summary=args.summary
|
|
1387
|
+
)
|
|
1388
|
+
print(_shown(written_path))
|
|
1389
|
+
return 0
|
|
1390
|
+
written = emit_stepwise_skill(
|
|
1391
|
+
loaded,
|
|
1392
|
+
out,
|
|
1393
|
+
display_name=args.display_name,
|
|
1394
|
+
role=args.role,
|
|
1395
|
+
parent_command=args.parent_command,
|
|
1396
|
+
command=args.command_name,
|
|
1397
|
+
)
|
|
1398
|
+
for path in written:
|
|
1399
|
+
print(_shown(path))
|
|
1400
|
+
print(f"emit: wrote {len(written)} file(s) to {_shown(out)}")
|
|
1401
|
+
return 0
|
|
1402
|
+
if args.command == "drive":
|
|
1403
|
+
results, findings = drive(
|
|
1404
|
+
root,
|
|
1405
|
+
manifest,
|
|
1406
|
+
ref=args.ref,
|
|
1407
|
+
binding_id=args.binding,
|
|
1408
|
+
)
|
|
1409
|
+
output = (
|
|
1410
|
+
_json(results, repaired=not findings)
|
|
1411
|
+
if args.format == "json"
|
|
1412
|
+
else _text(results, repaired=not findings)
|
|
1413
|
+
)
|
|
1414
|
+
sys.stdout.write(output)
|
|
1415
|
+
if findings:
|
|
1416
|
+
for policy_finding in findings:
|
|
1417
|
+
print(
|
|
1418
|
+
f"[policy] {policy_finding.doc}:{policy_finding.line} "
|
|
1419
|
+
f"{policy_finding.rule}: {policy_finding.detail}",
|
|
1420
|
+
file=sys.stderr,
|
|
1421
|
+
)
|
|
1422
|
+
return 1
|
|
1423
|
+
print(
|
|
1424
|
+
f"drive: repaired {sum(result.changed for result in results if result.binding_type == 'region')} document(s); "
|
|
1425
|
+
"implemented policy checks passed"
|
|
1426
|
+
)
|
|
1427
|
+
return 1 if any(
|
|
1428
|
+
result.changed and result.binding_type != "region" for result in results
|
|
1429
|
+
) else 0
|
|
1430
|
+
if args.command == "plan":
|
|
1431
|
+
plan_manifest = manifest
|
|
1432
|
+
if args.project != Path(".") and args.manifest == Path(".sourcebound.yml"):
|
|
1433
|
+
plan_manifest = root / args.project / args.manifest
|
|
1434
|
+
impact_plan = build_impact_plan(
|
|
1435
|
+
root,
|
|
1436
|
+
plan_manifest,
|
|
1437
|
+
base=args.base,
|
|
1438
|
+
head=args.head,
|
|
1439
|
+
use_cache=not args.no_cache,
|
|
1440
|
+
project=args.project,
|
|
1441
|
+
execution_policy=(
|
|
1442
|
+
ExecutionPolicy.STATIC_ONLY
|
|
1443
|
+
if args.no_exec
|
|
1444
|
+
else ExecutionPolicy.TRUSTED
|
|
1445
|
+
),
|
|
1446
|
+
)
|
|
1447
|
+
if args.format == "json":
|
|
1448
|
+
print(json.dumps(impact_plan.as_dict(), indent=2))
|
|
1449
|
+
else:
|
|
1450
|
+
sys.stdout.write(render_impact_plan(impact_plan))
|
|
1451
|
+
return 0
|
|
1452
|
+
if args.command == "verdict":
|
|
1453
|
+
mutation_receipts = tuple(
|
|
1454
|
+
path if path.is_absolute() else root / path
|
|
1455
|
+
for path in args.mutation_receipt
|
|
1456
|
+
)
|
|
1457
|
+
try:
|
|
1458
|
+
pr_verdict = build_pr_verdict(
|
|
1459
|
+
root,
|
|
1460
|
+
manifest,
|
|
1461
|
+
base=args.base,
|
|
1462
|
+
head=args.head,
|
|
1463
|
+
mutation_receipt_paths=mutation_receipts,
|
|
1464
|
+
)
|
|
1465
|
+
except CleanDocsError as exc:
|
|
1466
|
+
if args.format == "json":
|
|
1467
|
+
print(
|
|
1468
|
+
json.dumps(
|
|
1469
|
+
{
|
|
1470
|
+
"schema": VERDICT_SCHEMA,
|
|
1471
|
+
"state": "invalid",
|
|
1472
|
+
"ready": False,
|
|
1473
|
+
"error": {
|
|
1474
|
+
"class": (
|
|
1475
|
+
"configuration"
|
|
1476
|
+
if isinstance(exc, ConfigurationError)
|
|
1477
|
+
else "extraction"
|
|
1478
|
+
),
|
|
1479
|
+
"detail": str(exc),
|
|
1480
|
+
},
|
|
1481
|
+
},
|
|
1482
|
+
indent=2,
|
|
1483
|
+
)
|
|
1484
|
+
)
|
|
1485
|
+
else:
|
|
1486
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1487
|
+
return exc.exit_code
|
|
1488
|
+
if args.format == "sarif":
|
|
1489
|
+
sys.stdout.write(render_verdict_sarif(pr_verdict))
|
|
1490
|
+
else:
|
|
1491
|
+
print(json.dumps(pr_verdict.as_dict(), indent=2))
|
|
1492
|
+
return 0 if pr_verdict.ok else 1
|
|
1493
|
+
if args.command == "check" and args.changed:
|
|
1494
|
+
if args.binding or args.ref:
|
|
1495
|
+
raise ConfigurationError(
|
|
1496
|
+
"check --changed cannot be combined with --binding or --ref"
|
|
1497
|
+
)
|
|
1498
|
+
changed_manifest = manifest
|
|
1499
|
+
if args.project != Path(".") and args.manifest == Path(".sourcebound.yml"):
|
|
1500
|
+
changed_manifest = root / args.project / args.manifest
|
|
1501
|
+
changed_report = check_changed(
|
|
1502
|
+
root,
|
|
1503
|
+
changed_manifest,
|
|
1504
|
+
base=args.base,
|
|
1505
|
+
head=args.head,
|
|
1506
|
+
use_cache=not args.no_cache,
|
|
1507
|
+
project=args.project,
|
|
1508
|
+
execution_policy=(
|
|
1509
|
+
ExecutionPolicy.STATIC_ONLY
|
|
1510
|
+
if args.no_exec
|
|
1511
|
+
else ExecutionPolicy.TRUSTED
|
|
1512
|
+
),
|
|
1513
|
+
)
|
|
1514
|
+
if args.format == "json":
|
|
1515
|
+
print(json.dumps(changed_report.as_dict(), indent=2))
|
|
1516
|
+
elif args.format == "sarif":
|
|
1517
|
+
sys.stdout.write(render_sarif(changed_report))
|
|
1518
|
+
else:
|
|
1519
|
+
print(f"changed: {changed_report.base}..{changed_report.head}")
|
|
1520
|
+
for section, section_findings in (
|
|
1521
|
+
("required", changed_report.required),
|
|
1522
|
+
("gap", changed_report.gaps),
|
|
1523
|
+
("ignored", changed_report.ignored),
|
|
1524
|
+
):
|
|
1525
|
+
for finding in section_findings:
|
|
1526
|
+
print(f"[{section}] {finding.id} {finding.message}")
|
|
1527
|
+
print(f"repair: {finding.repair}")
|
|
1528
|
+
return 0 if changed_report.ok else 1
|
|
1529
|
+
results = evaluate(
|
|
1530
|
+
root,
|
|
1531
|
+
manifest,
|
|
1532
|
+
ref=args.ref,
|
|
1533
|
+
binding_id=args.binding,
|
|
1534
|
+
execution_policy=(
|
|
1535
|
+
ExecutionPolicy.STATIC_ONLY
|
|
1536
|
+
if args.command == "check" and args.no_exec
|
|
1537
|
+
else ExecutionPolicy.TRUSTED
|
|
1538
|
+
),
|
|
1539
|
+
)
|
|
1540
|
+
if (
|
|
1541
|
+
args.command == "check"
|
|
1542
|
+
and args.binding is None
|
|
1543
|
+
and args.ref is None
|
|
1544
|
+
):
|
|
1545
|
+
loaded = load_manifest(manifest)
|
|
1546
|
+
if loaded.projections is not None:
|
|
1547
|
+
results.extend(evaluate_projections(root, loaded))
|
|
1548
|
+
if loaded.source_claim_checks:
|
|
1549
|
+
results.extend(
|
|
1550
|
+
claim_binding_results(
|
|
1551
|
+
scan_source_claims(
|
|
1552
|
+
root,
|
|
1553
|
+
loaded.source_claim_checks,
|
|
1554
|
+
discover=False,
|
|
1555
|
+
),
|
|
1556
|
+
# Required checks read only accepted document/source pairs.
|
|
1557
|
+
# Candidate discovery remains an explicit `claims` operation.
|
|
1558
|
+
ref=RepositorySnapshot(root).label,
|
|
1559
|
+
)
|
|
1560
|
+
)
|
|
1561
|
+
output = _json(results) if args.format == "json" else _text(results)
|
|
1562
|
+
sys.stdout.write(output)
|
|
1563
|
+
drift = any(
|
|
1564
|
+
result.changed and result.state != "skipped-untrusted-execution"
|
|
1565
|
+
for result in results
|
|
1566
|
+
)
|
|
1567
|
+
if args.command == "derive" and args.write and drift:
|
|
1568
|
+
write_results(root, results)
|
|
1569
|
+
sys.stdout.write(f"wrote {sum(result.changed for result in results)} document(s)\n")
|
|
1570
|
+
if args.command == "check" or getattr(args, "check", False):
|
|
1571
|
+
return 1 if drift else 0
|
|
1572
|
+
return 0
|
|
1573
|
+
except CleanDocsError as exc:
|
|
1574
|
+
print(f"sourcebound: {exc}", file=sys.stderr)
|
|
1575
|
+
return exc.exit_code
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1579
|
+
effective_argv = argv if argv is not None else sys.argv[1:]
|
|
1580
|
+
exit_code = _main(effective_argv)
|
|
1581
|
+
try:
|
|
1582
|
+
args = _parser().parse_args(effective_argv)
|
|
1583
|
+
if args.command != "feedback":
|
|
1584
|
+
proposer_outcome = None
|
|
1585
|
+
if args.command == "init" and args.model_config is not None:
|
|
1586
|
+
transcript = (
|
|
1587
|
+
args.model_transcript
|
|
1588
|
+
if args.model_transcript is not None
|
|
1589
|
+
else Path(".sourcebound/init-proposer-transcript.json")
|
|
1590
|
+
)
|
|
1591
|
+
transcript_path = transcript if transcript.is_absolute() else args.root / transcript
|
|
1592
|
+
try:
|
|
1593
|
+
proposer_outcome = json.loads(
|
|
1594
|
+
transcript_path.read_text(encoding="utf-8")
|
|
1595
|
+
).get("outcome")
|
|
1596
|
+
except (OSError, json.JSONDecodeError):
|
|
1597
|
+
proposer_outcome = None
|
|
1598
|
+
enqueue_feedback(
|
|
1599
|
+
args.root.resolve(),
|
|
1600
|
+
command=args.command,
|
|
1601
|
+
exit_code=exit_code,
|
|
1602
|
+
execution_policy=(
|
|
1603
|
+
ExecutionPolicy.STATIC_ONLY.value
|
|
1604
|
+
if getattr(args, "no_exec", False)
|
|
1605
|
+
else ExecutionPolicy.TRUSTED.value
|
|
1606
|
+
),
|
|
1607
|
+
outcome=proposer_outcome,
|
|
1608
|
+
)
|
|
1609
|
+
except (CleanDocsError, OSError, ValueError):
|
|
1610
|
+
# Feedback is an opt-in observation plane. It cannot alter gate behavior.
|
|
1611
|
+
pass
|
|
1612
|
+
return exit_code
|