From 4fc1d62ce265c44809637d28335ac9e1b51913b5 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Sun, 19 Jul 2026 18:40:04 +1000 Subject: [PATCH] agent skills: tcl-nslist namespace discovery; punk::ns 0.9.1 packagetails fix New scriptlib/developer/nslist.tcl backing the new cross-framework tcl-nslist agent skill (.agents/skills/ canonical, .claude/skills/ synced copy): the discovery counterpart of whatis.tcl - lists what a namespace/package actually exposes at runtime via punk::ns::get_ns_dicts (the n// machinery): child namespaces with loadable-package markers, commands classified by kind, punk::args-documented commands, -synopsis whole-namespace usage lines, -pathcommands namespace-path resolution. Auto-package-requires unloaded packages and follows ensemble commands to their implementation namespace. The tcl-whatis skill/script cross-reference it (whatis redirects with a hint when the queried name is a namespace, not a command). punk::ns 0.9.1: get_ns_dicts packagetails/packageprefixes initialization moved inside the per-namespace loop - with multiple matched namespaces (path globs, trailing **) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in nslist/n// underline styling and in the returned data; a child with several deeper packages below it was appended to packageprefixes once per matching package (now once). Pinned in nslist.test (nslist_classification_multins_package_buckets_reset). Documentation-only PUNKARGS added for get_ns_dicts (returned dict keys documented) and nslist_dict; parsing stays manual. Root AGENTS.md: introspection note + scriptlib/developer inventory updated. Project version 0.15.1 + CHANGELOG entry. punk::ns suite 102/102 pass. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- .agents/skills/tcl-nslist/SKILL.md | 85 ++++++ .agents/skills/tcl-whatis/SKILL.md | 9 +- .claude/skills/tcl-nslist/SKILL.md | 85 ++++++ .claude/skills/tcl-whatis/SKILL.md | 9 +- AGENTS.md | 4 +- CHANGELOG.md | 5 + punkproject.toml | 2 +- scriptlib/developer/nslist.tcl | 275 ++++++++++++++++++ scriptlib/developer/whatis.tcl | 14 + src/modules/punk/ns-999999.0a1.0.tm | 95 +++++- src/modules/punk/ns-buildversion.txt | 3 +- .../modules/punk/ns/testsuites/ns/nslist.test | 22 ++ 12 files changed, 598 insertions(+), 10 deletions(-) create mode 100644 .agents/skills/tcl-nslist/SKILL.md create mode 100644 .claude/skills/tcl-nslist/SKILL.md create mode 100644 scriptlib/developer/nslist.tcl diff --git a/.agents/skills/tcl-nslist/SKILL.md b/.agents/skills/tcl-nslist/SKILL.md new file mode 100644 index 00000000..05b20db9 --- /dev/null +++ b/.agents/skills/tcl-nslist/SKILL.md @@ -0,0 +1,85 @@ +--- +name: tcl-nslist +description: Use when you need to discover what exists in a Tcl namespace or package in this repo - what commands/procs a namespace exposes, whether a helper already exists before writing one, or what an ensemble's implementation namespace contains. No specific command name needed (that's tcl-whatis). Runtime listing via punkshell using punk::ns::get_ns_dicts (the machinery behind the interactive n// browser): child namespaces with loadable-package markers, commands classified by kind (proc/alias/ensemble/native/oo/imported), which commands have punk::args docs, optional whole-namespace usage synopses. Auto-loads the package; sees runtime-generated commands grep cannot. +--- + +# tcl-nslist + +Namespace/package discovery for the punkshell repo - the "what is available?" +counterpart of tcl-whatis ("how do I call X?"). Asks the live interpreter via +punk::ns::get_ns_dicts - the same machinery behind the interactive n// / n/// +namespace browsers - so it reports what is actually loaded, including +runtime-generated commands, aliases and imports that grep cannot see, and +auto-loads the providing package when the namespace is not yet populated. + +## Command + +Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works): + + punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? ... + +- `` - namespace name or glob, qualified or not: `punk::ns` is listed + as `::punk::ns::*`. If the namespace does not exist, `package require` of + the name (then of successively shorter :: prefixes) is attempted first. + Last-segment glob chars filter members (`punk::ns::ns*`); glob chars in + earlier segments match multiple namespaces (`::tcl::*::d*`); a trailing + `::**` segment recurses through all descendant namespaces + (`punk::nav::**`). Quote glob arguments in PowerShell. + An ensemble command name (e.g `string`) is followed to its implementation + namespace, with a note. +- `-synopsis` - append punk::args usage lines for every documented command in + the namespace (a one-call API cheat-sheet; large for big namespaces). +- `-pathcommands` - also list the commands resolvable via each `namespace + path` entry. + +Exit codes: 0 every pattern listed something, 1 some pattern matched nothing, +2 usage error. + +## Output fields + +One block per matched namespace (blank-line separated); empty categories are +omitted: + + namespace: ::punk::ns fully-qualified namespace + children: argdoc lib ... child namespace tails + packagetails: lib ... children that are also loadable package names + packageprefixes: ... children with deeper loadable packages below them + commands: 147 total member commands (count) + procs: ... script procs + aliases: ... interp aliases (may lack a same-named command) + ensembles: ... + native: ... C-implemented commands + imported: ... imported from another namespace + other: ... kinds not otherwise classified + exported: ... commands matching the namespace export patterns + documented: ... have punk::args docs - tcl-whatis gives full detail + exportpatterns: {[a-z]*} raw namespace export patterns + namespacepath: ... extra namespaces this ns resolves commands from + note: ... autoload / ensemble-follow / empty-namespace notes + synopsis: with -synopsis: usage line(s) per documented command + +ooclasses/ooobjects/ooprivateclasses/ooprivateobjects/coroutines/interps/ +zlibstreams lines appear when such commands exist. `packagetails` is the +discovery lever: a child listed there can be `package require`d and listed +itself. + +## Caveats + +- Keep the `src` launch argument: it loads and queries the working-tree + modules under `src/modules`; without it the kit-stamped copies inside the + executable answer instead. +- Load noise ("src mode: registered ...") goes to stderr; read stdout only + (append `2>/dev/null` in bash, `2>$null` in PowerShell). +- Only what is loaded in that session is reported. The script auto-requires + the queried package, but a namespace can be populated further by other + packages (e.g plugins) - an unexpectedly sparse listing may mean a related + package is not loaded, not that the commands do not exist. +- `documented:` lists commands with registered punk::args definitions; + undocumented commands are still real - tcl-whatis reports their proc + signature and `-body` even without an argdoc. +- From Tcl code, the same data is available structurally: + `punk::ns::get_ns_dicts ::the::ns::* ?-allbelow ? ?-nspathcommands ?` + returns a list of dicts (keys documented - see + `punk91 src script scriptlib/developer/whatis.tcl -doc punk::ns::get_ns_dicts`), + and `punk::ns::nslist_dict` accepts caller-relative globs. In an interactive + punkshell the `n//` / `n///` aliases render the same information. diff --git a/.agents/skills/tcl-whatis/SKILL.md b/.agents/skills/tcl-whatis/SKILL.md index 2d534f0d..f1abaa67 100644 --- a/.agents/skills/tcl-whatis/SKILL.md +++ b/.agents/skills/tcl-whatis/SKILL.md @@ -1,6 +1,6 @@ --- name: tcl-whatis -description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs. +description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs. For namespace/package-level discovery (listing what a namespace exposes, no command name needed) use the tcl-nslist skill. --- # tcl-whatis @@ -60,6 +60,13 @@ Ensembles report their namespace and subcommand map instead of `def:`. - Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per form under a single common header (description + all-forms synopsis). +## Namespace discovery (sibling skill) + +whatis needs a command name. For the discovery question - what commands and +child namespaces a namespace or package exposes at all - use the tcl-nslist +skill (scriptlib/developer/nslist.tcl). whatis prints a redirect hint when the +queried name turns out to be a namespace rather than a command. + ## Caveats - Keep the `src` launch argument: it loads the working-tree modules under diff --git a/.claude/skills/tcl-nslist/SKILL.md b/.claude/skills/tcl-nslist/SKILL.md new file mode 100644 index 00000000..05b20db9 --- /dev/null +++ b/.claude/skills/tcl-nslist/SKILL.md @@ -0,0 +1,85 @@ +--- +name: tcl-nslist +description: Use when you need to discover what exists in a Tcl namespace or package in this repo - what commands/procs a namespace exposes, whether a helper already exists before writing one, or what an ensemble's implementation namespace contains. No specific command name needed (that's tcl-whatis). Runtime listing via punkshell using punk::ns::get_ns_dicts (the machinery behind the interactive n// browser): child namespaces with loadable-package markers, commands classified by kind (proc/alias/ensemble/native/oo/imported), which commands have punk::args docs, optional whole-namespace usage synopses. Auto-loads the package; sees runtime-generated commands grep cannot. +--- + +# tcl-nslist + +Namespace/package discovery for the punkshell repo - the "what is available?" +counterpart of tcl-whatis ("how do I call X?"). Asks the live interpreter via +punk::ns::get_ns_dicts - the same machinery behind the interactive n// / n/// +namespace browsers - so it reports what is actually loaded, including +runtime-generated commands, aliases and imports that grep cannot see, and +auto-loads the providing package when the namespace is not yet populated. + +## Command + +Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works): + + punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? ... + +- `` - namespace name or glob, qualified or not: `punk::ns` is listed + as `::punk::ns::*`. If the namespace does not exist, `package require` of + the name (then of successively shorter :: prefixes) is attempted first. + Last-segment glob chars filter members (`punk::ns::ns*`); glob chars in + earlier segments match multiple namespaces (`::tcl::*::d*`); a trailing + `::**` segment recurses through all descendant namespaces + (`punk::nav::**`). Quote glob arguments in PowerShell. + An ensemble command name (e.g `string`) is followed to its implementation + namespace, with a note. +- `-synopsis` - append punk::args usage lines for every documented command in + the namespace (a one-call API cheat-sheet; large for big namespaces). +- `-pathcommands` - also list the commands resolvable via each `namespace + path` entry. + +Exit codes: 0 every pattern listed something, 1 some pattern matched nothing, +2 usage error. + +## Output fields + +One block per matched namespace (blank-line separated); empty categories are +omitted: + + namespace: ::punk::ns fully-qualified namespace + children: argdoc lib ... child namespace tails + packagetails: lib ... children that are also loadable package names + packageprefixes: ... children with deeper loadable packages below them + commands: 147 total member commands (count) + procs: ... script procs + aliases: ... interp aliases (may lack a same-named command) + ensembles: ... + native: ... C-implemented commands + imported: ... imported from another namespace + other: ... kinds not otherwise classified + exported: ... commands matching the namespace export patterns + documented: ... have punk::args docs - tcl-whatis gives full detail + exportpatterns: {[a-z]*} raw namespace export patterns + namespacepath: ... extra namespaces this ns resolves commands from + note: ... autoload / ensemble-follow / empty-namespace notes + synopsis: with -synopsis: usage line(s) per documented command + +ooclasses/ooobjects/ooprivateclasses/ooprivateobjects/coroutines/interps/ +zlibstreams lines appear when such commands exist. `packagetails` is the +discovery lever: a child listed there can be `package require`d and listed +itself. + +## Caveats + +- Keep the `src` launch argument: it loads and queries the working-tree + modules under `src/modules`; without it the kit-stamped copies inside the + executable answer instead. +- Load noise ("src mode: registered ...") goes to stderr; read stdout only + (append `2>/dev/null` in bash, `2>$null` in PowerShell). +- Only what is loaded in that session is reported. The script auto-requires + the queried package, but a namespace can be populated further by other + packages (e.g plugins) - an unexpectedly sparse listing may mean a related + package is not loaded, not that the commands do not exist. +- `documented:` lists commands with registered punk::args definitions; + undocumented commands are still real - tcl-whatis reports their proc + signature and `-body` even without an argdoc. +- From Tcl code, the same data is available structurally: + `punk::ns::get_ns_dicts ::the::ns::* ?-allbelow ? ?-nspathcommands ?` + returns a list of dicts (keys documented - see + `punk91 src script scriptlib/developer/whatis.tcl -doc punk::ns::get_ns_dicts`), + and `punk::ns::nslist_dict` accepts caller-relative globs. In an interactive + punkshell the `n//` / `n///` aliases render the same information. diff --git a/.claude/skills/tcl-whatis/SKILL.md b/.claude/skills/tcl-whatis/SKILL.md index 2d534f0d..f1abaa67 100644 --- a/.claude/skills/tcl-whatis/SKILL.md +++ b/.claude/skills/tcl-whatis/SKILL.md @@ -1,6 +1,6 @@ --- name: tcl-whatis -description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs. +description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs. For namespace/package-level discovery (listing what a namespace exposes, no command name needed) use the tcl-nslist skill. --- # tcl-whatis @@ -60,6 +60,13 @@ Ensembles report their namespace and subcommand map instead of `def:`. - Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per form under a single common header (description + all-forms synopsis). +## Namespace discovery (sibling skill) + +whatis needs a command name. For the discovery question - what commands and +child namespaces a namespace or package exposes at all - use the tcl-nslist +skill (scriptlib/developer/nslist.tcl). whatis prints a redirect hint when the +queried name turns out to be a namespace rather than a command. + ## Caveats - Keep the `src` launch argument: it loads the working-tree modules under diff --git a/AGENTS.md b/AGENTS.md index 40fc202d..3e54426a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -199,7 +199,7 @@ The project version is fully independent of module versions. A module bump (even - `TEMP_REFERENCE/` - Reference-only material for agents, not a work folder; ignored by both git and fossil, so its contents (including its AGENTS.md) are machine-local and unversioned. Holds the nested fossil checkout of the Tcl core sources at `TEMP_REFERENCE/tcl9`, which must stay on `trunk` per the fossil branch-discipline rule in User Preferences (see TEMP_REFERENCE/AGENTS.md when present) - Directories agents should not directly modify (no child DOX needed): - `callbacks/` — Experimental shellspy features, user-only - - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only. + - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only. - `bin/` — Built binaries and helpers, build output target. This includes the polyglot `.cmd` launcher/utility scripts (e.g `bin/runtime.cmd`): they are GENERATED by the punk::mix scriptwrap machinery from sources under `src/scriptapps/` — a request to "fix bin/.cmd" means editing `src/scriptapps/.*` + `_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output - `modules/` (root) — Build output target for `tclsh src/make.tcl modules` - `lib/` (root) — Build output target for `tclsh src/make.tcl libs` @@ -213,7 +213,7 @@ The project version is fully independent of module versions. A module bump (even - The project extends Tcl with ANSI terminal capabilities, functional and pattern-matching language features, and a comprehensive module ecosystem. - This root file is intentionally limited to DOX governance, global ownership boundaries, and the top-level Child DOX Index. - Source-tree build, testing, linting, and file-resolution workflow lives in `src/AGENTS.md`. -- Runtime Tcl command introspection for agents (resolved name/kind, subcommand-chain resolution e.g `string is`, providing package + loaded version, origin file, def file:line, punk::args synopsis; `-doc` = full plain-text argdoc of all forms): `punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? ?subcommand?...` - full usage and caveats in `.agents/skills/tcl-whatis/SKILL.md`. +- Runtime Tcl command introspection for agents (resolved name/kind, subcommand-chain resolution e.g `string is`, providing package + loaded version, origin file, def file:line, punk::args synopsis; `-doc` = full plain-text argdoc of all forms): `punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? ?subcommand?...` - full usage and caveats in `.agents/skills/tcl-whatis/SKILL.md`. Its discovery counterpart for namespace/package contents (child namespaces with loadable-package markers, commands classified by kind, punk::args-documented commands, `-synopsis` whole-namespace usage lines - via `punk::ns::get_ns_dicts`, the `n//` machinery): `punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? ...` - see `.agents/skills/tcl-nslist/SKILL.md`. - Tcl module authoring conventions live in `src/modules/AGENTS.md` and closer module child docs. - If AGENTS.md conflicts with CLAUDE.md, AGENTS.md wins. - Technical project goals live in root `GOALS.md` (summary index: ID, status, title, scope, detail pointer per goal) with the canonical Goal/Acceptance contract prose in `goals/G--.md` detail files - read the detail file of every goal whose scope intersects paths being edited. Goals are user-owned: agents add or edit goal contract elements (in either tier) only at the user's request, proposal-first (show the proposed wording, get explicit approval before writing - see the `GOALS.md` maintenance rules); never on their own initiative. Suggesting candidate goals when work surfaces something goal-worthy is welcome and encouraged - as a flagged proposal, not a file edit. Non-contract detail-file updates from user-directed work need only be reported in the completion summary. Agents auto-flip a goal to `achieved ` when its detail-file acceptance criterion is met as part of the DOX closeout pass - the flip includes archiving the entry to `GOALS-archive.md` and the detail file to `goals/archive/` - and must flag `proposed`→`active` transitions in their completion report for user confirmation. See `GOALS.md` for the full workflow, including how to author a new goal when asked. diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ced1f9..9b1555ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.15.1] - 2026-07-19 + +- punk::ns 0.9.1: `get_ns_dicts` per-namespace `packagetails`/`packageprefixes` fix - the two lists were initialized once before the matched-namespaces loop, so with multiple matched namespaces (path globs, trailing `**`) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in `nslist`/`n//` underline styling and in the returned data; a child with several deeper packages below it was also recorded once per matching package (now once). Single-namespace queries (the nslist.test tier A pins) were unaffected; the multi-namespace reset is now pinned in nslist.test. Documentation-only PUNKARGS added for `get_ns_dicts` (documents the returned per-namespace dict keys and `-allbelow`/`-nspathcommands`) and `nslist_dict`. +- New developer/agent namespace-discovery script `scriptlib/developer/nslist.tcl` backing the new cross-framework `tcl-nslist` agent skill (canonical `.agents/skills/`, synced copy in `.claude/skills/`): the discovery counterpart of `whatis.tcl` - lists what a namespace or package actually exposes at runtime via `punk::ns::get_ns_dicts` (child namespaces with loadable-package markers, commands classified by kind, punk::args-documented commands, `-synopsis` whole-namespace usage lines, `-pathcommands` namespace-path resolution), auto-`package require`s unloaded packages and follows ensemble commands to their implementation namespace. The `tcl-whatis` skill/script now cross-reference it (whatis redirects with a hint when the queried name is a namespace rather than a command). + ## [0.15.0] - 2026-07-18 - shellrun 0.2.0: `runx` gains `-teelog` - a live tee of the captured stdout/stderr to the shellfilter::log mechanism (shellthread worker threads; `-tag`/`-syslog host:port`/`-file`/`-raw` settings; log sources `stdout`/`stderr`) while the captured/returned data stays unchanged. Implemented as write traces on the capture variables rather than additional channel transforms: probing showed the unaudited float-placement logic can starve a tee_to_log float combined with the float-locked capture junction, and shellthread's parked-worker adoption does not re-apply settings (logs silently misrouted to a previous target) - both findings recorded for a future shellfilter/shellthread audit. Tag workers are left open for reuse across calls; a conflicting reconfiguration errors explicitly. diff --git a/punkproject.toml b/punkproject.toml index 12618870..bcc23a84 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.15.0" +version = "0.15.1" license = "BSD-2-Clause" diff --git a/scriptlib/developer/nslist.tcl b/scriptlib/developer/nslist.tcl new file mode 100644 index 00000000..15009882 --- /dev/null +++ b/scriptlib/developer/nslist.tcl @@ -0,0 +1,275 @@ +#!/usr/bin/env tclsh +# nslist.tcl - runtime namespace/command discovery report, aimed at agent/LLM use +# +# Answers "what does this namespace/package actually expose?" - the discovery +# counterpart of whatis.tcl (which answers "how do I call this command?"). +# Reports for each namespace matched by : +# - child namespaces, marking which are also loadable package names +# - commands classified by kind (procs, aliases, ensembles, native, imported, +# oo classes/objects, coroutines, interps, zlibstreams) +# - which commands have punk::args documentation ('documented:' line) - those +# give full usage detail via whatis.tcl / punk::ns::cmdhelp +# - namespace export patterns and 'namespace path' entries +# Data comes from punk::ns::get_ns_dicts - the same runtime machinery behind the +# interactive n// and n/// namespace browsers - so runtime-generated commands are +# included and only what is actually loaded is reported. +# If the target namespace does not exist, 'package require' of the name (then of +# successively shorter :: prefixes) is attempted first to populate it. +# An ensemble command name (e.g 'string') is followed to its implementation +# namespace, with a note. +# Options: +# -synopsis append punk::args usage lines for each documented command +# -pathcommands also list the commands resolvable via each 'namespace path' entry +# +# Must run inside a punkshell interpreter (needs punk::ns, punk::args): +# punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? ... +# Use the 'src' launch argument so the working-tree modules under src/modules are +# loaded and queried rather than the kit-stamped copies inside the executable. +# may be a plain namespace (punk::ns - listed as ::punk::ns::*) or a +# glob: last-segment glob chars match members, earlier-segment glob chars match +# multiple namespaces, and a trailing ::** segment recurses through all +# descendant namespaces. +# Exit 0 if every pattern produced a listing; 1 if any pattern matched nothing; +# 2 usage error. + +if {[catch {package require punk::ns} errmsg]} { + puts stderr "nslist: requires a punkshell interpreter (package require punk::ns failed: $errmsg)" + exit 1 +} +catch {package require punk::args} + +proc strip {text} { + if {[info commands ::ansistrip] ne ""} { + return [ansistrip $text] + } elseif {![catch {package require punk::ansi}]} { + return [punk::ansi::ansistrip $text] + } + return $text +} + +proc out {key val} { + puts [format "%-16s %s" $key: [strip $val]] +} + +# attempt to load a package matching $name, else successively shorter :: prefixes +proc try_autoload {name} { + set parts [lsearch -all -inline -not [split [string trimleft $name :] ::] {}] + for {set n [llength $parts]} {$n >= 1} {incr n -1} { + set prefix [join [lrange $parts 0 $n-1] ::] + if {![catch {package require $prefix}]} { + return $prefix + } + } + return {} +} + +# leading non-glob namespace of a fully-qualified (possibly glob) path e.g ::a::b* -> ::a +proc nonglob_base {fq} { + set parts [lsearch -all -inline -not [split [string trimleft $fq :] ::] {}] + set keep [list] + foreach seg $parts { + if {[regexp {[*?]} $seg]} { + break + } + lappend keep $seg + } + return ::[join $keep ::] +} + +proc emit_nsdict {nd opt_pathcommands opt_synopsis} { + set location [dict get $nd location] + out namespace $location + foreach {label key} { + children children + packagetails packagetails + packageprefixes packageprefixes + } { + set items [dict get $nd $key] + if {[llength $items]} { + out $label [lsort -dictionary $items] + } + } + set commands [dict get $nd commands] + out commands [llength $commands] + set classified [list] + foreach {label key} { + procs procs + aliases aliases + ensembles ensembles + native native + imported imported + ooclasses ooclasses + ooobjects ooobjects + ooprivateclasses ooprivateclasses + ooprivateobjects ooprivateobjects + coroutines coroutines + interps interps + zlibstreams zlibstreams + undetermined undetermined + } { + set items [dict get $nd $key] + if {[llength $items]} { + out $label [lsort -dictionary $items] + lappend classified {*}$items + } + } + set other [list] + foreach c $commands { + if {$c ni $classified} { + lappend other $c + } + } + if {[llength $other]} { + out other [lsort -dictionary $other] + } + set exported [dict get $nd exported] + if {[llength $exported]} { + out exported [lsort -dictionary $exported] + } + set documented [lsort -dictionary [dict get $nd usageinfo]] + if {[llength $documented]} { + out documented $documented + } + set exportpatterns [dict get $nd namespacexport] + if {[llength $exportpatterns]} { + out exportpatterns $exportpatterns + } + set pathdict [dict get $nd namespacepath] + if {[dict size $pathdict]} { + out namespacepath [dict keys $pathdict] + if {$opt_pathcommands} { + dict for {p pinfo} $pathdict { + set pcmds [list] + if {[dict exists $pinfo commands]} { + set pcmds [dict get $pinfo commands] + } + puts [format " pathcommands(%s): %s" $p [strip [lsort -dictionary $pcmds]]] + } + } + } + if {[dict get $nd itemcount] == 0} { + out note "(no members matched glob '[dict get $nd glob]' - namespace empty, or the package populating it is not loaded)" + } + if {$opt_synopsis && [llength $documented]} { + puts "synopsis:" + foreach c $documented { + set id [punk::ns::nsjoin $location $c] + if {![punk::args::id_exists $id]} { + # alias/import - resolve the documentation id the way cmdhelp does + set id "" + if {![catch {punk::ns::cmdinfo [punk::ns::nsjoin $location $c]} cinfo]} { + set docid [dict get $cinfo docid] + if {$docid ne "" && [punk::args::id_exists $docid]} { + set id $docid + } + } + } + set syn "" + if {$id ne "" && ![catch {punk::args::synopsis $id} s]} { + set syn [string trim [strip $s]] + } + if {$syn eq ""} { + puts [format " %s (no punk::args synopsis available)" $c] + } else { + foreach line [split $syn \n] { + puts " $line" + } + } + } + } +} + +# ---------------------------------------------------------------- arg handling +set opt_synopsis 0 +set opt_pathcommands 0 +set globs [list] +foreach a $argv { + switch -- $a { + -synopsis {set opt_synopsis 1} + -pathcommands {set opt_pathcommands 1} + default {lappend globs $a} + } +} +if {![llength $globs]} { + puts stderr "usage: punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? ..." + puts stderr " e.g: punk91 src script scriptlib/developer/nslist.tcl punk::ns" + exit 2 +} + +# ---------------------------------------------------------------- main +set failures 0 +set output_started 0 +foreach g $globs { + set fq [string map {:::: ::} $g] + if {![string match ::* $fq]} { + set fq ::[string trimleft $fq :] + } + set notes [list] + set base [nonglob_base $fq] + if {![namespace exists $base]} { + set loaded [try_autoload $base] + if {$loaded ne "" && [namespace exists $base]} { + lappend notes "loaded package '$loaded [package present $loaded]' to populate $base" + } + } + set query "" + if {![regexp {[*?]} $fq]} { + if {[namespace exists $fq]} { + set query ${fq}::* + if {[namespace ensemble exists $fq]} { + lappend notes "$fq is also an ensemble command - whatis.tcl shows its subcommand map" + } + } else { + # not a namespace - maybe a command (follow an ensemble to its namespace) + set resolved [uplevel #0 [list namespace which -command $fq]] + if {$resolved ne "" && [namespace ensemble exists $resolved]} { + set ensns [namespace ensemble configure $resolved -namespace] + lappend notes "'$g' is an ensemble command implemented in namespace $ensns - listing that (whatis.tcl shows the authoritative subcommand map)" + set query ${ensns}::* + } elseif {$resolved ne ""} { + puts stderr "nslist: '$g' is a command, not a namespace - use the tcl-whatis skill: punk91 src script scriptlib/developer/whatis.tcl [string trimleft $g :]" + incr failures + continue + } else { + puts stderr "nslist: no namespace (or command) '$g' found - and no package matching it could be loaded" + incr failures + continue + } + } + } else { + if {[namespace exists $base]} { + set query $fq + } else { + puts stderr "nslist: base namespace $base of glob '$g' not found - and no package matching it could be loaded" + incr failures + continue + } + } + if {[catch {punk::ns::get_ns_dicts $query -nspathcommands $opt_pathcommands} dicts]} { + puts stderr "nslist: punk::ns::get_ns_dicts failed for '$query': $dicts" + incr failures + continue + } + if {![llength $dicts]} { + puts stderr "nslist: no namespaces matched '$query'" + incr failures + continue + } + if {$output_started} { + puts "" + } + set output_started 1 + foreach n $notes { + out note $n + } + set i 0 + foreach nd $dicts { + if {$i > 0} { + puts "" + } + incr i + emit_nsdict $nd $opt_pathcommands $opt_synopsis + } +} + +exit [expr {$failures == 0 ? 0 : 1}] diff --git a/scriptlib/developer/whatis.tcl b/scriptlib/developer/whatis.tcl index 12220be4..aec2f3a5 100644 --- a/scriptlib/developer/whatis.tcl +++ b/scriptlib/developer/whatis.tcl @@ -21,6 +21,9 @@ # loaded; without it the kit-stamped copies inside the executable (//zipfs:/...) # are introspected and file:line will not refer to editable files. # Exit 0 on success; 1 command not found; 2 usage error. +# Companion: scriptlib/developer/nslist.tcl (tcl-nslist skill) answers the +# namespace-level question - what commands/child namespaces exist below a +# namespace or package at all. proc strip {text} { if {[info commands ::ansistrip] ne ""} { @@ -129,6 +132,17 @@ if {$resolved eq ""} { } } if {$resolved eq ""} { + # not a command - if it names a namespace (possibly after loading the package of + # the same name), point at the namespace-discovery counterpart script + set nsname ::[string trimleft $name :] + if {![namespace exists $nsname]} { + catch {package require [string trimleft $name :]} + } + if {[namespace exists $nsname]} { + puts stderr "whatis: '$name' is a namespace, not a command - list its contents with:" + puts stderr " punk91 src script scriptlib/developer/nslist.tcl [string trimleft $name :] (tcl-nslist skill)" + exit 1 + } puts stderr "whatis: command '$name' not found (and no matching package could be loaded)" exit 1 } diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index f260ae3e..e38a2b9d 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -1520,6 +1520,28 @@ tcl::namespace::eval punk::ns { return $output } #cli command - impure - relies on caller/ns_current + #2026-07-19 Agent-Generated: documentation-only PUNKARGS (options are forwarded to + #get_ns_dicts unparsed - keep synchronized with that proc's PUNKARGS). + punk::args::define { + @id -id ::punk::ns::nslist_dict + @cmd -name punk::ns::nslist_dict\ + -summary\ + "Namespace-member info dicts for a possibly-relative namespace glob."\ + -help\ + "Convenience wrapper over get_ns_dicts accepting a relative glob: glob is + resolved against the calling namespace context (nspath_here_absolute). + Options and the returned list of dicts are as for get_ns_dicts." + @leaders + glob -type string -optional 1 -default * -help\ + "Namespace glob, absolute or relative to the calling namespace." + @opts + -allbelow -type boolean -default 0 -help\ + "Also report every namespace below those matched (see get_ns_dicts)." + -nspathcommands -type boolean -default 1 -help\ + "Populate the per-entry 'commands' lists in the namespacepath dict + (see get_ns_dicts)." + @values -min 0 -max 0 + } proc nslist_dict {{glob "*"} args} { set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $glob]] return [get_ns_dicts $ns_absolute {*}$args] @@ -2990,7 +3012,67 @@ y" {return quirkykeyscript} #glob chars at last segment match contents/children of namespaces #glob chars in the path will result in multiple namespaces being matched #e.g ::tcl::*::d* will match commands beginning with d and child namespaces beginning with d in any namespaces 1 below ::tcl + #2026-07-19 Agent-Generated: documentation-only PUNKARGS - the proc parses manually (dict + #merge); keep behaviour and definition synchronized. Motivated by exposing this data layer + #to agent tooling: scriptlib/developer/nslist.tcl (tcl-nslist agent skill) consumes it. + punk::args::define { + @id -id ::punk::ns::get_ns_dicts + @cmd -name punk::ns::get_ns_dicts\ + -summary\ + "Namespace-member info dicts for a fully-qualified namespace glob."\ + -help\ + "Return a list of dicts - one per namespace matched by fq_glob - describing + each namespace's members. This is the data layer behind the interactive + namespace listings (n/ n// n/// nslist) and suits programmatic namespace + discovery (e.g scriptlib/developer/nslist.tcl for agents). + Glob chars (* ?) in the last segment of fq_glob match members (commands and + child namespaces); glob chars in earlier segments match multiple namespaces. + A trailing segment of ** additionally reports every namespace below the + matched path. + e.g ::tcl::*::d* matches commands and child namespaces beginning with d in + any namespace one level below ::tcl. + Keys of each returned dict: + location - the fully-qualified namespace described + children - matching child namespace tails (dictionary-sorted) + packagetails - children that exactly match a loadable package name + packageprefixes - children that are a prefix of deeper package names + commands - all matching command tails (every kind) + procs - script procs ('info procs' in the namespace) + exported - commands matching the namespace export patterns + imported - commands imported from another namespace + aliases - interp aliases named in the namespace (an alias may + have no matching command if it was renamed away) + ensembles native coroutines interps zlibstreams ooobjects ooclasses + ooprivateobjects ooprivateclasses - commands by punk::ns::cmdtype + classification + namespacexport - the raw namespace export patterns + undetermined - reserved (currently always empty) + usageinfo - commands with punk::args (or tepam) documentation + namespacepath - dict keyed by each 'namespace path' entry of the + namespace; with -nspathcommands true each value is a dict whose + 'commands' key lists the matching command tails resolvable via that + path entry (empty dicts otherwise) + glob - the member-matching glob applied (the last segment) + itemcount - count of matched members (children + commands) + Namespaces are reported even when itemcount is 0. The procs/exported/ + imported/usageinfo lists are overlapping views over 'commands' (plus + commandless renamed aliases); 'commands' is the complete member list." + @leaders + fq_glob -type string -optional 0 -help\ + "Fully-qualified namespace glob e.g ::punk::ns::* + (an error is raised if not fully qualified)." + @opts + -allbelow -type boolean -default 0 -help\ + "Also report every namespace below those matched by the namespace-path + part of fq_glob (the member glob still applies within each)." + -nspathcommands -type boolean -default 1 -help\ + "Populate the per-entry 'commands' lists in the namespacepath dict + (commands resolvable in the namespace via its 'namespace path' + entries). Use false to skip that work." + @values -min 0 -max 0 + } proc get_ns_dicts {fq_glob args} { + #manual args parsing (dict merge) - keep synchronized with PUNKARGS id ::punk::ns::get_ns_dicts #puts stderr "get_ns_dicts $fq_glob" set glob_is_absolute [expr {[string match ::* $fq_glob]}] if {!$glob_is_absolute} { @@ -3008,9 +3090,6 @@ y" {return quirkykeyscript} set nspathcommands [dict get $opts -nspathcommands] # -- --- --- --- --- --- --- --- --- --- --- --- - set packagetails [list] ;#child namespaces which are an exact match for a package name - set packageprefixes [list] ;#child namespaces which are a prefix match for a package name - but not an exact match - #set location [nsprefix $fq_glob] set commands [list] @@ -3437,6 +3516,14 @@ y" {return quirkykeyscript} # set childtailmatches [lsort $childtailmatches] #} set childtailmatches [lsort -dictionary $childtailmatches] + #2026-07-19 Agent-Updated: packagetails/packageprefixes initialization moved here from + #before the report_namespaces loop: with multiple matched namespaces (path globs, + #trailing **) every dict after the first carried the union of all earlier namespaces' + #entries, mis-marking non-package children in nslist/n// underline styling and in + #get_ns_dicts data. Also: a child with several deeper packages below it was appended + #to packageprefixes once per matching package - now recorded once. + set packagetails [list] ;#child namespaces which are an exact match for a package name + set packageprefixes [list] ;#child namespaces which are a prefix match for a package name - but not an exact match foreach ct $childtailmatches { set fqchild [nsjoin $location $ct] set searchname [string trimleft $fqchild :] @@ -3445,7 +3532,7 @@ y" {return quirkykeyscript} #exact match. lappend packagetails $ct } else { - if {[string match ${searchname}::* $pkgname]} { + if {[string match ${searchname}::* $pkgname] && $ct ni $packageprefixes} { #prefix match - but not exact match lappend packageprefixes $ct } diff --git a/src/modules/punk/ns-buildversion.txt b/src/modules/punk/ns-buildversion.txt index 2e29b9d5..d85796f6 100644 --- a/src/modules/punk/ns-buildversion.txt +++ b/src/modules/punk/ns-buildversion.txt @@ -1,6 +1,7 @@ -0.9.0 +0.9.1 #First line must be a semantic version number #all other lines are ignored. +#0.9.1 - get_ns_dicts fix: packagetails/packageprefixes initialization moved inside the per-namespace loop - with multiple matched namespaces (path globs, trailing **) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in nslist/n// underline styling and in the returned data; a child with several deeper packages was also appended to packageprefixes once per matching package (now once). Single-namespace queries (the nslist.test tier A pins) unaffected; the multi-namespace reset is pinned in nslist.test (nslist_classification_multins_package_buckets_reset). Documentation-only PUNKARGS added for get_ns_dicts (documents the returned per-namespace dict keys and -allbelow/-nspathcommands; parsing stays manual) and nslist_dict (caller-relative wrapper). Motivated by exposing the dict data layer to agent tooling: scriptlib/developer/nslist.tcl backing the new tcl-nslist agent skill. #0.9.0 - cmdhelp -return text: plain-text renderer for machine/LLM consumption - no ANSI codes, no table layout, and the argument section of EVERY command form (or of each form in an explicit -form selection) rendered in a single output, unlike the interactive displays which render only the G-041 best-matching form's argument table ('i -return text after' shows all six after forms' argument sections under one common header). Implementation: resolution is delegated to the existing -return dict recursion (aliases/ensembles/docid/form candidacy all inherited); each form's argument section renders via arg_error -return string -scheme nocolour restricted with -form, is ansistripraw'd, and multiform outputs join the per-form sections (headed ' --ARGUMENTS (form )-- ') below the common header+synopsis emitted once. Argument words still restrict the rendered forms per G-041 semantics ('i -return text after cancel xx' renders the matched cancelscript form only); no advisory-parse error preamble is included (the text form is documentation, not validation - use -return dict for parse status). -grepstr applies to the final text as for other return forms. Supported by the punk::args 0.12.6 arg_error changes (message below usage once; single-line 'name TYPE:... ?DEFAULT:...? ?MULTI:yes?' argument rows in the string renderer). Tests: cmdhelp.test cmdhelp_return_text_all_forms / _argword_form_selection / _single_form_plain_marker. #0.8.1 - synopsis: fixed 'list element in braces followed by "?" instead of space' error when rendering a command whose definition carries a custom @form -synopsis that is not a valid Tcl list (e.g Tcl manpage style '?-form {int|...}?' as ::punk::args::parse formerly used - 's parse' errored). The command-head replacement treated each synopsis line as a Tcl list (lreplace); non-list lines now take a textual fallback that replaces the leading word(s) via index walking and passes the tail through verbatim. Pinned in punk/args synopsis.test (synopsis_form_custom_override_nonlist) since that suite already exercises the punk::ns::synopsis path. #0.8.0 - removed the global command aliases nscommands1 and nscommands2 (user direction 2026-07-14, flagged as removal candidates in the 0.7.0 hygiene pass): earlier pipeline-based iterations superseded by the plain proc nscommands (which adds weird-namespace handling). Neither was documented/exported and no callers existed; nscommands1 was broken - it referenced nonexistent commands (nsthis2, inspect at global scope) and errored on first use. Recoverable via git history. diff --git a/src/tests/modules/punk/ns/testsuites/ns/nslist.test b/src/tests/modules/punk/ns/testsuites/ns/nslist.test index 144407e5..8f245ceb 100644 --- a/src/tests/modules/punk/ns/testsuites/ns/nslist.test +++ b/src/tests/modules/punk/ns/testsuites/ns/nslist.test @@ -191,6 +191,28 @@ namespace eval ::testspace { 1 1 0 ] + #added 2026-07-19 (agent) - regression pin for the punk::ns 0.9.1 fix: packagetails/ + #packageprefixes are per-namespace - with multiple matched namespaces (path globs, + #trailing **) every dict after the first previously carried the union of all earlier + #namespaces' entries. + test nslist_classification_multins_package_buckets_reset {get_ns_dicts packagetails/packageprefixes reset per matched namespace (multi-namespace query)}\ + -setup $fsetup -body { + set ds [punk::ns::get_ns_dicts ::navfix::**] + foreach d $ds { + if {[dict get $d location] eq "::navfix"} { + lappend result [list navfix tails [dict get $d packagetails] prefixes [dict get $d packageprefixes]] + } elseif {[dict get $d location] eq "::navfix::plainchild"} { + lappend result [list plainchild tails [dict get $d packagetails] prefixes [dict get $d packageprefixes]] + } + } + set result + }\ + -cleanup $fcleanup\ + -result [list\ + {navfix tails {bothy tailpkg} prefixes {bothy prefixer}}\ + {plainchild tails {} prefixes {}}\ + ] + test nslist_classification_native_builtin_ns {native bucket populates for a builtin namespace (fixture cannot contain native commands)}\ -setup { set result ""