diff --git a/.agents/skills/README.md b/.agents/skills/README.md index 3e8226f..6a821b8 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -1,18 +1,15 @@ -# Repo-local agent skills +# Local agent skills -This repo consumes The Interdependency organization skill library. +This directory contains repo-local copies of canonical skills from +`The-Interdependency/skill-lib`. -Canonical source: -- Preferred: `The-Interdependency/skill-lib` -- Temporary source: `The-Interdependency/a0/skill-lib` +Source commit: `c14ee9d500579a4b5d6821f62c9d82ca96e73608` -Source commit: `The-Interdependency/skill-lib` @ `a0cb6285e37734609b4b487ae4a2e44c6108d2b8` (verbatim sync). +Repo-local copies are not the source of truth. Edit `skill-lib` first, +then propagate from the canonical source. Installed skills: -- `msdmd/` — Module Self-Declared Metadata Markdown -- `test-build/` — test contract metadata blocks -- `meta-module-build/` — metadata-first module scaffolding -Agents working in this repo should read `meta-module-build/SKILL.md` before -creating new modules, routes, services, schemas, adapters, workers, engines, -UI panels, migrations, or experiments. \ No newline at end of file +- `meta-module-build/` +- `msdmd/` +- `test-build/` diff --git a/.agents/skills/msdmd/SKILL.md b/.agents/skills/msdmd/SKILL.md index 15f49d6..ffa10c4 100644 --- a/.agents/skills/msdmd/SKILL.md +++ b/.agents/skills/msdmd/SKILL.md @@ -48,10 +48,16 @@ claims to prove those obligations. See - **Fence**: `=== ===` opens, `=== END ===` closes. Block name is uppercase snake_case (e.g. `CONTRACTS`, `CHECKS`, `DOCS`, `CAPABILITIES`, `OWNERS`). -- **Comment marker**: whatever is idiomatic for the file's language. - `#` for Python / Ruby / Elixir / shell. `//` for TS / JS / Rust / Go / - Java / C / C++ / Swift. `--` for SQL / Lua / Haskell. The marker - appears at the start of every line inside the block. +- **Comment marker**: whatever is idiomatic for the file's language. The + reference parsers auto-detect these line-comment families by extension: + `#` for Python, Ruby, Elixir, shell, Perl, R, Julia, PowerShell, Tcl, and + Raku; `//` for TypeScript/JavaScript, Rust, Go, Java, C, C++ (including + `.c+`, `.c++`, `.cxx`, and header variants), Swift, Kotlin, C#, + Objective-C++, Scala, Dart, Zig, Groovy, and PHP; `--` for SQL, Lua, + Haskell, Ada, VHDL, and Lean; `%` for Erlang and Prolog; `;` for + Clojure/Lisp/Scheme/Racket; `!` for Fortran; `'` for Visual Basic; and + `*>` for COBOL. The marker appears at the start of every line in the block. + `COMMENT_MARKERS` in both universal parsers is the exact extension registry. - **Entry boundary**: every entry begins with `id:`. The id must be unique within its block and stable across refactors (so it can be referenced from external tooling). @@ -138,6 +144,17 @@ A reference implementation in pure stdlib Python lives at Both commit to zero non-stdlib dependencies so you can copy them into any project. +Extension detection refuses ambiguous suffixes rather than sniffing content. +For example, `.m` can mean Objective-C or MATLAB/Octave and therefore has no +automatic marker. A caller that already knows the language may still call +`parse_text` / `parseText` with an explicit marker. Languages that cannot carry +the msdmd shape as repeated line comments need a future versioned syntax +extension; they are not approximated with an invalid fence. + +The paired RATIOS helper preserves the interpreter boundary: a non-empty +line-1 shebang may precede opening RATIOS, which must then occupy line 2 with no +gap. See [`ratios/SKILL.md`](../ratios/SKILL.md) for the complete seal contract. + ## Repo collection point and visualizer Every consuming repo SHOULD maintain one repo-level collection point named @@ -219,9 +236,9 @@ Implementation rules every runner MUST follow: 1. **Walk the source tree** under a configurable root, skipping conventional non-source paths (`__pycache__`, `node_modules`, `.git`, build outputs, the runner's own test directory). -2. **Detect comment marker by extension**, not by content sniffing. - `.py / .rb / .ex / .sh → #`. `.ts / .js / .tsx / .jsx / .rs / .go / - .java / .c / .cpp / .swift → //`. `.sql / .lua / .hs → --`. +2. **Detect comment marker by extension**, not by content sniffing. Consume the + parser's `COMMENT_MARKERS` registry rather than maintaining a runner-local + language list. Python and TypeScript registries must remain identical. 3. **Parse all matching blocks** in each file. Multiple blocks of the same type concatenate; entries from different blocks are distinguishable only by id, not by source block. diff --git a/.agents/skills/msdmd/collection.ts b/.agents/skills/msdmd/collection.ts index 697dca2..00195df 100644 --- a/.agents/skills/msdmd/collection.ts +++ b/.agents/skills/msdmd/collection.ts @@ -1,4 +1,4 @@ -// ratios: loc_comments=67:0 imports_exports=0:0 calls_definitions=1:0 +// ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm /** * Shared TypeScript shapes for repo-level msdmd collection points. * @@ -72,4 +72,4 @@ export interface MsdmdCollection { export function defineMsdmdCollection(collection: MsdmdCollection): MsdmdCollection { return collection; } -// ratios: loc_comments=67:0 imports_exports=0:0 calls_definitions=1:0 +// ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm diff --git a/.agents/skills/msdmd/parsers/universal.py b/.agents/skills/msdmd/parsers/universal.py index 7bc86bf..204f533 100644 --- a/.agents/skills/msdmd/parsers/universal.py +++ b/.agents/skills/msdmd/parsers/universal.py @@ -1,4 +1,4 @@ -# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 +# ratios: loc_comments=161:57 imports_exports=4:7 calls_definitions=55:10 """Universal msdmd parser — pure stdlib. Implements the parser contract from ``msdmd/SKILL.md``: extracts every @@ -7,6 +7,8 @@ Comment marker is auto-detected by file extension. The block syntax itself is identical across languages; only the per-line marker changes. +``COMMENT_MARKERS`` is public so runners can distinguish parser support +from their narrower language-specific execution or metric coverage. Public API: @@ -15,12 +17,13 @@ walk_tree(root, block_name, *, skip=None, extensions=None) -> tuple[annotated, untested] RATIOS is the one msdmd declaration that is *not* a fenced block — it is a -single comment line carried on a file's first and last non-blank lines. The -reader for it lives here too, as a sanctioned extension rather than a fork: +single comment line carried on a file's opening and closing source boundaries. +A valid interpreter shebang owns literal line 1, so the opening RATIOS line is +literal line 2 in that case. The reader lives here as a sanctioned extension: parse_ratios(text, marker="#") -> list[dict] parse_ratios_file(path) -> list[dict] - ratios_placement(text, marker="#") -> tuple[first_ok, last_ok] + ratios_placement(text, marker="#") -> tuple[opening_ok, closing_ok] This module has zero non-stdlib dependencies and is safe to copy verbatim into any project that wants msdmd support. @@ -30,13 +33,39 @@ from pathlib import Path from typing import Iterable -# extension → comment marker -_MARKERS: dict[str, str] = { - ".py": "#", ".rb": "#", ".ex": "#", ".exs": "#", ".sh": "#", - ".ts": "//", ".tsx": "//", ".js": "//", ".jsx": "//", ".mjs": "//", - ".rs": "//", ".go": "//", ".java": "//", ".c": "//", ".cpp": "//", - ".cc": "//", ".h": "//", ".hpp": "//", ".swift": "//", ".kt": "//", +# extension → line-comment marker. Keep this registry entry-for-entry equivalent +# to universal.ts; tests fail if either parser gains or loses an extension alone. +COMMENT_MARKERS: dict[str, str] = { + ".py": "#", ".pyw": "#", ".pyi": "#", + ".rb": "#", ".rake": "#", ".gemspec": "#", + ".ex": "#", ".exs": "#", + ".sh": "#", ".bash": "#", ".zsh": "#", ".fish": "#", + ".pl": "#", ".pm": "#", ".t": "#", + ".r": "#", ".jl": "#", + ".ps1": "#", ".psm1": "#", ".tcl": "#", + ".raku": "#", ".rakumod": "#", + ".ts": "//", ".tsx": "//", ".mts": "//", ".cts": "//", + ".js": "//", ".jsx": "//", ".mjs": "//", ".cjs": "//", + ".rs": "//", ".go": "//", ".java": "//", + ".c": "//", ".cc": "//", ".cp": "//", ".cpp": "//", + ".cxx": "//", ".c+": "//", ".c++": "//", + ".h": "//", ".hh": "//", ".hp": "//", ".hpp": "//", + ".hxx": "//", ".h+": "//", ".h++": "//", + ".tcc": "//", ".ipp": "//", ".inl": "//", + ".swift": "//", ".kt": "//", ".kts": "//", ".cs": "//", + ".mm": "//", ".scala": "//", ".dart": "//", ".zig": "//", + ".groovy": "//", ".gradle": "//", ".php": "//", ".sql": "--", ".lua": "--", ".hs": "--", + ".adb": "--", ".ads": "--", ".vhd": "--", ".vhdl": "--", + ".lean": "--", + ".erl": "%", ".hrl": "%", ".prolog": "%", + ".clj": ";", ".cljs": ";", ".cljc": ";", ".bb": ";", + ".lisp": ";", ".lsp": ";", ".cl": ";", + ".scm": ";", ".ss": ";", ".rkt": ";", + ".f": "!", ".for": "!", ".f90": "!", ".f95": "!", + ".f03": "!", ".f08": "!", + ".vb": "'", ".vbs": "'", + ".cob": "*>", ".cbl": "*>", } _DEFAULT_SKIP = ( @@ -48,7 +77,7 @@ def marker_for(path: Path) -> str | None: """Return the comment marker for a file path, or None if unsupported.""" - return _MARKERS.get(path.suffix.lower()) + return COMMENT_MARKERS.get(path.suffix.lower()) def _block_regex(block_name: str, marker: str) -> re.Pattern[str]: @@ -124,7 +153,7 @@ def walk_tree( ext_set = ( set(e.lower() if e.startswith(".") else "." + e.lower() for e in extensions) if extensions is not None - else set(_MARKERS.keys()) + else set(COMMENT_MARKERS.keys()) ) def iter_source_files(path: Path) -> Iterable[Path]: @@ -155,8 +184,9 @@ def iter_source_files(path: Path) -> Iterable[Path]: # --- RATIOS single-line declaration (msdmd extension) -------------------- # Unlike every other declaration, RATIOS is not fenced. It is a single -# comment line carrying the three canonical ratios, placed on the file's -# first and last non-blank lines: +# comment line carrying the three canonical ratios at the opening source +# boundary and last non-blank line. A valid line-1 shebang moves the opening +# boundary to literal line 2: # ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M RATIO_IDS = ("loc_comments", "imports_exports", "calls_definitions") _RATIOS_TOKEN_RE = re.compile(r"(?P[a-z_]+)=(?P\S+)") @@ -171,9 +201,9 @@ def parse_ratios(text: str, marker: str = "#") -> list[dict]: RATIOS is not a fenced block: it is one comment line of the form `` ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M`` - placed on the file's first and last non-blank lines. Returns one flat - ``{"id", "value"}`` dict per (declaration line x ratio token) so a drift - gate can verify every occurrence. + placed at the file's opening and closing source boundaries. Returns one + flat ``{"id", "value"}`` dict per (declaration line x ratio token) so a + drift gate can verify every occurrence. """ line_re = _ratios_line_re(marker) out: list[dict] = [] @@ -198,17 +228,29 @@ def parse_ratios_file(path: Path) -> list[dict]: def ratios_placement(text: str, marker: str = "#") -> tuple[bool, bool]: - """Return ``(first_line_has_ratios, last_non_blank_line_has_ratios)``.""" + """Return ``(opening_ratios_ok, closing_ratios_ok)``. + + A non-empty ``#!`` interpreter directive may occupy literal line 1. It is + the only accepted preamble and RATIOS must immediately follow it. + """ line_re = _ratios_line_re(marker) lines = text.splitlines() if not lines: return (False, False) - first_ok = bool(line_re.match(lines[0].rstrip())) + has_shebang = lines[0].startswith("#!") and bool(lines[0][2:].strip()) + opening_index = 1 if has_shebang else 0 + opening_ok = ( + len(lines) > opening_index + and bool(line_re.match(lines[opening_index].rstrip())) + ) + if opening_index == 0 and len(lines) > 1: + displaced = lines[1].startswith("#!") and bool(lines[1][2:].strip()) + opening_ok = opening_ok and not displaced last_ok = False for raw in reversed(lines): if raw.strip() == "": continue last_ok = bool(line_re.match(raw.rstrip())) break - return (first_ok, last_ok) -# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 + return (opening_ok, last_ok) +# ratios: loc_comments=161:57 imports_exports=4:7 calls_definitions=55:10 diff --git a/.agents/skills/msdmd/parsers/universal.ts b/.agents/skills/msdmd/parsers/universal.ts index 2db076f..141f46c 100644 --- a/.agents/skills/msdmd/parsers/universal.ts +++ b/.agents/skills/msdmd/parsers/universal.ts @@ -1,4 +1,4 @@ -// ratios: loc_comments=176:0 imports_exports=2:0 calls_definitions=53:0 +// ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm /** * Universal msdmd parser — pure Node stdlib (fs, path). * @@ -9,12 +9,13 @@ * * Comment marker auto-detected by file extension. The block syntax * itself is identical across languages; only the per-line marker - * changes. + * changes. COMMENT_MARKERS is public so runners can keep their narrower + * language-specific execution or metric coverage separate. * * RATIOS is the one msdmd declaration that is not a fenced block — it is a - * single comment line on a file's first and last non-blank lines. Its reader - * (parseRatios / parseRatiosFile / ratiosPlacement) lives here too, as a - * sanctioned extension rather than a fork. + * single comment line on a file's opening and closing source boundaries. A + * valid interpreter shebang owns literal line 1, moving opening RATIOS to + * literal line 2. Its reader lives here as a sanctioned extension. * * Zero non-stdlib dependencies. Safe to copy verbatim into any * Node/Deno/Bun project that wants msdmd support. @@ -24,12 +25,39 @@ import { join, extname } from "node:path"; export type Entry = Record; -const MARKERS: Record = { - ".py": "#", ".rb": "#", ".ex": "#", ".exs": "#", ".sh": "#", - ".ts": "//", ".tsx": "//", ".js": "//", ".jsx": "//", ".mjs": "//", - ".rs": "//", ".go": "//", ".java": "//", ".c": "//", ".cpp": "//", - ".cc": "//", ".h": "//", ".hpp": "//", ".swift": "//", ".kt": "//", +// Keep this registry entry-for-entry equivalent to universal.py; tests fail if +// either parser gains or loses an extension alone. +export const COMMENT_MARKERS: Record = { + ".py": "#", ".pyw": "#", ".pyi": "#", + ".rb": "#", ".rake": "#", ".gemspec": "#", + ".ex": "#", ".exs": "#", + ".sh": "#", ".bash": "#", ".zsh": "#", ".fish": "#", + ".pl": "#", ".pm": "#", ".t": "#", + ".r": "#", ".jl": "#", + ".ps1": "#", ".psm1": "#", ".tcl": "#", + ".raku": "#", ".rakumod": "#", + ".ts": "//", ".tsx": "//", ".mts": "//", ".cts": "//", + ".js": "//", ".jsx": "//", ".mjs": "//", ".cjs": "//", + ".rs": "//", ".go": "//", ".java": "//", + ".c": "//", ".cc": "//", ".cp": "//", ".cpp": "//", + ".cxx": "//", ".c+": "//", ".c++": "//", + ".h": "//", ".hh": "//", ".hp": "//", ".hpp": "//", + ".hxx": "//", ".h+": "//", ".h++": "//", + ".tcc": "//", ".ipp": "//", ".inl": "//", + ".swift": "//", ".kt": "//", ".kts": "//", ".cs": "//", + ".mm": "//", ".scala": "//", ".dart": "//", ".zig": "//", + ".groovy": "//", ".gradle": "//", ".php": "//", ".sql": "--", ".lua": "--", ".hs": "--", + ".adb": "--", ".ads": "--", ".vhd": "--", ".vhdl": "--", + ".lean": "--", + ".erl": "%", ".hrl": "%", ".prolog": "%", + ".clj": ";", ".cljs": ";", ".cljc": ";", ".bb": ";", + ".lisp": ";", ".lsp": ";", ".cl": ";", + ".scm": ";", ".ss": ";", ".rkt": ";", + ".f": "!", ".for": "!", ".f90": "!", ".f95": "!", + ".f03": "!", ".f08": "!", + ".vb": "'", ".vbs": "'", + ".cob": "*>", ".cbl": "*>", }; const DEFAULT_SKIP = new Set([ @@ -43,7 +71,7 @@ function escapeRegex(s: string): string { } export function markerFor(path: string): string | null { - return MARKERS[extname(path).toLowerCase()] ?? null; + return COMMENT_MARKERS[extname(path).toLowerCase()] ?? null; } export function parseText( @@ -104,7 +132,7 @@ export function walkTree( ): { annotated: Array<[string, Entry[]]>; untested: string[] } { const skip = opts.skip ?? DEFAULT_SKIP; const extensions = - opts.extensions ?? new Set(Object.keys(MARKERS)); + opts.extensions ?? new Set(Object.keys(COMMENT_MARKERS)); const annotated: Array<[string, Entry[]]> = []; const untested: string[] = []; @@ -145,8 +173,9 @@ export function walkTree( // --- RATIOS single-line declaration (msdmd extension) -------------------- // Unlike every other declaration, RATIOS is not fenced. It is a single -// comment line carrying the three canonical ratios, placed on the file's -// first and last non-blank lines: +// comment line carrying the three canonical ratios at the opening source +// boundary and last non-blank line. A valid line-1 shebang moves the opening +// boundary to literal line 2: // ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M export const RATIO_IDS = ["loc_comments", "imports_exports", "calls_definitions"] as const; @@ -184,13 +213,22 @@ export function ratiosPlacement(text: string, marker: string = "#"): [boolean, b const lineRe = ratiosLineRe(marker); const lines = text.split("\n"); if (lines.length === 0) return [false, false]; - const firstOk = lineRe.test(lines[0].replace(/\s+$/, "")); + const hasShebang = lines[0].startsWith("#!") && lines[0].slice(2).trim().length > 0; + const openingIndex = hasShebang ? 1 : 0; + const openingOk = + lines.length > openingIndex && + lineRe.test(lines[openingIndex].replace(/\s+$/, "")); + const displacedShebang = + openingIndex === 0 && + lines.length > 1 && + lines[1].startsWith("#!") && + lines[1].slice(2).trim().length > 0; let lastOk = false; for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].trim() === "") continue; lastOk = lineRe.test(lines[i].replace(/\s+$/, "")); break; } - return [firstOk, lastOk]; + return [openingOk && !displacedShebang, lastOk]; } -// ratios: loc_comments=176:0 imports_exports=2:0 calls_definitions=53:0 +// ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm diff --git a/eml_ucns_msdmd.ts b/eml_ucns_msdmd.ts index 756fa81..7b4d055 100644 --- a/eml_ucns_msdmd.ts +++ b/eml_ucns_msdmd.ts @@ -44,6 +44,5 @@ export default defineMsdmdCollection({ } ], "gaps": [], - "repo": "eml_ucns", - "source_commit": "57ba9cc" + "repo": "eml_ucns" });