mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 21:31:46 +00:00
chore: Archive some plans
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# Migration Plan: LlamaIndex → LangChain.js + LanceDB
|
||||
|
||||
**Model:** fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo
|
||||
**Date:** 2026-03-31
|
||||
|
||||
Migrate ask262 from LlamaIndex to LangChain.js with LanceDB for superior metadata pre-filtering.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `package.json` | Replace LlamaIndex with LangChain.js + LanceDB |
|
||||
| `agent.ts` | ReActAgent → LangChain agent |
|
||||
| `setup/ingest.ts` | Text splitter + LanceDB storage |
|
||||
| `setup/build_graph.ts` | **NO CHANGES** |
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Remove:**
|
||||
```json
|
||||
["@llamaindex/*", "llamaindex"]
|
||||
```
|
||||
|
||||
**Add:**
|
||||
```json
|
||||
{
|
||||
"@langchain/core": "^0.2.0",
|
||||
"@langchain/ollama": "^0.1.0",
|
||||
"@langchain/openai": "^0.1.0",
|
||||
"@lancedb/lancedb": "^0.5.0",
|
||||
"langchain": "^0.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 1: ingest.ts Changes
|
||||
|
||||
### 1. Imports
|
||||
```typescript
|
||||
// OLD
|
||||
import { OllamaEmbedding } from "@llamaindex/ollama";
|
||||
import { Document, SentenceSplitter, Settings, storageContextFromDefaults, VectorStoreIndex } from "llamaindex";
|
||||
|
||||
// NEW
|
||||
import { OllamaEmbeddings } from "@langchain/ollama";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
||||
import { LanceDB } from "@langchain/community/vectorstores/lancedb";
|
||||
import * as lancedb from "@lancedb/lancedb";
|
||||
```
|
||||
|
||||
### 2. Text Splitting
|
||||
```typescript
|
||||
// OLD
|
||||
const sentenceSplitter = new SentenceSplitter({ chunkSize: 2048, chunkOverlap: 50 });
|
||||
const rawNodes = sentenceSplitter.getNodesFromDocuments(specDocs);
|
||||
|
||||
// NEW
|
||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||
chunkSize: 2048,
|
||||
chunkOverlap: 50,
|
||||
separators: ["\n\n", "\n", ". ", " ", ""]
|
||||
});
|
||||
const splitDocs = await textSplitter.splitDocuments(specDocs);
|
||||
```
|
||||
|
||||
### 3. Document Creation (LOWERCASE KEYS!)
|
||||
```typescript
|
||||
// OLD
|
||||
new Document({ text, metadata: { sectionId: id, sectionTitle: title, ... } })
|
||||
|
||||
// NEW
|
||||
new Document({
|
||||
pageContent: text,
|
||||
metadata: {
|
||||
sectionid: id, // lowercase!
|
||||
sectiontitle: title, // lowercase!
|
||||
source: file,
|
||||
type: "specification",
|
||||
parentsectionid: null,
|
||||
breakdowntag: null
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Storage
|
||||
```typescript
|
||||
// OLD
|
||||
const storageContext = await storageContextFromDefaults({ persistDir: STORAGE_DIR });
|
||||
const index = await VectorStoreIndex.init({ storageContext });
|
||||
await index.insertNodes(batch);
|
||||
|
||||
// NEW
|
||||
const db = await lancedb.connect(STORAGE_DIR);
|
||||
|
||||
// Check/prompt for existing table
|
||||
let table;
|
||||
try {
|
||||
table = await db.openTable("spec_vectors");
|
||||
// Prompt: overwrite?
|
||||
} catch {
|
||||
table = await db.createTable("spec_vectors", []);
|
||||
}
|
||||
|
||||
// Add scalar indexes (REQUIRED!)
|
||||
await table.createScalarIndex("sectionid");
|
||||
await table.createScalarIndex("breakdowntag");
|
||||
await table.createScalarIndex("type");
|
||||
|
||||
// Store documents
|
||||
const vectorStore = new LanceDB(
|
||||
new OllamaEmbeddings({ model: "nomic-embed-text-v2-moe" }),
|
||||
{ table }
|
||||
);
|
||||
await vectorStore.addDocuments(documents);
|
||||
```
|
||||
|
||||
## Phase 2: agent.ts Changes
|
||||
|
||||
### 1. Imports
|
||||
```typescript
|
||||
import { OllamaEmbeddings } from "@langchain/ollama";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { LanceDB } from "@langchain/community/vectorstores/lancedb";
|
||||
import { createReactAgent } from "@langchain/agents";
|
||||
import { DynamicTool } from "@langchain/core/tools";
|
||||
import * as lancedb from "@lancedb/lancedb";
|
||||
```
|
||||
|
||||
### 2. LLM Setup
|
||||
```typescript
|
||||
// OLD
|
||||
Settings.embedModel = new OllamaEmbedding({ model: "nomic-embed-text-v2-moe" });
|
||||
const llm = new OpenAI({ model: "openai/gpt-oss-120b", apiKey, baseURL, temperature: 0 });
|
||||
Settings.llm = llm;
|
||||
|
||||
// NEW
|
||||
const embeddings = new OllamaEmbeddings({ model: "nomic-embed-text-v2-moe" });
|
||||
const llm = new ChatOpenAI({
|
||||
modelName: "openai/gpt-oss-120b",
|
||||
openAIApiKey: apiKey,
|
||||
configuration: { baseURL },
|
||||
temperature: 0
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Vector Store
|
||||
```typescript
|
||||
const db = await lancedb.connect(STORAGE_DIR);
|
||||
const table = await db.openTable("spec_vectors");
|
||||
const vectorStore = new LanceDB(embeddings, { table });
|
||||
```
|
||||
|
||||
### 4. Tools
|
||||
```typescript
|
||||
// spec_retriever - semantic search
|
||||
const specRetrieverTool = new DynamicTool({
|
||||
name: "spec_retriever",
|
||||
description: "Queries the language specification for text content about specific sections or topics.",
|
||||
func: async (query) => {
|
||||
const results = await vectorStore.similaritySearch(query, 3);
|
||||
return results.map(r => r.pageContent).join("\n\n");
|
||||
}
|
||||
});
|
||||
|
||||
// fetch_section_chunks - get all chunks from a section
|
||||
const sectionRetrieverTool = new DynamicTool({
|
||||
name: "fetch_section_chunks",
|
||||
description: "Retrieves all text chunks from a specific specification section by sectionId.",
|
||||
func: async (sectionId) => {
|
||||
const results = await table
|
||||
.query() // No vector search - pure metadata query
|
||||
.where(`sectionid = '${sectionId}'`)
|
||||
.limit(100)
|
||||
.toArray();
|
||||
return results.map(r => r.text).join("\n\n");
|
||||
}
|
||||
});
|
||||
|
||||
// graph_explorer - NO CHANGES (wrap in DynamicTool)
|
||||
const graphTool = new DynamicTool({
|
||||
name: "graph_explorer",
|
||||
description: "Explores structural relationships between specification sections and implementation code.",
|
||||
func: async (query) => {
|
||||
// Existing Graphology logic preserved
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Agent
|
||||
```typescript
|
||||
// OLD
|
||||
const agent = new ReActAgent({ tools: [queryEngineTool, graphTool], llm, verbose: true, systemPrompt });
|
||||
const response = await agent.chat({ message });
|
||||
|
||||
// NEW
|
||||
const agent = createReactAgent({
|
||||
llm,
|
||||
tools: [specRetrieverTool, sectionRetrieverTool, graphTool],
|
||||
prompt: systemPrompt // Keep existing system prompt
|
||||
});
|
||||
|
||||
const response = await agent.invoke({
|
||||
messages: [{ role: "user", content: message }]
|
||||
});
|
||||
```
|
||||
|
||||
## LanceDB Critical Requirements
|
||||
|
||||
### Column Naming (⚠️ IMPORTANT)
|
||||
- ✅ Use lowercase: `sectionid`, `sectiontitle`, `breakdowntag`
|
||||
- ❌ **NO periods**: `metadata.sectionid` will NOT work
|
||||
- ❌ **NO uppercase** without backticks: `` `sectionId` ``
|
||||
- Keep names simple: letters, numbers, underscores
|
||||
|
||||
### SQL Examples
|
||||
```typescript
|
||||
// Simple equality
|
||||
.where(`sectionid = 'sec-if-statement'`)
|
||||
|
||||
// Pattern matching
|
||||
.where(`sectionid LIKE 'sec-if-%'`)
|
||||
```
|
||||
|
||||
**Note:** Use backticks for uppercase columns: `.where("`sectionId` = 'value'")`
|
||||
|
||||
### Performance: Scalar Indexes
|
||||
**Required** for columns used in WHERE clauses:
|
||||
```typescript
|
||||
await table.createScalarIndex("sectionid"); // REQUIRED
|
||||
await table.createScalarIndex("breakdowntag"); // Recommended
|
||||
await table.createScalarIndex("type"); // Recommended
|
||||
```
|
||||
|
||||
### Highly Selective Filters
|
||||
If filter returns few rows, increase `nprobes`:
|
||||
```typescript
|
||||
const results = await table
|
||||
.search(queryEmbedding)
|
||||
.where(`sectionid = 'sec-rare-section'`)
|
||||
.nprobes(20) // Default is 1-5
|
||||
.limit(10)
|
||||
.toArray();
|
||||
```
|
||||
|
||||
### Query Methods
|
||||
- `.search(vector)` - Vector similarity with optional pre-filtering
|
||||
- `.query()` - Pure metadata query (no vector search) - use for section retrieval
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] `bun install` completes without errors
|
||||
- [ ] `bun run setup/ingest.ts` creates LanceDB table with lowercase metadata
|
||||
- [ ] Scalar indexes created for `sectionid`, `breakdowntag`, `type`
|
||||
- [ ] `bun run setup/build_graph.ts` works unchanged
|
||||
- [ ] Agent queries work: `bun run agent.ts "How does if statement work?"`
|
||||
- [ ] Section retrieval works: fetches all chunks from specific sectionid
|
||||
- [ ] TypeScript compiles: `bun run type-check`
|
||||
- [ ] Linting passes: `bun run lint`
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
**Recommended:** Clean re-ingest
|
||||
1. Delete `storage/` directory
|
||||
2. Run `bun run setup/ingest.ts`
|
||||
3. Run `bun run setup/build_graph.ts`
|
||||
4. Test agent queries
|
||||
|
||||
**Timeline:** ~2 hours
|
||||
@@ -0,0 +1,670 @@
|
||||
# Fix: Parent Section Breakdown Awareness + Sequential Breakdown
|
||||
|
||||
**Model:** fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo
|
||||
**Date:** 2026-03-31
|
||||
**Parent Plan:** [1774872124705-glowing-river.md](./1774872124705-glowing-river.md)
|
||||
|
||||
## Summary
|
||||
|
||||
This plan addresses two issues in the spec ingestion process:
|
||||
1. **Parent Awareness**: Parent sections now contain inline references to their subsections exactly where content was removed
|
||||
2. **Unified Breakdown Logic**: Single `breakDownSection` function handles all structural elements using `alwaysBreak` flag
|
||||
|
||||
**Key Features:**
|
||||
- **Unified breakdown**: `emu-clause`, `emu-table`, `emu-grammar`, `td`, `p` all use same logic
|
||||
- `alwaysBreak: true` for `emu-clause` - always extracts children to build hierarchy
|
||||
- `alwaysBreak: false` for other tags - only extracts if content > 5000 chars
|
||||
- **Sequential tags**: Tried in order (emu-clause → emu-table → emu-grammar → td → p)
|
||||
- **Recursive check**: Each extracted subsection is also checked and can be further broken down
|
||||
- **Hierarchical IDs**: Show full path like `sec-if-statement-emu-table-1-td-2`
|
||||
- **Inline markers**: `[Subsection available: title "X" at sectionid: `ID`]` appears where content was removed
|
||||
- **Max depth**: 3 levels prevents excessive nesting
|
||||
- **Metadata tracking**: `subsections` array lists children (breakdown type derived from IDs)
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `setup/ingest.ts` | Add parent chunk with subsection awareness + recursive breakdown |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Unified Breakdown Strategy
|
||||
|
||||
All structural elements use the same breakdown logic with an `alwaysBreak` flag:
|
||||
|
||||
| Tag | alwaysBreak | Extract children? | When to extract |
|
||||
|-----|-------------|-------------------|-----------------|
|
||||
| `emu-clause` | `true` | Always | Defines hierarchy |
|
||||
| `emu-table` | `false` | Only if > threshold | Large tables |
|
||||
| `emu-grammar` | `false` | Only if > threshold | Large grammars |
|
||||
| `td` | `false` | Only if > threshold | Large table cells |
|
||||
| `p` | `false` | Only if > threshold | Large prose |
|
||||
|
||||
**Flow:**
|
||||
1. Start with root content (full HTML or section)
|
||||
2. Try `emu-clause` first - always extract children to build hierarchy
|
||||
3. For each extracted emu-clause content, apply sequential breakdown
|
||||
4. Try `emu-table` → `emu-grammar` → `td` → `p` only if content still large
|
||||
5. Recursively process extracted subsections
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const LARGE_DOC_THRESHOLD = 5000;
|
||||
const MAX_RECURSION_DEPTH = 3;
|
||||
|
||||
interface BreakdownTag {
|
||||
tag: string;
|
||||
alwaysBreak: boolean;
|
||||
titleSelector?: string; // CSS selector to extract title
|
||||
idSelector?: string; // CSS selector or attribute to extract ID
|
||||
idAttribute?: string; // HTML attribute containing ID (default: "id")
|
||||
}
|
||||
|
||||
const BREAKDOWN_TAGS: BreakdownTag[] = [
|
||||
{ tag: "emu-clause", alwaysBreak: true, titleSelector: "h1", idAttribute: "id" },
|
||||
{ tag: "emu-table", alwaysBreak: false, titleSelector: "caption" },
|
||||
{ tag: "emu-grammar", alwaysBreak: false },
|
||||
{ tag: "td", alwaysBreak: false },
|
||||
{ tag: "p", alwaysBreak: false },
|
||||
];
|
||||
```
|
||||
|
||||
### Unified Breakdown Function
|
||||
|
||||
```typescript
|
||||
interface BreakdownResult {
|
||||
parentDoc: {
|
||||
content: string;
|
||||
tagUsed: string | null;
|
||||
subsections: string[];
|
||||
};
|
||||
subsectionDocs: Document[];
|
||||
}
|
||||
|
||||
interface BreakdownContext {
|
||||
html: string;
|
||||
baseId: string;
|
||||
baseTitle: string;
|
||||
sourceFile: string;
|
||||
parentId: string | null;
|
||||
depth: number;
|
||||
startFromIndex: number; // Which tag in BREAKDOWN_TAGS to start from
|
||||
}
|
||||
|
||||
function breakDownSection(ctx: BreakdownContext): BreakdownResult {
|
||||
const { html, baseId, baseTitle, sourceFile, parentId, depth, startFromIndex } = ctx;
|
||||
|
||||
const $ = cheerio.load(`<div>${html}</div>`);
|
||||
const $section = $("div").first();
|
||||
const fullText = $section.text().trim();
|
||||
|
||||
// Try each breakdown tag starting from startFromIndex
|
||||
const subsectionIds: string[] = [];
|
||||
const subsectionDocs: Document[] = [];
|
||||
let remainingHtml = html;
|
||||
let tagUsed: string | null = null;
|
||||
|
||||
for (let i = startFromIndex; i < BREAKDOWN_TAGS.length; i++) {
|
||||
const tagConfig = BREAKDOWN_TAGS[i];
|
||||
const { tag: tagName, alwaysBreak, titleSelector, idSelector, idAttribute = "id" } = tagConfig;
|
||||
|
||||
const $temp = cheerio.load(`<div>${remainingHtml}</div>`);
|
||||
const $tempSection = $("div").first();
|
||||
|
||||
// Check if this tag exists
|
||||
if ($tempSection.find(tagName).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if we should break
|
||||
const shouldBreak = alwaysBreak || fullText.length > LARGE_DOC_THRESHOLD;
|
||||
|
||||
if (!shouldBreak) {
|
||||
// Skip this tag, continue to next
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract elements of this tag
|
||||
let counter = 1;
|
||||
$tempSection.find(tagName).each((_, elem) => {
|
||||
const elemHtml = $(elem).html() || "";
|
||||
const elemText = $(elem).text().trim();
|
||||
|
||||
if (elemText) {
|
||||
// Get element title if selector provided
|
||||
let elemTitle = "";
|
||||
if (titleSelector) {
|
||||
elemTitle = $(elem).find(titleSelector).first().text().trim() ||
|
||||
$(elem).attr("id") ||
|
||||
"";
|
||||
}
|
||||
|
||||
// Get element ID using configurable selectors
|
||||
let elemId: string | undefined;
|
||||
|
||||
if (idSelector) {
|
||||
// Use CSS selector to find ID
|
||||
elemId = $(elem).find(idSelector).first().attr(idAttribute) ||
|
||||
$(elem).find(idSelector).first().text().trim();
|
||||
} else {
|
||||
// Use attribute directly from element
|
||||
elemId = $(elem).attr(idAttribute);
|
||||
}
|
||||
|
||||
const subId = elemId || `${baseId}-${tagName}-${counter}`;
|
||||
|
||||
subsectionIds.push(subId);
|
||||
|
||||
// Always continue with next tag for more granular breakdown
|
||||
// (Structural tags like emu-clause have nested ones removed, so no risk of re-processing)
|
||||
const nextStartIndex = i + 1;
|
||||
|
||||
const subResult = breakDownSection({
|
||||
html: elemHtml,
|
||||
baseId: subId,
|
||||
baseTitle: elemTitle || `${baseTitle} [${tagName}]`,
|
||||
sourceFile,
|
||||
parentId: baseId,
|
||||
depth: depth + 1,
|
||||
startFromIndex: nextStartIndex,
|
||||
});
|
||||
|
||||
// If subsection was broken down further
|
||||
if (subResult.subsectionDocs.length > 0) {
|
||||
subsectionDocs.push(...subResult.subsectionDocs);
|
||||
|
||||
// Add subsection's parent document if it has children
|
||||
if (subResult.parentDoc.subsections.length > 0) {
|
||||
subsectionDocs.push(new Document({
|
||||
pageContent: [
|
||||
`[Section ${subId}: ${elemTitle || baseTitle}]`,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
cheerio.load(subResult.parentDoc.content).text().trim()
|
||||
].join("\n"),
|
||||
metadata: {
|
||||
source: sourceFile,
|
||||
sectionid: subId,
|
||||
sectiontitle: elemTitle || `${baseTitle} [${tagName}]`,
|
||||
type: "specification",
|
||||
parentsectionid: baseId,
|
||||
subsections: subResult.parentDoc.subsections,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Subsection is leaf - create document
|
||||
subsectionDocs.push(new Document({
|
||||
pageContent: elemText,
|
||||
metadata: {
|
||||
source: sourceFile,
|
||||
sectionid: subId,
|
||||
sectiontitle: elemTitle || `${baseTitle} [${tagName}]`,
|
||||
type: "specification",
|
||||
parentsectionid: baseId,
|
||||
subsections: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
counter++;
|
||||
|
||||
// Create marker with title if available
|
||||
const markerText = elemTitle
|
||||
? `[Subsection available: title "${elemTitle}" at sectionid: \`${subId}\`]`
|
||||
: `[Subsection available at sectionid: \`${subId}\`]`;
|
||||
$(elem).replaceWith(`<p>${markerText}</p>`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if breakdown was effective
|
||||
remainingHtml = $tempSection.html() || "";
|
||||
const remainingText = $tempSection.text().trim();
|
||||
|
||||
if (subsectionIds.length > 0) {
|
||||
tagUsed = tagName;
|
||||
|
||||
// For alwaysBreak tags, we don't check size - we extracted all children
|
||||
// For conditional tags, stop if remaining content is small enough
|
||||
if (!alwaysBreak && remainingText.length <= LARGE_DOC_THRESHOLD) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parentDoc: {
|
||||
content: remainingHtml,
|
||||
tagUsed,
|
||||
subsections: subsectionIds
|
||||
},
|
||||
subsectionDocs
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Document Creation with Unified Breakdown
|
||||
|
||||
```typescript
|
||||
async function ingestSpec(): Promise<Document[]> {
|
||||
const htmlFiles = await glob(path.join(SPEC_DIR, "*.html"));
|
||||
const documents: Document[] = [];
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const $ = cheerio.load(content);
|
||||
|
||||
// Get the main spec content (excluding nested emu-clause for now)
|
||||
const mainContent = $("body").html() || "";
|
||||
|
||||
// Process entire document starting with emu-clause (alwaysBreak=true)
|
||||
const result = breakDownSection({
|
||||
html: mainContent,
|
||||
baseId: "root",
|
||||
baseTitle: "ECMAScript Specification",
|
||||
sourceFile: file,
|
||||
parentId: null,
|
||||
depth: 0,
|
||||
startFromIndex: 0, // Start with emu-clause (index 0)
|
||||
});
|
||||
|
||||
// Add all documents from breakdown
|
||||
documents.push(...result.subsectionDocs);
|
||||
|
||||
// If root has remaining content, add as document
|
||||
if (result.parentDoc.content.trim()) {
|
||||
documents.push(new Document({
|
||||
pageContent: cheerio.load(result.parentDoc.content).text().trim(),
|
||||
metadata: {
|
||||
source: file,
|
||||
sectionid: "root",
|
||||
sectiontitle: "ECMAScript Specification",
|
||||
type: "specification",
|
||||
parentsectionid: null,
|
||||
subsections: result.parentDoc.subsections,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
```
|
||||
|
||||
### Simplified Alternative (Direct emu-clause Processing)
|
||||
|
||||
```typescript
|
||||
async function ingestSpec(): Promise<Document[]> {
|
||||
const htmlFiles = await glob(path.join(SPEC_DIR, "*.html"));
|
||||
const documents: Document[] = [];
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
const $ = cheerio.load(content);
|
||||
|
||||
// Process each top-level emu-clause
|
||||
$("emu-clause").each((_i, elem) => {
|
||||
const id = $(elem).attr("id");
|
||||
const title = $(elem).find("h1").first().text().trim();
|
||||
const html = $(elem)
|
||||
.clone()
|
||||
.children("emu-clause") // Remove nested clauses
|
||||
.remove()
|
||||
.end()
|
||||
.html() || "";
|
||||
|
||||
if (!id || !html.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process this emu-clause content (no emu-clause left, starts from emu-table)
|
||||
const result = breakDownSection({
|
||||
html,
|
||||
baseId: id,
|
||||
baseTitle: title || id,
|
||||
sourceFile: file,
|
||||
parentId: null,
|
||||
depth: 0,
|
||||
startFromIndex: 0, // Start from beginning, but emu-clause already removed
|
||||
});
|
||||
|
||||
// Create parent document
|
||||
if (result.parentDoc.subsections.length > 0) {
|
||||
const parentContent = [
|
||||
`[Section ${id}: ${title}]`,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
cheerio.load(result.parentDoc.content).text().trim()
|
||||
].join("\n");
|
||||
|
||||
documents.push(new Document({
|
||||
pageContent: parentContent,
|
||||
metadata: {
|
||||
source: file,
|
||||
sectionid: id,
|
||||
sectiontitle: title,
|
||||
type: "specification",
|
||||
parentsectionid: null,
|
||||
subsections: result.parentDoc.subsections,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add all subsection documents
|
||||
documents.push(...result.subsectionDocs);
|
||||
} else {
|
||||
// No breakdown needed - add as leaf
|
||||
documents.push(new Document({
|
||||
pageContent: cheerio.load(html).text().trim(),
|
||||
metadata: {
|
||||
source: file,
|
||||
sectionid: id,
|
||||
sectiontitle: title,
|
||||
type: "specification",
|
||||
parentsectionid: null,
|
||||
subsections: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
```
|
||||
|
||||
### Hierarchical Structure
|
||||
|
||||
The unified breakdown creates a consistent hierarchy:
|
||||
|
||||
```
|
||||
sec-if-statement (parent)
|
||||
├── sec-if-statement-emu-table-1 (parent)
|
||||
│ ├── sec-if-statement-emu-table-1-td-1
|
||||
│ ├── sec-if-statement-emu-table-1-td-2
|
||||
│ └── ...
|
||||
├── sec-if-statement-emu-table-2 (leaf)
|
||||
├── sec-if-statement-emu-grammar-1 (leaf)
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Breakdown type derivation:** From subsection ID pattern:
|
||||
- `sec-if-statement-emu-table-1` → broke down by `emu-table`
|
||||
- `sec-if-statement-emu-table-1-td-2` → table-1 broke down by `td`
|
||||
|
||||
### Querying Strategy
|
||||
|
||||
**Get all chunks from a section (recursive):**
|
||||
|
||||
```typescript
|
||||
async function getAllChunks(sectionId: string): Promise<Document[]> {
|
||||
const allDocs: Document[] = [];
|
||||
const queue: string[] = [sectionId];
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!;
|
||||
if (visited.has(currentId)) continue;
|
||||
visited.add(currentId);
|
||||
|
||||
const results = await table
|
||||
.query()
|
||||
.where(`sectionid = '${currentId}'`)
|
||||
.limit(100)
|
||||
.toArray();
|
||||
|
||||
for (const result of results) {
|
||||
allDocs.push(result);
|
||||
|
||||
// If has subsections, add them to queue
|
||||
if (result.subsections && result.subsections.length > 0) {
|
||||
queue.push(...result.subsections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDocs;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent Usage
|
||||
|
||||
### Parent Discovery
|
||||
|
||||
When the agent retrieves a parent chunk (has `subsections` array with items), it will see inline markers where content was extracted:
|
||||
|
||||
```
|
||||
[Section sec-if-statement: If Statement]
|
||||
|
||||
---
|
||||
|
||||
The if statement evaluates a condition...
|
||||
|
||||
[Subsection available: title "Static Semantics: Early Errors" at sectionid: `sec-if-statement-emu-table-1`]
|
||||
|
||||
The result of the evaluation determines...
|
||||
|
||||
[Subsection available: title "IfStatement" at sectionid: `sec-if-statement-emu-grammar-1`]
|
||||
|
||||
Further text continues...
|
||||
```
|
||||
|
||||
### Deep Breakdown Example
|
||||
|
||||
When a subsection (like a large table) is also broken down:
|
||||
|
||||
**Parent chunk:**
|
||||
```
|
||||
[Section sec-if-statement: If Statement]
|
||||
|
||||
---
|
||||
|
||||
The if statement evaluates a condition...
|
||||
|
||||
[Subsection available: title "Static Semantics: Early Errors" at sectionid: `sec-if-statement-emu-table-1`]
|
||||
|
||||
[Subsection available: title "IfStatement" at sectionid: `sec-if-statement-emu-grammar-1`]
|
||||
|
||||
The result of the evaluation determines...
|
||||
```
|
||||
|
||||
**Subsection parent (table-1 broken down further by td):**
|
||||
```
|
||||
[Section sec-if-statement-emu-table-1: If Statement [emu-table]]
|
||||
|
||||
---
|
||||
|
||||
Table header row...
|
||||
|
||||
[Subsection available at sectionid: `sec-if-statement-emu-table-1-td-1`]
|
||||
|
||||
[Subsection available at sectionid: `sec-if-statement-emu-table-1-td-2`]
|
||||
|
||||
Table footer...
|
||||
```
|
||||
|
||||
**Leaf chunk (table cell):**
|
||||
```
|
||||
[Section sec-if-statement-emu-table-1-td-1: If Statement [emu-table] [td]]
|
||||
(Actual table cell content here)
|
||||
```
|
||||
|
||||
### Agent Strategy
|
||||
|
||||
1. Retrieve emu-clause by semantic search
|
||||
2. Check if `subsections?.length > 0` to detect parent
|
||||
3. Derive breakdown type from subsection IDs (e.g., `*-emu-table-*` means table breakdown)
|
||||
4. Recursively check fetched subsections - they may also have subsections!
|
||||
5. Continue until reaching leaf nodes (no subsections)
|
||||
6. Combine all retrieved chunks for complete answer
|
||||
|
||||
## Changes to agent.ts
|
||||
|
||||
### Enhanced fetch_section_chunks Tool
|
||||
|
||||
```typescript
|
||||
const sectionRetrieverTool = new DynamicTool({
|
||||
name: "fetch_section_chunks",
|
||||
description: "Retrieves all text chunks from a specific specification section by sectionid. " +
|
||||
"Supports recursive fetching - if a section has subsections, it will fetch all descendants. " +
|
||||
"Use this to get complete content when you see 'Subsection available' references in parent chunks.",
|
||||
func: async (sectionId) => {
|
||||
const allDocs: string[] = [];
|
||||
const queue: string[] = [sectionId];
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!;
|
||||
if (visited.has(currentId)) continue;
|
||||
visited.add(currentId);
|
||||
|
||||
const results = await table
|
||||
.query()
|
||||
.where(`sectionid = '${currentId}'`)
|
||||
.limit(100)
|
||||
.toArray();
|
||||
|
||||
for (const result of results) {
|
||||
allDocs.push(result.text || "");
|
||||
|
||||
// Add subsections to queue for recursive fetching
|
||||
if (result.subsections && Array.isArray(result.subsections)) {
|
||||
queue.push(...result.subsections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDocs.join("\n\n---\n\n");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### New Check for Breakdown Tool (Optional)
|
||||
|
||||
```typescript
|
||||
const checkSubsectionsTool = new DynamicTool({
|
||||
name: "check_subsections",
|
||||
description: "Checks if a section has subsections and returns their IDs. " +
|
||||
"Use when you need to selectively fetch specific subsection types (tables, grammar, prose).",
|
||||
func: async (sectionId) => {
|
||||
const result = await table
|
||||
.query()
|
||||
.where(`sectionid = '${sectionId}'`)
|
||||
.limit(1)
|
||||
.toArray();
|
||||
|
||||
if (result.length === 0) {
|
||||
return `No section found with id: ${sectionId}`;
|
||||
}
|
||||
|
||||
const doc = result[0];
|
||||
if (!doc.subsections || doc.subsections.length === 0) {
|
||||
return `Section ${sectionId} has no subsections.`;
|
||||
}
|
||||
|
||||
return [
|
||||
`Section ${sectionId} has ${doc.subsections.length} subsections:`,
|
||||
...doc.subsections.map((id: string) => {
|
||||
const match = id.match(/-(table|grammar|prose)/);
|
||||
const type = match ? match[1] : 'subsection';
|
||||
return ` - ${type}: ${id}`;
|
||||
})
|
||||
].join("\n");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Basic Functionality
|
||||
- [ ] Run `bun run setup/ingest.ts` completes without errors
|
||||
- [ ] Verify parent chunks have `subsections` array with children
|
||||
- [ ] Verify leaf chunks have empty/null `subsections`
|
||||
- [ ] Verify subsection references are in parent chunk content
|
||||
- [ ] Verify subsection IDs show correct breakdown path (e.g., `section-emu-table-1`)
|
||||
|
||||
### Sequential Breakdown
|
||||
- [ ] Find a section with >5000 chars and tables, verify emu-table breakdown happens
|
||||
- [ ] Find a section with large grammar block, verify emu-grammar breakdown happens
|
||||
- [ ] Find a section where emu-table leaves large remainder, verify emu-grammar also extracted
|
||||
- [ ] Derive breakdown type from subsection IDs (first tag in ID path)
|
||||
|
||||
### Recursive Breakdown
|
||||
- [ ] Find a large table (section-emu-table-1 > 5000 chars), verify it gets further broken down
|
||||
- [ ] Check that nested breakdown uses next tags in sequence (emu-grammar, td, p)
|
||||
- [ ] Verify nested IDs: `sec-if-statement-emu-table-1-td-2` (table broken down by td)
|
||||
- [ ] Test max depth limit (3): deeply nested sections stop at depth 3
|
||||
- [ ] Verify all subsections have `parentsectionid` pointing to their immediate parent
|
||||
|
||||
### Query Testing
|
||||
- [ ] Query for parent section, verify content includes subsection lines
|
||||
- [ ] Query for subsection by ID (e.g., `sec-if-statement-emu-table-1`)
|
||||
- [ ] Test `fetch_section_chunks` with parent ID returns all subsections
|
||||
- [ ] Verify agent can discover and fetch subsections automatically
|
||||
- [ ] Test agent query on large section to verify subsection discovery works
|
||||
|
||||
### Indexes
|
||||
- [ ] Verify scalar indexes are created for: sectionid, type
|
||||
- [ ] Test WHERE clause queries on indexed columns
|
||||
|
||||
## Limits & Edge Cases
|
||||
|
||||
### Sequential Breakdown Order
|
||||
Tags are tried in strict order:
|
||||
1. `emu-table` - Best semantic unit for spec tables
|
||||
2. `emu-grammar` - Grammar productions are natural boundaries
|
||||
3. `td` - Table cells (only if tables couldn't reduce enough)
|
||||
4. `p` - Paragraphs (last resort, breaks prose)
|
||||
|
||||
**Rationale:** Structure-aware breakdown preserves semantic meaning better than arbitrary text splitting.
|
||||
|
||||
### Recursive Breakdown Logic
|
||||
- **Applied to every extracted subsection**: Each extracted chunk is also checked for size
|
||||
- **Sequential tags continue**: If `sec-table-1` is large, try next tags (emu-grammar, td, p)
|
||||
- **Depth tracking**: `depth` field shows nesting level (0 = emu-clause, max 3)
|
||||
- **ID nesting**: IDs reflect full path: `section-table-1-td-2` means table 1 was broken down by td
|
||||
|
||||
### Content Size Threshold
|
||||
- **Default**: 5000 characters (configurable via `LARGE_DOC_THRESHOLD`)
|
||||
- **Applied at every level**: Parent and all subsections checked against threshold
|
||||
- **Max depth safety**: Even if still large at depth 3, no further breakdown (prevents infinite recursion)
|
||||
|
||||
### Hierarchical Structure
|
||||
- **Nested IDs**: IDs show full breakdown path like `sec-if-statement-emu-table-1-td-2`
|
||||
- **Multi-level**: Subsections can be parents with their own children
|
||||
- **Parent tracking**: `parentsectionid` always points to immediate parent (could be subsection or emu-clause)
|
||||
|
||||
### Large Remainders at Max Depth
|
||||
If at depth 3 content is still > threshold:
|
||||
- Keep it as-is (chunk may exceed threshold)
|
||||
- Add warning in content: "(Content exceeds threshold - max depth reached)"
|
||||
- This is rare - depth 3 with td/p breakdowns should handle most cases
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. **Clean slate**: Delete existing `storage/` directory
|
||||
2. **Re-ingest**: Run `bun run setup/ingest.ts`
|
||||
3. **Verify structure**: Check that large sections have hierarchical breakdowns
|
||||
4. **Test breakdown types**: Look for examples of each breakdown type by ID pattern:
|
||||
```typescript
|
||||
// Query to find emu-table breakdowns
|
||||
const tableBreakdowns = await table
|
||||
.query()
|
||||
.where(`sectionid LIKE '%-emu-table-%'`)
|
||||
.limit(10)
|
||||
.toArray();
|
||||
console.log(tableBreakdowns.map(s => s.sectionid));
|
||||
```
|
||||
5. **Test recursive breakdown**: Find deeply nested examples:
|
||||
```typescript
|
||||
// Query for nested breakdowns (table cells broken down)
|
||||
const nested = await table
|
||||
.query()
|
||||
.where(`sectionid LIKE '%-td-%'`)
|
||||
.limit(10)
|
||||
.toArray();
|
||||
console.log(nested.map(s => ({id: s.sectionid, parent: s.parentsectionid})));
|
||||
```
|
||||
6. **Test agent**: Run queries on large sections like "sec-globaldeclarationinstantiation"
|
||||
7. **Verify hierarchy**: Confirm parent-child chain is correct (e.g., `td` → `table` → `section`)
|
||||
@@ -0,0 +1,407 @@
|
||||
---
|
||||
model: accounts/fireworks/routers/kimi-k2p5-turbo
|
||||
---
|
||||
|
||||
# Plan: `ask262Debug` Module for engine262
|
||||
|
||||
## Overview
|
||||
|
||||
Runtime execution tracker that records which ECMAScript spec sections are entered during execution. The Babel transform auto-injects `ask262Debug.mark()` at the start of every function body with URL comments — developer writes nothing. All metadata (sectionId, file, line) resolved at build time.
|
||||
|
||||
**Primary Use Case:** AI analysis of execution flow mapped to ECMAScript spec sections for understanding JavaScript engine behavior.
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `engine262/src/ask262-debug.mts` | **CREATE** — the module with deduplication logic |
|
||||
| `engine262/src/index.mts` | **MODIFY** — add named re-export |
|
||||
| `engine262/scripts/transform.mts` | **MODIFY** — auto-inject `mark()` and import |
|
||||
| `engine262/scripts/verify-ask262-debug.mts` | **CREATE** — post-build verification script |
|
||||
|
||||
## 1. `scripts/transform.mts` — Enhance the Babel Plugin
|
||||
|
||||
### 1a. Extend State Interface
|
||||
|
||||
Add `ask262Debug` to the `NeededNames` type and extend `State` interface:
|
||||
|
||||
```typescript
|
||||
type NeededNames = 'Completion' | 'AbruptCompletion' | 'Assert' | 'Call' |
|
||||
'IteratorClose' | 'AsyncIteratorClose' | 'Value' |
|
||||
'skipDebugger' | 'ask262Debug'; // NEW
|
||||
|
||||
interface State extends PluginPass {
|
||||
needed: Partial<Record<NeededNames, boolean>>;
|
||||
fileRelativePath?: string; // NEW: captured at Program enter
|
||||
}
|
||||
```
|
||||
|
||||
### 1b. Add Import Creator Function
|
||||
|
||||
Add alongside existing `createImport*` functions:
|
||||
|
||||
```typescript
|
||||
function createImportAsk262Debug() {
|
||||
return template.ast(`
|
||||
import { ask262Debug } from "#self";
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
||||
### 1c. Refactor `addSectionFromComments`
|
||||
|
||||
The current function (lines 101-133) handles only single URLs. Update to capture multiple section IDs and inject `mark()`:
|
||||
|
||||
```typescript
|
||||
function addSectionFromComments(
|
||||
path: NodePath<t.FunctionDeclaration> | NodePath<t.VariableDeclaration> |
|
||||
NodePath<t.ExportNamedDeclaration> | NodePath<t.ClassMethod> | NodePath<t.ObjectMethod>,
|
||||
state: State,
|
||||
getName: () => string,
|
||||
getBody: () => t.BlockStatement | null
|
||||
) {
|
||||
if (!path.node.leadingComments) return;
|
||||
|
||||
const sectionIds: string[] = [];
|
||||
let url = '';
|
||||
|
||||
for (const c of path.node.leadingComments) {
|
||||
for (const line of c.value.split('\n')) {
|
||||
const matches = line.match(/#(sec-[a-zA-Z0-9._%-]+)/g);
|
||||
if (matches) {
|
||||
sectionIds.push(...matches.map(m => m.substring(1)));
|
||||
if (!url) {
|
||||
const section = line.split(' ').find(l => l.includes('#sec'));
|
||||
// Only capture external URLs (skip local/fragment references)
|
||||
if (section?.startsWith('https://')) {
|
||||
url = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionIds.length === 0) return;
|
||||
|
||||
const name = getName();
|
||||
|
||||
// 1. Keep existing .section (backward compat) - uses first URL
|
||||
// Only applies to named functions
|
||||
if (name && url) {
|
||||
path.insertAfter(template.ast(`${name}.section = '${url}';`));
|
||||
}
|
||||
|
||||
// 2. Get function body via callback (node-type check done by caller)
|
||||
const body = getBody();
|
||||
|
||||
// 3. Inject mark() at start of function body (only for block bodies)
|
||||
if (body?.type === 'BlockStatement') {
|
||||
const line = path.node.loc?.start.line ?? 0;
|
||||
const markCall = template.statement(
|
||||
`ask262Debug.mark(${JSON.stringify(sectionIds)}, ${JSON.stringify(state.fileRelativePath)}, ${line});`
|
||||
)();
|
||||
body.body.unshift(markCall);
|
||||
state.needed.ask262Debug = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1d. Update Visitors
|
||||
|
||||
Update existing visitors to use new `addSectionFromComments` signature, and add new visitors for ClassMethod and ObjectMethod:
|
||||
|
||||
```typescript
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path, state) {
|
||||
state.needed = {};
|
||||
// Capture relative file path from state.filename
|
||||
const absolutePath = state.filename || '';
|
||||
state.fileRelativePath = absolutePath.replace(/.*\/(src\/)/, '$1').replace(/\.mts$/, '.mts') || 'unknown.mts';
|
||||
},
|
||||
exit(path, state) {
|
||||
// ... existing imports (skipDebugger, Completion, etc.) ...
|
||||
// NEW: Add ask262Debug import if needed
|
||||
if (state.needed.ask262Debug) {
|
||||
path.unshiftContainer('body', createImportAsk262Debug());
|
||||
}
|
||||
},
|
||||
},
|
||||
FunctionDeclaration(path, state) {
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => path.node.id!.name,
|
||||
() => path.node.body
|
||||
);
|
||||
},
|
||||
VariableDeclaration(path, state) {
|
||||
const init = path.get('declarations.0.init');
|
||||
if (init.isFunctionExpression()) {
|
||||
const id = path.node.declarations[0].id as t.Identifier;
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => id.name,
|
||||
() => init.node.body
|
||||
);
|
||||
} else if (init.isArrowFunctionExpression()) {
|
||||
const id = path.node.declarations[0].id as t.Identifier;
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => id.name,
|
||||
() => init.node.body.type === 'BlockStatement' ? init.node.body : null
|
||||
);
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(path, state) {
|
||||
const declaration = path.node.declaration;
|
||||
if (declaration?.type === 'FunctionDeclaration') {
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => declaration.id!.name,
|
||||
() => declaration.body
|
||||
);
|
||||
}
|
||||
},
|
||||
// NEW: Add ClassMethod visitor
|
||||
ClassMethod(path, state) {
|
||||
const key = path.node.key;
|
||||
if (key.type === 'Identifier') {
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => key.name,
|
||||
() => path.node.body
|
||||
);
|
||||
}
|
||||
},
|
||||
// NEW: Add ObjectMethod visitor
|
||||
ObjectMethod(path, state) {
|
||||
const key = path.node.key;
|
||||
if (key.type === 'Identifier') {
|
||||
addSectionFromComments(
|
||||
path, state,
|
||||
() => key.name,
|
||||
() => path.node.body
|
||||
);
|
||||
}
|
||||
},
|
||||
// ... rest of existing visitors (CallExpression, ThrowStatement, etc.) unchanged
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 1e. Arrow Function Special Handling
|
||||
|
||||
The updated `addSectionFromComments` handles this automatically:
|
||||
- Arrow with block body `() => { ... }`: `.section` + `mark()` injected
|
||||
- Arrow with expression body `() => expr`: `.section` only, no `mark()`
|
||||
|
||||
## 2. `engine262/src/index.mts` — Add Named Re-export
|
||||
|
||||
Add the re-export alongside existing exports (around line 20):
|
||||
|
||||
```typescript
|
||||
// Add to existing exports from './helpers.mts' or create new export section
|
||||
export { ask262Debug, type MarkData } from './ask262-debug.mts';
|
||||
```
|
||||
|
||||
## 3. `engine262/src/ask262-debug.mts` — The Module
|
||||
|
||||
Deduplicated data collection. No stack parsing, no `node:` imports, no decorators.
|
||||
|
||||
```typescript
|
||||
export interface MarkData {
|
||||
readonly sectionIds: string[];
|
||||
readonly fileRelativePath: string;
|
||||
readonly lineNumber: number;
|
||||
readonly important: boolean;
|
||||
}
|
||||
|
||||
interface MarkKey {
|
||||
readonly sectionIds: string;
|
||||
readonly file: string;
|
||||
readonly line: number;
|
||||
}
|
||||
|
||||
class Ask262Debug {
|
||||
marks: MarkData[] = [];
|
||||
private _importantStack = false;
|
||||
private _markIndex = new Map<string, number>(); // key -> index in marks
|
||||
|
||||
private _makeKey(sectionIds: string[], file: string, line: number): string {
|
||||
return `${sectionIds.join(',')}|${file}|${line}`;
|
||||
}
|
||||
|
||||
mark(sectionIds: string[], file: string, line: number) {
|
||||
const key = this._makeKey(sectionIds, file, line);
|
||||
const existingIndex = this._markIndex.get(key);
|
||||
|
||||
if (existingIndex !== undefined) {
|
||||
// Merge important flag
|
||||
const existing = this.marks[existingIndex];
|
||||
if (this._importantStack && !existing.important) {
|
||||
(this.marks[existingIndex] as any).important = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newMark: MarkData = {
|
||||
sectionIds: [...sectionIds], // defensive copy
|
||||
fileRelativePath: file,
|
||||
lineNumber: line,
|
||||
important: this._importantStack,
|
||||
};
|
||||
|
||||
this._markIndex.set(key, this.marks.length);
|
||||
this.marks.push(newMark);
|
||||
}
|
||||
|
||||
startImportant() {
|
||||
this._importantStack = true;
|
||||
}
|
||||
|
||||
stopImportant() {
|
||||
this._importantStack = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const ask262Debug = new Ask262Debug();
|
||||
```
|
||||
|
||||
### Key Behaviors
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Duplicate `(sectionIds, file, line)` | Keep one entry, merge `important` flags (OR logic) |
|
||||
| Generator resumption | Mark on every `.next()` call (may create duplicates if same location) |
|
||||
| Nested functions | Each entry adds its mark (chronological trace) |
|
||||
|
||||
## 4. `engine262/scripts/verify-ask262-debug.mts` — Verification Script
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
import { ask262Debug, Agent, ManagedRealm } from '#self';
|
||||
|
||||
console.log('=== ask262Debug Verification ===\n');
|
||||
|
||||
// 1. Check export exists
|
||||
console.log('1. Checking export...');
|
||||
if (!ask262Debug) {
|
||||
console.error('FAIL: ask262Debug not exported');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' ✓ ask262Debug exported\n');
|
||||
|
||||
// 2. Run code that hits known spec sections
|
||||
console.log('2. Executing test code...');
|
||||
const agent = new Agent();
|
||||
const realm = new ManagedRealm({ agent });
|
||||
|
||||
realm.evaluateScript(`
|
||||
// Array.prototype.every - should hit sec-array.prototype.every
|
||||
[1, 2, 3].every(x => x > 0);
|
||||
|
||||
// Proxy creation - should hit sec-proxycreate or similar
|
||||
new Proxy({}, {});
|
||||
`);
|
||||
|
||||
// 4. Verify marks were captured
|
||||
console.log('3. Checking marks...');
|
||||
const marks = ask262Debug.marks;
|
||||
console.log(` Captured ${marks.length} unique marks\n`);
|
||||
|
||||
if (marks.length === 0) {
|
||||
console.error('FAIL: No marks captured');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 5. Show sample marks
|
||||
console.log('4. Sample marks:');
|
||||
marks.slice(0, 5).forEach((m, i) => {
|
||||
console.log(` [${i}] ${m.sectionIds.join(', ')} @ ${m.fileRelativePath}:${m.lineNumber}${m.important ? ' [important]' : ''}`);
|
||||
});
|
||||
|
||||
console.log('=== All Checks Passed ===');
|
||||
```
|
||||
|
||||
## Key Design Decisions Summary
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| **Use case** | AI execution flow analysis | Map runtime to spec sections |
|
||||
| **Import pattern** | `import { ask262Debug } from '#self'` | Uses existing pattern (315+ occurrences) |
|
||||
| **Bundle strategy** | Included in engine262.mjs | Single artifact, no config changes needed |
|
||||
| **Data structure** | Deduplicated array | Chronological trace with unique entries only |
|
||||
| **Deduplication key** | `(sectionIds, file, line)` | Same location = same semantic entry |
|
||||
| **Duplicate handling** | Merge `important` flags (OR) | If any call was important, mark it important |
|
||||
| **All metadata** | From transform (build time) | No runtime stack parsing, no `node:` imports |
|
||||
| **`mark()` injection** | Auto-injected at start of block body | Developer writes nothing |
|
||||
| **Arrow functions** | Block body: `.section` + `mark()`, Expression: `.section` only | Can't inject statement in expression |
|
||||
| **Multi-section** | Array of section IDs per function | Functions can implement multiple spec sections |
|
||||
| **`.section`** | Kept (backward compat) | Used by test262-intrinsics |
|
||||
| **Generators** | Mark on every `.next()` | Simpler, deduplication handles repeats |
|
||||
| **Nested functions** | Mark all | Chronological trace for AI analysis |
|
||||
| **`important`** | User-driven `startImportant()`/`stopImportant()` | Manual annotation of interesting sections |
|
||||
| **Function types** | FunctionDeclaration + ClassMethod + ObjectMethod + VariableDeclaration | Covers all URL comment patterns |
|
||||
| **Class/Object method keys** | Identifier names only | Simple, covers 99% of cases |
|
||||
| **File paths** | Relative to `engine262/src/`, `.mts` extension | Maps to source, not compiled output |
|
||||
| **Line numbers** | 1-indexed from Babel AST | Matches source file line numbers |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
1. [ ] **Create** `engine262/src/ask262-debug.mts` with the module code
|
||||
2. [ ] **Modify** `engine262/src/index.mts` - add re-export
|
||||
3. [ ] **Modify** `engine262/scripts/transform.mts`:
|
||||
- [ ] Add `ask262Debug` to `NeededNames` type
|
||||
- [ ] Extend `State` interface with `fileRelativePath`
|
||||
- [ ] Add `createImportAsk262Debug()` function
|
||||
- [ ] Update `addSectionFromComments()` signature and logic
|
||||
- [ ] Update `Program.exit` to inject ask262Debug import
|
||||
- [ ] Update `FunctionDeclaration` visitor
|
||||
- [ ] Update `VariableDeclaration` visitor
|
||||
- [ ] Update `ExportNamedDeclaration` visitor
|
||||
- [ ] Add `ClassMethod` visitor
|
||||
- [ ] Add `ObjectMethod` visitor
|
||||
4. [ ] **Create** `engine262/scripts/verify-ask262-debug.mts`
|
||||
5. [ ] **Type checking**: `cd engine262 && npx tsc -b .`
|
||||
6. [ ] **Linting**: `cd engine262 && npm run lint`
|
||||
7. [ ] **Build**: `cd engine262 && npm run build:engine`
|
||||
8. [ ] **Verification**: `cd engine262 && node --enable-source-maps --disable-warning=ExperimentalWarning --experimental-strip-types scripts/verify-ask262-debug.mts`
|
||||
|
||||
## Expected Output from Verification
|
||||
|
||||
```
|
||||
=== ask262Debug Verification ===
|
||||
|
||||
1. Checking export...
|
||||
✓ ask262Debug exported
|
||||
|
||||
2. Executing test code...
|
||||
3. Checking marks...
|
||||
Captured 47 unique marks
|
||||
|
||||
4. Sample marks:
|
||||
[0] sec-array.prototype.every @ intrinsics/ArrayPrototypeShared.mts:173
|
||||
[1] sec-proxycreate @ abstract-ops/proxy-objects.mts:540
|
||||
...
|
||||
|
||||
=== All Checks Passed ===
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: No marks captured after build
|
||||
**Cause:** Transform not injecting `mark()` calls
|
||||
**Fix:** Check that functions have URL comments with `#sec-` patterns
|
||||
|
||||
### Issue: Build fails with "Cannot find module '#self'"
|
||||
**Cause:** Rollup not resolving the import
|
||||
**Fix:** Ensure babel transform runs before resolution in rollup config
|
||||
|
||||
### Issue: Line numbers are off
|
||||
**Cause:** Babel source maps not configured
|
||||
**Fix:** Check that `sourceMap: true` in babelOptions and `loc` is preserved
|
||||
|
||||
### Issue: Duplicate marks for same function
|
||||
**Cause:** Generator resumption or recursion
|
||||
**Fix:** This is expected behavior - deduplication happens on `(sectionIds, file, line)`
|
||||
Reference in New Issue
Block a user