Browse Source
Drafted from a measured scalability review asking what breaks if every command - including third-party libraries - carries punk::args documentation. Baseline probes (punk91, 58 registered namespaces / 693 definitions) found three cost centres and one governance gap; the two goals split them by kind. G-177 (engine, measurable): the resolved-spec caches key on the whole raw definition text (~args:2152, the author's own todo), so ~4 full-text hashes ride every parse call - an isolation probe shows the ENTIRE per-call growth with documentation size sits in those keys (220 B definition 28.8us vs 9776 B definition of identical argument shape 56.8us; resolve 1.5 -> 29.6us). Registry membership is lists with a nested-loop ldiff behind a length-equality early-out that never fires in a stock session (registered=58 loaded=60 - argdoc namespaces enter loaded_packages un-registered and registration is not deduped), so update_definitions costs 9.3us at R=100 and 2515us at R=2000 while being called per ensemble subcommand. The scan phase is eager and global (15us per definition). Acceptance is six machine-independent ratio pins (1.15x doc-size independence, 2x registry-size independence, early-out liveness plus its false-positive, O(1) scan, 40% raw-text residency drop, byte-identical corpus renders) rather than absolute times, so the suite pins the complexity class and survives hardware changes. G-178 (governance, policy): flat global id namespace with silent collisions (the "last will silently win" todo at ~args:5137), non-idempotent registration into a foreign namespace variable, no definition format-level declaration, and third-party help text that is tstr-evaluated with -allowcommands at resolve time - documentation that runs code. Split from G-177 per the goals/AGENTS.md rule that a goal must be summarizable by title and Scope alone. Overlap survey (goals_xref paths + score): Related lines to G-175 (disjoint - it owns how many times a render resolves, G-177 owns what one resolve costs), G-044/G-050 (latency consumers), G-075 (shared id-lookup path), G-167 + G-056 (both deliberately change render output, which G-177 pins byte-identical - whichever lands second re-baselines the characterization), G-046 (archived predecessor whose display-field deferral is why the residual doc-size tax is purely the cache keys). No archived follow-on disposition required flipping. goals_lint clean (81 active-index goals / 97 archived). Claude-Session: https://claude.ai/code/session_01Y5vEJUZkD4p8jmEFEBVhLi Assisted-by: harness=claude; primary-model=claude-opus-5[1m]; api-location=anthropic.commaster
3 changed files with 150 additions and 0 deletions
@ -0,0 +1,95 @@
|
||||
# G-177 punk::args scale independence: cost decoupled from documentation size and registry size |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/args-999999.0a1.0.tm (resolve/define/undefine cache keying, update_definitions registry structures and scan phasing, parse/get_dict by-id entry path, parse_cache bounding); src/modules/punk/ns-999999.0a1.0.tm (per-subcommand update_definitions call sites in the cmdhelp and get_ns_dicts walks); src/tests/modules/punk/args/testsuites/args/ (new scaling suite + corpus render characterization) |
||||
Goal: punk::args' per-call and per-query costs are decoupled from the two quantities a universally-documented corpus makes large - the byte size of the documentation a command carries, and the number of registered definition namespaces - so that a richly documented command parses no slower than a sparsely documented one of the same argument shape, a single-namespace documentation query costs the same whether 50 or 5000 namespaces are registered, and the raw definition text of the corpus is resident once rather than twice, with every existing render byte-identical across the change. |
||||
Acceptance: measured in one process on one machine with the pre-change baseline recorded in this file - (1) two definitions of identical argument shape whose -help text differs by 40x or more in bytes parse within 1.15x of each other, and punk::args::resolve on a warm cache likewise within 1.15x (today 1.97x and ~20x); (2) update_definitions for one namespace against a fully-loaded registry costs no more than 2x at 4000 registered namespaces what it costs at 100, and an unknown-id real_id miss holds the same bound (today ~270x at 2000); (3) a pin proves the no-work early-out actually fires in a stock session, and a second pin proves a genuinely unloaded namespace still loads when registered/loaded counts coincide - the false-positive the current length-equality test permits; (4) a counter pin shows the namespaces scanned to answer a single-namespace query is O(1) rather than O(registry); (5) raw definition text resident per definition drops by 40% or more against the recorded baseline, and parse_cache has a documented bound with an eviction pin; (6) usage, synopsis and arg_error output over the whole existing definition corpus is byte-identical before and after; full punk/args and punk/ns suites pass under the canonical runtests interpreter. |
||||
|
||||
## Context |
||||
|
||||
Drafted 2026-08-08 from a scalability review asking what breaks if every command - |
||||
including third-party libraries - carries punk::args documentation. Baseline |
||||
measured via `bin/punk91.exe src script`, corpus of 58 registered namespaces / |
||||
693 definitions (point-in-time; line anchors likewise): |
||||
|
||||
parse withid, 220 B definition 28.8 us |
||||
parse withid, 9776 B definition (same arg shape) 56.8 us |
||||
resolve warm, same pair 1.5 -> 29.6 us |
||||
dict exists on the text key, same pair 0.27 -> 5.80 us |
||||
update_definitions one ns, R=100 / 500 / 2000 9.3 us / 218 us / 2515 us |
||||
punklib_ldiff, n=58 / 250 / 1000 / 4000 8.9 us / 85 us / 914 us / 12.0 ms |
||||
cold update_definitions * 10.3 ms / 693 defs = 15 us per definition |
||||
memory 1625 B raw text/def held TWICE |
||||
+ 7721 B resolved spec/def |
||||
~= 11 KB per documented command, per interp |
||||
|
||||
Three root causes: |
||||
|
||||
1. Definition text is the cache key. resolve keys rawdef_cache_about and |
||||
rawdef_cache_argdata on the whole raw definition (~args:2152, where the author |
||||
already flagged it: "for now we will just key using the whole string... |
||||
memory usage probably not ideal"); parse -cache 1 keys parse_cache on |
||||
[list $parseargs $deflist $form] (~args:7234). That is ~4 full-text hashes per |
||||
parse call. The isolation probe shows the ENTIRE per-call growth with |
||||
documentation size sits inside resolve - in the keys, not in the parsing work. |
||||
A documentation tax levied at runtime, on every invocation, permanently. |
||||
|
||||
2. Registry membership is lists. punklib_ldiff (~args:15595) is a nested-loop `ni` |
||||
scan, and the early-out is [llength $loaded_packages] == [llength $registered] |
||||
(~args:5106). In a stock session registered=58 loaded=60 - ::punk::auto_exec::argdoc |
||||
enters loaded_packages without ever being in NAMESPACES, and registration is not |
||||
deduped (appending ::punk::lib twice leaves two entries) - so the fast path never |
||||
fires and every call pays the quadratic. punk::ns calls this per ensemble |
||||
subcommand: at R=2000 a 30-subcommand help render burns ~75 ms in bookkeeping |
||||
that returns nothing. The same length test can also coincidentally hold while a |
||||
namespace is genuinely unloaded - silently missing documentation, and likelier as |
||||
the registrant count grows. |
||||
|
||||
3. The scan phase is eager and global. The first call scans EVERY registered |
||||
namespace regardless of the requested nslist (~args:5113-5157) at 15 us per |
||||
definition - ~300 ms at 20000 definitions, paid on the first help keystroke to |
||||
answer a question about one namespace. |
||||
|
||||
## Approach |
||||
|
||||
1. Key the resolved-spec caches on the definition id (or a cheap digest), keeping |
||||
text -> id only for the anonymous withdef path. The seam already exists: |
||||
parse withid holds the id, calls raw_def to get the text, then hands the text to |
||||
get_dict, which re-derives the id from it. |
||||
2. Dict-as-set for registered/scanned/loaded; dedup at registration; replace the |
||||
length-equality early-out with an explicit pending-set emptiness test. |
||||
3. Scan per-namespace on demand rather than globally on first call. |
||||
4. Drop the double residency of raw text; give parse_cache a bound and eviction. |
||||
5. Ratio pins rather than absolute times, so the suite pins the complexity class |
||||
and survives hardware changes. |
||||
|
||||
## Notes |
||||
|
||||
- id_cache_spec (~args:614) is declared and initialised but never read - dead; |
||||
remove in step. |
||||
- Related: G-175 (@dynamic resolve multiplicity) - same resolve path, disjoint |
||||
question. G-175 owns HOW MANY TIMES one render resolves; G-177 owns WHAT ONE |
||||
RESOLVE COSTS. Neither subsumes the other, and G-177's re-keying does not help |
||||
@dynamic definitions, which bypass the resolved cache entirely |
||||
(`if {!$is_dynamic}` ~args:2175). |
||||
- Related: G-044 (repl command completion and hinting) - the latency consumer; |
||||
high-frequency id lookups during typing are what these ratios make viable. |
||||
- Related: G-050 (punk::ns::synopsis validity marking) - consumer of synopsis cost |
||||
(627 us cold / 48 us warm per command measured at the baseline above). |
||||
- Related: G-075 (punk::args (package) ids) - shares the id-lookup and |
||||
update_definitions prefix-handling path. |
||||
- Related: G-178 (multi-provider safety) - the registration dedup lands here as a |
||||
performance requirement and there as a correctness one; whichever lands first |
||||
satisfies the other's share. |
||||
- Related: G-167 (version-delta availability) and G-056 (display-time word |
||||
wrapping) - both deliberately CHANGE render output, which acceptance (6) pins |
||||
byte-identical across G-177. No conflict, but whichever lands second re-baselines |
||||
the corpus render characterization; G-177's pin is "unchanged by the re-keying", |
||||
never "unchanged forever". |
||||
- Predecessor: G-046 (achieved - see |
||||
goals/archive/G-046-punkargs-deferred-help-and-fixes.md) deferred expensive |
||||
-help processing to display time. That deferral is why the residual doc-size tax |
||||
is PURELY the cache keys; a re-keying must compose with it, not duplicate it. |
||||
- Candidate deliverable, deliberately NOT part of acceptance: a |
||||
scriptlib/developer/punkargs_bench.tcl sibling to goals_lint.tcl so regressions |
||||
are diagnosable rather than merely detectable. |
||||
@ -0,0 +1,47 @@
|
||||
# G-178 punk::args multi-provider safety: id ownership, idempotent registration, declared format level |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/args-999999.0a1.0.tm (duplicate-id detection and policy, register protocol idempotency and diagnostics, definition format-level declaration, display-field command-substitution trust boundary); src/modules/AGENTS.md (third-party definition authoring guidance); src/tests/modules/punk/args/testsuites/args/ (new coverage) |
||||
Goal: punk::args is safe to open to definition providers outside this repo - a second definition claiming an existing id is detected and reported rather than silently overwriting the first, namespace registration is idempotent and its failures are diagnosable rather than silent, a definition can declare the punk::args feature level it requires so an older punk::args fails with a named error instead of an opaque parse failure, and the trust boundary for command substitution inside third-party help text is documented and enforceable. |
||||
Acceptance: a second definition claiming an existing id produces a named duplicate-id diagnostic identifying both providers with the resolution policy documented (today the todo at ~args:5137 - "last will silently win"); registering the same namespace twice leaves exactly one registry entry (pin) and a registered namespace that never materializes is reported rather than silently skipped; a definition may declare a required format/feature level and one requiring a level above the running punk::args raises a named error naming both levels (pin) while an unmarked definition keeps today's behaviour; the command-substitution trust boundary for display fields is documented in define -help and src/modules/AGENTS.md with a mode in which untrusted definitions render without command substitution, pinned by a fixture whose help text would otherwise execute a command; full punk/args suite passes. |
||||
|
||||
## Context |
||||
|
||||
Drafted 2026-08-08 from the same scalability review as G-177 (see that file for the |
||||
measured baseline). Where G-177 is about cost, this goal is about what happens when |
||||
the definition corpus stops being single-authored. |
||||
|
||||
- Flat global id namespace with silent collisions. Ids are plain strings in one |
||||
dict, and update_definitions carries an explicit |
||||
`#todo - detect duplicate ids (last will silently win.. should be reported somewhere)` |
||||
(~args:5137). Ids such as (script)::build are plausible collisions across vendors, |
||||
and the loser vanishes without a trace. |
||||
|
||||
- Registration is lappend into a foreign namespace variable |
||||
(`lappend ::punk::args::register::NAMESPACES ::punk::lib ...`) - no dedup |
||||
(measured: two entries after a double append), no unregister, no diagnostic if the |
||||
namespace never appears. The lazy no-load-required design here is GOOD and worth |
||||
preserving; only its bookkeeping is unguarded. |
||||
|
||||
- No format-level declaration. There is no @requires-style directive and one interp |
||||
holds one punk::args. A library authored against newer directives fails opaquely |
||||
on an older punk::args, with no way to state the dependency. |
||||
|
||||
- Help text is executed code. ${...} bodies are tstr-evaluated with -allowcommands |
||||
in the defining namespace at resolve time (~args:2197). Rendering third-party |
||||
documentation runs third-party code - defensible in-repo, but it needs stating |
||||
before external providers arrive. |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-167 (version-delta availability) - adjacent but distinct, and named |
||||
here to prevent conflation. G-167 declares deltas in a COMMAND'S OWN INTERFACE |
||||
across Tcl versions; G-178 declares the DEFINITION FORMAT LEVEL a definition needs |
||||
from punk::args. |
||||
- Related: G-075 (punk::args (package) ids) - shares the id registry; a duplicate-id |
||||
policy must cover (package)- and (autodef)-prefixed ids. |
||||
- Related: G-177 (scale independence) - the engine goal. Registration dedup is a |
||||
performance requirement there and a correctness requirement here; whichever lands |
||||
first satisfies the other's share. |
||||
- Ordering: G-177 first is the natural sequence - its set-based registry work is |
||||
where dedup and the "registered but never materialized" diagnostic naturally sit. |
||||
Loading…
Reference in new issue