From fc9b4e0563cf51bc61568eddd15df3500e0169b0 Mon Sep 17 00:00:00 2001 From: Rafael Richards Date: Sun, 10 May 2026 23:00:21 -0400 Subject: [PATCH 1/2] phase2-onboarding: adopt Phase 0 contract for m-stdlib-vscode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier-2 onboarding per AI-discoverability-plan.md §3.4. This repo had no CLAUDE.md to rename — AGENTS.md is authored from scratch with the container-tier frontmatter style (kind / status / languages / distribution / exposes / consumes / companions / incompatibilities) and the five required Phase-0 sections (Setup / Test / Build / Verify / Guardrails). CLAUDE.md is a symlink. dist/extension-info.json mirrors the relevant package.json values (name, publisher, version, license, marketplace_id, engines, settings, manifest_discovery_order, snippets_path) — hand-authored, sorted keys, 2-space indent, trailing newline. dist/repo.meta.json is the Phase-0 contract entry: exposes extension_info + package_json, consumes tool:m-stdlib, verification_commands = make check-manifest, license MIT. tools/check-manifest.py is the same stdlib-only validator that landed in tree-sitter-m + m-test-engine. Makefile carries the three Phase-0 targets (manifest informational no-op, check-manifest, check-docs-prose). .github/workflows/ci.yml is this repo's first CI workflow: the Phase-0 manifest + docs-prose gates run before npm install, then npm test + npm run compile. .gitignore did not ignore dist/ — no exception line needed. --- .github/workflows/ci.yml | 39 +++++++++ AGENTS.md | 166 +++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + Makefile | 51 ++++++++++++ dist/extension-info.json | 44 +++++++++++ dist/repo.meta.json | 17 ++++ tools/check-manifest.py | 108 +++++++++++++++++++++++++ 7 files changed, 426 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 Makefile create mode 100644 dist/extension-info.json create mode 100644 dist/repo.meta.json create mode 100755 tools/check-manifest.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e806e8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Phase-0 manifest contract gate + # Asserts dist/repo.meta.json parses, carries all required fields, + # and every exposes.* path resolves on disk. Cross-repo tier-2 + # contract per .github/docs/AI-discoverability-plan.md §3.4. + # Runs before npm install so a broken manifest fails fast. + run: make check-manifest + + - name: docs/ prose-only gate + # Cross-repo guardrail: docs/ holds only human-readable prose. + run: make check-docs-prose + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Build + test + # `npm test` is `tsc -p ./ --noEmit && node --test tests/*.test.ts` + # — it type-checks src/ + tests/ and runs the unit suite. + run: npm test + + - name: Compile extension + # Produces out/extension.js — the marketplace entry point. + run: npm run compile diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e326583 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,166 @@ +--- +# Machine-readable project descriptor — schema v1 (2026-05-05). +name: m-stdlib-vscode +kind: [editor-extension, language-tooling] +status: active +languages: [typescript] + +distribution: + github: m-dev-tools/m-stdlib-vscode + marketplace_id: rafael5.m-stdlib-vscode + +location: ~/m-dev-tools/m-stdlib-vscode + +exposes: + extension_info: "dist/extension-info.json" + package_json: "package.json" + snippets: "snippets/m.json" + bundled_manifest: "assets/stdlib-manifest.json (snapshot of m-stdlib/dist/stdlib-manifest.json, refreshed at release time)" + +consumes: + formats: + - "m-stdlib/dist/stdlib-manifest.json (schema v1)" + services: [] + upstream_data: + - "m-stdlib — runtime input: hover docs, goto-def line numbers, completion lists all come from the resolved stdlib-manifest.json" + +companions: + - project: m-stdlib + relation: "primary input — the manifest this extension reads is m-stdlib's `make manifest` output; m-stdlib has architectural priority" + - project: m-cli + relation: "shares the `$M_CLI_MANIFEST` env var as a discovery hint; if a user has m-cli set up, the extension picks up the same manifest without per-workspace config" + - project: tree-sitter-m-vscode + relation: "sibling editor extension — different concern (syntax highlighting via tree-sitter); both extensions co-exist in a `.m` buffer without conflict" + +incompatibilities: + - "Not a full M language server. Scope is exactly the m-stdlib public surface (`STD*` modules + their public labels); non-stdlib symbols are deliberately left alone." + - "Not a syntax highlighter. That's tree-sitter-m-vscode." + - "ObjectScript out of scope — M only." + +docs: + primary: README.md +--- + +# m-stdlib-vscode — Claude project context + +VS Code extension that surfaces m-stdlib's `dist/stdlib-manifest.json` +as hover docs, goto-definition, and completion inside any open `.m` +file. The manifest is the single source of truth; this extension is a +thin presentation layer over it. + +The full design rationale, manifest-discovery order, and per-feature +behaviour is in `README.md`. + +## What this is + +- A VS Code extension (`rafael5.m-stdlib-vscode`) that registers + hover, definition, and completion providers for the `m` language. +- A manifest reader: at activation it walks a documented discovery + order to resolve `dist/stdlib-manifest.json`, then watches the + resolved file for changes and rebuilds its in-memory index on save. +- A snippet pack (`snippets/m.json`) for the canonical m-stdlib + idioms (the same set documented in m-stdlib's how-to guides). +- A bundled manifest snapshot (`assets/stdlib-manifest.json`) so the + extension works on a stock install with no other repo on disk; + refreshed at release time by copying m-stdlib's `dist/stdlib-manifest.json`. + +## What this is NOT + +- A full M (MUMPS) language server. Non-`STD*` symbols are out of scope. +- A syntax highlighter. The TextMate / tree-sitter grammar that + tokenises `.m` files lives in **tree-sitter-m-vscode**; the two + extensions are deliberately independent so a user can install either + one without the other. +- A linter. M linting belongs to `m-cli`. +- A test runner. M testing belongs to `m-cli` + `m-test-engine`. +- A regenerator of m-stdlib's manifest. The manifest is produced by + `make manifest` in `m-stdlib`; this extension only reads it. + +## Setup + +```bash +npm ci # install vscode + tsc + @types/node +``` + +Node ≥ 18 is required for the TypeScript build. No M toolchain +required — the manifest is read as JSON and the extension never +shells out to YottaDB / IRIS. + +## Test + +```bash +npm test # tsc --noEmit + node --test on tests/*.test.ts +``` + +The `npm test` script runs `tsc -p ./ --noEmit` (full type-check of +`src/` and `tests/`) followed by `node --test --experimental-strip-types tests/*.test.ts` +(unit tests for the manifest resolver, token recogniser, and snippet +schema). No VS Code instance is launched; the tests exercise the pure +TypeScript modules in isolation. + +## Build / generate + +```bash +npm run compile # tsc -p ./ → out/extension.js +npm run watch # tsc -watch (development host loop) +``` + +The `out/` directory is the VS Code runtime entry point (`main` in +`package.json`). It is gitignored — published `.vsix` packages +include it via `npm run vscode:prepublish`. + +The `dist/` directory in this repo is separate from `out/` and is +**not** a TypeScript build output. `dist/extension-info.json` and +`dist/repo.meta.json` are hand-authored Phase-0 contract payloads +that describe the extension to the org-level AI-discoverability +catalog. When `package.json` changes a value referenced by +`dist/extension-info.json` (version, publisher, engine pin, settings +schema), update the dist file in the same commit — the +`check-manifest` gate only verifies the dist file exists, not that it +agrees with `package.json`, so drift is on the author. + +## Verify + +The `verification_commands` declared in `dist/repo.meta.json`: + +```bash +make check-manifest # dist/repo.meta.json valid + exposes.* paths exist +``` + +Cross-repo guardrail: + +```bash +make check-docs-prose # docs/ holds only prose (this repo has no docs/ at all) +``` + +## Guardrails + +- **Do not hand-edit `dist/extension-info.json` to disagree with + `package.json`.** The dist file is hand-authored, but it's a mirror + of `package.json` (version, publisher, engine pin, settings keys, + marketplace id). When `package.json` changes a mirrored value, + update the dist file in the same commit. +- **The manifest-discovery order is contract.** Consumers + (m-stdlib's docs, m-cli's `--manifest` flag, the bundled-manifest + fallback story) reference the four-step order: setting → + `$M_CLI_MANIFEST` → workspace walk-up → bundled assets. Reordering + or removing a step is a breaking change requiring coordinated docs + updates in m-stdlib. +- **`m-stdlib` has architectural priority over `m-stdlib-vscode`.** + This extension is a downstream consumer of m-stdlib's manifest + schema. If a needed manifest field is missing, propose adding it + to m-stdlib first; do not synthesise it client-side. +- **Do not expand scope beyond `STD*`.** A "full M language server" + is out of scope and would conflict with tree-sitter-m-vscode and + any future m-cli LSP work. Hover / goto-def / completion stay + scoped to symbols the manifest knows about; everything else is + passed through to other providers. +- **Do not hand-edit `dist/repo.meta.json` `verified_on` to a future + date.** The org smoke test rejects manifests older than 90 days; + bump the date only when the manifest changes materially (new + exposes payload, version bump, publisher change). +- **Bundled `assets/stdlib-manifest.json` is a release-time snapshot, + not a live artefact.** Do not regenerate it from a local m-stdlib + checkout outside a release flow — that would silently desync + installed users from what their `m-stdlib.manifestPath` setting + resolves to. Refresh it only when bumping the extension version. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9dc9bc4 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# m-stdlib-vscode — Phase-0 AI-discoverability contract gates. +# +# The VS Code extension build itself runs through npm scripts: +# +# npm ci # install vscode + tsc + @types/node +# npm test # tsc --noEmit + node --test on tests/*.test.ts +# npm run compile # tsc -p ./ → out/extension.js +# +# This Makefile only carries the cross-repo gates so verification_commands +# in dist/repo.meta.json line up with the other org repos. + +.PHONY: manifest check-manifest check-docs-prose + +# ── Phase-0 AI-discoverability contract ─────────────────────────────── +# +# Tier-2 entry to the org catalog. See +# https://github.com/m-dev-tools/.github/blob/main/docs/AI-discoverability-plan.md +# +# `dist/extension-info.json` and `dist/repo.meta.json` are hand-authored, +# not regenerated. The source of truth for what they mirror lives in +# `package.json` (version, publisher, engine pin, settings schema, +# marketplace id). When `package.json` changes a mirrored value, update +# the dist file in the same commit (this is captured in AGENTS.md +# § Guardrails). +# +# `make manifest` is therefore a pointer no-op — it exists so +# verification_commands in dist/repo.meta.json line up with other org +# repos that DO have a generator. + +manifest: + @echo "m-stdlib-vscode: dist/extension-info.json is hand-authored alongside package.json." + @echo " see AGENTS.md § Build / generate for the rebuild-when-it-changes guardrail." + +check-manifest: + python3 tools/check-manifest.py + +# Guardrail: docs/ holds only human-readable prose. Same target name +# as the tier-1 repos so cross-repo muscle memory works. +check-docs-prose: + @if [ ! -d docs ]; then echo "check-docs-prose: no docs/ directory ✓"; exit 0; fi; \ + violations=$$(find docs -type f \ + ! -name '*.md' ! -name '*.markdown' \ + ! -name '*.png' ! -name '*.jpg' ! -name '*.jpeg' \ + ! -name '*.gif' ! -name '*.svg' ! -name '*.webp' \ + ! -name '.gitkeep'); \ + if [ -n "$$violations" ]; then \ + echo "ERROR: non-prose files under docs/ — move to a top-level domain dir:" >&2; \ + echo "$$violations" >&2; \ + exit 1; \ + fi; \ + echo "check-docs-prose: docs/ is prose-only ✓" diff --git a/dist/extension-info.json b/dist/extension-info.json new file mode 100644 index 0000000..e4b30d9 --- /dev/null +++ b/dist/extension-info.json @@ -0,0 +1,44 @@ +{ + "engines": { + "vscode": "^1.85.0" + }, + "license": "MIT", + "manifest_discovery_order": [ + "m-stdlib.manifestPath setting", + "$M_CLI_MANIFEST env var", + "workspace folder walk-up", + "bundled assets/stdlib-manifest.json" + ], + "marketplace_id": "rafael5.m-stdlib-vscode", + "name": "m-stdlib-vscode", + "publisher": "rafael5", + "schema_version": "1", + "settings": [ + { + "default": "", + "description": "Path to dist/stdlib-manifest.json. Empty = auto-discover: workspace folder walk-up, then $M_CLI_MANIFEST, then the manifest bundled with the extension. Set this to your m-stdlib checkout's dist/stdlib-manifest.json for live tracking.", + "key": "m-stdlib.manifestPath", + "type": "string" + }, + { + "default": true, + "description": "Show hover docs on `^STD*` and `label^STDxxx` symbols.", + "key": "m-stdlib.enableHover", + "type": "boolean" + }, + { + "default": true, + "description": "Enable Cmd/Ctrl-click goto-definition for `^STD*` symbols.", + "key": "m-stdlib.enableDefinition", + "type": "boolean" + }, + { + "default": true, + "description": "Suggest module + label names as completions in `.m` files.", + "key": "m-stdlib.enableCompletion", + "type": "boolean" + } + ], + "snippets_path": "snippets/", + "version": "0.2.0" +} diff --git a/dist/repo.meta.json b/dist/repo.meta.json new file mode 100644 index 0000000..59e24e0 --- /dev/null +++ b/dist/repo.meta.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/m-dev-tools/.github/main/profile/repo.meta.schema.json", + "id": "tool:m-stdlib-vscode", + "repo": "https://github.com/m-dev-tools/m-stdlib-vscode", + "role": "VS Code extension — manifest-driven hover / goto-def / completion for STD* symbols in .m files", + "language": ["typescript"], + "license": "MIT", + "agent_instructions": "AGENTS.md", + "verified_on": "2026-05-10", + "exposes": { + "extension_info": "dist/extension-info.json", + "package_json": "package.json" + }, + "consumes": ["tool:m-stdlib"], + "verification_commands": ["make check-manifest"], + "status": "active" +} diff --git a/tools/check-manifest.py b/tools/check-manifest.py new file mode 100755 index 0000000..f01d59a --- /dev/null +++ b/tools/check-manifest.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Phase-0 contract gate for dist/repo.meta.json. + +Validates that: + 1. dist/repo.meta.json parses as JSON. + 2. Required fields from the org-level repo.meta.schema.json contract + are present. + 3. Each path under `exposes.*` resolves on disk. + 4. (Best-effort) full schema validation if jsonschema is available + and the canonical schema URL is reachable. + +Exits 0 on success; non-zero with structured stderr on failure. + +Engine-free, no Node, no Python deps beyond the standard library +unless jsonschema happens to be installed. +""" + +from __future__ import annotations + +import json +import sys +import urllib.request +from pathlib import Path + +MANIFEST = Path("dist/repo.meta.json") + +REQUIRED_FIELDS = ( + "id", + "repo", + "role", + "language", + "license", + "agent_instructions", + "verified_on", + "exposes", + "verification_commands", +) + + +def main() -> int: + if not MANIFEST.exists(): + print(f"ERROR: {MANIFEST} not found", file=sys.stderr) + return 1 + + try: + data = json.loads(MANIFEST.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + print(f"ERROR: {MANIFEST} is invalid JSON: {exc}", file=sys.stderr) + return 1 + + missing = [f for f in REQUIRED_FIELDS if f not in data] + if missing: + print(f"ERROR: missing required fields: {missing}", file=sys.stderr) + return 1 + + fail = False + for key, rel_path in data["exposes"].items(): + if rel_path.startswith(("http://", "https://")): + continue + if not Path(rel_path).exists(): + print( + f"ERROR: exposes.{key} payload missing on disk: {rel_path}", + file=sys.stderr, + ) + fail = True + if fail: + return 1 + + # Best-effort full schema validation. Skipped silently if jsonschema + # isn't available (the canonical Track-A validator runs in the org + # smoke test against the same manifest). + try: + from jsonschema import Draft202012Validator # type: ignore + except ImportError: + print( + "check-manifest: dist/repo.meta.json valid; " + "all exposes.* present ✓ (jsonschema not installed — " + "skipping full schema validation)" + ) + return 0 + + schema_uri = data.get("$schema", "") + try: + with urllib.request.urlopen(schema_uri, timeout=5) as resp: + schema = json.load(resp) + except Exception as exc: # noqa: BLE001 + print( + f"check-manifest: dist/repo.meta.json valid; all exposes.* " + f"present ✓ (skipped live schema fetch: {exc})" + ) + return 0 + + errors = list(Draft202012Validator(schema).iter_errors(data)) + if errors: + for err in errors: + path = "/".join(str(p) for p in err.absolute_path) or "" + print(f"SCHEMA ERROR at {path}: {err.message}", file=sys.stderr) + return 1 + + print( + "check-manifest: dist/repo.meta.json valid against org schema; " + "all exposes.* present ✓" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 2135a7de2bc5cc157452f5fc8ff37a118ed0bcee Mon Sep 17 00:00:00 2001 From: Rafael Richards Date: Sun, 10 May 2026 23:01:31 -0400 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20bump=20node=20to=2022=20=E2=80=94=20n?= =?UTF-8?q?pm=20test=20uses=20--experimental-strip-types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm test` is `tsc --noEmit && node --test --experimental-strip-types tests/*.test.ts`. Node 20 rejects --experimental-strip-types; Node 22 ships it as a recognised flag (stable since 22.6). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e806e8c..e892b87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,9 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' + # Node 22 — required for `node --test --experimental-strip-types` + # which is the test runner npm test invokes against tests/*.test.ts. + node-version: '22' - name: Install dependencies run: npm ci