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.
Files changed (71) hide show
  1. clean_docs/__init__.py +26 -0
  2. clean_docs/__main__.py +3 -0
  3. clean_docs/accessibility.py +182 -0
  4. clean_docs/adapters/__init__.py +1 -0
  5. clean_docs/adapters/event_capture.py +56 -0
  6. clean_docs/adapters/mdx_dependencies.json +714 -0
  7. clean_docs/adapters/mdx_parser.mjs +29992 -0
  8. clean_docs/applicability.py +330 -0
  9. clean_docs/audit.py +1120 -0
  10. clean_docs/bootstrap.py +507 -0
  11. clean_docs/capabilities.py +200 -0
  12. clean_docs/changed.py +452 -0
  13. clean_docs/claims.py +840 -0
  14. clean_docs/cli.py +1612 -0
  15. clean_docs/context.py +307 -0
  16. clean_docs/corpus.py +377 -0
  17. clean_docs/demo.py +369 -0
  18. clean_docs/doctor.py +184 -0
  19. clean_docs/emit/__init__.py +4 -0
  20. clean_docs/emit/llms_txt.py +102 -0
  21. clean_docs/emit/stepwise.py +168 -0
  22. clean_docs/engine.py +324 -0
  23. clean_docs/errors.py +20 -0
  24. clean_docs/evaluation.py +867 -0
  25. clean_docs/execution.py +138 -0
  26. clean_docs/explain.py +123 -0
  27. clean_docs/extractors/__init__.py +19 -0
  28. clean_docs/extractors/command.py +51 -0
  29. clean_docs/extractors/inventory.py +176 -0
  30. clean_docs/extractors/json_pointer.py +84 -0
  31. clean_docs/extractors/python_literal.py +104 -0
  32. clean_docs/extractors/static.py +111 -0
  33. clean_docs/feedback.py +1390 -0
  34. clean_docs/impact.py +1624 -0
  35. clean_docs/improvements.py +1178 -0
  36. clean_docs/inventory.py +474 -0
  37. clean_docs/isolation.py +157 -0
  38. clean_docs/manifest.py +898 -0
  39. clean_docs/mdx.py +272 -0
  40. clean_docs/migration.py +121 -0
  41. clean_docs/models.py +194 -0
  42. clean_docs/outcomes.py +296 -0
  43. clean_docs/performance.py +123 -0
  44. clean_docs/phrasing.py +448 -0
  45. clean_docs/plugins.py +249 -0
  46. clean_docs/policy.py +536 -0
  47. clean_docs/projections.py +255 -0
  48. clean_docs/regions.py +75 -0
  49. clean_docs/release.py +232 -0
  50. clean_docs/renderers.py +57 -0
  51. clean_docs/residue.py +311 -0
  52. clean_docs/review_contracts.py +862 -0
  53. clean_docs/review_ledger.py +297 -0
  54. clean_docs/review_limits.py +9 -0
  55. clean_docs/sensitivity.py +602 -0
  56. clean_docs/snapshot.py +212 -0
  57. clean_docs/standard.py +281 -0
  58. clean_docs/standards/default.json +309 -0
  59. clean_docs/standards/exemplars.md +87 -0
  60. clean_docs/standards/v0-migrations.json +26 -0
  61. clean_docs/symbols.py +47 -0
  62. clean_docs/templates.py +96 -0
  63. clean_docs/verdict.py +1397 -0
  64. clean_docs/visuals.py +346 -0
  65. clean_docs/write_gate.py +170 -0
  66. sourcebound-1.2.1.dist-info/LICENSE +21 -0
  67. sourcebound-1.2.1.dist-info/METADATA +109 -0
  68. sourcebound-1.2.1.dist-info/RECORD +71 -0
  69. sourcebound-1.2.1.dist-info/WHEEL +5 -0
  70. sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
  71. 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