Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,6 @@ build/

# IDE files
.vscode/
.idea/
.idea/

.DS_Store
143 changes: 143 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a TypeScript library providing language support for NASA's SeqDev Sequence Editor. It implements parsers, linters, formatters, and CodeMirror extensions for multiple sequence languages used in spacecraft operations.

**Supported Languages:**
- **SeqN** (`.seqN.txt`) - Primary input/authoring language
- **SATF/SASF** - Spacecraft Activity Time File / Spacecraft Activity Sequence File formats
- **SeqJSON** - JSON-based sequence representation
- **VML** - Vehicle Markup Language with CDL dictionary support
- **F-Prime** (`.seq`) - NASA's F' flight software sequencing format
- **Handlebars** - Template support for SeqN

## Development Commands

### Building
```bash
npm run build # Full build: clean + grammar generation + TypeScript compilation (CJS + ESM)
npm run clean # Remove dist/ directory
npm run grammar-builder # Generate all grammar parsers from .grammar files
```

### Grammar Generation (Required Before Build/Test)
```bash
npm run satf # Generate SATF/SASF grammar parser
npm run seqn # Generate SeqN grammar parser
npm run vml # Generate VML grammar parser
npm run fprime # Generate F-Prime grammar parser
```

**Important:** Grammar files must be regenerated whenever `.grammar` files are modified. The build process automatically runs grammar generation, but if you're iterating on grammar changes, run the specific grammar command directly.

### Testing
```bash
npm test # Run all tests (includes grammar generation)
vitest run # Alternative test command
vitest watch # Run tests in watch mode
```

Test fixtures are located in `tests/dictionary/` (command dictionaries) and `tests/sequence/` (example sequences).

### Code Quality
```bash
npm run format:check # Check Prettier formatting
npm run format:write # Apply Prettier formatting
```

## Architecture

### Language Implementation Pattern

Each language follows a consistent architecture based on two core interfaces:

**InputLanguage** (authoring languages like SeqN):
- Provides CodeMirror extensions (linting, completion, tooltips, formatting)
- Implements `commandInfoMapper` to feed Phoenix UI command panel
- Optionally provides `getLibrarySequences` for workspace-wide call signatures
- Example: `src/languages/seq-n/language.ts`

**OutputLanguage** (export/interchange formats like SATF):
- Implements bidirectional converters: `toOutputFormat` and `toInputFormat`
- Used for converting between authoring and delivery formats
- Example: converters in `src/converters/`

### Phoenix Integration

Languages integrate with Phoenix via the `PhoenixAdaptation` interface:
```typescript
interface PhoenixAdaptation {
input: InputLanguage;
outputs: OutputLanguage[];
}
```

**PhoenixResources**: Static resources abstracted from CodeMirror/UI (indentService, linter, hoverTooltip, etc.)

**PhoenixContext**: Dynamic context including command/channel dictionaries and library sequences

Extensions are created using factory patterns that accept `PhoenixContext` and `PhoenixResources`, following the pattern in `getSeqnExtensions()` which returns keyed extension objects for easy composition.

### Grammar Files (Lezer)

Grammars are defined in `.grammar` files using Lezer syntax:
- `src/languages/satf/grammar/satf-sasf.grammar`
- `src/languages/seq-n/seq-n.grammar`
- `src/languages/vml/vml.grammar`
- `src/languages/fprime/fprime.grammar`

Lezer-generator compiles these into TypeScript parsers (`.grammar.js` and `.grammar.d.ts` files). The generated files are gitignored and must be regenerated on each build.

**F-Prime Sequence Format:**
- Commands with absolute time (`AYYYY-DDDTHH:MM:SS[.sss]`) or relative time (`RHH:MM:SS[.sss]`)
- Command mnemonics can include dots for namespacing (e.g., `module.CMD_NAME`)
- Arguments: strings in double quotes, numbers (decimal or hex with `0x` prefix)
- Comments start with semicolon (`;`)

### Key Utilities

**Tree Utilities** (`src/utils/tree-utils.ts`):
- Generic tree traversal and node manipulation for Lezer syntax trees

**Sequence Utilities** (`src/utils/sequence-utils.ts`):
- Command dictionary interaction (FSW/hardware commands)
- Argument parsing and validation
- Type guards for different argument types

**String Utilities** (`src/utils/string.ts`):
- Quote handling: `isQuoted`, `unquoteUnescape`, `quoteEscape`, `removeEscapedQuotes`
- Used throughout parsers for literal handling

**Converters** (`src/converters/`):
- `seqnToSeqJson` / `seqJsonToSeqn`: SeqN ↔ SeqJSON transformation
- `satf-sasf-utils.ts`: SeqN ↔ SATF/SASF conversions

### CommandInfoMapper

The `CommandInfoMapper` interface feeds the Phoenix UI command panel and provides command metadata. Each input language implements its own mapper (e.g., `SeqNCommandInfoMapper`) to extract command information from parse trees.

## Build Artifacts

The package produces dual module outputs:
- **CommonJS**: `dist/cjs/` (main entry)
- **ES Modules**: `dist/esm/` (module entry)

Both include TypeScript declarations. The package.json exports field provides conditional exports for modern bundlers.

## External Dependencies

- **@lezer/common, @lezer/lr**: Core Lezer parsing infrastructure
- **@codemirror/***: Editor extensions (autocomplete, commands, language, lint, view)
- **@nasa-jpl/plandev-ampcs**: Command dictionary types and utilities
- **@nasa-jpl/seq-json-schema**: SeqJSON type definitions
- **@nasa-jpl/plandev-time-utils**: Time parsing and validation

## Development Notes

- Tests are co-located with source files (e.g., `*.test.ts` files in `src/`)
- The sanitizeTextExtension in SeqN automatically converts smart quotes to ASCII quotes on paste/open
- ViewPlugin factories should access PhoenixResources instead of direct CodeMirror imports to follow library conventions (see recent refactor)
- Format functions operate on EditorView instances and should be idempotent
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"satf": "rimraf ./src/languages/satf/grammar/satf-sasf.grammar.js; lezer-generator ./src/languages/satf/grammar/satf-sasf.grammar -o ./src/languages/satf/grammar/satf-sasf.grammar.js",
"seqn": "rimraf ./src/languages/seq-n/seq-n.grammar.js; lezer-generator ./src/languages/seq-n/seq-n.grammar -o ./src/languages/seq-n/seq-n.grammar.js",
"vml": "rimraf ./src/languages/vml/vml.grammar.js; lezer-generator ./src/languages/vml/vml.grammar -o ./src/languages/vml/vml.grammar.js",
"grammar-builder": "npm run satf; npm run seqn && npm run vml",
"fprime": "rimraf ./src/languages/fprime/fprime.grammar.js; lezer-generator ./src/languages/fprime/fprime.grammar -o ./src/languages/fprime/fprime.grammar.js",
"grammar-builder": "npm run satf; npm run seqn; npm run vml; npm run fprime",
"test": "npm run grammar-builder && vitest run",
"build": "npm run clean; npm run grammar-builder; tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
"clean": "rimraf ./dist",
Expand All @@ -51,6 +52,8 @@
"satf",
"sasf",
"seqN",
"fprime",
"f-prime",
"sequence",
"language"
],
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ export {
export { seqJsonLanguage } from './languages/seqjson/language.js';

export { parseCdlDictionary, toAmpcsXml } from './languages/vml/cdl-dictionary.js';

export { fprimeLanguage, getFprimeExtensions } from './languages/fprime/language.js';
export { fprimeParser, getFprimeLRLanguage } from './languages/fprime/fprime.js';
export { FPRIME_NODES } from './languages/fprime/fprime-grammar-constants.js';
export type { FprimeNode } from './languages/fprime/fprime-grammar-constants.js';
export { FPrimeCommandInfoMapper, getNameNode as getFprimeNameNode, getContainingCommand as getFprimeContainingCommand } from './languages/fprime/fprime-tree-utils.js';
export { fprimeLinter } from './languages/fprime/fprime-linter.js';
export { fprimeTooltip } from './languages/fprime/fprime-tooltip.js';
2 changes: 1 addition & 1 deletion src/interfaces/command-info-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type ArgTextDef = {
*/
export interface CommandInfoMapper {
/** format string of multiple arguments */
formatArgumentArray(values: string[], commandNode: SyntaxNode | null): string;
formatArgumentArray(values: string[], commandNode: SyntaxNode | null, editorViewOrSource?: EditorView | string): string;

/** get insert position for missing arguments */
getArgumentAppendPosition(node: SyntaxNode | null): number | undefined;
Expand Down
148 changes: 148 additions & 0 deletions src/languages/fprime/BUGFIX_RELATIVE_TIME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Bug Fix: Relative Time Computation

## Problem (Updated)

All relative time tags were displaying the same computed absolute time, regardless of their position in the sequence.

**Status**: Fixed in two stages:
1. ✅ Initial fix: Corrected cumulative time calculation logic
2. ✅ Second fix: Corrected tree iteration API usage

## Root Cause

In the `computeAbsoluteTime()` function, when finding an absolute time base, the code was incorrectly adding the **current command's** relative time to the base immediately:

```typescript
// WRONG - This adds the current relative time too early
if (absoluteNode) {
baseYear = parsed.year;
baseDayOfYear = parsed.dayOfYear;
cumulativeMs = timeToMilliseconds(parsed) + timeToMilliseconds(currentRelativeTime); // ❌ BUG
}
```

This caused every relative command to compute from: `base + currentRelativeTime`, ignoring all intermediate relative offsets.

## Example of Bug

Given this sequence:
```fprime
A2015-075T22:00:00.000 CMD_1
R00:10:00.000 CMD_2
R00:05:00.000 CMD_3
```

**Before fix:**
- CMD_2: 22:00:00 + 00:10:00 = 22:10:00 ✓ (correct by accident)
- CMD_3: 22:00:00 + 00:05:00 = 22:05:00 ❌ (WRONG - should be 22:15:00!)

Both commands calculated from the same base, ignoring prior relative offsets.

## Solution

The fix properly accumulates all relative time offsets in order:

```typescript
// CORRECT - Set base time only
if (absoluteNode) {
baseYear = parsed.year;
baseDayOfYear = parsed.dayOfYear;
cumulativeMs = timeToMilliseconds(parsed); // Just the base
}

// Add intermediate relative offsets
else if (relativeNode) {
cumulativeMs += timeToMilliseconds(parsed);
}

// Finally, add the current command's relative time
cumulativeMs += timeToMilliseconds(currentRelativeTime); // ✓ At the end
```

**After fix:**
- CMD_2: 22:00:00 + 00:10:00 = 22:10:00 ✓
- CMD_3: 22:00:00 + 00:10:00 + 00:05:00 = 22:15:00 ✓

## Test Coverage

Added comprehensive test `should compute different absolute times for each relative command` that verifies:

```typescript
// CMD_2: base + 10 minutes = 22:10:00
// CMD_3: base + 10 minutes + 5 minutes = 22:15:00 (not 22:05:00!)
```

All 9 time computation tests now pass.

## Algorithm Summary

For each relative time tag:

1. **Initialize** cumulative time to 0
2. **Walk through all prior commands** in sequence order:
- If absolute time: set cumulative = absolute time (reset the base)
- If relative time: add offset to cumulative
3. **Add current command's relative time** to cumulative
4. **Convert** cumulative time to time components
5. **Return** absolute time with base year/day + computed time

This ensures each relative command correctly accumulates all prior offsets.

## Second Issue: Incorrect Tree Iteration API

After the first fix, the issue persisted because the tree iteration was using the wrong API.

### Problem

The code was using:
```typescript
// WRONG API
sequenceNode.cursor().iterate((node) => {
if (node.name === FPRIME_NODES.Command) {
// ...
}
});
```

This doesn't work correctly with Lezer's iterate API.

### Solution

Changed to use the correct Lezer tree iteration API with an object containing an `enter` callback:

```typescript
// CORRECT API
tree.iterate({
enter: (node) => {
if (node.name === FPRIME_NODES.Command) {
// Process commands in document order
if (commandNode.from === currentCommandNode.from) {
reachedCurrentCommand = true;
return false; // Stop iteration
}
}
},
});
```

Additionally added a `reachedCurrentCommand` flag to properly track when we've reached the current command and stop processing.

### Why This Matters

The incorrect API usage meant that:
- The iteration might not have been visiting nodes in the expected order
- The return value to stop iteration might not have been working
- Commands might have been processed multiple times or in the wrong order

With the correct API:
- Nodes are visited in document order (top to bottom)
- Returning `false` from `enter` properly stops iteration
- Each command is processed exactly once in the correct order

## Final Result

Both fixes together ensure that:
1. ✅ Time is accumulated correctly (add base + all prior offsets + current offset)
2. ✅ Commands are processed in the correct order
3. ✅ Iteration stops at the right place
4. ✅ Each relative command gets its own unique computed absolute time
Loading
Loading