@grifhinz/logics-manager 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -157
- package/VERSION +1 -1
- package/logics_manager/assist.py +366 -0
- package/logics_manager/cli.py +116 -38
- package/logics_manager/flow.py +580 -8
- package/logics_manager/mcp.py +1188 -0
- package/logics_manager/sync.py +613 -0
- package/logics_manager/termstyle.py +75 -0
- package/package.json +10 -1
- package/pyproject.toml +1 -1
package/logics_manager/sync.py
CHANGED
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
from .config import find_repo_root
|
|
12
12
|
from .lint import expected_workflow_mermaid_signature
|
|
13
|
+
from .termstyle import colorize_help
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@dataclass(frozen=True)
|
|
@@ -37,6 +38,8 @@ REF_PREFIXES = ("req", "item", "task", "prod", "adr", "spec")
|
|
|
37
38
|
_CONTEXT_PACK_CACHE: dict[str, dict[str, object]] = {}
|
|
38
39
|
MERMAID_BLOCK_PATTERN = re.compile(r"```mermaid\s*\n(.*?)\n```", re.DOTALL)
|
|
39
40
|
MERMAID_SIGNATURE_PATTERN = re.compile(r"^\s*%%\s*logics-signature:\s*(.+?)\s*$", re.MULTILINE)
|
|
41
|
+
APPROVED_WORKFLOW_INDICATORS = ("Status", "Progress", "Understanding", "Confidence", "Theme", "Complexity")
|
|
42
|
+
MAX_MUTATION_TEXT_CHARS = 2000
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
def _read_text(path: Path) -> str:
|
|
@@ -302,6 +305,9 @@ def _resolve_target_docs(repo_root: Path, sources: list[str]) -> list[tuple[str,
|
|
|
302
305
|
|
|
303
306
|
resolved: list[tuple[str, Path]] = []
|
|
304
307
|
for source in sources:
|
|
308
|
+
raw_source = Path(source)
|
|
309
|
+
if raw_source.is_absolute() or any(part == ".." for part in raw_source.parts):
|
|
310
|
+
raise SystemExit(f"Unsupported workflow doc target `{source}`.")
|
|
305
311
|
candidate = (repo_root / source).resolve()
|
|
306
312
|
if candidate.is_file():
|
|
307
313
|
for kind_name, kind in DOC_KINDS.items():
|
|
@@ -341,6 +347,249 @@ def _schema_status(repo_root: Path, targets: list[str]) -> dict[str, object]:
|
|
|
341
347
|
}
|
|
342
348
|
|
|
343
349
|
|
|
350
|
+
def build_context_pack_payload(repo_root: Path, ref: str, *, mode: str = "summary-only", profile: str = "normal", config: dict[str, object] | None = None) -> dict[str, object]:
|
|
351
|
+
return _build_context_pack(repo_root, ref, mode=mode, profile=profile, config=config)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _default_section_names(kind: str) -> list[str]:
|
|
355
|
+
return {
|
|
356
|
+
"request": ["Needs", "Context", "Acceptance criteria", "Backlog", "Tasks", "AI Context"],
|
|
357
|
+
"backlog": ["Problem", "Scope", "Acceptance criteria", "AC Traceability", "Tasks", "AI Context"],
|
|
358
|
+
"task": ["Definition of Done (DoD)", "Backlog", "Acceptance criteria", "Validation", "Report", "AI Context"],
|
|
359
|
+
}.get(kind, ["AI Context"])
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def read_logics_doc_payload(repo_root: Path, source: str, *, max_chars: int = 4000, sections: list[str] | None = None) -> dict[str, object]:
|
|
363
|
+
targets = _resolve_target_docs(repo_root, [source])
|
|
364
|
+
if len(targets) != 1:
|
|
365
|
+
raise SystemExit(f"Expected one workflow doc target for `{source}`.")
|
|
366
|
+
kind, path = targets[0]
|
|
367
|
+
doc = parse_workflow_doc(path, repo_root=repo_root)
|
|
368
|
+
requested_sections = sections or _default_section_names(kind)
|
|
369
|
+
selected_sections = {
|
|
370
|
+
heading: [line for line in doc.sections.get(heading, []) if line.strip()]
|
|
371
|
+
for heading in requested_sections
|
|
372
|
+
if heading in doc.sections
|
|
373
|
+
}
|
|
374
|
+
text = _read_text(path)
|
|
375
|
+
return {
|
|
376
|
+
"ref": doc.ref,
|
|
377
|
+
"kind": doc.kind,
|
|
378
|
+
"path": doc.path,
|
|
379
|
+
"title": doc.title,
|
|
380
|
+
"status": doc.indicators.get("Status", ""),
|
|
381
|
+
"indicators": doc.indicators,
|
|
382
|
+
"linked_refs": {prefix: refs for prefix, refs in doc.refs.items() if refs},
|
|
383
|
+
"sections": selected_sections,
|
|
384
|
+
"content": text[:max_chars],
|
|
385
|
+
"truncated": len(text) > max_chars,
|
|
386
|
+
"max_chars": max_chars,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def list_logics_docs_payload(
|
|
391
|
+
repo_root: Path,
|
|
392
|
+
*,
|
|
393
|
+
kind: str = "all",
|
|
394
|
+
status: str | None = None,
|
|
395
|
+
ref_prefix: str | None = None,
|
|
396
|
+
limit: int = 50,
|
|
397
|
+
) -> dict[str, object]:
|
|
398
|
+
docs = sorted(_load_workflow_docs(repo_root).values(), key=lambda doc: doc.path)
|
|
399
|
+
if kind != "all":
|
|
400
|
+
docs = [doc for doc in docs if doc.kind == kind]
|
|
401
|
+
if status:
|
|
402
|
+
expected_status = " ".join(status.split()).lower()
|
|
403
|
+
docs = [doc for doc in docs if " ".join(doc.indicators.get("Status", "").split()).lower() == expected_status]
|
|
404
|
+
if ref_prefix:
|
|
405
|
+
docs = [doc for doc in docs if doc.ref.startswith(ref_prefix)]
|
|
406
|
+
limited = docs[:limit]
|
|
407
|
+
return {
|
|
408
|
+
"items": [
|
|
409
|
+
{
|
|
410
|
+
"ref": doc.ref,
|
|
411
|
+
"kind": doc.kind,
|
|
412
|
+
"path": doc.path,
|
|
413
|
+
"title": doc.title,
|
|
414
|
+
"status": doc.indicators.get("Status", ""),
|
|
415
|
+
"linked_refs": {prefix: refs for prefix, refs in doc.refs.items() if refs},
|
|
416
|
+
}
|
|
417
|
+
for doc in limited
|
|
418
|
+
],
|
|
419
|
+
"total_count": len(docs),
|
|
420
|
+
"returned_count": len(limited),
|
|
421
|
+
"truncated": len(docs) > len(limited),
|
|
422
|
+
"limit": limit,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _snippet_for_line(lines: list[str], index: int, *, max_chars: int) -> str:
|
|
427
|
+
start = max(0, index - 1)
|
|
428
|
+
end = min(len(lines), index + 2)
|
|
429
|
+
snippet = "\n".join(line for line in lines[start:end] if line.strip())
|
|
430
|
+
return snippet[:max_chars]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def search_logics_docs_payload(
|
|
434
|
+
repo_root: Path,
|
|
435
|
+
query: str,
|
|
436
|
+
*,
|
|
437
|
+
kind: str = "all",
|
|
438
|
+
status: str | None = None,
|
|
439
|
+
limit: int = 20,
|
|
440
|
+
max_snippet_chars: int = 240,
|
|
441
|
+
) -> dict[str, object]:
|
|
442
|
+
normalized_query = query.strip().lower()
|
|
443
|
+
if not normalized_query:
|
|
444
|
+
raise SystemExit("Search query is required.")
|
|
445
|
+
docs_payload = list_logics_docs_payload(repo_root, kind=kind, status=status, limit=10000)
|
|
446
|
+
docs_by_ref = _load_workflow_docs(repo_root)
|
|
447
|
+
matches: list[dict[str, object]] = []
|
|
448
|
+
for item in docs_payload["items"]:
|
|
449
|
+
ref = str(item["ref"])
|
|
450
|
+
doc = docs_by_ref.get(ref)
|
|
451
|
+
if doc is None:
|
|
452
|
+
continue
|
|
453
|
+
text = _strip_mermaid_blocks(_read_text(repo_root / doc.path))
|
|
454
|
+
lines = text.splitlines()
|
|
455
|
+
for idx, line in enumerate(lines):
|
|
456
|
+
if normalized_query in line.lower():
|
|
457
|
+
matches.append(
|
|
458
|
+
{
|
|
459
|
+
"ref": doc.ref,
|
|
460
|
+
"kind": doc.kind,
|
|
461
|
+
"path": doc.path,
|
|
462
|
+
"title": doc.title,
|
|
463
|
+
"status": doc.indicators.get("Status", ""),
|
|
464
|
+
"line": idx + 1,
|
|
465
|
+
"snippet": _snippet_for_line(lines, idx, max_chars=max_snippet_chars),
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
break
|
|
469
|
+
if len(matches) >= limit:
|
|
470
|
+
break
|
|
471
|
+
return {
|
|
472
|
+
"query": query,
|
|
473
|
+
"matches": matches,
|
|
474
|
+
"returned_count": len(matches),
|
|
475
|
+
"truncated": len(matches) >= limit,
|
|
476
|
+
"limit": limit,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _clean_mutation_text(text: str, *, field: str) -> str:
|
|
481
|
+
cleaned = " ".join(text.split())
|
|
482
|
+
if not cleaned:
|
|
483
|
+
raise SystemExit(f"{field} is required.")
|
|
484
|
+
if len(cleaned) > MAX_MUTATION_TEXT_CHARS:
|
|
485
|
+
raise SystemExit(f"{field} exceeds {MAX_MUTATION_TEXT_CHARS} characters.")
|
|
486
|
+
if cleaned.startswith("#") or "```" in cleaned:
|
|
487
|
+
raise SystemExit(f"{field} contains unsupported Markdown structure.")
|
|
488
|
+
return cleaned
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _replace_indicator(lines: list[str], key: str, value: str) -> tuple[list[str], bool]:
|
|
492
|
+
rendered = f"> {key}: {value}"
|
|
493
|
+
for idx, line in enumerate(lines):
|
|
494
|
+
if line.startswith(f"> {key}:"):
|
|
495
|
+
if line == rendered:
|
|
496
|
+
return lines, False
|
|
497
|
+
updated = list(lines)
|
|
498
|
+
updated[idx] = rendered
|
|
499
|
+
return updated, True
|
|
500
|
+
insert_at = 1
|
|
501
|
+
while insert_at < len(lines) and lines[insert_at].startswith("> "):
|
|
502
|
+
insert_at += 1
|
|
503
|
+
updated = list(lines)
|
|
504
|
+
updated.insert(insert_at, rendered)
|
|
505
|
+
return updated, True
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def update_workflow_indicators_payload(repo_root: Path, source: str, indicators: dict[str, str], *, dry_run: bool = False) -> dict[str, object]:
|
|
509
|
+
unknown = sorted(set(indicators) - set(APPROVED_WORKFLOW_INDICATORS))
|
|
510
|
+
if unknown:
|
|
511
|
+
raise SystemExit(f"Unsupported workflow indicator(s): {', '.join(unknown)}.")
|
|
512
|
+
cleaned = {key: _clean_mutation_text(value, field=key) for key, value in indicators.items() if value is not None}
|
|
513
|
+
if not cleaned:
|
|
514
|
+
raise SystemExit("At least one workflow indicator is required.")
|
|
515
|
+
|
|
516
|
+
targets = _resolve_target_docs(repo_root, [source])
|
|
517
|
+
if len(targets) != 1:
|
|
518
|
+
raise SystemExit(f"Expected one workflow doc target for `{source}`.")
|
|
519
|
+
kind, path = targets[0]
|
|
520
|
+
lines = _read_lines(path)
|
|
521
|
+
changed = False
|
|
522
|
+
for key in APPROVED_WORKFLOW_INDICATORS:
|
|
523
|
+
if key not in cleaned:
|
|
524
|
+
continue
|
|
525
|
+
if key == "Progress" and kind not in {"backlog", "task"}:
|
|
526
|
+
raise SystemExit("Progress is only supported for backlog and task documents.")
|
|
527
|
+
lines, key_changed = _replace_indicator(lines, key, cleaned[key])
|
|
528
|
+
changed = changed or key_changed
|
|
529
|
+
if changed and not dry_run:
|
|
530
|
+
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
531
|
+
refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
|
|
532
|
+
return {
|
|
533
|
+
"path": path.relative_to(repo_root).as_posix(),
|
|
534
|
+
"ref": path.stem,
|
|
535
|
+
"kind": kind,
|
|
536
|
+
"updated_indicators": cleaned,
|
|
537
|
+
"changed": changed,
|
|
538
|
+
"dry_run": dry_run,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _section_for_note(kind: str, note_kind: str) -> str:
|
|
543
|
+
if note_kind == "report":
|
|
544
|
+
if kind != "task":
|
|
545
|
+
raise SystemExit("Report entries are only supported for task documents.")
|
|
546
|
+
return "Report"
|
|
547
|
+
if note_kind == "validation":
|
|
548
|
+
return "Validation"
|
|
549
|
+
if note_kind == "decision":
|
|
550
|
+
return "Decision framing" if kind == "backlog" else "Notes"
|
|
551
|
+
raise SystemExit(f"Unsupported note kind `{note_kind}`.")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def append_workflow_note_payload(repo_root: Path, source: str, *, note_kind: str, text: str, dry_run: bool = False) -> dict[str, object]:
|
|
555
|
+
targets = _resolve_target_docs(repo_root, [source])
|
|
556
|
+
if len(targets) != 1:
|
|
557
|
+
raise SystemExit(f"Expected one workflow doc target for `{source}`.")
|
|
558
|
+
kind, path = targets[0]
|
|
559
|
+
section = _section_for_note(kind, note_kind)
|
|
560
|
+
cleaned = _clean_mutation_text(text, field="text")
|
|
561
|
+
bullet = f"- {cleaned}"
|
|
562
|
+
lines = _read_lines(path)
|
|
563
|
+
insert_at = None
|
|
564
|
+
for idx, line in enumerate(lines):
|
|
565
|
+
if line.startswith("# ") and line[2:].strip().lower() == section.lower():
|
|
566
|
+
insert_at = idx + 1
|
|
567
|
+
while insert_at < len(lines) and lines[insert_at].strip().startswith("- "):
|
|
568
|
+
insert_at += 1
|
|
569
|
+
break
|
|
570
|
+
changed = True
|
|
571
|
+
if insert_at is None:
|
|
572
|
+
lines.extend(["", f"# {section}", bullet])
|
|
573
|
+
else:
|
|
574
|
+
existing = {line.strip() for line in lines if line.strip().startswith("- ")}
|
|
575
|
+
if bullet in existing:
|
|
576
|
+
changed = False
|
|
577
|
+
else:
|
|
578
|
+
lines.insert(insert_at, bullet)
|
|
579
|
+
if changed and not dry_run:
|
|
580
|
+
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
581
|
+
refresh_workflow_mermaid_signature_file(path, kind, dry_run=False, repo_root=repo_root)
|
|
582
|
+
return {
|
|
583
|
+
"path": path.relative_to(repo_root).as_posix(),
|
|
584
|
+
"ref": path.stem,
|
|
585
|
+
"kind": kind,
|
|
586
|
+
"section": section,
|
|
587
|
+
"text": cleaned,
|
|
588
|
+
"changed": changed,
|
|
589
|
+
"dry_run": dry_run,
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
344
593
|
def _graph_payload(repo_root: Path, *, config: dict[str, object] | None = None) -> dict[str, object]:
|
|
345
594
|
docs = _load_workflow_docs(repo_root)
|
|
346
595
|
nodes = []
|
|
@@ -481,6 +730,50 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
481
730
|
schema_status.add_argument("--format", choices=("text", "json"), default="text")
|
|
482
731
|
schema_status.set_defaults(func=cmd_schema_status)
|
|
483
732
|
|
|
733
|
+
read_doc = sub.add_parser("read-doc", help="Read a bounded workflow document payload by ref or path.")
|
|
734
|
+
read_doc.add_argument("source", help="Workflow ref or repo-relative path.")
|
|
735
|
+
read_doc.add_argument("--max-chars", type=int, default=4000)
|
|
736
|
+
read_doc.add_argument("--section", action="append", default=[], help="Section heading to include; repeatable.")
|
|
737
|
+
read_doc.add_argument("--format", choices=("text", "json"), default="text")
|
|
738
|
+
read_doc.set_defaults(func=cmd_read_doc)
|
|
739
|
+
|
|
740
|
+
list_docs = sub.add_parser("list-docs", help="List workflow docs by bounded criteria.")
|
|
741
|
+
list_docs.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
|
|
742
|
+
list_docs.add_argument("--status", default=None)
|
|
743
|
+
list_docs.add_argument("--ref-prefix", default=None)
|
|
744
|
+
list_docs.add_argument("--limit", type=int, default=50)
|
|
745
|
+
list_docs.add_argument("--format", choices=("text", "json"), default="text")
|
|
746
|
+
list_docs.set_defaults(func=cmd_list_docs)
|
|
747
|
+
|
|
748
|
+
search_docs = sub.add_parser("search-docs", help="Search approved workflow docs with bounded snippets.")
|
|
749
|
+
search_docs.add_argument("query")
|
|
750
|
+
search_docs.add_argument("--kind", choices=("all", "request", "backlog", "task"), default="all")
|
|
751
|
+
search_docs.add_argument("--status", default=None)
|
|
752
|
+
search_docs.add_argument("--limit", type=int, default=20)
|
|
753
|
+
search_docs.add_argument("--max-snippet-chars", type=int, default=240)
|
|
754
|
+
search_docs.add_argument("--format", choices=("text", "json"), default="text")
|
|
755
|
+
search_docs.set_defaults(func=cmd_search_docs)
|
|
756
|
+
|
|
757
|
+
update_indicators = sub.add_parser("update-indicators", help="Update approved indicators on one workflow doc.")
|
|
758
|
+
update_indicators.add_argument("source", help="Workflow ref or repo-relative path.")
|
|
759
|
+
update_indicators.add_argument("--status")
|
|
760
|
+
update_indicators.add_argument("--progress")
|
|
761
|
+
update_indicators.add_argument("--understanding")
|
|
762
|
+
update_indicators.add_argument("--confidence")
|
|
763
|
+
update_indicators.add_argument("--theme")
|
|
764
|
+
update_indicators.add_argument("--complexity")
|
|
765
|
+
update_indicators.add_argument("--format", choices=("text", "json"), default="text")
|
|
766
|
+
update_indicators.add_argument("--dry-run", action="store_true")
|
|
767
|
+
update_indicators.set_defaults(func=cmd_update_indicators)
|
|
768
|
+
|
|
769
|
+
append_note = sub.add_parser("append-note", help="Append a bounded note to an approved workflow section.")
|
|
770
|
+
append_note.add_argument("source", help="Workflow ref or repo-relative path.")
|
|
771
|
+
append_note.add_argument("--section", choices=("report", "validation", "decision"), required=True)
|
|
772
|
+
append_note.add_argument("--text", required=True)
|
|
773
|
+
append_note.add_argument("--format", choices=("text", "json"), default="text")
|
|
774
|
+
append_note.add_argument("--dry-run", action="store_true")
|
|
775
|
+
append_note.set_defaults(func=cmd_append_note)
|
|
776
|
+
|
|
484
777
|
context_pack = sub.add_parser("context-pack", help="Build a compact context pack from workflow docs.")
|
|
485
778
|
context_pack.add_argument("ref", help="Seed workflow ref for the context pack.")
|
|
486
779
|
context_pack.add_argument("--mode", choices=("summary-only", "diff-first", "full"), default="summary-only")
|
|
@@ -499,6 +792,242 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
499
792
|
return parser
|
|
500
793
|
|
|
501
794
|
|
|
795
|
+
def _build_help() -> str:
|
|
796
|
+
return "\n".join(
|
|
797
|
+
[
|
|
798
|
+
"Logics Sync CLI",
|
|
799
|
+
"Manage workflow transitions and exports.",
|
|
800
|
+
"",
|
|
801
|
+
"Usage:",
|
|
802
|
+
" logics-manager sync <command> [args...]",
|
|
803
|
+
"",
|
|
804
|
+
"Commands:",
|
|
805
|
+
" close-eligible-requests",
|
|
806
|
+
" Auto-close requests when all linked backlog items are done.",
|
|
807
|
+
" Flags: --format {text,json}, --dry-run",
|
|
808
|
+
"",
|
|
809
|
+
" refresh-mermaid-signatures",
|
|
810
|
+
" Refresh stale Mermaid signatures without rewriting diagram bodies.",
|
|
811
|
+
" Flags: --format {text,json}, --dry-run",
|
|
812
|
+
"",
|
|
813
|
+
" schema-status [sources...]",
|
|
814
|
+
" Report schema-version coverage for selected workflow docs.",
|
|
815
|
+
" Flags: --format {text,json}",
|
|
816
|
+
"",
|
|
817
|
+
" context-pack <ref>",
|
|
818
|
+
" Build a compact JSON context pack from workflow docs.",
|
|
819
|
+
" Flags: --mode {summary-only,diff-first,full}, --profile {tiny,normal,deep}, --out, --format {text,json}, --dry-run",
|
|
820
|
+
"",
|
|
821
|
+
" read-doc <source>",
|
|
822
|
+
" Read a bounded workflow document payload by ref or path.",
|
|
823
|
+
" Flags: --max-chars, --section, --format {text,json}",
|
|
824
|
+
"",
|
|
825
|
+
" list-docs",
|
|
826
|
+
" List workflow docs by bounded criteria.",
|
|
827
|
+
" Flags: --kind {all,request,backlog,task}, --status, --ref-prefix, --limit, --format {text,json}",
|
|
828
|
+
"",
|
|
829
|
+
" search-docs <query>",
|
|
830
|
+
" Search approved workflow docs with bounded snippets.",
|
|
831
|
+
" Flags: --kind {all,request,backlog,task}, --status, --limit, --max-snippet-chars, --format {text,json}",
|
|
832
|
+
"",
|
|
833
|
+
" update-indicators <source>",
|
|
834
|
+
" Update approved indicators on one workflow doc.",
|
|
835
|
+
" Flags: --status, --progress, --understanding, --confidence, --theme, --complexity, --format {text,json}, --dry-run",
|
|
836
|
+
"",
|
|
837
|
+
" append-note <source>",
|
|
838
|
+
" Append a bounded note to an approved workflow section.",
|
|
839
|
+
" Flags: --section {report,validation,decision}, --text, --format {text,json}, --dry-run",
|
|
840
|
+
"",
|
|
841
|
+
" export-graph",
|
|
842
|
+
" Export workflow relationships as a machine-readable graph.",
|
|
843
|
+
" Flags: --out, --format {text,json}, --dry-run",
|
|
844
|
+
"",
|
|
845
|
+
"Examples:",
|
|
846
|
+
" logics-manager sync schema-status",
|
|
847
|
+
" logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
|
|
848
|
+
" logics-manager sync export-graph --format json",
|
|
849
|
+
]
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _build_subcommand_help(command: str) -> str:
|
|
854
|
+
if command == "close-eligible-requests":
|
|
855
|
+
return "\n".join(
|
|
856
|
+
[
|
|
857
|
+
"Logics Sync Close Eligible Requests",
|
|
858
|
+
"Auto-close requests when all linked backlog items are done.",
|
|
859
|
+
"",
|
|
860
|
+
"Usage:",
|
|
861
|
+
" logics-manager sync close-eligible-requests [args...]",
|
|
862
|
+
"",
|
|
863
|
+
"Flags:",
|
|
864
|
+
" --format {text,json}",
|
|
865
|
+
" --dry-run",
|
|
866
|
+
"",
|
|
867
|
+
"Example:",
|
|
868
|
+
" logics-manager sync close-eligible-requests --dry-run",
|
|
869
|
+
]
|
|
870
|
+
)
|
|
871
|
+
if command == "refresh-mermaid-signatures":
|
|
872
|
+
return "\n".join(
|
|
873
|
+
[
|
|
874
|
+
"Logics Sync Refresh Mermaid Signatures",
|
|
875
|
+
"Refresh stale workflow Mermaid signatures without rewriting diagram bodies.",
|
|
876
|
+
"",
|
|
877
|
+
"Usage:",
|
|
878
|
+
" logics-manager sync refresh-mermaid-signatures [args...]",
|
|
879
|
+
"",
|
|
880
|
+
"Flags:",
|
|
881
|
+
" --format {text,json}",
|
|
882
|
+
" --dry-run",
|
|
883
|
+
]
|
|
884
|
+
)
|
|
885
|
+
if command == "schema-status":
|
|
886
|
+
return "\n".join(
|
|
887
|
+
[
|
|
888
|
+
"Logics Sync Schema Status",
|
|
889
|
+
"Report schema-version coverage for workflow docs.",
|
|
890
|
+
"",
|
|
891
|
+
"Usage:",
|
|
892
|
+
" logics-manager sync schema-status [sources...]",
|
|
893
|
+
"",
|
|
894
|
+
"Flags:",
|
|
895
|
+
" --format {text,json}",
|
|
896
|
+
"",
|
|
897
|
+
"Example:",
|
|
898
|
+
" logics-manager sync schema-status logics/request",
|
|
899
|
+
]
|
|
900
|
+
)
|
|
901
|
+
if command == "context-pack":
|
|
902
|
+
return "\n".join(
|
|
903
|
+
[
|
|
904
|
+
"Logics Sync Context Pack",
|
|
905
|
+
"Build a compact JSON context pack from workflow docs.",
|
|
906
|
+
"",
|
|
907
|
+
"Usage:",
|
|
908
|
+
" logics-manager sync context-pack <ref> [args...]",
|
|
909
|
+
"",
|
|
910
|
+
"Flags:",
|
|
911
|
+
" --mode {summary-only,diff-first,full}",
|
|
912
|
+
" --profile {tiny,normal,deep}",
|
|
913
|
+
" --out",
|
|
914
|
+
" --format {text,json}",
|
|
915
|
+
" --dry-run",
|
|
916
|
+
"",
|
|
917
|
+
"Example:",
|
|
918
|
+
" logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
|
|
919
|
+
]
|
|
920
|
+
)
|
|
921
|
+
if command == "read-doc":
|
|
922
|
+
return "\n".join(
|
|
923
|
+
[
|
|
924
|
+
"Logics Sync Read Doc",
|
|
925
|
+
"Read a bounded workflow document payload by ref or path.",
|
|
926
|
+
"",
|
|
927
|
+
"Usage:",
|
|
928
|
+
" logics-manager sync read-doc <source> [args...]",
|
|
929
|
+
"",
|
|
930
|
+
"Flags:",
|
|
931
|
+
" --max-chars",
|
|
932
|
+
" --section",
|
|
933
|
+
" --format {text,json}",
|
|
934
|
+
]
|
|
935
|
+
)
|
|
936
|
+
if command == "list-docs":
|
|
937
|
+
return "\n".join(
|
|
938
|
+
[
|
|
939
|
+
"Logics Sync List Docs",
|
|
940
|
+
"List workflow docs by bounded criteria.",
|
|
941
|
+
"",
|
|
942
|
+
"Usage:",
|
|
943
|
+
" logics-manager sync list-docs [args...]",
|
|
944
|
+
"",
|
|
945
|
+
"Flags:",
|
|
946
|
+
" --kind {all,request,backlog,task}",
|
|
947
|
+
" --status",
|
|
948
|
+
" --ref-prefix",
|
|
949
|
+
" --limit",
|
|
950
|
+
" --format {text,json}",
|
|
951
|
+
]
|
|
952
|
+
)
|
|
953
|
+
if command == "search-docs":
|
|
954
|
+
return "\n".join(
|
|
955
|
+
[
|
|
956
|
+
"Logics Sync Search Docs",
|
|
957
|
+
"Search approved workflow docs with bounded snippets.",
|
|
958
|
+
"",
|
|
959
|
+
"Usage:",
|
|
960
|
+
" logics-manager sync search-docs <query> [args...]",
|
|
961
|
+
"",
|
|
962
|
+
"Flags:",
|
|
963
|
+
" --kind {all,request,backlog,task}",
|
|
964
|
+
" --status",
|
|
965
|
+
" --limit",
|
|
966
|
+
" --max-snippet-chars",
|
|
967
|
+
" --format {text,json}",
|
|
968
|
+
]
|
|
969
|
+
)
|
|
970
|
+
if command == "update-indicators":
|
|
971
|
+
return "\n".join(
|
|
972
|
+
[
|
|
973
|
+
"Logics Sync Update Indicators",
|
|
974
|
+
"Update approved indicators on one workflow doc.",
|
|
975
|
+
"",
|
|
976
|
+
"Usage:",
|
|
977
|
+
" logics-manager sync update-indicators <source> [args...]",
|
|
978
|
+
"",
|
|
979
|
+
"Flags:",
|
|
980
|
+
" --status",
|
|
981
|
+
" --progress",
|
|
982
|
+
" --understanding",
|
|
983
|
+
" --confidence",
|
|
984
|
+
" --theme",
|
|
985
|
+
" --complexity",
|
|
986
|
+
" --format {text,json}",
|
|
987
|
+
" --dry-run",
|
|
988
|
+
]
|
|
989
|
+
)
|
|
990
|
+
if command == "append-note":
|
|
991
|
+
return "\n".join(
|
|
992
|
+
[
|
|
993
|
+
"Logics Sync Append Note",
|
|
994
|
+
"Append a bounded note to an approved workflow section.",
|
|
995
|
+
"",
|
|
996
|
+
"Usage:",
|
|
997
|
+
" logics-manager sync append-note <source> --section <section> --text <text> [args...]",
|
|
998
|
+
"",
|
|
999
|
+
"Flags:",
|
|
1000
|
+
" --section {report,validation,decision}",
|
|
1001
|
+
" --text",
|
|
1002
|
+
" --format {text,json}",
|
|
1003
|
+
" --dry-run",
|
|
1004
|
+
]
|
|
1005
|
+
)
|
|
1006
|
+
if command == "export-graph":
|
|
1007
|
+
return "\n".join(
|
|
1008
|
+
[
|
|
1009
|
+
"Logics Sync Export Graph",
|
|
1010
|
+
"Export workflow relationships as a machine-readable graph.",
|
|
1011
|
+
"",
|
|
1012
|
+
"Usage:",
|
|
1013
|
+
" logics-manager sync export-graph [args...]",
|
|
1014
|
+
"",
|
|
1015
|
+
"Flags:",
|
|
1016
|
+
" --out",
|
|
1017
|
+
" --format {text,json}",
|
|
1018
|
+
" --dry-run",
|
|
1019
|
+
"",
|
|
1020
|
+
"Example:",
|
|
1021
|
+
" logics-manager sync export-graph --format json",
|
|
1022
|
+
]
|
|
1023
|
+
)
|
|
1024
|
+
return _build_help()
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _print_help(text: str) -> None:
|
|
1028
|
+
print(colorize_help(text))
|
|
1029
|
+
|
|
1030
|
+
|
|
502
1031
|
def cmd_close_eligible_requests(args: argparse.Namespace) -> dict[str, object]:
|
|
503
1032
|
repo_root = _find_repo_root(Path.cwd())
|
|
504
1033
|
scanned, closed = _close_eligible_requests(repo_root, args.dry_run)
|
|
@@ -557,6 +1086,84 @@ def cmd_schema_status(args: argparse.Namespace) -> dict[str, object]:
|
|
|
557
1086
|
return {"command": "sync", "kind": "schema-status", "repo_root": repo_root.as_posix(), **payload}
|
|
558
1087
|
|
|
559
1088
|
|
|
1089
|
+
def _bounded_positive(value: int, *, default: int, maximum: int) -> int:
|
|
1090
|
+
if value <= 0:
|
|
1091
|
+
return default
|
|
1092
|
+
return min(value, maximum)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def cmd_read_doc(args: argparse.Namespace) -> dict[str, object]:
|
|
1096
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1097
|
+
payload = read_logics_doc_payload(repo_root, args.source, max_chars=_bounded_positive(args.max_chars, default=4000, maximum=12000), sections=args.section or None)
|
|
1098
|
+
if args.format == "json":
|
|
1099
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1100
|
+
else:
|
|
1101
|
+
print(f"{payload['ref']} ({payload['kind']}): {payload['title']}")
|
|
1102
|
+
print(f"- path: {payload['path']}")
|
|
1103
|
+
print(f"- status: {payload['status']}")
|
|
1104
|
+
print(f"- truncated: {payload['truncated']}")
|
|
1105
|
+
return {"command": "sync", "kind": "read-doc", "repo_root": repo_root.as_posix(), **payload}
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def cmd_list_docs(args: argparse.Namespace) -> dict[str, object]:
|
|
1109
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1110
|
+
payload = list_logics_docs_payload(repo_root, kind=args.kind, status=args.status, ref_prefix=args.ref_prefix, limit=_bounded_positive(args.limit, default=50, maximum=200))
|
|
1111
|
+
if args.format == "json":
|
|
1112
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1113
|
+
else:
|
|
1114
|
+
print(f"Workflow docs: {payload['returned_count']} returned of {payload['total_count']}")
|
|
1115
|
+
for item in payload["items"]:
|
|
1116
|
+
print(f"- {item['ref']} [{item['status']}]: {item['title']}")
|
|
1117
|
+
return {"command": "sync", "kind": "list-docs", "repo_root": repo_root.as_posix(), **payload}
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def cmd_search_docs(args: argparse.Namespace) -> dict[str, object]:
|
|
1121
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1122
|
+
payload = search_logics_docs_payload(
|
|
1123
|
+
repo_root,
|
|
1124
|
+
args.query,
|
|
1125
|
+
kind=args.kind,
|
|
1126
|
+
status=args.status,
|
|
1127
|
+
limit=_bounded_positive(args.limit, default=20, maximum=100),
|
|
1128
|
+
max_snippet_chars=_bounded_positive(args.max_snippet_chars, default=240, maximum=1000),
|
|
1129
|
+
)
|
|
1130
|
+
if args.format == "json":
|
|
1131
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1132
|
+
else:
|
|
1133
|
+
print(f"Search `{payload['query']}`: {payload['returned_count']} match(es)")
|
|
1134
|
+
for match in payload["matches"]:
|
|
1135
|
+
print(f"- {match['ref']}:{match['line']} {match['title']}")
|
|
1136
|
+
return {"command": "sync", "kind": "search-docs", "repo_root": repo_root.as_posix(), **payload}
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def cmd_update_indicators(args: argparse.Namespace) -> dict[str, object]:
|
|
1140
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1141
|
+
indicators = {
|
|
1142
|
+
"Status": args.status,
|
|
1143
|
+
"Progress": args.progress,
|
|
1144
|
+
"Understanding": args.understanding,
|
|
1145
|
+
"Confidence": args.confidence,
|
|
1146
|
+
"Theme": args.theme,
|
|
1147
|
+
"Complexity": args.complexity,
|
|
1148
|
+
}
|
|
1149
|
+
payload = update_workflow_indicators_payload(repo_root, args.source, {key: value for key, value in indicators.items() if value is not None}, dry_run=args.dry_run)
|
|
1150
|
+
if args.format == "json":
|
|
1151
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1152
|
+
else:
|
|
1153
|
+
print(f"Updated indicators for {payload['path']} (changed: {payload['changed']}).")
|
|
1154
|
+
return {"command": "sync", "kind": "update-indicators", "repo_root": repo_root.as_posix(), **payload}
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def cmd_append_note(args: argparse.Namespace) -> dict[str, object]:
|
|
1158
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
1159
|
+
payload = append_workflow_note_payload(repo_root, args.source, note_kind=args.section, text=args.text, dry_run=args.dry_run)
|
|
1160
|
+
if args.format == "json":
|
|
1161
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
1162
|
+
else:
|
|
1163
|
+
print(f"Appended {args.section} note to {payload['path']} (changed: {payload['changed']}).")
|
|
1164
|
+
return {"command": "sync", "kind": "append-note", "repo_root": repo_root.as_posix(), **payload}
|
|
1165
|
+
|
|
1166
|
+
|
|
560
1167
|
def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
|
|
561
1168
|
repo_root = _find_repo_root(Path.cwd())
|
|
562
1169
|
payload = _build_context_pack(repo_root, args.ref, mode=args.mode, profile=args.profile, config=None)
|
|
@@ -598,6 +1205,12 @@ def cmd_export_graph(args: argparse.Namespace) -> dict[str, object]:
|
|
|
598
1205
|
|
|
599
1206
|
|
|
600
1207
|
def main(argv: list[str]) -> int:
|
|
1208
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
1209
|
+
_print_help(_build_help())
|
|
1210
|
+
return 0
|
|
1211
|
+
if argv[0] in {"close-eligible-requests", "refresh-mermaid-signatures", "schema-status", "read-doc", "list-docs", "search-docs", "update-indicators", "append-note", "context-pack", "export-graph"} and len(argv) > 1 and argv[1] in ("-h", "--help"):
|
|
1212
|
+
_print_help(_build_subcommand_help(argv[0]))
|
|
1213
|
+
return 0
|
|
601
1214
|
parser = build_parser()
|
|
602
1215
|
args = parser.parse_args(argv)
|
|
603
1216
|
payload = args.func(args)
|