Delete design-session citations (decision/audit/plan ordinals, stack positions), change narration, review choreography, and reviewer-addressed justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and generator templates; restate every affected fact as current-state contract prose. Fix generated docs at their sources and regenerate the catalogs and cordis-surface regions; re-paste type-equiv blocks; update every bilingual counterpart and re-record the pairs. Record the citation rule in the committed-artifact-citations Agent Note.
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/**
|
|
* Trigger detection pure core. Scans backward from
|
|
* the caret for a live trigger char under the guard tier and applies the
|
|
* word-boundary rules. Zero React / DOM / cordis.
|
|
*/
|
|
import type { TriggerChar } from '../types.ts'
|
|
import type { DetectTrigger } from './contract.ts'
|
|
|
|
const WORD_CHAR = /[\p{L}\p{N}_]/u
|
|
const WHITESPACE = /\s/u
|
|
|
|
/**
|
|
* Word-boundary rule: a trigger char opens only at start-of-draft, after
|
|
* whitespace (newlines included), or after punctuation. Two URL carve-outs
|
|
* keep '/' dead inside URLs (both pinned by tests): '/' after a ':' that
|
|
* itself follows a non-whitespace char (scheme separator, `https:/…`), and
|
|
* '/' directly after another '/' (second slash of `//`).
|
|
*/
|
|
function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
|
|
if (index === 0) return true
|
|
const prev = draft.charAt(index - 1)
|
|
if (WHITESPACE.test(prev)) return true
|
|
if (WORD_CHAR.test(prev)) return false
|
|
if (char === '/') {
|
|
if (prev === '/') return false
|
|
if (prev === ':' && index >= 2 && !WHITESPACE.test(draft.charAt(index - 2))) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Detect a trigger token at the caret. Scans left from the caret and stops
|
|
* at the first whitespace (the token under edit never spans whitespace);
|
|
* trigger chars failing the guard tier or the word boundary are treated as
|
|
* ordinary token chars and the scan continues (`user@host`, URL slashes).
|
|
* Guard tiers: plain = both chars live; claimed = '/' fully suppressed,
|
|
* '@' live; frozen = none.
|
|
*
|
|
* @param draft - Full draft text.
|
|
* @param caret - Caret offset into `draft`.
|
|
* @param guard - Availability tier derived from the input phase.
|
|
* @returns The hit with `query` = trigger-to-caret slice and `span` =
|
|
* `{start: triggerIndex, end: caret}`; `span.draftRev` is a placeholder `0`
|
|
* — the calling shell stamps the real revision. Null when no trigger is
|
|
* live at the caret.
|
|
*/
|
|
export const detectTrigger: DetectTrigger = (draft, caret, guard) => {
|
|
if (guard.tier === 'frozen') return null
|
|
for (let i = caret - 1; i >= 0; i--) {
|
|
const ch = draft.charAt(i)
|
|
if (WHITESPACE.test(ch)) return null
|
|
if (ch !== '/' && ch !== '@') continue
|
|
if (guard.tier === 'claimed' && ch === '/') continue
|
|
if (!boundaryOk(draft, i, ch)) continue
|
|
return {
|
|
trigger: ch,
|
|
query: draft.slice(i + 1, caret),
|
|
position: draft.search(/\S/) === i ? 'leading' : 'inline',
|
|
span: { start: i, end: caret, draftRev: 0 },
|
|
}
|
|
}
|
|
return null
|
|
}
|