Browse Source

ARCHITECTURE.md: current-state system mechanics map + reference linter

New root ARCHITECTURE.md: descriptive, pointer-first map of boot/launch, interp/thread topology, console abstraction, module loading, build/provenance/kits and the test harness - own lane beside DOX (what you must do) and the goals system (what changed and why). In-flux subsystems documented as goal pointers only.

scriptlib/developer/architecture_lint.tcl validates the doc's reference contract: backticked repo paths exist, G-<id> references resolve against both goal indexes, 400-line size budget. Plain tclsh, no deps, mutation-tested.

Root AGENTS.md: Child DOX Index entry, Closeout item 5 (update-or-prune described mechanisms + run the linter), scriptlib/developer inventory.

Assisted-by: harness=opencode; primary-model=openrouter/moonshotai/kimi-k3; api-location=openrouter.ai
master
Julian Noble 2 weeks ago
parent
commit
804a322f7e
  1. 10
      AGENTS.md
  2. 113
      ARCHITECTURE.md
  3. 137
      scriptlib/developer/architecture_lint.tcl

10
AGENTS.md

@ -83,9 +83,10 @@ Default section order:
2. Update nearest owning docs and any affected parents or children 2. Update nearest owning docs and any affected parents or children
3. Refresh every affected Child DOX Index 3. Refresh every affected Child DOX Index
4. Remove stale or contradictory text 4. Remove stale or contradictory text
5. Re-check modified `.tm` files for new procs lacking PUNKARGS `argdoc` blocks 5. If the change altered a mechanism described in `ARCHITECTURE.md`, update or prune the affected section in the same change-set and run `tclsh scriptlib/developer/architecture_lint.tcl`
6. Run existing verification when relevant 6. Re-check modified `.tm` files for new procs lacking PUNKARGS `argdoc` blocks
7. Report any docs intentionally left unchanged and why 7. Run existing verification when relevant
8. Report any docs intentionally left unchanged and why
## User Preferences ## User Preferences
@ -195,12 +196,13 @@ The project version is fully independent of module versions. A module bump (even
- `.agents/skills/` — Project agent skills in the cross-framework Agent Skills SKILL.md format (read natively by pi, OpenCode, Codex and others). Canonical home; `.claude/skills/` holds byte-identical copies for Claude Code discovery - edit under `.agents/skills/` and re-copy, verify sync with a plain diff (no child AGENTS.md) - `.agents/skills/` — Project agent skills in the cross-framework Agent Skills SKILL.md format (read natively by pi, OpenCode, Codex and others). Canonical home; `.claude/skills/` holds byte-identical copies for Claude Code discovery - edit under `.agents/skills/` and re-copy, verify sync with a plain diff (no child AGENTS.md)
- `GOALS.md` — Top-level technical goal index, summary-only (ID, status, title, scope, detail pointer per goal); required read for non-trivial work (no child AGENTS.md; the file documents its own format, read workflow, and the agent goal-authoring workflow) - `GOALS.md` — Top-level technical goal index, summary-only (ID, status, title, scope, detail pointer per goal); required read for non-trivial work (no child AGENTS.md; the file documents its own format, read workflow, and the agent goal-authoring workflow)
- `GOALS-archive.md` — Summary records of achieved goals moved out of the active index (historical context only) - `GOALS-archive.md` — Summary records of achieved goals moved out of the active index (historical context only)
- `ARCHITECTURE.md` - Descriptive current-state map of punkshell system mechanics (boot/launch, interp/thread topology, console abstraction, module loading, build/provenance/kits, test harness). Own lane beside DOX and the goals system: AGENTS.md files say what you must do, GOALS.md/goals say what changed and why, ARCHITECTURE.md says how it works now. Read for orientation before non-trivial work in an unfamiliar subsystem. Pointer-first and current-state only - never transcribe content owned by another doc; keep sections durable and thin. Verified by `tclsh scriptlib/developer/architecture_lint.tcl` (referenced paths exist, goal references resolve, size budget) - run it after editing the file (no child AGENTS.md; the file's front matter carries its own contract)
- `goals/` — Per-goal detail files carrying the canonical Goal/Acceptance contract prose plus supporting detail (see goals/AGENTS.md) - `goals/` — Per-goal detail files carrying the canonical Goal/Acceptance contract prose plus supporting detail (see goals/AGENTS.md)
- `goals/archive/` — Detail files for achieved/archived goals - `goals/archive/` — Detail files for achieved/archived goals
- `TEMP_REFERENCE/` - Reference-only material for agents, not a work folder; ignored by both git and fossil, so its contents (including its AGENTS.md) are machine-local and unversioned. Holds the nested fossil checkout of the Tcl core sources at `TEMP_REFERENCE/tcl9`, which must stay on `trunk` per the fossil branch-discipline rule in User Preferences (see TEMP_REFERENCE/AGENTS.md when present) - `TEMP_REFERENCE/` - Reference-only material for agents, not a work folder; ignored by both git and fossil, so its contents (including its AGENTS.md) are machine-local and unversioned. Holds the nested fossil checkout of the Tcl core sources at `TEMP_REFERENCE/tcl9`, which must stay on `trunk` per the fossil branch-discipline rule in User Preferences (see TEMP_REFERENCE/AGENTS.md when present)
- Directories agents should not directly modify (no child DOX needed): - Directories agents should not directly modify (no child DOX needed):
- `callbacks/` — Experimental shellspy features, user-only - `callbacks/` — Experimental shellspy features, user-only
- `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/<name>`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only. - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/<name>`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification, `architecture_lint.tcl` validating root `ARCHITECTURE.md` references - path existence, goal-id resolution, size budget); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only.
- `bin/` — Built binaries and helpers, build output target. This includes the polyglot `.cmd` launcher/utility scripts (e.g `bin/punk-runtime.cmd`): they are GENERATED by the punk::mix scriptwrap machinery from sources under `src/scriptapps/` — a request to "fix bin/<name>.cmd" means editing `src/scriptapps/<name>.*` + `<name>_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output - `bin/` — Built binaries and helpers, build output target. This includes the polyglot `.cmd` launcher/utility scripts (e.g `bin/punk-runtime.cmd`): they are GENERATED by the punk::mix scriptwrap machinery from sources under `src/scriptapps/` — a request to "fix bin/<name>.cmd" means editing `src/scriptapps/<name>.*` + `<name>_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output
- `modules/` (root) — Build output target for `tclsh src/make.tcl modules` - `modules/` (root) — Build output target for `tclsh src/make.tcl modules`
- `lib/` (root) — Build output target for `tclsh src/make.tcl libs` - `lib/` (root) — Build output target for `tclsh src/make.tcl libs`

113
ARCHITECTURE.md

@ -0,0 +1,113 @@
# ARCHITECTURE.md - Punkshell System Mechanics
## About this document
This file is the descriptive, current-state map of how punkshell fits together. It occupies its own lane alongside the project's other documentation:
- `AGENTS.md` files (root + children) are binding work contracts: what you must do when working here.
- `GOALS.md`, `GOALS-archive.md` and `goals/` track intended and landed changes with rationale: what changed and why.
- This file says how the system works now.
Rules that keep it useful instead of rotten:
1. **Pointer-first.** Sections state durable mechanics briefly and route to canonical sources (module files, owning AGENTS.md, goal detail files). Never transcribe content that lives elsewhere - point at it. If a section wants to grow past a screenful, the detail belongs in the module or a child doc, with this section pointing at it.
2. **Current state only.** History, rejected alternatives and verification records stay in goal detail files; release-level history stays in `CHANGELOG.md`. Do not narrate change history here.
3. **Staleness.** When this file conflicts with the code, the code wins and this file is stale. Update or delete the stale section in the same change that breaks it (root `AGENTS.md` Closeout). Delete stale sections rather than annotating them.
4. **In-flux areas.** Subsystems under active redesign are documented as one-liners with goal pointers, not detailed prose (see "In-flux areas").
5. **Verification.** `tclsh scriptlib/developer/architecture_lint.tcl` - backtick-quoted repo paths must exist, `G-<id>` references must resolve against the goal indexes, size budget enforced. Run after editing this file.
## The ten-thousand-foot view
Punkshell is an alternative Tcl shell distributed as self-contained executables ("kits"): a zipfs/metakit-aware Tcl runtime wrapped with a virtual filesystem (VFS) payload carrying the punk module set. A kit boots through a single entry script, which selects a package mode (which module sources to trust) and a subcommand (which application to run: interactive shell, non-interactive script runner, stock tclsh emulation, and others). The interactive shell is a REPL in the main interp that evaluates user code in a separate code thread and talks to the terminal through a two-layer console abstraction. Everything is built, tested and released by a Tcl-driven build system (`src/make.tcl`) with file-level provenance tracking (`punkcheck`).
```
bin/<kit>.exe = Tcl runtime (zipfs/metakit) + <name>.vfs payload
| boots
v
src/vfs/_config/punk_main.tcl
- package mode: dev / os / src / internal (which module sources to trust)
- subcommand: shell / script / punk / tclsh / shellspy
|
v
app package from src/lib/ (app-punkshell, app-punkscript, app-repl, app-shellspy)
|
v
+------------------- interactive shell --------------------+
| REPL interp (punk::repl) <--> console abstraction |
| + codethread (user code) (punk::console over |
| + shellthread workers opunk::Console) |
+----------------------------------------------------------+
```
## Boot and launch chain
- **Kit anatomy.** Each `src/vfs/*.vfs` folder is a runtime payload; `src/runtime/mapvfs.config` maps payload folders to platform runtimes under `bin/runtime/<platform>/`; `src/vfs/_vfscommon.vfs/` is a generated merge of common libraries (never hand-edited); `src/vfs/_config/` holds the entry scripts. Sources: `src/vfs/AGENTS.md`, `src/runtime/AGENTS.md`.
- **Entry point.** `src/vfs/_config/punk_main.tcl`. An optional first argument is a dash-delimited package mode composed of tokens from `dev`, `os`, `src`, `internal` (`internal` is always appended when absent). The next argument is the subcommand: `shell`, `script`, `punk`, `tclsh`, `shellspy`. Any other first argument is treated as a script invocation; no arguments at all means interactive shell. Subcommands map to app packages under `src/lib/` (`app-punkshell`, `app-punkscript`, `app-repl`, `app-shellspy`).
- **Package modes pick module provenance.** `internal` uses module snapshots baked into the kit at build time; `dev` loads build outputs from repo-root `modules/` and `lib/`; `src` loads the unbuilt working tree (`src/modules`, `src/lib`, `src/bootsupport`, `src/vendormodules`) with `package prefer latest` so magic-version dev modules (`999999.0a1.0`) beat stamped snapshots. `src` mode is the standard way to verify working-tree changes without a rebuild. Source: `bin/AGENTS.md` "Launch package modes".
- **Static package capture (G-058, achieved).** Boot probe-loads the runtime's statically linked packages in a throwaway interp and records what each provides; every interp the shell fabricates (boot, codethread, shellthread workers) seeds `package ifneeded <name> <ver> {load {} <prefix>}` from that record, and `punk::packagepreference` resolves static-vs-bundled version-aware. Sources: `src/vfs/AGENTS.md`, `goals/archive/G-058-static-runtime-packages.md`.
- **`script` subcommand (G-015, achieved).** The lean non-interactive path: default punk shell module/alias environment, no shellfilter stacks/transforms, honest exit codes, launch plumbing emits nothing on stdout/stderr (exec-style callers see only the script's own output). Supports `lib:<name>` scriptlib resolution. App package: `src/lib/app-punkscript/`.
- **`bin/` launch surface.** Built kits plus polyglot `.cmd` utilities (`punk-runtime`, `punk-getzig`, `punk-tclargs`, `dtplite`) generated from `src/scriptapps/` sources by the `punk::mix` scriptwrap machinery - outputs are never hand-edited. `bin/punk-runtime.cmd` fetches/lists/selects plain runtimes under `bin/runtime/<platform>/` from the punkbin artifact server with sha1 verification. Source: `bin/AGENTS.md`.
## Interp and thread topology
- **REPL interp vs codethread.** `punk::repl` runs the reader/prompt loop in the main interp; user commands evaluate in a separate thread's interp (`src/modules/punk/repl/codethread-999999.0a1.0.tm`) so the reader stays responsive and user-code state is isolated from shell state.
- **shellfilter stacks.** Channel transform/logging layers. Among other roles, `var` JUNCTION stacks divert the code interp's stdout/stderr into per-run variables that the repl collects and emits to the console after each run (G-001). The API is known-clumsy; audit is proposed as G-090. Source: `src/modules/shellfilter-999999.0a1.0.tm`.
- **shellthread workers.** Pooled worker threads (logging, tee handlers). tcludp 1.0.12's Windows per-thread exit handler closed process-global events and wedged worker event loops (G-036, achieved); kits now bundle tcludp >= 1.0.13 and a runtime `has_bug`-style check surfaces the vulnerable combination. Worker lifecycle audit is also G-090. Source: `src/modules/shellthread-999999.0a1.0.tm`.
- **Shared state.** Infrastructure `tsv` arrays are `punk_`-prefixed by contract (legacy unprefixed arrays predate the rule); console facts, ownership and mode caches live in tsv so every thread sees one truth (G-007). Source: `src/modules/AGENTS.md`.
- **Subshells.** Named secondary repls exist; the substantial subshell roadmap is in-flux - see "In-flux areas".
## Console abstraction
Two layers with a deliberate dependency direction (the class never depends on the integrating layer):
- **`opunk::Console`** (`src/modules/opunk/`) - a `voo` value-OO base class modelling a console as an in/out pair with settled capability facts. Objects are plain Tcl list values; `-virtual` methods dispatch on a class tag so subclasses can override everything channel-related. Cooperative seams are pluggable: the probe-byte store (`waiting_chunks_arrayvar`), the size-query provider (`size_query_provider`), and an anchor lifecycle callback. Source: `src/modules/opunk/AGENTS.md`.
- **`punk::console`** (`src/modules/punk/console-999999.0a1.0.tm`) - the integrating layer: console-spec resolution, per-console terminal-property facts, raw mode, ANSI emit/query helpers, and registration of the providers the base class consumes.
- **Spec forms and selection.** A console spec is an `{in out}` channel pair, an anchored instance name, or an `::opunk::Console` object value, normalised by `punk::console::console_spec_resolve`. `repl::init -console <spec>` selects the console per repl (G-001, achieved); the default is the process console `{stdin stdout}`. Source: `src/modules/punk/AGENTS.md`.
- **Backends (G-001, achieved).** Reference subclasses `opunk::console::test`, `::ssh`, `::tk` (deterministic test double, socket-carried terminal, Tk text widget) prove non-detectable terminal-like devices can host a repl with no base-class or `punk::console` changes. Detail: `goals/archive/G-001-pluggable-console-backends.md`.
- **Location transparency (G-007, achieved).** Facts are tsv-backed and terminal queries are owner-routed: a query from any thread forwards to the console-owning thread through a `thread::send` choke point, so code-interp code sees the same console the repl sees without `repl eval` round-trips. Ownership lives in a tsv registry keyed by canonical channel pair.
- **Raw mode.** twapi on Windows when available; otherwise a persistent powershell console-mode named-pipe server serves raw transitions on twapi-less runtimes (G-106, achieved) - quiet, lazy-started, exits with the owning process.
- **Dead-console handling.** The Tcl 9 Windows console driver never reported a killed host terminal to the script, leaving orphaned shells spinning a core (G-039, achieved; upstream ticket filed and fixed). The repl arms a close-on-probe-failure watchdog only on runtimes a `punk::lib::check` classifier reports as affected (G-076, achieved); runtimes at or past 9.0.5 deliver the dead console as a readable event and use the normal eof teardown.
## Module ecosystem and package loading
- **Editable modules** live in `src/modules/` (+ `src/modules_tcl8/`, `src/modules_tcl9/` siblings); filenames carry the magic dev version `999999.0a1.0` with the real semver in a `<name>-buildversion.txt` sidecar, and namespaces mirror directory layout (`punk/ansi/` = `punk::ansi`). `punk::libunknown` is the manually-versioned exception. Source: `src/modules/AGENTS.md`.
- **`punk::args` is both parser and documentation system.** `PUNKARGS`/`argdoc` blocks travel with each proc and power inline usage tables (`i <command>` at the repl); even manually-parsing procs carry documentation-only definitions. Much of the goals-era work has been hardening this system - see the `punkargs` entries across `GOALS.md` and `GOALS-archive.md`.
- **Discovery and modpods.** `punk::libunknown` provides module discovery/registration beyond `tcl::tm` defaults; `#modpod-*` source directories pack into zip-based multi-file `.tm` modules at build time (src mode registers them via an inline boot scanner written in builtins).
- **Bootsupport.** `src/bootsupport/` holds snapshot copies of build-time-critical modules (manifest: `include_modules.config`), analogous to devDependencies. `make.tcl` classifies snapshot staleness for five runtime-critical packages (`punkcheck`, `punk::repo`, `punk::mix`, `punk::tdl`, `punk::args`) as abort/prompt/warn from the version delta. Source: `src/bootsupport/AGENTS.md`.
- **opunk layer.** `src/modules/opunk/` explores `voo`-based value-OO reimplementations (objects as plain Tcl values); the `punk::*` modules remain the production implementations.
- **Vendored code.** Third-party packages live in `src/vendormodules/` and `src/vendorlib/` (+ `_tcl8`/`_tcl9` siblings), refreshed by `make.tcl vendorupdate` from config; never hand-edited (enforcement policy is proposed as G-026).
## Build, provenance and kits
- **`src/make.tcl` is the single build entry.** Key subcommands: `modules`/`libs`/`packages` (repo-root build outputs), `vfscommonupdate` (regenerate `_vfscommon.vfs`), `vfs`, `bin`, `project` (full kit build into `bin/`), `bootsupport` (snapshot refresh), `vendorupdate`, `check`, `workflow`, `projectversion`. Dispatch and help dogfood `punk::args` (G-030, achieved) and degrade to plain scan/help when bootsupport `punk::args` is stale (`PUNKBOOT_PLAIN=1` forces the degraded mode). Source: `src/AGENTS.md`.
- **punkcheck** records every install/delete as events in per-folder `.punkcheck` directories - the basis for skip/copy change detection, superseded-module pruning and provenance. Single OO record lifecycle (G-094, achieved) with atomic saves and advisory event-scoped locking for concurrent writers (G-095, achieved).
- **Provenance gates.** Build/promotion commands warn on uncommitted `src/` changes (column-0 `PROVENANCE-WARNING:` token, `-dirty-abort` for strict mode); `vendorupdate` warns for dirty source-project checkouts.
- **Buildsuites and the kit family.** `src/buildsuites/suite_tcl90/` builds Tcl/Tk/tcllib from source with a pinned zig toolchain and produces the runtime kit family (plain / punk / bi) plus artifact metadata (G-096-G-117 era: see archived goals G-096, G-098, G-102, G-103, G-107); artifacts publish to the punkbin repo that `bin/punk-runtime.cmd` fetches from. The 8.6 family and kit container strategy are in-flux - see "In-flux areas".
- **Project generation.** `dev project.new` composes thin layouts from `src/project_layouts/` (overlay chain with `.anti` deletion markers and `name@base` derivation - G-087, achieved) and injects bootsupport modules from the generating shell at generation time.
- **Workflow overview.** `tclsh src/make.tcl workflow` prints the embedded ASCII data-flow overview of the build/release pipeline, with its own update contract in `src/AGENTS.md`. This section deliberately summarises rather than copies it.
## Test harness
- **`src/tests/runtests.tcl` is the entry point.** Path-glob discovery over `testsuites/` with the `X/***`-style lattice (`-include-paths`/`-exclude-paths`, G-093), single-process or multi-process child modes, a `-jobs N` worker pool (G-091), report modes (compact/markdown/json) and an optional UDP watch stream. Source: `src/tests/AGENTS.md`.
- **Characterization style.** Suites pin observed current behaviour, including `_GAP_`-named tests that pin known bugs and flip when a goal fixes them - goal acceptance criteria routinely name the pins they flip.
- **Child-process suites by design.** A repl cannot run inside the shared testinterp (codethread exit callbacks target the thread's main interp), so console/repl verification drives child processes (`src/tests/testsupport/repl_console_driver.tcl`); kit and launcher behaviour is likewise exec-driven under `src/tests/shell/`.
- **Mode parity.** `scriptlib/developer/runtests_parity.tcl` compares `-report json` outputs across runner modes.
## In-flux areas
Mechanics here are documented as pointers only - detailed prose would be obsolete on arrival. Read the goal detail files for intended end states and the code for current state.
- Subshell roadmap: G-002, G-003, G-008, G-009, G-010 (non-nested subshells, resource limits, scoped console state, themed profiles, tree navigation).
- Input mode and completion: G-013 (raw-mode default), G-044 (punk::args-driven completion).
- Stored configuration and help: G-014 (punk::config toml), G-042 (subshell help topics), G-043 (subshell definition plugins).
- Console err channel: G-011.
- Launch surface: G-023 (version-named binaries), G-033 (`proj:` package mode), G-077 (`-e` one-liners), G-089 (scriptlib in kits), G-112 (make.tcl subcommand rename), G-118 (tclsh subcommand review).
- 8.6 buildsuite family: G-099, G-100, G-101.
## Doc map - which lane holds what
- How it works now (durable mechanics): this file.
- What you must do when working here: root `AGENTS.md` + the child AGENTS.md chain (binding).
- What changed and why: `GOALS.md`, `GOALS-archive.md`, `goals/` detail files; `CHANGELOG.md` for release-level history.
- Per-command and per-proc usage: PUNKARGS blocks in the modules (`i <command>` at a repl); generated docs under `src/doc/`.
- User-facing features: `README.md`.

137
scriptlib/developer/architecture_lint.tcl

@ -0,0 +1,137 @@
#!/usr/bin/env tclsh
# architecture_lint.tcl - lint the root ARCHITECTURE.md descriptive map
#
# Validates the front-matter contract declared in ARCHITECTURE.md:
# - every backtick-quoted repo-relative path reference exists
# (anchored tokens only - see $path_anchors / $root_files below;
# tokens with glob/placeholder characters are patterns, not paths)
# - every G-<id> reference resolves to a goal in GOALS.md or
# GOALS-archive.md
# - size budget: the file stays a cheap full read (<= $max_lines lines)
# - line 1 is a '# ' title
#
# Plain tclsh (8.6+ or 9), no package dependencies - runnable from any checkout:
# tclsh scriptlib/developer/architecture_lint.tcl [repo-root]
# Exit 0 when clean; exit 1 with one 'path: problem' line per finding.
set root [expr {$argc >= 1 ? [lindex $argv 0] : [file dirname [file dirname [file dirname [file normalize [info script]]]]]}]
set archfile [file join $root ARCHITECTURE.md]
if {![file isfile $archfile]} {
puts stderr "architecture_lint: '$root' does not look like the punkshell repo root (need ARCHITECTURE.md)"
exit 2
}
set findings {}
proc flag {path msg} {
global findings
lappend findings "$path: $msg"
}
proc read_lines {path} {
set f [open $path r]
fconfigure $f -encoding utf-8
set lines [split [read $f] \n]
close $f
return $lines
}
set max_lines 400
set path_anchors {src/ bin/ goals/ scriptlib/ .fossil-settings/}
set root_files {AGENTS.md ARCHITECTURE.md GOALS.md GOALS-archive.md CHANGELOG.md README.md LICENSE.txt punkproject.toml CLAUDE.md tclint.toml}
# --- collect goal ids from both indexes --------------------------------------
set goalids [dict create]
foreach indexname {GOALS.md GOALS-archive.md} {
set indexpath [file join $root $indexname]
if {![file isfile $indexpath]} {
flag $indexname "goal index missing - cannot resolve G-<id> references"
continue
}
foreach line [read_lines $indexpath] {
if {[regexp {^### G-(\d+) \[} $line -> id]} {
dict set goalids $id 1
}
}
}
# --- classify one backtick token ----------------------------------------------
# returns: skip | file | dir
proc classify_token {tok} {
global path_anchors root_files
# patterns and placeholders are not literal paths
if {[regexp {[*<>$%\\ ()`;:'",]} $tok]} {return skip}
if {[string index $tok 0] eq "-"} {return skip}
set anchored 0
foreach anchor $path_anchors {
if {[string match "$anchor*" $tok]} {
set anchored 1
break
}
}
if {!$anchored && $tok ni $root_files} {return skip}
if {[string index $tok end] eq "/"} {return dir}
return file
}
# --- run ----------------------------------------------------------------------
set lines [read_lines $archfile]
set nlines [llength $lines]
if {$nlines > $max_lines} {
flag ARCHITECTURE.md "$nlines lines exceeds the $max_lines-line budget - the file must stay a cheap full read; move detail into owning docs and point at it"
}
if {![regexp {^# } [lindex $lines 0]]} {
flag ARCHITECTURE.md "line 1 is not a '# ' title"
}
# path references (unique; first-mention line reported)
set seen_paths [dict create]
# goal references (unique; first-mention line reported)
set seen_goals [dict create]
set lineno 0
foreach line $lines {
incr lineno
foreach {- tok} [regexp -all -inline {`([^`\n]+)`} $line] {
set kind [classify_token $tok]
if {$kind eq "skip"} continue
if {![dict exists $seen_paths $tok]} {
dict set seen_paths $tok [list $kind $lineno]
}
}
foreach gid [regexp -all -inline {G-\d+} $line] {
if {![dict exists $seen_goals $gid]} {
dict set seen_goals $gid $lineno
}
}
}
dict for {tok info} $seen_paths {
lassign $info kind lineno
if {$kind eq "dir"} {
set tok [string range $tok 0 end-1]
}
set full [file join $root $tok]
if {$kind eq "dir"} {
if {![file isdirectory $full]} {
flag ARCHITECTURE.md "line $lineno: referenced directory '$tok/' does not exist"
}
} else {
if {![file exists $full]} {
flag ARCHITECTURE.md "line $lineno: referenced path '$tok' does not exist"
}
}
}
dict for {gid lineno} $seen_goals {
set num [string range $gid 2 end]
if {![dict exists $goalids $num]} {
flag ARCHITECTURE.md "line $lineno: $gid does not resolve to a goal in GOALS.md or GOALS-archive.md"
}
}
if {[llength $findings]} {
puts [join $findings \n]
puts stderr "architecture_lint: [llength $findings] finding(s)"
exit 1
}
puts "architecture_lint: clean ([dict size $seen_paths] path refs, [dict size $seen_goals] goal refs, $nlines lines)"
exit 0
Loading…
Cancel
Save