sf snapshot: pre-dispatch, uncommitted changes after 46m inactivity

This commit is contained in:
Mikael Hugo 2026-04-30 21:07:36 +02:00
parent b43bf6991e
commit a7b96cd004
9 changed files with 211 additions and 13 deletions

View file

@ -24,12 +24,14 @@
clippy
git
nodejs_24
pkg-config
nodePackages.typescript
protobuf
rust-analyzer
rustc
rustfmt
uv
zlib
];
shellHook = ''

View file

@ -1,6 +1,6 @@
{
"name": "singularity-forge",
"version": "2.75.1",
"version": "2.75.2",
"description": "Singularity Forge runtime core",
"license": "MIT",
"repository": {

View file

@ -10,18 +10,30 @@ import { native } from "../native.js";
import type {
BatchParseResult,
FrontmatterResult,
JsonlParseResult,
NativePlan,
NativeRoadmap,
NativeSummary,
SectionResult,
SfTreeEntry,
} from "./types.js";
export type {
BatchParseResult,
FrontmatterResult,
JsonlParseResult,
NativeBoundaryMapEntry,
NativeFileModified,
NativePlan,
NativeRoadmap,
NativeRoadmapSlice,
NativeSummary,
NativeSummaryFrontmatter,
NativeSummaryRequires,
NativeTaskEntry,
ParsedSfFile,
SectionResult,
SfTreeEntry,
} from "./types.js";
/**
@ -96,3 +108,45 @@ export function parseRoadmapFile(content: string): NativeRoadmap {
content,
) as NativeRoadmap;
}
/**
* Scan a `.sf/` directory tree.
*/
export function scanSfTree(directory: string): SfTreeEntry[] {
return (native as Record<string, Function>).scanSfTree(
directory,
) as SfTreeEntry[];
}
/**
* Parse the tail of a JSONL file without loading the full file into JS memory.
*/
export function parseJsonlTail(
filePath: string,
maxBytes?: number,
maxEntries?: number,
): JsonlParseResult {
return (native as Record<string, Function>).parseJsonlTail(
filePath,
maxBytes,
maxEntries,
) as JsonlParseResult;
}
/**
* Parse a task plan markdown file into structured data.
*/
export function parsePlanFile(content: string): NativePlan {
return (native as Record<string, Function>).parsePlanFile(
content,
) as NativePlan;
}
/**
* Parse a summary markdown file into structured data.
*/
export function parseSummaryFile(content: string): NativeSummary {
return (native as Record<string, Function>).parseSummaryFile(
content,
) as NativeSummary;
}

View file

@ -28,6 +28,8 @@ export interface ParsedSfFile {
body: string;
/** Map of section heading to content, serialized as JSON. */
sections: string;
/** Original raw file content. */
rawContent: string;
}
export interface BatchParseResult {
@ -60,3 +62,72 @@ export interface NativeRoadmap {
slices: NativeRoadmapSlice[];
boundaryMap: NativeBoundaryMapEntry[];
}
export interface SfTreeEntry {
path: string;
name: string;
isDir: boolean;
}
export interface JsonlParseResult {
entries: string;
count: number;
truncated: boolean;
}
export interface NativeTaskEntry {
id: string;
title: string;
description: string;
done: boolean;
estimate: string;
files: string[];
verify: string;
}
export interface NativePlan {
id: string;
title: string;
goal: string;
demo: string;
mustHaves: string[];
tasks: NativeTaskEntry[];
filesLikelyTouched: string[];
}
export interface NativeSummaryRequires {
slice: string;
provides: string;
}
export interface NativeSummaryFrontmatter {
id: string;
parent: string;
milestone: string;
provides: string[];
requires: NativeSummaryRequires[];
affects: string[];
keyFiles: string[];
keyDecisions: string[];
patternsEstablished: string[];
drillDownPaths: string[];
observabilitySurfaces: string[];
duration: string;
verificationResult: string;
completedAt: string;
blockerDiscovered: boolean;
}
export interface NativeFileModified {
path: string;
description: string;
}
export interface NativeSummary {
frontmatter: NativeSummaryFrontmatter;
title: string;
oneLiner: string;
whatHappened: string;
deviations: string;
filesModified: NativeFileModified[];
}

View file

@ -109,19 +109,32 @@ export type { StreamState, StreamChunkResult } from "./stream-process/index.js";
export {
parseFrontmatter,
extractSection,
extractSection as nativeExtractSection,
extractAllSections,
batchParseSfFiles,
parseRoadmapFile,
scanSfTree,
parseJsonlTail,
parsePlanFile,
parseSummaryFile,
} from "./forge-parser/index.js";
export type {
BatchParseResult,
FrontmatterResult,
JsonlParseResult,
NativeBoundaryMapEntry,
NativeFileModified,
NativePlan,
NativeRoadmap,
NativeRoadmapSlice,
NativeSummary,
NativeSummaryFrontmatter,
NativeSummaryRequires,
NativeTaskEntry,
ParsedSfFile,
SectionResult,
SfTreeEntry,
} from "./forge-parser/index.js";
export { truncateTail, truncateHead, truncateOutput } from "./truncate/index.js";

View file

@ -27,11 +27,39 @@ const profile = isDev ? "debug" : "release";
const cargoArgs = ["build"];
if (!isDev) cargoArgs.push("--release");
function shellValue(command) {
try {
return execSync(command, {
stdio: ["ignore", "pipe", "ignore"],
env: process.env,
}).toString().trim() || undefined;
} catch {
return undefined;
}
}
function getZlibLibDir() {
return (
process.env.ZLIB_LIB_DIR ||
shellValue("pkg-config --variable=libdir zlib")
);
}
function getCargoEnvironment() {
const zlibLibDir = getZlibLibDir();
const defaultRustFlags = process.env.RUSTFLAGS || "-C target-cpu=native";
const rustFlags = zlibLibDir && !defaultRustFlags.includes(zlibLibDir)
? `${defaultRustFlags} -L native=${zlibLibDir}`
: defaultRustFlags;
const libraryPath = [zlibLibDir, process.env.LIBRARY_PATH]
.filter(Boolean)
.join(path.delimiter);
return {
...process.env,
// Optimize for native CPU when building locally
RUSTFLAGS: process.env.RUSTFLAGS || "-C target-cpu=native",
RUSTFLAGS: rustFlags,
...(libraryPath ? { LIBRARY_PATH: libraryPath } : {}),
};
}

View file

@ -27,7 +27,7 @@ Then do the thing `STATE.md` says to do next.
## The Hierarchy
```
```text
Milestone → a shippable version (4-10 slices)
Slice → one demoable vertical capability (1-7 tasks)
Task → one context-window-sized unit of work (fits in one session)
@ -41,7 +41,7 @@ Milestone → a shippable version (4-10 slices)
All artifacts live in `.sf/` at the project root:
```
```text
.sf/
STATE.md # Dashboard — always read first (derived cache; runtime, gitignored)
DECISIONS.md # Append-only decisions register
@ -121,6 +121,7 @@ Consumes from S01:
```
The boundary map is a **planning artifact** — not runnable code. It:
- Forces upfront thinking about slice boundaries before implementation
- Gives downstream slices a concrete target to code against
- Enables deterministic verification that slices actually connect
@ -250,6 +251,7 @@ Exact next thing to do.
```
**Rules:**
- **Append-only** — rows are never edited or removed. To reverse a decision, add a new row that supersedes it (reference the old ID).
- **#** — Sequential ID (`D001`, `D002`, ...), never reused.
- **When** — Where the decision was made: `M001`, `M001/S01`, or `M001/S01/T02`.
@ -273,6 +275,7 @@ Work flows through these phases. Each phase produces a file.
**When to skip:** When the user already knows exactly what they want, or told you to just go.
**How to do it manually:**
1. Read the roadmap to understand the scope.
2. Identify 3-5 gray areas — implementation decisions the user cares about.
3. Use `ask_user_questions` to discuss each area, one round at a time. Never fabricate user input; wait for the user's actual response before the next round.
@ -287,6 +290,7 @@ Work flows through these phases. Each phase produces a file.
**When to skip:** When the codebase is familiar and the work is straightforward.
**How to do it manually:**
1. Read `M###-CONTEXT.md` and/or `S##-CONTEXT.md` if they exist — know what decisions are locked.
2. Scout relevant code: `rg`, `find`, read key files.
3. Use `resolve_library` / `get_library_docs` if needed.
@ -330,6 +334,7 @@ The **Don't Hand-Roll** and **Common Pitfalls** sections prevent the most expens
**Produces:** `S##-PLAN.md` + individual `T01-PLAN.md` files.
**For a milestone (roadmap):**
1. Read `M###-CONTEXT.md`, `M###-RESEARCH.md`, and `.sf/DECISIONS.md` if they exist.
2. Decompose the vision into 4-10 demoable vertical slices.
3. Order by risk (high-risk first to validate feasibility early).
@ -337,6 +342,7 @@ The **Don't Hand-Roll** and **Common Pitfalls** sections prevent the most expens
5. **Write the boundary map** — for each slice, specify what it produces (functions, types, interfaces, endpoints) and what it consumes from upstream slices. This forces interface thinking before implementation and enables deterministic verification that slices actually connect.
**For a slice (task decomposition):**
1. Read the slice's entry in `M###-ROADMAP.md` **and its boundary map section** — know what interfaces this slice must produce and consume.
2. Read `M###-CONTEXT.md`, `S##-CONTEXT.md`, `M###-RESEARCH.md`, `S##-RESEARCH.md`, and `.sf/DECISIONS.md` if they exist for this slice.
3. Read summaries from dependency slices (check `depends:[]` in roadmap).
@ -352,6 +358,7 @@ The **Don't Hand-Roll** and **Common Pitfalls** sections prevent the most expens
**Produces:** Code changes + `[DONE:n]` markers.
**How to do it manually:**
1. Read the task's `T##-PLAN.md`.
2. Read relevant summaries from prior tasks (for context on what's already built).
3. Execute each step. Mark progress with `[DONE:n]` in responses.
@ -364,6 +371,7 @@ The **Don't Hand-Roll** and **Common Pitfalls** sections prevent the most expens
**Produces:** Pass/fail determination.
**Verification ladder — use the strongest tier you can reach:**
1. **Static:** Files exist, exports present, wiring connected, not stubs.
2. **Command:** Tests pass, build succeeds, lint clean, blocked command works.
3. **Behavioral:** Browser flows work, API responses correct.
@ -373,26 +381,30 @@ The **Don't Hand-Roll** and **Common Pitfalls** sections prevent the most expens
**Verification report format** (written into the summary or surfaced on failure):
```
```markdown
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | User can sign up | ✓ PASS | POST /api/auth/signup returns 201 |
| 2 | Login returns JWT | ✗ FAIL | Returns 500 — missing env var |
### Artifacts
| File | Expected | Status | Evidence |
|------|----------|--------|---------|
| src/lib/auth.ts | JWT helpers, min 30 lines | ✓ SUBSTANTIVE | 87 lines, exports generateTokens |
| src/lib/email.ts | Email sending | ✗ STUB | 8 lines, console.log instead of sending |
### Key Links
| From | To | Via | Status |
|------|----|----|--------|
| login/route.ts | auth.ts | import generateTokens | ✓ WIRED |
| email.ts | Resend API | resend.emails.send() | ✗ NOT WIRED |
### Anti-Patterns Found
| File | Line | Pattern | Severity |
|------|------|---------|----------|
| src/lib/email.ts | 5 | console.log stub | 🛑 Blocker |
@ -457,11 +469,13 @@ The one-liner must be substantive: "JWT auth with refresh rotation using jose" n
**Purpose:** Mark work done and move to the next thing.
**After a task completes:**
1. Mark the task done in `S##-PLAN.md` (checkbox).
2. Check if there's a next task in the slice → execute it.
3. If slice is complete → write slice summary, mark slice done in `M###-ROADMAP.md`.
**After a slice completes:**
1. Write slice `S##-SUMMARY.md` (compresses all task summaries).
2. Write slice `S##-UAT.md` — a non-blocking human test script derived from the slice's must-haves and demo sentence. The agent does NOT wait for UAT results.
3. Mark the slice checkbox in `M###-ROADMAP.md` as `[x]`.
@ -476,6 +490,7 @@ The one-liner must be substantive: "JWT auth with refresh rotation using jose" n
## Continue-Here Protocol
**When to write `continue.md`:**
- You're about to lose context (compaction, session end, Ctrl+C).
- The current task isn't done yet.
- You want to pause and come back later.
@ -508,6 +523,7 @@ The EXACT first thing to do when resuming. Not vague. Specific.
```
**How to resume:**
1. Read `continue.md`.
2. Delete `continue.md` (it's consumed, not permanent).
3. Pick up from "Next Action".
@ -521,12 +537,14 @@ The EXACT first thing to do when resuming. Not vague. Specific.
It is NOT the source of truth. It's a convenience dashboard.
**Sources of truth:**
- `M###-ROADMAP.md` → which slices exist and which are done
- `S##-PLAN.md` → which tasks exist within a slice
- `T##-SUMMARY.md` → what happened during a task
- `S##-SUMMARY.md` and `M###-SUMMARY.md` → compressed slice and milestone outcomes
**Update `STATE.md`** after every significant action:
- Active milestone/slice/task
- Recent decisions (last 3-5)
- Blockers
@ -535,6 +553,7 @@ It is NOT the source of truth. It's a convenience dashboard.
### Reconciliation
If files disagree, **pause and surface to the user**:
- Roadmap says slice done but task summaries missing → inconsistency
- Task marked done but no summary → treat as incomplete
- Continue file exists for completed task → delete continue file
@ -555,7 +574,7 @@ If files disagree, **pause and surface to the user**:
### What Main Looks Like
```
```git
feat(M001/S03): milestone and slice discuss commands
feat(M001/S02): extension scaffold and command routing
feat(M001/S01): file I/O foundation
@ -565,7 +584,7 @@ One commit per slice. Individually revertable. Reads like a changelog.
### What the Branch Looks Like
```
```git
sf/M001/S01:
test(S01/T03): round-trip tests passing
feat(S01/T03): file writer with round-trip fidelity
@ -584,6 +603,7 @@ sf/M001/S01:
| State rebuild | `chore(S01/T02): auto-commit after state-rebuild` | Bookkeeping only |
The system reads the task summary after execution and builds a meaningful commit message:
- **Subject**: `{type}({sliceId}/{taskId}): {one-liner}` — the one-liner from the summary frontmatter
- **Type**: Inferred from the task title and one-liner (`feat`, `fix`, `test`, `refactor`, `docs`, `perf`, `chore`)
- **Body**: Key files from the summary frontmatter (up to 8 files listed)
@ -592,7 +612,7 @@ Commit types: `feat`, `fix`, `test`, `refactor`, `docs`, `perf`, `chore`
### Squash Merge Message
```
```git
feat(M001/S01): file I/O foundation
Agent can parse, format, load, and save all SF file types with round-trip fidelity.
@ -625,6 +645,7 @@ When planning or executing a task, load relevant prior context:
6. If the dependency chain is too large, drop the oldest/least-relevant summaries first.
**Aim for:**
- ~5 provides per summary
- ~10 key_files per summary
- ~5 key_decisions per summary

View file

@ -4,11 +4,15 @@
//
// Functions fall back to JS implementations if the native module is unavailable.
import { createRequire } from "node:module";
import type { RiskLevel, Roadmap } from "./types.js";
// Issue #453: auto-mode post-turn reconciliation must stay on the stable JS path
// unless the native parser is explicitly requested.
const NATIVE_SF_PARSER_ENABLED = process.env.SF_ENABLE_NATIVE_SF_PARSER === "1";
// Prefer the Rust parser for SF markdown. The bridge still falls back to the
// JS implementation when the native addon is unavailable, and this env var
// keeps a runtime escape hatch for platform-specific native issues.
const NATIVE_SF_PARSER_ENABLED =
process.env.SF_DISABLE_NATIVE_SF_PARSER !== "1";
const requireNative = createRequire(import.meta.url);
let nativeModule: {
parseFrontmatter: (content: string) => { metadata: string; body: string };
@ -68,8 +72,7 @@ function loadNative(): typeof nativeModule {
try {
// Dynamic import to avoid hard dependency - fails gracefully if native module not built
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require("@singularity-forge/native");
const mod = requireNative("@singularity-forge/native");
if (mod.parseFrontmatter && mod.extractSection && mod.batchParseSfFiles) {
nativeModule = mod;
}

View file

@ -63,6 +63,12 @@ const MARKDOWNLINT_CONFIG = {
MD013: false, // line length
MD024: false, // duplicate heading text
MD033: false, // inline HTML
// These rules flag cosmetic issues in documentation examples and template
// placeholders that don't affect readability or correctness.
MD031: false, // blank lines around fenced code blocks
MD032: false, // blank lines around lists
MD040: false, // fenced code blocks need language
MD060: false, // table column style
};
const VALIDATORS: Record<string, ContentValidatorFn> = {