project-tiny-context-harness 0.2.48 → 0.2.49

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.
@@ -1,276 +1,276 @@
1
- #!/usr/bin/env python3
2
- from pathlib import Path
3
- import re
4
- import sys
5
-
6
- ROOT = Path.cwd()
7
-
8
- GLOBAL_CHECKS = [
9
- ("project goal", ["project goal", "项目目标", "目标"]),
10
- ("boundaries", ["non-goals", "boundaries", "非目标", "边界"]),
11
- ("design rationale", ["design rationale", "设计思路", "设计原因"]),
12
- ("architecture context", ["architecture context", "架构上下文", "architecture.md"]),
13
- ("verification entry points", ["verification entry", "验证入口", "测试入口"]),
14
- ("current state", ["current state", "当前状态"]),
15
- ("next safe action", ["next safe action", "下一步安全动作"]),
16
- ("context index", ["context index", "module index", "上下文索引", "模块索引"]),
17
- ]
18
-
19
- ARCHITECTURE_CHECKS = [
20
- ("system boundary", ["system boundary", "系统边界", "边界"]),
21
- ("component map", ["component map", "组件", "模块关系"]),
22
- ("data or control flow", ["data / control flow", "data flow", "control flow", "数据流", "控制流"]),
23
- ("design rationale", ["design rationale", "设计思路", "设计原因"]),
24
- ("verification implications", ["verification implications", "验证影响", "验证入口"]),
25
- ]
26
-
27
- VALID_ROLES = {
28
- "global",
29
- "architecture",
30
- "area",
31
- "domain",
32
- "subdomain",
33
- "foundation",
34
- "archive",
35
- "contract",
36
- "verification",
37
- "deployment",
38
- "implementation-index",
39
- "decision-rationale",
40
- }
41
-
42
- ROLE_ALIASES = {
43
- "implementation_index": "implementation-index",
44
- "decision_rationale": "decision-rationale",
45
- }
46
-
47
- READ_POLICIES = {"default", "always", "optional", "on-demand", "never-default"}
48
- CONFIG_CANDIDATES = [
49
- ".agent/config.yaml",
50
- ".codex/config.yaml",
51
- ".harness/config.yaml",
52
- ".claude/config.yaml",
53
- ".cursor/config.yaml",
54
- ".cline/config.yaml",
55
- ".roo/config.yaml",
56
- ".gemini/config.yaml",
57
- ]
58
-
59
-
60
- def has_any(text, terms):
61
- lower = text.lower()
62
- return any(term.lower() in lower for term in terms)
63
-
64
-
65
- def normalize_role(value):
66
- role = ROLE_ALIASES.get(value.strip().lower(), value.strip().lower())
67
- return role if role in VALID_ROLES else None
68
-
69
-
70
- def parse_front_matter(text):
71
- lines = text.splitlines()
72
- if not lines or lines[0].strip() != "---":
73
- return {}
74
- result = {}
75
- for line in lines[1:]:
76
- if line.strip() == "---":
77
- return result
78
- match = re.match(r"^([A-Za-z0-9_-]+):\s*(.+?)\s*$", line.strip())
79
- if match:
80
- result[match.group(1)] = match.group(2).strip().strip("\"'")
81
- return result
82
-
83
-
84
- def strip_comment(line):
85
- quote = None
86
- for index, char in enumerate(line):
87
- if char in {"'", "\""} and (index == 0 or line[index - 1] != "\\"):
88
- quote = None if quote == char else quote or char
89
- if char == "#" and quote is None:
90
- return line[:index]
91
- return line
92
-
93
-
94
- def parse_toml_value(raw):
95
- value = raw.strip()
96
- if value == "true":
97
- return True
98
- if value == "false":
99
- return False
100
- if (value.startswith("\"") and value.endswith("\"")) or (value.startswith("'") and value.endswith("'")):
101
- return value[1:-1]
102
- if value.startswith("[") and value.endswith("]"):
103
- inner = value[1:-1].strip()
104
- if not inner:
105
- return []
106
- items = []
107
- for part in inner.split(","):
108
- item = part.strip()
109
- if not ((item.startswith("\"") and item.endswith("\"")) or (item.startswith("'") and item.endswith("'"))):
110
- return None
111
- items.append(item[1:-1])
112
- return items
113
- return None
114
-
115
-
116
- def parse_manifest(path, errors):
117
- manifest = {"areas": [], "context": []}
118
- current = None
119
- for line_number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
120
- line = strip_comment(raw).strip()
121
- if not line:
122
- continue
123
- match = re.match(r"^\[\[(areas|context)\]\]$", line)
124
- if match:
125
- current = {"line": line_number}
126
- manifest[match.group(1)].append(current)
127
- continue
128
- if current is None:
129
- errors.append(f"project_context/context.toml line {line_number} must appear inside [[areas]] or [[context]]")
130
- continue
131
- assignment = re.match(r"^([A-Za-z0-9_-]+)\s*=\s*(.+)$", line)
132
- if not assignment:
133
- errors.append(f"project_context/context.toml line {line_number} is not a supported assignment")
134
- continue
135
- value = parse_toml_value(assignment.group(2))
136
- if value is None:
137
- errors.append(f"project_context/context.toml line {line_number} has an unsupported value")
138
- continue
139
- current[assignment.group(1)] = value
140
- return manifest
141
-
142
-
143
- def normalize_context_path(value):
144
- return value.replace("\\", "/").removeprefix("./")
145
-
146
-
147
- def add_manifest_role(entry, role, roles, errors):
148
- raw_path = entry.get("context") or entry.get("path")
149
- if not isinstance(raw_path, str):
150
- errors.append(f"project_context/context.toml line {entry['line']} must include a context/path string")
151
- return
152
- rel = normalize_context_path(raw_path)
153
- if not rel.startswith("project_context/") or not rel.endswith(".md"):
154
- errors.append(f"project_context/context.toml line {entry['line']} must reference a markdown file under project_context/")
155
- return
156
- existing = roles.get(rel)
157
- if existing and existing != role:
158
- errors.append(f"project_context/context.toml assigns conflicting roles to {rel}: {existing} and {role}")
159
- return
160
- roles[rel] = role
161
- if not (ROOT / rel).exists():
162
- errors.append(f"project_context/context.toml references missing context file: {rel}")
163
-
164
-
165
- def validate_manifest(errors):
166
- manifest_path = ROOT / "project_context/context.toml"
167
- roles = {}
168
- if not manifest_path.exists():
169
- if schema_requires_manifest():
170
- errors.append("missing project_context/context.toml; run sdlc-harness upgrade to create the Schema v4 Context graph manifest")
171
- return roles
172
- manifest = parse_manifest(manifest_path, errors)
173
- if not manifest["areas"]:
174
- errors.append("project_context/context.toml must declare at least one [[areas]] entry")
175
- has_default_area = False
176
- for area in manifest["areas"]:
177
- for key in ["id", "root", "context"]:
178
- if not isinstance(area.get(key), str):
179
- errors.append(f"project_context/context.toml line {area['line']} must include string field {key}")
180
- default_area = area.get("default")
181
- if default_area is not None and not isinstance(default_area, bool):
182
- errors.append(f"project_context/context.toml line {area['line']} default must be a boolean")
183
- has_default_area = has_default_area or default_area is True
184
- deps = area.get("forbidden_runtime_dependencies")
185
- if deps is not None and not is_string_array(deps):
186
- errors.append(f"project_context/context.toml line {area['line']} forbidden_runtime_dependencies must be an array of strings")
187
- add_manifest_role(area, "area", roles, errors)
188
- if manifest["areas"] and not has_default_area:
189
- errors.append("project_context/context.toml must mark one [[areas]] entry with default = true")
190
- for context in manifest["context"]:
191
- role = normalize_role(context.get("role", "")) if isinstance(context.get("role"), str) else None
192
- if role is None:
193
- errors.append(f"project_context/context.toml line {context['line']} has unsupported context role")
194
- continue
195
- read_policy = context.get("read_policy")
196
- if read_policy is not None and read_policy not in READ_POLICIES:
197
- errors.append(f"project_context/context.toml line {context['line']} has unsupported read_policy: {read_policy}")
198
- for key in ["triggers", "default_children"]:
199
- value = context.get(key)
200
- if value is not None and not is_string_array(value):
201
- errors.append(f"project_context/context.toml line {context['line']} {key} must be an array of strings")
202
- add_manifest_role(context, role, roles, errors)
203
- return roles
204
-
205
-
206
- def is_string_array(value):
207
- return isinstance(value, list) and all(isinstance(item, str) for item in value)
208
-
209
-
210
- def schema_requires_manifest():
211
- schema_version = "4"
212
- for candidate in CONFIG_CANDIDATES:
213
- path = ROOT / candidate
214
- if not path.exists():
215
- continue
216
- match = re.search(r'schema_version:\s*["\']?([^"\'\s]+)', path.read_text(encoding="utf-8"))
217
- if match:
218
- schema_version = match.group(1)
219
- break
220
- try:
221
- return int(schema_version.split(".", 1)[0]) >= 4
222
- except ValueError:
223
- return True
224
-
225
-
226
- def validate_checks(rel, text, checks, errors):
227
- for label, terms in checks:
228
- if not has_any(text, terms):
229
- errors.append(f"{rel} must include {label}")
230
-
231
-
232
- def main():
233
- errors = []
234
- global_path = ROOT / "project_context/global.md"
235
- if not global_path.exists():
236
- errors.append("missing project_context/global.md")
237
- else:
238
- validate_checks("project_context/global.md", global_path.read_text(encoding="utf-8"), GLOBAL_CHECKS, errors)
239
-
240
- architecture_path = ROOT / "project_context/architecture.md"
241
- if not architecture_path.exists():
242
- errors.append("missing project_context/architecture.md")
243
- else:
244
- validate_checks("project_context/architecture.md", architecture_path.read_text(encoding="utf-8"), ARCHITECTURE_CHECKS, errors)
245
-
246
- manifest_roles = validate_manifest(errors)
247
- context_root = ROOT / "project_context"
248
- context_files = sorted(context_root.rglob("*.md")) if context_root.exists() else []
249
- checked = 0
250
- for path in context_files:
251
- rel = path.relative_to(ROOT).as_posix()
252
- if rel in {"project_context/global.md", "project_context/architecture.md"}:
253
- continue
254
- text = path.read_text(encoding="utf-8")
255
- front_matter = parse_front_matter(text)
256
- declared_role = front_matter.get("context_role")
257
- front_matter_role = normalize_role(declared_role) if declared_role else None
258
- if declared_role and front_matter_role is None:
259
- errors.append(f"{rel} has unsupported context_role: {declared_role}")
260
- read_policy = front_matter.get("read_policy")
261
- if read_policy and read_policy not in READ_POLICIES:
262
- errors.append(f"{rel} has unsupported read_policy: {read_policy}")
263
- role = manifest_roles.get(rel) or front_matter_role
264
- if role is not None and role not in {"global", "architecture"}:
265
- checked += 1
266
-
267
- if errors:
268
- for error in errors:
269
- print(f"error: {error}", file=sys.stderr)
270
- return 1
271
- print(f"Context OK: {2 + checked} context file(s)")
272
- return 0
273
-
274
-
275
- if __name__ == "__main__":
276
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ from pathlib import Path
3
+ import re
4
+ import sys
5
+
6
+ ROOT = Path.cwd()
7
+
8
+ GLOBAL_CHECKS = [
9
+ ("project goal", ["project goal", "项目目标", "目标"]),
10
+ ("boundaries", ["non-goals", "boundaries", "非目标", "边界"]),
11
+ ("design rationale", ["design rationale", "设计思路", "设计原因"]),
12
+ ("architecture context", ["architecture context", "架构上下文", "architecture.md"]),
13
+ ("verification entry points", ["verification entry", "验证入口", "测试入口"]),
14
+ ("current state", ["current state", "当前状态"]),
15
+ ("next safe action", ["next safe action", "下一步安全动作"]),
16
+ ("context index", ["context index", "module index", "上下文索引", "模块索引"]),
17
+ ]
18
+
19
+ ARCHITECTURE_CHECKS = [
20
+ ("system boundary", ["system boundary", "系统边界", "边界"]),
21
+ ("component map", ["component map", "组件", "模块关系"]),
22
+ ("data or control flow", ["data / control flow", "data flow", "control flow", "数据流", "控制流"]),
23
+ ("design rationale", ["design rationale", "设计思路", "设计原因"]),
24
+ ("verification implications", ["verification implications", "验证影响", "验证入口"]),
25
+ ]
26
+
27
+ VALID_ROLES = {
28
+ "global",
29
+ "architecture",
30
+ "area",
31
+ "domain",
32
+ "subdomain",
33
+ "foundation",
34
+ "archive",
35
+ "contract",
36
+ "verification",
37
+ "deployment",
38
+ "implementation-index",
39
+ "decision-rationale",
40
+ }
41
+
42
+ ROLE_ALIASES = {
43
+ "implementation_index": "implementation-index",
44
+ "decision_rationale": "decision-rationale",
45
+ }
46
+
47
+ READ_POLICIES = {"default", "always", "optional", "on-demand", "never-default"}
48
+ CONFIG_CANDIDATES = [
49
+ ".agent/config.yaml",
50
+ ".codex/config.yaml",
51
+ ".harness/config.yaml",
52
+ ".claude/config.yaml",
53
+ ".cursor/config.yaml",
54
+ ".cline/config.yaml",
55
+ ".roo/config.yaml",
56
+ ".gemini/config.yaml",
57
+ ]
58
+
59
+
60
+ def has_any(text, terms):
61
+ lower = text.lower()
62
+ return any(term.lower() in lower for term in terms)
63
+
64
+
65
+ def normalize_role(value):
66
+ role = ROLE_ALIASES.get(value.strip().lower(), value.strip().lower())
67
+ return role if role in VALID_ROLES else None
68
+
69
+
70
+ def parse_front_matter(text):
71
+ lines = text.splitlines()
72
+ if not lines or lines[0].strip() != "---":
73
+ return {}
74
+ result = {}
75
+ for line in lines[1:]:
76
+ if line.strip() == "---":
77
+ return result
78
+ match = re.match(r"^([A-Za-z0-9_-]+):\s*(.+?)\s*$", line.strip())
79
+ if match:
80
+ result[match.group(1)] = match.group(2).strip().strip("\"'")
81
+ return result
82
+
83
+
84
+ def strip_comment(line):
85
+ quote = None
86
+ for index, char in enumerate(line):
87
+ if char in {"'", "\""} and (index == 0 or line[index - 1] != "\\"):
88
+ quote = None if quote == char else quote or char
89
+ if char == "#" and quote is None:
90
+ return line[:index]
91
+ return line
92
+
93
+
94
+ def parse_toml_value(raw):
95
+ value = raw.strip()
96
+ if value == "true":
97
+ return True
98
+ if value == "false":
99
+ return False
100
+ if (value.startswith("\"") and value.endswith("\"")) or (value.startswith("'") and value.endswith("'")):
101
+ return value[1:-1]
102
+ if value.startswith("[") and value.endswith("]"):
103
+ inner = value[1:-1].strip()
104
+ if not inner:
105
+ return []
106
+ items = []
107
+ for part in inner.split(","):
108
+ item = part.strip()
109
+ if not ((item.startswith("\"") and item.endswith("\"")) or (item.startswith("'") and item.endswith("'"))):
110
+ return None
111
+ items.append(item[1:-1])
112
+ return items
113
+ return None
114
+
115
+
116
+ def parse_manifest(path, errors):
117
+ manifest = {"areas": [], "context": []}
118
+ current = None
119
+ for line_number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
120
+ line = strip_comment(raw).strip()
121
+ if not line:
122
+ continue
123
+ match = re.match(r"^\[\[(areas|context)\]\]$", line)
124
+ if match:
125
+ current = {"line": line_number}
126
+ manifest[match.group(1)].append(current)
127
+ continue
128
+ if current is None:
129
+ errors.append(f"project_context/context.toml line {line_number} must appear inside [[areas]] or [[context]]")
130
+ continue
131
+ assignment = re.match(r"^([A-Za-z0-9_-]+)\s*=\s*(.+)$", line)
132
+ if not assignment:
133
+ errors.append(f"project_context/context.toml line {line_number} is not a supported assignment")
134
+ continue
135
+ value = parse_toml_value(assignment.group(2))
136
+ if value is None:
137
+ errors.append(f"project_context/context.toml line {line_number} has an unsupported value")
138
+ continue
139
+ current[assignment.group(1)] = value
140
+ return manifest
141
+
142
+
143
+ def normalize_context_path(value):
144
+ return value.replace("\\", "/").removeprefix("./")
145
+
146
+
147
+ def add_manifest_role(entry, role, roles, errors):
148
+ raw_path = entry.get("context") or entry.get("path")
149
+ if not isinstance(raw_path, str):
150
+ errors.append(f"project_context/context.toml line {entry['line']} must include a context/path string")
151
+ return
152
+ rel = normalize_context_path(raw_path)
153
+ if not rel.startswith("project_context/") or not rel.endswith(".md"):
154
+ errors.append(f"project_context/context.toml line {entry['line']} must reference a markdown file under project_context/")
155
+ return
156
+ existing = roles.get(rel)
157
+ if existing and existing != role:
158
+ errors.append(f"project_context/context.toml assigns conflicting roles to {rel}: {existing} and {role}")
159
+ return
160
+ roles[rel] = role
161
+ if not (ROOT / rel).exists():
162
+ errors.append(f"project_context/context.toml references missing context file: {rel}")
163
+
164
+
165
+ def validate_manifest(errors):
166
+ manifest_path = ROOT / "project_context/context.toml"
167
+ roles = {}
168
+ if not manifest_path.exists():
169
+ if schema_requires_manifest():
170
+ errors.append("missing project_context/context.toml; run sdlc-harness upgrade to create the Schema v4 Context graph manifest")
171
+ return roles
172
+ manifest = parse_manifest(manifest_path, errors)
173
+ if not manifest["areas"]:
174
+ errors.append("project_context/context.toml must declare at least one [[areas]] entry")
175
+ has_default_area = False
176
+ for area in manifest["areas"]:
177
+ for key in ["id", "root", "context"]:
178
+ if not isinstance(area.get(key), str):
179
+ errors.append(f"project_context/context.toml line {area['line']} must include string field {key}")
180
+ default_area = area.get("default")
181
+ if default_area is not None and not isinstance(default_area, bool):
182
+ errors.append(f"project_context/context.toml line {area['line']} default must be a boolean")
183
+ has_default_area = has_default_area or default_area is True
184
+ deps = area.get("forbidden_runtime_dependencies")
185
+ if deps is not None and not is_string_array(deps):
186
+ errors.append(f"project_context/context.toml line {area['line']} forbidden_runtime_dependencies must be an array of strings")
187
+ add_manifest_role(area, "area", roles, errors)
188
+ if manifest["areas"] and not has_default_area:
189
+ errors.append("project_context/context.toml must mark one [[areas]] entry with default = true")
190
+ for context in manifest["context"]:
191
+ role = normalize_role(context.get("role", "")) if isinstance(context.get("role"), str) else None
192
+ if role is None:
193
+ errors.append(f"project_context/context.toml line {context['line']} has unsupported context role")
194
+ continue
195
+ read_policy = context.get("read_policy")
196
+ if read_policy is not None and read_policy not in READ_POLICIES:
197
+ errors.append(f"project_context/context.toml line {context['line']} has unsupported read_policy: {read_policy}")
198
+ for key in ["triggers", "default_children"]:
199
+ value = context.get(key)
200
+ if value is not None and not is_string_array(value):
201
+ errors.append(f"project_context/context.toml line {context['line']} {key} must be an array of strings")
202
+ add_manifest_role(context, role, roles, errors)
203
+ return roles
204
+
205
+
206
+ def is_string_array(value):
207
+ return isinstance(value, list) and all(isinstance(item, str) for item in value)
208
+
209
+
210
+ def schema_requires_manifest():
211
+ schema_version = "4"
212
+ for candidate in CONFIG_CANDIDATES:
213
+ path = ROOT / candidate
214
+ if not path.exists():
215
+ continue
216
+ match = re.search(r'schema_version:\s*["\']?([^"\'\s]+)', path.read_text(encoding="utf-8"))
217
+ if match:
218
+ schema_version = match.group(1)
219
+ break
220
+ try:
221
+ return int(schema_version.split(".", 1)[0]) >= 4
222
+ except ValueError:
223
+ return True
224
+
225
+
226
+ def validate_checks(rel, text, checks, errors):
227
+ for label, terms in checks:
228
+ if not has_any(text, terms):
229
+ errors.append(f"{rel} must include {label}")
230
+
231
+
232
+ def main():
233
+ errors = []
234
+ global_path = ROOT / "project_context/global.md"
235
+ if not global_path.exists():
236
+ errors.append("missing project_context/global.md")
237
+ else:
238
+ validate_checks("project_context/global.md", global_path.read_text(encoding="utf-8"), GLOBAL_CHECKS, errors)
239
+
240
+ architecture_path = ROOT / "project_context/architecture.md"
241
+ if not architecture_path.exists():
242
+ errors.append("missing project_context/architecture.md")
243
+ else:
244
+ validate_checks("project_context/architecture.md", architecture_path.read_text(encoding="utf-8"), ARCHITECTURE_CHECKS, errors)
245
+
246
+ manifest_roles = validate_manifest(errors)
247
+ context_root = ROOT / "project_context"
248
+ context_files = sorted(context_root.rglob("*.md")) if context_root.exists() else []
249
+ checked = 0
250
+ for path in context_files:
251
+ rel = path.relative_to(ROOT).as_posix()
252
+ if rel in {"project_context/global.md", "project_context/architecture.md"}:
253
+ continue
254
+ text = path.read_text(encoding="utf-8")
255
+ front_matter = parse_front_matter(text)
256
+ declared_role = front_matter.get("context_role")
257
+ front_matter_role = normalize_role(declared_role) if declared_role else None
258
+ if declared_role and front_matter_role is None:
259
+ errors.append(f"{rel} has unsupported context_role: {declared_role}")
260
+ read_policy = front_matter.get("read_policy")
261
+ if read_policy and read_policy not in READ_POLICIES:
262
+ errors.append(f"{rel} has unsupported read_policy: {read_policy}")
263
+ role = manifest_roles.get(rel) or front_matter_role
264
+ if role is not None and role not in {"global", "architecture"}:
265
+ checked += 1
266
+
267
+ if errors:
268
+ for error in errors:
269
+ print(f"error: {error}", file=sys.stderr)
270
+ return 1
271
+ print(f"Context OK: {2 + checked} context file(s)")
272
+ return 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())
@@ -136,11 +136,11 @@ function printWarnings(warnings) {
136
136
  }
137
137
  }
138
138
  function helpText() {
139
- return `sdlc-harness export-context:
140
- export-context --full [--output tmp/sdlc/context-exports/<name>.md] [--check]
141
- export-context --code [--output tmp/sdlc/context-exports/<name>.md] [--check]
142
- export-context --all [--check]
143
-
139
+ return `sdlc-harness export-context:
140
+ export-context --full [--output tmp/sdlc/context-exports/<name>.md] [--check]
141
+ export-context --code [--output tmp/sdlc/context-exports/<name>.md] [--check]
142
+ export-context --all [--check]
143
+
144
144
  Creates temporary Markdown artifacts for copying or external-tool ingestion.
145
145
  --full exports the project Context summary as a full-project-context artifact.
146
146
  --code exports one current implementation snapshot as a code-level-implementation artifact.
@@ -18,16 +18,16 @@ export const commands = {
18
18
  package: packageSource
19
19
  };
20
20
  export function help() {
21
- console.log(`sdlc-harness commands:
22
- init [--adopt] [--harness-folder <path>]
23
- Initialize/adopt a project; without --harness-folder, choose target agent first
24
- sync Materialize canonical assets into the workspace
25
- upgrade Run migrations and then sync
26
- doctor Diagnose project configuration and drift
27
- export-context --full|--code|--all [--output <path>] [--check]
28
- Export a temporary Context summary or code implementation Markdown artifact
29
- validate <gate> Run a Harness validation gate (Minimal Context only)
30
- validate-context Validate Minimal Context fact-source recoverability
31
- validate-harness Compatibility alias for validate-context
21
+ console.log(`sdlc-harness commands:
22
+ init [--adopt] [--harness-folder <path>]
23
+ Initialize/adopt a project; without --harness-folder, choose target agent first
24
+ sync Materialize canonical assets into the workspace
25
+ upgrade Run migrations and then sync
26
+ doctor Diagnose project configuration and drift
27
+ export-context --full|--code|--all [--output <path>] [--check]
28
+ Export a temporary Context summary or code implementation Markdown artifact
29
+ validate <gate> Run a Harness validation gate (Minimal Context only)
30
+ validate-context Validate Minimal Context fact-source recoverability
31
+ validate-harness Compatibility alias for validate-context
32
32
  package <subcommand> Maintain package canonical source`);
33
33
  }
@@ -18,7 +18,7 @@ export async function packageSource(args) {
18
18
  console.log("package source OK");
19
19
  return;
20
20
  }
21
- console.log(`sdlc-harness package commands:
22
- sync-source Update package canonical assets from this source workspace
21
+ console.log(`sdlc-harness package commands:
22
+ sync-source Update package canonical assets from this source workspace
23
23
  check-source Verify package canonical assets match this source workspace`);
24
24
  }
@@ -1,3 +1,3 @@
1
- # Migrations
2
-
3
- Schema migrations for `.harness/config.yaml` and managed file layout belong here.
1
+ # Migrations
2
+
3
+ Schema migrations for `.harness/config.yaml` and managed file layout belong here.