@momentiq/dark-factory-cli 2.10.0 → 2.11.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.
|
@@ -1068,19 +1068,33 @@ def _declared_refs(trailers: "Trailers") -> set[str]:
|
|
|
1068
1068
|
return refs
|
|
1069
1069
|
|
|
1070
1070
|
|
|
1071
|
-
def validate_objectives(
|
|
1071
|
+
def validate_objectives(
|
|
1072
|
+
repo_root: Path, trailers: "Trailers", changed_files: Iterable[str]
|
|
1073
|
+
) -> list[str]:
|
|
1072
1074
|
"""Validate .darkfactory/objectives.yaml against PR context. Empty list = ok.
|
|
1073
1075
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1076
|
+
**Validated ONLY when this PR authors/edits the manifest** (it appears in the
|
|
1077
|
+
PR's changed files). A committed manifest from an earlier PR must NOT be
|
|
1078
|
+
re-validated against an unrelated later PR's trailers — that was the
|
|
1079
|
+
stale-manifest gate-break (momentiq-ai/dark-factory#207). Combined with
|
|
1080
|
+
per-PR authoring (the agent drafts the manifest at PR-open), each PR's
|
|
1081
|
+
objectives are checked against that PR's own trailers and nothing else.
|
|
1082
|
+
|
|
1083
|
+
Structural checks mirror the TS ``parseObjectivesManifest`` contract (id
|
|
1084
|
+
format, id↔source consistency, non-empty text, ``enforced`` boolean, and the
|
|
1085
|
+
``attestedBy`` binding shapes) so the Python gate is not a looser subset that
|
|
1086
|
+
can silently drift from the TS parser. The PR-context checks (source linked
|
|
1087
|
+
by a trailer, ``route`` binding exists in config) are gate-only — they need
|
|
1088
|
+
PR state the pure parser doesn't have. NO coverage check (the ``enforced``
|
|
1089
|
+
flag is the future ratchet).
|
|
1090
|
+
|
|
1091
|
+
Uses a deferred ``import yaml`` so a missing pyyaml installation only errors
|
|
1092
|
+
when a repo actually ships the manifest (the skip/absent paths never import
|
|
1093
|
+
yaml), preserving the validator's dependency-light boot.
|
|
1083
1094
|
"""
|
|
1095
|
+
# Gate: only author-time validation. PRs that don't touch the manifest skip.
|
|
1096
|
+
if OBJECTIVES_MANIFEST_PATH not in set(changed_files):
|
|
1097
|
+
return []
|
|
1084
1098
|
manifest_path = repo_root / OBJECTIVES_MANIFEST_PATH
|
|
1085
1099
|
if not manifest_path.exists():
|
|
1086
1100
|
return []
|
|
@@ -1096,6 +1110,11 @@ def validate_objectives(repo_root: Path, trailers: "Trailers") -> list[str]:
|
|
|
1096
1110
|
return [f"{OBJECTIVES_MANIFEST_PATH}: invalid YAML — {exc}"]
|
|
1097
1111
|
if not isinstance(data, dict):
|
|
1098
1112
|
return [f"{OBJECTIVES_MANIFEST_PATH}: top-level must be a mapping"]
|
|
1113
|
+
if data.get("schemaVersion") != 1:
|
|
1114
|
+
return [
|
|
1115
|
+
f"{OBJECTIVES_MANIFEST_PATH}: schemaVersion must be 1, got "
|
|
1116
|
+
f"{data.get('schemaVersion')!r}"
|
|
1117
|
+
]
|
|
1099
1118
|
objectives = data.get("objectives")
|
|
1100
1119
|
if not isinstance(objectives, list):
|
|
1101
1120
|
return [f"{OBJECTIVES_MANIFEST_PATH}: 'objectives' must be a list"]
|
|
@@ -1113,31 +1132,72 @@ def validate_objectives(repo_root: Path, trailers: "Trailers") -> list[str]:
|
|
|
1113
1132
|
errors.append(
|
|
1114
1133
|
f"{loc}.id: expected 'cycle<N>#ec<k>' or 'issue<N>#ac<k>', got {oid!r}"
|
|
1115
1134
|
)
|
|
1135
|
+
text = obj.get("text")
|
|
1136
|
+
if not isinstance(text, str) or not text:
|
|
1137
|
+
errors.append(f"{loc}.text: expected a non-empty string")
|
|
1138
|
+
if not isinstance(obj.get("enforced"), bool):
|
|
1139
|
+
errors.append(f"{loc}.enforced: expected a boolean")
|
|
1116
1140
|
source = obj.get("source")
|
|
1117
1141
|
if not isinstance(source, dict):
|
|
1118
1142
|
errors.append(f"{loc}.source: expected a mapping, got {source!r}")
|
|
1119
1143
|
continue
|
|
1120
1144
|
kind, ref = source.get("kind"), source.get("ref")
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1145
|
+
if kind not in ("cycle", "issue"):
|
|
1146
|
+
errors.append(f"{loc}.source.kind: expected 'cycle' | 'issue', got {kind!r}")
|
|
1147
|
+
if not isinstance(ref, str) or not ref:
|
|
1148
|
+
errors.append(f"{loc}.source.ref: expected a non-empty string")
|
|
1149
|
+
# id↔source consistency (mirrors the TS parseObjective invariant): the id
|
|
1150
|
+
# must be namespaced by its source, e.g. cycle21#ec1 ↔ {cycle, "21"}.
|
|
1151
|
+
if isinstance(oid, str) and isinstance(kind, str) and isinstance(ref, str):
|
|
1152
|
+
ref_num = re.sub(r"^#", "", ref) if kind == "issue" else ref
|
|
1153
|
+
if not oid.startswith(f"{kind}{ref_num}#"):
|
|
1154
|
+
errors.append(
|
|
1155
|
+
f"{loc}.id: {oid!r} is inconsistent with source "
|
|
1156
|
+
f"{{kind: {kind}, ref: {ref!r}}}"
|
|
1157
|
+
)
|
|
1158
|
+
# PR-context: the source must be linked by one of this PR's trailers.
|
|
1159
|
+
# Only run when kind/ref are themselves valid — otherwise their own
|
|
1160
|
+
# errors above already fired, and a "not linked" message here would be
|
|
1161
|
+
# misleading noise the TS parser wouldn't surface. Normalize to match
|
|
1162
|
+
# _declared_refs (cycle via normalize_cycle_id; issue strips a leading "#").
|
|
1163
|
+
if kind in ("cycle", "issue") and isinstance(ref, str):
|
|
1164
|
+
normalized_ref = normalize_cycle_id(ref) if kind == "cycle" else re.sub(r'^#', '', ref)
|
|
1165
|
+
if f"{kind}:{normalized_ref}" not in declared:
|
|
1166
|
+
errors.append(
|
|
1167
|
+
f"{loc}.source: {kind} {ref!r} is not linked by any Cycle:/Closes #N trailer on this PR"
|
|
1168
|
+
)
|
|
1169
|
+
# attestedBy: structural validation of each binding (kind + required
|
|
1170
|
+
# field), plus route-existence (PR context) for route bindings.
|
|
1171
|
+
bindings = obj.get("attestedBy")
|
|
1172
|
+
if not isinstance(bindings, list):
|
|
1173
|
+
errors.append(f"{loc}.attestedBy: expected a list")
|
|
1174
|
+
continue
|
|
1175
|
+
for j, binding in enumerate(bindings):
|
|
1176
|
+
bloc = f"{loc}.attestedBy[{j}]"
|
|
1177
|
+
if not isinstance(binding, dict):
|
|
1178
|
+
errors.append(f"{bloc}: expected a mapping")
|
|
1179
|
+
continue
|
|
1180
|
+
bkind = binding.get("kind")
|
|
1181
|
+
if bkind == "route":
|
|
1136
1182
|
rid = binding.get("routeId")
|
|
1137
|
-
if rid not
|
|
1183
|
+
if not isinstance(rid, str) or not rid:
|
|
1184
|
+
errors.append(f"{bloc}.routeId: expected a non-empty string")
|
|
1185
|
+
elif rid not in route_ids:
|
|
1138
1186
|
errors.append(
|
|
1139
|
-
f"{
|
|
1187
|
+
f"{bloc}.routeId: {rid!r} is not a verificationRoute in .agent-review/config.json"
|
|
1140
1188
|
)
|
|
1189
|
+
elif bkind == "critic":
|
|
1190
|
+
cid = binding.get("criticId")
|
|
1191
|
+
if not isinstance(cid, str) or not cid:
|
|
1192
|
+
errors.append(f"{bloc}.criticId: expected a non-empty string")
|
|
1193
|
+
elif bkind == "test":
|
|
1194
|
+
tref = binding.get("ref")
|
|
1195
|
+
if not isinstance(tref, str) or not tref:
|
|
1196
|
+
errors.append(f"{bloc}.ref: expected a non-empty string")
|
|
1197
|
+
else:
|
|
1198
|
+
errors.append(
|
|
1199
|
+
f"{bloc}.kind: expected 'route' | 'critic' | 'test', got {bkind!r}"
|
|
1200
|
+
)
|
|
1141
1201
|
return errors
|
|
1142
1202
|
|
|
1143
1203
|
|
|
@@ -1162,7 +1222,8 @@ def validate(
|
|
|
1162
1222
|
return errors
|
|
1163
1223
|
|
|
1164
1224
|
trailers = parse_trailers(body)
|
|
1165
|
-
|
|
1225
|
+
changed_files = list(changed_files)
|
|
1226
|
+
errors.extend(validate_objectives(REPO_ROOT, trailers, changed_files))
|
|
1166
1227
|
plan = is_plan_pr(title, labels_list, changed_files)
|
|
1167
1228
|
pr_type = "plan-pr" if plan else "code-pr"
|
|
1168
1229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momentiq/dark-factory-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verify
|
|
3
|
-
description: Use to PRODUCE evidence-gated validation-route evidence for a change before pushing — run the change-appropriate verification route(s) (terraform validate, helm lint+template, docker build, the touched service's targeted tests, the UI/Playwright route) and write per-SHA, diffHash-bound QualityGateEvidence the local pre-push gate (df gate-push / enforceVerificationRoutes) asserts. Use when asked to "verify a change", "produce route evidence", a routed push is blocked with verification_route_missing / verification_route_failed, or before pushing a commit that touches a routed surface (infra/terraform, deploy/helm, a Dockerfile, services/*/src, web/** or *.tsx).
|
|
3
|
+
description: Use to PRODUCE evidence-gated validation-route evidence for a change before pushing — run the change-appropriate verification route(s) (terraform validate, helm lint+template, docker build, the touched service's targeted tests, the UI/Playwright route) and write per-SHA, diffHash-bound QualityGateEvidence the local pre-push gate (df gate-push / enforceVerificationRoutes) asserts. Use when asked to "verify a change", "produce route evidence", a routed push is blocked with verification_route_missing / verification_route_failed, or before pushing a commit that touches a routed surface (infra/terraform, deploy/helm, a Dockerfile, services/*/src, web/** or *.tsx). ALSO use at CLOSEOUT — when a change declares objectives in .darkfactory/objectives.yaml, run `df prove` to bind that evidence to the objectives and "declare victory with proof" (a proven/pending/failed readout) instead of asserting "done".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /verify — evidence-gated validation routes
|
|
@@ -98,3 +98,31 @@ CLI. Copy the reference files from the `@momentiq/dark-factory-cli` package
|
|
|
98
98
|
(routes + required headings), wire an optional auth storage-state for protected
|
|
99
99
|
surfaces, and arm the `playwright` route's `command`. The full walkthrough is in
|
|
100
100
|
`docs/guides/ui-verification-route.md`.
|
|
101
|
+
|
|
102
|
+
## Closeout — declare victory with proof, not "done"
|
|
103
|
+
|
|
104
|
+
Producing evidence is half the loop; the other half is **proving the objectives
|
|
105
|
+
it attests**. When a change declares objectives in `.darkfactory/objectives.yaml`,
|
|
106
|
+
the closeout step is **`df prove`** — it joins each objective against the evidence
|
|
107
|
+
(`df verify` route exit codes + `df review` critic verdicts) and reports, per
|
|
108
|
+
objective, **proven / pending / failed**. An agent's final turn runs `df prove`
|
|
109
|
+
(or the `df_prove` MCP tool) and **cites that readout as the completion claim** —
|
|
110
|
+
proof derived from diffHash-bound artifacts — rather than asserting "done".
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
df prove # human readout: per-objective proven/pending/failed + bindings
|
|
114
|
+
df prove --json # the BoundProofRecord (for agents / the df_prove MCP tool)
|
|
115
|
+
df prove --strict # exit 1 if ANY objective is not proven (the gate form)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **`pending` is honest, not a failure.** A `critic` binding is `pending` until the
|
|
119
|
+
fleet runs on HEAD (run `df review` or wait for CI); a `route` binding you can
|
|
120
|
+
satisfy now with `df verify`. Only `failed` (evidence is negative) is a problem.
|
|
121
|
+
- **Trust boundary.** This readout is *agent-attested, evidence-backed* — stronger
|
|
122
|
+
than free-text "done" (every status is derived, not asserted), but not yet
|
|
123
|
+
independent verification (that closes with runner-attested evidence + the
|
|
124
|
+
source-criterion ratchet).
|
|
125
|
+
- **The ratchet.** v1 is informational (`enforced: false` — `df prove` exits 0 and
|
|
126
|
+
surfaces pending/failed). Flip an objective to `enforced: true`, or run
|
|
127
|
+
`df prove --strict` (e.g. a `.husky/pre-push` step — see CONSUMER-ADOPTION §5),
|
|
128
|
+
to turn unproven objectives into a hard block with no code change.
|