From 804a322f7e325f0e6a0972f7f33c7906757855c7 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 23 Jul 2026 13:19:19 +1000 Subject: [PATCH] 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- 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 --- AGENTS.md | 10 +- ARCHITECTURE.md | 113 ++++++++++++++++++ scriptlib/developer/architecture_lint.tcl | 137 ++++++++++++++++++++++ 3 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 scriptlib/developer/architecture_lint.tcl diff --git a/AGENTS.md b/AGENTS.md index 28c6ae91..7c645f4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,9 +83,10 @@ Default section order: 2. Update nearest owning docs and any affected parents or children 3. Refresh every affected Child DOX Index 4. Remove stale or contradictory text -5. Re-check modified `.tm` files for new procs lacking PUNKARGS `argdoc` blocks -6. Run existing verification when relevant -7. Report any docs intentionally left unchanged and why +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. Re-check modified `.tm` files for new procs lacking PUNKARGS `argdoc` blocks +7. Run existing verification when relevant +8. Report any docs intentionally left unchanged and why ## 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) - `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) +- `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/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) - Directories agents should not directly modify (no child DOX needed): - `callbacks/` — Experimental shellspy features, user-only - - `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `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/`); 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/.cmd" means editing `src/scriptapps/.*` + `_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output - `modules/` (root) — Build output target for `tclsh src/make.tcl modules` - `lib/` (root) — Build output target for `tclsh src/make.tcl libs` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..2997e9ab --- /dev/null +++ b/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-` 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/.exe = Tcl runtime (zipfs/metakit) + .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//`; `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 {load {} }` 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:` 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//` 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 ` 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 `-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 ` 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 ` at a repl); generated docs under `src/doc/`. +- User-facing features: `README.md`. diff --git a/scriptlib/developer/architecture_lint.tcl b/scriptlib/developer/architecture_lint.tcl new file mode 100644 index 00000000..2a434a82 --- /dev/null +++ b/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- 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- 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