Skip to content

Feat: add Vim grammar token model - #119

Merged
8 commits merged into
mainfrom
power-moves/vim-grammar-token-model
Jun 9, 2026
Merged

8 commits merged into
mainfrom
power-moves/vim-grammar-token-model

Conversation

@flyingrobots

Copy link
Copy Markdown
Owner

Summary

  • add a pure Vim key token model for commands, motions, text objects, registers, counts, prefixes, macros, and command-line input
  • add grammar snapshots covering representative power-move chords
  • update the Vim power-moves design retrospective with the token model slice proof

Validation

  • npm run build
  • node --test --test-concurrency=1 spec/vim-power-grammar.spec.mjs
  • node --test --test-concurrency=1 spec/design-cycle-policy.spec.mjs spec/vim-power-parity-matrix.spec.mjs spec/vim-power-target-usability-fixtures.spec.mjs
  • npx --yes markdownlint-cli2 docs/design/0105-vim-power-moves-causal-parity.md docs/design/0106-emacs-ideas-to-steal-causally.md
  • npm run --silent quality
  • git diff --check

@coderabbitai

coderabbitai Bot commented Jun 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added Vim grammar parsing and command tokenization for power-move support (operators, motions, text objects, registers, macros, marks, and visual modes).
    • Implemented workspace history listing, export, and deterministic replay proof verification.
    • Expanded application hosting pattern with durability and semantic history boundaries.
  • Documentation

    • Added comprehensive design roadmap for Vim parity expansion and Emacs-inspired feature integration.
    • Created matrix and fixture specifications for Vim feature coverage and workflow validation.
  • Tests

    • Added test coverage for grammar parsing, history listing, replay verification, and CLI operations.

Walkthrough

This PR closes WSC evidence slices 127–136 by implementing deterministic history listing, replay proof verification, and export tooling, while establishing comprehensive design roadmaps for Vim power-moves parity (WF-0105) and Emacs-inspired causal features (WF-0106), backed by parity matrices, usability fixtures, and snapshot-driven grammar tests.

Changes

WSC History, Replay Proof, Vim Grammar Parity, and Design Roadmaps

Layer / File(s) Summary
WSC History & Replay Proof Port Contracts
src/ports/jedit-wsc-history-listing.ts, src/ports/jedit-wsc-replay-proof.ts
Exported TypeScript contracts defining JeditWscHistoryRecord (outcome, sequence, digest, export coordinates), JeditWscHistoryListed/JeditWscHistoryListObstructed result unions, JeditWscReplayMatch/JeditWscReplayMismatch/JeditWscReplayCloseout proof states, and evidence coordinate shapes for semantic identity comparison and closeout readiness.
WSC History Listing Implementation
src/app/jedit-wsc-history-listing.ts
Exports listJeditWscHistory() and listJeditWscHistoryForFile() to deterministically decode envelope payloads by schema version, compute reading digests, sort by submission timestamp with envelope-ID fallback, assign 1-based sequences, and aggregate per-file summaries counting applied/rejected/missing evidence with latest basis/time tracking. Handles malformed and read-obstructed evidence with structured obstruction codes.
WSC Replay Proof Verification & Closeout
src/app/jedit-wsc-replay-proof.ts
Exports proveJeditWscReplay() to compare two histories via identity field projection and mismatch-coordinate detection, and jeditWscReplayCloseout() to verify stage coverage (applied, rejected, missing, malformed, unsupported outcomes), require at least one non-applied outcome, and return ready/obstructed closeout with evidence coordinates and deterministic-on-clean-checkout flag.
Vim Grammar Token Vocabulary
src/app/workspace/vim-grammar-vocabulary.ts
Exports discriminated union token types (VimGrammarToken), operator/motion/scope/target/mode-switch/visual name types, lookup maps for single-key (SINGLE_OPERATORS, SINGLE_MOTIONS), double-key (DOUBLE_MOTIONS, G_OPERATORS), text-object (TEXT_OBJECTS), mode-switch, and visual-prefix definitions, plus constants for Vim prefixes, registers, scopes, and count bounds.
Vim Grammar Tokenizer & Readers
src/app/workspace/vim-grammar.ts
Exports tokenizeVimKeys() to iteratively scan input key sequences and dispatch specialized readers for command-line invocation, numeric counts (digit runs), register selection, text-object scope/target, macro record/replay with optional registers, double motions, g-operators, linewise operators, mapped single keys, incomplete prefixes, and unknown fallback, emitting a fully-typed token stream.
WSC CLI History Export Script & Export Tolerance
scripts/jedit-wsc-history.mjs, src/app/jedit-wsc-current-history-export.ts
CLI script parses --json, --workspace, --basis, --output flags and routes list to history listing and export to basis export with settlement materialization; tolerates missing submittedAtMs in envelopes by skipping instead of aborting, and validates schema version/reading structure with JSON output and exit codes.
Vim Grammar Snapshot Fixtures & Tests
spec/fixtures/vim-power-grammar-snapshots.json, spec/vim-power-grammar.spec.mjs
Fixture defines 13+ snapshot objects with name/keys/expected tokens for counts, operators, motions, text objects, macros, visual prefixes, command-line, registers, and unknown keys; tests snapshot-based tokenization and VimGrammarTokenKinds constant string stability.
WSC History Listing & Replay Proof Test Suites
spec/jedit-wsc-history-listing.spec.mjs, spec/jedit-wsc-replay-proof.spec.mjs, spec/jedit-wsc-agent-history-cli.spec.mjs
Test suites validate deterministic listing order/sequence/digest/file-summary derivation, missing/obstructed envelope handling, file-path filtering, replay proof match/mismatch with semantic reading digest stability across host-timing permutations, closeout stage coverage and non-applied outcome counting, CLI list/export/obstruction behavior, and settlement/rejection envelope parsing.
Vim Parity Matrix & Usability Fixtures Validation
docs/design/0105-vim-power-moves-parity-matrix.json, docs/design/0105-vim-power-target-usability-fixtures.json, spec/vim-power-parity-matrix.spec.mjs, spec/vim-power-target-usability-fixtures.spec.mjs
JSON matrices enumerate Vim motions/operators/transaction-semantics/text-objects/visual-modes/registers/repeat/macros/marks/search-substitute/ex-commands with parity status, target slices, current proof, proof gaps, and echo boundaries; fixtures specify deterministic workflows with witness commands, workspace setup, step expectations, and required fact identifiers; tests validate structural integrity, metadata stability, complete key coverage, and status-proof alignment.
WF-0105 Vim Power Moves & WF-0106 Emacs-Causal Design Roadmaps
docs/design/0105-vim-power-moves-causal-parity.md, docs/design/0106-emacs-ideas-to-steal-causally.md
WF-0105 defines Vim parity scope (grammar, motion algebra, text objects, operators, registers/macros, marks, search/substitute, visual modes, causal enhancements), runtime API contract shapes (VimChordSyntax, VimResolvedTarget, VimPowerMoveIntent), algebra requirements, operator parity mappings, replay/macro semantics, register transaction doctrine, and 30 implementation slices with tests/acceptance criteria and validation gates. WF-0106 establishes causal borrowing rule for Emacs ideas (command discovery, buffers, macros, undo-tree, REPL), product shape combining Vim+Emacs+Echo, debugging DX with replay levels, command-catalog contracts, and brainstorm slices.
Documentation Updates & Release Gate Integration
docs/echo-application-hosting-guide.md, docs/BEARING.md, spec/echo-application-hosting-guide.spec.mjs, scripts/jedit-echo-release-gate.mjs, spec/release-gate-script.spec.mjs
Adds durability/historical export template section to hosting guide with app/Echo/Wesley authority boundaries, fake-port constraints, and Graft/Think readiness checklist; extends current evidence commands with WSC test/script runs; updates BEARING closure narrative for slices 127–136 and Vim Power Moves plan; marks checklist items complete; extends release-gate focused tests to include WSC history/export/replay/agent specs and validates gate script references new witnesses.

Sequence Diagram(s)

No sequence diagrams generated: the changes comprise design documentation, test fixtures, port contracts, and implementation of deterministic history listing/replay proof/grammar tokenization—primarily data modeling and offline verification logic without multi-component request/response flows or user interaction sequences that would benefit from visualization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Rationale: Substantial heterogeneous changes across deterministic history/replay logic, Vim grammar tokenization with multiple specialized readers, comprehensive test suites, exhaustive parity matrix JSON (744 lines), detailed design documents (1236 + 306 lines), and cascading documentation updates. Logic density is moderate (sorting, digest computation, mismatch detection, envelope parsing), files are diverse (TypeScript, JSON, specs, markdown), and proof-of-correctness assertions are numerous but repetitive in pattern. Requires close attention to semantic identity field ordering, obstruction code paths, envelope schema dispatch, token reader precedence/lookahead, and alignment of parity matrix/fixture/test expectations.

Possibly related PRs

  • flyingrobots/jedit#27: This PR extends the jedit Echo release-gate script with new WSC history/export/replay test specs that build on the trusted lifecycle/gate infrastructure established in #27.

Poem

🎯 Evidence Flows & Grammar Rules Aligned ✍️
Deterministic listings and proofs, matrices bright,
Vim power-moves parsed, split left from right,
History persists through each replica's flight,
Causal and Echo together—all correct, all tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat: add Vim grammar token model' accurately and specifically describes the primary feature addition—a Vim key tokenizer with supporting vocabulary and grammar parsing logic.
Description check ✅ Passed The description clearly articulates the changes (Vim token model, grammar snapshots, design updates) and includes comprehensive validation steps matching the changeset scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d692199e06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return {
artifact: undefined,
materialize(envelope) {
const payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return a typed obstruction for malformed export bases

When an agent exports a retained basis whose envelope digest is valid but whose bytes are malformed JSON, this JSON.parse throws out of the materializer instead of returning JEDIT_WSC_CURRENT_HISTORY_EXPORT_OBSTRUCTED. That makes scripts/jedit-wsc-history.mjs export --json ... terminate with a stack trace/non-JSON output for a basis that the new history listing path otherwise represents as malformed evidence, so callers cannot handle the documented typed obstruction path.

Useful? React with 👍 / 👎.

Comment on lines +41 to +45
closeoutStage('submission', (record) => record.submissionId != null),
closeoutStage('admission', (record) => record.admissionId != null),
closeoutStage('ticket', (record) => record.ticketId != null),
closeoutStage('execution', (record) => record.executionId != null),
closeoutStage('receipt', (record) => record.receiptId != null),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid requiring fields settlements never retain

For closeout on real WSC history, these stage predicates require submissionId, admissionId, ticketId, executionId, and checkpointId, but the actual settlement envelope writer only persists filePath, bufferId, commandKind, range, submittedAtMs, receiptId, reading, and aperture in workspace-text-wsc-settlement.ts. As a result, jeditWscReplayCloseout(listJeditWscHistory(realStore)) always obstructs at the first missing stage even when applied and rejected WSC envelopes are retained; the spec passes only because it uses fake payload fields that production does not write.

Useful? React with 👍 / 👎.

Comment on lines +149 to +155
function readTextObject(keys: readonly string[], index: number): TokenRead | undefined {
const scope = textObjectScope(keys[index]);
const target = TEXT_OBJECTS.get(keys[index + 1] ?? '');
if (scope == null || target == null) {
return undefined;
}
return singleToken(textObjectToken(scope, target, keys.slice(index, index + 2), index), index + 2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not greedily parse standalone insert keys as text objects

When tokenizing a normal-mode sequence like ['i', 'w'] or ['a', 'p'], this reader runs before mode-switch parsing and consumes the pair as an inner word/around paragraph text object even though text objects are only valid after an operator or in visual/operator-pending context. That loses the i/a mode-switch token and will mis-model ordinary insert/append commands followed by typed keys; the text-object reader needs operator/visual context or should not greedily match at top level.

Useful? React with 👍 / 👎.

export const TEXT_OBJECT_INNER_SCOPE: VimTextObjectScope = 'inner';

export const REGISTER_NAMES = new Set('"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+*='.split(''));
export const LINEWISE_OPERATOR_KEYS = new Set(['c', 'd', 'y']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve linewise motions for repeated operators

When tokenizing Vim linewise operator commands such as >>, <<, ==, or !!, this allow-list only recognizes c, d, and y, so the tokenizer emits two operator tokens instead of an operator plus the lineCurrent motion. Downstream operator compilation will not be able to distinguish these common linewise commands from incomplete operator chains, so the repeated linewise operators need to be included here or handled with an equivalent multi-key rule.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/BEARING.md`:
- Around line 74-81: The main progress ledger still shows slices 127-136 as
unchecked while the narrative and sub-checklists mark them complete; update the
top-level progress ledger entries for slices 127-136 to checked ([x]) so the
authoritative progress surface matches the detailed slice closeouts, and commit
that change alongside the slice closeout to satisfy the guideline about checking
off items just before committing.

In `@scripts/jedit-wsc-history.mjs`:
- Around line 86-106: The materialize function currently calls
Buffer.from(envelope.bytes).toString(UTF8_ENCODING) and JSON.parse(...) without
protection; wrap the entire parsing and payload validation (the
Buffer.from(envelope.bytes), .toString, and JSON.parse steps that produce
`payload`) in a try-catch, and on any error return the same obstruction shape
used elsewhere (status:
ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED and obstruction.code:
ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED with a descriptive
message) instead of throwing; keep downstream logic (setting this.artifact and
returning JEDIT_WSC_CURRENT_HISTORY_MATERIALIZED) unchanged when parsing
succeeds.

In `@spec/vim-power-grammar.spec.mjs`:
- Around line 19-29: The test 'Vim grammar token kind constants are stable for
agent witnesses' only asserts 7 of 13 VimGrammarTokenKinds; add assertions for
the missing discriminants (CommandLineText, ModeSwitch, Prefix, Unknown,
VisualPrefix) inside that same test so all token kinds are verified for
stability, referencing grammar.VimGrammarTokenKinds.CommandLineText,
.ModeSwitch, .Prefix, .Unknown and .VisualPrefix and asserting they equal their
expected string discriminants.

In `@spec/vim-power-parity-matrix.spec.mjs`:
- Around line 125-133: The check in assertTargetSlices uses magic numbers 2 and
30 for the WF-0105 allowed slice range; define named constants (e.g.,
WF_0105_MIN_SLICE = 2 and WF_0105_MAX_SLICE = 30) at the top of the spec file
and replace the hardcoded values in assertTargetSlices with those constants
(update the assertion slice >= WF_0105_MIN_SLICE && slice <= WF_0105_MAX_SLICE)
so the constraint is explicit and maintainable.
- Around line 6-8: Replace the locally defined REPO_ROOT in
spec/vim-power-parity-matrix.spec.mjs with the shared constant exported by
spec/dist-helpers.mjs: remove the line "const REPO_ROOT = process.cwd();" and
instead import { REPO_ROOT } from the dist-helpers.mjs module at the top of the
file, keeping MATRIX_PATH and DESIGN_PATH usage unchanged so all specs use the
single shared REPO_ROOT constant.

In `@spec/vim-power-target-usability-fixtures.spec.mjs`:
- Around line 6-10: Replace the local REPO_ROOT declaration with an import from
the shared helper: remove the line that defines REPO_ROOT = process.cwd() and
instead import the exported REPO_ROOT from spec/dist-helpers.mjs at the top of
spec/vim-power-target-usability-fixtures.spec.mjs; keep the existing
FIXTURES_PATH, MATRIX_PATH, DESIGN_PATH and MATRIX_REPO_PATH variables but
construct them using the imported REPO_ROOT so all path constants
(FIXTURES_PATH, MATRIX_PATH) reference the shared REPO_ROOT symbol rather than a
duplicated local value.
- Around line 59-78: The test uses magic numbers for validation thresholds (40,
4, 5); replace them with named constants to make intent clear: define
MIN_HUMAN_TASK_LENGTH, MIN_WITNESS_STEPS, and MIN_REQUIRED_FACTS at the top of
the test (or file) and use them in the assertions that check
workflow.humanTask.length, workflow.witness.steps.length, and
workflow.witness.requiredFacts.length respectively; update the test named "Vim
power target usability fixtures separate human task prose from witness data" to
reference those constants when asserting minimums so the policy is explicit and
maintainable.

In `@src/app/jedit-wsc-history-listing.ts`:
- Around line 155-238: The literal 'rejected' used as a fallback in
rejectedRecord should be extracted to a module-level constant; add a constant
like DEFAULT_REJECTION_REASON = 'rejected' near the top of the file and replace
the inline fallback in rejectedRecord (the expression
String(payload.rejectionReason ?? payload.reason ?? 'rejected')) with
String(payload.rejectionReason ?? payload.reason ?? DEFAULT_REJECTION_REASON) so
the magic string is centralized and reusable.

In `@src/app/jedit-wsc-replay-proof.ts`:
- Around line 75-103: The function jeditWscReplayCloseout embeds literal
obstruction messages; extract those magic strings into named constants (e.g.,
CLOSEOUT_OBSTRUCTION_MISSING_STAGE_MESSAGE and
CLOSEOUT_OBSTRUCTION_MISSING_NON_APPLIED_MESSAGE) and use the constants in the
calls to closeoutObstructed instead of inline strings; update
jeditWscReplayCloseout to pass the appropriate constant (or a templated constant
+ missingStage.name if dynamic text is required) and add the new constant
declarations near the other module-level constants so all diagnostic messages
follow the "no magic strings" guideline.

In `@src/app/workspace/vim-grammar.ts`:
- Around line 253-259: Replace the ad-hoc lexicographic checks in isDigitKey and
isCountStartKey with explicit Set membership tests: add a DIGIT_KEYS Set
(containing '0'..'9') and a COUNT_START_KEYS Set (containing '1'..'9') to
vim-grammar-vocabulary.ts, import those constants into vim-grammar.ts, and
change isDigitKey(key: string) and isCountStartKey(key: string) to return
membership (DIGIT_KEYS.has(key) and COUNT_START_KEYS.has(key)) so the logic
follows the project's Set-based key-validation pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 996098f3-858f-442f-8783-a06ca65263f0

📥 Commits

Reviewing files that changed from the base of the PR and between acf881c and d692199.

📒 Files selected for processing (25)
  • docs/BEARING.md
  • docs/design/0105-vim-power-moves-causal-parity.md
  • docs/design/0105-vim-power-moves-parity-matrix.json
  • docs/design/0105-vim-power-target-usability-fixtures.json
  • docs/design/0106-emacs-ideas-to-steal-causally.md
  • docs/echo-application-hosting-guide.md
  • scripts/jedit-echo-release-gate.mjs
  • scripts/jedit-wsc-history.mjs
  • spec/echo-application-hosting-guide.spec.mjs
  • spec/fixtures/vim-power-grammar-snapshots.json
  • spec/jedit-wsc-agent-history-cli.spec.mjs
  • spec/jedit-wsc-current-history-export.spec.mjs
  • spec/jedit-wsc-history-listing.spec.mjs
  • spec/jedit-wsc-replay-proof.spec.mjs
  • spec/release-gate-script.spec.mjs
  • spec/vim-power-grammar.spec.mjs
  • spec/vim-power-parity-matrix.spec.mjs
  • spec/vim-power-target-usability-fixtures.spec.mjs
  • src/app/jedit-wsc-current-history-export.ts
  • src/app/jedit-wsc-history-listing.ts
  • src/app/jedit-wsc-replay-proof.ts
  • src/app/workspace/vim-grammar-vocabulary.ts
  • src/app/workspace/vim-grammar.ts
  • src/ports/jedit-wsc-history-listing.ts
  • src/ports/jedit-wsc-replay-proof.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test / cycle-proofs
  • GitHub Check: test / echo-authority
  • GitHub Check: test / contract-api
  • GitHub Check: release-gate / jedit-echo
🧰 Additional context used
📓 Path-based instructions (5)
**

⚙️ CodeRabbit configuration file

**: # jedit Agent Contract

Git Safety

  • Never amend commits. Make a new commit.
  • Never rebase unless the user explicitly approves it after a concrete explanation.
  • Never force git operations.
  • Draft pull requests are allowed only for design-cycle kickoff or active cycle
    work. Convert them to ready before merge. If draft PRs are unavailable, use a
    normal PR with the work-in-progress label.

Quality Doctrine

These are hard repo rules, not suggestions:

  1. No any.
  2. No unknown.
  3. Hexagonal architecture, strict.
  4. Encoding and decoding happen only at the boundary, inside a port/adapter.
  5. One file equals one runtime truth.
  6. Runtime truth beats compile-time theater.
  7. No ad hoc string comparison in core logic. Prefer instanceof, numeric tags, symbols, or explicit runtime objects.
  8. No magic strings.
  9. No magic numbers.
  10. SOLID.
  11. DRY.
  12. DI.
  13. Red to green.
  14. Tests are spec.
  15. Agent-first.
  16. No file over 500 lines of code.

Structure

  • ARCHITECTURE.md is the canonical repo architecture doctrine.
  • docs/BEARING.md records the current execution gravity.
  • docs/method/process.md is the canonical cycle workflow.
  • docs/method/roadmap-planning.md defines release-gate, roadmap, slice, and
    proof policy for multi-cycle planning.
  • docs/design/TEMPLATE.md is the required template for full cycle designs.
  • docs/design/0034-design-cycle-template-and-lifecycle.md records the policy
    decision that made executable design docs official.
  • docs/design/0024-jedit-powered-by-echo-release-gate.md is the completed
    baseline plan for the first thirty slices proving jedit has an installed
    package/evidence path while Echo remains generic.
  • docs/design/0025-echo-application-hosting-pattern.md is the completed
    post-release-pressure ten-slice plan for proving the reusable Echo
    application-hosting pattern before applying it to Graft, Think, or other apps.
  • docs/design/0026-echo-hosting-hardening-first-twenty.md is th...

Files:

  • spec/release-gate-script.spec.mjs
  • spec/vim-power-grammar.spec.mjs
  • spec/fixtures/vim-power-grammar-snapshots.json
  • spec/echo-application-hosting-guide.spec.mjs
  • scripts/jedit-echo-release-gate.mjs
  • spec/vim-power-parity-matrix.spec.mjs
  • src/ports/jedit-wsc-history-listing.ts
  • src/ports/jedit-wsc-replay-proof.ts
  • docs/echo-application-hosting-guide.md
  • spec/jedit-wsc-replay-proof.spec.mjs
  • src/app/jedit-wsc-replay-proof.ts
  • spec/jedit-wsc-current-history-export.spec.mjs
  • docs/design/0105-vim-power-target-usability-fixtures.json
  • spec/jedit-wsc-agent-history-cli.spec.mjs
  • spec/jedit-wsc-history-listing.spec.mjs
  • scripts/jedit-wsc-history.mjs
  • spec/vim-power-target-usability-fixtures.spec.mjs
  • docs/design/0106-emacs-ideas-to-steal-causally.md
  • docs/design/0105-vim-power-moves-parity-matrix.json
  • src/app/workspace/vim-grammar-vocabulary.ts
  • src/app/jedit-wsc-history-listing.ts
  • src/app/workspace/vim-grammar.ts
  • docs/BEARING.md
  • docs/design/0105-vim-power-moves-causal-parity.md
  • src/app/jedit-wsc-current-history-export.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: No any type usage in TypeScript code
No unknown type usage in TypeScript code
No TypeScript file over 500 lines of code
Make invalid states unrepresentable at runtime where possible
Prefer constructor or factory injection over ambient singletons for new code

Files:

  • src/ports/jedit-wsc-history-listing.ts
  • src/ports/jedit-wsc-replay-proof.ts
  • src/app/jedit-wsc-replay-proof.ts
  • src/app/workspace/vim-grammar-vocabulary.ts
  • src/app/jedit-wsc-history-listing.ts
  • src/app/workspace/vim-grammar.ts
  • src/app/jedit-wsc-current-history-export.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js}: No ad hoc string comparison in core logic; prefer instanceof, numeric tags, symbols, or explicit runtime objects
No magic strings in code
No magic numbers in code

Files:

  • src/ports/jedit-wsc-history-listing.ts
  • src/ports/jedit-wsc-replay-proof.ts
  • src/app/jedit-wsc-replay-proof.ts
  • src/app/workspace/vim-grammar-vocabulary.ts
  • src/app/jedit-wsc-history-listing.ts
  • src/app/workspace/vim-grammar.ts
  • src/app/jedit-wsc-current-history-export.ts
docs/design/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Use the template from docs/design/TEMPLATE.md for all full cycle designs

Files:

  • docs/design/0106-emacs-ideas-to-steal-causally.md
  • docs/design/0105-vim-power-moves-causal-parity.md
docs/BEARING.md

📄 CodeRabbit inference engine (AGENTS.md)

Record current execution gravity in docs/BEARING.md

Files:

  • docs/BEARING.md
🪛 LanguageTool
docs/design/0106-emacs-ideas-to-steal-causally.md

[style] ~106-~106: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...t is the jedit-owned semantic model? 3. What Echo evidence, receipt, retained materi...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~281-~281: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...nal command transcripts with anchors. - Define debug traces with honest replay levels ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

docs/design/0105-vim-power-moves-causal-parity.md

[style] ~105-~105: Consider using “incomplete” to avoid wordiness.
Context: ...rtant current caveats: - Vim parity is not complete. - Motions are mostly imperative UI cur...

(NOT_ABLE_PREMIUM)


[grammar] ~241-~241: Use a hyphen to join words.
Context: ...structed; - a command preview when an ex operation spans many edits; - an optiona...

(QB_NEW_EN_HYPHEN)


[grammar] ~555-~555: Use a hyphen to join words.
Context: ...or Parity Operators should compile over resolved targets: | Operator | Vim pari...

(QB_NEW_EN_HYPHEN)


[style] ~1205-~1205: Consider using “incomplete” to avoid wordiness.
Context: ...etrospective Roadmap implementation is not complete. Slices 2, 3, and 4 now have inspectabl...

(NOT_ABLE_PREMIUM)

🔇 Additional comments (38)
src/app/workspace/vim-grammar-vocabulary.ts (1)

1-291: LGTM!

src/app/workspace/vim-grammar.ts (1)

1-252: LGTM!

Also applies to: 260-325

spec/fixtures/vim-power-grammar-snapshots.json (1)

1-111: LGTM!

spec/vim-power-grammar.spec.mjs (1)

11-17: LGTM!

docs/echo-application-hosting-guide.md (2)

234-293: LGTM!


304-306: LGTM!

docs/BEARING.md (2)

100-130: LGTM!


1735-1738: LGTM!

Also applies to: 1763-1766, 1791-1794, 1818-1821, 1845-1848, 1872-1875, 1900-1903, 1928-1931, 1957-1960, 1984-1987

spec/echo-application-hosting-guide.spec.mjs (1)

18-23: LGTM!

scripts/jedit-echo-release-gate.mjs (1)

43-47: LGTM!

spec/release-gate-script.spec.mjs (1)

77-81: LGTM!

src/ports/jedit-wsc-history-listing.ts (3)

1-11: LGTM!


13-36: LGTM!


38-66: LGTM!

src/ports/jedit-wsc-replay-proof.ts (4)

1-11: LGTM!


13-47: LGTM!


49-77: LGTM!


79-92: LGTM!

src/app/jedit-wsc-history-listing.ts (7)

1-42: LGTM!


44-106: LGTM!


108-127: LGTM!


129-153: LGTM!


240-266: LGTM!


268-322: LGTM!


324-369: LGTM!

src/app/jedit-wsc-replay-proof.ts (4)

1-49: LGTM!


51-73: LGTM!


105-153: LGTM!


155-193: LGTM!

src/app/jedit-wsc-current-history-export.ts (3)

44-47: LGTM!


187-196: LGTM!


200-210: LGTM!

spec/jedit-wsc-agent-history-cli.spec.mjs (1)

1-123: LGTM!

spec/jedit-wsc-history-listing.spec.mjs (1)

1-160: LGTM!

spec/jedit-wsc-replay-proof.spec.mjs (1)

1-134: LGTM!

spec/jedit-wsc-current-history-export.spec.mjs (1)

101-123: LGTM!

Also applies to: 428-434

scripts/jedit-wsc-history.mjs (2)

30-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use strict equality.

Loose equality == works here but violates modern JavaScript best practices. Strict equality makes null/undefined handling explicit.

♻️ Prefer strict equality
-  if (parsed.command == null || parsed.command.startsWith('--')) {
+  if (parsed.command === undefined || parsed.command.startsWith('--')) {
     throwUsage('missing command');
   }

As per coding guidelines: "No ad hoc string comparison in core logic; prefer instanceof, numeric tags, symbols, or explicit runtime objects" and general JavaScript best practices favor strict equality.

			> Likely an incorrect or invalid review comment.

Source: Coding guidelines


110-118: Confirm result.filePath is part of the export contract. exportedBody is only used when result.status === JEDIT_WSC_CURRENT_HISTORY_EXPORTED, and the corresponding JeditWscCurrentHistoryExported type requires filePath: string (populated as filePath: artifact.filePath in exportJeditWscHistoryBasis), so path: result.filePath can’t come from a missing/renamed field.

Comment thread docs/BEARING.md
Comment on lines +74 to +81
The current branch closes slices 127-136: WSC history listing now exposes
app-safe submissions, outcomes, receipts, readings, checkpoints, rejection
reasons, export refs, and typed missing-evidence posture; WSC replay compares
semantic evidence while excluding host timing; current export ignores retained
rejections for applied-text export; an agent JSON CLI lists and exports
historical bases; the release gate includes the WSC history/export/replay
witnesses; and the reusable Echo hosting guide carries the portability template
and Graft/Think readiness checklist.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical inconsistency: narrative claims completion but main ledger disagrees.

Lines 74-81 state "The current branch closes slices 127-136" with detailed accomplishments listed. However, the main progress ledger at lines 626-636 still shows these slices as unchecked [ ]. Only the sub-checklists within each detailed slice description (lines 1735-1987) were marked complete.

The coding guidelines require: "Keep checking off slice items just before committing the slice that closes them." The main ledger at lines 626-636 is the authoritative progress surface and must be updated to [x] for slices 127-136 to match this closeout narrative.

🔧 Proposed fix

Update the main progress ledger to mark slices 127-136 as complete:

 - [x] 125 - Current History Export.
 - [x] 126 - Point-In-Time Export.
-- [ ] 127 - History Listing And Evidence View.
-- [ ] 128 - Replay Same Edits Same Evidence.
-- [ ] 129 - Host Timing Permutation Proof.
-- [ ] 130 - Conflict And Rejection Retention.
-- [ ] 131 - Multi-File History Recovery.
-- [ ] 132 - Agent Historical Export.
-- [ ] 133 - WSC Release Gate Integration.
-- [ ] 134 - DIND Replay Closeout.
-- [ ] 135 - Portability Template Update.
-- [ ] 136 - Graft/Think Readiness Checklist.
+- [x] 127 - History Listing And Evidence View.
+- [x] 128 - Replay Same Edits Same Evidence.
+- [x] 129 - Host Timing Permutation Proof.
+- [x] 130 - Conflict And Rejection Retention.
+- [x] 131 - Multi-File History Recovery.
+- [x] 132 - Agent Historical Export.
+- [x] 133 - WSC Release Gate Integration.
+- [x] 134 - DIND Replay Closeout.
+- [x] 135 - Portability Template Update.
+- [x] 136 - Graft/Think Readiness Checklist.
 - [ ] 137 - End-To-End Guide Refresh.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The current branch closes slices 127-136: WSC history listing now exposes
app-safe submissions, outcomes, receipts, readings, checkpoints, rejection
reasons, export refs, and typed missing-evidence posture; WSC replay compares
semantic evidence while excluding host timing; current export ignores retained
rejections for applied-text export; an agent JSON CLI lists and exports
historical bases; the release gate includes the WSC history/export/replay
witnesses; and the reusable Echo hosting guide carries the portability template
and Graft/Think readiness checklist.
- [x] 125 - Current History Export.
- [x] 126 - Point-In-Time Export.
- [x] 127 - History Listing And Evidence View.
- [x] 128 - Replay Same Edits Same Evidence.
- [x] 129 - Host Timing Permutation Proof.
- [x] 130 - Conflict And Rejection Retention.
- [x] 131 - Multi-File History Recovery.
- [x] 132 - Agent Historical Export.
- [x] 133 - WSC Release Gate Integration.
- [x] 134 - DIND Replay Closeout.
- [x] 135 - Portability Template Update.
- [x] 136 - Graft/Think Readiness Checklist.
- [ ] 137 - End-To-End Guide Refresh.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/BEARING.md` around lines 74 - 81, The main progress ledger still shows
slices 127-136 as unchecked while the narrative and sub-checklists mark them
complete; update the top-level progress ledger entries for slices 127-136 to
checked ([x]) so the authoritative progress surface matches the detailed slice
closeouts, and commit that change alongside the slice closeout to satisfy the
guideline about checking off items just before committing.

Source: Coding guidelines

Comment on lines +86 to +106
materialize(envelope) {
const payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));
if (payload?.schemaVersion !== SETTLEMENT_SCHEMA_VERSION || !Array.isArray(payload.reading?.lines)) {
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED,
obstruction: {
code: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED,
message: 'basis is not an applied WSC settlement envelope',
},
};
}
this.artifact = {
filePath: path.resolve(outputPath),
lines: payload.reading.lines,
readingId: String(payload.reading.readingId ?? ''),
};
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZED,
artifact: this.artifact,
};
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Unguarded JSON parsing will crash the CLI on malformed envelopes.

Line 87 chains three operations that can throw:

  • Buffer.from(envelope.bytes) if bytes are invalid
  • .toString(UTF8_ENCODING) on malformed UTF-8
  • JSON.parse() on invalid JSON

When an envelope is corrupted or tampered with, the materialize function will propagate an unhandled exception, killing the CLI process without a typed obstruction response.

🛡️ Wrap parsing in try-catch and return obstruction
   materialize(envelope) {
+    let payload;
+    try {
+      payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));
+    } catch (error) {
+      return {
+        status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED,
+        obstruction: {
+          code: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED,
+          message: 'basis envelope is malformed or unreadable',
+        },
+      };
+    }
-    const payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));
     if (payload?.schemaVersion !== SETTLEMENT_SCHEMA_VERSION || !Array.isArray(payload.reading?.lines)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
materialize(envelope) {
const payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));
if (payload?.schemaVersion !== SETTLEMENT_SCHEMA_VERSION || !Array.isArray(payload.reading?.lines)) {
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED,
obstruction: {
code: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED,
message: 'basis is not an applied WSC settlement envelope',
},
};
}
this.artifact = {
filePath: path.resolve(outputPath),
lines: payload.reading.lines,
readingId: String(payload.reading.readingId ?? ''),
};
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZED,
artifact: this.artifact,
};
},
materialize(envelope) {
let payload;
try {
payload = JSON.parse(Buffer.from(envelope.bytes).toString(UTF8_ENCODING));
} catch (error) {
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED,
obstruction: {
code: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED,
message: 'basis envelope is malformed or unreadable',
},
};
}
if (payload?.schemaVersion !== SETTLEMENT_SCHEMA_VERSION || !Array.isArray(payload.reading?.lines)) {
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED,
obstruction: {
code: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED,
message: 'basis is not an applied WSC settlement envelope',
},
};
}
this.artifact = {
filePath: path.resolve(outputPath),
lines: payload.reading.lines,
readingId: String(payload.reading.readingId ?? ''),
};
return {
status: ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZED,
artifact: this.artifact,
};
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/jedit-wsc-history.mjs` around lines 86 - 106, The materialize
function currently calls Buffer.from(envelope.bytes).toString(UTF8_ENCODING) and
JSON.parse(...) without protection; wrap the entire parsing and payload
validation (the Buffer.from(envelope.bytes), .toString, and JSON.parse steps
that produce `payload`) in a try-catch, and on any error return the same
obstruction shape used elsewhere (status:
ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_OBSTRUCTED and obstruction.code:
ports.JEDIT_WSC_CURRENT_HISTORY_MATERIALIZATION_FAILED with a descriptive
message) instead of throwing; keep downstream logic (setting this.artifact and
returning JEDIT_WSC_CURRENT_HISTORY_MATERIALIZED) unchanged when parsing
succeeds.

Comment on lines +19 to +29
test('Vim grammar token kind constants are stable for agent witnesses', async () => {
const grammar = await importDist('app', 'workspace', 'vim-grammar.js');

assert.equal(grammar.VimGrammarTokenKinds.Count, 'count');
assert.equal(grammar.VimGrammarTokenKinds.Register, 'register');
assert.equal(grammar.VimGrammarTokenKinds.Operator, 'operator');
assert.equal(grammar.VimGrammarTokenKinds.Motion, 'motion');
assert.equal(grammar.VimGrammarTokenKinds.TextObject, 'textObject');
assert.equal(grammar.VimGrammarTokenKinds.CommandLineInvocation, 'commandLineInvocation');
assert.equal(grammar.VimGrammarTokenKinds.MacroControl, 'macroControl');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test incomplete: 6 of 13 token kinds not verified for stability.

The test title claims to verify token kind constants are "stable for agent witnesses," but only checks 7 of the 13 VimGrammarTokenKind discriminants defined in the vocabulary. Missing assertions for:

  • CommandLineText
  • ModeSwitch
  • Prefix
  • Unknown
  • VisualPrefix

If agent witness stability matters, all token kinds should be tested. Otherwise, a refactor could silently change these discriminant values and break agents.

🧪 Add missing stability assertions
   assert.equal(grammar.VimGrammarTokenKinds.TextObject, 'textObject');
   assert.equal(grammar.VimGrammarTokenKinds.CommandLineInvocation, 'commandLineInvocation');
+  assert.equal(grammar.VimGrammarTokenKinds.CommandLineText, 'commandLineText');
   assert.equal(grammar.VimGrammarTokenKinds.MacroControl, 'macroControl');
+  assert.equal(grammar.VimGrammarTokenKinds.ModeSwitch, 'modeSwitch');
+  assert.equal(grammar.VimGrammarTokenKinds.Prefix, 'prefix');
+  assert.equal(grammar.VimGrammarTokenKinds.Unknown, 'unknown');
+  assert.equal(grammar.VimGrammarTokenKinds.VisualPrefix, 'visualPrefix');
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('Vim grammar token kind constants are stable for agent witnesses', async () => {
const grammar = await importDist('app', 'workspace', 'vim-grammar.js');
assert.equal(grammar.VimGrammarTokenKinds.Count, 'count');
assert.equal(grammar.VimGrammarTokenKinds.Register, 'register');
assert.equal(grammar.VimGrammarTokenKinds.Operator, 'operator');
assert.equal(grammar.VimGrammarTokenKinds.Motion, 'motion');
assert.equal(grammar.VimGrammarTokenKinds.TextObject, 'textObject');
assert.equal(grammar.VimGrammarTokenKinds.CommandLineInvocation, 'commandLineInvocation');
assert.equal(grammar.VimGrammarTokenKinds.MacroControl, 'macroControl');
});
test('Vim grammar token kind constants are stable for agent witnesses', async () => {
const grammar = await importDist('app', 'workspace', 'vim-grammar.js');
assert.equal(grammar.VimGrammarTokenKinds.Count, 'count');
assert.equal(grammar.VimGrammarTokenKinds.Register, 'register');
assert.equal(grammar.VimGrammarTokenKinds.Operator, 'operator');
assert.equal(grammar.VimGrammarTokenKinds.Motion, 'motion');
assert.equal(grammar.VimGrammarTokenKinds.TextObject, 'textObject');
assert.equal(grammar.VimGrammarTokenKinds.CommandLineInvocation, 'commandLineInvocation');
assert.equal(grammar.VimGrammarTokenKinds.CommandLineText, 'commandLineText');
assert.equal(grammar.VimGrammarTokenKinds.MacroControl, 'macroControl');
assert.equal(grammar.VimGrammarTokenKinds.ModeSwitch, 'modeSwitch');
assert.equal(grammar.VimGrammarTokenKinds.Prefix, 'prefix');
assert.equal(grammar.VimGrammarTokenKinds.Unknown, 'unknown');
assert.equal(grammar.VimGrammarTokenKinds.VisualPrefix, 'visualPrefix');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/vim-power-grammar.spec.mjs` around lines 19 - 29, The test 'Vim grammar
token kind constants are stable for agent witnesses' only asserts 7 of 13
VimGrammarTokenKinds; add assertions for the missing discriminants
(CommandLineText, ModeSwitch, Prefix, Unknown, VisualPrefix) inside that same
test so all token kinds are verified for stability, referencing
grammar.VimGrammarTokenKinds.CommandLineText, .ModeSwitch, .Prefix, .Unknown and
.VisualPrefix and asserting they equal their expected string discriminants.

Comment on lines +6 to +8
const REPO_ROOT = process.cwd();
const MATRIX_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-moves-parity-matrix.json');
const DESIGN_PATH = 'docs/design/0105-vim-power-moves-causal-parity.md';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extract REPO_ROOT from the existing dist-helpers.mjs module to follow DRY.

The constant REPO_ROOT is defined here as process.cwd(), but the relevant code snippet shows that spec/dist-helpers.mjs already exports this exact constant. Importing it from the shared module eliminates duplication and ensures consistent path resolution across all specs.

♻️ Refactor to import shared constant
 import assert from 'node:assert/strict';
 import { readFileSync } from 'node:fs';
 import path from 'node:path';
 import test from 'node:test';
+import { REPO_ROOT } from './dist-helpers.mjs';

-const REPO_ROOT = process.cwd();
 const MATRIX_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-moves-parity-matrix.json');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/vim-power-parity-matrix.spec.mjs` around lines 6 - 8, Replace the
locally defined REPO_ROOT in spec/vim-power-parity-matrix.spec.mjs with the
shared constant exported by spec/dist-helpers.mjs: remove the line "const
REPO_ROOT = process.cwd();" and instead import { REPO_ROOT } from the
dist-helpers.mjs module at the top of the file, keeping MATRIX_PATH and
DESIGN_PATH usage unchanged so all specs use the single shared REPO_ROOT
constant.

Comment on lines +125 to +133
function assertTargetSlices(item) {
assert.ok(Array.isArray(item.targetSlices), `${item.id} targetSlices must be an array`);
assert.ok(item.targetSlices.length > 0, `${item.id} targetSlices must not be empty`);

for (const slice of item.targetSlices) {
assert.equal(Number.isInteger(slice), true, `${item.id} target slice must be an integer`);
assert.ok(slice >= 2 && slice <= 30, `${item.id} target slice must fit WF-0105`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace magic numbers with named constants for WF-0105 slice range boundaries.

The hardcoded values 2 and 30 on line 131 represent the valid slice range for the WF-0105 design cycle. These are magic numbers. Extract them as named constants at the top of the file (e.g., WF_0105_MIN_SLICE = 2, WF_0105_MAX_SLICE = 30) to make the constraint explicit and maintainable. As per coding guidelines, no magic numbers should appear in code.

♻️ Extract slice range constants
+const WF_0105_MIN_SLICE = 2;
+const WF_0105_MAX_SLICE = 30;
+
 const STATUS_VOCABULARY = Object.freeze([
   'supported',
   'partial',
   'unsupported',
   'causally-enhanced',
 ]);

 // ... later in assertTargetSlices:

 function assertTargetSlices(item) {
   assert.ok(Array.isArray(item.targetSlices), `${item.id} targetSlices must be an array`);
   assert.ok(item.targetSlices.length > 0, `${item.id} targetSlices must not be empty`);

   for (const slice of item.targetSlices) {
     assert.equal(Number.isInteger(slice), true, `${item.id} target slice must be an integer`);
-    assert.ok(slice >= 2 && slice <= 30, `${item.id} target slice must fit WF-0105`);
+    assert.ok(slice >= WF_0105_MIN_SLICE && slice <= WF_0105_MAX_SLICE, `${item.id} target slice must fit WF-0105 range [${WF_0105_MIN_SLICE}, ${WF_0105_MAX_SLICE}]`);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/vim-power-parity-matrix.spec.mjs` around lines 125 - 133, The check in
assertTargetSlices uses magic numbers 2 and 30 for the WF-0105 allowed slice
range; define named constants (e.g., WF_0105_MIN_SLICE = 2 and WF_0105_MAX_SLICE
= 30) at the top of the spec file and replace the hardcoded values in
assertTargetSlices with those constants (update the assertion slice >=
WF_0105_MIN_SLICE && slice <= WF_0105_MAX_SLICE) so the constraint is explicit
and maintainable.

Source: Coding guidelines

Comment on lines +6 to +10
const REPO_ROOT = process.cwd();
const FIXTURES_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-target-usability-fixtures.json');
const MATRIX_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-moves-parity-matrix.json');
const DESIGN_PATH = 'docs/design/0105-vim-power-moves-causal-parity.md';
const MATRIX_REPO_PATH = 'docs/design/0105-vim-power-moves-parity-matrix.json';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Import REPO_ROOT from the shared dist-helpers.mjs module to eliminate duplication.

This file redefines REPO_ROOT as process.cwd(), duplicating the constant already exported by spec/dist-helpers.mjs (per the relevant code snippet). Import the shared constant to follow DRY principles and ensure consistent path resolution across the test suite.

♻️ Import shared REPO_ROOT
 import assert from 'node:assert/strict';
 import { readFileSync } from 'node:fs';
 import path from 'node:path';
 import test from 'node:test';
+import { REPO_ROOT } from './dist-helpers.mjs';

-const REPO_ROOT = process.cwd();
 const FIXTURES_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-target-usability-fixtures.json');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const REPO_ROOT = process.cwd();
const FIXTURES_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-target-usability-fixtures.json');
const MATRIX_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-moves-parity-matrix.json');
const DESIGN_PATH = 'docs/design/0105-vim-power-moves-causal-parity.md';
const MATRIX_REPO_PATH = 'docs/design/0105-vim-power-moves-parity-matrix.json';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { REPO_ROOT } from './dist-helpers.mjs';
const FIXTURES_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-target-usability-fixtures.json');
const MATRIX_PATH = path.join(REPO_ROOT, 'docs', 'design', '0105-vim-power-moves-parity-matrix.json');
const DESIGN_PATH = 'docs/design/0105-vim-power-moves-causal-parity.md';
const MATRIX_REPO_PATH = 'docs/design/0105-vim-power-moves-parity-matrix.json';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/vim-power-target-usability-fixtures.spec.mjs` around lines 6 - 10,
Replace the local REPO_ROOT declaration with an import from the shared helper:
remove the line that defines REPO_ROOT = process.cwd() and instead import the
exported REPO_ROOT from spec/dist-helpers.mjs at the top of
spec/vim-power-target-usability-fixtures.spec.mjs; keep the existing
FIXTURES_PATH, MATRIX_PATH, DESIGN_PATH and MATRIX_REPO_PATH variables but
construct them using the imported REPO_ROOT so all path constants
(FIXTURES_PATH, MATRIX_PATH) reference the shared REPO_ROOT symbol rather than a
duplicated local value.

Comment on lines +59 to +78
test('Vim power target usability fixtures separate human task prose from witness data', () => {
const fixtures = readJson(FIXTURES_PATH);

for (const workflow of fixtures.workflows) {
assert.equal(typeof workflow.humanTask, 'string', `${workflow.id} humanTask missing`);
assert.ok(workflow.humanTask.length > 40, `${workflow.id} humanTask should be descriptive`);
assert.equal(typeof workflow.witness.command, 'string', `${workflow.id} witness command missing`);
assert.match(workflow.witness.command, /jedit-vim-power-witness\.mjs --json/);
assert.equal(typeof workflow.witness.workspace.cwd, 'string', `${workflow.id} workspace cwd missing`);
assert.ok(workflow.witness.workspace.files.length > 0, `${workflow.id} needs seed files`);
assert.ok(workflow.witness.steps.length >= 4, `${workflow.id} needs enough witness steps`);
assert.ok(workflow.witness.requiredFacts.length >= 5, `${workflow.id} needs required facts`);

for (const step of workflow.witness.steps) {
assert.equal(typeof step.id, 'string', `${workflow.id} step id missing`);
assert.equal(typeof step.input, 'string', `${workflow.id}:${step.id} input missing`);
assert.equal(typeof step.expect, 'string', `${workflow.id}:${step.id} expectation missing`);
}
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract magic numbers for workflow validation thresholds into named constants.

Lines 64, 69, and 70 contain magic numbers (40, 4, 5) representing minimum quality thresholds for workflow fixtures. These should be named constants (e.g., MIN_HUMAN_TASK_LENGTH, MIN_WITNESS_STEPS, MIN_REQUIRED_FACTS) to make the validation policy explicit and maintainable. As per coding guidelines, magic numbers should not appear in code.

♻️ Extract validation threshold constants
+const MIN_HUMAN_TASK_LENGTH = 40;
+const MIN_WITNESS_STEPS = 4;
+const MIN_REQUIRED_FACTS = 5;
+
 const REQUIRED_WORKFLOWS = Object.freeze([
   'open-edit-save-reopen',
   'text-object-surgery',

 // ... later in the test:

 test('Vim power target usability fixtures separate human task prose from witness data', () => {
   const fixtures = readJson(FIXTURES_PATH);

   for (const workflow of fixtures.workflows) {
     assert.equal(typeof workflow.humanTask, 'string', `${workflow.id} humanTask missing`);
-    assert.ok(workflow.humanTask.length > 40, `${workflow.id} humanTask should be descriptive`);
+    assert.ok(workflow.humanTask.length > MIN_HUMAN_TASK_LENGTH, `${workflow.id} humanTask should be descriptive (min ${MIN_HUMAN_TASK_LENGTH} chars)`);
     assert.equal(typeof workflow.witness.command, 'string', `${workflow.id} witness command missing`);
     assert.match(workflow.witness.command, /jedit-vim-power-witness\.mjs --json/);
     assert.equal(typeof workflow.witness.workspace.cwd, 'string', `${workflow.id} workspace cwd missing`);
     assert.ok(workflow.witness.workspace.files.length > 0, `${workflow.id} needs seed files`);
-    assert.ok(workflow.witness.steps.length >= 4, `${workflow.id} needs enough witness steps`);
-    assert.ok(workflow.witness.requiredFacts.length >= 5, `${workflow.id} needs required facts`);
+    assert.ok(workflow.witness.steps.length >= MIN_WITNESS_STEPS, `${workflow.id} needs at least ${MIN_WITNESS_STEPS} witness steps`);
+    assert.ok(workflow.witness.requiredFacts.length >= MIN_REQUIRED_FACTS, `${workflow.id} needs at least ${MIN_REQUIRED_FACTS} required facts`);

     for (const step of workflow.witness.steps) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/vim-power-target-usability-fixtures.spec.mjs` around lines 59 - 78, The
test uses magic numbers for validation thresholds (40, 4, 5); replace them with
named constants to make intent clear: define MIN_HUMAN_TASK_LENGTH,
MIN_WITNESS_STEPS, and MIN_REQUIRED_FACTS at the top of the test (or file) and
use them in the assertions that check workflow.humanTask.length,
workflow.witness.steps.length, and workflow.witness.requiredFacts.length
respectively; update the test named "Vim power target usability fixtures
separate human task prose from witness data" to reference those constants when
asserting minimums so the policy is explicit and maintainable.

Source: Coding guidelines

Comment on lines +155 to +238
function appliedRecord(
envelopeId: string,
payload: JeditWscHistoryEnvelopePayload,
): JeditWscHistoryRecordDraft {
const readingId = payload.reading?.readingId;
return {
basisId: envelopeId,
envelopeId,
outcomeStatus: JEDIT_WSC_HISTORY_APPLIED,
evidencePosture: JEDIT_WSC_HISTORY_SETTLEMENT_EVIDENCE,
...commonPayloadFields(payload),
readingId,
readingLineCount: payload.reading?.lineCount,
readingTextDigest: readingDigest(payload.reading?.lines),
exportEvidenceId: readingId != null
? `${JEDIT_WSC_CURRENT_HISTORY_EXPORT_EVIDENCE_PREFIX}${envelopeId}:${readingId}`
: undefined,
};
}

function rejectedRecord(
envelopeId: string,
payload: JeditWscHistoryEnvelopePayload,
): JeditWscHistoryRecordDraft {
return {
basisId: envelopeId,
envelopeId,
outcomeStatus: JEDIT_WSC_HISTORY_REJECTED,
evidencePosture: JEDIT_WSC_HISTORY_REJECTION_EVIDENCE,
...commonPayloadFields(payload),
rejectionReason: String(payload.rejectionReason ?? payload.reason ?? 'rejected'),
};
}

function commonPayloadFields(payload: JeditWscHistoryEnvelopePayload): JeditWscHistoryCommonFields {
return {
filePath: payload.filePath,
bufferId: payload.bufferId,
commandKind: payload.commandKind,
submittedAtMs: payload.submittedAtMs,
receiptId: payload.receiptId,
checkpointId: payload.checkpointId,
submissionId: payload.submissionId,
admissionId: payload.admissionId,
ticketId: payload.ticketId,
executionId: payload.executionId,
};
}

function missingEvidenceRecord(
envelopeId: string,
obstruction: JeditWscWorkspaceStoreObstruction,
): JeditWscHistoryRecordDraft {
return {
basisId: envelopeId,
envelopeId,
outcomeStatus: JEDIT_WSC_HISTORY_MISSING_EVIDENCE,
evidencePosture: JEDIT_WSC_HISTORY_READ_OBSTRUCTED_EVIDENCE,
obstructionCode: obstruction.code,
obstructionMessage: obstruction.message,
};
}

function unsupportedEvidenceRecord(envelopeId: string): JeditWscHistoryRecordDraft {
return {
basisId: envelopeId,
envelopeId,
outcomeStatus: JEDIT_WSC_HISTORY_MISSING_EVIDENCE,
evidencePosture: JEDIT_WSC_HISTORY_UNSUPPORTED_EVIDENCE,
obstructionCode: JEDIT_WSC_HISTORY_UNSUPPORTED_EVIDENCE,
obstructionMessage: `unsupported WSC envelope schema: ${envelopeId}`,
};
}

function malformedEvidenceRecord(envelopeId: string, message: string): JeditWscHistoryRecordDraft {
return {
basisId: envelopeId,
envelopeId,
outcomeStatus: JEDIT_WSC_HISTORY_MISSING_EVIDENCE,
evidencePosture: JEDIT_WSC_HISTORY_MALFORMED_EVIDENCE,
obstructionCode: JEDIT_WSC_HISTORY_MALFORMED_EVIDENCE,
obstructionMessage: message,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider extracting the default rejection reason literal.

Line 185: The fallback string 'rejected' is used when both rejectionReason and reason fields are missing. Per coding guidelines, this is a magic string in application logic. Extract it to a named constant like DEFAULT_REJECTION_REASON = 'rejected' at the module level.

♻️ Proposed fix

Add constant near the top of the file (after line 42):

+const DEFAULT_REJECTION_REASON = 'rejected';

Then update line 185:

-    rejectionReason: String(payload.rejectionReason ?? payload.reason ?? 'rejected'),
+    rejectionReason: String(payload.rejectionReason ?? payload.reason ?? DEFAULT_REJECTION_REASON),

As per coding guidelines: "No magic strings in code" applies to **/*.{ts,tsx,js} files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/jedit-wsc-history-listing.ts` around lines 155 - 238, The literal
'rejected' used as a fallback in rejectedRecord should be extracted to a
module-level constant; add a constant like DEFAULT_REJECTION_REASON = 'rejected'
near the top of the file and replace the inline fallback in rejectedRecord (the
expression String(payload.rejectionReason ?? payload.reason ?? 'rejected')) with
String(payload.rejectionReason ?? payload.reason ?? DEFAULT_REJECTION_REASON) so
the magic string is centralized and reusable.

Source: Coding guidelines

Comment on lines +75 to +103
export function jeditWscReplayCloseout(
history: JeditWscHistoryListed,
): JeditWscReplayCloseout {
const evidenceCoordinates = history.records.map(evidenceCoordinate);
const coveredStages = CLOSEOUT_STAGES
.filter((stage) => history.records.some(stage.coveredBy))
.map((stage) => stage.name);
const missingStage = CLOSEOUT_STAGES.find((stage) => !coveredStages.includes(stage.name));
if (missingStage != null) {
return closeoutObstructed(JEDIT_WSC_REPLAY_MISSING_STAGE_COVERAGE, missingStage.name, evidenceCoordinates);
}
const nonAppliedOutcomeCount = history.records
.filter((record) => record.outcomeStatus !== JEDIT_WSC_HISTORY_APPLIED)
.length;
if (nonAppliedOutcomeCount === 0) {
return closeoutObstructed(
JEDIT_WSC_REPLAY_MISSING_NON_APPLIED_OUTCOME,
'replay closeout requires at least one retained non-applied outcome',
evidenceCoordinates,
);
}
return {
status: JEDIT_WSC_REPLAY_CLOSEOUT_READY,
evidenceCoordinates,
coveredStages,
nonAppliedOutcomeCount,
deterministicOnCleanCheckout: true,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider extracting obstruction messages to constants.

Lines 84 and 92: The obstruction message strings are embedded directly in the closeout logic. While these are diagnostic messages rather than protocol discriminators, the coding guidelines specify "No magic strings in code" for all **/*.{ts,tsx} files. Extract these to named constants like CLOSEOUT_OBSTRUCTION_MISSING_STAGE_MESSAGE and CLOSEOUT_OBSTRUCTION_MISSING_NON_APPLIED_MESSAGE for consistency and maintainability.

♻️ Proposed fix

Add constants after line 21:

 const WALL_CLOCK_CADENCE_IS_SEMANTIC = false;
+const CLOSEOUT_OBSTRUCTION_MISSING_NON_APPLIED_MESSAGE = 'replay closeout requires at least one retained non-applied outcome';

Then update lines 84 and 92:

   if (missingStage != null) {
-    return closeoutObstructed(JEDIT_WSC_REPLAY_MISSING_STAGE_COVERAGE, missingStage.name, evidenceCoordinates);
+    return closeoutObstructed(JEDIT_WSC_REPLAY_MISSING_STAGE_COVERAGE, `missing stage: ${missingStage.name}`, evidenceCoordinates);
   }
   const nonAppliedOutcomeCount = history.records
     .filter((record) => record.outcomeStatus !== JEDIT_WSC_HISTORY_APPLIED)
     .length;
   if (nonAppliedOutcomeCount === 0) {
     return closeoutObstructed(
       JEDIT_WSC_REPLAY_MISSING_NON_APPLIED_OUTCOME,
-      'replay closeout requires at least one retained non-applied outcome',
+      CLOSEOUT_OBSTRUCTION_MISSING_NON_APPLIED_MESSAGE,
       evidenceCoordinates,
     );
   }

Note: Line 84's current behavior passes missingStage.name as the message, which is just the stage name. If this is intentional, consider a template constant or leaving as-is with a comment explaining the dynamic message generation.

As per coding guidelines: "No magic strings in code" applies to **/*.{ts,tsx} files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/jedit-wsc-replay-proof.ts` around lines 75 - 103, The function
jeditWscReplayCloseout embeds literal obstruction messages; extract those magic
strings into named constants (e.g., CLOSEOUT_OBSTRUCTION_MISSING_STAGE_MESSAGE
and CLOSEOUT_OBSTRUCTION_MISSING_NON_APPLIED_MESSAGE) and use the constants in
the calls to closeoutObstructed instead of inline strings; update
jeditWscReplayCloseout to pass the appropriate constant (or a templated constant
+ missingStage.name if dynamic text is required) and add the new constant
declarations near the other module-level constants so all diagnostic messages
follow the "no magic strings" guideline.

Source: Coding guidelines

Comment on lines +253 to +259
function isDigitKey(key: string): boolean {
return key >= ZERO_KEY && key <= COUNT_MAX;
}

function isCountStartKey(key: string): boolean {
return key >= COUNT_MIN && key <= COUNT_MAX;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace ad hoc string range comparison with Set membership checks.

These functions use lexicographic string comparison (key >= '0' && key <= '9') to validate digit characters. This violates the Quality Doctrine guideline: "No ad hoc string comparison in core logic." The rest of the codebase consistently uses Sets for key membership checks (e.g., REGISTER_NAMES, INCOMPLETE_PREFIX_KEYS). As per coding guidelines, "No ad hoc string comparison in core logic; prefer instanceof, numeric tags, symbols, or explicit runtime objects."

♻️ Refactor to use Set-based membership checks

Add these constants to vim-grammar-vocabulary.ts:

 export const REGISTER_NAMES = new Set('"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+*='.split(''));
+export const DIGIT_KEYS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
+export const COUNT_START_KEYS = new Set(['1', '2', '3', '4', '5', '6', '7', '8', '9']);
 export const LINEWISE_OPERATOR_KEYS = new Set(['c', 'd', 'y']);

Update imports in vim-grammar.ts:

 import {
   COMMAND_LINE_PREFIX,
-  COUNT_MAX,
-  COUNT_MIN,
+  COUNT_START_KEYS,
+  DIGIT_KEYS,
   DOUBLE_MOTIONS,

Apply this refactor:

 function isDigitKey(key: string): boolean {
-  return key >= ZERO_KEY && key <= COUNT_MAX;
+  return DIGIT_KEYS.has(key);
 }
 
 function isCountStartKey(key: string): boolean {
-  return key >= COUNT_MIN && key <= COUNT_MAX;
+  return COUNT_START_KEYS.has(key);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/workspace/vim-grammar.ts` around lines 253 - 259, Replace the ad-hoc
lexicographic checks in isDigitKey and isCountStartKey with explicit Set
membership tests: add a DIGIT_KEYS Set (containing '0'..'9') and a
COUNT_START_KEYS Set (containing '1'..'9') to vim-grammar-vocabulary.ts, import
those constants into vim-grammar.ts, and change isDigitKey(key: string) and
isCountStartKey(key: string) to return membership (DIGIT_KEYS.has(key) and
COUNT_START_KEYS.has(key)) so the logic follows the project's Set-based
key-validation pattern.

Source: Coding guidelines

@flyingrobots flyingrobots closed this pull request by merging all changes into main in 61223ed Jun 9, 2026
@flyingrobots
flyingrobots deleted the power-moves/vim-grammar-token-model branch June 9, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant