From c09901f9a680054c15a65abe05616575d0475fb8 Mon Sep 17 00:00:00 2001 From: vitya Date: Sat, 29 Aug 2026 23:55:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(project-bootstrap):=20.mappa=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=80=D0=BA=D0=B5=D1=80=20=D0=BF=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0=20=E2=80=94=20=D1=88=D0=B0=D0=B3=205.8?= =?UTF-8?q?=20+=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80-=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=B5=D1=82,=20project-create=20=D1=88=D0=B0=D0=B3=205.5=20(wi?= =?UTF-8?q?ki:3340,=20task:1583)=20[v3.3.0]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/project-bootstrap.skill | Bin 17083 -> 25459 bytes scripts/build.ps1 | 2 +- skills/project-bootstrap/SKILL.md | 45 ++- .../assets/dot_mappa_marker.py | 263 ++++++++++++++++ .../assets/test_dot_mappa_marker.py | 290 ++++++++++++++++++ skills/project-create/SKILL.md | 36 ++- 6 files changed, 630 insertions(+), 6 deletions(-) create mode 100644 skills/project-bootstrap/assets/dot_mappa_marker.py create mode 100644 skills/project-bootstrap/assets/test_dot_mappa_marker.py diff --git a/dist/project-bootstrap.skill b/dist/project-bootstrap.skill index 34066ed53ea1382f9f7c2c07a9115fe315c296fc..f802f41c61f5335a4d50ff73e4b65b35d90f58c9 100644 GIT binary patch delta 24117 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 Initializes or upgrades a project in the current folder: git, .gitignore, README.md, .wiki/ using Karpathy's method, .tasks/ for task tracking, AGENTS.md (canon) with @@ -9,6 +9,7 @@ description: > Creates remote Gitea repo and syncs projects-meta cache for greenfield projects. Includes the mappa-bootstrap-project module (v3, решение 4 mappa-as-product): mappa MCP connect + mappa-конвенции + методика-install (версия в манифест). + Creates the `.mappa` marker (wiki:3340) so the folder is a mappa project. Use this skill when the user says "initialize project", "bootstrap", "setup project", "upgrade project", "add wiki", "add tasks", "start project", "set everything up", "create new project", or launches the agent in a new folder and wants a full setup. @@ -36,6 +37,7 @@ ls -A 2>/dev/null | grep -q . && echo "empty:no" || echo "empty:yes" [ -d .tasks ] && echo "tasks:yes" || echo "tasks:no" [ -f CLAUDE.md ] && echo "claude:yes" || echo "claude:no" [ -f README.md ] && echo "readme:yes" || echo "readme:no" +[ -d .mappa ] && echo "mappa-marker:yes" || echo "mappa-marker:no" ``` Determine mode: @@ -682,6 +684,46 @@ install-команда — скил mappa-bootstrap, см. 5.7.3). --- +## Step 5.8 — `.mappa` маркер (контракт wiki:3340) + +Машиночитаемый маркер проекта в корне папки (схема v1 — `.mappa/config.yaml`): +гейт mappa-скилов («без маркера папка не участвует в mappa-операциях», +task:1546) + признак корня проекта для харнессов. Создаётся на bootstrap — +без ручного прогона генератора (task:1583). Детерминированный рендер: +фиксированный порядок полей, без секретов, без timestamp — повторный запуск +no-op (`keep`). + +1. **Собрать значения** (реестр mappa → локальное знание): + - `project` — канон папки (`basename "$PWD"`); если проект уже в реестре + (`projects_resolve`) — сверить, не расходится ли; + - `tenant` — `MAPPA_TENANT` (по умолчанию `vitya`); + - `url` — `MAPPA_CORE_URL` (без trailing slash); + - `git_provider` — из реестра `projects.git_provider` (например `gitea`), + иначе из шага 1.5 (создано через Gitea API → gitea); опционально; + - `git` — `projects.qualified` (owner/repo) из реестра, иначе из remote + шага 1.5; опционально (опустить, если неизвестно). +2. **Записать маркер** (скрипт — ассет этого скила, реализует контракт + wiki:3340; в репо: `skills/project-bootstrap/assets/dot_mappa_marker.py`): + + ```bash + python assets/dot_mappa_marker.py write \ + --project "$(basename "$PWD")" --tenant vitya --url "$MAPPA_CORE_URL" \ + --git-provider gitea --git "$OWNER/$REPO" + ``` + + Без `--git-provider`/`--git`, если поля неизвестны. Повторный прогон — + no-op (`keep`); отличающийся существующий маркер без `--force` НЕ + перезаписывается — покажи diff и спроси (правило «never overwrite»). +3. **Верифицировать**: `python assets/dot_mappa_marker.py check` → exit 0. +4. **Контракт-тест** (TDD, task:1583): `python assets/test_dot_mappa_marker.py` + — «после bootstrap есть `.mappa/config.yaml`», детерминизм, без секретов, + порядок полей, идемпотентность. + +Маркер публичен (без секретов) и попадает в коммит шага 6. Валидный +существующий маркер не трогаем. + +--- + ## Step 6 — Commit ```bash @@ -706,6 +748,7 @@ Print a final report: ✅ Done! Created: .wiki/ — project wiki (Karpathy method) .tasks/ — task tracking system + .mappa/ — mappa project marker (wiki:3340, schema v1) AGENTS.md — skill triggers (canon) CLAUDE.md — legacy pointer .gitignore — standard template diff --git a/skills/project-bootstrap/assets/dot_mappa_marker.py b/skills/project-bootstrap/assets/dot_mappa_marker.py new file mode 100644 index 0000000..a4917a1 --- /dev/null +++ b/skills/project-bootstrap/assets/dot_mappa_marker.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""dot_mappa_marker.py — deterministic render + write of the `.mappa` marker. + +Contract: mappa wiki:3340 (concepts/dot-mappa-marker), schema v1. +Used by project-create (step 5.5) and project-bootstrap (step 5.8) so a project +folder gets its marker at create time — no manual generator run needed +(task:1583). The batch generator (mappa `server/scripts/gen-dot-mappa-markers.ts`) +remains for registry-wide migration; this is the per-project create path. + +Guarantees (the contract): + * `.mappa/config.yaml` — каталог + файл внутри + * fixed field order (schema_version, protocol_version, project, tenant, url, + git_provider?, git?) + * deterministic render — no timestamps, same input → same bytes + * NO secrets — only public registry fields; url with credentials is rejected + * optional fields (`git_provider`, `git`) omitted when absent + * idempotent write: same content → no-op (keep); different content → refuse + without --force + +Usage: + python dot_mappa_marker.py render --project NAME --tenant TENANT --url URL \ + [--git-provider P] [--git OWNER/REPO] # print content to stdout + python dot_mappa_marker.py write --project NAME --tenant TENANT --url URL \ + [--git-provider P] [--git OWNER/REPO] [--dir PATH] [--force] # write marker + python dot_mappa_marker.py check --dir PATH # verify existing marker +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +SCHEMA_VERSION = 1 +PROTOCOL_VERSION = 1 + +# Canonical header comment — same as the contract example (wiki:3340). +HEADER = "# mappa project marker — machine-readable identifier of a mappa project folder" + +# YAML: these are indicator characters / reserved tokens — never plain. +_INDICATOR_START = set("!&*{}[],#|>@`\"'%?:~-") +_RESERVED_PLAIN = {"null", "Null", "NULL", "~", "true", "True", "TRUE", "false", + "False", "FALSE", "yes", "Yes", "YES", "no", "No", "NO", + "on", "On", "ON", "off", "Off", "OFF", "-", "?", ":"} + + +class MarkerConflict(Exception): + """An existing marker differs from the requested one and --force is absent.""" + + +def yaml_scalar(value: str) -> str: + """YAML plain-safe scalar: plain without quotes when safe, else double-quoted. + + Plain-safe = non-empty, no leading indicator, not a reserved token, no flow + chars, no embedded newlines, no surrounding whitespace. Double-quoting uses + JSON escaping, which is a valid subset of YAML double-quoted style. + """ + s = str(value) + if s == "": + return '""' + if s.strip() != s: + return json.dumps(s) + if s[0] in _INDICATOR_START or s in _RESERVED_PLAIN: + return json.dumps(s) + if s.startswith(("- ", "? ", ": ")): + return json.dumps(s) + # plain scalars stay plain unless they would confuse the parser: + # ": " (mapping indicator), trailing ":", " #" (comment), newlines/tabs + if ": " in s or s.endswith(":") or " #" in s or "\n" in s or "\t" in s: + return json.dumps(s) + return s + + +def validate_folder_name(name: str) -> str: + """A folder name (canon/tenant/git_provider) must be a single sane segment.""" + if not name or name in (".", ".."): + raise ValueError(f"invalid name {name!r}: must be a non-empty folder name") + if any(sep in name for sep in ("/", "\\", "\x00")): + raise ValueError(f"invalid name {name!r}: must be a single path segment") + if name != name.strip(): + raise ValueError(f"invalid name {name!r}: no surrounding whitespace allowed") + return name + + +def validate_git_ref(git: str) -> str: + """`git` = projects.qualified (owner/repo) — no credentials, no colon.""" + g = str(git) + if not g or "/" not in g: + raise ValueError(f"invalid git ref {g!r}: expected owner/repo") + if any(c in g for c in ("@", ":", " ", "\t", "\n", "\\")): + raise ValueError(f"invalid git ref {g!r}: no credentials / separators allowed") + if g.startswith("/") or g.endswith("/") or ".." in g.split("/"): + raise ValueError(f"invalid git ref {g!r}: must be owner/repo, not a path") + return g + + +def normalize_url(url: str) -> str: + """Absolute http(s) URL without credentials and without trailing slash.""" + u = str(url).strip() + if not (u.startswith("http://") or u.startswith("https://")): + raise ValueError(f"invalid url {u!r}: must be http(s)://host...") + authority = u.split("://", 1)[1].split("/", 1)[0] + if "@" in authority: + raise ValueError("url must not contain credentials (no secrets in the marker)") + return u.rstrip("/") + + +def render( + project: str, + tenant: str, + url: str, + git_provider: str | None = None, + git: str | None = None, +) -> str: + """Deterministic `.mappa/config.yaml` content per wiki:3340 schema v1.""" + project = validate_folder_name(project) + tenant = validate_folder_name(tenant) + url = normalize_url(url) + lines = [ + HEADER, + f"schema_version: {SCHEMA_VERSION}", + f"protocol_version: {PROTOCOL_VERSION}", + f"project: {yaml_scalar(project)}", + f"tenant: {yaml_scalar(tenant)}", + f"url: {yaml_scalar(url)}", + ] + if git_provider: + lines.append(f"git_provider: {yaml_scalar(validate_folder_name(git_provider))}") + if git: + lines.append(f"git: {yaml_scalar(validate_git_ref(git))}") + return "\n".join(lines) + "\n" + + +def _sane_dir(directory: str | Path) -> Path: + """Resolve the target directory; reject `..` segments and non-dirs.""" + p = Path(directory) + if ".." in p.parts: + raise ValueError(f"invalid directory {str(directory)!r}: '..' segments not allowed") + if p.exists() and not p.is_dir(): + raise ValueError(f"invalid directory {str(directory)!r}: not a directory") + return p + + +def write_marker(directory: str | Path, content: str, force: bool = False) -> tuple[Path, str]: + """Write `.mappa/config.yaml` under `directory`. + + Returns (marker_path, outcome) where outcome is one of + "created" | "keep" (idempotent no-op) | "overwrite" (force). + Raises MarkerConflict when an existing marker differs and force is False. + """ + marker = _sane_dir(directory) / ".mappa" / "config.yaml" + if marker.exists(): + existing = marker.read_text(encoding="utf-8") + if existing == content: + return marker, "keep" + if not force: + raise MarkerConflict( + f"{marker} already exists with different content; " + "pass --force to overwrite (contract: no silent overwrite)" + ) + marker.write_text(content, encoding="utf-8") + return marker, "overwrite" + marker.parent.mkdir(parents=True, exist_ok=True) + marker.write_text(content, encoding="utf-8") + return marker, "created" + + +def _parse_marker_lines(body: str) -> list[tuple[str, str]]: + """(key, value) pairs of data lines — comments skipped, first colon splits.""" + pairs = [] + for line in body.splitlines(): + if not line or line.startswith("#"): + continue + if ": " not in line: + raise ValueError(f"malformed marker line (no 'key: value'): {line!r}") + key, value = line.split(": ", 1) + pairs.append((key, value.strip())) + return pairs + + +def check_marker(directory: str | Path) -> tuple[bool, str]: + """Gate check (wiki:3340 / task:1546): is `directory` a mappa project? + + Returns (ok, message). ok means `.mappa/config.yaml` exists and its data + lines start with exactly the required fields (schema_version, + protocol_version, project, tenant, url) in canonical order with valid + values; optional `git_provider`/`git` may follow. + """ + marker = _sane_dir(directory) / ".mappa" / "config.yaml" + if not marker.is_file(): + return False, f"no marker: {marker} (folder without marker is not a mappa project)" + try: + pairs = _parse_marker_lines(marker.read_text(encoding="utf-8")) + except ValueError as e: + return False, f"marker {marker}: {e}" + if len(pairs) < 5: + return False, f"marker {marker}: fewer than the 5 required fields" + required = ["schema_version", "protocol_version", "project", "tenant", "url"] + if [k for k, _ in pairs[:5]] != required: + return False, f"marker {marker}: field order mismatch ({[k for k, _ in pairs[:5]]})" + values = dict(pairs) + if values["schema_version"] != str(SCHEMA_VERSION): + return False, f"marker {marker}: schema_version must be {SCHEMA_VERSION}" + if values["protocol_version"] != str(PROTOCOL_VERSION): + return False, f"marker {marker}: protocol_version must be {PROTOCOL_VERSION}" + try: + validate_folder_name(values["project"]) + validate_folder_name(values["tenant"]) + normalize_url(values["url"]) + if "git_provider" in values: + validate_folder_name(values["git_provider"]) + if "git" in values: + validate_git_ref(values["git"]) + except ValueError as e: + return False, f"marker {marker}: {e}" + return True, f"marker ok: {marker}" + + +def _add_common(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--project", required=True, help="канон папки = реестр projects.name (slug)") + parser.add_argument("--tenant", required=True, help="тенант, где живёт проект (MAPPA_TENANT)") + parser.add_argument("--url", required=True, help="MAPPA_CORE_URL (без trailing slash)") + parser.add_argument("--git-provider", default=None, help="projects.git_provider (gitea/...) — опционально") + parser.add_argument("--git", default=None, help="projects.qualified (owner/repo) — опционально") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=".mappa marker per wiki:3340 (schema v1)") + sub = parser.add_subparsers(dest="cmd", required=True) + + p_render = sub.add_parser("render", help="print deterministic marker content") + _add_common(p_render) + + p_write = sub.add_parser("write", help="write .mappa/config.yaml into a folder") + _add_common(p_write) + p_write.add_argument("--dir", default=".", help="project folder (default: cwd)") + p_write.add_argument("--force", action="store_true", help="overwrite a differing marker") + + p_check = sub.add_parser("check", help="gate check: is the folder a mappa project?") + p_check.add_argument("--dir", default=".", help="project folder (default: cwd)") + + args = parser.parse_args(argv) + + if args.cmd in ("render", "write"): + content = render(args.project, args.tenant, args.url, args.git_provider, args.git) + if args.cmd == "render": + sys.stdout.write(content) + return 0 + marker, outcome = write_marker(args.dir, content, force=args.force) + print(f"{outcome}: {marker}") + return 0 + + if args.cmd == "check": + ok, msg = check_marker(args.dir) + print(msg) + return 0 if ok else 1 + + return 2 # unreachable + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/project-bootstrap/assets/test_dot_mappa_marker.py b/skills/project-bootstrap/assets/test_dot_mappa_marker.py new file mode 100644 index 0000000..35a2190 --- /dev/null +++ b/skills/project-bootstrap/assets/test_dot_mappa_marker.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""Contract test for the `.mappa` marker — mappa wiki:3340 (concepts/dot-mappa-marker). + +The contract under test (task:1583): after the project-create/bootstrap marker +step, the project folder contains `.mappa/config.yaml` matching schema v1: +fixed field order, deterministic render (no timestamps), NO secrets, optional +fields (`git_provider`, `git`) omitted when absent, idempotent write. + +Run: python test_dot_mappa_marker.py (or: python -m unittest test_dot_mappa_marker) +""" + +from __future__ import annotations + +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import dot_mappa_marker as dmm # noqa: E402 + +CANON = "skills" # registry projects.name — канон папки (slug) +TENANT = "vitya" +URL = "https://mappa.vds.kzntsv.site" +GIT_PROVIDER = "gitea" +GIT = "OpeItcLoc03/skills" + +FIELD_ORDER = [ + "schema_version", + "protocol_version", + "project", + "tenant", + "url", + "git_provider", + "git", +] + + +def field_keys(body: str) -> list[str]: + return [ + line.split(":", 1)[0] + for line in body.splitlines() + if line and not line.startswith("#") and ": " in line + ] + + +def write_contract_marker(tmp: str) -> Path: + """Helper: create a valid marker as the bootstrap step would.""" + marker, outcome = dmm.write_marker(tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)) + assert outcome == "created" + return marker + + +class ContractTests(unittest.TestCase): + """Contract: after bootstrap there is `.mappa/config.yaml` (task:1583).""" + + def setUp(self) -> None: + self.tmp = tempfile.mkdtemp(prefix="mappa-marker-test-") + + def tearDown(self) -> None: + shutil.rmtree(self.tmp, ignore_errors=True) + + # --- presence / shape ------------------------------------------------- + + def test_bootstrap_marker_step_creates_config_yaml(self) -> None: + """The bootstrap marker step leaves `.mappa/config.yaml` in the folder.""" + content = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + marker, outcome = dmm.write_marker(self.tmp, content) + self.assertEqual(outcome, "created") + self.assertTrue(marker.is_file()) + self.assertEqual(marker.name, "config.yaml") + self.assertEqual(marker.parent.name, ".mappa") + + def test_fixed_field_order(self) -> None: + body = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + self.assertEqual(field_keys(body), FIELD_ORDER) + + def test_deterministic_render_no_timestamp(self) -> None: + a = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + b = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + self.assertEqual(a, b) + # no ISO-date-like content + self.assertNotRegex(a, r"\d{4}-\d{2}-\d{2}") + + def test_optional_fields_omitted_when_absent(self) -> None: + body = dmm.render(CANON, TENANT, URL) + self.assertEqual(field_keys(body), FIELD_ORDER[:5]) + self.assertNotIn("git_provider", body) + self.assertNotIn("\ngit:", body) + + def test_no_secrets_in_marker(self) -> None: + body = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + lowered = body.lower() + # credentials in the url authority are rejected separately + for secret in ("token", "password", "secret", "api_key", "key:", "@"): + self.assertNotIn(secret, lowered) + + # --- idempotent write -------------------------------------------------- + + def test_idempotent_write_keeps_same_content(self) -> None: + content = dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT) + marker, first = dmm.write_marker(self.tmp, content) + marker, second = dmm.write_marker(self.tmp, content) + self.assertEqual(first, "created") + self.assertEqual(second, "keep") + self.assertEqual(marker.read_text(encoding="utf-8"), content) + + def test_refuses_overwrite_of_different_marker_without_force(self) -> None: + dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)) + with self.assertRaises(dmm.MarkerConflict): + dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, "github", GIT)) + + def test_force_overwrites_different_marker(self) -> None: + dmm.write_marker(self.tmp, dmm.render(CANON, TENANT, URL, GIT_PROVIDER, GIT)) + marker, outcome = dmm.write_marker( + self.tmp, dmm.render(CANON, TENANT, URL, "github", GIT), force=True + ) + self.assertEqual(outcome, "overwrite") + self.assertIn("git_provider: github", marker.read_text(encoding="utf-8")) + + # --- input validation --------------------------------------------------- + + def test_folder_name_path_segments_rejected(self) -> None: + for bad in ("../evil", "a/b", "a\\b", ".", "..", ""): + with self.assertRaises(ValueError, msg=f"name {bad!r} must be rejected"): + dmm.render(bad, TENANT, URL) + + def test_url_trailing_slash_stripped_but_path_kept(self) -> None: + body = dmm.render(CANON, TENANT, URL + "//") + self.assertIn(f"url: {URL}", body) + # a trailing slash after a path must be stripped, the path kept + body2 = dmm.render(CANON, TENANT, "https://example.com/mappa/") + self.assertIn("url: https://example.com/mappa", body2) + + def test_url_with_credentials_rejected(self) -> None: + with self.assertRaises(ValueError): + dmm.render(CANON, TENANT, "https://user:pass@mappa.vds.kzntsv.site") + + def test_url_scheme_restricted_to_http_https(self) -> None: + for bad in ("ftp://mappa.example", "javascript://x", "mappa.vds.kzntsv.site", "://x"): + with self.assertRaises(ValueError, msg=f"url {bad!r} must be rejected"): + dmm.render(CANON, TENANT, bad) + + def test_git_ref_with_credentials_rejected(self) -> None: + for bad in ("user:pass@host/repo", "victor/repo@token", "../config", "/owner/repo", "owner/repo/", "owner repo", "norepo"): + with self.assertRaises(ValueError, msg=f"git {bad!r} must be rejected"): + dmm.render(CANON, TENANT, URL, GIT_PROVIDER, bad) + + def test_directory_with_parent_segments_rejected(self) -> None: + with self.assertRaises(ValueError): + dmm.write_marker("some/../elsewhere", dmm.render(CANON, TENANT, URL)) + with self.assertRaises(ValueError): + dmm.check_marker("../etc") + + # --- YAML scalar edge cases ------------------------------------------- + + def test_yaml_scalar_quoting_edge_cases(self) -> None: + # reserved tokens / indicators must be double-quoted (never plain) + for special in ("~", "@host", "-", "?", ":", "null", "yes", "on", "true", + "a: b", "ends:", " #lead", "has tab\tinside"): + self.assertTrue(dmm.yaml_scalar(special).startswith('"'), + f"{special!r} must be double-quoted, got {dmm.yaml_scalar(special)!r}") + # plain-safe values stay plain + for plain in ("vitya", "OpeItcLoc03/skills", "https://mappa.vds.kzntsv.site", + "a:b", "x#y", "lead#ing", "my-proj"): + self.assertEqual(dmm.yaml_scalar(plain), plain) + self.assertEqual(dmm.yaml_scalar(""), '""') + + # --- check_marker (gate) ---------------------------------------------- + + def test_check_ok_on_valid_marker(self) -> None: + write_contract_marker(self.tmp) + ok, msg = dmm.check_marker(self.tmp) + self.assertTrue(ok, msg) + + def test_check_fails_on_missing_marker(self) -> None: + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_fails_on_wrong_field_order(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "# c\nproject: skills\nschema_version: 1\nprotocol_version: 1\n" + "tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_fails_on_extra_field_before_required(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "extra: sneaky\nschema_version: 1\nprotocol_version: 1\n" + "project: skills\ntenant: vitya\nurl: https://mappa.vds.kzntsv.site\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_fails_on_wrong_versions(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "schema_version: 2\nprotocol_version: 1\nproject: skills\n" + "tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_fails_on_malicious_project_value(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "schema_version: 1\nprotocol_version: 1\nproject: ../../evil\n" + "tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_fails_on_malformed_line(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "schema_version: 1\nprotocol_version: 1\nproject skills\n" + "tenant: vitya\nurl: https://mappa.vds.kzntsv.site\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertFalse(ok) + + def test_check_accepts_url_with_port(self) -> None: + (Path(self.tmp) / ".mappa").mkdir() + (Path(self.tmp) / ".mappa" / "config.yaml").write_text( + "schema_version: 1\nprotocol_version: 1\nproject: skills\n" + "tenant: vitya\nurl: https://mappa.example:8443\n", + encoding="utf-8", + ) + ok, _ = dmm.check_marker(self.tmp) + self.assertTrue(ok) + + # --- CLI end-to-end ---------------------------------------------------- + + def test_cli_write_creates_marker(self) -> None: + """End-to-end: the documented CLI command produces the marker.""" + proc = subprocess.run( + [ + sys.executable, + str(Path(__file__).resolve().parent / "dot_mappa_marker.py"), + "write", + "--project", CANON, + "--tenant", TENANT, + "--url", URL, + "--git-provider", GIT_PROVIDER, + "--git", GIT, + "--dir", self.tmp, + ], + capture_output=True, + text=True, + ) + self.assertEqual(proc.returncode, 0, proc.stderr) + marker = Path(self.tmp) / ".mappa" / "config.yaml" + self.assertTrue(marker.is_file()) + self.assertEqual(field_keys(marker.read_text(encoding="utf-8")), FIELD_ORDER) + + def test_cli_check_verifies_marker(self) -> None: + write_contract_marker(self.tmp) + script = Path(__file__).resolve().parent / "dot_mappa_marker.py" + ok = subprocess.run( + [sys.executable, str(script), "check", "--dir", self.tmp], + capture_output=True, + text=True, + ) + self.assertEqual(ok.returncode, 0, ok.stderr) + # check on an empty dir fails (gate semantics: no marker → not a mappa project) + empty = tempfile.mkdtemp(prefix="mappa-marker-empty-") + try: + missing = subprocess.run( + [sys.executable, str(script), "check", "--dir", empty], + capture_output=True, + text=True, + ) + self.assertNotEqual(missing.returncode, 0) + finally: + shutil.rmtree(empty, ignore_errors=True) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/skills/project-create/SKILL.md b/skills/project-create/SKILL.md index 1ce9293..b5bd680 100644 --- a/skills/project-create/SKILL.md +++ b/skills/project-create/SKILL.md @@ -1,15 +1,16 @@ --- name: project-create author: ours -version: 0.1.1 +version: 0.2.0 description: > Mappa-side cycle of creating a new project: ask the operator for the hosting address (platform + user/org) FIRST — never derive it from neighbouring projects — then pre-flight checks (free in mappa AND in gitea), then create mappa registration and the gitea repo SIMULTANEOUSLY (repo via .admin: task + covering letter, priority P0, no paired review for ops), then ask where on - disk the project folder goes and what it's named, create the folder, and - hand over to project-bootstrap (general skill). Triggers (bilingual): + disk the project folder goes and what it's named, create the folder, write + the `.mappa` marker (wiki:3340), and hand over to project-bootstrap (general + skill). Triggers (bilingual): «создай проект», «заведи проект», «новый проект», «создать проект на гите», "create a project", "start a new project", "set up a project", «куда разместить проект». NOT repo content/bootstrap (→ project-bootstrap), @@ -51,7 +52,34 @@ Before any `projects_register` / gitea repo creation for a **new** project. - repo via `.admin` — `task_create` in `.admin` (**priority P0** — it blocks the project chain) + covering letter via `inbox_send` (a task on the board doesn't ping a live session). The repo task is an **ops task → NO paired review** (mappa-delegation: skip review for ops). Acceptance: repo created private, answer letter with clone URLs + which token is needed for push. 4. **Ask the operator: where on disk the project folder goes and what it's named** (location AND folder name — both are the operator's call) — do not guess the path or the name. 5. **Create the folder** on the agent's local filesystem (the operator's workstation, not a remote host). -6. **Hand over to bootstrap** — `project-bootstrap` (general skill) does git init, .gitignore, README, AGENTS.md, remote connect, push. Separate step, after the repo exists. +6. **Write the `.mappa` marker** — see Step 5.5 below. Right after the folder exists, the marker values are known from this cycle; no manual generator run. +7. **Hand over to bootstrap** — `project-bootstrap` (general skill) does git init, .gitignore, README, AGENTS.md, remote connect, push. Separate step, after the repo exists. + +## Step 5.5 — `.mappa` маркер (контракт wiki:3340) + +Сразу после создания папки — маркер `.mappa/config.yaml` (гейт mappa-скилов: +«без маркера папка не участвует в mappa-операциях», task:1546). Значения уже +известны из этого цикла — ручной прогон генератора не нужен (task:1583): + +- `project` — канон (имя папки, выбрано оператором в шаге 4); +- `tenant` — `MAPPA_TENANT` (по умолчанию `vitya`); +- `url` — `MAPPA_CORE_URL` (без trailing slash); +- `git_provider` — платформа из шага 1 (gitea/github/…); +- `git` — qualified (owner/repo) из ответа `.admin` (шаг 3, clone URL). + +Запись — детерминированный рендер по контракту. Канон-скрипт — ассет +`project-bootstrap` (в репо: `skills/project-bootstrap/assets/dot_mappa_marker.py`; +тот же шаг в bootstrap 5.8 — повторный прогон там no-op): + +```bash +python /skills/project-bootstrap/assets/dot_mappa_marker.py write \ + --project "$FOLDER_NAME" --tenant vitya --url "$MAPPA_CORE_URL" \ + --git-provider gitea --git "$OWNER/$REPO" --dir "$FOLDER_PATH" +``` + +Верифицировать: `python .../dot_mappa_marker.py check --dir "$FOLDER_PATH"` +→ exit 0. Маркер без секретов, коммитится. Если project-bootstrap не +установлен — маркер всё равно появится на хэндовере (шаг 7 → bootstrap 5.8). ## Why the repo goes through `.admin`