From db4970d8db7254fcf9cc82d2a1d6ac84873787cb Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Tue, 11 Aug 2026 19:36:36 -0400 Subject: [PATCH 1/2] docs(issues): file 185-188 - upstream findings from the languages-ng port Migrated from the languages-ng repo's dev-docs/issues, where they were filed with `target: ourPLCC/plcc-ng` and held pending go-ahead. Rewritten from this repo's perspective and re-verified against current src/ rather than the installed CLI: - 185 plcc-rep parses each SOURCE independently (docs) - 186 plcc-rep deadlocks on a partial stdout line (fix) - 187 plcc-rep lacks output and clean-exit record kinds (feat) - 188 FOLLOW set omits the nullable tail (fix) The other four upstream-targeted issues in that repo (003, 004, 006, 010) already landed here as 162, 163, 164, and 174. Co-Authored-By: Claude Opus 5 --- dev-docs/issues/.next-id.txt | 2 +- ...85-rep-parses-each-source-independently.md | 75 +++++++++++ ...86-rep-deadlocks-on-partial-stdout-line.md | 84 +++++++++++++ ...rep-lacks-output-and-clean-exit-records.md | 99 +++++++++++++++ .../188-follow-set-omits-nullable-tail.md | 116 ++++++++++++++++++ dev-docs/roadmap.md | 11 ++ 6 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 dev-docs/issues/185-rep-parses-each-source-independently.md create mode 100644 dev-docs/issues/186-rep-deadlocks-on-partial-stdout-line.md create mode 100644 dev-docs/issues/187-rep-lacks-output-and-clean-exit-records.md create mode 100644 dev-docs/issues/188-follow-set-omits-nullable-tail.md diff --git a/dev-docs/issues/.next-id.txt b/dev-docs/issues/.next-id.txt index 725a5ba2..6c412452 100644 --- a/dev-docs/issues/.next-id.txt +++ b/dev-docs/issues/.next-id.txt @@ -1 +1 @@ -185 +189 diff --git a/dev-docs/issues/185-rep-parses-each-source-independently.md b/dev-docs/issues/185-rep-parses-each-source-independently.md new file mode 100644 index 00000000..0099510c --- /dev/null +++ b/dev-docs/issues/185-rep-parses-each-source-independently.md @@ -0,0 +1,75 @@ +# 185 - plcc-rep parses each SOURCE independently, unlike PLCC's `rep` + +**Type:** docs +**Date:** 2026-08-11 + + + +## Description + +`plcc-rep` parses each `SOURCE` argument as an independent token stream. +Old PLCC's `rep` joined its file arguments into a single stream, so a +program split across two files parsed as one program. Under plcc-ng the +same invocation fails. + +This is not a defect — per-source parsing is the design issue +[#008](done/008-parse-multi-program-streaming.md) settled on ("Each source +is parsed independently, producing one or more trees per source"), and +[`SourceRunner.run`](../../src/plcc/cmd/source_runner.py) implements it +uniformly for `plcc-scan`, `plcc-parse`, and `plcc-rep`. But it is an +undocumented breaking change: [docs/migration.md](../../docs/migration.md)'s +command table maps `rep [-t] [-n] [file...]` → `plcc-rep [file...]` with an +empty notes column, which reads as "same behavior, new name." + +Splitting one program across files is a real course-material pattern — an +exercise that hands students a partial program in one file and the +remainder in another — so this silently changes what an existing +demonstration does, and does it with a parse error rather than a diagnosis. + +## Steps to Reproduce + +1. With a spec for a simple expression language, put `+(3` in `p1` and + `,4)` in `p2`: + + ``` + $ plcc-rep p1 p2 + plcc-parser-table: -:1:3: error: expected 'RPAREN', got end of file + plcc-parser-table: -:1:1: error: unexpected 'COMMA', no production for 'Program' + ``` + +2. Concatenating first works as expected: + + ``` + $ cat p1 p2 | plcc-rep + 7 + ``` + +## Notes + +The minimum fix is documentation: add a **Breaking behavior changes** entry +and fill in the `rep` row's notes column in +[docs/migration.md](../../docs/migration.md), stating that each `SOURCE` is +its own token stream and that `cat f1 f2 | plcc-rep` reproduces the old +behavior. Issue +[#159](done/159-migration-guide-missing-breaking-changes-callout.md) is the +precedent for that shape. + +Worth deciding at the same time whether the divergence is wanted at all for +`plcc-rep` specifically. `plcc-scan` and `plcc-parse` have a clear reason to +keep sources separate — their output is per-source records carrying a +`source` field. `plcc-rep`'s output is the *program's* result, where "these +files are one program" is at least as plausible a reading as "these files +are separate programs." If joining is the wanted behavior, it is a `feat` on +top of this docs entry, not a replacement for it: the divergence has already +shipped and still needs a migration-guide note. + +Found while migrating a set of course languages to plcc-ng, where the +affected example is a two-file program built exactly this way. diff --git a/dev-docs/issues/186-rep-deadlocks-on-partial-stdout-line.md b/dev-docs/issues/186-rep-deadlocks-on-partial-stdout-line.md new file mode 100644 index 00000000..c2211bd0 --- /dev/null +++ b/dev-docs/issues/186-rep-deadlocks-on-partial-stdout-line.md @@ -0,0 +1,84 @@ +# 186 - plcc-rep deadlocks on a partial stdout line + +**Type:** fix +**Date:** 2026-08-11 + + + +## Description + +A semantic action that writes a partial line (no trailing newline) to +stdout deadlocks `plcc-rep` with no diagnostic: no stdout, no stderr, no +exit. + +`plcc-rep` runs the generated program as a subprocess and treats its stdout +as a private, line-oriented JSON channel. +[`_read_response`](../../src/plcc/cmd/rep.py) reads one line at a time; a +line that fails to parse as JSON (or parses to something without a `kind`) +is printed verbatim and the loop continues, waiting for the next line: + +```python +line = raw.decode('utf-8', errors='replace').rstrip('\n') +try: + record = json.loads(line) +except json.JSONDecodeError: + print(line) + continue +``` + +That works when the stray text ends in a newline — the result record +arrives intact on the next `readline()`. It does not work for a partial +line: the unterminated text merges with the following JSON result line into +one unparseable line, which is printed (destroying the result record), and +`readline()` then blocks forever waiting for a result that will never come. + +Measured in both the Python and JavaScript targets, via stdin and via a +`SOURCE` file: `timeout` reports exit 124, with no stdout and no stderr — +the worst available failure mode, since nothing tells the caller what +happened. A student who puts a `print` (or the target-language equivalent) +in a semantic action and forgets the trailing newline gets a hang with no +message, indistinguishable from an infinite loop in their own program. + +Note that the newline-terminated case survives only through the +unparseable-line fallback above, which is an accident of the implementation +rather than a supported output channel. It happens to print the raw line +before the real result, which fakes the interleaving a language's `display` +would want. + +## Steps to Reproduce + +1. A spec whose semantic action for some expression writes a partial line, + e.g. `sys.stdout.write("7")` with no trailing `\n`. +2. `echo '' | timeout 5 plcc-rep` +3. Actual: exit 124, no output at all. Expected: either the partial write is + surfaced, or `plcc-rep` reports the malformed channel — anything other + than hanging silently. + +## Notes + +The narrow fix is for `_read_response` to stop treating an unbounded +`readline()` as acceptable: read incrementally, or bound the wait, or at +minimum detect that the subprocess has gone idle with unterminated data +buffered and report it. Whatever the mechanism, the requirement is that a +partial-line write produce a message rather than a hang. + +The wider fix is issue [#187](187-rep-lacks-output-and-clean-exit-records.md): +give semantic actions a supported `output` record kind, so nothing they emit +travels as raw stdout in the first place. That would close this off at the +source, but the defensive handling here is still worth having on its own — +user code can always write to stdout directly. + +Found while migrating a course language whose `display`, `display#`, `putc`, +`puts`, and `newline` primitives are partial-line writers by design (a +trailing newline is exactly what its separate `newline` expression is for). +That port works around this by buffering all output and returning it as part +of `_run()`'s result rather than writing it directly — a workaround with its +own cost, described in [#187](187-rep-lacks-output-and-clean-exit-records.md). diff --git a/dev-docs/issues/187-rep-lacks-output-and-clean-exit-records.md b/dev-docs/issues/187-rep-lacks-output-and-clean-exit-records.md new file mode 100644 index 00000000..7dd06621 --- /dev/null +++ b/dev-docs/issues/187-rep-lacks-output-and-clean-exit-records.md @@ -0,0 +1,99 @@ +# 187 - plcc-rep lacks output and clean-exit record kinds + +**Type:** feat +**Date:** 2026-08-11 + + + +## Description + +Semantic actions have no supported way to emit user-visible output, and no +way to end the session cleanly. Both are missing record kinds in +`plcc-rep`'s protocol. + +[`_render_record`](../../src/plcc/cmd/rep.py) already dispatches on +`record['kind']` — `result`, `error`, `specification_error`, and anything +else is a hard error — so the shape of the fix is a new record kind handled +there, plus a hook in each target runtime that lets `_run()` emit it. Two +gaps this would close: + +1. **Output.** There is no record kind for "the program printed this." + Writing directly to stdout collides with `plcc-rep`'s use of stdout as + its own JSON channel, and hangs outright on a partial line (issue + [#186](186-rep-deadlocks-on-partial-stdout-line.md)). The only way out + today is to buffer everything a semantic action would have printed and + fold it into `_run()`'s returned value. An `output` record kind, emitted + as each write happens rather than buffered and flushed at the end, would + let output interleave with results the way old PLCC's `rep` did, and + would remove the need for that workaround entirely. + +2. **Clean exit.** There is no record kind for "the program is + intentionally done." A language with an `exit` expression calls the + target language's process-exit function, which — because `plcc-rep` runs + the generated program as a subprocess — closes the pipe mid-protocol. + Measured: + + ``` + $ printf 'display 1\nexit\ndisplay 2\n' | plcc-rep + 1nil # stdout + plcc-rep: interpreter exited unexpectedly # stderr + $ echo $? + 1 + ``` + + Under old PLCC, `rep` *was* the process, so this was a clean quit with + status 0. Under plcc-ng a deliberate quit reads as a crash: wrong stderr + message, wrong exit status, for want of a `session-end` (or similar) + record the subprocess could emit before exiting. + +### Second symptom: the buffering workaround swallows output when evaluation raises + +Measured on the Python target, in a port using the buffered-output +workaround from (1): + +``` +$ printf '{display 1 ; error "boom"}\n' | plcc-rep +[98,111,111,109] +``` + +The `1` is gone. Old PLCC's `System.out.print` had already written it to +stdout by the time the exception was thrown; a buffer standing in for +stdout is discarded along with the statement. A definition whose right-hand +side raises loses its output the same way. + +That is not a defect in the workaround — it is what the workaround costs. +Buffering is only necessary because there is no supported channel, and the +one alternative that would preserve the output (writing to stdout as it is +produced) is exactly what deadlocks the tool. An `output` record kind +removes the trade-off: each write emits its record the moment it runs, so an +error later in the same statement cannot retroactively swallow it, and +nothing has to be held until `_run` returns. + +It is worth recording as a distinct symptom because it changes observable +language behavior, where the deadlock only changes failure mode. + +## Notes + +Both additions are protocol changes, so they touch every target runtime +(Python, Java, JavaScript, Haskell) as well as `_render_record`, and they +belong in [docs/language-guide/](../../docs/language-guide/) once the +record kinds are documented. `_wait_for_ready`'s `ready` record is the +precedent for a non-`result` kind flowing across the same channel. + +Cross-links issue [#186](186-rep-deadlocks-on-partial-stdout-line.md), the +partial-line deadlock this same addition would also close off, since output +would travel through a typed record instead of raw stdout. + +Found while migrating a course language whose `display` family and `exit` +expression need exactly these two channels. That port does not block on +this: the buffered-output workaround ships, and `exit`'s status-code +divergence ships as-is with a comment in each spec explaining it, since +`exit` is used by no example programs and no tests. diff --git a/dev-docs/issues/188-follow-set-omits-nullable-tail.md b/dev-docs/issues/188-follow-set-omits-nullable-tail.md new file mode 100644 index 00000000..01bf7c1e --- /dev/null +++ b/dev-docs/issues/188-follow-set-omits-nullable-tail.md @@ -0,0 +1,116 @@ +# 188 - FOLLOW set omits the nullable tail, breaking empty alternatives + +**Type:** fix +**Date:** 2026-08-11 + + + +## Description + +The FOLLOW set of a nonterminal that is followed by a *nullable* symbol is +under-approximated. The predict set of an **empty alternative** is exactly +FOLLOW of its nonterminal, so the generated parse table silently loses +entries and valid programs fail to parse — while `plcc-ll1` still reports +`is_ll1: true` and no conflicts. + +The defect is in +[`FollowSetBuilder._updateWithSingleOccuranceOfNonterminalInProduction`](../../src/plcc/spec/syntax/validations/ll1/build_follow_sets.py): + +```python +else: + self._addFirstOfNextSymbol(rules[index + 1], nonterminal) + if self._canDeriveEmpty(rules[index + 1:]): + self._addFollowOfLHS(lhs, nonterminal) +``` + +It adds FIRST of the **single** next symbol, then jumps straight to the +"whole remainder is nullable" case. The standard algorithm walks forward +from `index + 1`, adding `FIRST(X_j) \ {ε}` and stopping at the first `X_j` +that is not nullable. When `X_{i+1}` is nullable but the remainder is not, +every symbol between them is skipped. + +A repeating (`**=`) nonterminal makes this easy to hit, because it is +nullable by construction and is a natural thing to place in a sequence. + +Distinct from issue +[#170](done/170-arbno-follow-set-missing-eof.md), which was about +nullability being tested against only a nonterminal's first-registered +production. That fix landed (`_canDeriveEmpty` now consults the FIRST sets); +this is the separate forward-walk defect in the same function. + +## Steps to Reproduce + +1. A grammar in which a nonterminal with an empty alternative is followed by + a nullable symbol — here `` (which has an empty alternative) is + followed by `` (nullable, because `**=`): + + ``` + ::= CLASS END + ::= EXTENDS + ::= + **= STATIC EQUALS + **= FIELD + **= METHOD EQUALS + ``` + +2. `plcc-ll1` reports `is_ll1: true`, no conflicts, and + `FOLLOW(Ext) = ['STATIC']`. The correct set is + `{STATIC, FIELD, METHOD, END}`. + +3. `printf 'class static x = 3 end\n' | plcc-parse` — parses. This is the + one class shape whose next token happens to be in the truncated FOLLOW + set. + +4. `printf 'class field x end\n' | plcc-parse` — fails: + + ``` + plcc-parser-table: -:1:7: error: unexpected 'FIELD', no production for 'Ext' + ``` + + Same for `class method m = proc() 1 end` and for the empty `class end`. + +## Notes + +Two things make this worse than an ordinary parse bug. + +**It is silent.** `plcc-ll1` reports the grammar LL(1)-clean, so nothing +warns the spec author. The failure appears later, on one particular input +shape. + +**The diagnosis is inverted.** The message names the token that *is* there +(`unexpected 'FIELD'`) and the nonterminal that has no entry for it, which +reads like a grammar-ambiguity problem. The actual cause is several rules +away, in a symbol the author never looked at. + +Suggested fix, replacing the `else` branch above: + +```python +else: + for j in range(index + 1, len(rules)): + self._addFirstOfNextSymbol(rules[j], nonterminal) + if not self._canDeriveEmpty([rules[j]]): + break + else: + self._addFollowOfLHS(lhs, nonterminal) +``` + +Whoever picks this up should add a unit test at the `build_follow_sets` +level directly, not only an end-to-end regression: a minimal grammar +`S -> A B C`, `A -> a | ε`, `B -> b | ε`, `C -> c`, asserting +`FOLLOW(A) = {b, c}` rather than `{b}`. The grammar above is the +end-to-end case. + +Found while migrating a course language whose class-declaration rule has +exactly this shape. That port works around it by splitting the class body +into a non-nullable nonterminal, so the symbol immediately following the +empty-alternative nonterminal has a correctly computed FIRST set; the +accepted language is unchanged, and the workaround can be reverted once +this is fixed. diff --git a/dev-docs/roadmap.md b/dev-docs/roadmap.md index 7b73f708..29163ea5 100644 --- a/dev-docs/roadmap.md +++ b/dev-docs/roadmap.md @@ -6,11 +6,22 @@ - **[#160](issues/160-concurrent-plcc-build-dir-race.md) — Concurrent plcc-scan/plcc-make invocations race on shared build dir** Two CLI invocations sharing the same `./plcc-ng/` build dir race on temp-file creation/cleanup and crash with a raw `FileNotFoundError` traceback instead of a friendly error. +- **[#186](issues/186-rep-deadlocks-on-partial-stdout-line.md) — plcc-rep deadlocks on a partial stdout line** + A semantic action that writes without a trailing newline merges with the JSON result line, so `_read_response` destroys the result and blocks forever — exit 124, no stdout, no stderr. +- **[#188](issues/188-follow-set-omits-nullable-tail.md) — FOLLOW set omits the nullable tail, breaking empty alternatives** + `_updateWithSingleOccuranceOfNonterminalInProduction` adds FIRST of only the *next* symbol instead of walking forward through nullable ones, so empty alternatives silently lose parse-table entries while `plcc-ll1` still reports `is_ll1: true`. ### Feat - **[#161](issues/161-rename-plcc-rep-to-plcc-eval.md) — Consider renaming plcc-rep to plcc-eval for phase-naming consistency** `plcc-rep` is named after its interaction mode (REPL), not its phase, breaking the `scan`/`parse`/`?` naming pattern; an alias or rename to `plcc-eval` would restore it. +- **[#187](issues/187-rep-lacks-output-and-clean-exit-records.md) — plcc-rep lacks output and clean-exit record kinds** + Semantic actions have no supported channel for user-visible output and no way to end the session cleanly, so output must be buffered into the result and a deliberate `exit` reads as a crash. + +### Docs + +- **[#185](issues/185-rep-parses-each-source-independently.md) — plcc-rep parses each SOURCE independently, unlike PLCC's `rep`** + Old PLCC's `rep` joined its file arguments into one stream; plcc-ng parses each separately, so a program split across two files no longer parses — an undocumented breaking change. ### Test From e6d102e2a5178c6240895912a52fe3ee6fb3a2b4 Mon Sep 17 00:00:00 2001 From: Stoney Jackson Date: Tue, 11 Aug 2026 19:43:39 -0400 Subject: [PATCH 2/2] docs(issues): file 189 - adopt the languages-ng issue-system shape The languages-ng issue system is a fork of this one that diverged in three ways: issues never move (a `closed:` frontmatter date is the status), YAML frontmatter replaces the `**Type:**`/`**Date:**` headers, and a `target:` field names the repo an issue is about. The first is the load-bearing one. Closing by `git mv` into done/ means every link written before a close goes stale; #149 cleaned that up once and #150 built rewriting into close.bash to stop it recurring, but 14 links under issues/done/ are broken today. Both remaining shapes are structural: a link rewritten correctly at close time breaks when the issue it points *at* closes later, and the blanket depth rewrite cannot distinguish an already-wrong relative path from a right one. Co-Authored-By: Claude Opus 5 --- dev-docs/issues/.next-id.txt | 2 +- ...89-align-issue-system-with-languages-ng.md | 175 ++++++++++++++++++ dev-docs/roadmap.md | 2 + 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 dev-docs/issues/189-align-issue-system-with-languages-ng.md diff --git a/dev-docs/issues/.next-id.txt b/dev-docs/issues/.next-id.txt index 6c412452..598ed30e 100644 --- a/dev-docs/issues/.next-id.txt +++ b/dev-docs/issues/.next-id.txt @@ -1 +1 @@ -189 +190 diff --git a/dev-docs/issues/189-align-issue-system-with-languages-ng.md b/dev-docs/issues/189-align-issue-system-with-languages-ng.md new file mode 100644 index 00000000..af3dc6d5 --- /dev/null +++ b/dev-docs/issues/189-align-issue-system-with-languages-ng.md @@ -0,0 +1,175 @@ +# 189 - Adopt the languages-ng issue-system shape: issues never move, status is frontmatter + +**Type:** chore +**Date:** 2026-08-11 + + + +## Description + +The `languages-ng` repo's issue system is a fork of this one, evolved +under use, and it has diverged in three deliberate ways. This proposes +adopting that shape here. The three are separable; only the first is +load-bearing. + +### 1. Issues never move; `closed:` is the status + +Today a close does `git mv` into [issues/done/](done/), so an issue's path +changes exactly once in its life — and every link written before that +moment goes stale. Issue +[#149](done/149-fix-stale-issues-done-links.md) was the one-time cleanup of +~23 such links; issue [#150](done/150-close-script-auto-fix-links.md) then +built link-rewriting into `close.bash` so the class would stop recurring. + +**It did not stop recurring. There are 14 broken links in `issues/done/` +right now** (command in Steps to Reproduce). They come in two shapes, and +both are structural rather than bugs in the rewriting: + +- **Links that go stale on a *later* close** (8 of the 14). When #179 + closed, `close.bash` correctly rewrote its bare sibling link + `176-integration-tier-has-no-arbno-coverage.md` to + `../176-integration-tier-has-no-arbno-coverage.md` — correct at that + moment, because #176 was still open in `issues/`. Then #176 closed and + moved. Closing #176 rewrites occurrences of the literal string + `issues/176-…` across `dev-docs/`, but #179's link now reads `../176-…`, + which does not contain `issues/`, so it is missed. Every close + invalidates links written by earlier closes, and no rewriting rule keyed + on the closing issue's own path can see them. + +- **Depth rewrites applied to already-wrong paths** (6 of the 14). #150's + blanket "add one more `../` to anything climbing out of `issues/`" cannot + tell a correct relative path from an incorrect one. #157 was filed with + `[issues/TEMPLATE.md](../TEMPLATE.md)` — already wrong by one level — and + the close deepened it to `../../TEMPLATE.md`, still wrong. The five + `../../src/plcc/cmd/source_runner.py` links in #013/#014/#018/#020/#021 + are the same shape from before #150 landed. + +Under the languages-ng shape none of this exists. A link to an issue is +`issues/NNN-slug.md` from the day it is filed until forever, no file ever +changes depth, and `close.bash` touches no links at all — its +[close.bash](../../bin/issues/close.bash) counterpart is *shorter* than +ours despite doing strictly more validation, because roughly 40 lines of +link surgery are simply gone. + +Closed state moves into the frontmatter instead: + +```yaml +closed: 2026-08-11 +``` + +Empty while open, a date once closed. Their `check.bash` guards the +regression directly: + +```bash +if [[ -d "${ISSUES_DIR}/done" ]]; then + fail "${ISSUES_DIR}/done exists; status is a 'closed:' date, not a directory" +fi +``` + +### 2. YAML frontmatter instead of `**Type:**` / `**Date:**` + +```yaml +--- +type: chore +target: this repo +opened: 2026-07-31 +closed: +--- +``` + +Two gains over our bold-label lines. First, **we currently record no close +date at all** — our `**Date:**` is the filing date, and when an issue +closed survives only in git history and in which directory the file landed +in. `opened` + `closed` puts the issue's whole lifespan in the file. +Second, it is machine-readable: their `check.bash` validates that the block +is well-formed, that all four keys are present, and that both dates parse. +Ours can validate none of that. + +The block is deliberately **flat scalars, one key per line** — no nesting, +no lists, no multi-line values — precisely so `grep` and `awk` suffice and +no YAML dependency enters `bin/`: + +```bash +grep -l '^closed: [0-9]' dev-docs/issues/[0-9]*.md # closed +grep -L '^closed: [0-9]' dev-docs/issues/[0-9]*.md # open +``` + +### 3. A `target:` field + +Names the repository an issue is actually about, defaulting to `this repo`, +so a defect found here but belonging to another repo can be filed here +without pretending it is ours. This is the weakest of the three for us: +languages-ng needs it because it is downstream of this repo and +accumulates upstream findings, whereas we are usually the upstream. It is +not worthless — issues [#160](160-concurrent-plcc-build-dir-race.md) +and [#161](161-rename-plcc-rep-to-plcc-eval.md) were hand-migrated +from `ourPLCC/plcc-ng-demo`, and adjacent repos (`plcc-ng-demo`, +`plcc-ng-devcontainer`) could use the same treatment — but it is easily +dropped without affecting (1) or (2). + +## Steps to Reproduce + +The 14 currently-broken links, from the repository root: + +```bash +for f in dev-docs/issues/done/*.md; do + grep -o '](\.\.[^)]*)' "$f" | tr -d '()' | sed 's/^]//' | while read -r l; do + [ -e "$(dirname "$f")/$l" ] || echo "$f -> $l" + done +done +``` + +Every hit is inside `issues/done/`, and every one is a consequence of the +file having moved. + +## Notes + +**Migration sketch** — one branch, mechanical, but not small: + +1. `git mv dev-docs/issues/done/*.md dev-docs/issues/` (177 files; 9 are + already open, for 186 total). Note that IDs 035, 039, and 043 each name + two different files from before `.next-id.txt` existed — the slugs + differ so there is no filename collision, but anything keyed on ID alone + should be checked. +2. Convert all 186 files' `**Type:**`/`**Date:**` headers to frontmatter, + mapping `Date:` → `opened:`. Backfill `closed:` from each file's close + commit date (`git log --diff-filter=R --follow`); spot-check rather than + trust it, since some closes were amended or batched. +3. Un-rewrite the links #150 rewrote — the exact inverse of its rules: + `../NNN-slug.md` → `NNN-slug.md` for sibling issues, and one fewer `../` + on paths climbing out of `issues/`. Fix the 14 broken ones by hand while + in there. +4. Port [check.bash](../../bin/issues/check.bash) from languages-ng (frontmatter + validators, the `done/` guard, checkbox-vs-`closed` agreement) and delete + `close.bash`'s link-rewriting block. +5. Trim `tests/bats/commands/issues-close.bats` — most of its 126 lines + exercise link rewriting that will no longer exist — and add coverage for + the `closed:` fill-in. +6. Update [issue-conventions.md](../issue-conventions.md) and + [CLAUDE.md](../../CLAUDE.md). + +**Honest cost.** One directory holding 186 files, 177 of them closed, is +worse to browse than 9-plus-an-archive. languages-ng has 38 issues total, +so it has not felt this yet. The mitigation is the `grep -L` one-liner +above (their conventions document it for exactly this reason) plus the fact +that the roadmap already lists precisely the open set. Worth deciding +deliberately, because it is the one real thing being traded away for the +link stability. + +**Timing.** Now is unusually cheap: `roadmap.md` currently has no milestone +sections, and milestone links are the other place the move rule bites (ours +must be repointed at `done/` on close; theirs never change). Doing this +before the next milestone list is written avoids the conversion entirely. + +Filed after hand-migrating issues +[#185](185-rep-parses-each-source-independently.md)–[#188](188-follow-set-omits-nullable-tail.md) +from languages-ng, which is what surfaced the divergence between the two +systems. diff --git a/dev-docs/roadmap.md b/dev-docs/roadmap.md index 29163ea5..4b1f1afc 100644 --- a/dev-docs/roadmap.md +++ b/dev-docs/roadmap.md @@ -34,3 +34,5 @@ Pinned to 9.x (locked 9.21.2); latest is 10.5.3. Dev-only dependency, consider updating. - **[#156](issues/156-mkdocs-1x-successor-decision.md) — Decide our MkDocs 1.x successor** mkdocs-material hard-pins mkdocs<2; mkdocs-kroki-plugin already pulls in properdocs. Not urgent yet, but we'll need to pick ProperDocs, Zensical, or stay pinned once MkDocs 1.x actually breaks. +- **[#189](issues/189-align-issue-system-with-languages-ng.md) — Adopt the languages-ng issue-system shape: issues never move, status is frontmatter** + Moving issues to `done/` on close keeps breaking links (14 are broken right now, despite #150's rewriting); making `closed:` a frontmatter date instead deletes the bug class and the machinery guarding it.