@grifhinz/logics-manager 2.0.5 → 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 +163 -156
- package/VERSION +1 -1
- package/logics_manager/cli.py +13 -3
- package/logics_manager/flow.py +10 -7
- package/logics_manager/mcp.py +1188 -0
- package/logics_manager/sync.py +476 -1
- package/package.json +9 -1
- package/pyproject.toml +1 -1
package/logics_manager/sync.py
CHANGED
|
@@ -38,6 +38,8 @@ REF_PREFIXES = ("req", "item", "task", "prod", "adr", "spec")
|
|
|
38
38
|
_CONTEXT_PACK_CACHE: dict[str, dict[str, object]] = {}
|
|
39
39
|
MERMAID_BLOCK_PATTERN = re.compile(r"```mermaid\s*\n(.*?)\n```", re.DOTALL)
|
|
40
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
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def _read_text(path: Path) -> str:
|
|
@@ -303,6 +305,9 @@ def _resolve_target_docs(repo_root: Path, sources: list[str]) -> list[tuple[str,
|
|
|
303
305
|
|
|
304
306
|
resolved: list[tuple[str, Path]] = []
|
|
305
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}`.")
|
|
306
311
|
candidate = (repo_root / source).resolve()
|
|
307
312
|
if candidate.is_file():
|
|
308
313
|
for kind_name, kind in DOC_KINDS.items():
|
|
@@ -342,6 +347,249 @@ def _schema_status(repo_root: Path, targets: list[str]) -> dict[str, object]:
|
|
|
342
347
|
}
|
|
343
348
|
|
|
344
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
|
+
|
|
345
593
|
def _graph_payload(repo_root: Path, *, config: dict[str, object] | None = None) -> dict[str, object]:
|
|
346
594
|
docs = _load_workflow_docs(repo_root)
|
|
347
595
|
nodes = []
|
|
@@ -482,6 +730,50 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
482
730
|
schema_status.add_argument("--format", choices=("text", "json"), default="text")
|
|
483
731
|
schema_status.set_defaults(func=cmd_schema_status)
|
|
484
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
|
+
|
|
485
777
|
context_pack = sub.add_parser("context-pack", help="Build a compact context pack from workflow docs.")
|
|
486
778
|
context_pack.add_argument("ref", help="Seed workflow ref for the context pack.")
|
|
487
779
|
context_pack.add_argument("--mode", choices=("summary-only", "diff-first", "full"), default="summary-only")
|
|
@@ -526,6 +818,26 @@ def _build_help() -> str:
|
|
|
526
818
|
" Build a compact JSON context pack from workflow docs.",
|
|
527
819
|
" Flags: --mode {summary-only,diff-first,full}, --profile {tiny,normal,deep}, --out, --format {text,json}, --dry-run",
|
|
528
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
|
+
"",
|
|
529
841
|
" export-graph",
|
|
530
842
|
" Export workflow relationships as a machine-readable graph.",
|
|
531
843
|
" Flags: --out, --format {text,json}, --dry-run",
|
|
@@ -606,6 +918,91 @@ def _build_subcommand_help(command: str) -> str:
|
|
|
606
918
|
" logics-manager sync context-pack req_001_my_request --out logics/context-pack.json",
|
|
607
919
|
]
|
|
608
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
|
+
)
|
|
609
1006
|
if command == "export-graph":
|
|
610
1007
|
return "\n".join(
|
|
611
1008
|
[
|
|
@@ -689,6 +1086,84 @@ def cmd_schema_status(args: argparse.Namespace) -> dict[str, object]:
|
|
|
689
1086
|
return {"command": "sync", "kind": "schema-status", "repo_root": repo_root.as_posix(), **payload}
|
|
690
1087
|
|
|
691
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
|
+
|
|
692
1167
|
def cmd_context_pack(args: argparse.Namespace) -> dict[str, object]:
|
|
693
1168
|
repo_root = _find_repo_root(Path.cwd())
|
|
694
1169
|
payload = _build_context_pack(repo_root, args.ref, mode=args.mode, profile=args.profile, config=None)
|
|
@@ -733,7 +1208,7 @@ def main(argv: list[str]) -> int:
|
|
|
733
1208
|
if not argv or argv[0] in ("-h", "--help"):
|
|
734
1209
|
_print_help(_build_help())
|
|
735
1210
|
return 0
|
|
736
|
-
if argv[0] in {"close-eligible-requests", "refresh-mermaid-signatures", "schema-status", "context-pack", "export-graph"} and len(argv) > 1 and argv[1] in ("-h", "--help"):
|
|
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"):
|
|
737
1212
|
_print_help(_build_subcommand_help(argv[0]))
|
|
738
1213
|
return 0
|
|
739
1214
|
parser = build_parser()
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@grifhinz/logics-manager",
|
|
3
3
|
"displayName": "Logics Orchestrator",
|
|
4
4
|
"description": "Visual orchestration for Logics workflows inside VS Code.",
|
|
5
|
-
"version": "2.0
|
|
5
|
+
"version": "2.1.0",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "media/icon.png",
|
|
8
8
|
"repository": {
|
|
@@ -159,5 +159,13 @@
|
|
|
159
159
|
"vitest": "^4.1.2",
|
|
160
160
|
"yaml": "^2.8.3",
|
|
161
161
|
"yauzl": "^3.2.0"
|
|
162
|
+
},
|
|
163
|
+
"overrides": {
|
|
164
|
+
"fast-uri": "3.1.2",
|
|
165
|
+
"minimatch@10.2.5": {
|
|
166
|
+
"brace-expansion": "5.0.6"
|
|
167
|
+
},
|
|
168
|
+
"qs": "6.15.2",
|
|
169
|
+
"ws": "8.21.0"
|
|
162
170
|
}
|
|
163
171
|
}
|