ecoportal-api-graphql 1.3.11 → 1.3.13
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.
- checksums.yaml +4 -4
- data/.ai-assistance/code/filter_contract_matrix.md +177 -0
- data/.ai-assistance/projects/compat-layer-audit/COMPAT_AUDIT.md +244 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +10 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +11 -0
- data/.ai-assistance/projects/template-model-logic/CATEGORY_CATALOG.md +236 -0
- data/.ai-assistance/projects/template-model-logic/CLASSIFICATION_SPIKE.md +243 -0
- data/.ai-assistance/projects/template-model-logic/DESIGN_NOTE.md +154 -0
- data/.ai-assistance/scripts/bridge-inbox-check.sh +75 -0
- data/.ai-assistance/skills/procedural-memory/SKILL.md +319 -0
- data/.ai-assistance/skills/project-self-docs/SKILL.md +181 -0
- data/.ai-assistance/skills/project-self-docs/scripts/self_docs_scan.py +378 -0
- data/.ai-assistance/standards-version.json +22 -21
- data/.claude/settings.json +150 -146
- data/.gitlab-ci.yml +45 -0
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +11 -0
- data/docs/self-docs/ARCHITECTURE.md +88 -0
- data/docs/self-docs/CHANGES.jsonl +12 -0
- data/docs/self-docs/COMPLIANCE.md +79 -0
- data/docs/self-docs/CONVENTIONS.md +74 -0
- data/docs/self-docs/INTEGRATIONS.md +65 -0
- data/docs/self-docs/OPERATIONS.md +76 -0
- data/docs/self-docs/OVERVIEW.md +64 -0
- data/docs/self-docs/STATUS.md +73 -0
- data/docs/self-docs/self-docs-index.json +51 -0
- data/docs/worklog.md +15 -20
- data/lib/ecoportal/api/common/graphql/client.rb +2 -0
- data/lib/ecoportal/api/common/graphql/http_client.rb +189 -177
- data/lib/ecoportal/api/common/graphql/model/diffable.rb +54 -54
- data/lib/ecoportal/api/graphql/base/action.rb +43 -43
- data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +67 -67
- data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +7 -0
- data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -28
- data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -25
- data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -24
- data/lib/ecoportal/api/graphql/base/page/section_collection.rb +85 -79
- data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -17
- data/lib/ecoportal/api/graphql/base/register.rb +18 -18
- data/lib/ecoportal/api/graphql/compat/filter_translator.rb +63 -28
- data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -51
- data/lib/ecoportal/api/graphql/concerns.rb +14 -14
- data/lib/ecoportal/api/graphql/file_upload/client.rb +181 -181
- data/lib/ecoportal/api/graphql/fragment/page.rb +85 -85
- data/lib/ecoportal/api/graphql/input/action/update.rb +14 -14
- data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +41 -41
- data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +47 -47
- data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +49 -49
- data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +27 -27
- data/lib/ecoportal/api/graphql/input/search_conf.rb +436 -367
- data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +30 -30
- data/lib/ecoportal/api/graphql/logic/input.rb +26 -26
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- metadata +20 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
self_docs_scan.py -- Project self-docs OBSERVE stage (deterministic, zero-LLM core).
|
|
4
|
+
|
|
5
|
+
Skill (semantic refresh + Act): skills-library/project-self-docs/SKILL.md.
|
|
6
|
+
|
|
7
|
+
Runs FROM A TARGET REPO (the repo documenting itself), not from ep-ai-standards. It does the
|
|
8
|
+
DETERMINISTIC half of self-documentation so no interactive Claude tokens are spent on facts the
|
|
9
|
+
filesystem already knows:
|
|
10
|
+
- detect the repo type using the same signals as skills-library/discovery-manifest.yaml
|
|
11
|
+
- collect a structural fingerprint (top-level dirs, key config files, git head/branch/remote,
|
|
12
|
+
commit-activity clusters, existing .ai-assistance/code specs, CI files, doc files)
|
|
13
|
+
- compute a stable content hash per self-doc so refreshes are idempotent and the changelog
|
|
14
|
+
only records REAL changes
|
|
15
|
+
- emit a structured JSON "observation" the skill consumes to fill the self-doc set
|
|
16
|
+
|
|
17
|
+
It does NOT write project prose, summarise purpose, or judge architecture -- that semantic work is
|
|
18
|
+
the LLM skill's job (batched to Gemini per the token-frugality rule), gated by human review before
|
|
19
|
+
any egress. This scanner is the thermometer; the skill is the (human-gated) actuator.
|
|
20
|
+
|
|
21
|
+
HARD CONSTRAINTS:
|
|
22
|
+
- READ-ONLY by default. It never writes into the target repo except the self-doc index/changelog
|
|
23
|
+
under --write, and even then only to the self-docs directory (default docs/self-docs/).
|
|
24
|
+
- NO absolute paths are emitted into any committed artefact: paths are repo-relative.
|
|
25
|
+
- NO Confluence / network egress. Publish target is UNDECIDED (see SKILL.md); this script only
|
|
26
|
+
ever touches the local filesystem.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
python self_docs_scan.py # human summary of the observation (target = cwd)
|
|
30
|
+
python self_docs_scan.py --json # structured observation to stdout
|
|
31
|
+
python self_docs_scan.py --target <path> # observe a different LOCAL repo root (convenience
|
|
32
|
+
# only -- NEVER to generate another repo's self-docs
|
|
33
|
+
# from here; run the skill in that repo's own session)
|
|
34
|
+
python self_docs_scan.py --out docs/self-docs # self-docs dir (default docs/self-docs)
|
|
35
|
+
python self_docs_scan.py --write # (re)write the machine index + changelog only
|
|
36
|
+
"""
|
|
37
|
+
import argparse
|
|
38
|
+
import hashlib
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
import subprocess
|
|
43
|
+
import sys
|
|
44
|
+
from datetime import datetime, timezone
|
|
45
|
+
|
|
46
|
+
SCHEMA_VERSION = "1.1"
|
|
47
|
+
|
|
48
|
+
# Self-doc set: stable filenames the skill fills in. Keeping this list here (not only in the
|
|
49
|
+
# template) lets the deterministic scanner report which docs already exist and which are missing,
|
|
50
|
+
# and lets the index/changelog be generated without the LLM.
|
|
51
|
+
SELF_DOC_SET = [
|
|
52
|
+
("OVERVIEW.md", "What the project is, who it serves, current priority, discovery & access"),
|
|
53
|
+
("ARCHITECTURE.md", "Top-level structure, key components, how they fit"),
|
|
54
|
+
("CONVENTIONS.md", "Coding style, branch naming, commit style, test conventions"),
|
|
55
|
+
("INTEGRATIONS.md", "External services + data + permissions, upstream/downstream repos"),
|
|
56
|
+
("STATUS.md", "Active work, migration state, known risks, open questions"),
|
|
57
|
+
("COMPLIANCE.md", "ISO-27001/audit, data classes + PII, vendors, AI content, leak controls"),
|
|
58
|
+
("OPERATIONS.md", "Infrastructure, public exposure + Cloudflare, DevOps audit, usage KPIs"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Detection signals mirror skills-library/discovery-manifest.yaml. Order matters: first match wins.
|
|
62
|
+
# (file, ...) => all must exist; file_any => any of the globs; not_file => must be absent.
|
|
63
|
+
REPO_TYPE_RULES = [
|
|
64
|
+
("aws-cdk", {"file": ["cdk.json"]}),
|
|
65
|
+
("browser-extension", {"file": ["manifest.json"], "file_any": ["package.json", "Gruntfile.js", "webpack.config.js"]}),
|
|
66
|
+
("ruby-gem", {"file_any": ["*.gemspec"]}),
|
|
67
|
+
("rails-app", {"file": ["config/application.rb"]}),
|
|
68
|
+
("ruby-scripts", {"file": ["Gemfile"], "not_file": ["*.gemspec", "config/application.rb"]}),
|
|
69
|
+
("typescript-lib", {"file": ["package.json"], "not_file": ["cdk.json", "manifest.json"]}),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
KEY_CONFIG_FILES = [
|
|
73
|
+
"package.json", "Gemfile", "cdk.json", "manifest.json", "go.mod", "Cargo.toml",
|
|
74
|
+
"pyproject.toml", "requirements.txt", "tsconfig.json", ".eslintrc", ".eslintrc.json",
|
|
75
|
+
".eslintrc.js", ".prettierrc", ".rubocop.yml", ".editorconfig",
|
|
76
|
+
]
|
|
77
|
+
CI_FILES = [".gitlab-ci.yml", "Jenkinsfile", "Makefile"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _iso_now():
|
|
81
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _exists(root, rel):
|
|
85
|
+
return os.path.exists(os.path.join(root, rel))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _glob_any(root, patterns):
|
|
89
|
+
import fnmatch
|
|
90
|
+
try:
|
|
91
|
+
top = os.listdir(root)
|
|
92
|
+
except OSError:
|
|
93
|
+
return False
|
|
94
|
+
for pat in patterns:
|
|
95
|
+
if any(fnmatch.fnmatch(name, pat) for name in top):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_repo_type(root):
|
|
101
|
+
for type_name, rule in REPO_TYPE_RULES:
|
|
102
|
+
ok = True
|
|
103
|
+
for f in rule.get("file", []):
|
|
104
|
+
if not _exists(root, f):
|
|
105
|
+
ok = False
|
|
106
|
+
break
|
|
107
|
+
if ok and rule.get("file_any"):
|
|
108
|
+
ok = _glob_any(root, rule["file_any"])
|
|
109
|
+
if ok and rule.get("not_file"):
|
|
110
|
+
for f in rule["not_file"]:
|
|
111
|
+
if "*" in f:
|
|
112
|
+
if _glob_any(root, [f]):
|
|
113
|
+
ok = False
|
|
114
|
+
break
|
|
115
|
+
elif _exists(root, f):
|
|
116
|
+
ok = False
|
|
117
|
+
break
|
|
118
|
+
if ok:
|
|
119
|
+
return type_name
|
|
120
|
+
return "unknown"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _git(root, args):
|
|
124
|
+
try:
|
|
125
|
+
out = subprocess.run(
|
|
126
|
+
["git", "-C", root] + args,
|
|
127
|
+
capture_output=True, text=True, timeout=15,
|
|
128
|
+
)
|
|
129
|
+
return out.stdout.strip() if out.returncode == 0 else ""
|
|
130
|
+
except (OSError, subprocess.SubprocessError):
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def git_facts(root):
|
|
135
|
+
branch = _git(root, ["rev-parse", "--abbrev-ref", "HEAD"])
|
|
136
|
+
head = _git(root, ["rev-parse", "--short", "HEAD"])
|
|
137
|
+
remote = _git(root, ["config", "--get", "remote.origin.url"])
|
|
138
|
+
# Real repo name from the remote, never a local folder name that may be renamed.
|
|
139
|
+
repo_name = ""
|
|
140
|
+
if remote and "/" in remote:
|
|
141
|
+
repo_name = re.sub(r"\.git$", "", remote.rstrip("/").split("/")[-1])
|
|
142
|
+
# Activity clusters: which top-level dirs churn most in the last 50 commits.
|
|
143
|
+
log = _git(root, ["log", "-50", "--name-only", "--pretty=format:"])
|
|
144
|
+
clusters = {}
|
|
145
|
+
for line in log.splitlines():
|
|
146
|
+
line = line.strip()
|
|
147
|
+
if not line:
|
|
148
|
+
continue
|
|
149
|
+
top = line.split("/")[0] if "/" in line else line
|
|
150
|
+
clusters[top] = clusters.get(top, 0) + 1
|
|
151
|
+
top_clusters = sorted(clusters.items(), key=lambda kv: kv[1], reverse=True)[:6]
|
|
152
|
+
return {
|
|
153
|
+
"branch": branch,
|
|
154
|
+
"head": head,
|
|
155
|
+
"repo_name": repo_name,
|
|
156
|
+
"has_remote": bool(remote),
|
|
157
|
+
"activity_clusters": [{"path": k, "touches": v} for k, v in top_clusters],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def structure(root):
|
|
162
|
+
dirs, files = [], []
|
|
163
|
+
try:
|
|
164
|
+
for name in sorted(os.listdir(root)):
|
|
165
|
+
if name.startswith(".git"):
|
|
166
|
+
continue
|
|
167
|
+
full = os.path.join(root, name)
|
|
168
|
+
if os.path.isdir(full):
|
|
169
|
+
dirs.append(name)
|
|
170
|
+
else:
|
|
171
|
+
files.append(name)
|
|
172
|
+
except OSError:
|
|
173
|
+
pass
|
|
174
|
+
return {
|
|
175
|
+
"top_level_dirs": dirs,
|
|
176
|
+
"key_config_files": [f for f in KEY_CONFIG_FILES if _exists(root, f)],
|
|
177
|
+
"ci_files": [f for f in CI_FILES if _exists(root, f)],
|
|
178
|
+
"has_readme": _exists(root, "README.md") or _exists(root, "README"),
|
|
179
|
+
"has_changelog": _exists(root, "CHANGELOG.md"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def existing_context(root):
|
|
184
|
+
"""Reusable inputs that already exist -- the skill composes from these, not from scratch."""
|
|
185
|
+
ctx = {"code_specs": [], "repo_context": None, "conventions": None}
|
|
186
|
+
code_dir = os.path.join(root, ".ai-assistance", "code")
|
|
187
|
+
if os.path.isdir(code_dir):
|
|
188
|
+
for dirpath, _, filenames in os.walk(code_dir):
|
|
189
|
+
for fn in filenames:
|
|
190
|
+
if fn.endswith(".md"):
|
|
191
|
+
rel = os.path.relpath(os.path.join(dirpath, fn), root).replace("\\", "/")
|
|
192
|
+
ctx["code_specs"].append(rel)
|
|
193
|
+
ctx["code_specs"].sort()
|
|
194
|
+
local = os.path.join(root, ".ai-assistance", "local")
|
|
195
|
+
if os.path.isfile(os.path.join(local, "repo-context.md")):
|
|
196
|
+
ctx["repo_context"] = ".ai-assistance/local/repo-context.md"
|
|
197
|
+
if os.path.isfile(os.path.join(local, "conventions.md")):
|
|
198
|
+
ctx["conventions"] = ".ai-assistance/local/conventions.md"
|
|
199
|
+
return ctx
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _content_hash(path):
|
|
203
|
+
"""Hash the self-doc BODY (below the frontmatter) so index/changelog record real changes,
|
|
204
|
+
not just a re-run timestamp. Missing file -> None."""
|
|
205
|
+
if not os.path.isfile(path):
|
|
206
|
+
return None
|
|
207
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
208
|
+
text = fh.read()
|
|
209
|
+
body = text
|
|
210
|
+
if text.startswith("---"):
|
|
211
|
+
end = text.find("\n---", 3)
|
|
212
|
+
if end != -1:
|
|
213
|
+
body = text[end + 4:]
|
|
214
|
+
# Normalise line endings + trailing whitespace so cross-platform re-runs are stable.
|
|
215
|
+
norm = "\n".join(line.rstrip() for line in body.replace("\r\n", "\n").split("\n")).strip()
|
|
216
|
+
return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def self_doc_status(root, out_rel):
|
|
220
|
+
out_dir = os.path.join(root, out_rel)
|
|
221
|
+
docs = []
|
|
222
|
+
for fname, purpose in SELF_DOC_SET:
|
|
223
|
+
path = os.path.join(out_dir, fname)
|
|
224
|
+
docs.append({
|
|
225
|
+
"file": f"{out_rel}/{fname}".replace("\\", "/"),
|
|
226
|
+
"purpose": purpose,
|
|
227
|
+
"exists": os.path.isfile(path),
|
|
228
|
+
"content_hash": _content_hash(path),
|
|
229
|
+
})
|
|
230
|
+
return docs
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def observe(root, out_rel):
|
|
234
|
+
return {
|
|
235
|
+
"schema_version": SCHEMA_VERSION,
|
|
236
|
+
"observed_at": _iso_now(),
|
|
237
|
+
"repo_type": detect_repo_type(root),
|
|
238
|
+
"git": git_facts(root),
|
|
239
|
+
"structure": structure(root),
|
|
240
|
+
"existing_context": existing_context(root),
|
|
241
|
+
"self_doc_set": self_doc_status(root, out_rel),
|
|
242
|
+
"out_dir": out_rel.replace("\\", "/"),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# == index + changelog (deterministic writers) ======================================
|
|
247
|
+
|
|
248
|
+
def _read_json(path, default):
|
|
249
|
+
if not os.path.isfile(path):
|
|
250
|
+
return default
|
|
251
|
+
try:
|
|
252
|
+
with open(path, encoding="utf-8") as fh:
|
|
253
|
+
return json.load(fh)
|
|
254
|
+
except (OSError, ValueError):
|
|
255
|
+
return default
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def write_index_and_changelog(root, out_rel, obs):
|
|
259
|
+
"""Rewrite the machine-readable index and append changelog deltas -- deterministically.
|
|
260
|
+
|
|
261
|
+
The index (self-docs-index.json) is the machine surface a central hub consumes: schema
|
|
262
|
+
version, repo identity, and a hash per doc. The changelog (CHANGES.jsonl) appends one line
|
|
263
|
+
per doc whose content hash CHANGED since the previous index -- so the hub sees real deltas,
|
|
264
|
+
and a no-op re-run appends nothing (idempotent)."""
|
|
265
|
+
out_dir = os.path.join(root, out_rel)
|
|
266
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
267
|
+
index_path = os.path.join(out_dir, "self-docs-index.json")
|
|
268
|
+
changes_path = os.path.join(out_dir, "CHANGES.jsonl")
|
|
269
|
+
|
|
270
|
+
prev = _read_json(index_path, {})
|
|
271
|
+
prev_hashes = {d["file"]: d.get("content_hash") for d in prev.get("docs", [])}
|
|
272
|
+
|
|
273
|
+
docs = obs["self_doc_set"]
|
|
274
|
+
now = obs["observed_at"]
|
|
275
|
+
deltas = []
|
|
276
|
+
for d in docs:
|
|
277
|
+
new_h = d["content_hash"]
|
|
278
|
+
old_h = prev_hashes.get(d["file"])
|
|
279
|
+
if new_h != old_h:
|
|
280
|
+
deltas.append({
|
|
281
|
+
"ts": now,
|
|
282
|
+
"file": d["file"],
|
|
283
|
+
"change": ("created" if old_h is None and new_h is not None
|
|
284
|
+
else "removed" if new_h is None
|
|
285
|
+
else "updated"),
|
|
286
|
+
"from_hash": old_h,
|
|
287
|
+
"to_hash": new_h,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
index = {
|
|
291
|
+
"schema_version": SCHEMA_VERSION,
|
|
292
|
+
"generated_at": now,
|
|
293
|
+
"repo_name": obs["git"].get("repo_name") or "",
|
|
294
|
+
"repo_type": obs["repo_type"],
|
|
295
|
+
"head": obs["git"].get("head", ""),
|
|
296
|
+
"docs": [
|
|
297
|
+
{"file": d["file"], "purpose": d["purpose"],
|
|
298
|
+
"exists": d["exists"], "content_hash": d["content_hash"]}
|
|
299
|
+
for d in docs
|
|
300
|
+
],
|
|
301
|
+
}
|
|
302
|
+
with open(index_path, "w", encoding="utf-8") as fh:
|
|
303
|
+
json.dump(index, fh, indent=2)
|
|
304
|
+
fh.write("\n")
|
|
305
|
+
|
|
306
|
+
if deltas:
|
|
307
|
+
with open(changes_path, "a", encoding="utf-8") as fh:
|
|
308
|
+
for delta in deltas:
|
|
309
|
+
fh.write(json.dumps(delta) + "\n")
|
|
310
|
+
|
|
311
|
+
return {"index": f"{out_rel}/self-docs-index.json".replace("\\", "/"),
|
|
312
|
+
"changelog": f"{out_rel}/CHANGES.jsonl".replace("\\", "/"),
|
|
313
|
+
"deltas_appended": len(deltas)}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _human(obs):
|
|
317
|
+
g = obs["git"]
|
|
318
|
+
print(f"self-docs observation (schema {obs['schema_version']})")
|
|
319
|
+
print(f" repo_type : {obs['repo_type']}")
|
|
320
|
+
print(f" repo_name : {g.get('repo_name') or '(no remote)'} @ {g.get('head') or '?'} "
|
|
321
|
+
f"on {g.get('branch') or '?'}")
|
|
322
|
+
print(f" out_dir : {obs['out_dir']}")
|
|
323
|
+
print(" top-level dirs:", ", ".join(obs["structure"]["top_level_dirs"]) or "(none)")
|
|
324
|
+
if obs["structure"]["key_config_files"]:
|
|
325
|
+
print(" config :", ", ".join(obs["structure"]["key_config_files"]))
|
|
326
|
+
if g.get("activity_clusters"):
|
|
327
|
+
hot = ", ".join(f"{c['path']}({c['touches']})" for c in g["activity_clusters"])
|
|
328
|
+
print(" hot paths :", hot)
|
|
329
|
+
ec = obs["existing_context"]
|
|
330
|
+
reuse = []
|
|
331
|
+
if ec["repo_context"]:
|
|
332
|
+
reuse.append("ai-discovery repo-context.md")
|
|
333
|
+
if ec["conventions"]:
|
|
334
|
+
reuse.append("ai-discovery conventions.md")
|
|
335
|
+
if ec["code_specs"]:
|
|
336
|
+
reuse.append(f"{len(ec['code_specs'])} code-specs")
|
|
337
|
+
print(" reuse from:", ", ".join(reuse) or "(none -- run ai-discovery/code-specs first)")
|
|
338
|
+
print(" self-doc set:")
|
|
339
|
+
for d in obs["self_doc_set"]:
|
|
340
|
+
state = "present" if d["exists"] else "MISSING"
|
|
341
|
+
print(f" [{state:7}] {d['file']} ({d['content_hash'] or '-'})")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def main():
|
|
345
|
+
ap = argparse.ArgumentParser(description="Deterministic project self-docs observation")
|
|
346
|
+
ap.add_argument("--target", default=".",
|
|
347
|
+
help="LOCAL repo root to observe (default: cwd). Convenience only -- do NOT use to "
|
|
348
|
+
"generate ANOTHER repo's self-docs from here; run the skill in that repo's own "
|
|
349
|
+
"session (self-docs are owned + generated by the repo that documents itself).")
|
|
350
|
+
ap.add_argument("--out", default="docs/self-docs", help="self-docs dir (default: docs/self-docs)")
|
|
351
|
+
ap.add_argument("--json", action="store_true", help="emit the structured observation")
|
|
352
|
+
ap.add_argument("--write", action="store_true",
|
|
353
|
+
help="(re)write the machine index + changelog deltas only")
|
|
354
|
+
args = ap.parse_args()
|
|
355
|
+
|
|
356
|
+
root = os.path.abspath(args.target)
|
|
357
|
+
if not os.path.isdir(root):
|
|
358
|
+
print(f"[self-docs] target not a directory: {root}", file=sys.stderr)
|
|
359
|
+
return 2
|
|
360
|
+
|
|
361
|
+
obs = observe(root, args.out)
|
|
362
|
+
|
|
363
|
+
if args.write:
|
|
364
|
+
result = write_index_and_changelog(root, args.out, obs)
|
|
365
|
+
obs["_write_result"] = result
|
|
366
|
+
|
|
367
|
+
if args.json:
|
|
368
|
+
print(json.dumps(obs, indent=2))
|
|
369
|
+
else:
|
|
370
|
+
_human(obs)
|
|
371
|
+
if args.write:
|
|
372
|
+
r = obs["_write_result"]
|
|
373
|
+
print(f" wrote index -> {r['index']}; changelog deltas appended: {r['deltas_appended']}")
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
sys.exit(main())
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ep-ai-standards-version": "1.5.0",
|
|
3
|
-
"applied-at": "2026-06-13",
|
|
4
|
-
"project-type": [
|
|
5
|
-
"ruby"
|
|
6
|
-
],
|
|
7
|
-
"deferred": [],
|
|
8
|
-
"installed-components": {
|
|
9
|
-
"skills/
|
|
10
|
-
"skills/
|
|
11
|
-
"skills/
|
|
12
|
-
"skills/
|
|
13
|
-
"skills/ai-
|
|
14
|
-
"skills/
|
|
15
|
-
"skills/
|
|
16
|
-
"skills/
|
|
17
|
-
"skills/
|
|
18
|
-
"skills/
|
|
19
|
-
"skills/
|
|
20
|
-
|
|
21
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"ep-ai-standards-version": "1.5.0",
|
|
3
|
+
"applied-at": "2026-06-13",
|
|
4
|
+
"project-type": [
|
|
5
|
+
"ruby"
|
|
6
|
+
],
|
|
7
|
+
"deferred": [],
|
|
8
|
+
"installed-components": {
|
|
9
|
+
"skills/ai-instructions": "0.0.0",
|
|
10
|
+
"skills/code-specs": "0.0.0",
|
|
11
|
+
"skills/corporate-policies": "1.0.0",
|
|
12
|
+
"skills/dep-graph": "0.0.0",
|
|
13
|
+
"skills/ep-ai-manager": "2.2.0",
|
|
14
|
+
"skills/gemini-assist": "0.0.0",
|
|
15
|
+
"skills/graphql-schema-analysis": "0.0.0",
|
|
16
|
+
"skills/project-cycle": "0.0.0",
|
|
17
|
+
"skills/project-self-docs": "0.2.0",
|
|
18
|
+
"skills/refactor": "0.0.0",
|
|
19
|
+
"skills/ruby-scripting": "0.1.0",
|
|
20
|
+
"skills/spec-generation": "0.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|