@foggy-projects/deepseek-harness-plugin 0.4.0-beta.5 → 0.4.0-beta.7

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.
@@ -34,6 +34,7 @@ PROFILE_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{0,62}$")
34
34
  ENV_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
35
35
  OPAQUE_PROFILE_PATTERN = re.compile(r"^fop_[a-f0-9]{32}$")
36
36
  OPAQUE_REVISION_PATTERN = re.compile(r"^sha256:[a-f0-9]{64}$")
37
+ OPAQUE_PROFILE_SCHEMA = "foggy-runtime-onboarding-profile/v1"
37
38
  MODEL_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$")
38
39
 
39
40
 
@@ -283,11 +284,20 @@ def materialize(
283
284
  progress_index: int = 0,
284
285
  progress_total: int = 1,
285
286
  progress_message: str = "Downloading and verifying asset",
287
+ replace_corrupt: bool = False,
286
288
  ) -> dict:
287
289
  destination.parent.mkdir(parents=True, exist_ok=True)
288
290
  if destination.is_file():
289
- verify_asset(destination, asset["sha256"])
290
- return {"file": asset["file"], "path": str(destination), "source": "existing", "sha256": asset["sha256"]}
291
+ try:
292
+ verify_asset(destination, asset["sha256"])
293
+ return {"file": asset["file"], "path": str(destination), "source": "existing", "sha256": asset["sha256"]}
294
+ except OnboardingError:
295
+ if not replace_corrupt:
296
+ raise
297
+ quarantine = destination.with_name(
298
+ destination.name + f".corrupt-{dt.datetime.now().strftime('%Y%m%d-%H%M%S-%f')}"
299
+ )
300
+ destination.replace(quarantine)
291
301
  cached = cached_asset(asset["file"], asset["sha256"], cache_dirs)
292
302
  if cached:
293
303
  shutil.copy2(cached, destination)
@@ -448,6 +458,180 @@ def onboarding_state_path(data_root: Path, profile: str) -> Path:
448
458
  return data_root / "onboarding" / "profiles" / f"{safe_profile(profile)}.json"
449
459
 
450
460
 
461
+ def configure_profile_store(data_root: Path, *, create: bool = True) -> Path:
462
+ """Keep opaque CLI profiles in the persistent, private Foggy data root by default."""
463
+ configured = os.environ.get("FOGGY_RUNTIME_PROFILE_STORE")
464
+ destination = normalized(configured) if configured else data_root / "cli-profiles"
465
+ if not configured:
466
+ os.environ["FOGGY_RUNTIME_PROFILE_STORE"] = str(destination)
467
+ if create:
468
+ destination.mkdir(parents=True, exist_ok=True)
469
+ if os.name != "nt":
470
+ destination.chmod(0o700)
471
+ return destination
472
+
473
+
474
+ def legacy_profile_stores(destination: Path) -> list[Path]:
475
+ configured = [
476
+ normalized(item)
477
+ for item in os.environ.get("FOGGY_RUNTIME_PROFILE_LEGACY_STORES", "").split(os.pathsep)
478
+ if item.strip()
479
+ ]
480
+ default_legacy = Path(tempfile.gettempdir()) / "foggy-profiles"
481
+ candidates = [*configured, normalized(default_legacy)]
482
+ unique: list[Path] = []
483
+ for candidate in candidates:
484
+ if candidate == destination or candidate in unique:
485
+ continue
486
+ unique.append(candidate)
487
+ return unique
488
+
489
+
490
+ def validate_opaque_profile_document(payload: dict, path: Path) -> dict:
491
+ allowed = {"schemaVersion", "profileId", "revision", "createdAt", "updatedAt", "connection"}
492
+ unexpected = sorted(set(payload) - allowed)
493
+ if unexpected:
494
+ raise OnboardingError(f"Unsupported opaque profile fields in {path.name}: {', '.join(unexpected)}")
495
+ if payload.get("schemaVersion") != OPAQUE_PROFILE_SCHEMA:
496
+ raise OnboardingError(f"Unexpected opaque profile schema in {path.name}")
497
+ profile_id = payload.get("profileId")
498
+ revision = payload.get("revision")
499
+ if not isinstance(profile_id, str) or not OPAQUE_PROFILE_PATTERN.fullmatch(profile_id):
500
+ raise OnboardingError(f"Invalid opaque profile ID in {path.name}")
501
+ if path.stem != profile_id:
502
+ raise OnboardingError(f"Opaque profile filename does not match its ID: {path.name}")
503
+ if not isinstance(revision, str) or not OPAQUE_REVISION_PATTERN.fullmatch(revision):
504
+ raise OnboardingError(f"Invalid opaque profile revision in {path.name}")
505
+ connection = payload.get("connection")
506
+ if not isinstance(connection, dict):
507
+ raise OnboardingError(f"Opaque profile connection is missing in {path.name}")
508
+ allowed_connection = {"name", "type", "jdbcUrl", "username", "passwordEnv", "namespace"}
509
+ unexpected_connection = sorted(set(connection) - allowed_connection)
510
+ if unexpected_connection:
511
+ raise OnboardingError(
512
+ f"Unsupported opaque connection fields in {path.name}: {', '.join(unexpected_connection)}"
513
+ )
514
+ for name in ("name", "type", "jdbcUrl", "namespace"):
515
+ if not isinstance(connection.get(name), str) or not connection[name].strip():
516
+ raise OnboardingError(f"Opaque profile connection.{name} is invalid in {path.name}")
517
+ password_env = connection.get("passwordEnv")
518
+ if password_env is not None and (
519
+ not isinstance(password_env, str) or not ENV_NAME_PATTERN.fullmatch(password_env)
520
+ ):
521
+ raise OnboardingError(f"Opaque profile passwordEnv is invalid in {path.name}")
522
+ jdbc_url = connection["jdbcUrl"]
523
+ if re.search(r"(?i)(?:password|passwd|pwd)\s*=", jdbc_url) or re.search(r"//[^/@:]+:[^/@]+@", jdbc_url):
524
+ raise OnboardingError(f"Opaque profile embeds a password in jdbcUrl: {path.name}")
525
+ return payload
526
+
527
+
528
+ def profile_migration_inventory(data_root: Path) -> dict:
529
+ destination = configure_profile_store(data_root, create=False)
530
+ entries: list[dict] = []
531
+ for source in legacy_profile_stores(destination):
532
+ if not source.is_dir():
533
+ continue
534
+ for path in sorted(source.glob("fop_*.json")):
535
+ try:
536
+ payload = validate_opaque_profile_document(read_json_object(path, "Opaque profile"), path)
537
+ target = destination / path.name
538
+ status = "pending"
539
+ if target.is_file():
540
+ existing = validate_opaque_profile_document(read_json_object(target, "Opaque profile"), target)
541
+ status = "migrated" if existing == payload else "conflict"
542
+ entries.append({
543
+ "profileId": payload["profileId"],
544
+ "revision": payload["revision"],
545
+ "source": str(source),
546
+ "destination": str(destination),
547
+ "status": status,
548
+ })
549
+ except OnboardingError as exc:
550
+ entries.append({
551
+ "profileId": path.stem if OPAQUE_PROFILE_PATTERN.fullmatch(path.stem) else None,
552
+ "source": str(source),
553
+ "destination": str(destination),
554
+ "status": "invalid",
555
+ "error": str(exc),
556
+ })
557
+ return {
558
+ "schemaVersion": "foggy-deepseek-profile-migration-status/v1",
559
+ "profileStore": str(destination),
560
+ "legacyStores": [str(path) for path in legacy_profile_stores(destination)],
561
+ "entries": entries,
562
+ "pendingCount": sum(item["status"] == "pending" for item in entries),
563
+ "conflictCount": sum(item["status"] in {"conflict", "invalid"} for item in entries),
564
+ }
565
+
566
+
567
+ def profile_migration_status_command(args: argparse.Namespace) -> dict:
568
+ install_root = normalized(args.install_root or default_install_root())
569
+ install_state = read_install_state(install_root)
570
+ data_root = normalized(args.data_root or install_state["dataRoot"])
571
+ return {"success": True, **profile_migration_inventory(data_root), "productionReady": False}
572
+
573
+
574
+ def profile_migrate_command(args: argparse.Namespace) -> dict:
575
+ if not args.approve:
576
+ raise OnboardingError("Profile migration requires --approve")
577
+ install_root = normalized(args.install_root or default_install_root())
578
+ install_state = read_install_state(install_root)
579
+ data_root = normalized(args.data_root or install_state["dataRoot"])
580
+ inventory = profile_migration_inventory(data_root)
581
+ if inventory["conflictCount"]:
582
+ raise OnboardingError("Legacy profile migration has conflicts or invalid entries; inspect status first")
583
+ destination = configure_profile_store(data_root)
584
+ migrated: list[dict] = []
585
+ for item in inventory["entries"]:
586
+ if item["status"] != "pending":
587
+ continue
588
+ source = normalized(Path(item["source"]) / f"{item['profileId']}.json")
589
+ payload = validate_opaque_profile_document(read_json_object(source, "Opaque profile"), source)
590
+ target = destination / source.name
591
+ atomic_json(target, payload)
592
+ if os.name != "nt":
593
+ target.chmod(0o600)
594
+ if read_json_object(target, "Migrated opaque profile") != payload:
595
+ target.unlink(missing_ok=True)
596
+ raise OnboardingError(f"Migrated profile verification failed: {source.name}")
597
+ backup_root = data_root / "profile-migration-backups" / dt.datetime.now().strftime("%Y%m%d-%H%M%S-%f")
598
+ backup_root.mkdir(parents=True, exist_ok=True)
599
+ if os.name != "nt":
600
+ backup_root.chmod(0o700)
601
+ archived = backup_root / (source.name + ".bak")
602
+ source.replace(archived)
603
+ if os.name != "nt":
604
+ archived.chmod(0o600)
605
+ migrated.append({
606
+ "profileId": payload["profileId"],
607
+ "revision": payload["revision"],
608
+ "destination": str(target),
609
+ "legacyBackup": str(archived),
610
+ })
611
+ return {
612
+ "success": True,
613
+ "schemaVersion": "foggy-deepseek-profile-migration/v1",
614
+ "profileStore": str(destination),
615
+ "migrated": migrated,
616
+ "migratedCount": len(migrated),
617
+ "productionReady": False,
618
+ }
619
+
620
+
621
+ def resolve_profile_name(data_root: Path, requested: str | None) -> str:
622
+ if requested:
623
+ return safe_profile(requested)
624
+ profiles_dir = data_root / "onboarding" / "profiles"
625
+ candidates = sorted(path.stem for path in profiles_dir.glob("*.json") if PROFILE_PATTERN.fullmatch(path.stem))
626
+ if len(candidates) == 1:
627
+ return candidates[0]
628
+ if not candidates:
629
+ return "default"
630
+ raise OnboardingError(
631
+ "Multiple onboarding profiles exist; pass --profile explicitly: " + ", ".join(candidates)
632
+ )
633
+
634
+
451
635
  def read_onboarding_state(data_root: Path, profile: str, required: bool = True) -> dict | None:
452
636
  path = onboarding_state_path(data_root, profile)
453
637
  if not path.is_file():
@@ -798,21 +982,26 @@ def install_command(args: argparse.Namespace) -> dict:
798
982
  data_root = normalized(args.data_root or default_data_root())
799
983
  cache_dirs = [normalized(item) for item in args.asset_cache_dir]
800
984
  components = versions["components"]
985
+ repair_component = getattr(args, "repair_component", None)
801
986
  assert_managed_root(install_root, "Install root")
802
987
  assert_managed_root(data_root, "Data root")
803
988
  if install_root == data_root:
804
989
  raise OnboardingError("Install root and data root must be different")
990
+ profile_store = normalized(os.environ.get("FOGGY_RUNTIME_PROFILE_STORE") or data_root / "cli-profiles")
805
991
  plan = {
806
992
  "schemaVersion": "foggy-deepseek-onboarding-plan/v1",
807
993
  "installRoot": str(install_root),
808
994
  "dataRoot": str(data_root),
995
+ "profileStore": str(profile_store),
809
996
  "workspaceMode": "dsh-session-cwd",
810
997
  "versions": {name: value.get("version") for name, value in components.items()},
998
+ "repairComponent": repair_component,
811
999
  "operations": ["install isolated CLI", "verify Launcher assets", "install global analysis Skill", "write install state"],
812
1000
  "productionReady": False,
813
1001
  }
814
1002
  if args.dry_run:
815
1003
  return {"success": True, "dryRun": True, "plan": plan}
1004
+ profile_store = configure_profile_store(data_root)
816
1005
  progress = ProgressReporter(
817
1006
  getattr(args, "progress_file", None),
818
1007
  getattr(args, "operation_id", None),
@@ -840,6 +1029,7 @@ def install_command(args: argparse.Namespace) -> dict:
840
1029
  progress=progress, progress_phase="cli", progress_step=1,
841
1030
  progress_index=index, progress_total=len(cli_assets),
842
1031
  progress_message="Downloading and verifying CLI",
1032
+ replace_corrupt=repair_component == "cli",
843
1033
  ))
844
1034
  progress.update(
845
1035
  "cli", 1, "Downloading and verifying CLI",
@@ -895,6 +1085,7 @@ def install_command(args: argparse.Namespace) -> dict:
895
1085
  progress=progress, progress_phase="launcher", progress_step=2,
896
1086
  progress_index=index, progress_total=len(launcher_assets),
897
1087
  progress_message="Downloading and verifying Launcher",
1088
+ replace_corrupt=repair_component == "launcher",
898
1089
  ))
899
1090
  progress.update(
900
1091
  "launcher", 2, "Downloading and verifying Launcher",
@@ -917,6 +1108,7 @@ def install_command(args: argparse.Namespace) -> dict:
917
1108
  progress=progress, progress_phase="analysis-skill", progress_step=3,
918
1109
  progress_index=index, progress_total=len(analysis_assets),
919
1110
  progress_message="Downloading and verifying analysis Skill",
1111
+ replace_corrupt=repair_component == "analysis-skill",
920
1112
  ))
921
1113
  progress.update(
922
1114
  "analysis-skill", 3, "Downloading and verifying analysis Skill",
@@ -927,7 +1119,7 @@ def install_command(args: argparse.Namespace) -> dict:
927
1119
  progress.update("analysis-skill", 3, "Installing analysis Skill", fraction=0.9, current_file=zip_asset["file"])
928
1120
  analysis_skill = install_analysis_skill(
929
1121
  downloads / "skill" / zip_asset["file"], install_root, components["analysisSkill"]["version"],
930
- zip_asset["sha256"], versions["packageVersion"], args.replace_skill,
1122
+ zip_asset["sha256"], versions["packageVersion"], args.replace_skill or repair_component == "analysis-skill",
931
1123
  )
932
1124
  progress.update("analysis-skill", 3, "Analysis Skill ready", fraction=1.0)
933
1125
  progress.update("workspace-skills", 4, "Registering native DSH Skills", fraction=0.1)
@@ -945,6 +1137,7 @@ def install_command(args: argparse.Namespace) -> dict:
945
1137
  "packageVersion": versions["packageVersion"],
946
1138
  "installRoot": str(install_root),
947
1139
  "dataRoot": str(data_root),
1140
+ "profileStore": str(profile_store),
948
1141
  "workspaceMode": "dsh-session-cwd",
949
1142
  "cli": {"version": cli_component["version"], "command": str(cli_command), "mode": cli_mode},
950
1143
  "launcher": {"version": components["launcher"]["version"], "path": str(launcher_dir)},
@@ -1057,8 +1250,61 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1057
1250
  existing = data_root / "runtime-state.json"
1058
1251
  if existing.is_file():
1059
1252
  prior = json.loads(existing.read_text(encoding="utf-8"))
1060
- if process_info(int(prior.get("pid", 0)))["running"]:
1061
- raise OnboardingError(f"Recorded Runtime is already running with PID {prior['pid']}")
1253
+ prior_info = process_info(int(prior.get("pid", 0)))
1254
+ if prior_info["running"]:
1255
+ expected_jar = f"foggy-runtime-launcher-{state['launcher']['version']}.jar"
1256
+ if prior_info.get("commandLine") and expected_jar not in prior_info["commandLine"]:
1257
+ raise OnboardingError(
1258
+ f"Recorded Runtime PID {prior['pid']} does not match the pinned Launcher"
1259
+ )
1260
+ cli = state["cli"]["command"]
1261
+ namespace = args.namespace or prior.get("namespace") or versions["defaults"]["namespace"]
1262
+ base_url = prior.get("runtimeUrl")
1263
+ if not isinstance(base_url, str) or not base_url:
1264
+ raise OnboardingError("Recorded Runtime does not contain runtimeUrl")
1265
+ wait_result = command_result(
1266
+ [cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "wait-ready",
1267
+ "--timeout-seconds", str(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]),
1268
+ "--interval-seconds", "1"],
1269
+ timeout=(args.timeout or versions["defaults"]["readinessTimeoutSeconds"]) + 30,
1270
+ )
1271
+ wait_payload = parse_json_output(wait_result, "wait-ready")
1272
+ if wait_payload.get("success") is not True:
1273
+ raise OnboardingError("wait-ready returned success=false for recorded Runtime")
1274
+ capabilities = parse_json_output(
1275
+ command_result(
1276
+ [cli, "--base-url", base_url, "--namespace", namespace, "--output", "json", "capabilities"],
1277
+ timeout=30,
1278
+ ),
1279
+ "capabilities",
1280
+ )
1281
+ expected_contract = versions["components"]["launcher"]["runtimeApiContract"]
1282
+ if capabilities.get("success") is not True or capabilities.get("runtimeApiVersion") != expected_contract:
1283
+ raise OnboardingError(f"Unexpected Runtime API contract; expected {expected_contract}")
1284
+ if capabilities.get("data", {}).get("securityMode") != versions["defaults"]["securityMode"]:
1285
+ raise OnboardingError("Recorded Runtime did not report the expected dev/test security mode")
1286
+ verified_at = now_utc()
1287
+ identity = {
1288
+ "engine": capabilities.get("engine"),
1289
+ "runtimeApiVersion": capabilities.get("runtimeApiVersion"),
1290
+ "schemaVersion": capabilities.get("data", {}).get("schemaVersion"),
1291
+ "securityMode": capabilities.get("data", {}).get("securityMode"),
1292
+ }
1293
+ return {
1294
+ "success": True,
1295
+ **prior,
1296
+ "identity": identity,
1297
+ "lastVerifiedAt": verified_at,
1298
+ "verification": {
1299
+ "waitReady": True,
1300
+ "capabilities": identity,
1301
+ "persisted": False,
1302
+ "instruction": "Capture this JSON in the current workspace when fresh verification evidence is required",
1303
+ },
1304
+ "action": "already-running-verified",
1305
+ "resumed": True,
1306
+ "productionReady": False,
1307
+ }
1062
1308
  existing.unlink()
1063
1309
  port = args.port or int(versions["defaults"]["port"])
1064
1310
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
@@ -1163,6 +1409,7 @@ def onboarding_context(args: argparse.Namespace, require_runtime: bool = False)
1163
1409
  install_state = read_install_state(install_root)
1164
1410
  data_root = normalized(args.data_root or install_state["dataRoot"])
1165
1411
  assert_managed_root(data_root, "Data root")
1412
+ configure_profile_store(data_root, create=False)
1166
1413
  runtime_state_path = data_root / "runtime-state.json"
1167
1414
  runtime_state = read_json_object(runtime_state_path, "Runtime state") if runtime_state_path.is_file() else None
1168
1415
  if runtime_state and runtime_state.get("schemaVersion") != RUNTIME_STATE_SCHEMA:
@@ -1222,6 +1469,28 @@ def require_opaque_profile_cli(install_state: dict) -> None:
1222
1469
  )
1223
1470
 
1224
1471
 
1472
+ def datasource_entries(payload: dict) -> list[dict]:
1473
+ for item in nested_data_objects(payload):
1474
+ entries = item.get("datasources")
1475
+ if isinstance(entries, list):
1476
+ return [entry for entry in entries if isinstance(entry, dict)]
1477
+ return []
1478
+
1479
+
1480
+ def matching_datasource(payload: dict, connection: dict) -> dict | None:
1481
+ expected_name = connection["name"]
1482
+ expected_type = connection["type"].lower()
1483
+ if expected_type == "postgresql":
1484
+ expected_type = "postgres"
1485
+ for entry in datasource_entries(payload):
1486
+ entry_type = str(entry.get("type", "")).lower()
1487
+ if entry_type == "postgresql":
1488
+ entry_type = "postgres"
1489
+ if entry.get("name") == expected_name and entry_type == expected_type:
1490
+ return entry
1491
+ return None
1492
+
1493
+
1225
1494
  def onboarding_plan_command(args: argparse.Namespace) -> dict:
1226
1495
  install_root, install_state, data_root, runtime_state = onboarding_context(args, require_runtime=False)
1227
1496
  profile = safe_profile(args.profile)
@@ -1283,7 +1552,9 @@ def onboarding_plan_command(args: argparse.Namespace) -> dict:
1283
1552
 
1284
1553
  def require_profile(args: argparse.Namespace, require_runtime: bool = False) -> tuple[dict, dict, Path, dict | None]:
1285
1554
  _install_root, install_state, data_root, runtime_state = onboarding_context(args, require_runtime=require_runtime)
1286
- state = read_onboarding_state(data_root, safe_profile(args.profile))
1555
+ profile = resolve_profile_name(data_root, getattr(args, "profile", None))
1556
+ args.profile = profile
1557
+ state = read_onboarding_state(data_root, profile)
1287
1558
  if normalized(state["installRoot"]) != normalized(install_state["installRoot"]):
1288
1559
  raise OnboardingError("Onboarding profile belongs to a different install root")
1289
1560
  return state, install_state, data_root, runtime_state
@@ -1329,10 +1600,31 @@ def datasource_configure_command(args: argparse.Namespace) -> dict:
1329
1600
  label = "datasources add"
1330
1601
  if args.replace:
1331
1602
  command.append("--replace")
1332
- result = redact_connection_material(
1333
- cli_json(install_state, runtime_state, connection["namespace"], command, label)
1334
- )
1335
- mark_step(state, "datasourceConfigured", "completed", replace=args.replace)
1603
+ already_present = False
1604
+ try:
1605
+ result = redact_connection_material(
1606
+ cli_json(install_state, runtime_state, connection["namespace"], command, label)
1607
+ )
1608
+ except OnboardingError as exc:
1609
+ if "DATASOURCE_ALREADY_EXISTS" not in str(exc):
1610
+ raise
1611
+ listed = cli_json(
1612
+ install_state, runtime_state, connection["namespace"], ["datasources", "list"], "datasources list"
1613
+ )
1614
+ existing = matching_datasource(listed, connection)
1615
+ if existing is None:
1616
+ raise OnboardingError(
1617
+ f"Datasource {connection['name']} already exists but its public type does not match the approved plan; "
1618
+ "do not replace it without explicit approval"
1619
+ ) from exc
1620
+ already_present = True
1621
+ result = {
1622
+ "success": True,
1623
+ "idempotent": True,
1624
+ "status": "already-present",
1625
+ "dataSource": redact_connection_material(existing),
1626
+ }
1627
+ mark_step(state, "datasourceConfigured", "completed", replace=args.replace, alreadyPresent=already_present)
1336
1628
  path = write_onboarding_state(data_root, state)
1337
1629
  return {
1338
1630
  "success": True,
@@ -1340,6 +1632,7 @@ def datasource_configure_command(args: argparse.Namespace) -> dict:
1340
1632
  "profile": state["profile"],
1341
1633
  "statePath": str(path),
1342
1634
  "dataSource": connection["name"],
1635
+ "alreadyPresent": already_present,
1343
1636
  "runtime": result,
1344
1637
  "next": "run datasource-verify; add --bind to approve namespace binding",
1345
1638
  "productionReady": False,
@@ -1756,7 +2049,9 @@ def semantic_verify_command(args: argparse.Namespace) -> dict:
1756
2049
  raise OnboardingError("--query-model must be declared in the registered semantic plan")
1757
2050
  if not args.query_payload:
1758
2051
  raise OnboardingError("--query-payload is required for semantic verification")
1759
- project_root = normalized(state["projectRoot"])
2052
+ project_root = normalized(getattr(args, "project_root", None) or state["projectRoot"])
2053
+ if not project_root_is_bound(state, project_root):
2054
+ raise OnboardingError("Query projectRoot is not bound to this onboarding profile")
1760
2055
  payload_path = normalized(args.query_payload)
1761
2056
  if not is_child(payload_path, project_root):
1762
2057
  raise OnboardingError("Query payload must stay inside projectRoot")
@@ -1798,7 +2093,29 @@ def semantic_verify_command(args: argparse.Namespace) -> dict:
1798
2093
  )
1799
2094
  atomic_json(evidence_dir / "query-execute.json", executed)
1800
2095
  row_count = response_row_count(executed)
1801
- mark_step(state, "semanticVerified", "completed", queryModel=query_model, queryValidated=True, queryExecuted=True)
2096
+ mark_step(
2097
+ state,
2098
+ "semanticVerified",
2099
+ "completed",
2100
+ queryModel=query_model,
2101
+ queryValidated=True,
2102
+ queryExecuted=True,
2103
+ rowCount=row_count,
2104
+ queryPayloadDigest=sha256(payload_path),
2105
+ projectRoot=str(project_root),
2106
+ )
2107
+ workspace_verifications = state.setdefault("workspaceVerifications", [])
2108
+ workspace_verifications[:] = [
2109
+ item for item in workspace_verifications
2110
+ if not (item.get("projectRoot") == str(project_root) and item.get("queryModel") == query_model)
2111
+ ]
2112
+ workspace_verifications.append({
2113
+ "projectRoot": str(project_root),
2114
+ "queryModel": query_model,
2115
+ "queryPayloadDigest": sha256(payload_path),
2116
+ "rowCount": row_count,
2117
+ "verifiedAt": now_utc(),
2118
+ })
1802
2119
  state.setdefault("artifacts", {})["semanticVerifyEvidence"] = str(evidence_dir)
1803
2120
  path = write_onboarding_state(data_root, state)
1804
2121
  return {
@@ -1818,14 +2135,15 @@ def semantic_verify_command(args: argparse.Namespace) -> dict:
1818
2135
 
1819
2136
 
1820
2137
  def next_onboarding_action(state: dict) -> dict:
2138
+ profile_flag = f" --profile {state['profile']}"
1821
2139
  ordered = [
1822
- ("datasourceConfigured", "datasource-configure --apply"),
1823
- ("datasourceVerified", "datasource-verify --bind"),
1824
- ("schemaDiscovered", "schema-discover"),
1825
- ("semanticDrafted", "semantic-draft --semantic-plan <json>"),
1826
- ("semanticValidated", "semantic-validate --apply"),
1827
- ("semanticPublished", "semantic-publish --apply"),
1828
- ("semanticVerified", "semantic-verify --query-payload <json> --execute"),
2140
+ ("datasourceConfigured", f"datasource-configure{profile_flag} --apply"),
2141
+ ("datasourceVerified", f"datasource-verify{profile_flag} --bind"),
2142
+ ("schemaDiscovered", f"schema-discover{profile_flag}"),
2143
+ ("semanticDrafted", f"semantic-draft{profile_flag} --semantic-plan <json>"),
2144
+ ("semanticValidated", f"semantic-validate{profile_flag} --apply"),
2145
+ ("semanticPublished", f"semantic-publish{profile_flag} --apply"),
2146
+ ("semanticVerified", f"semantic-verify{profile_flag} --query-payload <json> --execute"),
1829
2147
  ]
1830
2148
  for name, command in ordered:
1831
2149
  current = state.get("steps", {}).get(name, {})
@@ -1838,7 +2156,7 @@ def next_onboarding_action(state: dict) -> dict:
1838
2156
  "instruction": "Inspect semantic publish evidence and repair refresh before any republish attempt.",
1839
2157
  }
1840
2158
  if name == "semanticValidated" and current.get("status") == "failed":
1841
- command = "repair TM/QM, then semantic-draft --semantic-plan <json>"
2159
+ command = f"repair TM/QM, then semantic-draft{profile_flag} --semantic-plan <json>"
1842
2160
  return {"step": name, "command": command, "status": current.get("status", "pending")}
1843
2161
  return {"step": None, "command": None, "status": "completed"}
1844
2162
 
@@ -1856,12 +2174,63 @@ def onboarding_status_command(args: argparse.Namespace) -> dict:
1856
2174
  "passwordEnv": password_env,
1857
2175
  "passwordEnvPresent": bool(password_env and os.environ.get(password_env)),
1858
2176
  "steps": state["steps"],
2177
+ "projectRoot": state.get("projectRoot"),
2178
+ "workspaceBindings": [str(path) for path in bound_project_roots(state)],
2179
+ "workspaceVerifications": state.get("workspaceVerifications", []),
1859
2180
  "artifacts": state.get("artifacts", {}),
1860
2181
  "next": next_onboarding_action(state),
1861
2182
  "productionReady": False,
1862
2183
  }
1863
2184
 
1864
2185
 
2186
+ def onboarding_list_command(args: argparse.Namespace) -> dict:
2187
+ install_root = normalized(args.install_root or default_install_root())
2188
+ install_state = read_install_state(install_root)
2189
+ data_root = normalized(args.data_root or install_state["dataRoot"])
2190
+ profiles_dir = data_root / "onboarding" / "profiles"
2191
+ profiles: list[dict] = []
2192
+ ordered_steps = [
2193
+ "planned", "datasourceConfigured", "datasourceVerified", "schemaDiscovered",
2194
+ "semanticDrafted", "semanticValidated", "semanticPublished", "semanticVerified",
2195
+ ]
2196
+ for path in sorted(profiles_dir.glob("*.json")):
2197
+ if not PROFILE_PATTERN.fullmatch(path.stem):
2198
+ continue
2199
+ try:
2200
+ state = read_onboarding_state(data_root, path.stem)
2201
+ steps = state.get("steps", {})
2202
+ completed = sum(steps.get(name, {}).get("status") == "completed" for name in ordered_steps)
2203
+ profiles.append({
2204
+ "profile": state["profile"],
2205
+ "projectRoot": state.get("projectRoot"),
2206
+ "workspaceBindings": [str(item) for item in bound_project_roots(state)],
2207
+ "updatedAt": state.get("updatedAt"),
2208
+ "completedSteps": completed,
2209
+ "totalSteps": len(ordered_steps),
2210
+ "steps": {name: steps.get(name, {"status": "pending"}) for name in ordered_steps},
2211
+ "next": next_onboarding_action(state),
2212
+ })
2213
+ except OnboardingError as exc:
2214
+ profiles.append({
2215
+ "profile": path.stem,
2216
+ "projectRoot": None,
2217
+ "updatedAt": None,
2218
+ "completedSteps": 0,
2219
+ "totalSteps": len(ordered_steps),
2220
+ "steps": {},
2221
+ "next": {"status": "invalid"},
2222
+ "error": str(exc),
2223
+ })
2224
+ profiles.sort(key=lambda item: item.get("updatedAt") or "", reverse=True)
2225
+ return {
2226
+ "success": True,
2227
+ "schemaVersion": "foggy-deepseek-onboarding-list/v1",
2228
+ "profiles": profiles,
2229
+ "profileCount": len(profiles),
2230
+ "productionReady": False,
2231
+ }
2232
+
2233
+
1865
2234
  def onboarding_resume_command(args: argparse.Namespace) -> dict:
1866
2235
  result = onboarding_status_command(args)
1867
2236
  result["schemaVersion"] = "foggy-deepseek-onboarding-resume/v1"
@@ -1883,6 +2252,52 @@ def save_composite_result(evidence_dir: Path, name: str, payload: dict, files: l
1883
2252
  files.append(str(path))
1884
2253
 
1885
2254
 
2255
+ def resumed_phase(profile: str, phase: str, **values: object) -> dict:
2256
+ return {
2257
+ "success": True,
2258
+ "schemaVersion": "foggy-deepseek-onboarding-resumed/v1",
2259
+ "profile": profile,
2260
+ "phase": phase,
2261
+ "resumed": True,
2262
+ **values,
2263
+ }
2264
+
2265
+
2266
+ def step_completed(state: dict, name: str) -> bool:
2267
+ return state.get("steps", {}).get(name, {}).get("status") == "completed"
2268
+
2269
+
2270
+ def bound_project_roots(state: dict) -> list[Path]:
2271
+ values = [state.get("projectRoot"), *state.get("workspaceBindings", [])]
2272
+ result: list[Path] = []
2273
+ for value in values:
2274
+ if not isinstance(value, str) or not value:
2275
+ continue
2276
+ path = normalized(value)
2277
+ if path not in result:
2278
+ result.append(path)
2279
+ return result
2280
+
2281
+
2282
+ def project_root_is_bound(state: dict, project_root: Path) -> bool:
2283
+ return normalized(project_root) in bound_project_roots(state)
2284
+
2285
+
2286
+ def bind_completed_workspace(state: dict, data_root: Path, project_root: Path) -> bool:
2287
+ project_root = normalized(project_root)
2288
+ if project_root_is_bound(state, project_root):
2289
+ return False
2290
+ required = ("datasourceConfigured", "datasourceVerified", "schemaDiscovered", "semanticPublished")
2291
+ if not all(step_completed(state, name) for name in required):
2292
+ raise OnboardingError(
2293
+ "Existing onboarding profile belongs to a different projectRoot and is not complete enough for safe reuse; "
2294
+ "resume from its original DSH workspace or choose a new profile name"
2295
+ )
2296
+ state.setdefault("workspaceBindings", []).append(str(project_root))
2297
+ write_onboarding_state(data_root, state)
2298
+ return True
2299
+
2300
+
1886
2301
  def datasource_run_command(args: argparse.Namespace) -> dict:
1887
2302
  _install_root, install_state, data_root, _runtime_state = onboarding_context(args, require_runtime=True)
1888
2303
  project_root = normalized(args.project_root or Path.cwd())
@@ -1902,11 +2317,14 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
1902
2317
  if existing:
1903
2318
  if existing.get("connection") != requested_connection:
1904
2319
  raise OnboardingError("Existing onboarding profile does not match the requested connection plan")
2320
+ adopted = bind_completed_workspace(existing, data_root, project_root)
1905
2321
  plan_result = {
1906
2322
  "success": True,
1907
2323
  "schemaVersion": "foggy-deepseek-onboarding-plan-result/v1",
1908
2324
  "profile": profile,
1909
2325
  "resumed": True,
2326
+ "workspaceAdopted": adopted,
2327
+ "projectRoot": str(project_root),
1910
2328
  "statePath": str(onboarding_state_path(data_root, profile)),
1911
2329
  "next": next_onboarding_action(existing),
1912
2330
  "productionReady": False,
@@ -1921,59 +2339,81 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
1921
2339
  replace_plan=False,
1922
2340
  ))
1923
2341
  save_composite_result(evidence_dir, "01-plan.json", plan_result, files)
2342
+ state = read_onboarding_state(data_root, profile)
1924
2343
 
1925
- configure_dry = datasource_configure_command(argparse.Namespace(
1926
- install_root=args.install_root, data_root=args.data_root, profile=profile, apply=False, replace=False,
1927
- ))
1928
- save_composite_result(evidence_dir, "02-datasource-dry.json", configure_dry, files)
1929
- if not args.approve_configure:
1930
- return {
1931
- "success": True,
1932
- "schemaVersion": "foggy-deepseek-datasource-run/v1",
1933
- "profile": profile,
1934
- "phaseStatus": "awaiting-configure-approval",
1935
- "evidenceDir": str(evidence_dir),
1936
- "evidenceFiles": files,
1937
- "next": "rerun with --approve-configure after the datasource mutation is approved",
1938
- "productionReady": False,
1939
- }
1940
-
1941
- configured = datasource_configure_command(argparse.Namespace(
1942
- install_root=args.install_root, data_root=args.data_root, profile=profile, apply=True, replace=False,
1943
- ))
1944
- save_composite_result(evidence_dir, "03-datasource-apply.json", configured, files)
1945
- tested = datasource_verify_command(argparse.Namespace(
1946
- install_root=args.install_root, data_root=args.data_root, profile=profile, bind=False,
1947
- ))
1948
- save_composite_result(evidence_dir, "04-datasource-test.json", tested, files)
1949
- if not args.approve_bind:
1950
- return {
1951
- "success": True,
1952
- "schemaVersion": "foggy-deepseek-datasource-run/v1",
1953
- "profile": profile,
1954
- "phaseStatus": "awaiting-bind-approval",
1955
- "evidenceDir": str(evidence_dir),
1956
- "evidenceFiles": files,
1957
- "next": "rerun with --approve-configure --approve-bind after namespace binding is approved",
1958
- "productionReady": False,
1959
- }
2344
+ if step_completed(state, "datasourceConfigured"):
2345
+ configured = resumed_phase(profile, "datasourceConfigured")
2346
+ save_composite_result(evidence_dir, "02-datasource-dry.json", configured, files)
2347
+ save_composite_result(evidence_dir, "03-datasource-apply.json", configured, files)
2348
+ else:
2349
+ configure_dry = datasource_configure_command(argparse.Namespace(
2350
+ install_root=args.install_root, data_root=args.data_root, profile=profile, apply=False, replace=False,
2351
+ ))
2352
+ save_composite_result(evidence_dir, "02-datasource-dry.json", configure_dry, files)
2353
+ if not args.approve_configure:
2354
+ return {
2355
+ "success": True,
2356
+ "schemaVersion": "foggy-deepseek-datasource-run/v1",
2357
+ "profile": profile,
2358
+ "phaseStatus": "awaiting-configure-approval",
2359
+ "evidenceDir": str(evidence_dir),
2360
+ "evidenceFiles": files,
2361
+ "next": "rerun with --approve-configure after the datasource mutation is approved",
2362
+ "productionReady": False,
2363
+ }
2364
+ configured = datasource_configure_command(argparse.Namespace(
2365
+ install_root=args.install_root, data_root=args.data_root, profile=profile, apply=True, replace=False,
2366
+ ))
2367
+ save_composite_result(evidence_dir, "03-datasource-apply.json", configured, files)
2368
+ state = read_onboarding_state(data_root, profile)
1960
2369
 
1961
- bound = datasource_verify_command(argparse.Namespace(
1962
- install_root=args.install_root, data_root=args.data_root, profile=profile, bind=True,
1963
- ))
1964
- save_composite_result(evidence_dir, "05-datasource-bind.json", bound, files)
1965
- discovered = schema_discover_command(argparse.Namespace(
1966
- install_root=args.install_root,
1967
- data_root=args.data_root,
1968
- profile=profile,
1969
- schema=args.schema,
1970
- pattern=args.pattern,
1971
- table=args.table,
1972
- max_tables=args.max_tables,
1973
- list_only=False,
1974
- no_views=args.no_views,
1975
- include_indexes=args.include_indexes,
1976
- ))
2370
+ if step_completed(state, "datasourceVerified"):
2371
+ bound = resumed_phase(profile, "datasourceVerified")
2372
+ save_composite_result(evidence_dir, "04-datasource-test.json", bound, files)
2373
+ save_composite_result(evidence_dir, "05-datasource-bind.json", bound, files)
2374
+ else:
2375
+ tested = datasource_verify_command(argparse.Namespace(
2376
+ install_root=args.install_root, data_root=args.data_root, profile=profile, bind=False,
2377
+ ))
2378
+ save_composite_result(evidence_dir, "04-datasource-test.json", tested, files)
2379
+ if not args.approve_bind:
2380
+ return {
2381
+ "success": True,
2382
+ "schemaVersion": "foggy-deepseek-datasource-run/v1",
2383
+ "profile": profile,
2384
+ "phaseStatus": "awaiting-bind-approval",
2385
+ "evidenceDir": str(evidence_dir),
2386
+ "evidenceFiles": files,
2387
+ "next": "rerun with --approve-configure --approve-bind after namespace binding is approved",
2388
+ "productionReady": False,
2389
+ }
2390
+ bound = datasource_verify_command(argparse.Namespace(
2391
+ install_root=args.install_root, data_root=args.data_root, profile=profile, bind=True,
2392
+ ))
2393
+ save_composite_result(evidence_dir, "05-datasource-bind.json", bound, files)
2394
+ state = read_onboarding_state(data_root, profile)
2395
+
2396
+ if step_completed(state, "schemaDiscovered"):
2397
+ schema_step = state["steps"]["schemaDiscovered"]
2398
+ discovered = resumed_phase(
2399
+ profile,
2400
+ "schemaDiscovered",
2401
+ selectedCount=schema_step.get("selectedCount", 0),
2402
+ artifactPath=state.get("artifacts", {}).get("schemaDiscovery"),
2403
+ )
2404
+ else:
2405
+ discovered = schema_discover_command(argparse.Namespace(
2406
+ install_root=args.install_root,
2407
+ data_root=args.data_root,
2408
+ profile=profile,
2409
+ schema=args.schema,
2410
+ pattern=args.pattern,
2411
+ table=args.table,
2412
+ max_tables=args.max_tables,
2413
+ list_only=False,
2414
+ no_views=args.no_views,
2415
+ include_indexes=args.include_indexes,
2416
+ ))
1977
2417
  save_composite_result(evidence_dir, "06-schema.json", discovered, files)
1978
2418
  return {
1979
2419
  "success": True,
@@ -1989,6 +2429,45 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
1989
2429
  }
1990
2430
 
1991
2431
 
2432
+ def semantic_plan_snapshot(state: dict, plan: dict, project_root: Path | None = None) -> tuple[Path, dict, bool]:
2433
+ project_root = normalized(project_root or state["projectRoot"])
2434
+ draft_dir = normalized(project_root / plan["draftDir"])
2435
+ if not is_child(draft_dir, project_root) or draft_dir == project_root:
2436
+ raise OnboardingError("draftDir must stay inside projectRoot and cannot equal it")
2437
+ manifest = semantic_manifest(draft_dir)
2438
+ semantic = state.get("semantic", {})
2439
+ registered = semantic.get("draftManifest") or {}
2440
+ matches = bool(
2441
+ step_completed(state, "semanticDrafted")
2442
+ and normalized(semantic.get("draftDir", draft_dir)) == draft_dir
2443
+ and semantic.get("bundleName") == plan["bundleName"]
2444
+ and semantic.get("queryModels") == plan["queryModels"]
2445
+ and registered.get("digest") == manifest["digest"]
2446
+ )
2447
+ return draft_dir, manifest, matches
2448
+
2449
+
2450
+ def published_digest_matches(state: dict, digest: str) -> bool:
2451
+ published = state.get("steps", {}).get("semanticPublished", {})
2452
+ return published.get("status") == "completed" and published.get("digest") == digest
2453
+
2454
+
2455
+ def workspace_query_verification(state: dict, project_root: Path, query_model: str, digest: str) -> dict | None:
2456
+ requested_root = str(normalized(project_root))
2457
+ candidates = list(state.get("workspaceVerifications", []))
2458
+ legacy = state.get("steps", {}).get("semanticVerified", {})
2459
+ if legacy.get("status") == "completed" and legacy.get("projectRoot"):
2460
+ candidates.append(legacy)
2461
+ for item in candidates:
2462
+ if (
2463
+ item.get("projectRoot") == requested_root
2464
+ and item.get("queryModel") == query_model
2465
+ and item.get("queryPayloadDigest") == digest
2466
+ ):
2467
+ return item
2468
+ return None
2469
+
2470
+
1992
2471
  def semantic_run_command(args: argparse.Namespace) -> dict:
1993
2472
  approved_plan = validate_semantic_plan(read_json_object(normalized(args.semantic_plan), "Semantic plan"))
1994
2473
  if not approved_plan.get("profile"):
@@ -1999,7 +2478,22 @@ def semantic_run_command(args: argparse.Namespace) -> dict:
1999
2478
  profile_args = argparse.Namespace(**vars(args))
2000
2479
  profile_args.profile = profile
2001
2480
  state, _install_state, _data_root, _runtime_state = require_profile(profile_args, require_runtime=True)
2002
- project_root = normalized(state["projectRoot"])
2481
+ project_root = normalized(getattr(args, "project_root", None) or state["projectRoot"])
2482
+ if not project_root_is_bound(state, project_root):
2483
+ raise OnboardingError(
2484
+ "Current projectRoot is not bound to this completed profile; run onboard-datasource-run from this workspace first"
2485
+ )
2486
+ payload_path = normalized(args.query_payload)
2487
+ if not is_child(payload_path, project_root):
2488
+ raise OnboardingError(
2489
+ "Query payload must stay inside projectRoot; place contracts under .foggy/onboarding-contracts before approval"
2490
+ )
2491
+ bounded_query_payload(payload_path)
2492
+ declared_query_model = args.query_model or (approved_plan["queryModels"][0] if len(approved_plan["queryModels"]) == 1 else None)
2493
+ if not declared_query_model:
2494
+ raise OnboardingError("--query-model is required when the semantic plan declares multiple query models")
2495
+ if declared_query_model not in approved_plan["queryModels"]:
2496
+ raise OnboardingError("--query-model must be declared in semanticPlan.queryModels")
2003
2497
  contract_evidence = approved_plan.get("evidenceDir")
2004
2498
  if contract_evidence and args.evidence_dir:
2005
2499
  if normalized(project_root / contract_evidence) != normalized(args.evidence_dir):
@@ -2007,79 +2501,144 @@ def semantic_run_command(args: argparse.Namespace) -> dict:
2007
2501
  evidence_dir = composite_evidence_dir(project_root, profile, str(project_root / contract_evidence) if contract_evidence else args.evidence_dir)
2008
2502
  files: list[str] = []
2009
2503
 
2010
- drafted = semantic_draft_command(argparse.Namespace(
2011
- install_root=args.install_root,
2012
- data_root=args.data_root,
2013
- profile=profile,
2014
- semantic_plan=args.semantic_plan,
2015
- ))
2504
+ _draft_dir, manifest, draft_matches = semantic_plan_snapshot(state, approved_plan, project_root)
2505
+ published_matches = published_digest_matches(state, manifest["digest"])
2506
+ if normalized(state["projectRoot"]) != project_root and not published_matches:
2507
+ raise OnboardingError(
2508
+ "A secondary workspace may reuse an identical published semantic layer but cannot replace it; "
2509
+ "publish changes from the original projectRoot or choose a new profile"
2510
+ )
2511
+ if draft_matches or published_matches:
2512
+ drafted = resumed_phase(profile, "semanticDrafted", digest=manifest["digest"])
2513
+ else:
2514
+ drafted = semantic_draft_command(argparse.Namespace(
2515
+ install_root=args.install_root,
2516
+ data_root=args.data_root,
2517
+ profile=profile,
2518
+ semantic_plan=args.semantic_plan,
2519
+ ))
2520
+ state = read_onboarding_state(normalized(state["dataRoot"]), profile)
2521
+ manifest = state["semantic"]["draftManifest"]
2016
2522
  save_composite_result(evidence_dir, "07-semantic-draft.json", drafted, files)
2017
- validate_dry = semantic_validate_command(argparse.Namespace(
2018
- install_root=args.install_root,
2019
- data_root=args.data_root,
2020
- profile=profile,
2021
- apply=False,
2022
- include_stack_trace=False,
2023
- ))
2024
- save_composite_result(evidence_dir, "08-semantic-validate-dry.json", validate_dry, files)
2025
- if not args.approve_validate:
2026
- return {
2027
- "success": True,
2028
- "schemaVersion": "foggy-deepseek-semantic-run/v1",
2029
- "profile": profile,
2030
- "phaseStatus": "awaiting-validate-approval",
2031
- "evidenceDir": str(evidence_dir),
2032
- "evidenceFiles": files,
2033
- "next": "rerun with --approve-validate after the validation catalog mutation is approved",
2034
- "productionReady": False,
2035
- }
2036
-
2037
- validated = semantic_validate_command(argparse.Namespace(
2038
- install_root=args.install_root,
2039
- data_root=args.data_root,
2040
- profile=profile,
2041
- apply=True,
2042
- include_stack_trace=False,
2043
- ))
2523
+ state = read_onboarding_state(normalized(state["dataRoot"]), profile)
2524
+ validation_step = state.get("steps", {}).get("semanticValidated", {})
2525
+ published_matches = published_digest_matches(state, manifest["digest"])
2526
+ validation_matches = validation_step.get("status") == "completed" and validation_step.get("digest") == manifest["digest"]
2527
+ if validation_matches or published_matches:
2528
+ if published_matches and not validation_matches:
2529
+ mark_step(state, "semanticValidated", "completed", digest=manifest["digest"], resumedFromPublished=True)
2530
+ write_onboarding_state(normalized(state["dataRoot"]), state)
2531
+ validated = resumed_phase(profile, "semanticValidated", digest=manifest["digest"])
2532
+ save_composite_result(evidence_dir, "08-semantic-validate-dry.json", validated, files)
2533
+ else:
2534
+ validate_dry = semantic_validate_command(argparse.Namespace(
2535
+ install_root=args.install_root,
2536
+ data_root=args.data_root,
2537
+ profile=profile,
2538
+ apply=False,
2539
+ include_stack_trace=False,
2540
+ ))
2541
+ save_composite_result(evidence_dir, "08-semantic-validate-dry.json", validate_dry, files)
2542
+ if not args.approve_validate:
2543
+ return {
2544
+ "success": True,
2545
+ "schemaVersion": "foggy-deepseek-semantic-run/v1",
2546
+ "profile": profile,
2547
+ "phaseStatus": "awaiting-validate-approval",
2548
+ "evidenceDir": str(evidence_dir),
2549
+ "evidenceFiles": files,
2550
+ "next": "rerun with --approve-validate after the validation catalog mutation is approved",
2551
+ "productionReady": False,
2552
+ }
2553
+ validated = semantic_validate_command(argparse.Namespace(
2554
+ install_root=args.install_root,
2555
+ data_root=args.data_root,
2556
+ profile=profile,
2557
+ apply=True,
2558
+ include_stack_trace=False,
2559
+ ))
2044
2560
  save_composite_result(evidence_dir, "09-semantic-validate-apply.json", validated, files)
2045
- publish_dry = semantic_publish_command(argparse.Namespace(
2046
- install_root=args.install_root,
2047
- data_root=args.data_root,
2048
- profile=profile,
2049
- apply=False,
2050
- replace_bundle=False,
2051
- watch=False,
2052
- prune=False,
2053
- ))
2054
- save_composite_result(evidence_dir, "10-semantic-publish-dry.json", publish_dry, files)
2055
- if not args.approve_publish:
2561
+ state = read_onboarding_state(normalized(state["dataRoot"]), profile)
2562
+ if published_digest_matches(state, manifest["digest"]):
2563
+ published = resumed_phase(
2564
+ profile,
2565
+ "semanticPublished",
2566
+ digest=manifest["digest"],
2567
+ bundleName=state["semantic"]["bundleName"],
2568
+ )
2569
+ save_composite_result(evidence_dir, "10-semantic-publish-dry.json", published, files)
2570
+ else:
2571
+ publish_dry = semantic_publish_command(argparse.Namespace(
2572
+ install_root=args.install_root,
2573
+ data_root=args.data_root,
2574
+ profile=profile,
2575
+ apply=False,
2576
+ replace_bundle=False,
2577
+ watch=False,
2578
+ prune=False,
2579
+ ))
2580
+ save_composite_result(evidence_dir, "10-semantic-publish-dry.json", publish_dry, files)
2581
+ if not args.approve_publish:
2582
+ return {
2583
+ "success": True,
2584
+ "schemaVersion": "foggy-deepseek-semantic-run/v1",
2585
+ "profile": profile,
2586
+ "phaseStatus": "awaiting-publish-approval",
2587
+ "evidenceDir": str(evidence_dir),
2588
+ "evidenceFiles": files,
2589
+ "next": "rerun with --approve-validate --approve-publish after publication is approved",
2590
+ "productionReady": False,
2591
+ }
2592
+ published = semantic_publish_command(argparse.Namespace(
2593
+ install_root=args.install_root,
2594
+ data_root=args.data_root,
2595
+ profile=profile,
2596
+ apply=True,
2597
+ replace_bundle=False,
2598
+ watch=False,
2599
+ prune=False,
2600
+ ))
2601
+ save_composite_result(evidence_dir, "11-semantic-publish-apply.json", published, files)
2602
+ state = read_onboarding_state(normalized(state["dataRoot"]), profile)
2603
+ payload_digest = sha256(payload_path)
2604
+ verified_step = workspace_query_verification(state, project_root, declared_query_model, payload_digest)
2605
+ if verified_step:
2606
+ resumed = resumed_phase(
2607
+ profile,
2608
+ "semanticVerified",
2609
+ queryModel=declared_query_model,
2610
+ queryValidated=True,
2611
+ queryExecuted=True,
2612
+ rowCount=verified_step.get("rowCount"),
2613
+ queryPayloadDigest=payload_digest,
2614
+ )
2615
+ save_composite_result(evidence_dir, "12-query-validate.json", resumed, files)
2616
+ save_composite_result(evidence_dir, "13-query-execute.json", resumed, files)
2617
+ status = onboarding_status_command(argparse.Namespace(
2618
+ install_root=args.install_root, data_root=args.data_root, profile=profile,
2619
+ ))
2620
+ save_composite_result(evidence_dir, "14-status.json", status, files)
2056
2621
  return {
2057
2622
  "success": True,
2058
2623
  "schemaVersion": "foggy-deepseek-semantic-run/v1",
2059
2624
  "profile": profile,
2060
- "phaseStatus": "awaiting-publish-approval",
2625
+ "phaseStatus": "completed",
2626
+ "resumed": True,
2627
+ "queryModel": declared_query_model,
2628
+ "queryValidated": True,
2629
+ "queryExecuted": True,
2630
+ "rowCount": verified_step.get("rowCount"),
2061
2631
  "evidenceDir": str(evidence_dir),
2062
2632
  "evidenceFiles": files,
2063
- "next": "rerun with --approve-validate --approve-publish after publication is approved",
2064
2633
  "productionReady": False,
2065
2634
  }
2066
-
2067
- published = semantic_publish_command(argparse.Namespace(
2068
- install_root=args.install_root,
2069
- data_root=args.data_root,
2070
- profile=profile,
2071
- apply=True,
2072
- replace_bundle=False,
2073
- watch=False,
2074
- prune=False,
2075
- ))
2076
- save_composite_result(evidence_dir, "11-semantic-publish-apply.json", published, files)
2077
2635
  query_validated = semantic_verify_command(argparse.Namespace(
2078
2636
  install_root=args.install_root,
2079
2637
  data_root=args.data_root,
2080
2638
  profile=profile,
2081
- query_model=args.query_model,
2639
+ query_model=declared_query_model,
2082
2640
  query_payload=args.query_payload,
2641
+ project_root=str(project_root),
2083
2642
  execute=False,
2084
2643
  ))
2085
2644
  save_composite_result(evidence_dir, "12-query-validate.json", query_validated, files)
@@ -2101,8 +2660,9 @@ def semantic_run_command(args: argparse.Namespace) -> dict:
2101
2660
  install_root=args.install_root,
2102
2661
  data_root=args.data_root,
2103
2662
  profile=profile,
2104
- query_model=args.query_model,
2663
+ query_model=declared_query_model,
2105
2664
  query_payload=args.query_payload,
2665
+ project_root=str(project_root),
2106
2666
  execute=True,
2107
2667
  ))
2108
2668
  save_composite_result(evidence_dir, "13-query-execute.json", executed, files)
@@ -2237,6 +2797,7 @@ def build_parser() -> argparse.ArgumentParser:
2237
2797
  install.add_argument("--project-root")
2238
2798
  install.add_argument("--asset-cache-dir", action="append", default=[])
2239
2799
  install.add_argument("--replace-skill", action="store_true")
2800
+ install.add_argument("--repair-component", choices=("cli", "launcher", "analysis-skill"))
2240
2801
  install.add_argument("--skip-cli-install", action="store_true")
2241
2802
  install.add_argument("--cli-command")
2242
2803
  install.add_argument("--progress-file")
@@ -2280,15 +2841,31 @@ def build_parser() -> argparse.ArgumentParser:
2280
2841
  status = sub.add_parser("onboard-status")
2281
2842
  status.add_argument("--install-root")
2282
2843
  status.add_argument("--data-root")
2283
- status.add_argument("--profile", default="default")
2844
+ status.add_argument("--profile")
2284
2845
  status.set_defaults(handler=onboarding_status_command)
2285
2846
 
2286
2847
  resume = sub.add_parser("onboard-resume")
2287
2848
  resume.add_argument("--install-root")
2288
2849
  resume.add_argument("--data-root")
2289
- resume.add_argument("--profile", default="default")
2850
+ resume.add_argument("--profile")
2290
2851
  resume.set_defaults(handler=onboarding_resume_command)
2291
2852
 
2853
+ onboarding_list = sub.add_parser("onboard-list")
2854
+ onboarding_list.add_argument("--install-root")
2855
+ onboarding_list.add_argument("--data-root")
2856
+ onboarding_list.set_defaults(handler=onboarding_list_command)
2857
+
2858
+ migration_status = sub.add_parser("profile-migration-status")
2859
+ migration_status.add_argument("--install-root")
2860
+ migration_status.add_argument("--data-root")
2861
+ migration_status.set_defaults(handler=profile_migration_status_command)
2862
+
2863
+ migrate = sub.add_parser("profile-migrate")
2864
+ migrate.add_argument("--install-root")
2865
+ migrate.add_argument("--data-root")
2866
+ migrate.add_argument("--approve", action="store_true")
2867
+ migrate.set_defaults(handler=profile_migrate_command)
2868
+
2292
2869
  datasource_run = sub.add_parser("onboard-datasource-run")
2293
2870
  datasource_run.add_argument("--install-root")
2294
2871
  datasource_run.add_argument("--data-root")
@@ -2309,6 +2886,7 @@ def build_parser() -> argparse.ArgumentParser:
2309
2886
  semantic_run = sub.add_parser("onboard-semantic-run")
2310
2887
  semantic_run.add_argument("--install-root")
2311
2888
  semantic_run.add_argument("--data-root")
2889
+ semantic_run.add_argument("--project-root")
2312
2890
  semantic_run.add_argument("--profile")
2313
2891
  semantic_run.add_argument("--semantic-plan", required=True)
2314
2892
  semantic_run.add_argument("--query-payload", required=True)
@@ -2322,7 +2900,7 @@ def build_parser() -> argparse.ArgumentParser:
2322
2900
  datasource_configure = sub.add_parser("datasource-configure")
2323
2901
  datasource_configure.add_argument("--install-root")
2324
2902
  datasource_configure.add_argument("--data-root")
2325
- datasource_configure.add_argument("--profile", default="default")
2903
+ datasource_configure.add_argument("--profile")
2326
2904
  datasource_configure.add_argument("--apply", action="store_true")
2327
2905
  datasource_configure.add_argument("--replace", action="store_true")
2328
2906
  datasource_configure.set_defaults(handler=datasource_configure_command)
@@ -2330,14 +2908,14 @@ def build_parser() -> argparse.ArgumentParser:
2330
2908
  datasource_verify = sub.add_parser("datasource-verify")
2331
2909
  datasource_verify.add_argument("--install-root")
2332
2910
  datasource_verify.add_argument("--data-root")
2333
- datasource_verify.add_argument("--profile", default="default")
2911
+ datasource_verify.add_argument("--profile")
2334
2912
  datasource_verify.add_argument("--bind", action="store_true")
2335
2913
  datasource_verify.set_defaults(handler=datasource_verify_command)
2336
2914
 
2337
2915
  schema_discover = sub.add_parser("schema-discover")
2338
2916
  schema_discover.add_argument("--install-root")
2339
2917
  schema_discover.add_argument("--data-root")
2340
- schema_discover.add_argument("--profile", default="default")
2918
+ schema_discover.add_argument("--profile")
2341
2919
  schema_discover.add_argument("--schema", action="append")
2342
2920
  schema_discover.add_argument("--pattern")
2343
2921
  schema_discover.add_argument("--table", action="append")
@@ -2350,14 +2928,14 @@ def build_parser() -> argparse.ArgumentParser:
2350
2928
  semantic_draft = sub.add_parser("semantic-draft")
2351
2929
  semantic_draft.add_argument("--install-root")
2352
2930
  semantic_draft.add_argument("--data-root")
2353
- semantic_draft.add_argument("--profile", default="default")
2931
+ semantic_draft.add_argument("--profile")
2354
2932
  semantic_draft.add_argument("--semantic-plan", required=True)
2355
2933
  semantic_draft.set_defaults(handler=semantic_draft_command)
2356
2934
 
2357
2935
  semantic_validate = sub.add_parser("semantic-validate")
2358
2936
  semantic_validate.add_argument("--install-root")
2359
2937
  semantic_validate.add_argument("--data-root")
2360
- semantic_validate.add_argument("--profile", default="default")
2938
+ semantic_validate.add_argument("--profile")
2361
2939
  semantic_validate.add_argument("--apply", action="store_true")
2362
2940
  semantic_validate.add_argument("--include-stack-trace", action="store_true")
2363
2941
  semantic_validate.set_defaults(handler=semantic_validate_command)
@@ -2365,7 +2943,7 @@ def build_parser() -> argparse.ArgumentParser:
2365
2943
  semantic_publish = sub.add_parser("semantic-publish")
2366
2944
  semantic_publish.add_argument("--install-root")
2367
2945
  semantic_publish.add_argument("--data-root")
2368
- semantic_publish.add_argument("--profile", default="default")
2946
+ semantic_publish.add_argument("--profile")
2369
2947
  semantic_publish.add_argument("--apply", action="store_true")
2370
2948
  semantic_publish.add_argument("--replace-bundle", action="store_true")
2371
2949
  semantic_publish.add_argument("--watch", action="store_true")
@@ -2375,7 +2953,8 @@ def build_parser() -> argparse.ArgumentParser:
2375
2953
  semantic_verify = sub.add_parser("semantic-verify")
2376
2954
  semantic_verify.add_argument("--install-root")
2377
2955
  semantic_verify.add_argument("--data-root")
2378
- semantic_verify.add_argument("--profile", default="default")
2956
+ semantic_verify.add_argument("--project-root")
2957
+ semantic_verify.add_argument("--profile")
2379
2958
  semantic_verify.add_argument("--query-model")
2380
2959
  semantic_verify.add_argument("--query-payload", required=True)
2381
2960
  semantic_verify.add_argument("--execute", action="store_true")