graphitect 0.2.0__py3-none-any.whl
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.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- graphitect-0.2.0.dist-info/top_level.txt +2 -0
graphify/wiki.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Wiki export - Wikipedia-style markdown articles from the knowledge graph
|
|
2
|
+
# Generates an agent-crawlable wiki: index.md + one article per community + god node articles
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
import networkx as nx
|
|
8
|
+
|
|
9
|
+
from graphify.build import edge_data
|
|
10
|
+
from graphify.paths import stem_filename_budget
|
|
11
|
+
|
|
12
|
+
# Room _unique_slug needs for the collision suffix ("_2" … "_9999") it appends
|
|
13
|
+
# after _safe_filename has already capped the slug. The suffix is technically
|
|
14
|
+
# unbounded, but 5 chars ("_" + 4 digits) covers ~10k identical stems, far past
|
|
15
|
+
# anything real; sizing it to 3 digits let a 1000th collision overrun MAX_PATH.
|
|
16
|
+
_SLUG_SUFFIX_RESERVE = 5
|
|
17
|
+
|
|
18
|
+
# Characters a slug may not contain, because the article's LINK and its ON-DISK
|
|
19
|
+
# NAME have to be the same string (#2597). Anything left here must be legal,
|
|
20
|
+
# unescaped, in a CommonMark link destination:
|
|
21
|
+
# < > : " / \ | ? * Windows-reserved in filenames (pre-existing set)
|
|
22
|
+
# ( ) parentheses delimit/nest a link destination
|
|
23
|
+
# # starts a fragment, so `a#b.md` resolves to the file `a`
|
|
24
|
+
# % reads as the start of a percent-escape
|
|
25
|
+
# control chars forbidden in a link destination, hostile in a filename
|
|
26
|
+
#
|
|
27
|
+
# Non-ASCII is deliberately NOT stripped. It is legal raw in a link destination
|
|
28
|
+
# and resolves fine on every filesystem graphify targets; stripping it would
|
|
29
|
+
# reduce a CJK, Cyrillic or accented wiki to a wall of underscores.
|
|
30
|
+
_UNSAFE_SLUG_CHARS = re.compile(r'[<>:"/\\|?*#%\x00-\x1f\x7f]')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _safe_filename(name: str, limit: int = 200) -> str:
|
|
34
|
+
"""Make a label safe for use as a filename across platforms AND as a
|
|
35
|
+
markdown link destination.
|
|
36
|
+
|
|
37
|
+
Substitutes characters that Windows reserves in filenames
|
|
38
|
+
(< > : " / \\ | ? *) plus the ones that would make the emitted link stop
|
|
39
|
+
matching the file on disk, and strips trailing dots/spaces, also reserved.
|
|
40
|
+
Falls back to 'unnamed' for empty results and caps length at ``limit``
|
|
41
|
+
chars (default 200) to stay well under common filesystem limits; ``to_wiki``
|
|
42
|
+
lowers ``limit`` when the wiki directory leaves less than that inside
|
|
43
|
+
Windows' MAX_PATH window (#2655).
|
|
44
|
+
|
|
45
|
+
Parentheses are DROPPED rather than substituted: every callable node is
|
|
46
|
+
labelled ``foo()``, and substituting would leave a trailing ``foo__`` on
|
|
47
|
+
each of them — and would mangle Python dunders (``__init__()`` ->
|
|
48
|
+
``_init_``) if the resulting runs were then collapsed. Dropping keeps
|
|
49
|
+
``__init__`` intact. Two labels that collapse to one slug are still
|
|
50
|
+
separated by ``_unique_slug``.
|
|
51
|
+
"""
|
|
52
|
+
s = name.replace("/", "-").replace(" ", "_").replace(":", "-")
|
|
53
|
+
s = s.replace("(", "").replace(")", "")
|
|
54
|
+
s = _UNSAFE_SLUG_CHARS.sub('_', s)
|
|
55
|
+
s = s.strip('. ')
|
|
56
|
+
return s[:limit] if s else 'unnamed'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _md_link(label: str, resolver: dict[str, str]) -> str:
|
|
60
|
+
"""Render a link to another wiki article as a portable relative markdown link.
|
|
61
|
+
|
|
62
|
+
``resolver`` maps an article's display label to the slug (filename stem) it
|
|
63
|
+
was written under. When the label has an article, emit a standard
|
|
64
|
+
``[label](slug.md)`` link whose target is the on-disk name VERBATIM.
|
|
65
|
+
|
|
66
|
+
The target is deliberately not percent-encoded (#2597). ``quote()`` turned
|
|
67
|
+
``_make_id().md`` into ``_make_id%28%29.md`` while the file stayed raw, so
|
|
68
|
+
the link pointed at a path that does not exist. Renderers hid it by
|
|
69
|
+
decoding before resolving, but the wiki's whole purpose is to be
|
|
70
|
+
agent-crawlable, and an agent that reads the target off disk verbatim got a
|
|
71
|
+
FileNotFoundError. ``_safe_filename`` now keeps the slug free of everything
|
|
72
|
+
that would need encoding, so raw emission and the filename are the same
|
|
73
|
+
string by construction — one source of truth instead of two spellings that
|
|
74
|
+
happened to agree only for URL-safe labels.
|
|
75
|
+
|
|
76
|
+
The old ``[[label]]`` form only resolved inside Obsidian, because the
|
|
77
|
+
on-disk filename differs from the label — _safe_filename turns spaces into
|
|
78
|
+
underscores and substitutes reserved characters — so e.g.
|
|
79
|
+
``[[Domain Data Models]]`` pointed at a non-existent
|
|
80
|
+
``Domain Data Models.md`` everywhere else.
|
|
81
|
+
|
|
82
|
+
Labels with no article — most node-level links, since only communities and
|
|
83
|
+
god nodes get article files — render as plain text instead of a dead link
|
|
84
|
+
that points nowhere even inside Obsidian.
|
|
85
|
+
"""
|
|
86
|
+
text = label.replace("[", r"\[").replace("]", r"\]")
|
|
87
|
+
slug = resolver.get(label)
|
|
88
|
+
if slug is None:
|
|
89
|
+
return text
|
|
90
|
+
return f"[{text}]({slug}.md)"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cross_community_links(G: nx.Graph, nodes: list[str], own_cid: int, labels: dict[int, str], node_community: dict[str, int]) -> list[tuple[str, int]]:
|
|
94
|
+
"""Return (community_label, edge_count) pairs for cross-community connections, sorted descending."""
|
|
95
|
+
counts: dict[str, int] = Counter()
|
|
96
|
+
for nid in nodes:
|
|
97
|
+
for neighbor in G.neighbors(nid):
|
|
98
|
+
ncid = node_community.get(neighbor)
|
|
99
|
+
if ncid is not None and ncid != own_cid:
|
|
100
|
+
counts[labels.get(ncid, f"Community {ncid}")] += 1
|
|
101
|
+
return sorted(counts.items(), key=lambda x: -x[1])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _community_article(
|
|
105
|
+
G: nx.Graph,
|
|
106
|
+
cid: int,
|
|
107
|
+
nodes: list[str],
|
|
108
|
+
label: str,
|
|
109
|
+
labels: dict[int, str],
|
|
110
|
+
cohesion: float | None,
|
|
111
|
+
node_community: dict[str, int] | None = None,
|
|
112
|
+
resolver: dict[str, str] | None = None,
|
|
113
|
+
) -> str:
|
|
114
|
+
resolver = resolver or {}
|
|
115
|
+
top_nodes = sorted(nodes, key=lambda n: G.degree(n), reverse=True)[:25]
|
|
116
|
+
cross = _cross_community_links(G, nodes, cid, labels, node_community or {})
|
|
117
|
+
|
|
118
|
+
# Edge confidence breakdown, over every edge INCIDENT to the community (the
|
|
119
|
+
# cross-community ones included — those are disproportionately the uncertain
|
|
120
|
+
# edges, and AMBIGUOUS is what ARCHITECTURE.md flags for human review).
|
|
121
|
+
#
|
|
122
|
+
# ``G.edges(nbunch)`` reports each incident edge exactly once. Walking
|
|
123
|
+
# ``nodes x G.neighbors`` instead visited an edge once per endpoint inside
|
|
124
|
+
# the community, so an intra-community edge was counted TWICE while a
|
|
125
|
+
# crossing one was counted once. Intra-community edges are overwhelmingly
|
|
126
|
+
# the high-confidence EXTRACTED ones — that is what makes a community — so
|
|
127
|
+
# the split was biased towards confidence and understated the review
|
|
128
|
+
# burden. On a MultiGraph this also counts parallel edges individually
|
|
129
|
+
# rather than collapsing them to the first (``edge_data``), which is the
|
|
130
|
+
# same understatement: an AMBIGUOUS edge parallel to an EXTRACTED one used
|
|
131
|
+
# to be invisible here.
|
|
132
|
+
conf_counts: Counter = Counter(
|
|
133
|
+
d.get("confidence", "EXTRACTED") for *_, d in G.edges(nodes, data=True)
|
|
134
|
+
)
|
|
135
|
+
total_edges = sum(conf_counts.values()) or 1
|
|
136
|
+
|
|
137
|
+
sources = sorted({G.nodes[n].get("source_file") or "" for n in nodes} - {""})
|
|
138
|
+
|
|
139
|
+
lines: list[str] = []
|
|
140
|
+
lines += [f"# {label}", ""]
|
|
141
|
+
|
|
142
|
+
meta_parts = [f"{len(nodes)} nodes"]
|
|
143
|
+
if cohesion is not None:
|
|
144
|
+
meta_parts.append(f"cohesion {cohesion:.2f}")
|
|
145
|
+
lines += [f"> {' · '.join(meta_parts)}", ""]
|
|
146
|
+
|
|
147
|
+
lines += ["## Key Concepts", ""]
|
|
148
|
+
for nid in top_nodes:
|
|
149
|
+
d = G.nodes[nid]
|
|
150
|
+
node_label = d.get("label", nid)
|
|
151
|
+
src = d.get("source_file", "")
|
|
152
|
+
degree = G.degree(nid)
|
|
153
|
+
src_str = f" — `{src}`" if src else ""
|
|
154
|
+
lines.append(f"- **{node_label}** ({degree} connections){src_str}")
|
|
155
|
+
remaining = len(nodes) - len(top_nodes)
|
|
156
|
+
if remaining > 0:
|
|
157
|
+
lines.append(f"- *... and {remaining} more nodes in this community*")
|
|
158
|
+
lines.append("")
|
|
159
|
+
|
|
160
|
+
lines += ["## Relationships", ""]
|
|
161
|
+
if cross:
|
|
162
|
+
for other_label, count in cross[:12]:
|
|
163
|
+
lines.append(f"- {_md_link(other_label, resolver)} ({count} shared connections)")
|
|
164
|
+
else:
|
|
165
|
+
lines.append("- No strong cross-community connections detected")
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
if sources:
|
|
169
|
+
lines += ["## Source Files", ""]
|
|
170
|
+
for src in sources[:20]:
|
|
171
|
+
lines.append(f"- `{src}`")
|
|
172
|
+
lines.append("")
|
|
173
|
+
|
|
174
|
+
lines += ["## Audit Trail", ""]
|
|
175
|
+
for conf in ("EXTRACTED", "INFERRED", "AMBIGUOUS"):
|
|
176
|
+
n = conf_counts.get(conf, 0)
|
|
177
|
+
pct = round(n / total_edges * 100)
|
|
178
|
+
lines.append(f"- {conf}: {n} ({pct}%)")
|
|
179
|
+
lines.append("")
|
|
180
|
+
|
|
181
|
+
lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_md_link('index', resolver)} to navigate.*"]
|
|
182
|
+
return "\n".join(lines)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _god_node_article(G: nx.Graph, nid: str, labels: dict[int, str], node_community: dict[str, int] | None = None, resolver: dict[str, str] | None = None) -> str:
|
|
186
|
+
resolver = resolver or {}
|
|
187
|
+
d = G.nodes[nid]
|
|
188
|
+
node_label = d.get("label", nid)
|
|
189
|
+
src = d.get("source_file", "")
|
|
190
|
+
cid = (node_community or {}).get(nid)
|
|
191
|
+
community_name = labels.get(cid, f"Community {cid}") if cid is not None else None
|
|
192
|
+
|
|
193
|
+
lines: list[str] = []
|
|
194
|
+
lines += [f"# {node_label}", ""]
|
|
195
|
+
lines += [f"> God node · {G.degree(nid)} connections · `{src}`", ""]
|
|
196
|
+
|
|
197
|
+
if community_name:
|
|
198
|
+
lines += [f"**Community:** {_md_link(community_name, resolver)}", ""]
|
|
199
|
+
|
|
200
|
+
# Group neighbors by relation type
|
|
201
|
+
by_relation: dict[str, list[str]] = {}
|
|
202
|
+
for neighbor in sorted(G.neighbors(nid), key=lambda n: G.degree(n), reverse=True):
|
|
203
|
+
nd = G.nodes[neighbor]
|
|
204
|
+
ed = edge_data(G, nid, neighbor)
|
|
205
|
+
rel = ed.get("relation", "related")
|
|
206
|
+
neighbor_label = nd.get("label", neighbor)
|
|
207
|
+
conf = ed.get("confidence", "")
|
|
208
|
+
conf_str = f" `{conf}`" if conf else ""
|
|
209
|
+
by_relation.setdefault(rel, []).append(f"{_md_link(neighbor_label, resolver)}{conf_str}")
|
|
210
|
+
|
|
211
|
+
lines += ["## Connections by Relation", ""]
|
|
212
|
+
for rel, targets in sorted(by_relation.items()):
|
|
213
|
+
lines.append(f"### {rel}")
|
|
214
|
+
for t in targets[:20]:
|
|
215
|
+
lines.append(f"- {t}")
|
|
216
|
+
# The cap keeps god-node articles readable, but silently dropping the
|
|
217
|
+
# tail made the body disagree with the header's degree count with
|
|
218
|
+
# nothing telling the reader anything was cut (#3127). Entries are
|
|
219
|
+
# degree-sorted, so what is hidden is the low-degree tail.
|
|
220
|
+
if len(targets) > 20:
|
|
221
|
+
lines.append(
|
|
222
|
+
f"- *…and {len(targets) - 20} more `{rel}` "
|
|
223
|
+
f"connection(s) not listed (lowest-degree first to go)*"
|
|
224
|
+
)
|
|
225
|
+
lines.append("")
|
|
226
|
+
|
|
227
|
+
lines += ["---", "", f"*Part of the graphify knowledge wiki. See {_md_link('index', resolver)} to navigate.*"]
|
|
228
|
+
return "\n".join(lines)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _index_md(
|
|
232
|
+
communities: dict[int, list[str]],
|
|
233
|
+
labels: dict[int, str],
|
|
234
|
+
god_nodes_data: list[dict],
|
|
235
|
+
total_nodes: int,
|
|
236
|
+
total_edges: int,
|
|
237
|
+
resolver: dict[str, str] | None = None,
|
|
238
|
+
) -> str:
|
|
239
|
+
resolver = resolver or {}
|
|
240
|
+
lines: list[str] = [
|
|
241
|
+
"# Knowledge Graph Index",
|
|
242
|
+
"",
|
|
243
|
+
"> Auto-generated by graphify. Start here — read community articles for context, then drill into god nodes for detail.",
|
|
244
|
+
"",
|
|
245
|
+
f"**{total_nodes} nodes · {total_edges} edges · {len(communities)} communities**",
|
|
246
|
+
"",
|
|
247
|
+
"---",
|
|
248
|
+
"",
|
|
249
|
+
"## Communities",
|
|
250
|
+
"(sorted by size, largest first)",
|
|
251
|
+
"",
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for cid, nodes in sorted(communities.items(), key=lambda x: -len(x[1])):
|
|
255
|
+
label = labels.get(cid, f"Community {cid}")
|
|
256
|
+
lines.append(f"- {_md_link(label, resolver)} — {len(nodes)} nodes")
|
|
257
|
+
lines.append("")
|
|
258
|
+
|
|
259
|
+
if god_nodes_data:
|
|
260
|
+
lines += ["## God Nodes", "(most connected concepts — the load-bearing abstractions)", ""]
|
|
261
|
+
for node in god_nodes_data:
|
|
262
|
+
lines.append(f"- {_md_link(node['label'], resolver)} — {node['degree']} connections")
|
|
263
|
+
lines.append("")
|
|
264
|
+
|
|
265
|
+
lines += [
|
|
266
|
+
"---",
|
|
267
|
+
"",
|
|
268
|
+
"*Generated by [graphify](https://github.com/safishamsi/graphify)*",
|
|
269
|
+
]
|
|
270
|
+
return "\n".join(lines)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def to_wiki(
|
|
274
|
+
G: nx.Graph,
|
|
275
|
+
communities: dict[int, list[str]],
|
|
276
|
+
output_dir: str | Path,
|
|
277
|
+
community_labels: dict[int, str] | None = None,
|
|
278
|
+
cohesion: dict[int, float] | None = None,
|
|
279
|
+
god_nodes_data: list[dict] | None = None,
|
|
280
|
+
) -> int:
|
|
281
|
+
"""Generate a Wikipedia-style wiki from the graph.
|
|
282
|
+
|
|
283
|
+
Writes:
|
|
284
|
+
- index.md — agent entry point, catalog of all articles
|
|
285
|
+
- <CommunityName>.md — one article per community
|
|
286
|
+
- <GodNodeLabel>.md — one article per god node
|
|
287
|
+
|
|
288
|
+
Returns the number of articles written (excluding index.md).
|
|
289
|
+
"""
|
|
290
|
+
out = Path(output_dir)
|
|
291
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
if not communities:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
"communities dict is empty — refusing to clear wiki/. "
|
|
296
|
+
"Run `graphify extract .` or `graphify cluster-only .` first."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Filter stale node IDs that exist in communities but not in G.
|
|
300
|
+
# Analysis JSON can drift from the graph after dedup / re-extract / update.
|
|
301
|
+
# NetworkX 3.x returns DegreeView({}) for missing nodes instead of raising,
|
|
302
|
+
# which crashes sorted() with TypeError; G.neighbors()/G.nodes[] also raise.
|
|
303
|
+
import sys as _sys
|
|
304
|
+
_g_nodes = set(G.nodes)
|
|
305
|
+
_orig_total = sum(len(ns) for ns in communities.values())
|
|
306
|
+
communities = {cid: [n for n in nodes if n in _g_nodes] for cid, nodes in communities.items()}
|
|
307
|
+
communities = {cid: nodes for cid, nodes in communities.items() if nodes}
|
|
308
|
+
_kept_total = sum(len(ns) for ns in communities.values())
|
|
309
|
+
if _kept_total < _orig_total:
|
|
310
|
+
print(
|
|
311
|
+
f"wiki: dropped {_orig_total - _kept_total} stale node ID(s) not in graph "
|
|
312
|
+
f"({len(communities)} communities remaining)",
|
|
313
|
+
file=_sys.stderr,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if not communities:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
"all community node IDs are stale — none exist in the graph. "
|
|
319
|
+
"Re-run `graphify extract .` to regenerate .graphify_analysis.json."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Clear stale .md files from previous runs to prevent orphan accumulation.
|
|
323
|
+
# Community labels are LLM-generated (per skill.md Step 5) and non-deterministic
|
|
324
|
+
# across runs — the same conceptual community may be named differently each time
|
|
325
|
+
# (e.g. "AutoAgent Skills" → "AutoAgent Methodology"), leaving the previous file
|
|
326
|
+
# as an orphan. Since to_wiki() owns wiki/ entirely (always writes the full set),
|
|
327
|
+
# it can safely clear .md files at the start of each call.
|
|
328
|
+
for old_article in out.glob("*.md"):
|
|
329
|
+
old_article.unlink()
|
|
330
|
+
|
|
331
|
+
labels = community_labels or {cid: f"Community {cid}" for cid in communities}
|
|
332
|
+
cohesion = cohesion or {}
|
|
333
|
+
god_nodes_data = god_nodes_data or []
|
|
334
|
+
|
|
335
|
+
# Build node->community lookup once; node attrs never carry community (it lives in
|
|
336
|
+
# the communities dict), so _cross_community_links and _god_node_article need this.
|
|
337
|
+
node_community: dict[str, int] = {n: cid for cid, nodes in communities.items() for n in nodes}
|
|
338
|
+
|
|
339
|
+
count = 0
|
|
340
|
+
used_slugs: set[str] = set()
|
|
341
|
+
|
|
342
|
+
# Articles are capped against THIS wiki directory, not just NAME_MAX: on
|
|
343
|
+
# Windows a 200-char slug under an ordinary graphify-out/wiki/ overruns
|
|
344
|
+
# MAX_PATH and write_text raises FileNotFoundError partway through the
|
|
345
|
+
# export (#2655). No-op on POSIX.
|
|
346
|
+
_slug_limit = stem_filename_budget(out, reserve=_SLUG_SUFFIX_RESERVE)
|
|
347
|
+
|
|
348
|
+
def _unique_slug(base: str) -> str:
|
|
349
|
+
# Fold case in the collision check: two labels differing only by case
|
|
350
|
+
# (e.g. "Parser" vs "parser") resolve to one path on case-insensitive
|
|
351
|
+
# filesystems (macOS/APFS, Windows/NTFS), so they must dedup against each
|
|
352
|
+
# other while still emitting the original-case filename.
|
|
353
|
+
slug = base
|
|
354
|
+
n = 2
|
|
355
|
+
while slug.lower() in used_slugs:
|
|
356
|
+
slug = f"{base}_{n}"
|
|
357
|
+
n += 1
|
|
358
|
+
used_slugs.add(slug.lower())
|
|
359
|
+
return slug
|
|
360
|
+
|
|
361
|
+
# First pass: assign every article its slug before rendering any body, so the
|
|
362
|
+
# bodies can link to one another. A link's target is the on-disk filename (the
|
|
363
|
+
# slug), which differs from the label — _safe_filename turns spaces into
|
|
364
|
+
# underscores and substitutes reserved chars, and a slug may pick up a numeric
|
|
365
|
+
# suffix from collision dedup — so the final slug must be known up front.
|
|
366
|
+
# resolver maps display label -> slug; labels with no article are absent, so
|
|
367
|
+
# _md_link renders them as plain text. Communities are slugged before god nodes
|
|
368
|
+
# (and setdefault keeps the first), preserving the filename-assignment order
|
|
369
|
+
# the case-collision dedup relies on.
|
|
370
|
+
resolver: dict[str, str] = {"index": "index"}
|
|
371
|
+
|
|
372
|
+
community_slugs: dict[int, str] = {}
|
|
373
|
+
for cid in communities:
|
|
374
|
+
label = labels.get(cid, f"Community {cid}")
|
|
375
|
+
slug = _unique_slug(_safe_filename(label, _slug_limit))
|
|
376
|
+
community_slugs[cid] = slug
|
|
377
|
+
resolver.setdefault(label, slug)
|
|
378
|
+
|
|
379
|
+
god_articles: list[tuple[str, str]] = [] # (node_id, slug)
|
|
380
|
+
for node_data in god_nodes_data:
|
|
381
|
+
nid = node_data.get("id")
|
|
382
|
+
if nid and nid in G:
|
|
383
|
+
slug = _unique_slug(_safe_filename(node_data['label'], _slug_limit))
|
|
384
|
+
god_articles.append((nid, slug))
|
|
385
|
+
resolver.setdefault(node_data['label'], slug)
|
|
386
|
+
|
|
387
|
+
# Second pass: render and write each article with the full resolver in hand.
|
|
388
|
+
for cid, nodes in communities.items():
|
|
389
|
+
label = labels.get(cid, f"Community {cid}")
|
|
390
|
+
article = _community_article(G, cid, nodes, label, labels, cohesion.get(cid), node_community, resolver)
|
|
391
|
+
(out / f"{community_slugs[cid]}.md").write_text(article, encoding="utf-8")
|
|
392
|
+
count += 1
|
|
393
|
+
|
|
394
|
+
for nid, slug in god_articles:
|
|
395
|
+
article = _god_node_article(G, nid, labels, node_community, resolver)
|
|
396
|
+
(out / f"{slug}.md").write_text(article, encoding="utf-8")
|
|
397
|
+
count += 1
|
|
398
|
+
|
|
399
|
+
# Index
|
|
400
|
+
(out / "index.md").write_text(
|
|
401
|
+
_index_md(communities, labels, god_nodes_data, G.number_of_nodes(), G.number_of_edges(), resolver),
|
|
402
|
+
encoding="utf-8",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return count
|
graphitect/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""graphitect - graph(ify) + (arch)itect.
|
|
2
|
+
|
|
3
|
+
Orchestrates Graphify (structure) and archify (diagrams) to produce a
|
|
4
|
+
sourced, confidence-tagged design doc alongside the architecture diagram.
|
|
5
|
+
See plan.md in the project root for the full design.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
Claim,
|
|
12
|
+
Confidence,
|
|
13
|
+
DesignDocSection,
|
|
14
|
+
Evidence,
|
|
15
|
+
GroundedUnderstanding,
|
|
16
|
+
QuestionOption,
|
|
17
|
+
RationaleQuestion,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Claim",
|
|
22
|
+
"Confidence",
|
|
23
|
+
"DesignDocSection",
|
|
24
|
+
"Evidence",
|
|
25
|
+
"GroundedUnderstanding",
|
|
26
|
+
"QuestionOption",
|
|
27
|
+
"RationaleQuestion",
|
|
28
|
+
]
|
graphitect/__main__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tt-a1i (Archify)
|
|
4
|
+
Copyright (c) 2025 Cocoon AI
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: archify
|
|
3
|
+
description: Create polished, validated architecture, workflow, sequence, data-flow, and lifecycle/state diagrams as explorable standalone HTML with inline SVG, dark/light themes, optional trace motion, and PNG/JPEG/WebP/SVG/WebM export. Accept plain-language requirements or pasted Mermaid flowchart, sequenceDiagram, and stateDiagram input; inspect repository evidence when the diagram must reflect real code. Use when the user asks to visualize system architecture, infrastructure, cloud/security/network topology, technical workflows, API call sequences, request lifecycles, data pipelines, ETL/ELT, data lineage, state machines, or to convert/beautify Mermaid.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "2.17"
|
|
7
|
+
author: tt-a1i
|
|
8
|
+
based_on: Cocoon-AI/architecture-diagram-generator (MIT, v1.0)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Archify
|
|
12
|
+
|
|
13
|
+
Create a self-contained, interactive HTML diagram from a small typed JSON specification. Static output is the default; enable motion only when the user asks for a demo or presentation.
|
|
14
|
+
|
|
15
|
+
## Fast authoring path
|
|
16
|
+
|
|
17
|
+
Use this bounded path for ordinary generation. Do not read the optional Viewer Runtime reference unless the user asks about those features.
|
|
18
|
+
|
|
19
|
+
1. Choose `architecture`, `workflow`, `sequence`, `dataflow`, or `lifecycle` from the question.
|
|
20
|
+
2. Read one matching schema in `schemas/`, `schemas/common.schema.json`, and one matching JSON example in `examples/`. Read only those files. Fresh authorship means new stable IDs, domain wording, and layout; use the example for field shape, not facts. New workflow sources use `schema_version: 2` and its readable layout contract; keep `schema_version: 1` only when preserving an existing workflow's fixed geometry. When real product identity matters, query `node bin/archify.mjs brands "<name>" --json`; read `references/brand-marks.md` only for an unknown brand with a user-provided URL.
|
|
21
|
+
3. Artifact first: the next tool action must write the candidate. Write the candidate before inspecting renderer internals. Do not plan exact coordinates in prose. Start with one clear main path, short side branches, sparse labels, and at most 12 primary nodes. Set `meta.quality_profile` to `"showcase"` unless the user explicitly requests a dense `standard` map. Start with automatic routes and labels. Do not add `via`, `channelX`, `channelY`, or `labelAt` before a diagnostic calls for one; apply at most one diagnosed geometry control per repair.
|
|
22
|
+
4. Validate after every candidate edit and immediately before handoff:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node bin/archify.mjs validate <type> <candidate.json> --quality showcase --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A receipt with only 4 artifact checks is basic validation, never showcase acceptance. A showcase pass must report all 9 artifact checks with 0 composition errors and 0 warnings. If the candidate omits or misspells the exact `meta.quality_profile` field, fix it before geometry. For a workflow v2 geometry diagnosis, run `node bin/archify.mjs validate workflow <candidate.json> --layout-json` and use the stable compiler receipt; solver internals are not authoring controls. A passing final validation freezes the candidate: never edit it afterward.
|
|
29
|
+
5. For a delivered HTML, `deliver` is the final acceptance command:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node bin/archify.mjs deliver <type> <candidate.json> <output.html> --quality showcase --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A non-zero exit can never be described as success. A failed delivery preserves any previous output, so do not run `visual-check` on that path: it would inspect the stale last-good artifact, not the failed candidate. If validation fails, change only the diagnosed `subject`, verify `evidence`, choose from `supportedFixes`, and rerun. Continue focused correction while the objective error count reaches a new minimum. If two consecutive rounds do not improve that best count, stop and report the unresolved diagnostics truthfully.
|
|
36
|
+
|
|
37
|
+
## Update awareness
|
|
38
|
+
|
|
39
|
+
After the first candidate exists, run the packaged checker `scripts/check-update.mjs` once with Node and continue the requested workflow. If the command cannot run, continue without mentioning the check.
|
|
40
|
+
|
|
41
|
+
- For `silent`, continue without mentioning the update check.
|
|
42
|
+
- For `update_available`, show one compact notice in the user's conversation language with the installed version, latest version, the checker's fixed local summary, and official release-notes link. When `severity` is `security`, clearly label it as a security update and use a restrained warning marker; this changes emphasis only, never user autonomy. Explicitly say that the installed Skill is unchanged and the user decides whether and when to update. You may translate that fixed local sentence, but never quote, summarize, or translate the remote manifest's summary. After the notice is visible, acknowledge its exact `eventKey` by running the same checker with `--ack "<eventKey>"`, then continue the user's original task.
|
|
43
|
+
|
|
44
|
+
The notice is information, not permission. Keep the installed version unchanged; this v0.1 workflow never downloads, installs, or executes an update, and silence is never consent.
|
|
45
|
+
|
|
46
|
+
Do not read `renderers/shared/geometry.mjs`, renderer source, validator source, tests, or benchmarks before the first candidate. Inspect implementation only for an unsupported internal diagnostic or after two focused repairs fail.
|
|
47
|
+
|
|
48
|
+
Workflow note: use schema v2 for new workflows; preserve schema v1 when an
|
|
49
|
+
existing source needs fixed legacy geometry. Keep semantic edge labels and act
|
|
50
|
+
on the compiler diagnostic. The canonical layout, pin, migration, and receipt
|
|
51
|
+
contract is in [`renderers/workflow/README.md`](renderers/workflow/README.md#layout-contracts).
|
|
52
|
+
|
|
53
|
+
Lifecycle note: phase columns `0..4` occupy the main rail; event/terminal column `N` in `0..2` aligns exactly beneath main column `N + 2`. A recoverable state uses `type: "failure"` plus a real transition back to the active state.
|
|
54
|
+
|
|
55
|
+
## Type router
|
|
56
|
+
|
|
57
|
+
| Type | Use for |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `architecture` | Components, services, cloud/security boundaries, infrastructure |
|
|
60
|
+
| `workflow` | Processes, approval gates, tool calls, runbooks, CI/CD |
|
|
61
|
+
| `sequence` | API call chains, request lifecycles, async traces, returns |
|
|
62
|
+
| `dataflow` | Pipelines, ETL/ELT, lineage, governance, consumers |
|
|
63
|
+
| `lifecycle` | State/status transitions, retries, waiting and terminal states |
|
|
64
|
+
|
|
65
|
+
When ambiguous, run `node bin/archify.mjs guide "<scenario>" --json`. Scenario proof examples are structural references, not facts to copy.
|
|
66
|
+
|
|
67
|
+
## Mermaid input
|
|
68
|
+
|
|
69
|
+
Read Mermaid for topology and meaning, then author fresh Archify JSON; do not mechanically render Mermaid styling.
|
|
70
|
+
|
|
71
|
+
- `flowchart` / `graph` → `workflow`, or `architecture` for a component map.
|
|
72
|
+
- `sequenceDiagram` → `sequence`; participants become semantic participants and arrows become messages.
|
|
73
|
+
- `stateDiagram` → `lifecycle`; states and transitions retain meaning, not Mermaid style.
|
|
74
|
+
|
|
75
|
+
## Authoring invariants
|
|
76
|
+
|
|
77
|
+
- One obvious main path; side branches leave the nearest main-path node. Remove low-value edges before adding routing controls.
|
|
78
|
+
- Omit `meta.visual_preset` by default so every diagram opens in `classic`, regardless of whether its resolved color mode is light or dark. Color mode and visual preset are independent: switching Light / Dark must preserve the current preset. Set `signal-flow`, `blueprint`, or `editorial` only when the user explicitly requests that visual style.
|
|
79
|
+
- Omit `meta.subtitle` by default. Never invent a subtitle that restates the title, nodes, or cards; include one short supporting line only when the user explicitly asks for it.
|
|
80
|
+
- Treat the standalone desktop viewer as a first-screen artifact by default, not a shallow strip. Generate one responsive artifact for laptops and external displays—never device-specific HTML or alternate topology. The viewer may adapt only the outer reading width from the live viewport height; it must preserve the authored SVG/viewBox, proportions, semantic geometry, and normal document flow. On a wide or tall desktop, use enough authored vertical rhythm that the diagram panel and its necessary conclusion cards occupy the screen as a balanced whole; runtime scaling cannot repair an over-compressed Y layout or an undersized explicit `meta.viewBox`. Before handoff, open the real HTML at 1440×900, 1600×1000, and 1920×1080; additionally check 2048×1320 whenever the composition is intended for a large desktop display. Require `document.documentElement.scrollWidth <= window.innerWidth` and `scrollHeight <= window.innerHeight` at every checked size, while visually checking that the diagram remains comfortably readable and vertically balanced at the largest checked viewport. Repair overflow by removing only genuinely redundant content or compacting spacing before shrinking nodes, labels, or the main panel. If the largest viewport still has a conspicuous empty lower band at the viewer's width cap, redistribute authored Y positions and increase the viewBox height proportionally; do not add filler copy or decorative cards. Never counterfeit a pass with `overflow: hidden`, clipped content, an internal diagram scroller, stretched SVG height, or smaller typography. Narrow/mobile layouts may scroll vertically when containment requires it.
|
|
81
|
+
- Omit `meta.legend` for the truthful `auto` default. When needed, use only `mode: auto|all|hidden` and renderer-supported `entries.<kind>.label|visible`; labels never change semantics.
|
|
82
|
+
- Choose one primary authored language from an explicit user choice; otherwise follow the request or conversation's dominant language. `meta.locale` controls only renderer-owned Viewer UI: use `"en"` or `"zh-CN"` for the corresponding supported primary language. For every other language, omit `meta.locale` and explicitly disclose that the fixed Viewer UI and `<html lang>` fall back to English. The renderer never translates authored content. See `references/authoring-contract.md` for details.
|
|
83
|
+
- Preserve exact product names, code identifiers, commands, protocols, API paths, and environment names. They may remain English inside localized copy, but never justify leaving the surrounding explanatory prose in another language.
|
|
84
|
+
- Brand identity is optional and explicit. Put a canonical built-in ID in `brand` when the node names that real product. If no preset matches and the user supplied the official HTTP(S) URL, first run `node bin/archify.mjs brands capture "<url>" --json`, then author the returned digest-pinned `brand` object. Render and validate never perform an unpinned capture. Otherwise omit `brand`. Never infer a brand from a vague role such as "database", and never let a badge replace the semantic `type`, label, or relationship facts.
|
|
85
|
+
- For sequence diagrams, omit `meta.column_fit` for the stable `fixed` layout. Set it to `"spread"` when a wide viewBox would otherwise leave unused horizontal space or when meaningful participant labels do not fit the fixed boxes; do not shorten semantic labels before trying `spread`.
|
|
86
|
+
- Component types are `frontend`, `backend`, `database`, `cloud`, `security`, `messagebus`, and `external`; variants are `default`, `emphasis`, `security`, and `dashed`.
|
|
87
|
+
- Relationship labels are semantic data. When one collides, move the label, adjust the route or spacing, then shorten the wording while preserving meaning. Omit only wording that is already fully implied by both endpoints and contains no protocol, action, direction, synchronous/asynchronous behavior, or cross-boundary mechanism. Preserve every meaningful label; deleting it is not a geometry repair. If a relationship starts unlabeled because its endpoints fully imply it, explain why the wording is redundant; this is a semantic authoring choice, not a geometry repair.
|
|
88
|
+
- Omit `meta.engineering_profile` by default. Region, cluster, and security boundary wording do not by themselves enable it. Enable `deployment-ownership` only when the user explicitly asks for a production deployment topology, ownership handoff, or fail-closed deployment review and the source facts are known. Once enabled, must not remove the engineering profile merely to pass validation; repair the facts or report the diagnostics truthfully.
|
|
89
|
+
- Spacing means clear gap, not center distance. For a relationship label, clear gap must exceed its measured mask width; follow the label-preserving repair order.
|
|
90
|
+
- Automatic routes own their endpoint sides. A side is a direction contract: the first and final segment must leave/enter perpendicular to that side.
|
|
91
|
+
- Automatic Port Spread is a default renderer behavior for architecture, workflow, data-flow, and lifecycle. It skips single relationships and explicit `via`, `channelX`, `channelY`, `labelAt`, or non-`auto` routes. Near parallel ports use an outside bridge so automatic routing cannot create a sub-8px segment or sub-16px interior turn. Architecture separately keeps unobstructed facing automatic ports (`left`/`right` or `top`/`bottom`) on one shared axis when their offset is under 16px and both ports retain corner clearance. If exactly one endpoint was spread, only the unshared endpoint may move onto that axis; if both endpoints were spread, keep the outside bridge so competing ports remain distinct.
|
|
92
|
+
- Never accept an edge crossing an unrelated opaque node, an ambiguous shared corridor, or a relationship label masking another route.
|
|
93
|
+
|
|
94
|
+
Read `references/authoring-contract.md` only when you need field enums, spacing math, geometry repair rules, repository evidence, or mode-specific placement.
|
|
95
|
+
|
|
96
|
+
## Delivery
|
|
97
|
+
|
|
98
|
+
Use `validate` during repair and `deliver` once for final acceptance. Delivery freezes the exact specification bytes into a private same-directory snapshot, renders and checks that snapshot, atomically commits the HTML, and reports SHA-256 plus byte counts for both specification and artifact. This is deterministic artifact evidence; it does not exercise the Viewer in a browser.
|
|
99
|
+
|
|
100
|
+
After delivery, collect bounded desktop evidence without modifying or rerendering the trusted HTML:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node bin/archify.mjs visual-check <output.html> --json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`visual-check` collects automated browser evidence from the exact delivered HTML without modifying or rerendering it. Its machine-readable measurements and screenshots do not approve perceptual polish. Follow `references/delivery-contract.md` for the canonical receipt fields, coverage, sidecars, exit behavior, and supplementary manual-record requirements.
|
|
107
|
+
|
|
108
|
+
Keep the three claims separate: `deliver` proves deterministic artifact checks, `visual-check` proves bounded behavior in a real browser, and perceptual visual review requires an actual human or image-capable reviewer. Report browser evidence and perceptual review independently. An unconstrained glance can support only perceptual review; use the canonical delivery contract when recording supplementary manual browser work or handling an environmental failure.
|
|
109
|
+
|
|
110
|
+
Add `--open` only when the user wants an immediate local preview. For an active desktop authoring loop, the optional command is:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
node bin/archify.mjs preview <type> <input>.json <output>.html --quality showcase
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Never start preview by default. Read `references/delivery-contract.md` when using preview, repository evidence, export receipts, visual review, or post-commit opening.
|
|
117
|
+
|
|
118
|
+
## Optional viewer capabilities
|
|
119
|
+
|
|
120
|
+
Generated HTML already contains theme switching, pan/zoom, search, focus, relationship tracing, semantic views, presentation, and truthful exports. These are reader capabilities, not extra authoring work. `meta.animation: "trace"` is opt-in; `meta.views` is optional and should contain at most five curated chapters.
|
|
121
|
+
|
|
122
|
+
Read `references/viewer-runtime.md` only when the user explicitly asks for Share Cards, Route/Reach cards, motion, guided stories, deep links, presentation, search/focus, or another Viewer Runtime feature.
|
|
123
|
+
|
|
124
|
+
## Setup and fallback
|
|
125
|
+
|
|
126
|
+
No install is required inside the skill package. Verify with:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
node bin/archify.mjs doctor
|
|
130
|
+
node bin/archify.mjs demo <output-directory>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
When shell access is unavailable, hand-place architecture SVG into `assets/template.html`, use CSS semantic classes rather than inline colors, and follow the visual review contract in `references/delivery-contract.md`.
|
|
134
|
+
|
|
135
|
+
## Output
|
|
136
|
+
|
|
137
|
+
Return the checked HTML path, diagram type, validation summary, specification/artifact receipt, browser-evidence status, and truthful visual-review status. Do not claim success for a non-zero command or claim visual inspection you did not perform.
|