@grifhinz/logics-manager 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ from .flow_evidence import structured_validation_line as _structured_validation_
15
15
  from .index import index_payload
16
16
  from .lint import expected_workflow_mermaid_signature, lint_payload
17
17
  from .path_utils import ensure_relative_to
18
+ from .sync import read_logics_doc_payload
18
19
  from .termstyle import colorize_help
19
20
 
20
21
 
@@ -206,6 +207,10 @@ def _build_help() -> str:
206
207
  " List workflow docs that are still active.",
207
208
  " Flags: --kind {all,request,backlog,task}, --format {text,json}",
208
209
  "",
210
+ " show <ref>",
211
+ " Show a bounded workflow document view.",
212
+ " Flags: --max-chars, --section, --format {text,json}",
213
+ "",
209
214
  " companion <product|architecture>",
210
215
  " Create a companion doc from the integrated runtime.",
211
216
  " Flags: --title, --source-ref, --request-ref, --backlog-ref, --task-ref, --format {text,json}, --dry-run",
@@ -251,6 +256,7 @@ def _build_help() -> str:
251
256
  "Examples:",
252
257
  ' logics-manager flow new request --title "My request"',
253
258
  " logics-manager flow deliver --from-product prod_017_delivery_loop",
259
+ " logics-manager flow show req_001_my_request",
254
260
  " logics-manager flow validate-closeout task_003_fix_docs",
255
261
  " logics-manager flow repair gates task_003_fix_docs",
256
262
  " logics-manager flow closeout task_003_fix_docs --validation \"pytest passed\" --index --lint --audit",
@@ -338,6 +344,27 @@ def _build_list_help() -> str:
338
344
  )
339
345
 
340
346
 
347
+ def _build_show_help() -> str:
348
+ return "\n".join(
349
+ [
350
+ "Logics Flow Show",
351
+ "Show a bounded workflow document view.",
352
+ "",
353
+ "Usage:",
354
+ " logics-manager flow show <ref-or-path> [args...]",
355
+ "",
356
+ "Flags:",
357
+ " --max-chars",
358
+ " --section",
359
+ " --format {text,json}",
360
+ "",
361
+ "Examples:",
362
+ " logics-manager flow show req_001_my_request",
363
+ " logics-manager flow show task_003_fix_docs --section Validation",
364
+ ]
365
+ )
366
+
367
+
341
368
  def _build_companion_help() -> str:
342
369
  return "\n".join(
343
370
  [
@@ -800,6 +827,8 @@ def _workflow_mermaid_block(kind: str, signature: str) -> list[str]:
800
827
 
801
828
 
802
829
  def _with_workflow_mermaid_overview(kind: str, content: str) -> str:
830
+ if kind == "task":
831
+ return content
803
832
  lines = content.rstrip().splitlines()
804
833
  signature = expected_workflow_mermaid_signature(kind, lines)
805
834
  if not signature:
@@ -995,6 +1024,8 @@ def _mermaid_closeout_issue(path: Path, kind: str) -> str | None:
995
1024
  text = path.read_text(encoding="utf-8")
996
1025
  match = re.search(r"```mermaid\s*\n(.*?)\n```", text, flags=re.DOTALL)
997
1026
  if match is None:
1027
+ if kind == "task":
1028
+ return None
998
1029
  return "missing Mermaid overview block"
999
1030
  signature_match = re.search(r"^\s*%%\s*logics-signature:\s*(.+?)\s*$", match.group(1), flags=re.MULTILINE)
1000
1031
  expected = expected_workflow_mermaid_signature(kind, text.splitlines())
@@ -1625,7 +1656,7 @@ def _build_native_task_doc(
1625
1656
  "",
1626
1657
  ]
1627
1658
  ).rstrip() + "\n"
1628
- return _with_workflow_mermaid_overview("task", content)
1659
+ return content
1629
1660
 
1630
1661
 
1631
1662
  def _extract_doc_title(path: Path) -> str:
@@ -2183,7 +2214,7 @@ def _build_native_task_from_backlog(
2183
2214
  "",
2184
2215
  ]
2185
2216
  ).rstrip() + "\n"
2186
- return ref, _with_workflow_mermaid_overview("task", content)
2217
+ return ref, content
2187
2218
 
2188
2219
 
2189
2220
  def build_parser() -> argparse.ArgumentParser:
@@ -2210,6 +2241,13 @@ def build_parser() -> argparse.ArgumentParser:
2210
2241
  list_parser.add_argument("--format", choices=("text", "json"), default="text")
2211
2242
  list_parser.set_defaults(func=cmd_list)
2212
2243
 
2244
+ show_parser = sub.add_parser("show", help="Show a bounded workflow document view.")
2245
+ show_parser.add_argument("source")
2246
+ show_parser.add_argument("--max-chars", type=int, default=4000)
2247
+ show_parser.add_argument("--section", action="append", default=[])
2248
+ show_parser.add_argument("--format", choices=("text", "json"), default="text")
2249
+ show_parser.set_defaults(func=cmd_show)
2250
+
2213
2251
  companion_parser = sub.add_parser("companion", help="Create a companion doc from the integrated runtime.")
2214
2252
  companion_sub = companion_parser.add_subparsers(dest="kind", required=True)
2215
2253
  for kind in ("product", "architecture"):
@@ -2425,6 +2463,22 @@ def cmd_list(args: argparse.Namespace) -> dict[str, object]:
2425
2463
  return payload
2426
2464
 
2427
2465
 
2466
+ def cmd_show(args: argparse.Namespace) -> dict[str, object]:
2467
+ repo_root = _find_repo_root(Path.cwd())
2468
+ max_chars = args.max_chars if args.max_chars > 0 else 4000
2469
+ payload = read_logics_doc_payload(repo_root, args.source, max_chars=min(max_chars, 12000), sections=args.section or None)
2470
+ if args.format == "json":
2471
+ print_payload({"command": "show", **payload}, args.format)
2472
+ else:
2473
+ print(f"{payload['ref']} ({payload['kind']}): {payload['title']}")
2474
+ print(f"- path: {payload['path']}")
2475
+ print(f"- status: {payload['status']}")
2476
+ print(f"- truncated: {payload['truncated']}")
2477
+ print("")
2478
+ print(str(payload["content"]).rstrip())
2479
+ return payload
2480
+
2481
+
2428
2482
  def cmd_companion(args: argparse.Namespace) -> dict[str, object]:
2429
2483
  repo_root = _find_repo_root(Path.cwd())
2430
2484
  request_ref, backlog_ref, task_ref = _resolve_workflow_refs_for_companion(
@@ -2789,6 +2843,9 @@ def closeout_payload(
2789
2843
  finish_payload: dict[str, object] | None = None
2790
2844
  if not dry_run:
2791
2845
  _close_chain_for_kind(repo_root, task_path, DOC_KINDS["task"], dry_run=False, quiet=True)
2846
+ for ref in mermaid_refs:
2847
+ if ref.startswith(f"{DOC_KINDS['request'].prefix}_"):
2848
+ _maybe_close_request_chain(repo_root, ref, dry_run=False, quiet=True)
2792
2849
  finish_issues = _verify_finished_task_chain(repo_root, task_path)
2793
2850
  if finish_issues:
2794
2851
  raise SystemExit("Finish verification failed:\n" + "\n".join(f"- {issue}" for issue in finish_issues))
@@ -3240,6 +3297,9 @@ def main(argv: list[str]) -> int:
3240
3297
  if argv[0] == "list" and _help_requested(argv, 1):
3241
3298
  _print_help(_build_list_help())
3242
3299
  return 0
3300
+ if argv[0] == "show" and _help_requested(argv, 1):
3301
+ _print_help(_build_show_help())
3302
+ return 0
3243
3303
  if argv[0] == "companion" and _help_requested(argv, 1):
3244
3304
  _print_help(_build_companion_help())
3245
3305
  return 0
@@ -3285,9 +3345,13 @@ def main(argv: list[str]) -> int:
3285
3345
  if argv[0] == "finish" and len(argv) > 1 and argv[1] == "task" and _help_requested(argv, 2):
3286
3346
  _print_help(_build_finish_kind_help(argv[1]))
3287
3347
  return 0
3348
+ valid_commands = {"new", "list", "show", "companion", "deliver", "validate-closeout", "repair", "closeout", "promote", "split", "close", "finish"}
3349
+ if argv[0] not in valid_commands:
3350
+ hint = " Use `logics-manager flow show <ref>` to inspect a workflow doc." if argv[0] in {"read", "view", "cat"} else " Run `logics-manager flow --help` for valid commands."
3351
+ raise SystemExit(f"Unsupported flow subcommand: {argv[0]}.{hint}")
3288
3352
  parser = build_parser()
3289
3353
  args = parser.parse_args(argv)
3290
- if args.command not in {"new", "list", "companion", "deliver", "validate-closeout", "repair", "closeout", "promote", "split", "close", "finish"}:
3354
+ if args.command not in valid_commands:
3291
3355
  raise SystemExit("Unsupported flow subcommand for the native CLI slice.")
3292
3356
  payload = args.func(args)
3293
3357
  if args.command == "validate-closeout" and isinstance(payload, dict) and not payload.get("ok", False):
@@ -15,18 +15,19 @@ from .config import find_repo_root
15
15
  @dataclass(frozen=True)
16
16
  class Kind:
17
17
  directory: str
18
- prefix: str
18
+ prefixes: tuple[str, ...]
19
19
  requires_progress: bool
20
20
  required_indicators: tuple[str, ...]
21
21
  allowed_statuses: tuple[str, ...]
22
22
 
23
23
 
24
24
  KINDS = {
25
- "request": Kind("logics/request", "req", False, ("From version", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
26
- "backlog": Kind("logics/backlog", "item", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
27
- "task": Kind("logics/tasks", "task", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
28
- "product": Kind("logics/product", "prod", False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
29
- "architecture": Kind("logics/architecture", "adr", False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
25
+ "request": Kind("logics/request", ("req",), False, ("From version", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
26
+ "backlog": Kind("logics/backlog", ("item",), True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
27
+ "task": Kind("logics/tasks", ("task",), True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
28
+ "product": Kind("logics/product", ("prod",), False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
29
+ "architecture": Kind("logics/architecture", ("adr",), False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
30
+ "spec": Kind("logics/specs", ("spec", "req"), False, ("From version", "Status", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Done", "Validated", "Settled", "Archived")),
30
31
  }
31
32
 
32
33
  WORKFLOW_KINDS = {"request", "backlog", "task"}
@@ -503,7 +504,8 @@ def _lint_file(path: Path, kind_name: str, kind: Kind, require_status: bool, che
503
504
  issues: list[str] = []
504
505
  warnings: list[str] = []
505
506
  name = path.name
506
- if not re.match(rf"^{re.escape(kind.prefix)}_\d{{3}}_[a-z0-9_]+\.md$", name):
507
+ allowed_prefixes = "|".join(re.escape(prefix) for prefix in kind.prefixes)
508
+ if not re.match(rf"^({allowed_prefixes})_\d{{3}}_[a-z0-9_]+\.md$", name):
507
509
  issues.append(f"bad filename: {name}")
508
510
 
509
511
  lines = _read_lines(path)
@@ -557,6 +559,8 @@ def lint_payload(repo_root: Path, *, require_status: bool = False) -> dict[str,
557
559
  if not directory.is_dir():
558
560
  continue
559
561
  for path in sorted(directory.glob("*.md")):
562
+ if path.name == "README.md":
563
+ continue
560
564
  rel_path = path.relative_to(repo_root)
561
565
  issues, warnings = _lint_file(
562
566
  path,
@@ -259,11 +259,23 @@ def _build_context_pack(
259
259
  profile: str,
260
260
  config: dict[str, object] | None = None,
261
261
  ) -> dict[str, object]:
262
+ seed_refs = [ref for ref in seed_ref.split(",") if ref]
262
263
  docs = _load_workflow_docs(repo_root)
263
- seed = docs.get(seed_ref)
264
- if seed is None:
265
- raise SystemExit(f"Unknown workflow ref `{seed_ref}`.")
266
- ordered = _workflow_neighborhood(seed, docs)[: _context_profile_limit(profile)]
264
+ seeds = [docs.get(ref) for ref in seed_refs]
265
+ missing = [ref for ref, doc in zip(seed_refs, seeds) if doc is None]
266
+ if missing:
267
+ raise SystemExit(f"Unknown workflow ref(s): {', '.join(f'`{ref}`' for ref in missing)}.")
268
+ ordered: list[WorkflowDocModel] = []
269
+ seen: set[str] = set()
270
+ per_seed_limit = _context_profile_limit(profile)
271
+ for seed in seeds:
272
+ if seed is None:
273
+ continue
274
+ for doc in _workflow_neighborhood(seed, docs)[:per_seed_limit]:
275
+ if doc.ref in seen:
276
+ continue
277
+ ordered.append(doc)
278
+ seen.add(doc.ref)
267
279
  changed_paths = _git_changed_paths(repo_root) if mode == "diff-first" else []
268
280
  cache_key = _context_pack_cache_key(
269
281
  repo_root,
@@ -281,7 +293,8 @@ def _build_context_pack(
281
293
  "ref": seed_ref,
282
294
  "mode": mode,
283
295
  "profile": profile,
284
- "budgets": {"max_docs": _context_profile_limit(profile)},
296
+ "refs": seed_refs,
297
+ "budgets": {"max_docs": per_seed_limit * max(1, len(seed_refs)), "max_docs_per_ref": per_seed_limit},
285
298
  "changed_paths": changed_paths,
286
299
  "docs": pack_docs,
287
300
  "estimates": {
@@ -725,6 +738,8 @@ def build_parser() -> argparse.ArgumentParser:
725
738
  close_eligible.set_defaults(func=cmd_close_eligible_requests)
726
739
 
727
740
  refresh_mermaid = sub.add_parser("refresh-mermaid-signatures", help="Refresh stale workflow Mermaid signatures without rewriting the full diagram body.")
741
+ refresh_mermaid.add_argument("sources", nargs="*", help="Optional workflow refs or paths to scope the refresh.")
742
+ refresh_mermaid.add_argument("--changed-only", action="store_true", help="Refresh only changed workflow docs.")
728
743
  refresh_mermaid.add_argument("--format", choices=("text", "json"), default="text")
729
744
  refresh_mermaid.add_argument("--dry-run", action="store_true")
730
745
  refresh_mermaid.set_defaults(func=cmd_refresh_mermaid_signatures)
@@ -779,7 +794,7 @@ def build_parser() -> argparse.ArgumentParser:
779
794
  append_note.set_defaults(func=cmd_append_note)
780
795
 
781
796
  context_pack = sub.add_parser("context-pack", help="Build a compact context pack from workflow docs.")
782
- context_pack.add_argument("ref", help="Seed workflow ref for the context pack.")
797
+ context_pack.add_argument("refs", nargs="+", help="Seed workflow ref(s) for the context pack.")
783
798
  context_pack.add_argument("--mode", choices=("summary-only", "diff-first", "full"), default="summary-only")
784
799
  context_pack.add_argument("--profile", choices=("tiny", "normal", "deep"), default="normal")
785
800
  context_pack.add_argument("--out", help="Write the JSON artifact to this relative path.")
@@ -812,13 +827,14 @@ def _build_help() -> str:
812
827
  "",
813
828
  " refresh-mermaid-signatures",
814
829
  " Refresh stale Mermaid signatures without rewriting diagram bodies.",
815
- " Flags: --format {text,json}, --dry-run",
830
+ " Args: [refs-or-paths...]",
831
+ " Flags: --changed-only, --format {text,json}, --dry-run",
816
832
  "",
817
833
  " schema-status [sources...]",
818
834
  " Report schema-version coverage for selected workflow docs.",
819
835
  " Flags: --format {text,json}",
820
836
  "",
821
- " context-pack <ref>",
837
+ " context-pack <refs...>",
822
838
  " Build a compact JSON context pack from workflow docs.",
823
839
  " Flags: --mode {summary-only,diff-first,full}, --profile {tiny,normal,deep}, --out, --format {text,json}, --dry-run",
824
840
  "",
@@ -848,7 +864,7 @@ def _build_help() -> str:
848
864
  "",
849
865
  "Examples:",
850
866
  " logics-manager sync schema-status",
851
- " logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
867
+ " logics-manager sync context-pack req_001_my_request task_002_fix_bug --out logics/context-pack.json",
852
868
  " logics-manager sync export-graph --format json",
853
869
  ]
854
870
  )
@@ -879,9 +895,10 @@ def _build_subcommand_help(command: str) -> str:
879
895
  "Refresh stale workflow Mermaid signatures without rewriting diagram bodies.",
880
896
  "",
881
897
  "Usage:",
882
- " logics-manager sync refresh-mermaid-signatures [args...]",
898
+ " logics-manager sync refresh-mermaid-signatures [refs-or-paths...] [args...]",
883
899
  "",
884
900
  "Flags:",
901
+ " --changed-only",
885
902
  " --format {text,json}",
886
903
  " --dry-run",
887
904
  ]
@@ -909,7 +926,7 @@ def _build_subcommand_help(command: str) -> str:
909
926
  "Build a compact JSON context pack from workflow docs.",
910
927
  "",
911
928
  "Usage:",
912
- " logics-manager sync context-pack <ref> [args...]",
929
+ " logics-manager sync context-pack <refs...> [args...]",
913
930
  "",
914
931
  "Flags:",
915
932
  " --mode {summary-only,diff-first,full}",
@@ -919,7 +936,7 @@ def _build_subcommand_help(command: str) -> str:
919
936
  " --dry-run",
920
937
  "",
921
938
  "Example:",
922
- " logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
939
+ " logics-manager sync context-pack req_001_my_request task_002_fix_bug --out logics/context-pack.json",
923
940
  ]
924
941
  )
925
942
  if command == "read-doc":
@@ -1053,17 +1070,26 @@ def cmd_close_eligible_requests(args: argparse.Namespace) -> dict[str, object]:
1053
1070
  def cmd_refresh_mermaid_signatures(args: argparse.Namespace) -> dict[str, object]:
1054
1071
  repo_root = _find_repo_root(Path.cwd())
1055
1072
  modified: list[str] = []
1056
- for kind in ("request", "backlog", "task"):
1057
- directory = repo_root / DOC_KINDS[kind]["directory"]
1058
- for path in sorted(directory.glob("*.md")):
1059
- if refresh_workflow_mermaid_signature_file(path, kind, args.dry_run, repo_root=repo_root):
1060
- modified.append(path.relative_to(repo_root).as_posix())
1073
+ if args.changed_only:
1074
+ changed_sources = [
1075
+ path
1076
+ for path in _git_changed_paths(repo_root)
1077
+ if path.startswith(("logics/request/", "logics/backlog/", "logics/tasks/")) and path.endswith(".md")
1078
+ ]
1079
+ targets = _resolve_target_docs(repo_root, changed_sources)
1080
+ else:
1081
+ targets = _resolve_target_docs(repo_root, args.sources)
1082
+ for kind, path in targets:
1083
+ if refresh_workflow_mermaid_signature_file(path, kind, args.dry_run, repo_root=repo_root):
1084
+ modified.append(path.relative_to(repo_root).as_posix())
1061
1085
 
1062
1086
  payload = {
1063
1087
  "command": "sync",
1064
1088
  "kind": "refresh-mermaid-signatures",
1065
1089
  "repo_root": repo_root.as_posix(),
1066
1090
  "modified_files": modified,
1091
+ "scanned_files": [path.relative_to(repo_root).as_posix() for _kind, path in targets],
1092
+ "changed_only": args.changed_only,
1067
1093
  "dry_run": args.dry_run,
1068
1094
  }
1069
1095
  if args.format == "json":
@@ -1106,6 +1132,8 @@ def cmd_read_doc(args: argparse.Namespace) -> dict[str, object]:
1106
1132
  print(f"- path: {payload['path']}")
1107
1133
  print(f"- status: {payload['status']}")
1108
1134
  print(f"- truncated: {payload['truncated']}")
1135
+ print("")
1136
+ print(str(payload["content"]).rstrip())
1109
1137
  return {"command": "sync", "kind": "read-doc", "repo_root": repo_root.as_posix(), **payload}
1110
1138
 
1111
1139
 
@@ -1172,7 +1200,7 @@ def cmd_append_note(args: argparse.Namespace) -> dict[str, object]:
1172
1200
 
1173
1201
  def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
1174
1202
  repo_root = _find_repo_root(Path.cwd())
1175
- payload = _build_context_pack(repo_root, args.ref, mode=args.mode, profile=args.profile, config=None)
1203
+ payload = _build_context_pack(repo_root, ",".join(args.refs), mode=args.mode, profile=args.profile, config=None)
1176
1204
  if args.out:
1177
1205
  out_path, output_path = resolve_repo_output_path(repo_root, args.out)
1178
1206
  serialized = json.dumps(payload, indent=2, sort_keys=True) + "\n"
@@ -1188,7 +1216,7 @@ def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
1188
1216
  if args.format == "json":
1189
1217
  print(json.dumps(payload, indent=2, sort_keys=True))
1190
1218
  else:
1191
- print(f"Context pack: {payload['ref']} ({payload['mode']}, {payload['profile']})")
1219
+ print(f"Context pack: {', '.join(args.refs)} ({payload['mode']}, {payload['profile']})")
1192
1220
  print(f"- docs: {payload['estimates']['doc_count']}")
1193
1221
  return {"command": "sync", "kind": "context-pack", "repo_root": repo_root.as_posix(), **payload}
1194
1222