Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions .agent/workflows/interactive-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ To use a NodeJS interactive session to test your Tempo library, you can use the

// turbo
```bash
npx tsx -i --import ./test/repl.ts
npx tsx --conditions=development -i --harmony-temporal --import ./bin/repl.ts
```


### Purpose
This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` polyfill into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script.
This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` support into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script.

### Usage Examples
Once the REPL has started, you can run commands like:
Expand All @@ -32,4 +32,4 @@ t1.add({ days: 5 }).format('plain');
### Why this works
- `npx tsx`: Uses the `tsx` runner to handle TypeScript files on the fly.
- `-i`: Explicitly requests an interactive session.
- `--import ./test/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`.
- `--import ./bin/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`.
35 changes: 0 additions & 35 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,4 @@ jobs:
run: npm test
working-directory: packages/tempo

test-parse-prefilter:
name: Test with parse.preFilter enabled
runs-on: ubuntu-latest
timeout-minutes: 30
if: (github.event_name == 'push' || github.event_name == 'pull_request') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/D' || github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'release/D')
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install monorepo dependencies
run: npm ci
working-directory: ${{ github.workspace }}
- name: Run all tests with parse.preFilter
run: npm test
working-directory: packages/tempo
env:
TEMPO_PREFILTER_CI: 'true'
- name: Run end-to-end benchmark
run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json 2> bench-error.log
working-directory: packages/tempo
- name: Upload benchmark output
if: always()
uses: actions/upload-artifact@v4
with:
name: bench-parse-prefilter-e2e
path: |
packages/tempo/bench-output.json
packages/tempo/bench-error.log
- name: Validate benchmark output
run: |
node -e "const r=require('./packages/tempo/bench-output.json');if(!r.success){console.error('Benchmark failed:',r.errors);process.exit(1)}else{console.log('Benchmark passed.')}"
working-directory: ${{ github.workspace }}

147 changes: 147 additions & 0 deletions doc/main_branch_protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Preventing Writes to the Main Branch Locally

To prevent accidental writes (commits and pushes) to the `main` branch on your local workstation, you can use **Git Hooks**.


## Example: Local Hooks for Main Branch Protection
Set up local hooks to:
1. **`pre-commit`**: Prevent direct commits to the `main` branch.
2. **`pre-push`**: Prevent pushing changes to the remote `main` branch.

### How to Override
If you genuinely need to write to `main` (e.g., for an urgent fix), you have two options:
- **Environment Variable**: Prepend the command with the override flag:
- `ALLOW_MAIN_COMMIT=true git commit -m "Urgent fix"`
- `ALLOW_MAIN_PUSH=true git push`
- **Skip Hooks**: Use the standard Git flag:
- `git commit --no-verify`
- `git push --no-verify`

---

## Applying This Globally (Recommended)
If you want this protection to apply to **every repository** on your workstation, you can configure a global hooks directory.

### 1. Create a Global Hooks Directory
Choose a location (e.g., `~/.git-hooks`) and move the hook scripts there:

```bash
mkdir -p ~/.git-hooks
# Copy the hooks from your repository (replace /path/to/repo with your repo root or use the command below)
# Example using git to find the repo root:
cp $(git rev-parse --show-toplevel)/.git/hooks/pre-commit ~/.git-hooks/
cp $(git rev-parse --show-toplevel)/.git/hooks/pre-push ~/.git-hooks/
chmod +x ~/.git-hooks/*
```
Comment thread
magmacomputing marked this conversation as resolved.


### 2. Configure Git Globally (with Important Warning)
Run this command to tell Git to use your new global hooks directory:

```bash
git config --global core.hooksPath ~/.git-hooks
```

**⚠️ Warning:** Setting `core.hooksPath` with the `--global` flag disables all per-repository `.git/hooks/*` scripts. This will break tools that rely on per-project hooks, such as Husky, lefthook, lint-staged, and others. Any hooks defined in individual repositories will be ignored as long as the global `core.hooksPath` is set.

#### Recommended Alternatives

- **Chain per-repo hooks from your global hook scripts:**
- Manually update your global hook scripts (in `~/.git-hooks/`) to call any existing hooks in each repository’s `.git/hooks/` directory, so you don’t lose project-specific logic.
- **Scope the setting to individual repositories:**
- Instead of using `--global`, set the hooks path only for the current repository:
```bash
git config core.hooksPath ~/.git-hooks
```
- This way, only the current repo is affected, and other repos keep their own `.git/hooks/*` scripts.

For more details, see the [Git documentation on `core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehookspath).

---

## Hook Implementation Details

### pre-commit
This script checks the current branch before every commit.

```bash
#!/bin/bash
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "main" ]; then
if [ "$ALLOW_MAIN_COMMIT" != "true" ]; then
echo "❌ ERROR: Direct commit to 'main' branch is prohibited."
exit 1
fi
fi
```

### pre-push
This script checks the remote branch being pushed to.

```bash
#!/bin/bash
while read local_ref local_sha remote_ref remote_sha
do
if [ "$remote_ref" = "refs/heads/main" ]; then
if [ "$ALLOW_MAIN_PUSH" != "true" ]; then
echo "❌ ERROR: Pushing to 'main' branch is prohibited."
exit 1
fi
fi
done
```

---

## 🆘 I'm on 'main' and have changes, what do I do?

If you've already made changes on `main` and the hook blocks your commit, **don't panic and don't drop your stash!** You can easily move your work to a new branch.

### The "Magic" Command: Just Create a New Branch
Git allows you to create and switch to a new branch while keeping your uncommitted changes.

```bash
# 1. Create and switch to a new branch
git checkout -b feature/my-cool-feature
# OR (modern syntax)
git switch -c feature/my-cool-feature

# 2. Now you can commit normally
git add .
git commit -m "My feature changes"
```

### If you want to be extra safe (The Stash Method)
If you have a lot of complex changes and want to ensure `main` stays clean:

```bash
# 1. Save your work temporarily
git stash

# 2. Create and switch to the new branch
git checkout -b feature/my-cool-feature

# 3. Bring your changes back
git stash pop

# 4. Commit
git commit -am "My feature changes"
```

### "I accidentally committed before I added the hook!"
If you have local commits on `main` that you haven't pushed yet, you can move them to a new branch:

```bash
# 1. Create a new branch at your current (accidental) commit
git branch feature/my-feature

# 2. Make sure your local reference to 'origin/main' is up-to-date
git fetch origin
# 3. Reset your local 'main' back to where it should be (the remote version)
# (Fetching first ensures you don't accidentally reset to a stale origin/main reference)
git reset --hard origin/main

# 4. Switch to your new branch to continue working
git checkout feature/my-feature
```

12 changes: 12 additions & 0 deletions packages/tempo/doc/tempo.layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ When crafting raw regex, the following capture groups are used by the engine:
- `per`: Period string offset
- `unt`: Relative unit (e.g., `days`, `weeks`)

## Prototyping with `Tempo.regexp()`

You can use the static `Tempo.regexp()` method to "preview" how a layout string will be compiled by the engine. This is useful for testing custom regex logic before applying it to your configuration.

```typescript
// Expands {dd}, {sep}, {mm}, etc. into a final anchored RegExp
const regex = Tempo.regexp('{dd}{sep}{mm}{sep}{yy}');

console.log(regex.source);
// Output: ^(?<dd>...)(?:...)(?<mm>...)(?:...)(?<yy>...)$
```
Comment thread
magmacomputing marked this conversation as resolved.

---

## Professional Services
Expand Down
5 changes: 3 additions & 2 deletions packages/tempo/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,12 @@ function focusActiveCard() {
}

.tempo-btn-brand {
background-color: var(--vp-c-brand-1);
background-color: #3498db;
color: white;
}
.tempo-btn-brand:hover {
background-color: var(--vp-c-brand-2);
background-color: #2980b9;
color: white;
}

.tempo-btn-alt {
Expand Down
8 changes: 4 additions & 4 deletions packages/tempo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
"default": "./dist/core.index.js"
},
"#tempo/enums": {
"development": "./src/support/tempo.enum.ts",
"default": "./dist/support/tempo.enum.js"
"development": "./src/support/support.enum.ts",
"default": "./dist/support/support.enum.js"
},
"#tempo/duration": {
"development": "./src/module/module.duration.ts",
Expand Down Expand Up @@ -132,8 +132,8 @@
"import": "./dist/tempo.index.js"
},
"./enums": {
"types": "./dist/support/tempo.enum.d.ts",
"import": "./dist/support/tempo.enum.js"
"types": "./dist/support/support.enum.d.ts",
"import": "./dist/support/support.enum.js"
},
"./extend/*": {
"types": "./dist/plugin/extend/extend.*.d.ts",
Expand Down
23 changes: 12 additions & 11 deletions packages/tempo/plan/RELEASE-D.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat

## Task Breakdown & Tracking


### Pattern Compiler + Cache Extraction
- [ ] Extract `compileRegExp`, `setPatterns`, and helpers to new module
- [ ] Integrate memoization/caching logic as needed
- [ ] Refactor engine and consumers to use new module
- [ ] Ensure compatibility with snippet/layout definitions
- [ ] Add/expand unit tests for pattern logic and cache
- [ ] Update documentation and references
- [x] Extract `compileRegExp`, `setPatterns`, and helpers to new module (PatternCompiler)
- [x] Integrate memoization/caching logic as needed (PatternCompiler cache)
- [x] Refactor engine and consumers to use new PatternCompiler module
- [x] Ensure compatibility with snippet/layout definitions
- [x] Add/expand unit tests for pattern logic and cache
- [x] Update documentation and references

### Alias Resolution Engine Extraction
- [ ] Extract alias resolution logic to new module
- [ ] Define interfaces for registration, lookup, collision
- [ ] Refactor engine and plugins to use new APIs
- [ ] Add/expand unit tests for alias/collision
- [ ] Update documentation and references
- [x] Extract alias resolution logic to new module
- [x] Define interfaces for registration, lookup, collision
- [x] Refactor engine and plugins to use new APIs
- [x] Add/expand unit tests for alias/collision
- [x] Update documentation and references
Comment thread
magmacomputing marked this conversation as resolved.

### Guard Builder Extraction (Assessment)
- [ ] Identify all guard-building/token-ingestion logic
Expand Down
4 changes: 2 additions & 2 deletions packages/tempo/src/discrete/discrete.parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { defineInterpreterModule } from '../plugin/plugin.util.js';
import type { Range, ResolvedRange } from '../plugin/plugin.type.js';
import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js';
import { markConfig, setPatterns, init, extendState } from '../support/support.index.js';
import { setProperty } from '#tempo/support/tempo.util.js';
import enums from '../support/tempo.enum.js';
import { setProperty } from '#tempo/support/support.util.js';
import enums from '../support/support.enum.js';
import * as t from '../tempo.type.js';
import type { Tempo } from '../tempo.class.js';

Expand Down
11 changes: 4 additions & 7 deletions packages/tempo/src/engine/engine.alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,14 @@ export class AliasEngine {
this.#config = options.config;
this.#id = AliasEngine.#idCounter++;

if (this.#parent) {
if (!(this.#parent instanceof AliasEngine)) {
const msg = "Parent engine must be an instance of AliasEngine";
this.#logger?.error(this.#config, msg);
throw new TypeError(msg);
}

if (this.#parent instanceof AliasEngine) {
this.#depth = this.#parent.#depth + 1;
this.#state = Object.create(this.#parent.#state); // create a new state object that inherits from the parent engine's state
this.#words = Object.create(this.#parent.#words); // create a new words object that inherits from the parent engine's words for collision detection
} else {
if (this.#parent)
this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine");

this.#depth = 0;
this.#state = Object.create(null); // initialize an empty state for the root engine (no parent)
this.#words = Object.create(null); // initialize an empty words object for the root engine (no parent)
Expand Down
21 changes: 4 additions & 17 deletions packages/tempo/src/engine/engine.layout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ownEntries } from '#library/primitive.library.js';
import { Token } from '#tempo/support/tempo.symbol.js';
import { Token } from '#tempo/support/support.symbol.js';
import { resolveLayoutOrderPure } from './engine.resolver.js';
import type * as t from '../tempo.type.js';

export type LayoutEntry = [symbol, string];
Expand Down Expand Up @@ -94,22 +95,8 @@ export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layout
classification ?? DEFAULT_LAYOUT_CLASS,
);

const layouts = ownEntries(ordered) as LayoutEntry[];
let changed = false;

monthDayLayouts.forEach(([dmy, mdy]) => {
const idx1 = layouts.findIndex(([key]) => key.description === dmy);
const idx2 = layouts.findIndex(([key]) => key.description === mdy);

if (idx1 === -1 || idx2 === -1) return;

const swap1 = idx1 < idx2 && isMonthDay;
const swap2 = idx1 > idx2 && !isMonthDay;
if (swap1 || swap2) {
[layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
changed = true;
}
});
const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay);
const changed = layouts.some((entry, idx) => entry[0] !== (ownEntries(ordered)[idx] as LayoutEntry)[0]);

if (changed) return Object.fromEntries(layouts) as Record<symbol, string>;
return ordered;
Expand Down
Loading
Loading