@agimon-ai/doompi-hashline 0.0.1-alpha.48 → 0.0.1-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ const e=26n*26n*26n,t=/^[A-Za-z0-9_-]{8}$/u,n=/^@file (.+)#([A-Za-z0-9_-]{8})$/u
5
5
  `)||e.includes(`\r`))throw Error(`Invalid file header path. Expected one non-empty path without line breaks.`);return`@file ${e}#${u(t)}`}function f(e){let t=n.exec(e),r=t?.[1],i=t?.[2];return r===void 0||i===void 0?void 0:{path:r,tag:i}}function p(e,t,n=``,r=e){return y(t),`${n}${t}#${o(r)}|${e}`}function m(e,t=1){return e.map((e,n)=>p(e,t+n)).join(`
6
6
  `)}function h(e){let t=r.exec(e),n=t?.[2],i=t?.[3];if(n===void 0||i===void 0)return;let a=Number.parseInt(n,10);if(!Number.isSafeInteger(a)||a<1)return;let o=v(t?.[1]);return o===void 0?{content:i,line:a}:{content:i,line:a,marker:o}}function g(e){if(e.includes(`
7
7
  `)||e.includes(`\r`))throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc, not a pasted multiline block.`);let t=i.exec(e);if(!t)throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc from the latest read or grep result.`);let n=Number.parseInt(t[1]??``,10),r=t[2]?.toLowerCase();if(!Number.isSafeInteger(n)||n<1||r===void 0)throw Error(`Invalid line anchor: ${e}. Expected a positive line number and three letters, such as 5#abc.`);return{line:n,hash:r}}function _(e,t){let n=l(e),r=S(t.map(e=>b(e,n))).sort(C);w(r);let i=[...n];for(let e of[...r].reverse()){let t=e.to.line-e.from.line+1,n=e.content===null||e.content===``?[]:T(e.content);i.splice(e.from.line-1,t,...n)}return{content:i.join(`
8
- `),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
8
+ `),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=/* @__PURE__ */ new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
9
9
  `)?t.slice(0,-1):t;return n===``?[]:n.split(`
10
10
  `)}exports.applyHashlineEdits=_,exports.formatFileHeader=d,exports.formatTaggedLine=p,exports.formatTaggedLines=m,exports.hashLine=o,exports.normalizeFileTag=u,exports.normalizeToLf=s,exports.parseFileHeader=f,exports.parseLineAnchor=g,exports.parseTaggedLine=h,exports.splitLines=l,exports.stripBom=c;
11
11
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/services/hashline/index.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../../types/hashline';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/services/hashline/index.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../../types/hashline';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ const e=26n*26n*26n,t=/^[A-Za-z0-9_-]{8}$/u,n=/^@file (.+)#([A-Za-z0-9_-]{8})$/u
5
5
  `)||e.includes(`\r`))throw Error(`Invalid file header path. Expected one non-empty path without line breaks.`);return`@file ${e}#${u(t)}`}function f(e){let t=n.exec(e),r=t?.[1],i=t?.[2];return r===void 0||i===void 0?void 0:{path:r,tag:i}}function p(e,t,n=``,r=e){return y(t),`${n}${t}#${o(r)}|${e}`}function m(e,t=1){return e.map((e,n)=>p(e,t+n)).join(`
6
6
  `)}function h(e){let t=r.exec(e),n=t?.[2],i=t?.[3];if(n===void 0||i===void 0)return;let a=Number.parseInt(n,10);if(!Number.isSafeInteger(a)||a<1)return;let o=v(t?.[1]);return o===void 0?{content:i,line:a}:{content:i,line:a,marker:o}}function g(e){if(e.includes(`
7
7
  `)||e.includes(`\r`))throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc, not a pasted multiline block.`);let t=i.exec(e);if(!t)throw Error(`Invalid line anchor: ${e}. Expected one anchor such as 5#abc from the latest read or grep result.`);let n=Number.parseInt(t[1]??``,10),r=t[2]?.toLowerCase();if(!Number.isSafeInteger(n)||n<1||r===void 0)throw Error(`Invalid line anchor: ${e}. Expected a positive line number and three letters, such as 5#abc.`);return{line:n,hash:r}}function _(e,t){let n=l(e),r=S(t.map(e=>b(e,n))).sort(C);w(r);let i=[...n];for(let e of[...r].reverse()){let t=e.to.line-e.from.line+1,n=e.content===null||e.content===``?[]:T(e.content);i.splice(e.from.line-1,t,...n)}return{content:i.join(`
8
- `),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
8
+ `),edits:r}}function v(e){if(e===`>> `)return`match`;if(e===` `)return`context`}function y(e){if(!Number.isSafeInteger(e)||e<1)throw Error(`Invalid tagged line number: ${String(e)}. Expected a positive safe integer.`)}function b(e,t){let n=g(e.from),r=g(e.to);if(n.line>r.line)throw Error(`Invalid edit range ${e.from} to ${e.to}: the starting line follows the ending line.`);if(r.line>t.length)throw Error(`Invalid edit range ${e.from} to ${e.to}: the file has ${t.length} lines.`);return x(n,t),x(r,t),{from:n,to:r,content:e.content??null,source:e}}function x(e,t){let n=t[e.line-1];if(n===void 0)throw Error(`Line ${e.line} is outside the current file.`);let r=o(n);if(r!==e.hash)throw Error(`Stale line anchor ${e.line}#${e.hash}. Current anchor is ${e.line}#${r}. Re-read the file and retry.`)}function S(e){let t=/* @__PURE__ */ new Map;for(let n of e){let e=JSON.stringify([n.from.line,n.from.hash,n.to.line,n.to.hash,n.content]);t.has(e)||t.set(e,n)}return[...t.values()]}function C(e,t){return e.from.line-t.from.line||e.to.line-t.to.line}function w(e){for(let t=1;t<e.length;t+=1){let n=e[t-1],r=e[t];if(n!==void 0&&r!==void 0&&r.from.line<=n.to.line)throw Error(`Overlapping edit ranges ${n.from.line}-${n.to.line} and ${r.from.line}-${r.to.line}. Merge them and retry.`)}}function T(e){let t=s(e),n=t.endsWith(`
9
9
  `)?t.slice(0,-1):t;return n===``?[]:n.split(`
10
10
  `)}export{_ as applyHashlineEdits,d as formatFileHeader,p as formatTaggedLine,m as formatTaggedLines,o as hashLine,u as normalizeFileTag,s as normalizeToLf,f as parseFileHeader,g as parseLineAnchor,h as parseTaggedLine,l as splitLines,c as stripBom};
11
11
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/hashline/index.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../../types/hashline';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/services/hashline/index.ts"],"sourcesContent":["import type {\n AppliedHashlineEdits,\n FileHeader,\n HashlineRangeInput,\n LineAnchor,\n ParsedTaggedLine,\n PreparedHashlineEdit,\n TaggedLineMarker,\n TaggedLinePrefix,\n} from '../../types/hashline';\n\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst ANCHOR_SPACE = 26n * 26n * 26n;\nconst ASCII_A = 97;\nconst FILE_TAG_PATTERN = /^[A-Za-z0-9_-]{8}$/u;\nconst FILE_HEADER_PATTERN = /^@file (.+)#([A-Za-z0-9_-]{8})$/u;\nconst TAGGED_LINE_PATTERN = /^(>> | {3})?(\\d+)#[a-z]{3}\\|(.*)$/u;\nconst ANCHOR_PATTERN = /^\\s*[>+-]*\\s*(\\d+)\\s*[:#]\\s*([a-zA-Z]{3})(?:\\s*\\|.*)?\\s*$/u;\nconst ASCII_WHITESPACE = /[ \\t\\n\\r\\v\\f]/gu;\n\n/**\n * Return Phi's three-letter whitespace-insensitive FNV-1a64 line anchor.\n * Adapted from pulseaiclub/phi writetool under the MIT license.\n */\nexport function hashLine(line: string): string {\n const bytes = new TextEncoder().encode(line.replace(ASCII_WHITESPACE, ''));\n let hash = FNV_OFFSET_BASIS;\n for (const byte of bytes) {\n hash ^= BigInt(byte);\n hash = BigInt.asUintN(64, hash * FNV_PRIME);\n }\n\n let value = hash % ANCHOR_SPACE;\n const letters = Array.from({ length: 3 }, () => '');\n for (let index = letters.length - 1; index >= 0; index -= 1) {\n letters[index] = String.fromCharCode(ASCII_A + Number(value % 26n));\n value /= 26n;\n }\n return letters.join('');\n}\n\nexport function normalizeToLf(text: string): string {\n return text.replace(/\\r\\n/gu, '\\n').replace(/\\r/gu, '\\n');\n}\n\nexport function stripBom(text: string): string {\n return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n}\n\nexport function splitLines(text: string): string[] {\n return normalizeToLf(stripBom(text)).split('\\n');\n}\n\nexport function normalizeFileTag(value: string): string {\n const trimmed = value.trim();\n const hashIndex = trimmed.lastIndexOf('#');\n const tag = hashIndex === -1 ? trimmed : trimmed.slice(hashIndex + 1);\n if (!FILE_TAG_PATTERN.test(tag)) {\n throw new Error(`Invalid file hash: ${value}. Expected the eight-character tag from @file path#HASH.`);\n }\n return tag;\n}\n\nexport function formatFileHeader(path: string, tag: string): string {\n if (path.length === 0 || path.includes('\\n') || path.includes('\\r')) {\n throw new Error('Invalid file header path. Expected one non-empty path without line breaks.');\n }\n return `@file ${path}#${normalizeFileTag(tag)}`;\n}\n\nexport function parseFileHeader(value: string): FileHeader | undefined {\n const match = FILE_HEADER_PATTERN.exec(value);\n const path = match?.[1];\n const tag = match?.[2];\n return path === undefined || tag === undefined ? undefined : { path, tag };\n}\n\nexport function formatTaggedLine(\n line: string,\n lineNumber: number,\n prefix: TaggedLinePrefix = '',\n anchorSource = line,\n): string {\n assertLineNumber(lineNumber);\n return `${prefix}${lineNumber}#${hashLine(anchorSource)}|${line}`;\n}\n\nexport function formatTaggedLines(lines: readonly string[], firstLine = 1): string {\n return lines.map((line, index) => formatTaggedLine(line, firstLine + index)).join('\\n');\n}\n\nexport function parseTaggedLine(value: string): ParsedTaggedLine | undefined {\n const match = TAGGED_LINE_PATTERN.exec(value);\n const lineText = match?.[2];\n const content = match?.[3];\n if (lineText === undefined || content === undefined) return undefined;\n\n const line = Number.parseInt(lineText, 10);\n if (!Number.isSafeInteger(line) || line < 1) return undefined;\n\n const marker = taggedLineMarker(match?.[1]);\n return marker === undefined ? { content, line } : { content, line, marker };\n}\n\nexport function parseLineAnchor(value: string): LineAnchor {\n if (value.includes('\\n') || value.includes('\\r')) {\n throw new Error(`Invalid line anchor: ${value}. Expected one anchor such as 5#abc, not a pasted multiline block.`);\n }\n\n const match = ANCHOR_PATTERN.exec(value);\n if (!match) {\n throw new Error(\n `Invalid line anchor: ${value}. Expected one anchor such as 5#abc from the latest read or grep result.`,\n );\n }\n\n const line = Number.parseInt(match[1] ?? '', 10);\n const hash = match[2]?.toLowerCase();\n if (!Number.isSafeInteger(line) || line < 1 || hash === undefined) {\n throw new Error(`Invalid line anchor: ${value}. Expected a positive line number and three letters, such as 5#abc.`);\n }\n return { line, hash };\n}\n\nexport function applyHashlineEdits(\n originalContent: string,\n requestedEdits: readonly HashlineRangeInput[],\n): AppliedHashlineEdits {\n const lines = splitLines(originalContent);\n const validated = requestedEdits.map((edit) => validateEdit(edit, lines));\n const edits = deduplicateEdits(validated).sort(compareEdits);\n validateNoOverlaps(edits);\n\n const result = [...lines];\n for (const edit of [...edits].reverse()) {\n const deleteCount = edit.to.line - edit.from.line + 1;\n const replacements = edit.content === null || edit.content === '' ? [] : splitReplacement(edit.content);\n result.splice(edit.from.line - 1, deleteCount, ...replacements);\n }\n return { content: result.join('\\n'), edits };\n}\n\nfunction taggedLineMarker(prefix: string | undefined): TaggedLineMarker | undefined {\n if (prefix === '>> ') return 'match';\n if (prefix === ' ') return 'context';\n return undefined;\n}\n\nfunction assertLineNumber(line: number): void {\n if (!Number.isSafeInteger(line) || line < 1) {\n throw new Error(`Invalid tagged line number: ${String(line)}. Expected a positive safe integer.`);\n }\n}\n\nfunction validateEdit(edit: HashlineRangeInput, lines: readonly string[]): PreparedHashlineEdit {\n const from = parseLineAnchor(edit.from);\n const to = parseLineAnchor(edit.to);\n if (from.line > to.line) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the starting line follows the ending line.`);\n }\n if (to.line > lines.length) {\n throw new Error(`Invalid edit range ${edit.from} to ${edit.to}: the file has ${lines.length} lines.`);\n }\n\n validateAnchor(from, lines);\n validateAnchor(to, lines);\n return { from, to, content: edit.content ?? null, source: edit };\n}\n\nfunction validateAnchor(anchor: LineAnchor, lines: readonly string[]): void {\n const line = lines[anchor.line - 1];\n if (line === undefined) throw new Error(`Line ${anchor.line} is outside the current file.`);\n const currentHash = hashLine(line);\n if (currentHash !== anchor.hash) {\n throw new Error(\n `Stale line anchor ${anchor.line}#${anchor.hash}. Current anchor is ${anchor.line}#${currentHash}. Re-read the file and retry.`,\n );\n }\n}\n\nfunction deduplicateEdits(edits: readonly PreparedHashlineEdit[]): PreparedHashlineEdit[] {\n const unique = new Map<string, PreparedHashlineEdit>();\n for (const edit of edits) {\n const key = JSON.stringify([edit.from.line, edit.from.hash, edit.to.line, edit.to.hash, edit.content]);\n if (!unique.has(key)) unique.set(key, edit);\n }\n return [...unique.values()];\n}\n\nfunction compareEdits(left: PreparedHashlineEdit, right: PreparedHashlineEdit): number {\n return left.from.line - right.from.line || left.to.line - right.to.line;\n}\n\nfunction validateNoOverlaps(edits: readonly PreparedHashlineEdit[]): void {\n for (let index = 1; index < edits.length; index += 1) {\n const previous = edits[index - 1];\n const current = edits[index];\n if (previous !== undefined && current !== undefined && current.from.line <= previous.to.line) {\n throw new Error(\n `Overlapping edit ranges ${previous.from.line}-${previous.to.line} and ${current.from.line}-${current.to.line}. Merge them and retry.`,\n );\n }\n }\n}\n\nfunction splitReplacement(content: string): string[] {\n const normalized = normalizeToLf(content);\n const trimmed = normalized.endsWith('\\n') ? normalized.slice(0, -1) : normalized;\n return trimmed === '' ? [] : trimmed.split('\\n');\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-hashline",
3
- "version": "0.0.1-alpha.48",
3
+ "version": "0.0.1-alpha.49",
4
4
  "description": "Shared snapshot-bound file tags and line anchors for DoomPi tools.",
5
5
  "keywords": [
6
6
  "ai",
@@ -47,11 +47,11 @@
47
47
  "access": "public"
48
48
  },
49
49
  "devDependencies": {
50
- "@types/node": "26.5.1",
51
- "@vitest/coverage-v8": "5.0.0",
50
+ "@types/node": "26.6.1",
51
+ "@vitest/coverage-v8": "5.0.1",
52
52
  "tsdown": "0.23.0",
53
53
  "typescript": "7.0.2",
54
- "vitest": "5.0.0"
54
+ "vitest": "5.0.1"
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=22.19.0"