Compare commits
36 Commits
ab4fd7f89d
...
bfcbf705be
81 changed files with 6279 additions and 571 deletions
@ -0,0 +1,46 @@
|
||||
# .fossil-settings/ |
||||
|
||||
## Purpose |
||||
|
||||
Versioned fossil settings (one setting value or glob list per file) for this dual-VCS tree: git and fossil both track the same project. This file is also the canonical home of the git+fossil coexistence contract, including the rules for keeping `.gitignore` and `ignore-glob` in sync. |
||||
|
||||
## Ownership |
||||
|
||||
- `.gitignore` is the canonical statement of ignore intent; `ignore-glob` is hand-derived from it per the rules below - never the other direction. |
||||
- `.fossil-settings/` and `.fossil-custom/` are git-tracked (versioned fossil config rides in git even at sites where fossil is unused). |
||||
|
||||
## Local Contracts |
||||
|
||||
### Files each system must not track |
||||
|
||||
- **Fossil-generated checkout artifacts**: the `manifest` setting (value `rut`) makes fossil regenerate `manifest`, `manifest.uuid` and `manifest.tags` at the checkout root on every checkin/checkout. Fossil auto-manages them (they never appear in extras and cannot be added). Git must ignore them - root-anchored `/manifest`, `/manifest.uuid`, `/manifest.tags` in `.gitignore` (anchored because nested manifest-named files elsewhere are real content) - and must never track them. |
||||
- **Fossil control files**: `.fslckout` / `_FOSSIL_` (checkout db), `*.fossil` (repository db) - git-ignored; fossil auto-ignores its own. |
||||
- **Git control tree**: `.git` stays in `ignore-glob`. Fossil skips dot-prefixed names by default, but an `addremove --dotfiles` scan would otherwise descend into it. |
||||
- Git meta files that are project content (`.gitignore`, `.gitkeep` placeholders) ARE fossil-tracked for mirror completeness - capture them with `--dotfiles` on addremove/add scans. |
||||
|
||||
### ignore-glob derivation rules (verified against fossil 2.28) |
||||
|
||||
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list. |
||||
- A bare directory name prunes that whole tree; `*` crosses `/`; git patterns intended to match at any depth need an additional `*/` variant; `#` comment lines are honoured. |
||||
- Do not add the fossil-generated names (`manifest`, `manifest.uuid`, `manifest.tags`) to `ignore-glob` - fossil handles them itself. |
||||
- **Nested `.gitignore` files are git-only**: ignore files inside subtrees (e.g. the project-layout templates under `src/project_layouts/`) are honoured by git as nested ignores of the outer repo but invisible to fossil - template content they match is untracked in git yet managed by fossil. Treat such divergence as a signal to review the git side (usually `git add -f` of the affected template files). |
||||
- **Case sensitivity differs**: git ignore matching is case-insensitive on Windows (`core.ignorecase`), fossil glob matching is case-sensitive - e.g. a `todo.txt` pattern hits vendored `TODO.txt` in git but not fossil. |
||||
|
||||
### Safe sync procedure (when asked to "sync the ignores") |
||||
|
||||
1. Edit `.gitignore` first (canonical), then hand-translate to `ignore-glob` using the rules above. Never translate a negation literally. |
||||
2. Verify from the checkout root: |
||||
- `fossil extras | git check-ignore --stdin` must output **nothing** (fossil is not under-ignoring relative to git). |
||||
- Git-tracked files invisible to fossil must reduce to the documented exception set plus the fossil-generated manifests: compare `git ls-files | sort` against `(fossil ls; fossil extras) | sort` (beware false diffs from case and non-ASCII path encoding). |
||||
3. A fossil `addremove` after a glob change should use `--dotfiles` and be reviewed before commit; pending adds captured under older globs are stale - `fossil revert` cancels uncommitted adds without touching file content when nothing is committed on top of them. |
||||
|
||||
## Work Guidance |
||||
|
||||
## Verification |
||||
|
||||
- `fossil extras | git check-ignore --stdin` → empty |
||||
- `git ls-files | sort` vs `(fossil ls; fossil extras) | sort` → differences limited to the documented exception set + fossil-generated manifests |
||||
|
||||
## Child DOX Index |
||||
|
||||
(none) |
||||
@ -0,0 +1,41 @@
|
||||
# Changelog |
||||
|
||||
All notable changes to the punkshell project version are documented here. |
||||
The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.toml`. |
||||
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` |
||||
"Project Versioning" section for the bump policy. |
||||
|
||||
## [0.2.6] - 2026-07-06 |
||||
|
||||
- shellfilter 0.2.3: `shellfilter::run` now guarantees tee-stack removal (stdout/stderr restoration) via try/finally even when its body errors, and the decorative stack-status table rendering in `log::critical` blocks is catch-guarded. Previously an error there (observed: a stale same-version textblock snapshot calling the new punk::ansi `sgr_merge_singles` with old option syntax) aborted the run between channel diversion and restoration, leaving the process's stdout/stderr broken and all subsequent output silently lost. |
||||
- runtests.tcl: testinterp tm path order fixed so the bootsupport snapshot wins same-version module ties over vendormodules (verified rule on Tcl 9.0.3, standard and punk::libunknown scanners: `tcl::tm::add` prepends, head of `tcl::tm::list` wins ties, so the last `test_tmlist` element wins); testinterp auto_path gains the parent of `[info library]` so binary packages such as Thread resolve in projects without a root `lib_tcl<N>/<arch>` payload. |
||||
|
||||
## [0.2.5] - 2026-07-06 |
||||
|
||||
- `make.tcl vendorupdate` now checks each vendor source project's fossil/git checkout state and warns (non-fatal, once per VCS root) when pulling from a dirty tree — vendored artifacts built from uncommitted source have no committed provenance. Enforcement policy (abort/override, bootsupport coverage) tracked by goal G-026. |
||||
- punkboot::utils 0.1.1: the dirty-checkout check lives in the module as `vcs_dirty_warnings` (make.tcl calls it via a guarded lazy require and degrades to skipped if the bootsupport snapshot lacks it); bootsupport snapshots refreshed (punkboot::utils 0.1.1, shellrun 0.1.4, punk::console 0.7.1 content catch-up). |
||||
|
||||
## [0.2.4] - 2026-07-06 |
||||
|
||||
- shellrun 0.1.4: added missing `package require punk::args` (the 0.1.3 migration left `runx`/`run`/`runout`/`runerr` calling `punk::args::parse` without requiring it, breaking shellrun in bare interps — including the `src/tests/runtests.tcl` testinterp, which made every source-tree test run fail). |
||||
|
||||
## [0.2.3] - 2026-07-06 |
||||
|
||||
- shellrun: removed `-tcl` from `sh_run`, `sh_runout`, `sh_runerr`, `sh_runx` (was silently swallowed with no useful effect because `sh -c` is always an external shell; now rejected with a usage message). |
||||
- shellrun: migrated `runx` from the legacy `get_run_opts` parser to `punk::args::parse` with a full PUNKARGS spec; `runx` now honours `-debug` and `--timeout=` (previously accepted but silently ignored). |
||||
- shellrun: expanded PUNKARGS documentation for `run`, `runconsole`, `runout`, `runerr`, and `runx` with `@cmd` metadata and `-help` text on all opts/values. |
||||
- shellrun: migrated `sh_run`, `sh_runout`, `sh_runerr`, `sh_runx` from `get_run_opts` to `punk::args::parse` with dedicated PUNKARGS specs. |
||||
- shellrun: removed the internal `get_run_opts` proc; all callers now use `punk::args`. |
||||
|
||||
## [0.2.2] - 2026-07-06 |
||||
|
||||
- punk::console: terminal queries fired while an 8.6-based shell waits at an idle line-mode prompt now fail fast with a discriminable errorcode (`PUNK CONSOLE QUERY HOSTAGE_COOKED_READ`) and emit nothing, instead of timing out (~500ms) and later corrupting input with the swallowed response (idle-reader hostage guard; raw mode and mid-command queries unaffected). Owner-routed queries preserve the owner-side errorcode. |
||||
|
||||
## [0.2.1] - 2026-07-06 |
||||
|
||||
- `make.tcl projectversion`: new advisory subcommand verifying `CHANGELOG.md` matches `punkproject.toml` and warning if `src/` has commits since the last project-version bump. |
||||
- Clarified project versioning policy: the `make.tcl` command interface is part of the product surface. |
||||
|
||||
## [0.2.0] - 2026-07-05 |
||||
|
||||
- Initial tracking of the punkshell project version in `punkproject.toml`. |
||||
@ -0,0 +1,32 @@
|
||||
# bin/ |
||||
|
||||
## Purpose |
||||
|
||||
Built punk shell executables (kits with the punk boot layer), assorted build/experiment tooling, and plain runtime kits under `runtime/`. Executables here are build outputs - agents do not hand-edit binaries. |
||||
|
||||
## Local Contracts |
||||
|
||||
### Launch package modes (built punk shells) |
||||
|
||||
The first argument to a built punk shell may be a dash-delimited package mode composed of tokens from `dev`, `os`, `src`, `internal` (e.g. `punksys src`, `punk902z dev-os`). `internal` is the default and is always appended when absent. A first argument that is not a valid mode list is treated as a subcommand instead. Implementation: `src/vfs/_config/punk_main.tcl` (search `all_package_modes`). |
||||
|
||||
`src` mode is the one that matters for verifying working-tree changes: |
||||
|
||||
- Discovers the project root from the executable's location (exe in `bin/` -> parent directory), not from the cwd - it works from any working directory as long as the executable resides in the project's `bin/`. |
||||
- Prepends `src/modules`, `src/modules_tcl<N>`, `src/bootsupport/modules{,_tcl<N>}` and `src/vendormodules{,_tcl<N>}` to the module path, sets `package prefer latest` so dev-numbered `999999.0a1.0` source modules beat kit-stamped snapshots on unversioned `package require`, and registers `#modpod` modules from `src/modules` (startup notice: `src mode: registered N #modpod modules from .../src/modules`). |
||||
- Consequence: `punksys src` / `punk902z src` exercises current working-tree source with no kit rebuild; a plain launch (`punksys`) exercises the module snapshots baked into the kit at build time. `package present <pkg>` reporting `999999.0a1.0` (vs a release version like `0.7.1`) tells you which set a session is running - runbooks with version expectations must state the intended launch mode. |
||||
|
||||
### Interactive verification shells |
||||
|
||||
- Interactive console/repl verification should cover both Tcl generations - behaviour can differ materially (e.g. the Tcl 8.6 windows console channel driver vs the Tcl 9 rewrite). Use a Tcl 8.6-based punk shell (`punksys.exe`) and a current Tcl 9-based punk shell (named for the Tcl release it embeds, e.g. `punk902z.exe` at the time of writing - ask the user which is current rather than assuming). `info patchlevel` in-session confirms the runtime. |
||||
- `runtime/win32-x86_64/` holds plain tclkits/tclsh runtimes without the punk boot layer - use these for clean-environment probes isolating Tcl-level behaviour from punk (they may lack extensions such as twapi; add an external lib dir to `auto_path` when a probe needs one). |
||||
|
||||
## Work Guidance |
||||
|
||||
## Verification |
||||
|
||||
None - build outputs; behaviour is verified via `src/tests/` and interactive runbooks. |
||||
|
||||
## Child DOX Index |
||||
|
||||
- `runtime/win32-x86_64/` - plain runtime kits (no child AGENTS.md needed; covered by this file's contracts) |
||||
@ -0,0 +1,107 @@
|
||||
# G-011 Optional per-console err channel with defined stderr semantics |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/opunk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm |
||||
Acceptance: console_spec_resolve and every -console site accept an {in out err} spec (err optional; existing pair/instance-name/object spec forms unchanged); opunk::Console exposes the err channel (nullable, additive base-class change); an unset err resolves to stderr for the default console and to the console's own out channel otherwise; punk::console's own warnings/diagnostics emitted while operating on a resolvable console go to that console's err (raw puts stderr remains only where no console is in play); an emit-to-err path exists and is exercised by at least one real consumer (e.g punk::repl); the effective err is discoverable from any thread/interp via console_fact_get (fact key err, returning the effective err channel name); ownership/fact/mode-cache keys remain canonical {in out}; the existing console test suites pass unchanged. |
||||
|
||||
## Context |
||||
|
||||
punk::console models exactly two channels per console: {in out}, defaulting to {stdin stdout}. |
||||
For pure ANSI terminal control that is a faithful model - queries and responses share one |
||||
terminal connection by protocol design - but the goals collectively move punk::console from |
||||
"terminal control library" toward "session-wide console abstraction" (G-001 pluggable backends, |
||||
G-007 location transparency, G-008 scoped state, G-009 themed profiles), and there the missing |
||||
stderr stream becomes load-bearing: |
||||
|
||||
- **Error/diagnostic routing is ad-hoc.** punk::console's own warnings (enableRaw/disableRaw |
||||
failures, query timeouts, reader-cooperation warnings, test_char_width failures - dozens of |
||||
sites) go to raw `puts stderr`, bypassing the abstraction the module provides. A console |
||||
backed by an ssh channel or tk widget (G-001) has no principled way to receive its own error |
||||
output: backend authors must either merge stderr into stdout or bypass punk::console for |
||||
errors, fragmenting the abstraction. |
||||
- **No console-level separation of result vs diagnostic streams.** The shellrun |
||||
run/runout/runerr/runx family preserves the stdout/stderr distinction at the process layer, |
||||
but once output enters the console the distinction collapses into the single out channel. A |
||||
subshell wanting to display a process's stderr differently (colour, prefix, separate pane) |
||||
must do so outside the console model - and a G-009 theme has no stderr stream to style. |
||||
- **Cross-interp discovery has no slot.** The G-007 fact store and ownership registry are keyed |
||||
by canonical {in out}; "what is my console's stderr?" has no fact to read, so location |
||||
transparency covers stdout-shaped operations only. |
||||
- **Raw-mode restore is asymmetric.** `previous_stty_state_stderr` is tracked for stty-state |
||||
restoration, but the model gives no way to address stderr as a console channel. |
||||
|
||||
## Approach |
||||
|
||||
**err is an attribute of a console, not part of its identity.** Console identity - the key for |
||||
the ownership registry (punk_console_owners), fact store (punk_console_facts) and mode cache |
||||
(punk_console_modecache) - remains the canonical {in out} pair: two consoles differing only in |
||||
err target are the same terminal, and keeping the key stable means the G-007 slices are |
||||
untouched. |
||||
|
||||
1. **Spec grammar.** console_spec_resolve (and the -console option everywhere) accepts a |
||||
3-element {in out err} list. The 3-element slot is free in the existing grammar (pairs are |
||||
llength 2, instance names 1, voo object values >= 7). Existing spec forms are unchanged. |
||||
2. **Object layer.** ::opunk::Console gains a nullable err field beside o_in/o_out (additive |
||||
base-class change; subclasses without a stderr-equivalent leave it unset). |
||||
3. **Resolution defaults.** An unset err resolves to process stderr for the default console |
||||
{stdin stdout} (matching where its diagnostics go today) and to the console's own out |
||||
channel for any other console (single-stream backends merge, losing nothing they had). |
||||
4. **Diagnostics routing.** punk::console warnings/diagnostics raised while operating on a |
||||
resolvable console are emitted to that console's effective err. Raw `puts stderr` remains |
||||
only for contexts where no console is in play (e.g module-load-time warnings). |
||||
5. **Emit path.** A first-class emit-to-err form (shape decided at implementation time - e.g an |
||||
err-target variant of the opt_console_out convention) exercised by at least one real |
||||
consumer such as punk::repl. The two consumer-shaped clauses divide deliberately: the |
||||
internal diagnostics migration (item 4) proves the abstraction serves its own first |
||||
consumer and is self-contained; the external consumer proves the *public* emit form is |
||||
usable by subshells wanting to present stderr distinctly. |
||||
6. **Cross-thread discovery.** The effective err is exposed through console_fact_get under |
||||
fact key `err`, returning the effective err channel name, so any thread/interp of a |
||||
session can answer "where do this console's diagnostics go" (G-007 location transparency |
||||
extends to the err attribute). |
||||
|
||||
## Alternatives considered |
||||
|
||||
- **err in the identity key: canonical {in out err} triple** - rejected: conflates routing with |
||||
identity (a console does not become a different terminal when its diagnostic target changes), |
||||
and churns the G-007 registry/fact/cache keying and its tests for no modelling gain. |
||||
- **Scope-boundary documentation only (no third channel)** - rejected by the user for subshell |
||||
flexibility: G-001 backends and G-009 themes need a distinct diagnostic stream to be |
||||
first-class, and a parallel unmanaged diagnostics mechanism would fragment the abstraction. |
||||
(Had punk::console stayed a pure terminal-control library, this would have been the right |
||||
answer - the dual-channel model is correct for ANSI control itself.) |
||||
- **Uniform default err == out (including the default console)** - rejected: silently changes |
||||
where the default console's diagnostics land today (process stderr), breaking redirection |
||||
and 2>-style capture users rely on. |
||||
- **Unset err means none (error on emit-to-err)** - rejected: pushes fallback burden onto every |
||||
caller and makes diagnostics emission failure-prone exactly where it must be reliable. |
||||
|
||||
## Notes |
||||
|
||||
- **The "-console surface" is the choke points, not per-site edits.** The module's documented |
||||
convention already funnels every -console site through a small resolution set: |
||||
console_spec_resolve, internal::hybrid_console_spec, internal::opt_console_out / |
||||
opt_console_channels (+_var variants), internal::spec_to_channelpair, and the shared PUNKARGS |
||||
fragments ::punk::console::argdoc::console_opts / console_emit_opts. Accepting the 3-element |
||||
form there satisfies "every -console site" by construction; a site that bypasses these |
||||
helpers is already a convention violation to be fixed on discovery, not a reason to audit |
||||
the whole module up front. |
||||
- **Channel-name locality.** Like in/out, an err channel name is thread-local for non-std |
||||
channels; the fact-store exposure inherits the owner-qualified keying that already handles |
||||
this for other facts. What is discoverable cross-thread is the owner-context name (and the |
||||
default-console case, where std names are process-wide). |
||||
- **G-001 interaction.** G-001's acceptance requires the base class unchanged *by G-001's own |
||||
work*; the nullable err field is an additive change made by this goal. If G-011 lands first, |
||||
G-001 subclasses simply inherit the field; sequencing either way is safe. Backend authors |
||||
should note the default is "merge into out": a backend with a genuinely distinct diagnostic |
||||
stream (e.g a separate ssh extended-data channel or a dedicated tk widget pane) must set err |
||||
explicitly at construction to get separation. |
||||
- **previous_stty_state_stderr stays as-is.** It preserves stty state of the process stderr fd |
||||
across raw-mode cycling - terminal-mode restoration, orthogonal to diagnostic output routing. |
||||
This goal adds nothing to it and removes nothing from it. |
||||
- **G-008/G-009 interaction.** Once err exists, a per-subshell re-route of diagnostics is |
||||
console state that a G-008 state set could capture; that is enrichment for those goals, not |
||||
part of this goal's acceptance. |
||||
- **Not in scope.** Channel transforms/filtering on err (shellfilter stacks), a separate err |
||||
reader, and any change to shellrun's process-layer stdout/stderr handling - this goal defines |
||||
the console-side slot they can target. |
||||
@ -0,0 +1,80 @@
|
||||
# G-012 Template system: inert VCS-config payloads and explicit layout refresh |
||||
|
||||
Status: proposed |
||||
Scope: src/project_layouts/, src/make.tcl, src/modules/punk/mix/ (layout instantiation), fauxlink module (bootsupport 0.1.1 - promoted if chosen as mechanism) |
||||
Acceptance: a scan of src/project_layouts finds no file named .gitignore, and git check-ignore --no-index over every layout file matches only root-.gitignore rules (nested rules provably inert); a project generated from each affected layout receives a working .gitignore whose content matches the canonical payload/target; after editing a canonical source (e.g. root .gitignore), the make.tcl template-refresh step updates the derived layout payloads (punkcheck-tracked), covering vendor/punk layouts as well as custom/_project; the previously hidden template files (layout READMEs, vendored TODO-class files) remain git-tracked without per-file force-add exceptions. |
||||
|
||||
## Context |
||||
|
||||
Project layouts under `src/project_layouts/` carry `.gitignore` files as *payload* - the ignore |
||||
file a generated project is supposed to receive (essentially a copy of shellspy's own root |
||||
`.gitignore`). But git has no concept of payload: any `.gitignore` anywhere in the working tree |
||||
is live configuration for its subtree, with leading-`/` patterns anchored to the nested file's |
||||
own directory. The result is a template that self-censors - the layout ignores, in the shellspy |
||||
repo, exactly what a generated project would ignore in itself. |
||||
|
||||
Discovered 2026-07-06 during git/fossil tracked-set verification (fossil reads only the |
||||
checkout-root `.fossil-settings/ignore-glob`, so it managed template files git silently |
||||
dropped): four `vendor/punk/*` layouts each carry a live `.gitignore`, and |
||||
`.../project-0.1/{bin,lib,modules}/README.md` plus `bin/runtime/README.md` were untracked in |
||||
git. The `bin/runtime` case shows the nastier variant - git does not descend into an excluded |
||||
directory, so negation patterns cannot resurrect deeper paths. The immediate stopgap was |
||||
per-file `git add -f` (commit fc1c474c), which protects only existing files: any new file |
||||
placed in a template's ignored spots drops silently again. The dual-VCS contract |
||||
(.fossil-settings/AGENTS.md) records the git/fossil asymmetry and points here for the fix. |
||||
|
||||
A secondary weakness: template payloads that mirror living files (the root `.gitignore`, |
||||
build.tcl/make.tcl, bootsupport modules) have only partial refresh machinery. make.tcl's |
||||
`sync_layouts` step (punkcheck-tracked) syncs just `build.tcl` and `make.tcl`, and only into |
||||
the `custom/_project` layout base - the `vendor/punk/*` layouts have no refresh mechanism, so |
||||
payload drift is unmanaged. |
||||
|
||||
## Approach |
||||
|
||||
1. **Store VCS-config payloads inert.** Two candidate mechanisms; the choice is an |
||||
implementation decision to be recorded here when made: |
||||
- *Neutral rename*: payload stored as e.g. `_gitignore` or `gitignore.in`; layout |
||||
instantiation renames it to `.gitignore` in the generated project. Simple, no new |
||||
dependency; templates keep a literal copy (drift handled by the refresh step). |
||||
- *Fauxlink*: payload stored as `<nominal>#<encoded-target>.fauxlink` (see fauxlink module, |
||||
currently bootsupport 0.1.1) and resolved at generation. Inert for git by construction, |
||||
and the target can be the canonical root `.gitignore` itself - eliminating copy drift |
||||
rather than managing it. Requires generation-time resolution in punk::mix and possibly |
||||
promoting fauxlink to a first-class module dependency. |
||||
2. **Materialize at generation.** The punk::mix layout-instantiation path produces a real |
||||
`.gitignore` in the generated project regardless of mechanism, with content equal to the |
||||
canonical payload/target. |
||||
3. **Explicit template refresh in make.tcl.** Generalise the existing `sync_layouts` step into |
||||
a defined template-refresh covering the `vendor/punk/*` layout base as well as |
||||
`custom/_project`, and derived payloads beyond build.tcl/make.tcl (the gitignore payload, |
||||
and prospectively other mirrored content). punkcheck-tracked like the existing step, so |
||||
refreshes are recorded and skippable when sources are unchanged. |
||||
4. **Retire the stopgap.** With inert payloads, the per-file force-adds from fc1c474c become |
||||
ordinary tracked files (no ignore rule matches them); the accidental-ignore class is |
||||
structurally impossible rather than patched per file. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- **Negation entries inside the nested template .gitignore files** (e.g. `!README.md`) - |
||||
rejected: pollutes the payload that generated projects receive, and cannot fix the |
||||
excluded-directory case (`bin/runtime/`) because git does not descend into excluded dirs to |
||||
evaluate negations. |
||||
- **Keep per-file `git add -f` only** - rejected as the durable answer: silent drop recurs for |
||||
every new file placed under a template's ignored paths; nothing structural changes. |
||||
- **`.git/info/exclude` or repo-local config tricks** - rejected: not versioned/portable to |
||||
other clones and contributors. |
||||
- **Do nothing on the git side (fossil sees the files)** - rejected: git is the day-to-day VCS; |
||||
a mirror having the truer view is a symptom, not a solution. |
||||
|
||||
## Notes |
||||
|
||||
- Affected today: `src/project_layouts/vendor/punk/{basic,minimal,project-0.1,sample-0.1}/.gitignore`. |
||||
The same inert-payload treatment should apply to any future live-config payloads in layouts |
||||
(e.g. `.gitattributes`); `.fossil-settings/` payloads in layouts are inert by nature (fossil |
||||
reads them only at a checkout root) and need no change. |
||||
- fauxlink background: target encoded in the filename (`+` for `/`, url-style escapes), no |
||||
filesystem support required, application-driven resolution - see |
||||
`src/bootsupport/modules/fauxlink-0.1.1.tm` doctools header. |
||||
- Verification tie-in: the dual-VCS sync checks in `.fossil-settings/AGENTS.md` double as a |
||||
regression detector for this class - after this goal, the "git-tracked invisible to fossil" |
||||
comparison should show no template-payload entries. |
||||
@ -0,0 +1,71 @@
|
||||
# G-013 Raw mode as the repl's default input mode |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/console-999999.0a1.0.tm |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Line mode is today's default because on Windows it gives OS-provided line editing |
||||
and arrow-key history for free. Raw mode is the intended default, blocked by (1) |
||||
debug output emitted with cursor-move sequences, and (2) unfinished raw editor |
||||
features. Beyond display polish, flipping the default retires the last known |
||||
reader-cooperation hole on tcl 8.6: terminal queries fired while the line-mode |
||||
reader is armed at an idle prompt are hostage-prone by design (punk::console 0.7.1 |
||||
made this fail fast rather than hang; raw default makes it work). It also removes |
||||
the line-mode type-ahead-loss window during terminal-query raw cycling |
||||
(#review, repl-999999.0a1.0.tm ~1677). |
||||
|
||||
## Debug emission inventory (as of repl 0.2.2) |
||||
|
||||
1. **add_chunk top-left frame** (repl ~1145-1173) - per-keystroke framed dump of the |
||||
overtype::renderline merge, painted at row 2 col 1. Currently ungated. This is |
||||
definitely a debug feature: it gets its own toggle, operable from within a |
||||
running repl, default off. |
||||
2. **Right-hand live editbuf view** (console_editbufview, repl ~2043, called ~2315) - |
||||
currently ungated. An important development aid for per-keystroke features |
||||
(e.g. command completion) and stays available, but behind its own separate gate. |
||||
Its startup default should ultimately come from stored configuration via |
||||
::punk::config (see G-014) - toml-backed, independently configurable per named |
||||
subshell - with off as the built-in fallback. Longer term, once the console |
||||
framework is more complete (G-001 backends), this live view could be streamed |
||||
off-console instead - e.g. over udp to a monitoring app, or to a tk text-widget |
||||
system acting as a pseudo terminal. |
||||
3. **console_debugview** (repl ~1976) - already gated (debug_repl > 0). |
||||
4. **Marked-line form** (debug_repl_emit, repl ~2566-2599, debug_repl > 100) - emits |
||||
prefixed lines before the command result via the debugreport pseudo-channel, no |
||||
cursor addressing. Retained as the baseline debug form for simple terminals |
||||
(vt52, other console systems we implement) where complex terminal interaction |
||||
is unavailable or unwanted. |
||||
|
||||
## debugrepl activation review |
||||
|
||||
Current mechanism: first word "debugrepl" in the submitted command is intercepted |
||||
before eval; bare/N/cancel manipulate a oneshot decrementing counter whose |
||||
thresholds (>0, >100) select display layers. Review whether first-word |
||||
interception is worth retaining or whether a proper Tcl command is better: a real |
||||
command can be restricted in subshells via interp hide (aligns with G-003/G-009 |
||||
restriction mechanisms), and named toggles would replace the magic thresholds now |
||||
that gates are separate. Whatever the outcome, the marked-line output form itself |
||||
stays (see inventory item 4). |
||||
|
||||
## Approach notes |
||||
|
||||
- Cursor save/restore is a single slot, not a stack: the code already mixes |
||||
cursorsave_ and plain move_emitblock variants to avoid collisions (comment at |
||||
repl ~2031). Toggleable frames need one coherent cursor-bookkeeping policy. |
||||
- The raw chunk path calls punk::console::get_size per keystroke to position the |
||||
rhs frames (repl ~2296-2308, acknowledged 1+ms). Cache the size with explicit |
||||
invalidation; avoid any per-keystroke terminal query when no debug view is on. |
||||
- Raw editor stubs to replace: arrow-right/up and esc currently just rputs debug |
||||
notices (repl ~2477-2492); history navigation and cursor movement are the |
||||
parity items. |
||||
- Toggle startup defaults resolve through ::punk::config stored configuration when |
||||
available (G-014); built-in defaults apply when no configuration exists. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Keeping line mode as default and making idle-prompt queries cooperate with the |
||||
8.6 parked reader - rejected: the 8.6 console driver parks a cooked ReadConsole |
||||
that a raw flip cannot rescue (established 2026-07-05); cooperation is not |
||||
achievable there, only fail-fast. |
||||
@ -0,0 +1,61 @@
|
||||
# G-014 ::punk::config stored configuration: toml files with named-subshell scoping |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/config-0.1.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm, src/vendormodules/tomlish-*.tm (vendored; canonical source in the external tomlish project space) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
punk::config 0.1 already establishes the config-dir convention (XDG_CONFIG_HOME |
||||
preferred, ~/.config/punk/shell fallback - see punk::config::dir) and declares |
||||
toml as the config file format, with a configset/config.toml startup-file notion |
||||
sketched but not implemented. Runtime configuration today is a mix of env-var |
||||
derived values and hardcoded defaults; there is no path for a user to declare a |
||||
setting in a file and have a feature pick it up, and no way to configure named |
||||
subshells independently. |
||||
|
||||
The immediate consumer motivating completion is G-013: the raw-mode debug-view |
||||
toggles (notably the right-hand live editbuf view) need startup defaults that |
||||
come from stored configuration rather than hardcoded values, and a named subshell |
||||
should be able to carry its own defaults (a subshell dedicated to developing |
||||
per-keystroke features might default the editbuf view on, while the top-level |
||||
shell keeps it off). |
||||
|
||||
## Precondition: tomlish punk::args documentation |
||||
|
||||
All toml parsing/serialisation goes through the tomlish module - no ad-hoc toml |
||||
parsing in punk::config. tomlish is vendored (src/vendormodules/tomlish-1.1.8.tm |
||||
at time of writing, with a bootsupport copy) but its canonical source lives in a |
||||
separate project space under our control: documentation work happens upstream |
||||
there and lands here by re-vendoring, not by editing the vendored copies. |
||||
|
||||
As of 1.1.8, punk::args coverage exists only on the cmdline app layer |
||||
(tomlish::app::decode_to_typedjson, tomlish::app::encode_from_typedjson, |
||||
tomlish::app::test, and the tomlish::cmdline definition). The library API procs |
||||
punk::config would consume (e.g. tomlish::from_toml, tomlish::to_dict, |
||||
tomlish::from_dict, tomlish::update_tomlish_from_dict) have no PUNKARGS argdoc |
||||
blocks. Those procs must gain punk::args documentation upstream and the updated |
||||
tomlish be re-vendored before punk::config implementation proceeds. |
||||
|
||||
## Approach |
||||
|
||||
- All toml reading/writing via the tomlish module (see precondition above). |
||||
- toml files under the punk::config::dir location, organised by configset. |
||||
- Resolution order: named-subshell override -> parent/default scope -> built-in |
||||
default. Absence of any config files must be a fully silent, working state |
||||
(beyond the existing missing-dir notice). |
||||
- Settings must be resolvable from the contexts that need them (repl thread and |
||||
code interp / codethread workers), consistent with the G-007 |
||||
location-transparency model. |
||||
- Ties into G-009 themed subshell profiles: a named profile is a natural configset |
||||
consumer. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Write-back (persisting a toggle changed in a running repl to the toml configset |
||||
on explicit request, surviving restart) - deferred: kept out of acceptance to |
||||
stop this goal sprawling; a natural follow-on goal once read-side resolution is |
||||
proven. |
||||
- Reusing Tcl-native config formats (plain dict/script files) - rejected: toml is |
||||
already the declared format in punk::config 0.1 and is editable/diffable by |
||||
users without Tcl knowledge. |
||||
@ -0,0 +1,85 @@
|
||||
# G-015 Punk executable `script` subcommand: reliable non-interactive piped/script execution |
||||
|
||||
Status: proposed |
||||
Scope: src/vfs/_config/punk_main.tcl, src/lib/app-punkshell/punkshell.tcl (script path or a leaner dedicated app package) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Agents (and exec-style callers generally) currently have no reliable way to run a |
||||
one-shot command against a punk executable. The working pattern is to pipe repl |
||||
input into the default shell path, e.g. (powershell): |
||||
|
||||
'dev projects.work *tomlish*;exit' | punk902z |
||||
|
||||
This is fragile in two ways: |
||||
|
||||
1. The trailing `exit` is load-bearing: without it, stdin EOF triggers the |
||||
app-punkshell eof handling, whose default heuristic policy attempts to reopen |
||||
the console (`CONIN$` / `/dev/tty`) and drop into an interactive repl. When a |
||||
console exists but no human is watching (typical agent/CI harness), that is a |
||||
permanent hang. `PUNK_PIPE_EOF=exit` mitigates, but callers must know to set it. |
||||
2. If any command before the `exit` errors, the repl keeps consuming input and |
||||
the `exit` line may never run as intended - the session can still land |
||||
interactive, and the process exit code does not reflect the failure. |
||||
|
||||
Meanwhile the `script` subcommand in the punk_main.tcl dispatch |
||||
(src/vfs/_config/punk_main.tcl ~1163) is an incomplete stub: it prints stderr |
||||
diagnostics ("main.tcl launching script with args: ...") and sets |
||||
::tcl_interactive, but never actually executes anything. Note also the dispatch |
||||
default (~1090): any first argument that is not a recognised mode/subcommand is |
||||
reclassified as `script`, so completing this path also completes the bare |
||||
`<punkexe> <somefile>` form. |
||||
|
||||
The `shell` subcommand path (app-punkshell) wraps the session in shellfilter |
||||
channel stacks (`punkshellout`/`punkshellerr` with file/syslog logging targets, |
||||
plus ansiwrap on stderr in places). Those transforms exist for interactive-shell |
||||
ergonomics and logging; for a one-shot script call they add overhead, alter |
||||
output, and risk stderr chatter - which matters because Tcl `exec` treats any |
||||
stderr output as command failure by default (see the comment block in |
||||
punk_main.tcl's `punk` subcommand case). |
||||
|
||||
## Approach |
||||
|
||||
- `<punkexe> script <file> [<args>...]` - execute the file with conventional |
||||
::argv0/::argv semantics, exit with its status. |
||||
- `... | <punkexe> script` - read the whole of stdin as the script when no |
||||
scriptname is supplied and stdin is piped/redirected; terminate at EOF, never |
||||
reopening a console. The scriptname form must still work with piped stdin |
||||
present (the script may itself want to read stdin) - "with or without a |
||||
trailing scriptname". |
||||
- No shellfilter stacks/transforms on the script path; plain channels. |
||||
- No launcher output of its own on stdout or stderr (remove the stub |
||||
diagnostics); the script's own output is the only output. |
||||
- Errors: report on stderr, exit nonzero. Success: exit 0 (or the script's own |
||||
explicit exit code). No interactive fallback on this subcommand under any |
||||
eof/error condition - PUNK_PIPE_EOF and console availability are irrelevant |
||||
here by design. |
||||
- The script interp loads the basic punk modules and aliases that are available |
||||
by default in an interactive punk shell, so commands like `dev projects.work` |
||||
work in a piped one-liner with no `package require` boilerplate. This is |
||||
deliberately not the leanest environment for basic scripts - fast/bare script |
||||
execution is the future province of an expanded `tclsh` subcommand, out of |
||||
scope here. The exact recommended one-liner gets documented under G-017. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Hardening the shell/repl pipe path instead (PUNK_PIPE_EOF, error-aborts-input) |
||||
- insufficient alone: the shell path's channel transforms and repl semantics |
||||
are wrong for exec-style callers even when eof/error policy is fixed; a |
||||
dedicated clean path is simpler than making the interactive path dual-purpose. |
||||
- Requiring agents to set PUNK_PIPE_EOF=exit and keep the trailing `exit` - |
||||
rejected as the durable answer: it leaves exit codes meaningless on mid-script |
||||
errors and depends on every caller knowing the incantation. |
||||
- A bare interp (no punk modules/aliases preloaded) for the script path - |
||||
rejected for this subcommand: the point is that shell-default commands |
||||
(`dev projects.work` etc.) work as piped one-liners; lean bare-script |
||||
execution belongs to a later expansion of the `tclsh` subcommand. |
||||
|
||||
## Notes |
||||
|
||||
- The eof policy machinery (PUNK_PIPE_EOF exit/interactive/heuristic) in |
||||
app-punkshell (~punkshell.tcl 466-507) stays as-is for the `shell` subcommand; |
||||
this goal does not change interactive-shell behaviour. |
||||
- Related later goal: G-016 extends what the motivating `projects.work` call can |
||||
find; G-017 documents the agent-facing invocation pattern once this lands. |
||||
@ -0,0 +1,58 @@
|
||||
# G-016 `projects.work` discovers git-based projects alongside fossil |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/mix/commandset/project-999999.0a1.0.tm, src/modules/punk/repo-999999.0a1.0.tm |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
`dev projects.work <glob>` (punk::mix::commandset::project::collection::work) |
||||
answers "where are my projects checked out" by opening the central fossil |
||||
config-db, globbing repository database filenames, and listing the known |
||||
checkout directories per repo - with optional per-checkout file-state detail. |
||||
This makes it the natural mechanism for agents to locate sibling projects |
||||
(e.g. the tomlish project space referenced by G-014) instead of recursively |
||||
scanning the filesystem. |
||||
|
||||
The gap: it is fossil-only. Git-based projects are invisible to it, and git has |
||||
no equivalent of fossil's central config-db - there is no built-in registry of |
||||
clones on a machine. So git discovery needs a defined enumeration source of its |
||||
own before the listing can be extended. |
||||
|
||||
## Approach |
||||
|
||||
Enumeration-source candidates (implementation decision, to be recorded here when |
||||
made): |
||||
|
||||
- **Configured search roots**: a punk::config setting (natural G-014 consumer) |
||||
listing parent directories to scan one or two levels deep for `.git` dirs. |
||||
Bounded scan, no registry maintenance, but discovery limited to declared roots. |
||||
- **Punk-maintained registry**: record project paths when punk tooling |
||||
creates/opens them (and offer a scan-once command to seed it). Fast lookups, |
||||
works for arbitrary locations, but can go stale. |
||||
- Hybrid: registry seeded/refreshed by an explicit scan of configured roots. |
||||
|
||||
Result-shape considerations: |
||||
|
||||
- Each row identifies its VCS. Fossil rows keep their current columns |
||||
(repo db filename, project name/code, checkout dirs, dup-set annotations); |
||||
what the git analogue of "project name" is (dir name, remote URL tail, |
||||
configured name) is part of the design work. |
||||
- A dual git+fossil workdir (like the shellspy repo itself) is one project row |
||||
with both VCSs indicated, not two rows. |
||||
- Fossil supports multiple checkouts per repo db; git worktrees are the |
||||
analogous multi-workdir case and should at least not break the listing. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Filesystem-wide scanning at query time - rejected: unbounded cost and exactly |
||||
the behaviour this mechanism is meant to replace for agents. |
||||
- Relying on external tools' state (e.g. IDE/zoxide/gh caches) - rejected: |
||||
non-portable, not present on all machines, opaque formats. |
||||
|
||||
## Notes |
||||
|
||||
- Depends on nothing, but its value to agents is realised through G-015 |
||||
(reliable piped invocation) and G-017 (agent guidance documenting the call). |
||||
- The `-cd` / `-detail` options and case-insensitive glob behaviour of the |
||||
existing command are contracts to preserve. |
||||
@ -0,0 +1,56 @@
|
||||
# G-018 Zig-built plain tclsh kits: self-contained zip-based executables without punk infrastructure |
||||
|
||||
Status: proposed |
||||
Scope: build.zig / build.zig.zon (per G-005), src/runtime/, src/make.tcl integration |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
G-005 establishes zig-based build infrastructure to build binary dependencies |
||||
(including Tcl9) from source. This goal extends that infrastructure's output |
||||
targets: beyond building the punkshell product's own runtimes, developers should |
||||
be able to produce plain, self-contained, zip-based tclsh executables with no |
||||
punk-specific infrastructure at all - no punk boot layer (punk_main.tcl |
||||
dispatch), no punk modules, no punkshell apps. A general-purpose single-file |
||||
tclsh, useful independently of punkshell. |
||||
|
||||
This defines one end of a deliberate spectrum of buildable executables: |
||||
|
||||
- plain tclsh kit (this goal) - stock Tcl only |
||||
- trimmed punk executable (G-019) - punk infrastructure plus only the modules a |
||||
dependency scan proves needed |
||||
- 'batteries included' punk executable - the current full-kit approach |
||||
|
||||
## Approach |
||||
|
||||
- Zip-based (zipfs) kit assembly as the packaging mechanism, consistent with |
||||
Tcl 9's built-in zipfs support. |
||||
- **Tcl 8.6 phasing (decided 2026-07-06)**: the 8.6 kit mechanism is more |
||||
complex to support (no built-in zipfs) and is wanted in the end, but must not |
||||
block other work. Interim step: support plain (non-kit) tcl 8.6 builds from |
||||
the zig infrastructure, and implement 8.6 kits when practical. This goal's |
||||
acceptance is satisfiable by a Tcl 9 kit alone; 8.6 kit support is a |
||||
follow-on within the same goal's spirit, not a gate. |
||||
- The plain kit is a natural first smoke target for G-005's Tcl-from-source |
||||
build: it exercises the compiled runtime with minimal packaging logic before |
||||
the more elaborate punk kit assembly. |
||||
- `src/make.tcl` integration so the target is invocable through the normal |
||||
project build entrypoint as well as directly via zig. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Relying on third-party tclkit/kitcreator builds for plain kits - rejected as |
||||
the durable answer: the point of G-005 is developer-reproducible builds from |
||||
source in this project's toolchain, and external kit builders are exactly the |
||||
committed-binary/procurement dependency G-004..G-006 are removing. |
||||
|
||||
## Notes |
||||
|
||||
- Depends on G-005 (zig builds Tcl from source); complements G-004 (no committed |
||||
binaries) by making plain runtimes reproducible on demand. |
||||
- 'Self-contained' means: runs conventional tclsh invocations on a machine with |
||||
no Tcl installation - stock Tcl library scripts ride inside the kit. |
||||
- Wish executables are not a build target at any point on the spectrum: kits |
||||
that want Tk ship it as a loadable package (as the punk Windows kit already |
||||
does - `package require Tk` from the tclsh-style executable, console |
||||
retained). Applies to plain kits here and punk kits alike. |
||||
@ -0,0 +1,67 @@
|
||||
# G-019 Dependency-scan-driven module trimming for punk-based executables |
||||
|
||||
Status: proposed |
||||
Scope: src/make.tcl, src/modules/punk/lib-999999.0a1.0.tm (tclparser use), src/vfs/ (kit assembly), scanning module (new or existing punk module - to be determined) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Punk executables are currently 'batteries included' - the kit assembly ships the |
||||
full module snapshot regardless of what a given executable's app actually uses. |
||||
For executables built around a specific app or entrypoint, a package-dependency |
||||
scan could determine the module closure genuinely required and let the build |
||||
ship only that, giving smaller artifacts. Batteries-included builds remain a |
||||
supported alternative (the middle and far points of the spectrum described in |
||||
G-018's context section). |
||||
|
||||
The candidate scanning basis is static parsing of Tcl source via the tclparser |
||||
`parse` API (tclpro-descended; https://wiki.tcl-lang.org/page/tclparser). Its |
||||
current availability shapes the work: |
||||
|
||||
- The real implementation is the **c-only tclparser library** |
||||
(`package require parser`); punk::lib uses it opportunistically. |
||||
- punk::lib carries a same-API pure-Tcl fallback, `punk::lib::tclparser_tcl` |
||||
(src/modules/punk/lib-999999.0a1.0.tm ~3322), which is an unimplemented stub |
||||
that errors advising installation of the C library. |
||||
- punk::args::moduledoc::parser documents the parse API. |
||||
|
||||
Using the C library inside build tooling makes the build depend on a native |
||||
binary - intersecting the G-004/G-005/G-006 binary-provenance story (zig-built |
||||
or consent-downloaded, never committed). Completing the pure-Tcl fallback would |
||||
remove that coupling at some accuracy/performance cost. The choice (or ordering |
||||
- e.g. C library first, fallback later) is an implementation decision to record |
||||
here. |
||||
|
||||
## Approach |
||||
|
||||
- Static scan from the executable's entrypoint/app: walk `package require` |
||||
(and `tcl::tm` / source-time load constructs) through the reachable sources, |
||||
resolving the transitive closure against the project's module trees. |
||||
- **Dynamic requires are the known limit**: computed package names, plugin-style |
||||
loading, and `package require` inside rarely-hit code paths cannot be proven |
||||
statically. A declaration mechanism for extras (per-executable manifest of |
||||
packages to include beyond the scan result) is part of the goal's acceptance, |
||||
not an afterthought. |
||||
- Scan output should be a reviewable artifact (the closure list), feeding kit |
||||
assembly rather than silently filtering it - so a wrong exclusion is |
||||
diagnosable from the build record. |
||||
- Verification of a trimmed kit is behavioural (starts, passes its app's smoke |
||||
check) plus structural (module listing strictly a subset of the |
||||
batteries-included equivalent). |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Runtime tracing (run the app, record `package require` calls) instead of |
||||
static parsing - rejected as primary: coverage-dependent (only exercised |
||||
paths are seen), though it could later complement the static scan for |
||||
validating the declared-extras list. |
||||
- Naive regex scan for `package require` lines - rejected: misses computed |
||||
names it can't even flag, false-positives in strings/comments; the tclparser |
||||
parse API exists precisely to do this properly. |
||||
|
||||
## Notes |
||||
|
||||
- Interacts with G-005/G-006 (how the c-only tclparser binary is provisioned if |
||||
chosen) and G-018 (the spectrum of executable flavours). |
||||
- punk::lib sites already noting tclparser use/fallback: tclword_to_scriptlist |
||||
and related parsing helpers (~lib 3322-3580). |
||||
@ -0,0 +1,86 @@
|
||||
# G-020 Screen capture and input injection module with per-platform backends (Windows first) |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/ (new module - name TBD), src/vfs/punk9win.vfs/lib_tcl9/ (existing treectrl/Img/twapi payloads); scriptlib/aloupe.tcl stays untouched as a standalone app |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Punk already runs Tk from tclsh-style kits with the console retained (Tk is a |
||||
loadable package in the kit; the repl event loop services it - no wish |
||||
executable involved, and wish binaries are not a build target). The standalone |
||||
`scriptlib/aloupe.tcl` (aloupe 1.8.2, MIT) demonstrates the capture primitive: |
||||
treectrl's `loupe` command copies an arbitrary screen region into a Tk photo |
||||
(aloupe.tcl ~454), with Img for extra file formats. The Windows kit already |
||||
ships every needed binary: `src/vfs/punk9win.vfs/lib_tcl9/` has tk9.0, |
||||
treectrl 2.5.2 (tcl9treectrl252.dll), Img 2.0.1/2.1.0, twapi 5.0.2, and |
||||
cffi 2.0.3. |
||||
|
||||
This goal turns that demonstration into a scriptable module: capture plus |
||||
mouse/keyboard injection plus window location, so scripts (and, via G-021, |
||||
agents) can drive and observe GUI state. aloupe.tcl remains a standalone app; |
||||
the module is a separate development, not a refactor of it. |
||||
|
||||
## Platform assessment (investigated 2026-07-06) |
||||
|
||||
- **Windows (initial primary target)** - no show-stoppers, all deps already in |
||||
the kit. Capture: treectrl loupe (GDI). Input: twapi `send_input` |
||||
(SendInput). Window locate: twapi enumeration/geometry/foreground. Gotchas to |
||||
handle/document: desktop session must be unlocked and interactive (locked |
||||
workstation / disconnected RDP -> black captures, failed injection); |
||||
per-monitor DPI scaling affects coordinate mapping; UIPI blocks injecting |
||||
into elevated windows from a non-elevated process; multi-monitor negative |
||||
coordinates. |
||||
- **Linux (X11)** - supported: treectrl loupe X11 impl (XGetImage); injection |
||||
via XTest (xdotool external tool, or cffi binding to libXtst); locate via |
||||
xdotool/wmctrl. Xvfb gives a fully headless mode - the preferred arrangement |
||||
for automated visual verification. |
||||
- **Wayland-native - OUT OF SCOPE (decided 2026-07-06)**: XGetImage cannot see |
||||
native Wayland surfaces and XTest cannot inject into them by design; the |
||||
escape hatches (xdg-desktop-portal Screenshot/ScreenCast D-Bus APIs, ydotool |
||||
via uinput) are permission-heavy with no ready Tcl bindings. Revisit via |
||||
portals if/when needed; X11/Xvfb is the supported Linux mode. |
||||
- **FreeBSD** - same X11 story; tktreectrl/tkimg/xdotool in ports; twapi N/A. |
||||
- **macOS** - supportable, permission-gated: TCC requires Screen Recording |
||||
(capture) and Accessibility (injection) grants to the hosting process - |
||||
interactive one-time grants, awkward for unattended CI. treectrl loupe aqua |
||||
support is unverified and non-blocking: the built-in `screencapture` CLI |
||||
(region/window-id to file) is the natural capture backend; injection options |
||||
are osascript/System Events, cliclick (third-party), or cffi -> CGEventPost. |
||||
`src/vfs/macosx-arm/` already exists in the tree. |
||||
|
||||
## Approach |
||||
|
||||
- Three backend roles behind one API: **capture**, **input**, **window-locate**. |
||||
Per-platform backend selection with capability introspection - callers ask |
||||
what is supported rather than probing by failure. Echoes the G-001 pluggable |
||||
pattern. |
||||
- Windows backend first and complete (acceptance gates on it). X11 and macOS |
||||
backends are in-scope follow-ons behind the same contract; their absence on a |
||||
platform must produce a clean capability-based refusal. |
||||
- Output normalization: capture lands in a Tk photo; PNG via Tk's core photo |
||||
support (Img optional for other formats). File save and in-memory (base64) |
||||
both supported - the G-021 consumer needs both. |
||||
- cffi (bundled) is the escape hatch for gaps without new C extensions - e.g. |
||||
Windows PrintWindow for capturing occluded windows (region-based loupe cannot), |
||||
libXtst on X11 if avoiding the xdotool external-tool dependency, CGEventPost |
||||
on macOS. |
||||
- Binary deps (treectrl, Img, twapi, cffi) follow the G-004/G-005/G-006 |
||||
provisioning story on each platform; nothing new is committed to the repo. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Refactoring aloupe.tcl into the module - rejected: it is a vendored-style |
||||
standalone app under user-only scriptlib/; keeping it untouched preserves a |
||||
known-working reference implementation. |
||||
- Pure-twapi capture on Windows (skip treectrl) - not pursued initially: twapi |
||||
has no ready BitBlt/screen-grab wrapper; treectrl loupe is already shipped |
||||
and proven. cffi/PrintWindow may supplement later for occluded windows. |
||||
- Wayland-native support via portals/ydotool - out of scope (see platform |
||||
assessment); recorded for possible future revisit. |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-018 (kit composition; Tk as loadable package, no wish binaries), |
||||
G-019 (a trimmed capture-capable executable is a plausible scan target), |
||||
G-021 (agent-facing surface over this module), G-001 (backend plugin pattern). |
||||
@ -0,0 +1,55 @@
|
||||
# G-021 Agent-drivable visual/UI verification via piped snapshot and interaction calls |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/ (G-020 module's agent-facing surface), AGENTS.md guidance (post G-015 pattern), src/tests/ (visual-verification test hooks) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Some behaviours can only be verified visually (rendering, theming, layout, |
||||
ANSI-vs-Tk display parity) or by driving real input (mouse/keyboard-dependent |
||||
flows). Today an agent in a session has no sanctioned way to see or drive GUI |
||||
state. With G-020 providing capture/input/window-location as a module and |
||||
G-015 providing reliable piped one-shot script calls, the combination lets a |
||||
tool-calling agent: determine the applicable UI window, snapshot it, analyze |
||||
the image (agents are multimodal), and drive interactions - all from ordinary |
||||
tool calls during a session. |
||||
|
||||
## Approach |
||||
|
||||
- Thin agent-facing surface over the G-020 module, exercised through the G-015 |
||||
`script` subcommand: each operation is a single piped call with |
||||
machine-parseable output and G-015 exit-code semantics (nonzero + stderr on |
||||
failure, never an interactive shell). |
||||
- Operations: window list/match by pattern; snapshot a located window or region |
||||
to a caller-specified PNG path; optional base64-on-stdout emission for direct |
||||
image ingestion by the calling agent; scripted interaction sequences (focus, |
||||
click at offset, key/type, snapshot) composable in one piped script. |
||||
- Windows first, per G-020. Capability introspection flows through, so an agent |
||||
on an unsupported platform gets a clean "not supported here" rather than a |
||||
hang or crash. |
||||
- Documentation for agents lands alongside the G-017 guidance (the same "use |
||||
the punk mechanism, not ad-hoc scanning/screenshotting" pattern): exact |
||||
invocations, output shapes, and the session prerequisites (unlocked desktop, |
||||
DPI notes) from G-020's platform assessment. |
||||
- At least one real verification consumer in src/tests/ or a runbook (e.g. a |
||||
Tk app smoke test asserting on a snapshot) proves the loop end-to-end. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- OS-level screenshot tooling driven directly by agents (PowerShell/GDI |
||||
one-liners, screencapture, import/scrot) - rejected as the sanctioned path: |
||||
per-platform incantations with no window-location or input story, no |
||||
capability model, and nothing reusable by punk scripts; the punk module route |
||||
gives one contract everywhere punk runs. |
||||
- Building agent tooling (MCP server or similar) directly into punk now - |
||||
deferred: piped G-015 calls already fit existing agent Bash/exec tools; a |
||||
richer integration can layer later without changing the module. |
||||
|
||||
## Notes |
||||
|
||||
- Depends on G-020 (mechanism) and G-015 (reliable piped calls); guidance |
||||
placement follows G-017. |
||||
- Image handoff sizing: agents ingest images with dimension/size limits; |
||||
snapshot operations should support downscale/crop options eventually - |
||||
acceptance keeps to save + base64, with sizing noted as a natural follow-on. |
||||
@ -0,0 +1,75 @@
|
||||
# G-022 Scriptable safe fossil move/rename in `dev repo`; rename this project's fossil repo to punkshell |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/mix/commandset/repo-999999.0a1.0.tm, src/modules/punk/repo-999999.0a1.0.tm, src/tests/modules/punk/mix/testsuites/repo/ |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
This project's fossil repository is recorded as project-name 'shellspy' |
||||
(C:/Users/sleek/.fossils/shellspy.fossil at the time of writing) but the |
||||
desired project name is punkshell ('shellspy' names only the experimental |
||||
launch subcommand). Rather than having an agent perform ad-hoc repo surgery, |
||||
the rename should go through the `dev repo` system - which makes the |
||||
prerequisite the reliability of agent-driven `script` calls (G-015). |
||||
|
||||
Current capability (examined 2026-07-06, characterized by |
||||
src/tests/modules/punk/mix/testsuites/repo/fossilmove.test): |
||||
|
||||
- `punk::mix::commandset::repo::fossil-move-repository` exists but is fully |
||||
interactive (punk::repo::askuser prompts throughout) - unusable from a |
||||
piped script call. |
||||
- Its move primitive is `file copy` + `fossil test-move-repository` per |
||||
checkout. Verified in a FOSSIL_HOME-isolated sandbox, this sequence: |
||||
- correctly repoints the checkout's repository link (the part that works); |
||||
- leaves the old .fossil file on disk (copy, never delete); |
||||
- leaves a phantom `repo:` entry for the old path in the central fossil |
||||
config-db (`fossil all ls` lists both old and new paths); |
||||
- leaves a stale `ckout:` back-reference in the old repo db. |
||||
- Nothing renames the fossil project-name: moving/renaming the db file leaves |
||||
`project-name` in the repo db config table untouched, and fossil has no |
||||
plain CLI setter for it (web UI Admin/Configuration, or a config-table |
||||
update e.g. via `fossil sql`). |
||||
|
||||
Phantom central-db entries matter beyond hygiene: `dev projects.work` |
||||
(G-016/G-017's agent-facing project discovery) reads the central config-db, so |
||||
phantoms surface directly in agent search results. |
||||
|
||||
## Approach |
||||
|
||||
- Extend the commandset with a non-interactive mode (explicit flags for |
||||
target path/name, project-name change, old-db disposition) alongside the |
||||
existing interactive flow; punk::args-documented per module conventions. |
||||
- Safe-move sequence: copy to target, repoint every registered checkout |
||||
(`fossil test-move-repository` per checkout, or direct checkout-db vvar |
||||
update), remove the old central-db `repo:` entry, clear/transfer stale |
||||
`ckout:` records, then remove or archive the old db file - ordered so a |
||||
failure mid-way never leaves checkouts pointing at a missing db. |
||||
- Project-name rename: update `project-name` in the repo db config table |
||||
(project-code must never change - it is the repo's identity); surface both |
||||
names in the operation's report. |
||||
- The fossilmove.test GAP tests (3.2, 3.3, 3.4, 4.1) pin today's behaviour; |
||||
flipping them to assert clean behaviour is part of this goal's verification. |
||||
- The final application to this repo runs through a G-015 piped `script` |
||||
call, as the proving use of agent-driven `dev repo` operations. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Ad-hoc agent-performed rename (fossil sql + file rename + manual central-db |
||||
cleanup by hand) - rejected: the point is that repo surgery goes through |
||||
audited `dev repo` tooling agents can reuse, not one-off incantations. |
||||
- Renaming only the .fossil file and leaving project-name as shellspy - |
||||
rejected: the project-name is the visible identity in `fossil info`, |
||||
timelines and project listings; it is the thing actually wrong. |
||||
|
||||
## Notes |
||||
|
||||
- Dependent on G-015 (reliable piped script calls) for the application step; |
||||
the capability work itself has no dependency and can proceed earlier. |
||||
- The working-tree directory name (C:/repo/jn/shellspy) is out of scope - |
||||
fossil does not require checkout dirs to match the project name. |
||||
- Other checkouts of shellspy.fossil (e.g. a shellspy_copy working tree, if |
||||
open) must be enumerated and repointed by the same run. |
||||
- Test isolation pattern for anything touching fossil state: set FOSSIL_HOME |
||||
to a temp dir and verify `fossil info` reports config-db under it before |
||||
mutating (see fossilmove.test fixture guard). |
||||
@ -0,0 +1,67 @@
|
||||
# G-023 Version-named punk binaries per Tcl generation (versioned / dev / release-gated plain names) |
||||
|
||||
Status: proposed |
||||
Scope: src/make.tcl, src/runtime/ (mapping config - see G-024), bin/ (build outputs) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
The project version is now tracked and change-driven-bumped in punkproject.toml |
||||
(root AGENTS.md "Project Versioning"), so builds can be named after it. Today's |
||||
build outputs are fixed names declared line-by-line in src/runtime/mapvfs.config |
||||
(punksys, punk902z, ...) that encode the runtime generation informally and say |
||||
nothing about the project version they contain - which build a given exe |
||||
represents is unknowable without launching it (G-025 addresses the launching |
||||
side; this goal addresses the naming side). |
||||
|
||||
Naming scheme: |
||||
|
||||
- `punk8-<major>-<minor>-<patch>.exe` / `punk9-<major>-<minor>-<patch>.exe` - |
||||
produced as the version advances; a rebuild at an unchanged version refreshes |
||||
that version's binaries in place. |
||||
- `punk8-dev.exe` / `punk9-dev.exe` - always the latest build, refreshed every |
||||
build. |
||||
- `punk8.exe` / `punk9.exe` - created initially, then replaced only by an |
||||
explicit release step when an actual release is tagged. A normal build never |
||||
touches them. |
||||
|
||||
The `8`/`9` generation split matches the existing dual-generation verification |
||||
practice (bin/AGENTS.md: a Tcl 8.6 punk shell and a current Tcl 9 punk shell). |
||||
|
||||
## Approach |
||||
|
||||
- Names derive from punkproject.toml at build time - no hand-maintained |
||||
per-version config entries. The scheme is declared once, succinctly, in the |
||||
G-024 toml mapping; the expansion to concrete output names happens in |
||||
make.tcl. |
||||
- The release step that updates the plain punk8.exe/punk9.exe names should be |
||||
an explicit make.tcl action tied to release tagging, not a side effect of a |
||||
normal build. Its precise trigger (fossil/git tag presence, or a manual |
||||
subcommand) is an implementation decision to record here. |
||||
- Existing specifically-named outputs (punksys, punk902z, ...) keep working via |
||||
their explicit mappings (G-024 preserves them); the versioned scheme is |
||||
additive. |
||||
|
||||
## Space and archival |
||||
|
||||
These binaries are large. Accumulating one pair per project version is |
||||
accepted for now. Out of scope but recorded for later: a trigger mechanism for |
||||
archival/deletion (e.g. keep-latest-N per generation, prune non-release |
||||
versions older than a threshold, or move to an artifact store per G-006's |
||||
binary-artifacts repository). Revisit when the accumulation actually bites. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Encoding the version only inside the kit (G-025) and keeping fixed exe names |
||||
- rejected as insufficient alone: side-by-side versioned binaries are the |
||||
point (running/comparing multiple versions without rebuilds). |
||||
- Symlinks/hardlinks for -dev and plain names instead of copies - deferred to |
||||
implementation: Windows link semantics for running executables are fiddly; |
||||
copies are acceptable at current sizes. |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-024 (scheme declared in toml mapping), G-025 (exe self-reports its |
||||
version/provenance - the complement of name-encoded versioning once files |
||||
are renamed/copied), G-006 (future artifact-store home for archived |
||||
versions), G-018/G-019 (other points on the executable-flavour spectrum). |
||||
@ -0,0 +1,65 @@
|
||||
# G-024 mapvfs.config converted to toml (tomlish-parsed) with succinct scheme declarations |
||||
|
||||
Status: proposed |
||||
Scope: src/runtime/mapvfs.config (replaced/deprecated), src/make.tcl (parsing), src/bootsupport/modules/tomlish-*.tm (parser dependency) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
src/runtime/mapvfs.config maps runtime binaries and .vfs folders to named |
||||
executables in a custom line format: one line per runtime, being the runtime |
||||
filename followed by kit-configuration lists of 1-3 elements |
||||
({vfsfolder ?outputname? ?kittype?}, kittype in kit|zip|zipcat, `-` as runtime |
||||
for runtime-less .kit builds, no-entry runtimes matching same-named .vfs |
||||
folders implicitly). make.tcl parses it inline (~2795-2845). The format works |
||||
but is idiosyncratic (comment/blank handling, positional defaults), is parsed |
||||
ad hoc, and has no way to express a *scheme* - every output is one literal |
||||
mapping, which conflicts with G-023 where output names derive from the project |
||||
version at build time. |
||||
|
||||
tomlish is the project's toml parser (vendored at src/vendormodules and |
||||
src/bootsupport/modules - the bootsupport copy matters since make.tcl is build |
||||
tooling), so the replacement format is toml parsed via tomlish, consistent |
||||
with the G-014 direction of toml + tomlish for declared configuration. |
||||
|
||||
## Approach |
||||
|
||||
- A mapvfs toml file in src/runtime/ carrying two kinds of declarations: |
||||
- **Explicit mappings** - the current capability, one entry per output: |
||||
runtime, vfs folder, output name (defaulting from the vfs name), kit type. |
||||
Everything active in mapvfs.config today must be expressible. |
||||
- **Scheme declarations** - generative entries expanded by make.tcl, first |
||||
consumer being the G-023 versioned scheme: one entry per Tcl generation |
||||
declaring runtime + vfs + scheme=versioned (or similar) that expands to |
||||
punkN-<M>-<m>-<p>.exe, punkN-dev.exe and the release-gated punkN.exe |
||||
without enumerating versions. |
||||
- Validation with entry-level error reporting: a malformed or unresolvable |
||||
entry (missing runtime file, missing vfs folder, unknown kit type/scheme) |
||||
fails the build naming the offending entry - matching the current warnings |
||||
but structured. |
||||
- Legacy handling is a decision to record here: either full migration (convert |
||||
the file, delete the old parser) or a deprecation window (toml wins when |
||||
present, .config honoured with a deprecation warning). Full migration is |
||||
preferred - the file is small and repo-internal - unless project_layouts |
||||
templates complicate it (layout copies of make.tcl/mapvfs.config sync via |
||||
G-012 territory; note in completion report, do not hand-sync layouts). |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Keeping the custom line format and extending it with scheme syntax - |
||||
rejected: doubles down on an ad-hoc parser exactly where the project is |
||||
standardising on toml + tomlish. |
||||
- Tcl-script config (source-able dict) - rejected for the same reason as in |
||||
G-014: toml is the declared configuration format, editable without Tcl |
||||
knowledge. |
||||
|
||||
## Notes |
||||
|
||||
- The commented-out historical entries in mapvfs.config (broken-runtime notes, |
||||
test cases) are documentation of past pitfalls - carry the useful ones into |
||||
toml comments or this file rather than losing them. |
||||
- Related: G-023 (first scheme consumer), G-014 (toml+tomlish direction and |
||||
the tomlish punk::args-documentation precondition - that precondition gates |
||||
punk::config consumption; build-tooling use here can proceed with tomlish |
||||
as-is), G-012 (layout template refresh, if layout copies of runtime config |
||||
are affected). |
||||
@ -0,0 +1,82 @@
|
||||
# G-025 Punk executables self-report project version and build provenance |
||||
|
||||
Status: proposed |
||||
Scope: src/vfs/_config/punk_main.tcl (subcommand dispatch), src/make.tcl (stamping build info into the vfs), src/vfs/ (stamp payload location), src/modules/punk/ (in-shell command - the single implementation) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
With G-023 producing version-named binaries, an executable's name encodes its |
||||
version - but names are mutable (copies, renames, the -dev and release-gated |
||||
plain names) and say nothing about the runtime binary or vfs folder the build |
||||
consumed. The executable should answer for itself from embedded data: a |
||||
version-report subcommand at the launch layer, and the same-named command in |
||||
the punk module for scripts already running inside a punk shell. |
||||
|
||||
The in-shell command is not merely an optimization over exec'ing out. In a |
||||
tclsh-hosted punk shell (`tclsh src/make.tcl shell` loads punk::repl directly |
||||
into the host tclsh - make.tcl ~1913) there is no punk executable to exec: |
||||
`info nameofexecutable` is the external tclsh, and invoking it with the |
||||
subcommand would be meaningless. The in-process command is the only correct |
||||
path there - hence the module command is the single implementation and the |
||||
exe subcommand is a thin dispatch wrapper over it (which is also what |
||||
guarantees the two surfaces report identically). |
||||
|
||||
## Report semantics: three distinct notions |
||||
|
||||
Only in a plainly-launched stamped kit do these coincide; the report must keep |
||||
them distinct and never fabricate: |
||||
|
||||
1. **Build stamp** - project version, runtime binary name, vfs folder name, |
||||
fixed at kit assembly by make.tcl. Absent in unstamped contexts. |
||||
2. **Live runtime facts** - actual `info nameofexecutable`, patchlevel. |
||||
Always available. |
||||
3. **Source-tree project version** - punkproject.toml, available when running |
||||
from a source tree regardless of stamping. |
||||
|
||||
Edge cases driving the acceptance clauses: |
||||
|
||||
- `tclsh src/make.tcl shell`: no stamp. Stamp fields reported explicitly |
||||
absent; live facts show the real host tclsh; the source-tree version may be |
||||
reported but flagged as live-from-punkproject.toml, not as a stamp. |
||||
- Kit launched in `src` mode (`punk902z src`): a stamp exists, but the running |
||||
code is working-tree dev modules whose punkproject.toml may be ahead of the |
||||
stamped version. Reporting only the stamp misstates what is running; only |
||||
the live version misstates provenance - report both, distinctly labelled, |
||||
when they differ. |
||||
- Plain tclsh with installed punk modules and no source tree: no stamp, no |
||||
punkproject.toml - live facts only. |
||||
- Renamed/copied executables: the report comes from the stamp, so it stays |
||||
correct whatever the file is called. |
||||
- Pre-stamping kits: the subcommand/command fails gracefully with a clear |
||||
message. |
||||
|
||||
## Approach notes |
||||
|
||||
- The command must work from the code interp (where scripts run), not just the |
||||
repl thread - trivial for static stamp data, but stated so the G-007 |
||||
location-transparency expectation is met. |
||||
- The subcommand output must be machine-parseable, exit 0, and emit nothing |
||||
else on stdout/stderr - usable through G-015 piped calls and by exec-style |
||||
callers. |
||||
- Naming (e.g. `buildinfo`) is an implementation decision: the in-shell |
||||
command name must match the subcommand name and not collide with existing |
||||
repl commands. |
||||
- Stamp location within the vfs payload is an implementation decision to |
||||
record here (candidates: a dedicated stamp file under the vfs _config, or |
||||
fields merged into an existing boot config file). |
||||
|
||||
## Alternatives considered |
||||
|
||||
- In-shell command exec'ing its own executable with the subcommand - rejected: |
||||
wrong in tclsh-hosted shells (no punk executable to call), needless process |
||||
spawn elsewhere, and two code paths that can drift. |
||||
- Reporting only the stamped version and ignoring live/source-tree context - |
||||
rejected: actively misleading in `src`-mode sessions, which are the primary |
||||
working-tree verification workflow (bin/AGENTS.md). |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-023 (version-named binaries this complements), G-024 (build |
||||
mapping that will drive stamping inputs), G-015 (piped invocation of the |
||||
subcommand), G-014 (punkproject.toml reading may share tomlish plumbing). |
||||
@ -0,0 +1,76 @@
|
||||
# G-026 Enforceable clean-checkout provenance policy for vendor and bootsupport pulls |
||||
|
||||
Status: proposed |
||||
Scope: src/make.tcl (vendorupdate and bootsupport steps), src/vendormodules/include_modules.config, src/bootsupport/modules*/include_modules.config |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
`make.tcl vendorupdate` pulls the latest release-stamped module files from each |
||||
source project's built `modules/` folder (paths declared in |
||||
include_modules.config). Nothing verified the source project's checkout state, |
||||
so artifacts built from uncommitted source could be vendored with no committed |
||||
provenance anywhere. This actually happened: tomlish 1.1.7/1.1.8 were vendored |
||||
into punkshell from a tomlish working tree whose fossil history ended at 1.1.6 |
||||
- the released content existed only as uncommitted edits (recorded and |
||||
remedied 2026-07-06, tomlish checkin 37b71e82). |
||||
|
||||
A warn-only check was added in project 0.2.5 (fix-now/goal-later review, |
||||
2026-07-06): `vendor_source_dirty_warnings` in make.tcl walks up from each |
||||
vendor source path to the nearest fossil and/or git root, runs |
||||
`fossil changes` / `git status --porcelain`, and warns once per VCS root when |
||||
dirty. The first real run flagged 8 dirty source projects, confirming the |
||||
problem is endemic, and equally that **abort-by-default would have made |
||||
vendorupdate unusable on this machine today** - which is why enforcement is a |
||||
goal (requiring cleanup and a policy/override design) rather than part of the |
||||
quick fix. |
||||
|
||||
## Approach |
||||
|
||||
- Promote the warning to a policy: refuse to pull from dirty source projects |
||||
unless an explicit override is given (flag such as |
||||
-allow-dirty-sources, or per-project allowance); warn-only remains |
||||
selectable as a configured mode. Where the mode is configured (command flag |
||||
vs punk::config once G-014 lands) is an implementation decision to record. |
||||
- Share one check implementation between vendor_localupdate and |
||||
bootsupport_localupdate (make.tcl already carries a |
||||
"todo - sync alg with bootsupport_localupdate!" note; do not grow a second |
||||
divergent copy). |
||||
- Per-root reporting and unversioned-location silence are established |
||||
behaviour from the 0.2.5 check - keep them. |
||||
|
||||
## The residual staleness gap |
||||
|
||||
A clean checkout is necessary but not sufficient: the built `modules/` files |
||||
are produced from the working tree at build time, so they can predate the |
||||
latest commits (stale) or - the reverse of the tomlish case - have been built |
||||
from dirt that was subsequently committed. Candidate mechanisms, decision to |
||||
be recorded here: |
||||
|
||||
- Rebuild-on-demand: vendorupdate triggers the source project's module build |
||||
before pulling (correct, but couples projects' build systems). |
||||
- Provenance stamp: source projects stamp checkout id (fossil checkin / |
||||
git hash) into built modules at build time; vendorupdate compares stamp to |
||||
the source project's current checkout id (ties into the G-025 stamping |
||||
machinery pattern). |
||||
- Accepted risk: document that clean-checkout checking is the enforced bar and |
||||
staleness remains a manual concern. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Abort-by-default immediately (no goal, just flip the fix) - rejected for |
||||
now: the first run showed 8 dirty source projects; enforcement without a |
||||
cleanup pass and an override design would block routine work. |
||||
- Checking only the specific pulled files' dirtiness rather than the whole |
||||
source checkout - rejected: the built module is generated from many source |
||||
files; per-file dirt attribution against a build product is not tractable. |
||||
|
||||
## Notes |
||||
|
||||
- include_modules.config files carry their own "#todo - change to |
||||
include_modules.toml" - a natural companion change under the G-024 |
||||
toml-conversion pattern, but deliberately not part of this goal's |
||||
acceptance. |
||||
- Related: G-014 (tomlish provenance incident motivated this), G-024 (toml |
||||
config direction), G-025 (build stamping machinery a provenance stamp could |
||||
ride on). |
||||
@ -0,0 +1,82 @@
|
||||
# G-027 Pull-based infrastructure updates for punkshell-derived projects |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punk/mix/commandset/project-999999.0a1.0.tm (project.new push path), src/modules/punkcheck-999999.0a1.0.tm (install provenance records), src/make.tcl (derived-project pull entrypoint) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Projects initialised via `dev project.new` (punk::mix::commandset::project::new) |
||||
receive a snapshot of punkshell's layout system: make.tcl/build.tcl, bootsupport |
||||
modules and libs, template payloads. That snapshot then drifts. The current |
||||
remedy is a push from punkshell: re-running `dev project.new -force 1 -update 1` |
||||
against the derived project. A pull - one command run inside the derived |
||||
project - is the more natural shape: the derived project knows when it wants |
||||
updating, and its owner is the right party to review what changes. |
||||
|
||||
Motivating incident (2026-07-06, tomlish project): a make.tcl vintage 2025-04 |
||||
carried a bootsupport-update step that itself depended on a stale bundled |
||||
fileutil::traverse (Tcl 8.x-only constraint, broken under 9.0.3), so the tool |
||||
that should fix the drift was disabled by the drift; bootsupport/lib still had |
||||
the obsolete per-platform layout; include_modules.config files were empty |
||||
template stubs. Recovery required manual cross-project copying - exactly the |
||||
push-shaped work this goal eliminates. A drifted derived project must be |
||||
recoverable by a pull whose own machinery does not depend on the freshness of |
||||
the drifted parts (the pull entrypoint should be small/self-contained enough to |
||||
work from a stale project, or delivered as its first step). |
||||
|
||||
## Current mechanism and its limits |
||||
|
||||
- .punkcheck records in the derived project record where targets were installed |
||||
from (installer/source paths) - "roughly the right shape": provenance-driven, |
||||
change-detected, skippable when unchanged. |
||||
- Limits: |
||||
- Source references are local relative paths - resolution breaks or silently |
||||
mislocates if the derived project (or punkshell checkout) moves. |
||||
- No VCS awareness: nothing checks whether the punkshell source is dirty |
||||
(G-026 territory) or whether the pull would clobber uncommitted local |
||||
modifications in the derived project. |
||||
- Push-only initiation, from the punkshell side. |
||||
|
||||
## Approach |
||||
|
||||
- A pull entrypoint in the derived project (make.tcl subcommand or a small |
||||
standalone bootstrap that does not depend on the possibly-stale local |
||||
infrastructure). |
||||
- Provenance robust to moves: record the origin punkshell project by something |
||||
more durable than a relative path - candidates: fossil project-code / git |
||||
remote or commit identity, plus a locally-resolvable path hint refreshed on |
||||
each pull; `dev projects.work` discovery (G-016/G-017) can re-locate a moved |
||||
punkshell checkout by name/id when the stored path fails. |
||||
- VCS integration on both ends: |
||||
- Source side: G-026 clean-checkout policy shared, not reimplemented. |
||||
- Target side: before overwriting, classify each target file - unmodified |
||||
since last punkcheck install (safe), locally modified but committed |
||||
(report/confirm), uncommitted local changes (refuse or explicit override). |
||||
- Remote pulls: decide whether updates may come directly from a remote |
||||
punkshell repository (fossil clone/sync or git fetch of a punkshell repo, |
||||
or the G-006 artifact-download channel) rather than a local checkout. |
||||
Considerations: provenance/verification of remote content, version pinning |
||||
(pull a tagged punkshell release vs trunk), and interaction with the |
||||
consent-gating principle of G-006. May be recorded as deferred with |
||||
rationale. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Keeping push-only and just fixing its path robustness - rejected as the end |
||||
state: the updater lives on the wrong side of the relationship; the derived |
||||
project's owner should initiate and review. Push retained during transition. |
||||
- Making derived projects git/fossil-track punkshell as an upstream remote and |
||||
merge - rejected: derived projects are independent repos with their own |
||||
history; infrastructure files are a curated payload, not a branch to merge. |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-012 (inert template payloads and the make.tcl template-refresh |
||||
step feed what a pull delivers), G-016/G-017 (project discovery for |
||||
re-locating a moved origin), G-026 (clean-checkout policy on the source), |
||||
G-005/G-006 (remote artifact channels if remote pull proceeds). |
||||
- The 2025-04 tomlish checkin "update bootsupport and make.tcl from punkshell" |
||||
and the 2026-07-06 manual recovery are the concrete push-model precedents to |
||||
test the pull against: a scratch project seeded at an old layout vintage |
||||
should be fully recoverable by one pull. |
||||
@ -0,0 +1,67 @@
|
||||
# G-028 Name the process locking a file when builds cannot replace a target |
||||
|
||||
Status: proposed |
||||
Scope: src/modules/punkboot/utils-999999.0a1.0.tm (locker-report helper), src/make.tcl (kit deploy failure reporting) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Motivating incident (2026-07-07, tomlish project): `make.tcl project` failed |
||||
with "could not delete target binary at .../bin/tomlish.exe" and no further |
||||
information. The lock turned out to be 7-Zip holding the exe open for archive |
||||
inspection - but identifying that took several dead ends, because Windows has |
||||
no cheap reverse lookup from file to holding process: |
||||
|
||||
- tasklist / Get-Process by name only finds processes *running as* the file. |
||||
- Scanning Process.Modules only sees files mapped as images (loaded EXE/DLLs), |
||||
not plain open handles, and silently skips inaccessible processes. |
||||
- Full handle enumeration (NtQuerySystemInformation) needs elevation and |
||||
per-handle queries; `openfiles` requires a globally pre-enabled tracking |
||||
flag; Sysinternals handle.exe / Resource Monitor are external/manual. |
||||
|
||||
The **Restart Manager API** (RmStartSession/RmRegisterResources/RmGetList) is |
||||
the practical programmatic answer: it is what installers use for the "these |
||||
applications are using this file" dialog, does not require elevation for the |
||||
common cases, and directly answers "who is blocking my replace/delete". It is |
||||
callable from Tcl via twapi or cffi, both already shipped in the punk Windows |
||||
kit. |
||||
|
||||
Deploy-target locks will recur - G-023's versioned/dev binary outputs are |
||||
rewritten every build, and a running or inspected punk8/9-dev.exe is exactly |
||||
the kind of file this fails on. |
||||
|
||||
## Approach |
||||
|
||||
- `punkboot::utils` helper (e.g. file_lockers <path>) returning a list of |
||||
{pid <n> name <procname> ...} records, empty when unlocked. |
||||
- **Constraint: bootsupport carries no compiled extensions** (src/bootsupport |
||||
AGENTS.md). punkboot::utils stays pure Tcl; twapi (RmStartSession et al via |
||||
its API coverage) or cffi (direct Rstrtmgr.dll binding) is package-required |
||||
lazily inside the helper, from the environment's auto_path. Unavailable |
||||
binding, non-Windows platform, or API failure all yield a clean |
||||
'unavailable' result the caller renders as the existing message. |
||||
- make.tcl kit deploy (and any other replace-target failure sites worth |
||||
covering) appends the locker report to its failure output when available. |
||||
- Prefer twapi if its Restart Manager coverage suffices; cffi binding as the |
||||
fallback implementation choice - record the choice here when made. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Full handle-table enumeration (NtQuerySystemInformation) - rejected: |
||||
elevation-dependent, expensive, and reimplements what Restart Manager |
||||
already provides for the blocking-file case. |
||||
- Shelling out to Sysinternals handle.exe - rejected: external download, |
||||
license prompt on first run, elevation required; not dependable build |
||||
tooling. |
||||
- `openfiles` - rejected: requires a pre-enabled global tracking flag with a |
||||
standing performance cost. |
||||
|
||||
## Notes |
||||
|
||||
- Related: G-023 (versioned binary deploys are the recurring customer), |
||||
G-020 (same optionally-available twapi/cffi pattern for platform features), |
||||
punkboot::utils chicken-and-egg ordering rules in src/bootsupport/AGENTS.md |
||||
apply when adding the helper. |
||||
- Unix analogues (lsof/fuser) could slot behind the same helper interface |
||||
later; out of scope for acceptance (Windows is where the pain is and where |
||||
the API answer is non-obvious). |
||||
@ -0,0 +1,77 @@
|
||||
# G-029 Build packaged test::<modulename> #modpod modules from src/tests |
||||
|
||||
Status: proposed |
||||
Scope: src/make.tcl (test-module packaging step), src/tests/ (source of truth), src/modules/test/ (generated #modpod targets), src/modules/punk/mix/ (modpod tooling as needed) |
||||
Acceptance: as in root GOALS.md index (canonical). |
||||
|
||||
## Context |
||||
|
||||
Module tests currently exist in two forms: |
||||
|
||||
1. **src/tests/modules/<namespacepath>/testsuites/** - the source-tree harness |
||||
form: self-contained tcltest files (own package requires, final |
||||
cleanupTests, self-locating data) run via src/tests/runtests.tcl. This is |
||||
where test development happens and is the intended structure going forward. |
||||
2. **src/modules/test/#modpod-<module>-<ver>/** - the packaged form: a |
||||
test::<modulename> module (wrapper .tm providing a SUITE/RUN interface and |
||||
locating its bundled *_testsuites) that ships in kits and is run in built |
||||
executables, e.g `tomlish.exe -app test`. |
||||
|
||||
The packaged form is not just kit plumbing - it is a **distributable for |
||||
module consumers**: a user who downloads a built module can optionally |
||||
download the matching test::<modulename> package and verify the module's |
||||
behaviour on their own system/platform, with no source tree or test harness - |
||||
just `package require test::<modulename>` + its RUN interface, or a wrapped |
||||
executable's `-app test`. That end-user scenario is what fixes the packaged |
||||
form's requirements: fully self-sufficient (suites + data bundled), locatable |
||||
via ordinary module paths, and runnable in a bare interp. |
||||
|
||||
Nothing generates one form from the other, so they drift as parallel |
||||
hand-maintained copies. The tomlish project (2026-07-07) is the live example: |
||||
its modpod testsuites were ported to src/tests (with fixes - |
||||
self-containment, single cleanupTests, self-located data), while the modpod |
||||
remains the source the built test::tomlish module actually comes from. Both |
||||
are now fossil-tracked and must be kept in sync by hand until this goal lands. |
||||
|
||||
## Approach |
||||
|
||||
- A make.tcl step (natural companion to the existing module build steps) |
||||
packages src/tests/modules/<namespacepath>/testsuites/** into the |
||||
test::<modulename> #modpod - punkcheck-tracked so unchanged sources skip. |
||||
- The wrapper .tm (SUITE/RUN interface, suitesdir resolution) becomes part of |
||||
the generation: either templated per module or a shared runtime the modpod |
||||
references. Its interface must keep serving the -app test dispatch used by |
||||
wrapped executables. |
||||
- Because src/tests files are self-contained, the packaged copies can be |
||||
byte-identical file copies; the wrapper supplies discovery (suite names from |
||||
directory structure) rather than environment variables the files depend on. |
||||
Legacy wrapper variables (e.g ::test::tomlish::suitesdir) should not be |
||||
reintroduced into test files - self-containment is the contract. |
||||
- Data fidelity: suite data (e.g roundtrip toml files with deliberate |
||||
crlf/mixed line endings) must be copied byte-for-byte; the #modpod directory |
||||
form is plain files, so no archive-level transformation should apply. |
||||
- Aggregate suites in the old modpods (fauxlink-based tests/ and dev/ |
||||
collections) are a generation-time decision: reproduce via fauxlink, replace |
||||
with wrapper-level suite selection, or drop - record the choice here. |
||||
- Naming: test::<modulename> derives from the src/tests path |
||||
(modules/tomlish -> test::tomlish, modules/punk/args -> test::punk::args). |
||||
Only modules with testsuites content get a package generated. |
||||
|
||||
## Alternatives considered |
||||
|
||||
- Making the packaged modpod the source of truth and generating src/tests |
||||
from it - rejected: src/tests is where development happens (runtests.tcl |
||||
ergonomics, agent workflows, per-suite layout) and its self-contained-file |
||||
contract is the healthier base; the wrapper form is derivable, the reverse |
||||
requires stripping wrapper dependencies (as the tomlish port demonstrated). |
||||
- Keeping dual hand-maintained copies - rejected: that is the current drift |
||||
the tomlish incident demonstrated; sync-by-discipline does not hold. |
||||
|
||||
## Notes |
||||
|
||||
- Related: the tomlish project's src/tests port (its fossil checkins 20bd00dc, |
||||
feedb6f0 record the dual-copy interim and name this goal); punkshell's own |
||||
src/modules/test/ packages are further targets once the mechanism exists. |
||||
- The packaging step's output is a build artifact by policy even though it |
||||
lives under src/ - the punkcheck records make regeneration cheap and |
||||
hand-edits detectable. |
||||
@ -1,8 +0,0 @@
|
||||
C initial\sempty\scheck-in |
||||
D 2025-09-27T18:28:21.559 |
||||
R d41d8cd98f00b204e9800998ecf8427e |
||||
T *branch * trunk |
||||
T *sym-trunk * |
||||
U jnoble |
||||
Z 3f45351c1dc810e1e3525545cb4124cd |
||||
# Remove this line to create a well-formed Fossil manifest. |
||||
@ -1 +0,0 @@
|
||||
8617f402ca78b8a1ae3566af83ccb97bec705e0b273c1ec5e76e8c6aa276a182 |
||||
@ -0,0 +1,214 @@
|
||||
# -*- tcl -*- |
||||
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
||||
# |
||||
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
||||
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
# (C) 2023 |
||||
# |
||||
# @@ Meta Begin |
||||
# Application punkboot::utils 0.1.1 |
||||
# Meta platform tcl |
||||
# Meta license BSD |
||||
# @@ Meta End |
||||
|
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
## Requirements |
||||
##e.g package require frobz |
||||
package require Tcl 8.6- |
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
namespace eval punkboot::utils { |
||||
variable version 0.1.0 |
||||
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::parse_punkproject_version |
||||
@cmd -name "::punkboot::utils::parse_punkproject_version"\ |
||||
-summary\ |
||||
"Parse the [project] version from TOML content string"\ |
||||
-help\ |
||||
"Scans TOML content for the [project] section and returns |
||||
the value of the version key as a string. Returns an empty |
||||
string if the section or key is not found." |
||||
@leaders |
||||
content -type string -optional 0 -help\ |
||||
"TOML content string to parse" |
||||
}] |
||||
} |
||||
proc parse_punkproject_version {content} { |
||||
set in_project 0 |
||||
foreach line [split $content \n] { |
||||
set trimmed [string trim $line] |
||||
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} { |
||||
set in_project [expr {$trimmed eq "\[project\]"}] |
||||
continue |
||||
} |
||||
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_punkproject_version |
||||
@cmd -name "::punkboot::utils::read_punkproject_version"\ |
||||
-summary\ |
||||
"Read the [project] version from a TOML file"\ |
||||
-help\ |
||||
"Reads the file at tomlfile and returns the [project] |
||||
version value as a string. Returns an empty string if the |
||||
file does not exist or the version is not found." |
||||
@leaders |
||||
tomlfile -type string -optional 0 -help\ |
||||
"Path to the TOML file to read" |
||||
}] |
||||
} |
||||
proc read_punkproject_version {tomlfile} { |
||||
if {![file exists $tomlfile]} {return ""} |
||||
set fh [open $tomlfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
return [::punkboot::utils::parse_punkproject_version $content] |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_changelog_latest_version |
||||
@cmd -name "::punkboot::utils::read_changelog_latest_version"\ |
||||
-summary\ |
||||
"Read the latest ## [X.Y.Z] version header from a changelog file"\ |
||||
-help\ |
||||
"Scans the changelog file from the top and returns the |
||||
version string from the first '## [X.Y.Z]' header line. |
||||
Returns an empty string if the file does not exist or no |
||||
matching header is found." |
||||
@leaders |
||||
changelogfile -type string -optional 0 -help\ |
||||
"Path to the changelog file to read" |
||||
}] |
||||
} |
||||
proc read_changelog_latest_version {changelogfile} { |
||||
if {![file exists $changelogfile]} {return ""} |
||||
set fh [open $changelogfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
foreach line [split $content \n] { |
||||
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::vcs_dirty_warnings |
||||
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\ |
||||
-summary\ |
||||
"Warning lines for uncommitted VCS changes above a path"\ |
||||
-help\ |
||||
"Walks up from path to the nearest fossil and/or git root and |
||||
returns warning lines describing uncommitted changes. |
||||
Artifacts built from a dirty source tree have no committed |
||||
provenance - callers such as make.tcl vendorupdate use this |
||||
to flag source projects that should be committed first. |
||||
|
||||
checkedrootsvar names a dict variable in the caller's scope |
||||
used to report each VCS root at most once across calls. |
||||
label, if supplied, is prefixed into each warning to name |
||||
the calling operation (e.g. 'vendorupdate'). |
||||
|
||||
Returns an empty list if the path is clean, unversioned, |
||||
missing, already reported, or state cannot be determined." |
||||
@leaders |
||||
path -type string -optional 0 -help\ |
||||
"Path to check; the nearest enclosing fossil/git root is examined" |
||||
checkedrootsvar -type string -optional 0 -help\ |
||||
"Name of a dict variable in the caller's scope for dedupe across calls" |
||||
label -type string -optional 1 -default "" -help\ |
||||
"Operation name prefixed into warnings" |
||||
}] |
||||
} |
||||
proc vcs_dirty_warnings {path checkedrootsvar {label ""}} { |
||||
upvar 1 $checkedrootsvar checkedroots |
||||
if {![info exists checkedroots]} {set checkedroots [dict create]} |
||||
if {$label ne ""} {set label "$label "} |
||||
set warnings [list] |
||||
set dir [file normalize $path] |
||||
if {![file isdirectory $dir]} { |
||||
return $warnings ;#missing path - not this proc's business to report |
||||
} |
||||
set fossilroot "" |
||||
set gitroot "" |
||||
while {1} { |
||||
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} { |
||||
set fossilroot $dir |
||||
} |
||||
if {$gitroot eq "" && [file exists [file join $dir .git]]} { |
||||
set gitroot $dir |
||||
} |
||||
if {$fossilroot ne "" || $gitroot ne ""} { |
||||
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass |
||||
} |
||||
set parent [file dirname $dir] |
||||
if {$parent eq $dir} { break } |
||||
set dir $parent |
||||
} |
||||
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} { |
||||
dict set checkedroots fossil,$fossilroot 1 |
||||
#fossil changes must run from within the checkout |
||||
set original_cwd [pwd] |
||||
if {[catch { |
||||
cd $fossilroot |
||||
set fchanges [string trim [exec {*}[auto_execok fossil] changes]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)" |
||||
} elseif {$fchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
cd $original_cwd |
||||
} |
||||
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} { |
||||
dict set checkedroots git,$gitroot 1 |
||||
if {[catch { |
||||
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)" |
||||
} elseif {$gchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
} |
||||
return $warnings |
||||
} |
||||
} |
||||
|
||||
namespace eval ::punk::args::register { |
||||
#use fully qualified so 8.6 doesn't find existing var in global namespace |
||||
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc |
||||
} |
||||
|
||||
## Ready |
||||
package provide punkboot::utils [tcl::namespace::eval punkboot::utils { |
||||
variable version |
||||
#- this version number, exactly 0.1.1, is a literal used in src module folders |
||||
#- we refer to this sometimes as the magic version number |
||||
set version 0.1.1 |
||||
}] |
||||
return |
||||
@ -0,0 +1,214 @@
|
||||
# -*- tcl -*- |
||||
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
||||
# |
||||
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
||||
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
# (C) 2023 |
||||
# |
||||
# @@ Meta Begin |
||||
# Application punkboot::utils 999999.0a1.0 |
||||
# Meta platform tcl |
||||
# Meta license BSD |
||||
# @@ Meta End |
||||
|
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
## Requirements |
||||
##e.g package require frobz |
||||
package require Tcl 8.6- |
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
namespace eval punkboot::utils { |
||||
variable version 0.1.0 |
||||
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::parse_punkproject_version |
||||
@cmd -name "::punkboot::utils::parse_punkproject_version"\ |
||||
-summary\ |
||||
"Parse the [project] version from TOML content string"\ |
||||
-help\ |
||||
"Scans TOML content for the [project] section and returns |
||||
the value of the version key as a string. Returns an empty |
||||
string if the section or key is not found." |
||||
@leaders |
||||
content -type string -optional 0 -help\ |
||||
"TOML content string to parse" |
||||
}] |
||||
} |
||||
proc parse_punkproject_version {content} { |
||||
set in_project 0 |
||||
foreach line [split $content \n] { |
||||
set trimmed [string trim $line] |
||||
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} { |
||||
set in_project [expr {$trimmed eq "\[project\]"}] |
||||
continue |
||||
} |
||||
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_punkproject_version |
||||
@cmd -name "::punkboot::utils::read_punkproject_version"\ |
||||
-summary\ |
||||
"Read the [project] version from a TOML file"\ |
||||
-help\ |
||||
"Reads the file at tomlfile and returns the [project] |
||||
version value as a string. Returns an empty string if the |
||||
file does not exist or the version is not found." |
||||
@leaders |
||||
tomlfile -type string -optional 0 -help\ |
||||
"Path to the TOML file to read" |
||||
}] |
||||
} |
||||
proc read_punkproject_version {tomlfile} { |
||||
if {![file exists $tomlfile]} {return ""} |
||||
set fh [open $tomlfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
return [::punkboot::utils::parse_punkproject_version $content] |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_changelog_latest_version |
||||
@cmd -name "::punkboot::utils::read_changelog_latest_version"\ |
||||
-summary\ |
||||
"Read the latest ## [X.Y.Z] version header from a changelog file"\ |
||||
-help\ |
||||
"Scans the changelog file from the top and returns the |
||||
version string from the first '## [X.Y.Z]' header line. |
||||
Returns an empty string if the file does not exist or no |
||||
matching header is found." |
||||
@leaders |
||||
changelogfile -type string -optional 0 -help\ |
||||
"Path to the changelog file to read" |
||||
}] |
||||
} |
||||
proc read_changelog_latest_version {changelogfile} { |
||||
if {![file exists $changelogfile]} {return ""} |
||||
set fh [open $changelogfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
foreach line [split $content \n] { |
||||
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::vcs_dirty_warnings |
||||
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\ |
||||
-summary\ |
||||
"Warning lines for uncommitted VCS changes above a path"\ |
||||
-help\ |
||||
"Walks up from path to the nearest fossil and/or git root and |
||||
returns warning lines describing uncommitted changes. |
||||
Artifacts built from a dirty source tree have no committed |
||||
provenance - callers such as make.tcl vendorupdate use this |
||||
to flag source projects that should be committed first. |
||||
|
||||
checkedrootsvar names a dict variable in the caller's scope |
||||
used to report each VCS root at most once across calls. |
||||
label, if supplied, is prefixed into each warning to name |
||||
the calling operation (e.g. 'vendorupdate'). |
||||
|
||||
Returns an empty list if the path is clean, unversioned, |
||||
missing, already reported, or state cannot be determined." |
||||
@leaders |
||||
path -type string -optional 0 -help\ |
||||
"Path to check; the nearest enclosing fossil/git root is examined" |
||||
checkedrootsvar -type string -optional 0 -help\ |
||||
"Name of a dict variable in the caller's scope for dedupe across calls" |
||||
label -type string -optional 1 -default "" -help\ |
||||
"Operation name prefixed into warnings" |
||||
}] |
||||
} |
||||
proc vcs_dirty_warnings {path checkedrootsvar {label ""}} { |
||||
upvar 1 $checkedrootsvar checkedroots |
||||
if {![info exists checkedroots]} {set checkedroots [dict create]} |
||||
if {$label ne ""} {set label "$label "} |
||||
set warnings [list] |
||||
set dir [file normalize $path] |
||||
if {![file isdirectory $dir]} { |
||||
return $warnings ;#missing path - not this proc's business to report |
||||
} |
||||
set fossilroot "" |
||||
set gitroot "" |
||||
while {1} { |
||||
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} { |
||||
set fossilroot $dir |
||||
} |
||||
if {$gitroot eq "" && [file exists [file join $dir .git]]} { |
||||
set gitroot $dir |
||||
} |
||||
if {$fossilroot ne "" || $gitroot ne ""} { |
||||
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass |
||||
} |
||||
set parent [file dirname $dir] |
||||
if {$parent eq $dir} { break } |
||||
set dir $parent |
||||
} |
||||
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} { |
||||
dict set checkedroots fossil,$fossilroot 1 |
||||
#fossil changes must run from within the checkout |
||||
set original_cwd [pwd] |
||||
if {[catch { |
||||
cd $fossilroot |
||||
set fchanges [string trim [exec {*}[auto_execok fossil] changes]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)" |
||||
} elseif {$fchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
cd $original_cwd |
||||
} |
||||
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} { |
||||
dict set checkedroots git,$gitroot 1 |
||||
if {[catch { |
||||
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)" |
||||
} elseif {$gchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
} |
||||
return $warnings |
||||
} |
||||
} |
||||
|
||||
namespace eval ::punk::args::register { |
||||
#use fully qualified so 8.6 doesn't find existing var in global namespace |
||||
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc |
||||
} |
||||
|
||||
## Ready |
||||
package provide punkboot::utils [tcl::namespace::eval punkboot::utils { |
||||
variable version |
||||
#- this version number, exactly 999999.0a1.0, is a literal used in src module folders |
||||
#- we refer to this sometimes as the magic version number |
||||
set version 999999.0a1.0 |
||||
}] |
||||
return |
||||
@ -0,0 +1,5 @@
|
||||
0.1.1 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
#0.1.1 - added vcs_dirty_warnings (dirty fossil/git checkout provenance warnings; used by make.tcl vendorupdate, shared with bootsupport update per goal G-026) |
||||
#0.1.0 - initial: parse_punkproject_version, read_punkproject_version, read_changelog_latest_version extracted from make.tcl inline procs |
||||
@ -1,3 +1,4 @@
|
||||
0.2.2 |
||||
0.2.3 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
#all other lines are ignored. |
||||
#0.2.3 - shellfilter::run: try/finally guarantees tee-stack removal (stdout/stderr restoration) even when the body errors; decorative stack-status table rendering in log::critical blocks is catch-guarded so a rendering failure (e.g mismatched module snapshots - stale textblock vs new punk::ansi sgr_merge_singles options) degrades to a log note instead of aborting the run and leaving the process's stdout/stderr diverted (silent-output-loss failure mode found via tomlish runtests 2026-07-06) |
||||
@ -1,3 +1,10 @@
|
||||
0.1.2 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
0.1.4 |
||||
#First line must be a semantic version number |
||||
#all other lines are ignored. |
||||
#0.1.4 - added missing 'package require punk::args' (0.1.3 call sites use punk::args::parse; absence broke shellrun in bare interps e.g. the runtests testinterp) |
||||
#0.1.3 - migrated runx from get_run_opts to punk::args::parse (added PUNKARGS spec with @cmd + -help) |
||||
#0.1.3 - migrated sh_run/sh_runout/sh_runerr/sh_runx from get_run_opts to punk::args::parse (added PUNKARGS specs) |
||||
#0.1.3 - removed get_run_opts proc; all callers now use punk::args |
||||
#0.1.3 - expanded PUNKARGS documentation for run, runconsole, runout, runerr (added @cmd metadata + -help on opts/values) |
||||
#0.1.3 - runx now honours -debug and --timeout (previously accepted but silently ignored) |
||||
#0.1.3 - removed -tcl from sh_run/sh_runout/sh_runerr/sh_runx (was silently swallowed with no useful effect; now rejected with usage) |
||||
|
||||
@ -0,0 +1,214 @@
|
||||
# -*- tcl -*- |
||||
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
||||
# |
||||
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
||||
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
# (C) 2023 |
||||
# |
||||
# @@ Meta Begin |
||||
# Application punkboot::utils 0.1.1 |
||||
# Meta platform tcl |
||||
# Meta license BSD |
||||
# @@ Meta End |
||||
|
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
## Requirements |
||||
##e.g package require frobz |
||||
package require Tcl 8.6- |
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
namespace eval punkboot::utils { |
||||
variable version 0.1.0 |
||||
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::parse_punkproject_version |
||||
@cmd -name "::punkboot::utils::parse_punkproject_version"\ |
||||
-summary\ |
||||
"Parse the [project] version from TOML content string"\ |
||||
-help\ |
||||
"Scans TOML content for the [project] section and returns |
||||
the value of the version key as a string. Returns an empty |
||||
string if the section or key is not found." |
||||
@leaders |
||||
content -type string -optional 0 -help\ |
||||
"TOML content string to parse" |
||||
}] |
||||
} |
||||
proc parse_punkproject_version {content} { |
||||
set in_project 0 |
||||
foreach line [split $content \n] { |
||||
set trimmed [string trim $line] |
||||
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} { |
||||
set in_project [expr {$trimmed eq "\[project\]"}] |
||||
continue |
||||
} |
||||
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_punkproject_version |
||||
@cmd -name "::punkboot::utils::read_punkproject_version"\ |
||||
-summary\ |
||||
"Read the [project] version from a TOML file"\ |
||||
-help\ |
||||
"Reads the file at tomlfile and returns the [project] |
||||
version value as a string. Returns an empty string if the |
||||
file does not exist or the version is not found." |
||||
@leaders |
||||
tomlfile -type string -optional 0 -help\ |
||||
"Path to the TOML file to read" |
||||
}] |
||||
} |
||||
proc read_punkproject_version {tomlfile} { |
||||
if {![file exists $tomlfile]} {return ""} |
||||
set fh [open $tomlfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
return [::punkboot::utils::parse_punkproject_version $content] |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_changelog_latest_version |
||||
@cmd -name "::punkboot::utils::read_changelog_latest_version"\ |
||||
-summary\ |
||||
"Read the latest ## [X.Y.Z] version header from a changelog file"\ |
||||
-help\ |
||||
"Scans the changelog file from the top and returns the |
||||
version string from the first '## [X.Y.Z]' header line. |
||||
Returns an empty string if the file does not exist or no |
||||
matching header is found." |
||||
@leaders |
||||
changelogfile -type string -optional 0 -help\ |
||||
"Path to the changelog file to read" |
||||
}] |
||||
} |
||||
proc read_changelog_latest_version {changelogfile} { |
||||
if {![file exists $changelogfile]} {return ""} |
||||
set fh [open $changelogfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
foreach line [split $content \n] { |
||||
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::vcs_dirty_warnings |
||||
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\ |
||||
-summary\ |
||||
"Warning lines for uncommitted VCS changes above a path"\ |
||||
-help\ |
||||
"Walks up from path to the nearest fossil and/or git root and |
||||
returns warning lines describing uncommitted changes. |
||||
Artifacts built from a dirty source tree have no committed |
||||
provenance - callers such as make.tcl vendorupdate use this |
||||
to flag source projects that should be committed first. |
||||
|
||||
checkedrootsvar names a dict variable in the caller's scope |
||||
used to report each VCS root at most once across calls. |
||||
label, if supplied, is prefixed into each warning to name |
||||
the calling operation (e.g. 'vendorupdate'). |
||||
|
||||
Returns an empty list if the path is clean, unversioned, |
||||
missing, already reported, or state cannot be determined." |
||||
@leaders |
||||
path -type string -optional 0 -help\ |
||||
"Path to check; the nearest enclosing fossil/git root is examined" |
||||
checkedrootsvar -type string -optional 0 -help\ |
||||
"Name of a dict variable in the caller's scope for dedupe across calls" |
||||
label -type string -optional 1 -default "" -help\ |
||||
"Operation name prefixed into warnings" |
||||
}] |
||||
} |
||||
proc vcs_dirty_warnings {path checkedrootsvar {label ""}} { |
||||
upvar 1 $checkedrootsvar checkedroots |
||||
if {![info exists checkedroots]} {set checkedroots [dict create]} |
||||
if {$label ne ""} {set label "$label "} |
||||
set warnings [list] |
||||
set dir [file normalize $path] |
||||
if {![file isdirectory $dir]} { |
||||
return $warnings ;#missing path - not this proc's business to report |
||||
} |
||||
set fossilroot "" |
||||
set gitroot "" |
||||
while {1} { |
||||
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} { |
||||
set fossilroot $dir |
||||
} |
||||
if {$gitroot eq "" && [file exists [file join $dir .git]]} { |
||||
set gitroot $dir |
||||
} |
||||
if {$fossilroot ne "" || $gitroot ne ""} { |
||||
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass |
||||
} |
||||
set parent [file dirname $dir] |
||||
if {$parent eq $dir} { break } |
||||
set dir $parent |
||||
} |
||||
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} { |
||||
dict set checkedroots fossil,$fossilroot 1 |
||||
#fossil changes must run from within the checkout |
||||
set original_cwd [pwd] |
||||
if {[catch { |
||||
cd $fossilroot |
||||
set fchanges [string trim [exec {*}[auto_execok fossil] changes]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)" |
||||
} elseif {$fchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
cd $original_cwd |
||||
} |
||||
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} { |
||||
dict set checkedroots git,$gitroot 1 |
||||
if {[catch { |
||||
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)" |
||||
} elseif {$gchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
} |
||||
return $warnings |
||||
} |
||||
} |
||||
|
||||
namespace eval ::punk::args::register { |
||||
#use fully qualified so 8.6 doesn't find existing var in global namespace |
||||
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc |
||||
} |
||||
|
||||
## Ready |
||||
package provide punkboot::utils [tcl::namespace::eval punkboot::utils { |
||||
variable version |
||||
#- this version number, exactly 0.1.1, is a literal used in src module folders |
||||
#- we refer to this sometimes as the magic version number |
||||
set version 0.1.1 |
||||
}] |
||||
return |
||||
@ -0,0 +1,214 @@
|
||||
# -*- tcl -*- |
||||
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt |
||||
# |
||||
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
||||
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
# (C) 2023 |
||||
# |
||||
# @@ Meta Begin |
||||
# Application punkboot::utils 0.1.1 |
||||
# Meta platform tcl |
||||
# Meta license BSD |
||||
# @@ Meta End |
||||
|
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
## Requirements |
||||
##e.g package require frobz |
||||
package require Tcl 8.6- |
||||
|
||||
|
||||
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
||||
namespace eval punkboot::utils { |
||||
variable version 0.1.0 |
||||
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version vcs_dirty_warnings |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::parse_punkproject_version |
||||
@cmd -name "::punkboot::utils::parse_punkproject_version"\ |
||||
-summary\ |
||||
"Parse the [project] version from TOML content string"\ |
||||
-help\ |
||||
"Scans TOML content for the [project] section and returns |
||||
the value of the version key as a string. Returns an empty |
||||
string if the section or key is not found." |
||||
@leaders |
||||
content -type string -optional 0 -help\ |
||||
"TOML content string to parse" |
||||
}] |
||||
} |
||||
proc parse_punkproject_version {content} { |
||||
set in_project 0 |
||||
foreach line [split $content \n] { |
||||
set trimmed [string trim $line] |
||||
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} { |
||||
set in_project [expr {$trimmed eq "\[project\]"}] |
||||
continue |
||||
} |
||||
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_punkproject_version |
||||
@cmd -name "::punkboot::utils::read_punkproject_version"\ |
||||
-summary\ |
||||
"Read the [project] version from a TOML file"\ |
||||
-help\ |
||||
"Reads the file at tomlfile and returns the [project] |
||||
version value as a string. Returns an empty string if the |
||||
file does not exist or the version is not found." |
||||
@leaders |
||||
tomlfile -type string -optional 0 -help\ |
||||
"Path to the TOML file to read" |
||||
}] |
||||
} |
||||
proc read_punkproject_version {tomlfile} { |
||||
if {![file exists $tomlfile]} {return ""} |
||||
set fh [open $tomlfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
return [::punkboot::utils::parse_punkproject_version $content] |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::read_changelog_latest_version |
||||
@cmd -name "::punkboot::utils::read_changelog_latest_version"\ |
||||
-summary\ |
||||
"Read the latest ## [X.Y.Z] version header from a changelog file"\ |
||||
-help\ |
||||
"Scans the changelog file from the top and returns the |
||||
version string from the first '## [X.Y.Z]' header line. |
||||
Returns an empty string if the file does not exist or no |
||||
matching header is found." |
||||
@leaders |
||||
changelogfile -type string -optional 0 -help\ |
||||
"Path to the changelog file to read" |
||||
}] |
||||
} |
||||
proc read_changelog_latest_version {changelogfile} { |
||||
if {![file exists $changelogfile]} {return ""} |
||||
set fh [open $changelogfile r] |
||||
try { |
||||
set content [read $fh] |
||||
} finally { |
||||
close $fh |
||||
} |
||||
foreach line [split $content \n] { |
||||
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} { |
||||
return $v |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
|
||||
namespace eval argdoc { |
||||
lappend PUNKARGS [list { |
||||
@id -id ::punkboot::utils::vcs_dirty_warnings |
||||
@cmd -name "::punkboot::utils::vcs_dirty_warnings"\ |
||||
-summary\ |
||||
"Warning lines for uncommitted VCS changes above a path"\ |
||||
-help\ |
||||
"Walks up from path to the nearest fossil and/or git root and |
||||
returns warning lines describing uncommitted changes. |
||||
Artifacts built from a dirty source tree have no committed |
||||
provenance - callers such as make.tcl vendorupdate use this |
||||
to flag source projects that should be committed first. |
||||
|
||||
checkedrootsvar names a dict variable in the caller's scope |
||||
used to report each VCS root at most once across calls. |
||||
label, if supplied, is prefixed into each warning to name |
||||
the calling operation (e.g. 'vendorupdate'). |
||||
|
||||
Returns an empty list if the path is clean, unversioned, |
||||
missing, already reported, or state cannot be determined." |
||||
@leaders |
||||
path -type string -optional 0 -help\ |
||||
"Path to check; the nearest enclosing fossil/git root is examined" |
||||
checkedrootsvar -type string -optional 0 -help\ |
||||
"Name of a dict variable in the caller's scope for dedupe across calls" |
||||
label -type string -optional 1 -default "" -help\ |
||||
"Operation name prefixed into warnings" |
||||
}] |
||||
} |
||||
proc vcs_dirty_warnings {path checkedrootsvar {label ""}} { |
||||
upvar 1 $checkedrootsvar checkedroots |
||||
if {![info exists checkedroots]} {set checkedroots [dict create]} |
||||
if {$label ne ""} {set label "$label "} |
||||
set warnings [list] |
||||
set dir [file normalize $path] |
||||
if {![file isdirectory $dir]} { |
||||
return $warnings ;#missing path - not this proc's business to report |
||||
} |
||||
set fossilroot "" |
||||
set gitroot "" |
||||
while {1} { |
||||
if {$fossilroot eq "" && ([file exists [file join $dir .fslckout]] || [file exists [file join $dir _FOSSIL_]])} { |
||||
set fossilroot $dir |
||||
} |
||||
if {$gitroot eq "" && [file exists [file join $dir .git]]} { |
||||
set gitroot $dir |
||||
} |
||||
if {$fossilroot ne "" || $gitroot ne ""} { |
||||
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass |
||||
} |
||||
set parent [file dirname $dir] |
||||
if {$parent eq $dir} { break } |
||||
set dir $parent |
||||
} |
||||
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} { |
||||
dict set checkedroots fossil,$fossilroot 1 |
||||
#fossil changes must run from within the checkout |
||||
set original_cwd [pwd] |
||||
if {[catch { |
||||
cd $fossilroot |
||||
set fchanges [string trim [exec {*}[auto_execok fossil] changes]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)" |
||||
} elseif {$fchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
cd $original_cwd |
||||
} |
||||
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} { |
||||
dict set checkedroots git,$gitroot 1 |
||||
if {[catch { |
||||
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]] |
||||
} errM]} { |
||||
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)" |
||||
} elseif {$gchanges ne ""} { |
||||
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance" |
||||
} |
||||
} |
||||
return $warnings |
||||
} |
||||
} |
||||
|
||||
namespace eval ::punk::args::register { |
||||
#use fully qualified so 8.6 doesn't find existing var in global namespace |
||||
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc |
||||
} |
||||
|
||||
## Ready |
||||
package provide punkboot::utils [tcl::namespace::eval punkboot::utils { |
||||
variable version |
||||
#- this version number, exactly 0.1.1, is a literal used in src module folders |
||||
#- we refer to this sometimes as the magic version number |
||||
set version 0.1.1 |
||||
}] |
||||
return |
||||
@ -0,0 +1,3 @@
|
||||
Project scripts and executables |
||||
|
||||
These files usually shouldn't be modified directly as they are built from the files in the src folder |
||||
@ -0,0 +1,26 @@
|
||||
runtime Tcl Executables are installed here under the appropriate os_architecture folder. |
||||
e.g |
||||
windows 64bit binaries under win32-x86_64 |
||||
linux 64 bit binaries under linux-x86_64 |
||||
|
||||
The bin/runtime.cmd cross-platform script can be used to download prebuilt Tcl binaries. |
||||
e.g ./bin/runtime.cmd fetch |
||||
This will detect the platform and architecture, creating the containing folder as necessary. |
||||
|
||||
To see existing installed runtimes: |
||||
./bin/runtime.cmd list |
||||
Or to compare local runtimes with those available on the artifact server: |
||||
./bin/runtime.cmd list -remote |
||||
|
||||
An installed runtime can then be used to call the src/make.tcl file. |
||||
|
||||
e.g - initial check |
||||
./bin/runtime.cmd run src/make.tcl check |
||||
|
||||
e.g - to build (or just copy with latest version number from src if build not required) modules to projectdir/modules |
||||
./bin/runtime.cmd run src/make.tcl modules |
||||
for libs & modules |
||||
./bin/runtime.cmd run src/make.tcl packages |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
Tcl Libraries which use the pkgIndex.tcl system to load. |
||||
Each library will usually have its own folder in the lib folder. |
||||
Some groups of libraries may have a deeper structure - usually with a pkIndex.tcl at the top level, |
||||
and individual pkgIndex.tcl files further down. |
||||
|
||||
These files shouldn't be modified directly as they are built or copied from the files in the src folder |
||||
@ -0,0 +1,9 @@
|
||||
Tcl Modules |
||||
These are packaged as .tm file extension and the loadable locations are controlled by functions such as tcl::tm::path add|list|remove |
||||
|
||||
Via the make process, the modules may come from from src/vendormodules - being modules not generally developed within the current project. |
||||
|
||||
They are built from src/modules for those that are directly part of the project. |
||||
|
||||
These files shouldn't be modified directly as they are built or copied from the files in the src folder structure. |
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package require tcltest |
||||
tcltest::configure {*}$::argv |
||||
|
||||
#min-version bound documents that these tests target the dev module's API and protects |
||||
#against stable bootsupport copies shadowing the alpha dev version if this file is sourced outside |
||||
#runtests.tcl (whose testinterp runs 'package prefer latest' making the bound redundant there). |
||||
package require punk::console 999999.0a1.0- |
||||
|
||||
#Tests for punk::console::ensure_object_integration wiring-order behaviour: the graceful no-op |
||||
#when opunk::console is not loaded, and catch-up ownership registration for anchors created |
||||
#before the lifecycle callback was wired. |
||||
#ORDER MATTERS within this file: the first test relies on opunk::console not yet being loaded |
||||
#in this interp (runtests gives each test file a fresh testinterp), and the retro-registration |
||||
#test relies on creating its anchor before the first successful ensure_object_integration. |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
tcltest::testConstraint opunk_not_loaded [expr {[catch {package present opunk::console}]}] |
||||
|
||||
test ensure_object_integration_no_opunk {graceful no-op (0, no error, no latch) when opunk::console is not loaded}\ |
||||
-constraints opunk_not_loaded\ |
||||
-setup $common -body { |
||||
lappend result [punk::console::ensure_object_integration] |
||||
#must not have latched - a later call after opunk::console loads must still wire |
||||
lappend result [set ::punk::console::object_integration_done] |
||||
}\ |
||||
-cleanup {}\ |
||||
-result [list\ |
||||
0\ |
||||
0\ |
||||
] |
||||
|
||||
test ensure_object_integration_retro_registration {anchor created before wiring gains a registered owner when ensure_object_integration wires the callback}\ |
||||
-setup $common -body { |
||||
package require opunk::console |
||||
lassign [chan pipe] rd wr |
||||
#create BEFORE integration wiring - lifecycle callback is empty so no 'created' |
||||
#event fires and no owner is registered (the wiring-order hole) |
||||
opunk::console::create preintegration $rd $wr |
||||
lappend result [punk::console::console_owner_get [list $rd $wr]] |
||||
#wiring performs the catch-up registration for the pre-existing anchor |
||||
lappend result [punk::console::ensure_object_integration] |
||||
lappend result [expr {[punk::console::console_owner_get [list $rd $wr]] eq [thread::id]}] |
||||
#lifecycle callback is now wired - forget clears the entry as usual |
||||
opunk::console::forget preintegration |
||||
lappend result [punk::console::console_owner_get [list $rd $wr]] |
||||
}\ |
||||
-cleanup { |
||||
catch {opunk::console::forget preintegration} |
||||
catch {punk::console::console_owner_forget [list $rd $wr]} |
||||
catch {chan close $rd} |
||||
catch {chan close $wr} |
||||
}\ |
||||
-result [list\ |
||||
{}\ |
||||
1\ |
||||
1\ |
||||
{}\ |
||||
] |
||||
|
||||
test ensure_object_integration_idempotent {second call is a latched no-op returning 1}\ |
||||
-setup $common -body { |
||||
lappend result [punk::console::ensure_object_integration] |
||||
lappend result [set ::punk::console::object_integration_done] |
||||
}\ |
||||
-cleanup {}\ |
||||
-result [list\ |
||||
1\ |
||||
1\ |
||||
] |
||||
|
||||
test ensure_object_integration_create_after_wiring {anchor created after wiring registers via the lifecycle callback (no retro path involved)}\ |
||||
-setup $common -body { |
||||
lassign [chan pipe] rd wr |
||||
opunk::console::create postintegration $rd $wr |
||||
lappend result [expr {[punk::console::console_owner_get [list $rd $wr]] eq [thread::id]}] |
||||
opunk::console::forget postintegration |
||||
lappend result [punk::console::console_owner_get [list $rd $wr]] |
||||
}\ |
||||
-cleanup { |
||||
catch {opunk::console::forget postintegration} |
||||
catch {punk::console::console_owner_forget [list $rd $wr]} |
||||
catch {chan close $rd} |
||||
catch {chan close $wr} |
||||
}\ |
||||
-result [list\ |
||||
1\ |
||||
{}\ |
||||
] |
||||
|
||||
} |
||||
tcltest::cleanupTests ;#needed to produce test summary. |
||||
@ -0,0 +1,125 @@
|
||||
package require tcltest |
||||
tcltest::configure {*}$::argv |
||||
|
||||
#min-version bounds document that these tests target the dev modules' API and protect against |
||||
#stable copies shadowing them if this file is sourced outside runtests.tcl (whose testinterp |
||||
#runs 'package prefer latest'). |
||||
package require punk::console 999999.0a1.0- |
||||
|
||||
#Tests for G-007 choke-point brokering: a terminal query (internal::get_ansi_response_payload |
||||
#and every query proc layered above it) on the process-default console {stdin stdout} routes |
||||
#to the console-owning thread when the caller is not the owner; an unregistered console, an |
||||
#owner==caller console, and any non-default {in out} pair operate locally (non-std channel |
||||
#names are thread-local, so a pair spec always names the calling thread's own console). |
||||
#The routing decision is internal::console_route_owner; the transport is a synchronous |
||||
#thread::send into the owning thread's main interp (the established vt52-alias property: |
||||
#the owner services events while the caller blocks). |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
test routing_decision {console_route_owner: local for unregistered/self-owned/non-default/dead-owner; owner tid only for the default console owned by another live thread}\ |
||||
-setup [string cat $common { |
||||
set workertid [thread::create {thread::wait}] |
||||
}]\ |
||||
-body { |
||||
#unregistered default console - operate locally |
||||
lappend result [punk::console::internal::console_route_owner {stdin stdout}] |
||||
#caller is the owner - local fast path |
||||
punk::console::console_owner_register {stdin stdout} |
||||
lappend result [punk::console::internal::console_route_owner {stdin stdout}] |
||||
punk::console::console_owner_forget {stdin stdout} |
||||
#another live thread owns the default console - route to it |
||||
punk::console::console_owner_register {stdin stdout} $workertid |
||||
lappend result [expr {[punk::console::internal::console_route_owner {stdin stdout}] eq $workertid}] |
||||
#non-default pair never routes, even with a registry entry (the entry qualifies |
||||
#the shared fact store; the pair names the calling thread's own channels) |
||||
punk::console::console_owner_register {chanx chany} $workertid |
||||
lappend result [punk::console::internal::console_route_owner {chanx chany}] |
||||
punk::console::console_owner_forget {chanx chany} |
||||
#dead owner - consult-time liveness validation clears the entry, operate locally |
||||
thread::release $workertid |
||||
set deadline [expr {[clock milliseconds] + 2000}] |
||||
while {[thread::exists $workertid] && [clock milliseconds] < $deadline} { |
||||
after 10 |
||||
} |
||||
lappend result [punk::console::internal::console_route_owner {stdin stdout}] |
||||
}\ |
||||
-cleanup { |
||||
catch {punk::console::console_owner_forget {stdin stdout}} |
||||
catch {punk::console::console_owner_forget {chanx chany}} |
||||
catch {thread::release $workertid} |
||||
}\ |
||||
-result [list\ |
||||
{}\ |
||||
{}\ |
||||
1\ |
||||
{}\ |
||||
{}\ |
||||
] |
||||
|
||||
test routing_query_forwarded_to_owner {a query on the owned default console executes in the owning thread with opts/values intact; query procs above the choke point inherit the routing; owner-side errors propagate with routing context}\ |
||||
-setup [string cat $common { |
||||
set workertid [thread::create {thread::wait}] |
||||
#the worker loads punk::console from the same module paths as the testinterp |
||||
thread::send $workertid [list set ::tmpaths [tcl::tm::list]] |
||||
thread::send $workertid [list set ::auto_path $::auto_path] |
||||
thread::send $workertid { |
||||
foreach p $::tmpaths {tcl::tm::path add $p} |
||||
package prefer latest |
||||
package require punk::console 999999.0a1.0- |
||||
#replace the real query primitive with a recorder: no real terminal is |
||||
#available under runtests, and the transport - not the terminal I/O - is |
||||
#what this test exercises |
||||
rename ::punk::console::internal::get_ansi_response_payload ::punk::console::internal::get_ansi_response_payload.real |
||||
set ::routingtest_mode ok |
||||
proc ::punk::console::internal::get_ansi_response_payload {args} { |
||||
set ::routingtest_seen [list [thread::id] $args] |
||||
if {$::routingtest_mode eq "error"} { |
||||
error "recorder-failure" |
||||
} |
||||
return "5;7" |
||||
} |
||||
} |
||||
punk::console::console_owner_register {stdin stdout} $workertid |
||||
}]\ |
||||
-body { |
||||
#direct call on the choke point routes and returns the owner-side result |
||||
set payload [punk::console::internal::get_ansi_response_payload -console {stdin stdout} -expected_ms 123 "\x1b\[6n" {(.*)(\x1b\[([0-9]+;[0-9]+)R)$}] |
||||
lappend result $payload |
||||
lassign [thread::send $workertid {set ::routingtest_seen}] seentid seenargs |
||||
#the call executed in the owning thread |
||||
lappend result [expr {$seentid eq $workertid}] |
||||
#opts and values arrived intact (values are the trailing pair; opts precede them) |
||||
lappend result [lrange $seenargs end-1 end] |
||||
set seenopts [lrange $seenargs 0 end-2] |
||||
lappend result [dict get $seenopts -console] |
||||
lappend result [dict get $seenopts -expected_ms] |
||||
#a query proc layered above the choke point inherits the routing (get_cursor_pos |
||||
#returns the payload unchanged) |
||||
lappend result [punk::console::get_cursor_pos] |
||||
#an error raised in the owning thread propagates to the caller with routing context |
||||
thread::send $workertid {set ::routingtest_mode error} |
||||
lappend result [expr {[catch {punk::console::internal::get_ansi_response_payload -console {stdin stdout} "\x1b\[6n" {(.*)(\x1b\[([0-9]+;[0-9]+)R)$}} errM] |
||||
&& [string match "*routed to console-owning thread*recorder-failure*" $errM]}] |
||||
}\ |
||||
-cleanup { |
||||
catch {punk::console::console_owner_forget {stdin stdout}} |
||||
catch {thread::release $workertid} |
||||
}\ |
||||
-result [list\ |
||||
{5;7}\ |
||||
1\ |
||||
[list "\x1b\[6n" {(.*)(\x1b\[([0-9]+;[0-9]+)R)$}]\ |
||||
{stdin stdout}\ |
||||
123\ |
||||
{5;7}\ |
||||
1\ |
||||
] |
||||
|
||||
} |
||||
tcltest::cleanupTests ;#needed to produce test summary. |
||||
@ -0,0 +1,251 @@
|
||||
# -*- tcl -*- |
||||
# Tests for the fossil repository move/rename area used by |
||||
# punk::mix::commandset::repo::fossil-move-repository: |
||||
# - punk::repo fossil discovery primitives against a scratch repo+checkout |
||||
# (fossil_get_configdb, find_fossil, fossil_get_repository_file) |
||||
# - the move primitive sequence the commandset uses (file copy + fossil |
||||
# test-move-repository) and what it leaves behind |
||||
# The 3.x/4.x tests are CHARACTERIZATION tests: they pin the current behaviour, |
||||
# including the gaps a future scriptable safe-move/rename must close (see root |
||||
# GOALS.md G-022): the old repo db file is left on disk, the central config-db |
||||
# keeps a phantom repo: entry for the old path, the old repo db keeps a stale |
||||
# ckout: back-reference, and nothing renames the fossil project-name. |
||||
# When that capability lands, the gap tests here are expected to be updated to |
||||
# assert the new (clean) behaviour. |
||||
# |
||||
# Isolation: all fossil activity runs with FOSSIL_HOME pointing into a temp |
||||
# folder, so the user's real central fossil config-db is never touched. Every |
||||
# mutating step is gated on verifying fossil actually reports its config-db |
||||
# under that temp folder. |
||||
# |
||||
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punk/mix/** fossilmove.test |
||||
|
||||
package require tcltest |
||||
package require punk::lib |
||||
package require punk::repo |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# Constraints and isolation guard |
||||
# ------------------------------------------------------------------------- |
||||
set fossilcmd [auto_execok fossil] |
||||
tcltest::testConstraint fossil_available [expr {[llength $fossilcmd] > 0}] |
||||
|
||||
variable env_saved_exists [info exists ::env(FOSSIL_HOME)] |
||||
variable env_saved_value [expr {$env_saved_exists ? $::env(FOSSIL_HOME) : ""}] |
||||
|
||||
variable tempbase "" |
||||
variable fosshome "" |
||||
variable repodir "" |
||||
variable codir "" |
||||
|
||||
proc path_eq {a b} { |
||||
string equal -nocase [file normalize $a] [file normalize $b] |
||||
} |
||||
|
||||
proc run_fossil {workdir args} { |
||||
variable fossilcmd |
||||
set original_cwd [pwd] |
||||
try { |
||||
if {$workdir ne ""} { |
||||
cd $workdir |
||||
} |
||||
exec -ignorestderr {*}$fossilcmd {*}$args |
||||
} finally { |
||||
cd $original_cwd |
||||
} |
||||
} |
||||
|
||||
# central config-db repo entries as reported by fossil itself |
||||
proc central_repo_list {} { |
||||
variable codir |
||||
set lines [run_fossil $codir all ls] |
||||
set paths [list] |
||||
foreach ln [split [string map {\r\n \n} $lines] \n] { |
||||
set ln [string trim $ln] |
||||
if {$ln ne ""} { lappend paths $ln } |
||||
} |
||||
return $paths |
||||
} |
||||
|
||||
proc central_has_repo {repopath} { |
||||
foreach p [central_repo_list] { |
||||
if {[path_eq $p $repopath]} { return 1 } |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
# ckout: checkout-record paths registered in a repository db |
||||
proc repo_ckout_dirs {repodb} { |
||||
set lines [run_fossil "" sql -R $repodb \ |
||||
"select name from config where name like 'ckout:%'"] |
||||
set dirs [list] |
||||
foreach ln [split [string map {\r\n \n} $lines] \n] { |
||||
set ln [string trim $ln] |
||||
if {[string match ckout:* $ln]} { |
||||
lappend dirs [string trimright [string range $ln 6 end] /] |
||||
} |
||||
} |
||||
return $dirs |
||||
} |
||||
|
||||
proc repo_project_name {repodb} { |
||||
string trim [run_fossil "" sql -R $repodb \ |
||||
"select value from config where name='project-name'"] |
||||
} |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# Fixture: isolated FOSSIL_HOME, scratch repo db + one open checkout. |
||||
# Any failure (including an isolation check failure) disables the |
||||
# fossil_fixture constraint so no test proceeds against the wrong config-db. |
||||
# ------------------------------------------------------------------------- |
||||
variable fixture_error "" |
||||
if {[tcltest::testConstraint fossil_available]} { |
||||
try { |
||||
set tempbase [punk::lib::tempdir_newfolder -prefix pkfossilmovetest] |
||||
set fosshome [file join $tempbase fosshome] |
||||
set repodir [file join $tempbase repos] |
||||
set codir [file join $tempbase co] |
||||
file mkdir $fosshome $repodir $codir |
||||
set ::env(FOSSIL_HOME) $fosshome |
||||
|
||||
run_fossil "" init [file join $repodir repoA.fossil] -project-name testmoveproj |
||||
run_fossil $codir open [file join $repodir repoA.fossil] |
||||
|
||||
# isolation guard: fossil must report its config-db under our |
||||
# temp FOSSIL_HOME or nothing else may run |
||||
set infolines [run_fossil $codir info] |
||||
set configdb "" |
||||
foreach ln [split [string map {\r\n \n} $infolines] \n] { |
||||
if {[string match "config-db:*" [string trim $ln]]} { |
||||
set configdb [string trim [string range [string trim $ln] 10 end]] |
||||
} |
||||
} |
||||
if {$configdb eq "" || ![string match -nocase "[file normalize $fosshome]*" [file normalize $configdb]]} { |
||||
error "isolation check failed: fossil config-db '$configdb' is not under temp FOSSIL_HOME '$fosshome'" |
||||
} |
||||
} on error {result} { |
||||
set fixture_error $result |
||||
} |
||||
} |
||||
tcltest::testConstraint fossil_fixture [expr { |
||||
[tcltest::testConstraint fossil_available] && $fixture_error eq "" |
||||
}] |
||||
if {$fixture_error ne ""} { |
||||
puts stderr "fossilmove.test fixture setup failed (tests will be skipped): $fixture_error" |
||||
} |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# 1.x punk::repo discovery primitives against the scratch checkout |
||||
# ------------------------------------------------------------------------- |
||||
test fossilmove-1.1 {fossil_get_configdb resolves to the FOSSIL_HOME-isolated config-db} -constraints fossil_fixture -body { |
||||
variable fosshome |
||||
set configdb [punk::repo::fossil_get_configdb $::testspace::codir] |
||||
string match -nocase "[file normalize $fosshome]*" [file normalize $configdb] |
||||
} -result 1 |
||||
|
||||
test fossilmove-1.2 {find_fossil locates the scratch checkout root} -constraints fossil_fixture -body { |
||||
variable codir |
||||
path_eq [punk::repo::find_fossil $codir] $codir |
||||
} -result 1 |
||||
|
||||
test fossilmove-1.3 {fossil_get_repository_file returns the opened repo db path} -constraints fossil_fixture -body { |
||||
variable codir |
||||
variable repodir |
||||
path_eq [punk::repo::fossil_get_repository_file $codir] [file join $repodir repoA.fossil] |
||||
} -result 1 |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# 2.x central config-db registration baseline |
||||
# ------------------------------------------------------------------------- |
||||
test fossilmove-2.1 {open registers the repo db in the central config-db} -constraints fossil_fixture -body { |
||||
variable repodir |
||||
central_has_repo [file join $repodir repoA.fossil] |
||||
} -result 1 |
||||
|
||||
test fossilmove-2.2 {repo db records its checkout as a ckout: entry} -constraints fossil_fixture -body { |
||||
variable repodir |
||||
variable codir |
||||
set dirs [repo_ckout_dirs [file join $repodir repoA.fossil]] |
||||
expr {[llength $dirs] == 1 && [path_eq [lindex $dirs 0] $codir]} |
||||
} -result 1 |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# 3.x the move primitive sequence used by fossil-move-repository: |
||||
# file copy old->new, then fossil test-move-repository in the checkout. |
||||
# 3.1 pins the part that works; 3.2-3.4 pin the current gaps. |
||||
# ------------------------------------------------------------------------- |
||||
variable move_error "" |
||||
if {[tcltest::testConstraint fossil_fixture]} { |
||||
try { |
||||
file copy [file join $repodir repoA.fossil] [file join $repodir repoB.fossil] |
||||
run_fossil $codir test-move-repository [file join $repodir repoB.fossil] |
||||
} on error {result} { |
||||
set move_error $result |
||||
} |
||||
} |
||||
tcltest::testConstraint fossil_moved [expr { |
||||
[tcltest::testConstraint fossil_fixture] && $move_error eq "" |
||||
}] |
||||
if {$move_error ne ""} { |
||||
puts stderr "fossilmove.test move sequence failed (3.x/4.x will be skipped): $move_error" |
||||
} |
||||
|
||||
test fossilmove-3.1 {test-move-repository repoints the checkout to the new repo db} -constraints fossil_moved -body { |
||||
variable codir |
||||
variable repodir |
||||
path_eq [punk::repo::fossil_get_repository_file $codir] [file join $repodir repoB.fossil] |
||||
} -result 1 |
||||
|
||||
test fossilmove-3.2 {GAP: move-by-copy leaves the old repo db file on disk} -constraints fossil_moved -body { |
||||
# current commandset behaviour is file copy + repoint - the old db file |
||||
# remains; a safe scriptable move should remove/archive it once no |
||||
# checkout references it (G-022) |
||||
variable repodir |
||||
file exists [file join $repodir repoA.fossil] |
||||
} -result 1 |
||||
|
||||
test fossilmove-3.3 {GAP: central config-db keeps a phantom repo: entry for the old path} -constraints fossil_moved -body { |
||||
# both old and new paths are listed after the move - the old entry is |
||||
# the phantom a safe move must remove (G-022) |
||||
variable repodir |
||||
list [central_has_repo [file join $repodir repoA.fossil]] \ |
||||
[central_has_repo [file join $repodir repoB.fossil]] |
||||
} -result {1 1} |
||||
|
||||
test fossilmove-3.4 {GAP: old repo db retains a stale ckout: back-reference} -constraints fossil_moved -body { |
||||
# the checkout no longer uses repoA but repoA still claims it (G-022) |
||||
variable repodir |
||||
variable codir |
||||
set dirs [repo_ckout_dirs [file join $repodir repoA.fossil]] |
||||
expr {[llength $dirs] == 1 && [path_eq [lindex $dirs 0] $codir]} |
||||
} -result 1 |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# 4.x project-name: moving/renaming the db file does not rename the project |
||||
# ------------------------------------------------------------------------- |
||||
test fossilmove-4.1 {GAP: repo db file move/rename leaves fossil project-name unchanged} -constraints fossil_moved -body { |
||||
# no current commandset capability renames the project-name recorded in |
||||
# the repo db config table (G-022 requires it for shellspy->punkshell) |
||||
variable repodir |
||||
repo_project_name [file join $repodir repoB.fossil] |
||||
} -result testmoveproj |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# Cleanup: restore FOSSIL_HOME and remove the temp tree |
||||
# ------------------------------------------------------------------------- |
||||
variable env_saved_exists |
||||
if {$env_saved_exists} { |
||||
set ::env(FOSSIL_HOME) $env_saved_value |
||||
} else { |
||||
unset -nocomplain ::env(FOSSIL_HOME) |
||||
} |
||||
if {$tempbase ne "" && [file isdirectory $tempbase]} { |
||||
catch {file delete -force $tempbase} |
||||
} |
||||
} |
||||
|
||||
tcltest::cleanupTests |
||||
namespace delete ::testspace |
||||
@ -0,0 +1,198 @@
|
||||
# -*- tcl -*- |
||||
# Tests for punkboot::utils project-version helpers: |
||||
# - parse_punkproject_version: TOML content string -> version string |
||||
# - read_changelog_latest_version: changelog file -> latest ## [X.Y.Z] header |
||||
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/** utils.test |
||||
|
||||
package require tcltest |
||||
package require punk::lib |
||||
package require punkboot::utils |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
# ------------------------------------------------------------------------- |
||||
# Helper: write content to a temp file. Returns the file path. |
||||
# ------------------------------------------------------------------------- |
||||
proc write_temp_file {prefix content} { |
||||
set dir [punk::lib::tempdir_newfolder -prefix $prefix] |
||||
set path [file join $dir data] |
||||
set fd [open $path w] |
||||
try { puts -nonewline $fd $content } finally { close $fd } |
||||
return $path |
||||
} |
||||
|
||||
# ========================================================================= |
||||
# parse_punkproject_version |
||||
# ========================================================================= |
||||
|
||||
test parse_ppv_standard {Standard [project] version line} -body { |
||||
set toml {[project] |
||||
name = "punkshell" |
||||
version = "0.2.0" |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "0.2.0" |
||||
|
||||
test parse_ppv_no_version_field {Missing version key in [project] section} -body { |
||||
set toml {[project] |
||||
name = "punkshell" |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "" |
||||
|
||||
test parse_ppv_version_in_wrong_section {Version in a non-[project] section is ignored} -body { |
||||
set toml {[project] |
||||
name = "punkshell" |
||||
|
||||
[other] |
||||
version = "9.9.9" |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "" |
||||
|
||||
test parse_ppv_empty_content {Empty string returns empty} -body { |
||||
::punkboot::utils::parse_punkproject_version "" |
||||
} -result "" |
||||
|
||||
test parse_ppv_no_section {No section header at all} -body { |
||||
::punkboot::utils::parse_punkproject_version {version = "1.0.0"} |
||||
} -result "" |
||||
|
||||
test parse_ppv_whitespace_around_equals {Whitespace variations around = are tolerated} -body { |
||||
set toml {[project] |
||||
version = "0.3.1" |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "0.3.1" |
||||
|
||||
test parse_ppv_section_after_project {A later section does not override [project] version} -body { |
||||
set toml {[project] |
||||
version = "0.2.0" |
||||
|
||||
[dependencies] |
||||
version = "1.5.0" |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "0.2.0" |
||||
|
||||
test parse_ppv_singlequoted_version {Single-quoted value is not matched (TOML requires double quotes)} -body { |
||||
set toml {[project] |
||||
version = '0.2.0' |
||||
} |
||||
::punkboot::utils::parse_punkproject_version $toml |
||||
} -result "" |
||||
|
||||
# ========================================================================= |
||||
# read_punkproject_version |
||||
# ========================================================================= |
||||
|
||||
test read_ppv_file_not_found {Non-existent file returns empty} -body { |
||||
::punkboot::utils::read_punkproject_version /nonexistent/path/to/punkproject.toml |
||||
} -result "" |
||||
|
||||
test read_ppv_file_standard {Read a real-ish TOML file} -body { |
||||
set path [write_temp_file ppvread {[project] |
||||
name = "punkshell" |
||||
version = "1.2.3" |
||||
}] |
||||
set result [::punkboot::utils::read_punkproject_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "1.2.3" |
||||
|
||||
# ========================================================================= |
||||
# read_changelog_latest_version |
||||
# ========================================================================= |
||||
|
||||
test read_clv_standard {Standard changelog with one entry} -body { |
||||
set content {# Changelog |
||||
|
||||
## [0.2.0] - 2026-07-05 |
||||
|
||||
- Initial tracking. |
||||
} |
||||
set path [write_temp_file clvstd $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "0.2.0" |
||||
|
||||
test read_clv_multiple_entries {First (newest) header returned when multiple exist} -body { |
||||
set content {# Changelog |
||||
|
||||
## [0.2.1] - 2026-07-06 |
||||
|
||||
- New subcommand. |
||||
|
||||
## [0.2.0] - 2026-07-05 |
||||
|
||||
- Initial tracking. |
||||
} |
||||
set path [write_temp_file clvmulti $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "0.2.1" |
||||
|
||||
test read_clv_file_not_found {Non-existent file returns empty} -body { |
||||
::punkboot::utils::read_changelog_latest_version /nonexistent/path/to/CHANGELOG.md |
||||
} -result "" |
||||
|
||||
test read_clv_empty_file {Empty file returns empty} -body { |
||||
set path [write_temp_file clvempty ""] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "" |
||||
|
||||
test read_clv_no_headers {File with no ## [X.Y.Z] headers returns empty} -body { |
||||
set content {# Changelog |
||||
|
||||
Some prose without a version header. |
||||
} |
||||
set path [write_temp_file clvnohdr $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "" |
||||
|
||||
test read_clv_extra_spaces {Extra whitespace after ## is tolerated} -body { |
||||
set content {## [0.5.0] - 2026-08-01} |
||||
set path [write_temp_file clvspc $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "0.5.0" |
||||
|
||||
test read_clv_prerelease_suffix {Version with pre-release suffix is captured} -body { |
||||
set content {## [0.2.1-rc1] - 2026-07-06} |
||||
set path [write_temp_file clvpre $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "0.2.1-rc1" |
||||
|
||||
test read_clv_single_hash {Single # header is not matched (only ##)} -body { |
||||
set content {# [0.1.0] this is a single hash} |
||||
set path [write_temp_file clv1h $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "" |
||||
|
||||
test read_clv_triple_hash {Triple ### subheader is not matched} -body { |
||||
set content {## [0.2.0] - 2026-07-05 |
||||
|
||||
### Fixed |
||||
|
||||
- something |
||||
} |
||||
set path [write_temp_file clv3h $content] |
||||
set result [::punkboot::utils::read_changelog_latest_version $path] |
||||
file delete -force [file dirname $path] |
||||
set result |
||||
} -result "0.2.0" |
||||
|
||||
tcltest::cleanupTests |
||||
} |
||||
@ -0,0 +1,98 @@
|
||||
# -*- tcl -*- |
||||
# Tests for punkboot::utils::vcs_dirty_warnings: |
||||
# - walk-up VCS root discovery, dirty/clean reporting, per-root dedupe via the |
||||
# caller's checkedroots dict, label prefixing, silence for unversioned or |
||||
# missing paths. |
||||
# Uses a scratch git repo fixture (git side); the fossil branch is symmetric |
||||
# code exercised end-to-end by make.tcl vendorupdate and the FOSSIL_HOME |
||||
# isolation pattern in modules/punk/mix/testsuites/repo/fossilmove.test. |
||||
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/** vcsdirty.test |
||||
|
||||
package require tcltest |
||||
package require punk::lib |
||||
package require punkboot::utils |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
|
||||
set gitcmd [auto_execok git] |
||||
tcltest::testConstraint git_available [expr {[llength $gitcmd] > 0}] |
||||
|
||||
variable fixture_error "" |
||||
variable gitroot "" |
||||
variable subdir "" |
||||
if {[tcltest::testConstraint git_available]} { |
||||
try { |
||||
set gitroot [punk::lib::tempdir_newfolder -prefix pkvcsdirtytest] |
||||
exec {*}$gitcmd -C $gitroot init -q |
||||
set subdir [file join $gitroot deep nested] |
||||
file mkdir $subdir |
||||
set fd [open [file join $subdir f.txt] w] |
||||
try { puts $fd "uncommitted content" } finally { close $fd } |
||||
} on error {result} { |
||||
set fixture_error $result |
||||
} |
||||
} |
||||
tcltest::testConstraint git_fixture [expr { |
||||
[tcltest::testConstraint git_available] && $fixture_error eq "" |
||||
}] |
||||
if {$fixture_error ne ""} { |
||||
puts stderr "vcsdirty.test fixture setup failed (tests will be skipped): $fixture_error" |
||||
} |
||||
|
||||
test vcsdirty-1.1 {dirty git repo produces one warning naming the root} -constraints git_fixture -body { |
||||
variable subdir |
||||
variable gitroot |
||||
set checked [dict create] |
||||
set warnings [::punkboot::utils::vcs_dirty_warnings $subdir checked] |
||||
list [llength $warnings] \ |
||||
[string match "WARNING: source project at [file normalize $gitroot] has uncommitted git changes*" [lindex $warnings 0]] |
||||
} -result {1 1} |
||||
|
||||
test vcsdirty-1.2 {label is prefixed into the warning} -constraints git_fixture -body { |
||||
variable subdir |
||||
set checked [dict create] |
||||
set warnings [::punkboot::utils::vcs_dirty_warnings $subdir checked someop] |
||||
string match "WARNING: someop source project at *" [lindex $warnings 0] |
||||
} -result 1 |
||||
|
||||
test vcsdirty-2.1 {checkedroots dict dedupes repeat reports of the same root} -constraints git_fixture -body { |
||||
variable subdir |
||||
variable gitroot |
||||
set checked [dict create] |
||||
set first [::punkboot::utils::vcs_dirty_warnings $subdir checked] |
||||
set second [::punkboot::utils::vcs_dirty_warnings $gitroot checked] |
||||
list [llength $first] [llength $second] |
||||
} -result {1 0} |
||||
|
||||
test vcsdirty-3.1 {unversioned path is silent} -constraints git_available -body { |
||||
set checked [dict create] |
||||
set dir [punk::lib::tempdir_newfolder -prefix pkvcsdirtynone] |
||||
set result [::punkboot::utils::vcs_dirty_warnings $dir checked] |
||||
file delete -force $dir |
||||
set result |
||||
} -result {} |
||||
|
||||
test vcsdirty-3.2 {missing path is silent} -constraints git_available -body { |
||||
set checked [dict create] |
||||
::punkboot::utils::vcs_dirty_warnings [file join [punk::lib::tempdir] no_such_dir_xyz_12345] checked |
||||
} -result {} |
||||
|
||||
test vcsdirty-4.1 {clean git repo is silent} -constraints git_fixture -body { |
||||
variable gitroot |
||||
set gitcmd [auto_execok git] |
||||
exec {*}$gitcmd -C $gitroot add -A |
||||
exec {*}$gitcmd -C $gitroot -c user.email=t@t -c user.name=t commit -q -m fixture |
||||
set checked [dict create] |
||||
::punkboot::utils::vcs_dirty_warnings $gitroot checked |
||||
} -result {} |
||||
|
||||
# cleanup fixture |
||||
variable gitroot |
||||
if {$gitroot ne "" && [file isdirectory $gitroot]} { |
||||
catch {file delete -force $gitroot} |
||||
} |
||||
} |
||||
|
||||
tcltest::cleanupTests |
||||
namespace delete ::testspace |
||||
@ -0,0 +1,13 @@
|
||||
|
||||
(7) Document the @placeholders@ available to the various C code fragments. |
||||
|
||||
(8) Note that we can use #define's to make the class and instance |
||||
types available as fixed named without resorting to placeholders, |
||||
and still keeping it in a local scope. |
||||
|
||||
--- |
||||
|
||||
idea: critcl::cproc -- api to register return types, argument types |
||||
|
||||
argument types: conversion from Tcl_Obj*, type checking (values) |
||||
return types: conversion to Tcl_Obj*, (type/value checking ?) |
||||
@ -0,0 +1,13 @@
|
||||
|
||||
(7) Document the @placeholders@ available to the various C code fragments. |
||||
|
||||
(8) Note that we can use #define's to make the class and instance |
||||
types available as fixed named without resorting to placeholders, |
||||
and still keeping it in a local scope. |
||||
|
||||
--- |
||||
|
||||
idea: critcl::cproc -- api to register return types, argument types |
||||
|
||||
argument types: conversion from Tcl_Obj*, type checking (values) |
||||
return types: conversion to Tcl_Obj*, (type/value checking ?) |
||||
Loading…
Reference in new issue