doubled-graph 2.0.0__tar.gz → 2.0.2__tar.gz
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.
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/PKG-INFO +2 -2
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/README.md +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/docs/CHANGELOG.md +98 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/docs/TOOLS.md +3 -3
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/upgrade-methodology/00-entry-prompt.md +2 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/upgrade-methodology/01-content-gaps.md +84 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/pyproject.toml +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/cli.py +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/config.py +3 -3
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/graphs/declared.py +67 -8
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/graphs/grace4.py +8 -3
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/hooks_installer.py +18 -5
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/integrations/grace_cli.py +125 -9
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/migrations/__init__.py +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/migrations/grace3_to_grace4.py +443 -59
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/policy/phase.py +6 -3
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/server.py +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/setup.py +25 -6
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/context.py +4 -4
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/detect_changes.py +1 -1
- doubled_graph-2.0.2/tests/test_bom_support.py +204 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_grace4_artifacts.py +16 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_grace_cli_engine.py +1 -1
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_migration_grace3_to_grace4.py +306 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/.gitignore +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/ARTICLE.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/INDEX.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/LICENSE +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/docs/HOOKS.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/docs/SPEC.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-ai-existing-project/README.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-ai-maintenance/README.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-ai-new-project/README.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-humans/FAQ.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-humans/deep-dive.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/for-humans/getting-started.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/README.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/approval-checkpoints.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/artifacts.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/auto-scaling.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/drift-and-priority.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/_template.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/csharp.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/go.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/java.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/python.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/rust.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/swift.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/language-adapters/typescript.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/principles.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/roles.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/_template.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/claude-code.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/codex.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/continue.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/cursor.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/local-llm.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/runtime-adapters/windsurf.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/tools.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/methodology/workflow.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/maintenance/on-after-edit.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/maintenance/on-before-edit.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/maintenance/on-drift-detected.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/maintenance/on-user-manual-edit.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/00-entry-prompt.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/01-discover.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/02-reconstruct-intent.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/03-draft-plan.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/04-markup-codebase.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/05-verification-and-logs.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/migrate-existing-project/06-validation-gates.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/00-entry-prompt.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/01-intent-interview.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/02-requirements.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/03-technology.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/04-development-plan.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/05-verification-plan.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/06-generate.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/prompts/new-project/07-post-init-sync.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/__main__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-after-edit/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-before-edit/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-drift/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-guide/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-manual-edit/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-refactor/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/data/skills/doubled-graph-verify/SKILL.md +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/graphs/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/graphs/crossref.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/integrations/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/integrations/cgc.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/policy/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/storage/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/storage/paths.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/_meta.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/analyze.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/src/doubled_graph/tools/impact.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/__init__.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_cgc_contract.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_setup_artifacts.py +0 -0
- {doubled_graph-2.0.0 → doubled_graph-2.0.2}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: doubled-graph
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: MCP facade over CodeGraphContext + GRACE 4 (.grace artifacts): impact, context, drift-detect, migration for LLM-driven codebases.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rour/doubled-graph-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/rour/doubled-graph-mcp
|
|
@@ -154,7 +154,7 @@ head -1 "$(which doubled-graph)" # вывести shebang — это и ест
|
|
|
154
154
|
### 4. Проверить, что всё встало
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
|
-
doubled-graph --version # 2.0.
|
|
157
|
+
doubled-graph --version # 2.0.2
|
|
158
158
|
doubled-graph status --repo . # artifacts.project_kind = grace4, grace_engine.upgrade_required = false
|
|
159
159
|
doubled-graph lint --repo . # summary.errors = 0
|
|
160
160
|
doubled-graph migrate check --repo . # map_pending / placeholders — что осталось заполнить
|
|
@@ -119,7 +119,7 @@ head -1 "$(which doubled-graph)" # вывести shebang — это и ест
|
|
|
119
119
|
### 4. Проверить, что всё встало
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
doubled-graph --version # 2.0.
|
|
122
|
+
doubled-graph --version # 2.0.2
|
|
123
123
|
doubled-graph status --repo . # artifacts.project_kind = grace4, grace_engine.upgrade_required = false
|
|
124
124
|
doubled-graph lint --repo . # summary.errors = 0
|
|
125
125
|
doubled-graph migrate check --repo . # map_pending / placeholders — что осталось заполнить
|
|
@@ -2,6 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to doubled-graph. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [2.0.2] — 2026-10-23
|
|
6
|
+
|
|
7
|
+
BOM support on top of 2.0.1 (already published). No artifact-format change:
|
|
8
|
+
`pipx upgrade doubled-graph` is enough; no re-migration required.
|
|
9
|
+
|
|
10
|
+
**Why:** a real C# repository had 216 `.cs` files starting with a UTF-8 BOM.
|
|
11
|
+
The JS engine treats U+FEFF as whitespace, but Python's `\s` does not, so
|
|
12
|
+
doubled-graph saw those files as ungoverned while `grace lint` was green:
|
|
13
|
+
modules lost `localFiles`, `file show` found nothing, `detect-changes` reported
|
|
14
|
+
`code_without_module`.
|
|
15
|
+
|
|
16
|
+
### Fixed — UTF-8 BOM in sources and inputs
|
|
17
|
+
- UTF-8 BOM everywhere, not only in source files. Python's `\s` does not match
|
|
18
|
+
U+FEFF while the JS engine's `\s` does, so `^\s*// MODULE:` never matched and
|
|
19
|
+
every BOM file (216 in one real repository) looked ungoverned to
|
|
20
|
+
doubled-graph — modules lost their files, `module show` returned no
|
|
21
|
+
`localFiles`, `detect-changes` reported `code_without_module` — while
|
|
22
|
+
`grace lint` was green. All readers now use `utf-8-sig`: source scanning
|
|
23
|
+
(`graphs/grace4.py`, markup repair, `context`, `detect_changes`), `.grace`
|
|
24
|
+
documents, `.doubled-graph/config.json`, `.mcp.json`,
|
|
25
|
+
`.continue/config.json`, `.claude/settings.json`, `AGENTS.md` and legacy
|
|
26
|
+
`docs/*.xml`. The markup repair writes a source file back **with** its BOM;
|
|
27
|
+
writers of `.grace`/JSON/hook scripts never add one (a BOM-broken shebang
|
|
28
|
+
would not execute).
|
|
29
|
+
- `doubled-graph lint` now **works on BOM sources without modifying them**. The
|
|
30
|
+
engine's Python/TypeScript/Dart analyzers reject U+FEFF (`ast.parse`), and its
|
|
31
|
+
index check misses `.grace` documents behind a BOM — so lint runs against a
|
|
32
|
+
temporary snapshot in which BOM is normalised (`.grace/*.xml` and
|
|
33
|
+
adapter-backed sources only), with the engine `cwd` set to the snapshot and
|
|
34
|
+
`--path .` (4.2.1 reports `projection.*.unindexed-document` for an absolute
|
|
35
|
+
`--path` outside cwd). JSON paths are rebased to the real repository before
|
|
36
|
+
returning. Client files are never written: verified — `errors: 1` before,
|
|
37
|
+
`errors: 0` after, BOM still present in `src/a.py` and `.grace/graph/index.xml`.
|
|
38
|
+
Files in languages without an adapter (C#, Go, Rust, Java, Swift, …) are
|
|
39
|
+
copied verbatim, BOM included: the engine runs no analyzer for them, so their
|
|
40
|
+
BOM never reaches `ast.parse` and does not affect lint. `migrate check` reports `bom_files` / `bom_adapter_files` and
|
|
41
|
+
warns `migration.bom-in-code`; `01-content-gaps.md` explains the mirror.
|
|
42
|
+
## [2.0.1] — 2026-10-23
|
|
43
|
+
|
|
44
|
+
Migration-fidelity fixes on top of 2.0.0 (already published). No artifact-format
|
|
45
|
+
change: upgrade is additive, `pipx upgrade doubled-graph` and a re-run of
|
|
46
|
+
`doubled-graph setup` (or `migrate apply` on an unmigrated repo) is enough.
|
|
47
|
+
|
|
48
|
+
**Why:** 2.0.0 could migrate a real GRACE 3 repository into a project where
|
|
49
|
+
`grace lint` is green but the context documents are empty (`unknown`,
|
|
50
|
+
`Migrated project`, one generic goal) and `TestingStack` cannot produce a
|
|
51
|
+
`<Command>` for `V-<M>`. The checks below make that state visible and prevent
|
|
52
|
+
silent data loss during the copy.
|
|
53
|
+
|
|
54
|
+
### Fixed — legacy format edge cases (all lose data silently if missed)
|
|
55
|
+
- Namespaced legacy XML: tags arrive as `{urn}M-AUTH`, every `^M-` match failed
|
|
56
|
+
and a project migrated with **zero modules**. Namespaces are stripped before
|
|
57
|
+
matching (elements and attributes).
|
|
58
|
+
- UTF-16/UTF-32/BOM legacy documents: `read_text("utf-8")` raised
|
|
59
|
+
`UnicodeDecodeError`; reads now try `utf-8-sig` → `utf-16` → `utf-32` → `cp1251`.
|
|
60
|
+
- `CDATA` values (`<Language><![CDATA[C#]]></Language>`) were stripped to empty;
|
|
61
|
+
`CDATA` is unwrapped before tag removal.
|
|
62
|
+
- Module `NAME` / `STATUS` / `TYPE` attributes were dropped (the migrator wrote
|
|
63
|
+
`Name` from the id and `Status=existing`); they are preserved (`Type` as a
|
|
64
|
+
free child tag, which GRACE 4 does not validate).
|
|
65
|
+
- Several GRACE 3 verification records for one module (`V-M-AUTH-1`, `-2`, …)
|
|
66
|
+
collapsed into one canonical `V-M-AUTH` and only the first record's evidence
|
|
67
|
+
survived; commands, scenarios, markers, trace assertions and test files are
|
|
68
|
+
now merged.
|
|
69
|
+
- Lowercase/mixed-case contract keys (`role:`, `map_mode:`, `links:`) are
|
|
70
|
+
silently ignored by the engine's field parser and produced
|
|
71
|
+
`markup.invalid-role`; the markup repair normalises keys to UPPERCASE and
|
|
72
|
+
ROLE values to upper case.
|
|
73
|
+
- Windows-style paths (`src\\auth\\a.py`) are normalised to forward slashes, so
|
|
74
|
+
`LINKS`-based file-to-module linkage matches.
|
|
75
|
+
- Self-closing `<UC-1 ... />` entries were skipped by the use-case extractor;
|
|
76
|
+
they are now rendered as goals.
|
|
77
|
+
- `migration.context-skeleton` was emitted even when the repository has no
|
|
78
|
+
artifacts at all (kind `none`); the warning is now gated to migrated projects.
|
|
79
|
+
|
|
80
|
+
### Fixed — context documents are copied, not skeletonised
|
|
81
|
+
- The migrator now **copies** the legacy context documents instead of shipping a
|
|
82
|
+
skeleton: `technology.xml` fields (`Language`, `Runtime`, `Framework`,
|
|
83
|
+
`TestingStack`, `Observability`) and `requirements.xml` sections
|
|
84
|
+
(`ProjectName`, `Goals` from `UC-*` as text, `Users` from `ACTOR`, `Constraints`)
|
|
85
|
+
are taken from the old document; undeclared fields are marked
|
|
86
|
+
`not specified` (never guessed), and `LegacySource` keeps the archive copy.
|
|
87
|
+
- New `context_gaps` in the migration report plus `migration.context-skeleton`
|
|
88
|
+
warning. The check is gated: it fires when the copied documents are a skeleton
|
|
89
|
+
(0–1 real field) **or** when `TestingStack` is still a placeholder — a richer
|
|
90
|
+
document is trusted as-is. It reads the current `.grace/context`, not the
|
|
91
|
+
legacy documents (which stay on disk forever), so a completed migration
|
|
92
|
+
reports no gaps. Recognised placeholders include the old migrator/bootstrap
|
|
93
|
+
prose (`unknown`, `Migrated project`, `Keep the project governed…`,
|
|
94
|
+
`Describe the primary…`, `Restore the product intent…`), and the bootstrap
|
|
95
|
+
path now writes `not specified` instead of `unknown`.
|
|
96
|
+
- `01-content-gaps.md` gained **Шаг 0 «context-документы»** (before
|
|
97
|
+
`MODULE_MAP`) and a mandatory **Шаг 4a «context gate»**: a `grep` over
|
|
98
|
+
`.grace/context/*.xml` for placeholders plus `context_gaps == []` from
|
|
99
|
+
`migrate check`, with `deployment`/`ux` `not-applicable` explicitly allowed.
|
|
100
|
+
`<Command>` for `V-<M>` must not be derived while `TestingStack` is a
|
|
101
|
+
placeholder.
|
|
102
|
+
|
|
5
103
|
## [2.0.0] — 2026-10-23
|
|
6
104
|
|
|
7
105
|
**Breaking: GRACE 4.** The methodology now runs on the `.grace` artifact model
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"warnings": [
|
|
56
56
|
{"code": "HASKELL_PARSER_UNSTABLE", "file": "src/foo.hs", "detail": "…"}
|
|
57
57
|
],
|
|
58
|
-
"meta": {"phase": "post_migration", "tool_version": "2.0.
|
|
58
|
+
"meta": {"phase": "post_migration", "tool_version": "2.0.2", "timestamp_iso": "2026-10-23T10:22:04Z", "trace_id": "…"}
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -139,7 +139,7 @@ Declared-сторона (`graphs/grace4.py`) при `analyze` не вызыва
|
|
|
139
139
|
"phase=post_migration — contract drift check required"
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
|
-
"meta": {"phase": "post_migration", "tool_version": "2.0.
|
|
142
|
+
"meta": {"phase": "post_migration", "tool_version": "2.0.2", "timestamp_iso": "...", "trace_id": "..."}
|
|
143
143
|
}
|
|
144
144
|
```
|
|
145
145
|
|
|
@@ -232,7 +232,7 @@ Declared-сторона (`graphs/grace4.py`) при `analyze` не вызыва
|
|
|
232
232
|
"plan_status": "approved",
|
|
233
233
|
"tasks": ["T-001", "T-002"]
|
|
234
234
|
},
|
|
235
|
-
"meta": {"phase": "...", "tool_version": "2.0.
|
|
235
|
+
"meta": {"phase": "...", "tool_version": "2.0.2", "...": "..."}
|
|
236
236
|
}
|
|
237
237
|
```
|
|
238
238
|
|
|
@@ -56,6 +56,8 @@ doubled-graph lint --repo . # summary.errors
|
|
|
56
56
|
prompts/upgrade-methodology/01-content-gaps.md
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Первым его шагом идёт **Шаг 0 — context-документы**: если `migrate check` показывает `context_gaps` (legacy объявил 0–1 поле), ИИ заполняет `technology.xml` (в первую очередь `TestingStack`) и `requirements.xml` из `<LegacySource>` / манифестов проекта, иначе `<Command>` для `V-<M>` вывести не из чего.
|
|
60
|
+
|
|
59
61
|
Он детерминированно проходит `map_pending` (карты модулей), `placeholders` (пустые `V-<M>`), `markup_pending` (`LINKS`) и заканчивается финальными CLI-проверками. Выдуманные команды запрещены: если тест не найден или не проходит — запись остаётся пустой, ставится `Priority: low` и строка в `docs/DRIFT.md`.
|
|
60
62
|
|
|
61
63
|
6. Вернуться к обычному циклу: `prompts/maintenance/on-before-edit.md` / `on-after-edit.md`. Legacy `docs/` можно заархивировать отдельным решением (бэкап + approval), это не обязательный шаг.
|
|
@@ -23,6 +23,35 @@ doubled-graph health --repo . # модули без verification
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
## Шаг 0 — context-документы (до всего остального)
|
|
27
|
+
|
|
28
|
+
Мигратор копирует старые `requirements.xml`/`technology.xml` в `.grace/context/`, но если legacy объявлял 0–1 поле, документ остаётся скелетом: `migrate check` показывает `context_gaps` и warning `migration.context-skeleton`, а поля помечены `not specified`. Пока это не заполнено, из `technology.xml` нельзя вывести `<Command>` для `V-<M>` (шаг 2) — поэтому шаг 0 обязателен и идёт первым.
|
|
29
|
+
|
|
30
|
+
1. Проверь маркеры командой:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
grep -n "not specified\|unknown\|Migrated project" .grace/context/*.xml
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Заполняй поля в этом порядке (первый источник, который даёт факт, — истина):
|
|
37
|
+
|
|
38
|
+
| Поле | Источник 1 | Источник 2 | Источник 3 |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| `Language`, `Runtime`, `Framework`, `TestingStack`, `Observability` (`technology.xml`) | legacy-значения внутри `<LegacySource>` (там лежит старый `docs/technology.xml` целиком) | манифесты проекта: `*.csproj`/`*.sln`, `package.json`, `pyproject.toml`/`pytest.ini`, `go.mod`, `Cargo.toml`, `pom.xml`/`build.gradle`, `Gemfile`, `composer.json`, `Package.swift`, `mix.exs`, `CMakeLists.txt`/`Makefile` — найденные `find . -maxdepth 3 -name '<manifest>'` | README / CI (`.github/workflows/*`, `.gitlab-ci.yml`) |
|
|
41
|
+
| `ProjectName`, `Goals`, `Users`, `Constraints` (`requirements.xml`) | legacy `<LegacySource>`: `UC-*` → `Goal` текстом, `ACTOR`/`User` → `Users`, `Constraint`/`NFR` → `Constraints` | README (первый абзац/раздел «О проекте») и существующие issue/ADR | — |
|
|
42
|
+
| `deployment.xml` | манифесты инфраструктуры: `Dockerfile`, `docker-compose*.yml`, `k8s/*.yaml`, `helm/`, `Chart.yaml`, `Procfile`, `fly.toml`, `*.tf`, `serverless.yml`, `vercel.json` | README/CI deploy-шаг | `not-applicable` + `<Reason>infrastructure manifests not found</Reason>` |
|
|
43
|
+
| `ux-guidelines.xml` | манифесты фронтенда: `index.html`, `*.xaml`/`*.razor`/`*.vue`/`*.svelte`, `package.json` с react/vue/angular/svelte | README/скриншоты | `not-applicable` + `<Reason>frontend manifests not found</Reason>` |
|
|
44
|
+
|
|
45
|
+
3. Правила:
|
|
46
|
+
- значения **копируются** из legacy/манифестов, не придумываются; язык — по манифестам проекта, а не по типу задачи;
|
|
47
|
+
- `TestingStack` — команда раннера из манифеста (`package.json → scripts.test`, `pyproject/pytest.ini → pytest`, `*proj/*sln → dotnet test`, `go.mod → go test ./...`, `Cargo.toml → cargo test`, `pom.xml → mvn test`, `build.gradle → ./gradlew test`, `Gemfile → bundle exec rspec`, `composer.json → phpunit`, `Package.swift → swift test`, `mix.exs → mix test`, `CMakeLists.txt/Makefile → ctest/make test`);
|
|
48
|
+
- если факт не найден ни в одном источнике — поле остаётся `not specified`, строка в `docs/DRIFT.md`; шаг 2 в этом случае не выводит `Command` для `V-<M>` (модуль получает `Priority: low`, как в fallback шага 2);
|
|
49
|
+
- `deployment`/`ux` как `not-applicable` с причиной — законное состояние, а не пробел.
|
|
50
|
+
|
|
51
|
+
4. Проверь: `doubled-graph lint --repo .` (0 errors) и `doubled-graph migrate check --repo .` (`context_gaps == []`).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
26
55
|
## Шаг 1 — `MODULE_MAP` (устраняет `markup.module-map-missing`)
|
|
27
56
|
|
|
28
57
|
Для каждого пути из `map_pending`, по порядку:
|
|
@@ -155,6 +184,41 @@ doubled-graph module health <M-*> --repo . # критичные моду
|
|
|
155
184
|
|
|
156
185
|
---
|
|
157
186
|
|
|
187
|
+
## Шаг 4a — context gate: в context нет заглушек
|
|
188
|
+
|
|
189
|
+
Обязательная проверка перед отчётом: `context/*.xml` не должны содержать ни `unknown`, ни шаблонных фраз мигратора/бутстрапа. Проверяется командой, а не «на глаз»:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
grep -nE "not specified|unknown|Migrated project|Describe the primary|replace with the real product statement|Keep the project governed|Existing project maintainers|Legacy GRACE 3 documents stay in place|No operational constraints were declared|Users were not described|Restore the product intent|Not configured yet" .grace/context/*.xml
|
|
193
|
+
doubled-graph migrate check --repo . # context_gaps должен быть []
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Что означает каждая находка:
|
|
197
|
+
|
|
198
|
+
| Находка | Действие |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `technology: … TestingStack is still a placeholder` | заполнить `TestingStack` из `<LegacySource>` или манифеста (шаг 0), иначе `<Command>` для `V-<M>` не выводится |
|
|
201
|
+
| `technology: 0–1/5 real field(s)` | заполнить Language/Runtime/Framework/Observability по шагу 0 |
|
|
202
|
+
| `requirements: … real section(s)` / `Goals are still a placeholder` | перенести `UC-*`/README-цели в `<Goals>` (шаг 0) |
|
|
203
|
+
| `deployment`/`ux` с `not-applicable` + `<Reason>` | не находка: это законное состояние, пропустить |
|
|
204
|
+
|
|
205
|
+
Если после заполнения `context_gaps` пуст, а `grep` всё ещё что-то находит — это строка внутри `<LegacySource>` (архив), её не трогай: проверяй только рабочие секции, не архив.
|
|
206
|
+
|
|
207
|
+
### BOM в исходниках
|
|
208
|
+
|
|
209
|
+
`doubled-graph` сам читает BOM-файлы (`utf-8-sig`), но анализатор движка для Python/TypeScript/Dart через `ast.parse`/тулинг отвергает U+FEFF: `grace lint` даёт `analysis.adapter-failed: invalid non-printable character U+FEFF at line 1:1`. Проверь заранее:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
doubled-graph migrate check --repo . # bom_files / bom_adapter_files, warning migration.bom-in-code
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- **файлы клиента не изменяются**: `doubled-graph lint` при наличии BOM запускает движок на временной копии, где BOM нормализован (только `.grace/*.xml` и adapter-исходники `.py/.pyi/.ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.cts/.dart`), и переписывает пути отчёта обратно на реальный репозиторий;
|
|
216
|
+
- если `bom_adapter_files` непусто, но `lint` зелёный — это и есть работа зеркала: BOM остался в исходниках, гейт пройден;
|
|
217
|
+
- файлы без адаптера (C#, Go, Rust, Java, Swift, …) попадают в копию **как есть**, BOM у них не снимается: движок не запускает для них анализатор, поэтому lint от BOM не зависит;
|
|
218
|
+
- ничего не удаляй и не перекодируй вручную; если lint всё же даёт `analysis.adapter-failed` с U+FEFF — это сигнал, что зеркало не сработало (сообщи о баге), а не повод править файлы.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
158
222
|
## Шаг 5 — отчёт пользователю (формат фиксированный)
|
|
159
223
|
|
|
160
224
|
```
|
|
@@ -173,7 +237,8 @@ health: модулей без verification=<N>
|
|
|
173
237
|
| Проверка | Готово, когда |
|
|
174
238
|
|---|---|
|
|
175
239
|
| `doubled-graph lint` | `summary.errors == 0` |
|
|
176
|
-
| `doubled-graph migrate check` | `map_pending == []`, `placeholders == []` |
|
|
240
|
+
| `doubled-graph migrate check` | `map_pending == []`, `placeholders == []`, `context_gaps == []` |
|
|
241
|
+
| `grep -nE "not specified\|unknown\|Migrated project\|Describe the primary\|Keep the project governed" .grace/context/*.xml` | пусто вне `<LegacySource>`, кроме `deployment`/`ux` с `not-applicable` и `<Reason>` |
|
|
177
242
|
| `doubled-graph detect-changes --scope all` | `missing_verification` только у записей с `DRIFT.md` |
|
|
178
243
|
| `doubled-graph health` | нет модулей без `V-<M>` |
|
|
179
244
|
| `module health` критичных модулей | не `blocked` |
|
|
@@ -203,6 +268,19 @@ health: модулей без verification=<N>
|
|
|
203
268
|
doubled-graph health --repo .
|
|
204
269
|
Работай по спискам map_pending / placeholders / markup_pending из вывода.
|
|
205
270
|
|
|
271
|
+
Шаг 0. Context-документы (обязательно до MODULE_MAP и V-<M>):
|
|
272
|
+
- проверь маркеры: grep -n "not specified\|unknown\|Migrated project" .grace/context/*.xml
|
|
273
|
+
- technology.xml: Language/Runtime/Framework/TestingStack/Observability — из <LegacySource> (старый documents/technology.xml),
|
|
274
|
+
затем из манифестов проекта (find . -maxdepth 3: *.csproj/*.sln, package.json, pyproject.toml/pytest.ini, go.mod,
|
|
275
|
+
Cargo.toml, pom.xml/build.gradle, Gemfile, composer.json, Package.swift, mix.exs, CMakeLists.txt/Makefile), затем README/CI;
|
|
276
|
+
TestingStack = команда раннера из манифеста (не догадка);
|
|
277
|
+
- requirements.xml: ProjectName/Goals/Users/Constraints — из <LegacySource> (UC-* -> Goal текстом, ACTOR/User -> Users,
|
|
278
|
+
Constraint/NFR -> Constraints), затем README;
|
|
279
|
+
- deployment.xml/ux-guidelines.xml: по инфраструктурным/фронтенд-манифестам; если их нет — <Applicability>not-applicable</Applicability>
|
|
280
|
+
+ <Reason>;
|
|
281
|
+
- если факт не найден — поле оставь "not specified" и добавь строку в docs/DRIFT.md; НЕ выдумывай стек.
|
|
282
|
+
- проверь: doubled-graph migrate check --repo . -> context_gaps = []
|
|
283
|
+
|
|
206
284
|
Шаг 1. MODULE_MAP для каждого пути из map_pending:
|
|
207
285
|
- прочитай контракт: doubled-graph file show <path> --repo .;
|
|
208
286
|
- извлеки публичные символы командой `grep` по правилам для языка:
|
|
@@ -250,6 +328,11 @@ health: модулей без verification=<N>
|
|
|
250
328
|
Если lint вернул project.grace3-detected — выполни doubled-graph migrate apply --repo . и повтори шаги 1–3.
|
|
251
329
|
Если grace_engine.upgrade_required = true — npm install -g @osovv/grace-cli@4.2.1 и повтори setup.
|
|
252
330
|
|
|
331
|
+
Шаг 4a. Context gate (обязательно):
|
|
332
|
+
grep -nE "not specified|unknown|Migrated project|Describe the primary|replace with the real product statement|Keep the project governed|Existing project maintainers|Legacy GRACE 3 documents stay in place|No operational constraints were declared|Users were not described|Restore the product intent|Not configured yet" .grace/context/*.xml
|
|
333
|
+
doubled-graph migrate check --repo . (context_gaps = [])
|
|
334
|
+
Находки вне <LegacySource> исправляй по шагу 0; TestingStack-заглушка блокирует шаг 2.
|
|
335
|
+
|
|
253
336
|
Шаг 5. Отчёт в фиксированном формате:
|
|
254
337
|
MODULE_MAP: <N дополнено, K NONE/CONFIG>
|
|
255
338
|
V-<M>: <N заполнено и прогоняется, K без команды>
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "doubled-graph"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "MCP facade over CodeGraphContext + GRACE 4 (.grace artifacts): impact, context, drift-detect, migration for LLM-driven codebases."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -325,7 +325,7 @@ def _cmd_health(args: argparse.Namespace) -> int:
|
|
|
325
325
|
drift_md = repo / "docs" / "DRIFT.md"
|
|
326
326
|
open_drift_entries = 0
|
|
327
327
|
if drift_md.exists():
|
|
328
|
-
text = drift_md.read_text(encoding="utf-8")
|
|
328
|
+
text = drift_md.read_text(encoding="utf-8-sig")
|
|
329
329
|
# Sections separated by `## D-`; resolved if section contains "Resolved"
|
|
330
330
|
sections = [s for s in text.split("\n## D-") if s.strip()]
|
|
331
331
|
open_drift_entries = sum(1 for s in sections if "Resolved" not in s and "resolved" not in s)
|
|
@@ -47,9 +47,9 @@ def _load_yaml(p: Path) -> dict:
|
|
|
47
47
|
except ModuleNotFoundError:
|
|
48
48
|
import json as _json
|
|
49
49
|
|
|
50
|
-
with p.open("r", encoding="utf-8") as f:
|
|
50
|
+
with p.open("r", encoding="utf-8-sig") as f:
|
|
51
51
|
return _json.load(f)
|
|
52
|
-
with p.open("r", encoding="utf-8") as f:
|
|
52
|
+
with p.open("r", encoding="utf-8-sig") as f:
|
|
53
53
|
return yaml.safe_load(f) or {}
|
|
54
54
|
|
|
55
55
|
|
|
@@ -61,7 +61,7 @@ def load_config(repo_path: Path) -> Config:
|
|
|
61
61
|
if p.suffix in (".yaml", ".yml"):
|
|
62
62
|
data = _load_yaml(p)
|
|
63
63
|
else:
|
|
64
|
-
with p.open("r", encoding="utf-8") as f:
|
|
64
|
+
with p.open("r", encoding="utf-8-sig") as f:
|
|
65
65
|
data = json.load(f)
|
|
66
66
|
return Config(**data)
|
|
67
67
|
|
|
@@ -61,6 +61,11 @@ class DeclaredModule:
|
|
|
61
61
|
paths: list[str] = field(default_factory=list)
|
|
62
62
|
exports: list[str] = field(default_factory=list)
|
|
63
63
|
crosslinks: list[str] = field(default_factory=list)
|
|
64
|
+
#: GRACE 3 declared these as attributes; losing them silently rewrites
|
|
65
|
+
#: module identity/status during migration, so keep them.
|
|
66
|
+
name: str = ""
|
|
67
|
+
status: str = ""
|
|
68
|
+
type: str = ""
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
@dataclass
|
|
@@ -165,20 +170,66 @@ def _load_grace4(root: Path, scan_files: bool) -> DeclaredGraph:
|
|
|
165
170
|
# ---------------------------------------------------------------------------
|
|
166
171
|
|
|
167
172
|
|
|
173
|
+
def _read_xml_text(path: Path) -> str:
|
|
174
|
+
"""Read a legacy XML file whatever its encoding (UTF-8/UTF-16/BOM).
|
|
175
|
+
|
|
176
|
+
GRACE 3 projects were written by whatever editor the team used; a UTF-16
|
|
177
|
+
document is legal XML and must not abort a migration with a traceback.
|
|
178
|
+
"""
|
|
179
|
+
raw = path.read_bytes()
|
|
180
|
+
for encoding in ("utf-8-sig", "utf-16", "utf-32", "cp1251"):
|
|
181
|
+
try:
|
|
182
|
+
return raw.decode(encoding)
|
|
183
|
+
except (UnicodeDecodeError, UnicodeError, LookupError):
|
|
184
|
+
continue
|
|
185
|
+
return raw.decode("utf-8", errors="replace")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _strip_namespaces(root: ET.Element) -> ET.Element:
|
|
189
|
+
"""Drop ``{namespace}`` prefixes so tag matching works on namespaced XML.
|
|
190
|
+
|
|
191
|
+
Legacy documents occasionally declare a default ``xmlns``; ElementTree then
|
|
192
|
+
reports tags as ``{urn}M-AUTH`` and every ``^M-`` regex misses, which
|
|
193
|
+
silently produced a zero-module migration.
|
|
194
|
+
"""
|
|
195
|
+
for element in root.iter():
|
|
196
|
+
if isinstance(element.tag, str) and "}" in element.tag:
|
|
197
|
+
element.tag = element.tag.rsplit("}", 1)[-1]
|
|
198
|
+
for key in list(element.attrib):
|
|
199
|
+
if "}" in key:
|
|
200
|
+
element.attrib[key.rsplit("}", 1)[-1]] = element.attrib.pop(key)
|
|
201
|
+
return root
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _attribute(element: ET.Element, *names: str) -> str:
|
|
205
|
+
"""Attribute value by any of ``names``, case-insensitively."""
|
|
206
|
+
for key, value in element.attrib.items():
|
|
207
|
+
if key.lower() in {name.lower() for name in names}:
|
|
208
|
+
text = (value or "").strip()
|
|
209
|
+
if text:
|
|
210
|
+
return text
|
|
211
|
+
return ""
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _normalize_path(value: str) -> str:
|
|
215
|
+
"""Store paths in a single portable form (forward slashes)."""
|
|
216
|
+
return value.replace("\\", "/").strip()
|
|
217
|
+
|
|
218
|
+
|
|
168
219
|
def _safe_parse(path: Path) -> tuple[ET.Element | None, str]:
|
|
169
220
|
"""Parse XML, escaping bare ``&`` on retry. Returns (root, error_msg)."""
|
|
170
221
|
if not path.exists():
|
|
171
222
|
return None, ""
|
|
172
|
-
raw = path
|
|
223
|
+
raw = _read_xml_text(path)
|
|
173
224
|
try:
|
|
174
|
-
return ET.fromstring(raw), ""
|
|
225
|
+
return _strip_namespaces(ET.fromstring(raw)), ""
|
|
175
226
|
except ET.ParseError:
|
|
176
227
|
pass
|
|
177
228
|
sanitized = _BARE_AMP.sub("&", raw)
|
|
178
229
|
if sanitized == raw:
|
|
179
230
|
return None, f"XML parse error in {path.name}"
|
|
180
231
|
try:
|
|
181
|
-
return ET.fromstring(sanitized), ""
|
|
232
|
+
return _strip_namespaces(ET.fromstring(sanitized)), ""
|
|
182
233
|
except ET.ParseError as exc:
|
|
183
234
|
return None, f"XML parse error in {path.name}: {exc}"
|
|
184
235
|
|
|
@@ -215,12 +266,16 @@ def _merge_dev_plan_module(graph: DeclaredGraph, mod: ET.Element) -> None:
|
|
|
215
266
|
if purpose and not existing.purpose:
|
|
216
267
|
existing.purpose = purpose
|
|
217
268
|
|
|
269
|
+
existing.name = existing.name or _attribute(mod, "NAME", "Name")
|
|
270
|
+
existing.status = existing.status or _attribute(mod, "STATUS", "Status")
|
|
271
|
+
existing.type = existing.type or _attribute(mod, "TYPE", "Type")
|
|
272
|
+
|
|
218
273
|
for src in mod.findall("target/source"):
|
|
219
274
|
if src.text:
|
|
220
|
-
_append_unique(existing.paths, src.text
|
|
275
|
+
_append_unique(existing.paths, _normalize_path(src.text))
|
|
221
276
|
for tst in mod.findall("target/tests"):
|
|
222
277
|
if tst.text:
|
|
223
|
-
_append_unique(existing.paths, tst.text
|
|
278
|
+
_append_unique(existing.paths, _normalize_path(tst.text))
|
|
224
279
|
|
|
225
280
|
for el in mod.findall("interface/*"):
|
|
226
281
|
name = _export_name(el.tag or "")
|
|
@@ -238,9 +293,13 @@ def _merge_kg_module(graph: DeclaredGraph, mod: ET.Element) -> None:
|
|
|
238
293
|
if purpose and not existing.purpose:
|
|
239
294
|
existing.purpose = purpose
|
|
240
295
|
|
|
296
|
+
existing.name = existing.name or _attribute(mod, "NAME", "Name")
|
|
297
|
+
existing.status = existing.status or _attribute(mod, "STATUS", "Status")
|
|
298
|
+
existing.type = existing.type or _attribute(mod, "TYPE", "Type")
|
|
299
|
+
|
|
241
300
|
for el in mod.findall("path"):
|
|
242
301
|
if el.text:
|
|
243
|
-
_append_unique(existing.paths, el.text
|
|
302
|
+
_append_unique(existing.paths, _normalize_path(el.text))
|
|
244
303
|
|
|
245
304
|
for el in mod.findall("annotations/*"):
|
|
246
305
|
name = _export_name(el.tag or "")
|
|
@@ -282,8 +341,8 @@ def _load_legacy(repo_path: Path) -> DeclaredGraph:
|
|
|
282
341
|
for mod in _iter_module_tags(root):
|
|
283
342
|
_merge_kg_module(graph, mod)
|
|
284
343
|
for cl in root.iter("CrossLink"):
|
|
285
|
-
src_id = cl
|
|
286
|
-
tgt_id = cl
|
|
344
|
+
src_id = _attribute(cl, "from", "source", "src")
|
|
345
|
+
tgt_id = _attribute(cl, "to", "target", "dst")
|
|
287
346
|
if not src_id or not tgt_id:
|
|
288
347
|
continue
|
|
289
348
|
mod = graph.modules.get(src_id)
|
|
@@ -109,7 +109,9 @@ _BARE_AMP = re.compile(r"&(?!amp;|lt;|gt;|apos;|quot;|#\d+;|#x[0-9a-fA-F]+;)")
|
|
|
109
109
|
_PATH_FIELD = re.compile(r"(?:Path|File)\s+([^\s]+)")
|
|
110
110
|
_LINKS_VALUE = re.compile(r"(?im)^\s*(?://|#|--|;+|\*)\s*LINKS:\s*(?P<value>.+)$")
|
|
111
111
|
_CONTRACT_SECTION = re.compile(
|
|
112
|
-
|
|
112
|
+
# ``\ufeff?``: a BOM may survive when the text did not come from a
|
|
113
|
+
# ``utf-8-sig`` read; Python's ``\s`` does not match U+FEFF (JS does).
|
|
114
|
+
r"(?ms)^\ufeff?\s*(?://|#|--|;+|\*)\s*START_MODULE_CONTRACT\s*$(?P<body>.*?)^\s*(?://|#|--|;+|\*)\s*END_MODULE_CONTRACT\s*$"
|
|
113
115
|
)
|
|
114
116
|
|
|
115
117
|
|
|
@@ -247,7 +249,7 @@ def legacy_documents(root: Path) -> list[str]:
|
|
|
247
249
|
def _safe_parse(path: Path) -> tuple[ET.Element | None, str]:
|
|
248
250
|
"""Parse XML, escaping bare ``&`` on retry. Returns (root, error message)."""
|
|
249
251
|
try:
|
|
250
|
-
raw = path.read_text(encoding="utf-8")
|
|
252
|
+
raw = path.read_text(encoding="utf-8-sig")
|
|
251
253
|
except OSError as exc:
|
|
252
254
|
return None, f"cannot read {path.name}: {exc}"
|
|
253
255
|
try:
|
|
@@ -469,7 +471,10 @@ def scan_governed_links(
|
|
|
469
471
|
issues: list[Issue] = []
|
|
470
472
|
for file_path in iter_code_files(resolved, ignored):
|
|
471
473
|
try:
|
|
472
|
-
|
|
474
|
+
# `utf-8-sig` strips a BOM: Python's `\s` does not match U+FEFF (the
|
|
475
|
+
# JS engine's `\s` does), so a BOM would hide every marker from our
|
|
476
|
+
# `^\s*//` regex while `grace lint` still sees the file.
|
|
477
|
+
text = file_path.read_text(encoding="utf-8-sig", errors="replace")
|
|
473
478
|
except OSError:
|
|
474
479
|
continue
|
|
475
480
|
section = _CONTRACT_SECTION.search(text)
|
|
@@ -61,7 +61,7 @@ def _install_post_commit(repo: Path, dry_run: bool) -> dict:
|
|
|
61
61
|
|
|
62
62
|
# Soft-safety: if file exists and is non-empty AND doesn't contain our signature — back up.
|
|
63
63
|
if target.exists():
|
|
64
|
-
existing = target.read_text(encoding="utf-8", errors="replace")
|
|
64
|
+
existing = target.read_text(encoding="utf-8-sig", errors="replace")
|
|
65
65
|
if "doubled-graph init-hooks" not in existing:
|
|
66
66
|
backup = target.with_suffix(".backup")
|
|
67
67
|
target.rename(backup)
|
|
@@ -76,13 +76,25 @@ def _install_post_commit(repo: Path, dry_run: bool) -> dict:
|
|
|
76
76
|
return {"post_commit": "installed", "path": str(target), "note": replaced}
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
_BOM_PREFIX = b"\xef\xbb\xbf"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _had_bom(path: Path) -> bool:
|
|
83
|
+
"""True when an existing file starts with a UTF-8 BOM."""
|
|
84
|
+
try:
|
|
85
|
+
return path.read_bytes().startswith(_BOM_PREFIX)
|
|
86
|
+
except OSError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
79
90
|
def _install_claude_code(repo: Path, dry_run: bool) -> dict:
|
|
80
91
|
settings = repo / ".claude" / "settings.json"
|
|
81
92
|
settings.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
had_bom = _had_bom(settings)
|
|
82
94
|
data: dict = {}
|
|
83
95
|
if settings.exists():
|
|
84
96
|
try:
|
|
85
|
-
data = json.loads(settings.read_text(encoding="utf-8"))
|
|
97
|
+
data = json.loads(settings.read_text(encoding="utf-8-sig"))
|
|
86
98
|
except json.JSONDecodeError:
|
|
87
99
|
return {"claude_code": "error", "reason": f"cannot parse {settings}"}
|
|
88
100
|
hooks = data.setdefault("hooks", {})
|
|
@@ -100,7 +112,8 @@ def _install_claude_code(repo: Path, dry_run: bool) -> dict:
|
|
|
100
112
|
return {"claude_code": "dry-run", "path": str(settings), "would_add": CLAUDE_HOOK_ENTRY}
|
|
101
113
|
|
|
102
114
|
post_tool_use.append(CLAUDE_HOOK_ENTRY)
|
|
103
|
-
settings
|
|
115
|
+
# Keep the client's BOM: rewriting a settings file must not re-encode it.
|
|
116
|
+
settings.write_text(json.dumps(data, indent=2), encoding="utf-8-sig" if had_bom else "utf-8")
|
|
104
117
|
return {"claude_code": "installed", "path": str(settings)}
|
|
105
118
|
|
|
106
119
|
|
|
@@ -125,13 +138,13 @@ def ensure_gitignore_entries(repo_path: Path, dry_run: bool = False) -> dict:
|
|
|
125
138
|
so skipping hooks shouldn't leave the user's `git status` polluted.
|
|
126
139
|
"""
|
|
127
140
|
gi = repo_path / ".gitignore"
|
|
128
|
-
existing = gi.read_text(encoding="utf-8") if gi.exists() else ""
|
|
141
|
+
existing = gi.read_text(encoding="utf-8-sig") if gi.exists() else ""
|
|
129
142
|
missing = [line for line in DOUBLED_GRAPH_GITIGNORE_LINES if line not in existing]
|
|
130
143
|
if not missing:
|
|
131
144
|
return {"status": "already_present", "lines": DOUBLED_GRAPH_GITIGNORE_LINES}
|
|
132
145
|
if dry_run:
|
|
133
146
|
return {"status": "dry_run", "would_append": missing}
|
|
134
|
-
with gi.open("a", encoding="utf-8") as f:
|
|
147
|
+
with gi.open("a", encoding="utf-8-sig") as f:
|
|
135
148
|
if existing and not existing.endswith("\n"):
|
|
136
149
|
f.write("\n")
|
|
137
150
|
f.write("\n# doubled-graph\n" + "\n".join(missing) + "\n")
|