project-tiny-context-harness 0.2.49 → 0.2.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +247 -216
- package/assets/README.md +295 -258
- package/assets/agents/.gitkeep +1 -1
- package/assets/agents/AGENTS_CORE.md +56 -56
- package/assets/context_templates/architecture.md +31 -31
- package/assets/context_templates/area.md +24 -24
- package/assets/context_templates/context.toml +27 -27
- package/assets/context_templates/deployment.md +35 -35
- package/assets/context_templates/global.md +53 -53
- package/assets/context_templates/verification.md +28 -28
- package/assets/github/.gitkeep +1 -1
- package/assets/github/harness.yml +25 -25
- package/assets/make/.gitkeep +1 -1
- package/assets/make/sdlc-harness.mk +33 -33
- package/assets/skills/context_development_engineer/SKILL.md +64 -64
- package/assets/skills/context_full_project_export/SKILL.md +13 -13
- package/assets/skills/context_product_plan/SKILL.md +69 -69
- package/assets/skills/context_uiux_design/SKILL.md +110 -110
- package/assets/tools/validate_context.py +276 -276
- package/dist/commands/export-context.js +5 -5
- package/dist/commands/index.js +12 -11
- package/dist/commands/package-source.js +2 -2
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.js +56 -1
- package/dist/lib/migrations.d.ts +31 -3
- package/dist/lib/migrations.js +260 -11
- package/dist/lib/sync-engine.d.ts +4 -1
- package/dist/lib/sync-engine.js +9 -1
- package/dist/lib/upgrade.js +14 -4
- package/migrations/README.md +3 -3
- package/package.json +68 -68
- package/source-mappings.yaml +17 -17
|
@@ -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.
|
package/dist/commands/index.js
CHANGED
|
@@ -18,16 +18,17 @@ 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
|
|
25
|
-
upgrade
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
validate
|
|
31
|
-
validate-
|
|
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 Refresh managed assets; refuses when upgrade migrations are pending
|
|
25
|
+
upgrade [--check] [--json]
|
|
26
|
+
Run safe migrations, sync managed assets and doctor
|
|
27
|
+
doctor Diagnose project configuration and drift
|
|
28
|
+
export-context --full|--code|--all [--output <path>] [--check]
|
|
29
|
+
Export a temporary Context summary or code implementation Markdown artifact
|
|
30
|
+
validate <gate> Run a Harness validation gate (Minimal Context only)
|
|
31
|
+
validate-context Validate Minimal Context fact-source recoverability
|
|
32
|
+
validate-harness Compatibility alias for validate-context
|
|
32
33
|
package <subcommand> Maintain package canonical source`);
|
|
33
34
|
}
|
|
@@ -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 +1 @@
|
|
|
1
|
-
export declare function upgrade(): Promise<void>;
|
|
1
|
+
export declare function upgrade(args?: string[]): Promise<void>;
|