@momentiq/dark-factory-cli 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1036,6 +1036,111 @@ def terminal_status_error(cycle_id: str, doc: CycleDoc, pr_type: str, source: st
1036
1036
  return None
1037
1037
 
1038
1038
 
1039
+ OBJECTIVE_ID_RE = re.compile(r"^(cycle\d+(\.\d+)*#ec\d+|issue\d+#ac\d+)$")
1040
+ OBJECTIVES_MANIFEST_PATH = ".darkfactory/objectives.yaml"
1041
+
1042
+
1043
+ def _route_ids(repo_root: Path) -> set[str]:
1044
+ cfg = repo_root / ".agent-review" / "config.json"
1045
+ if not cfg.exists():
1046
+ return set()
1047
+ try:
1048
+ data = json.loads(cfg.read_text())
1049
+ except json.JSONDecodeError:
1050
+ return set()
1051
+ routes = (data.get("validation") or {}).get("verificationRoutes") or []
1052
+ return {r.get("id") for r in routes if isinstance(r, dict) and isinstance(r.get("id"), str)}
1053
+
1054
+
1055
+ def _declared_refs(trailers: "Trailers") -> set[str]:
1056
+ # The PR's declared sources of intent, keyed "<kind>:<ref>" to match an
1057
+ # objective's source. Cycle trailer ref is normalized via normalize_cycle_id
1058
+ # so dotted ids like "318.4" and trailer values like "Cycle 318.4" compare
1059
+ # equal. Issue ref strips a leading "#" so "1234" and "#1234" both match
1060
+ # (mirroring the TS parseObjective's `ref.replace(/^#/, "")`).
1061
+ refs: set[str] = set()
1062
+ if trailers.cycle:
1063
+ cycle_id = normalize_cycle_id(trailers.cycle)
1064
+ if cycle_id:
1065
+ refs.add(f"cycle:{cycle_id}")
1066
+ if trailers.issue:
1067
+ refs.add(f"issue:{re.sub(r'^#', '', trailers.issue).strip()}")
1068
+ return refs
1069
+
1070
+
1071
+ def validate_objectives(repo_root: Path, trailers: "Trailers") -> list[str]:
1072
+ """Validate .darkfactory/objectives.yaml against PR context. Empty list = ok.
1073
+
1074
+ v1 checks: id format, source linked by a PR trailer, attestedBy route exists.
1075
+ NO coverage check (the `enforced` flag is the future ratchet). Absent manifest
1076
+ is a no-op — objectives are optional in v1.
1077
+
1078
+ Uses a deferred ``import yaml`` so that a missing pyyaml installation only
1079
+ errors when a repo actually ships ``.darkfactory/objectives.yaml`` (the
1080
+ absent-manifest path is a no-op and never imports yaml). This preserves the
1081
+ validator's dependency-light boot on CI environments that have not installed
1082
+ pyyaml.
1083
+ """
1084
+ manifest_path = repo_root / OBJECTIVES_MANIFEST_PATH
1085
+ if not manifest_path.exists():
1086
+ return []
1087
+ try:
1088
+ import yaml # deferred: pyyaml not guaranteed in all CI environments
1089
+ data = yaml.safe_load(manifest_path.read_text()) or {}
1090
+ except ImportError:
1091
+ return [
1092
+ f"{OBJECTIVES_MANIFEST_PATH}: pyyaml is required to validate objectives "
1093
+ "(pip install pyyaml)"
1094
+ ]
1095
+ except yaml.YAMLError as exc:
1096
+ return [f"{OBJECTIVES_MANIFEST_PATH}: invalid YAML — {exc}"]
1097
+ if not isinstance(data, dict):
1098
+ return [f"{OBJECTIVES_MANIFEST_PATH}: top-level must be a mapping"]
1099
+ objectives = data.get("objectives")
1100
+ if not isinstance(objectives, list):
1101
+ return [f"{OBJECTIVES_MANIFEST_PATH}: 'objectives' must be a list"]
1102
+
1103
+ route_ids = _route_ids(repo_root)
1104
+ declared = _declared_refs(trailers)
1105
+ errors: list[str] = []
1106
+ for idx, obj in enumerate(objectives):
1107
+ loc = f"{OBJECTIVES_MANIFEST_PATH} objectives[{idx}]"
1108
+ if not isinstance(obj, dict):
1109
+ errors.append(f"{loc}: expected a mapping")
1110
+ continue
1111
+ oid = obj.get("id")
1112
+ if not isinstance(oid, str) or not OBJECTIVE_ID_RE.match(oid):
1113
+ errors.append(
1114
+ f"{loc}.id: expected 'cycle<N>#ec<k>' or 'issue<N>#ac<k>', got {oid!r}"
1115
+ )
1116
+ source = obj.get("source")
1117
+ if not isinstance(source, dict):
1118
+ errors.append(f"{loc}.source: expected a mapping, got {source!r}")
1119
+ continue
1120
+ kind, ref = source.get("kind"), source.get("ref")
1121
+ # Normalize refs to match _declared_refs: cycle ids go through
1122
+ # normalize_cycle_id (handles dotted ids and "Cycle N.N" prefixes);
1123
+ # issue refs strip a leading "#" so "1234" and "#1234" compare equal.
1124
+ if kind == "cycle" and isinstance(ref, str):
1125
+ normalized_ref = normalize_cycle_id(ref)
1126
+ elif kind == "issue" and isinstance(ref, str):
1127
+ normalized_ref = re.sub(r'^#', '', ref)
1128
+ else:
1129
+ normalized_ref = ref
1130
+ if f"{kind}:{normalized_ref}" not in declared:
1131
+ errors.append(
1132
+ f"{loc}.source: {kind} {ref!r} is not linked by any Cycle:/Closes #N trailer on this PR"
1133
+ )
1134
+ for j, binding in enumerate(obj.get("attestedBy") or []):
1135
+ if isinstance(binding, dict) and binding.get("kind") == "route":
1136
+ rid = binding.get("routeId")
1137
+ if rid not in route_ids:
1138
+ errors.append(
1139
+ f"{loc}.attestedBy[{j}].routeId: {rid!r} is not a verificationRoute in .agent-review/config.json"
1140
+ )
1141
+ return errors
1142
+
1143
+
1039
1144
  def validate(
1040
1145
  title: str,
1041
1146
  body: str,
@@ -1057,6 +1162,7 @@ def validate(
1057
1162
  return errors
1058
1163
 
1059
1164
  trailers = parse_trailers(body)
1165
+ errors.extend(validate_objectives(REPO_ROOT, trailers))
1060
1166
  plan = is_plan_pr(title, labels_list, changed_files)
1061
1167
  pr_type = "plan-pr" if plan else "code-pr"
1062
1168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentiq/dark-factory-cli",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Dark Factory OSS CLI — multi-vendor adversarial critic orchestration (Cursor, Codex, Gemini, Grok) with min-complete-quorum aggregation and trusted-surface rebind",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://momentiq.ai/dark-factory",
@@ -98,7 +98,7 @@
98
98
  "@google/genai": "^2.2.0",
99
99
  "@iarna/toml": "2.2.5",
100
100
  "@modelcontextprotocol/sdk": "^1.29.0",
101
- "@momentiq/dark-factory-schemas": "0.8.0",
101
+ "@momentiq/dark-factory-schemas": "0.9.0",
102
102
  "@openai/codex-sdk": "^0.130.0",
103
103
  "@yarnpkg/lockfile": "1.1.0",
104
104
  "ajv": "^8.20.0",