its-magic 0.1.2-36 → 0.1.2-38

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 CHANGED
@@ -367,6 +367,7 @@ deterministic **`intake_evidence`** gate — **`topic_coverage`** with valid **`
367
367
  asked-vs-covered alignment, and **`assumption_confirmation_ref`** when assumptions are affirmative.
368
368
 
369
369
  - Run `python scripts/intake_evidence_validate.py --self-test` (also exercised via `tests/run-tests.*` §26k).
370
+ - **Packaged installs (BUG-0001 / DEC-0063)**: the intake gate modules (`intake_evidence_validate.py`, `intake_evidence_lib.py`, `intake_bug_routing_guard.py`) ship under **`template/scripts/`** and hydrate consumer repos at **`scripts/`** (npm **`files`**, Chocolatey/Homebrew **`template/`** tree, **`installer.ps1` / `installer.sh`** + **`installer-owned-paths.manifest`**). **`--mode upgrade`** treats them as framework files (added/updated like other shipped scripts). CI parity: **`python scripts/check_intake_template_parity.py --repo .`** (`tests/run-tests.*` §26N).
370
371
  - Operator docs: **`decisions/DEC-0060.md`**, **`docs/engineering/architecture.md`** **`# US-0078`**, runbook section **Interactive intake evidence validation (US-0078 / DEC-0060)**.
371
372
  - **Guided** and **low-touch** share the **same pre-persistence validation pipeline**; low-touch does not bypass mandatory pack coverage.
372
373
 
package/installer.ps1 CHANGED
@@ -405,6 +405,24 @@ function Invoke-ScratchpadPostinstall {
405
405
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
406
406
  }
407
407
 
408
+ function Invoke-InstallCompletenessValidation {
409
+ param(
410
+ [string]$TargetRoot
411
+ )
412
+ $installerPy = Join-Path $scriptDir "installer.py"
413
+ if (-not (Test-Path $installerPy -PathType Leaf)) {
414
+ Write-Host "[INSTALL_COMPLETENESS_FAILED] installer.py missing next to installer.ps1."
415
+ exit 1
416
+ }
417
+ $py = Get-Command python -ErrorAction SilentlyContinue
418
+ if (-not $py) {
419
+ Write-Host "[INSTALL_COMPLETENESS_FAILED] PYTHON_NOT_FOUND: Python is required for deterministic installer completeness validation."
420
+ exit 1
421
+ }
422
+ & python $installerPy --validate-install-completeness --target $TargetRoot
423
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
424
+ }
425
+
408
426
  function Show-ItsMagicBanner([switch]$IncludeInstallMessage) {
409
427
  $prev = [Console]::OutputEncoding
410
428
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
@@ -643,6 +661,7 @@ if ($mode -eq "upgrade") {
643
661
  }
644
662
 
645
663
  Invoke-ScratchpadPostinstall -TargetRoot $targetRoot -Mode "upgrade"
664
+ Invoke-InstallCompletenessValidation -TargetRoot $targetRoot
646
665
 
647
666
  Write-InstalledVersion $targetRoot $appVersion
648
667
  Sync-RootReadmeToItsMagic $targetRoot | Out-Null
@@ -724,6 +743,7 @@ foreach ($rel in $files) {
724
743
  }
725
744
 
726
745
  Invoke-ScratchpadPostinstall -TargetRoot $targetRoot -Mode $mode
746
+ Invoke-InstallCompletenessValidation -TargetRoot $targetRoot
727
747
 
728
748
  Write-InstalledVersion $targetRoot $appVersion
729
749
  Sync-RootReadmeToItsMagic $targetRoot | Out-Null
package/installer.py CHANGED
@@ -11,6 +11,7 @@ from datetime import datetime
11
11
 
12
12
  REPO_URL = "https://github.com/fl0wm0ti0n/its-magic"
13
13
  MANIFEST_RELATIVE_PATH = os.path.join("docs", "engineering", "context", "installer-owned-paths.manifest")
14
+ MANIFEST_REQUIRED_SCRIPTS_SECTION = "required_install_script_paths"
14
15
 
15
16
 
16
17
  def normalize(path):
@@ -67,12 +68,42 @@ def load_ownership_manifest(source_root, script_dir):
67
68
  continue
68
69
  install_paths = read_manifest_paths(path, "install_include_paths")
69
70
  clean_paths = read_manifest_paths(path, "clean_paths")
71
+ required_script_paths = read_manifest_paths(path, MANIFEST_REQUIRED_SCRIPTS_SECTION)
70
72
  if not install_paths or not clean_paths:
71
73
  raise RuntimeError(f"[INSTALL_MANIFEST_ERROR] {path} is missing required sections or entries.")
72
- return install_paths, clean_paths
74
+ if not required_script_paths:
75
+ raise RuntimeError(
76
+ f"[INSTALL_MANIFEST_ERROR] {path} is missing [{MANIFEST_REQUIRED_SCRIPTS_SECTION}] entries."
77
+ )
78
+ return install_paths, clean_paths, required_script_paths, path
73
79
  raise RuntimeError("[INSTALL_SOURCE_ERROR] installer-owned-paths.manifest not found. Reinstall its-magic package.")
74
80
 
75
81
 
82
+ def validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
83
+ missing_paths = []
84
+ for rel in sorted(set(required_script_paths)):
85
+ src = os.path.join(source_root, rel)
86
+ dst = os.path.join(target_root, rel)
87
+ if not os.path.isfile(src) or not os.path.isfile(dst):
88
+ missing_paths.append(rel.replace("\\", "/"))
89
+ if not missing_paths:
90
+ return True
91
+ print(
92
+ "[INSTALL_COMPLETENESS_FAILED] Required installer scripts are missing after "
93
+ "copy/classification invariant check."
94
+ )
95
+ for rel in missing_paths:
96
+ print(f"[INSTALL_REQUIRED_SCRIPT_MISSING:{rel}]")
97
+ print(
98
+ "Fix: update manifest parity and required-script inventory at "
99
+ f"{MANIFEST_RELATIVE_PATH} (section [{MANIFEST_REQUIRED_SCRIPTS_SECTION}]), "
100
+ "ensure each listed script exists in template/scripts and clean-path ownership, "
101
+ "then rerun installer missing/upgrade."
102
+ )
103
+ print(f"Manifest source: {manifest_path}")
104
+ return False
105
+
106
+
76
107
  def ensure_parent(path):
77
108
  parent = os.path.dirname(path)
78
109
  if parent and not os.path.isdir(parent):
@@ -684,6 +715,12 @@ def main():
684
715
  action="store_true",
685
716
  help=argparse.SUPPRESS,
686
717
  )
718
+ parser.add_argument("--source-root", help=argparse.SUPPRESS)
719
+ parser.add_argument(
720
+ "--validate-install-completeness",
721
+ action="store_true",
722
+ help=argparse.SUPPRESS,
723
+ )
687
724
  args = parser.parse_args()
688
725
 
689
726
  if len(sys.argv) == 1 or args.help:
@@ -694,6 +731,9 @@ def main():
694
731
  print(f"its-magic v{version}")
695
732
  return 0
696
733
 
734
+ if args.source_root:
735
+ source_root = normalize(args.source_root)
736
+
697
737
  if args.scratchpad_postinstall:
698
738
  target_root = normalize(args.target) if args.target else normalize(".")
699
739
  mode = args.mode or "missing"
@@ -712,11 +752,31 @@ def main():
712
752
  ok = run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True)
713
753
  return 0 if ok else 1
714
754
 
755
+ if args.validate_install_completeness:
756
+ target_root = normalize(args.target) if args.target else normalize(".")
757
+ if not os.path.isdir(target_root):
758
+ print(f"[INSTALL_COMPLETENESS_FAILED] target directory missing: {target_root}")
759
+ return 1
760
+ if not os.path.isdir(source_root):
761
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
762
+ return 1
763
+ try:
764
+ _install_paths, _clean_paths, required_script_paths, manifest_path = load_ownership_manifest(
765
+ source_root, script_dir
766
+ )
767
+ except RuntimeError as exc:
768
+ print(str(exc))
769
+ return 1
770
+ ok = validate_install_completeness(target_root, source_root, required_script_paths, manifest_path)
771
+ return 0 if ok else 1
772
+
715
773
  if not os.path.isdir(source_root):
716
774
  print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
717
775
  return 1
718
776
  try:
719
- include_paths, clean_paths = load_ownership_manifest(source_root, script_dir)
777
+ include_paths, clean_paths, required_script_paths, manifest_path = load_ownership_manifest(
778
+ source_root, script_dir
779
+ )
720
780
  except RuntimeError as exc:
721
781
  print(str(exc))
722
782
  return 1
@@ -818,6 +878,8 @@ def main():
818
878
 
819
879
  if not run_scratchpad_postinstall(target_root, source_root, "upgrade", print_ok=True):
820
880
  return 1
881
+ if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
882
+ return 1
821
883
 
822
884
  write_installed_version(target_root, version)
823
885
  sync_root_readme_to_its_magic(target_root)
@@ -896,6 +958,8 @@ def main():
896
958
 
897
959
  if not run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
898
960
  return 1
961
+ if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
962
+ return 1
899
963
 
900
964
  write_installed_version(target_root, version)
901
965
  sync_root_readme_to_its_magic(target_root)
package/installer.sh CHANGED
@@ -151,6 +151,23 @@ scratchpad_postinstall() {
151
151
  fi
152
152
  }
153
153
 
154
+ validate_install_completeness() {
155
+ target_root="$1"
156
+ installer_py="$SCRIPT_DIR/installer.py"
157
+ if [ ! -f "$installer_py" ]; then
158
+ printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] installer.py missing next to installer.sh."
159
+ exit 1
160
+ fi
161
+ if command -v python3 >/dev/null 2>&1; then
162
+ python3 "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
163
+ elif command -v python >/dev/null 2>&1; then
164
+ python "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
165
+ else
166
+ printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] PYTHON_NOT_FOUND: Python is required for deterministic installer completeness validation."
167
+ exit 1
168
+ fi
169
+ }
170
+
154
171
  classify_file() {
155
172
  rel="$1"
156
173
  case "$rel" in
@@ -535,6 +552,7 @@ if [ "$MODE" = "upgrade" ]; then
535
552
  done
536
553
 
537
554
  scratchpad_postinstall "$TARGET_ROOT" "upgrade"
555
+ validate_install_completeness "$TARGET_ROOT"
538
556
 
539
557
  write_installed_version "$TARGET_ROOT" "$APP_VERSION"
540
558
  sync_root_readme_to_its_magic "$TARGET_ROOT" || true
@@ -606,6 +624,7 @@ for rel in $FILES; do
606
624
  done
607
625
 
608
626
  scratchpad_postinstall "$TARGET_ROOT" "$MODE"
627
+ validate_install_completeness "$TARGET_ROOT"
609
628
 
610
629
  write_installed_version "$TARGET_ROOT" "$APP_VERSION"
611
630
  sync_root_readme_to_its_magic "$TARGET_ROOT" || true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "its-magic",
3
- "version": "0.1.2-36",
3
+ "version": "0.1.2-38",
4
4
  "description": "its-magic - AI dev team workflow for Cursor.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -12,6 +12,11 @@
12
12
  "installer.sh",
13
13
  "installer.py",
14
14
  "scripts/doc_profile_lib.py",
15
+ "scripts/intake_evidence_validate.py",
16
+ "scripts/intake_evidence_lib.py",
17
+ "scripts/intake_bug_routing_guard.py",
18
+ "scripts/check_intake_template_parity.py",
19
+ "scripts/materialize_codebase_map.py",
15
20
  "bin/its-magic.js",
16
21
  "bin/postinstall.js"
17
22
  ],
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Verify active vs template/scripts/ bytes match for DEC-0063 intake gate modules (BUG-0001)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # Normative pairs: repo scripts/ (canonical dev) → template/scripts/ (packaged ship path).
11
+ INTAKE_TEMPLATE_PAIRS: tuple[tuple[str, str], ...] = (
12
+ ("scripts/intake_evidence_validate.py", "template/scripts/intake_evidence_validate.py"),
13
+ ("scripts/intake_evidence_lib.py", "template/scripts/intake_evidence_lib.py"),
14
+ ("scripts/intake_bug_routing_guard.py", "template/scripts/intake_bug_routing_guard.py"),
15
+ ("scripts/check_intake_template_parity.py", "template/scripts/check_intake_template_parity.py"),
16
+ )
17
+
18
+
19
+ def main() -> int:
20
+ p = argparse.ArgumentParser(description=__doc__)
21
+ p.add_argument(
22
+ "--repo",
23
+ type=Path,
24
+ default=Path(__file__).resolve().parent.parent,
25
+ help="Repository root",
26
+ )
27
+ args = p.parse_args()
28
+ root: Path = args.repo
29
+ failed = False
30
+ for rel_active, rel_tpl in INTAKE_TEMPLATE_PAIRS:
31
+ a = root / rel_active
32
+ t = root / rel_tpl
33
+ if not a.is_file() or not t.is_file():
34
+ print(f"[INTAKE_TEMPLATE_PARITY_ERROR] missing file: {rel_active} or {rel_tpl}")
35
+ failed = True
36
+ continue
37
+ ba = a.read_bytes()
38
+ bt = t.read_bytes()
39
+ if ba != bt:
40
+ print(
41
+ f"[INTAKE_TEMPLATE_PARITY_ERROR] mismatch: {rel_active} ({len(ba)}b) "
42
+ f"!= {rel_tpl} ({len(bt)}b)"
43
+ )
44
+ failed = True
45
+ if failed:
46
+ return 2
47
+ print("[INTAKE_TEMPLATE_PARITY_OK]")
48
+ return 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ sys.exit(main())
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fail-closed guard: defect-shaped prose must not persist as US-xxxx without bug routing (DEC-0061 §5).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import re
10
+ import sys
11
+
12
+ # Strong defect signals (deterministic heuristic — PO still must set INTAKE_WORK_ITEM_KIND or /intake bug)
13
+ _REPRO = re.compile(
14
+ r"\b(steps\s+to\s+reproduce|steps_to_reproduce|repro\s+steps|reproduction\s+steps)\b",
15
+ re.IGNORECASE,
16
+ )
17
+ _DEFECT = re.compile(
18
+ r"\b(bug|regression|defect|crash|stack\s+trace|broken|throws\s+exception)\b",
19
+ re.IGNORECASE,
20
+ )
21
+
22
+
23
+ def prose_looks_like_defect(text: str) -> bool:
24
+ low = text.lower()
25
+ if not _DEFECT.search(low):
26
+ return False
27
+ if _REPRO.search(low):
28
+ return True
29
+ if "expected" in low and "actual" in low:
30
+ return True
31
+ return False
32
+
33
+
34
+ def main() -> int:
35
+ ap = argparse.ArgumentParser(
36
+ description="If work item kind is story but prose looks like a defect report, fail with INTAKE_BUG_ROUTING_REQUIRED."
37
+ )
38
+ ap.add_argument("--kind", choices=("story", "bug"), required=True)
39
+ ap.add_argument("--file", help="Path to prose file (title+summary)")
40
+ ap.add_argument("--stdin", action="store_true", help="Read prose from stdin")
41
+ args = ap.parse_args()
42
+
43
+ if args.kind == "bug":
44
+ print("[INTAKE_BUG_ROUTING_OK] kind=bug")
45
+ return 0
46
+
47
+ if args.stdin:
48
+ text = sys.stdin.read()
49
+ elif args.file:
50
+ text = open(args.file, encoding="utf-8").read()
51
+ else:
52
+ print("INTAKE_BUG_ROUTING_GUARD_ERROR: provide --file or --stdin", file=sys.stderr)
53
+ return 2
54
+
55
+ if prose_looks_like_defect(text):
56
+ print(
57
+ "INTAKE_BUG_ROUTING_REQUIRED: defect-shaped prose with INTAKE_WORK_ITEM_KIND=story "
58
+ "(set INTAKE_WORK_ITEM_KIND=bug and/or use `/intake bug` per DEC-0061 §5)",
59
+ file=sys.stderr,
60
+ )
61
+ return 3
62
+ print("[INTAKE_BUG_ROUTING_OK] kind=story")
63
+ return 0
64
+
65
+
66
+ if __name__ == "__main__":
67
+ raise SystemExit(main())