runlist 0.0.0 → 0.94.1

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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1114 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +292 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/query.mjs +977 -0
  136. package/src/record-action.mjs +11 -0
  137. package/src/record-create.mjs +82 -0
  138. package/src/reference-planner.mjs +388 -0
  139. package/src/reference-values.mjs +11 -0
  140. package/src/rename.mjs +107 -0
  141. package/src/render.mjs +678 -0
  142. package/src/roadmap.mjs +184 -0
  143. package/src/runlist.mjs +944 -0
  144. package/src/section.mjs +225 -0
  145. package/src/semantic-search-client.mjs +58 -0
  146. package/src/ship.mjs +166 -0
  147. package/src/show.mjs +101 -0
  148. package/src/skill-drift.mjs +98 -0
  149. package/src/source-editor.mjs +491 -0
  150. package/src/stages.mjs +73 -0
  151. package/src/state-migration.mjs +123 -0
  152. package/src/stats.mjs +200 -0
  153. package/src/status-metadata.mjs +87 -0
  154. package/src/statuses.mjs +721 -0
  155. package/src/summary.mjs +74 -0
  156. package/src/surfaces.mjs +28 -0
  157. package/src/sync-status.mjs +316 -0
  158. package/src/template-store.mjs +62 -0
  159. package/src/update.mjs +338 -0
  160. package/src/use.mjs +58 -0
  161. package/src/util.mjs +337 -0
  162. package/src/validate.mjs +794 -0
  163. package/src/watch.mjs +56 -0
  164. package/src/xref.mjs +406 -0
  165. package/src/yardstick-command.mjs +178 -0
  166. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,80 @@
1
+ import { libraryKind, matchesLibraryStage, sortLibraryRows } from './app-library.mjs';
2
+ import { readPlanStage, readShipsFrontmatter } from './stages.mjs';
3
+ import {open} from 'node:fs/promises';
4
+ import {constants} from 'node:fs';
5
+ import {StringDecoder} from 'node:string_decoder';
6
+ import path from 'node:path';
7
+ import {extractFrontmatter,parseSimpleFrontmatter} from './frontmatter.mjs';
8
+ import {authorizeManagedSource} from './managed-path.mjs';
9
+
10
+ const detached=value=>JSON.parse(JSON.stringify(value));
11
+
12
+ // On-demand streaming search. No document bodies or content index are retained.
13
+ export async function searchCheckout(config,library,params) {
14
+ const words=(params.get('q')??'').slice(0,500).toLowerCase().trim().split(/\s+/).filter(Boolean);
15
+ const limit=Math.max(1,Math.min(100,Math.floor(Number(params.get('limit'))||50)));
16
+ const requestedOffset=Math.max(0,Math.floor(Number(params.get('offset'))||0));
17
+ const all=await library.all(),rows=all.filter(r=>(params.get('archived')==='1'||!r.archived)
18
+ &&(!params.get('kind')||params.get('kind')==='all'||params.get('kind')==='plans'&&r.kind==='plan'||params.get('kind')==='hubs'&&r.kind==='hub'||params.get('kind')==='documents'&&r.kind==='document')
19
+ &&matchesLibraryStage(r,params.get('stage'))
20
+ &&(!params.get('status')||r.status===params.get('status'))&&(!params.get('type')||r.type===params.get('type'))
21
+ &&(!params.get('folder')||r.folder===params.get('folder')||r.folder.startsWith(params.get('folder')+'/')));
22
+ sortLibraryRows(rows,params,config);
23
+ const documents=[],sections=[];let scanned=0,unavailable=0,bytesRead=0,total=0,partial=false,index=0;
24
+ if(!words.length)return {documents:rows.slice(requestedOffset,requestedOffset+limit),sections:[],total:rows.length,offset:requestedOffset,limit,hasMore:requestedOffset+limit<rows.length,scanned:0,inventoryTotal:rows.length,partial:false,unavailable:0};
25
+ const budget=128*1024*1024;
26
+ const worker=async()=>{
27
+ while(index<rows.length) {
28
+ if(bytesRead>=budget){partial=true;return;}
29
+ const row=rows[index++];let fd;
30
+ try {
31
+ const file=authorizeManagedSource(path.resolve(config.repoRoot,row.path),config).path;
32
+ fd=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW??0));const stamp=await fd.stat();
33
+ if(!stamp.isFile()||stamp.size>8*1024*1024){unavailable++;continue;}
34
+ const buffer=Buffer.alloc(64*1024),decoder=new StringDecoder('utf8');let offset=0,carry='',lineCarry='',bodyLine=0,fence=null,comment=false,frontmatter=false,first=true,excerpt='';
35
+ const found=new Set(),headings=[];let rejected=false;
36
+ const metadata=`${row.title} ${row.path} ${row.status} ${row.type} ${row.modules.join(' ')}`.toLowerCase();
37
+ words.forEach(w=>{if(metadata.includes(w))found.add(w);});
38
+ function line(raw){
39
+ if(bodyLine===0&&raw==='---'&&!frontmatter){frontmatter=true;return;}
40
+ if(frontmatter){if(raw==='---')frontmatter=false;return;}
41
+ bodyLine++;
42
+ if(raw.includes('<!--'))comment=true;
43
+ const marker=raw.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
44
+ if(marker&&!comment){if(!fence)fence=marker[1];else if(marker[1][0]===fence[0]&&marker[1].length>=fence.length&&!marker[2].trim())fence=null;}
45
+ const match=!fence&&!comment&&raw.match(/^ {0,3}#{1,6}\s+(.*?)(?:\s+#+\s*)?$/);
46
+ if(match&&words.every(w=>match[1].toLowerCase().includes(w))&&headings.length<8)headings.push({path:row.path,title:detached(match[1].slice(0,200)),line:bodyLine});
47
+ if(raw.includes('-->'))comment=false;
48
+ }
49
+ while(offset<stamp.size) {
50
+ if(bytesRead>=budget){partial=true;break;}
51
+ const {bytesRead:count}=await fd.read(buffer,0,buffer.length,offset);if(!count)break;offset+=count;bytesRead+=count;
52
+ const chunk=decoder.write(buffer.subarray(0,count));
53
+ if(first){
54
+ first=false;const parts=extractFrontmatter(chunk),header=parseSimpleFrontmatter(parts.frontmatter);
55
+ // Re-check current metadata, even when a cached library row predates a type change.
56
+ if(header.type==='prompt'||/^---\r?\n/.test(chunk)&&!parts.bodyLineOffset){unavailable++;rejected=true;break;}
57
+ row.kind=libraryKind(header,row.path);
58
+ if(row.kind==='plan'){const stage=readPlanStage(readShipsFrontmatter(parts.frontmatter));row.stage=stage.word;row.stageInvalid=stage.invalid;}else{delete row.stage;delete row.stageInvalid;}
59
+ if(!matchesLibraryStage(row,params.get('stage'))||params.get('kind')==='plans'&&row.kind!=='plan'){rejected=true;break;}
60
+ }
61
+ const haystack=(carry+chunk).toLowerCase();
62
+ for(const word of words){const at=haystack.indexOf(word);if(at>=0){found.add(word);if(!excerpt)excerpt=(carry+chunk).slice(Math.max(0,at-65),at+165).replace(/\s+/g,' ').slice(0,240);}}
63
+ carry=chunk.slice(-500);
64
+ const lines=(lineCarry+chunk).split('\n');lineCarry=lines.pop();for(const raw of lines)line(raw.replace(/\r$/,''));
65
+ // A pathological single-line file must not accumulate the entire body.
66
+ if(lineCarry.length>4096)lineCarry=lineCarry.slice(-4096);
67
+ }
68
+ if(rejected||offset<stamp.size)continue;
69
+ line(lineCarry+decoder.end());scanned++;
70
+ if(found.size===words.length){total++;documents.push({...row,excerpt:detached(excerpt)});}
71
+ for(const heading of headings)if(sections.length<limit)sections.push(heading);
72
+ }catch{unavailable++;}finally{await fd?.close();}
73
+ }
74
+ };
75
+ await Promise.all(Array.from({length:4},worker));
76
+ sortLibraryRows(documents,params,config);
77
+ sections.sort((a,b)=>a.path.localeCompare(b.path)||a.line-b.line);
78
+ const offset=Math.min(requestedOffset,Math.max(0,Math.floor((documents.length-1)/limit)*limit));
79
+ return {documents:documents.slice(offset,offset+limit),sections,total,offset,limit,hasMore:offset+limit<total,scanned,inventoryTotal:rows.length,partial,unavailable,bytesRead};
80
+ }
@@ -0,0 +1,105 @@
1
+ import {createHash,randomUUID} from 'node:crypto';
2
+ import {existsSync,readFileSync,realpathSync,lstatSync,statSync,readdirSync,mkdirSync,writeFileSync,renameSync,unlinkSync,constants} from 'node:fs';
3
+ import {open} from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import {authorizeManagedSource} from './managed-path.mjs';
7
+ import {extractFrontmatter,parseSimpleFrontmatter,normalizeEol} from './frontmatter.mjs';
8
+ import {extractFirstHeading} from './extractors.mjs';
9
+ import {sourceRevision,SourceEditError} from './source-editor.mjs';
10
+ import {readPlanStage,readShipsFrontmatter} from './stages.mjs';
11
+ import {libraryKind,matchesLibraryStage} from './app-library.mjs';
12
+ import {withSemanticClient} from './semantic-search-client.mjs';
13
+
14
+ const inside=(root,file)=>{const rel=path.relative(root,file);return rel!== '..'&&!rel.startsWith(`..${path.sep}`)&&!path.isAbsolute(rel);};
15
+ const messages={tool_missing:'gmax is optional and is not installed. Exact search is available.',tool_unavailable:'gmax could not be opened. Check its paths in Settings or update it for existing-index-only search.',unsupported_tool:'This gmax installation does not support existing-index-only document search. Update gmax externally, then retry.',unsupported_daemon:'The running gmax daemon needs the existing-index-only contract. Update it externally, then retry.',no_index:'This checkout has no matching gmax index. Add it through gmax externally, then retry.',index_unavailable:'This gmax index is not ready. Exact search is available.',daemon_unavailable:'Start gmax externally, then retry. Runlist does not start its services.',store_unavailable:'This index store is unavailable to the existing-index-only integration.',embedding_unavailable:'gmax has no available warm embedding worker. Finish its startup or current work externally, then retry.',embedding_mismatch:'The index and embedding runtime do not match. Repair through gmax externally.',host_pressure:'gmax declined the request because host resources could not be admitted. Exact search is available.',busy:'gmax is busy. Retry when its current work finishes.',cancelled:'Semantic search cancelled.',timeout:'Semantic search timed out. Narrow the query or retry.',no_coverage:'No indexed Markdown was found in the checked document scope. Check gmax indexing rules externally.',invalid_response:'gmax returned an unsupported response. Exact search is available.',response_too_large:'gmax returned too much data. Narrow the document scope.',tool_changed:'The saved gmax installation changed. Select its paths again in Settings.',search_unavailable:'Semantic search is unavailable. Exact search is available.'};
16
+ const failure=state=>{state=Object.hasOwn(messages,state)?state:'search_unavailable';return {state,message:messages[state],documents:[],sections:[]};};
17
+ function validateInstallation(value){
18
+ if(!value||typeof value.nodePath!=='string'||typeof value.entryPath!=='string'||!path.isAbsolute(value.nodePath)||!path.isAbsolute(value.entryPath)||value.nodePath.length>4096||value.entryPath.length>4096)throw new SourceEditError('invalid-request','Use absolute paths for the external Node executable and gmax entry file.');
19
+ const nodePath=realpathSync(value.nodePath),entryPath=realpathSync(value.entryPath),node=statSync(nodePath),entry=statSync(entryPath);
20
+ if(!node.isFile()||!entry.isFile()||entry.size>1024*1024||!['.js','.mjs','.cjs'].includes(path.extname(entryPath)))throw new Error('invalid installation');
21
+ const packagePath=path.resolve(path.dirname(entryPath),'../package.json');
22
+ if(statSync(packagePath).size>65536||!['node','node.exe'].includes(path.basename(nodePath)))throw new Error('invalid runtime');
23
+ const packageBytes=readFileSync(packagePath),pkg=JSON.parse(packageBytes.toString('utf8'));
24
+ if(pkg.name!=='grepmax')throw new Error('not gmax');
25
+ return {nodePath,entryPath,identity:`${node.dev}:${node.ino}:${node.size}:${node.mtimeMs}:${createHash('sha256').update(readFileSync(entryPath)).update(packageBytes).digest('hex')}`,version:pkg.version};
26
+ }
27
+ function discover(){
28
+ const candidates=[];
29
+ for(const directory of (process.env.PATH??'').split(path.delimiter).filter(p=>path.isAbsolute(p)))candidates.push({nodePath:path.join(directory,process.platform==='win32'?'node.exe':'node'),entryPath:path.join(directory,'gmax')});
30
+ const nvm=path.join(os.homedir(),'.nvm/versions/node');try{for(const version of readdirSync(nvm).filter(v=>/^v\d+\.\d+\.\d+$/.test(v)).sort((a,b)=>b.localeCompare(a,undefined,{numeric:true})).slice(0,12))candidates.push({nodePath:path.join(nvm,version,'bin/node'),entryPath:path.join(nvm,version,'lib/node_modules/grepmax/dist/bin.js')});}catch{}
31
+ if(process.platform==='win32')for(const prefix of [process.env.APPDATA&&path.join(process.env.APPDATA,'npm')].filter(Boolean))for(const candidate of candidates.slice(0,32))candidates.push({nodePath:candidate.nodePath,entryPath:path.join(prefix,'node_modules/grepmax/dist/bin.js')});
32
+ for(const candidate of candidates.slice(0,80))try{return validateInstallation(candidate);}catch{}
33
+ return null;
34
+ }
35
+ const defaultSettings=()=>path.join(process.env.XDG_CONFIG_HOME??(process.platform==='win32'?process.env.APPDATA??os.homedir():path.join(os.homedir(),'.config')),'runlist/semantic-search.json');
36
+ export function createAppSemanticSearch({config,library,settingsFile=defaultSettings(),connect=withSemanticClient,timeoutMs=10000}){
37
+ const jobs=new Map();let active=null;
38
+ function installation(){try{if(existsSync(settingsFile)){if(lstatSync(settingsFile).isSymbolicLink()||statSync(settingsFile).size>16384)return null;const value=JSON.parse(readFileSync(settingsFile,'utf8'));if(value.nodePath&&value.entryPath){const current=validateInstallation(value);return current.identity===value.identity?current:null;}}}catch{return null;}return discover();}
39
+ function settings(){const selected=installation();return {available:!!selected,nodePath:selected?.nodePath??'',entryPath:selected?.entryPath??'',version:selected?.version??null,experimental:true,readiness:'not_checked',message:selected?'External gmax was found. Index and embedding readiness are checked only when you search. Semantic search is experimental; text search remains available.':messages.tool_missing};}
40
+ function saveSettings(body){if(Object.keys(body).some(k=>!['nodePath','entryPath'].includes(k)))throw new SourceEditError('invalid-request','Choose only the external tool paths.');
41
+ const dir=path.dirname(settingsFile);mkdirSync(dir,{recursive:true,mode:0o700});if(lstatSync(dir).isSymbolicLink()||existsSync(settingsFile)&&lstatSync(settingsFile).isSymbolicLink())throw new SourceEditError('unsafe-local-state','Tool settings must be ordinary local files.');
42
+ if(body.nodePath===''&&body.entryPath===''){if(existsSync(settingsFile))unlinkSync(settingsFile);return settings();}
43
+ let value;try{value=validateInstallation(body);}catch{throw new SourceEditError('invalid-request','Choose a valid external Node executable and grepmax dist/bin.js entry file.');}
44
+ active?.controller.abort();const temp=settingsFile+'.'+randomUUID();writeFileSync(temp,JSON.stringify(value)+'\n',{mode:0o600,flag:'wx'});renameSync(temp,settingsFile);return settings();
45
+ }
46
+ function clean(){for(const [id,job] of jobs)if(job.finished&&Date.now()-job.at>60000)jobs.delete(id);while(jobs.size>=16){const first=[...jobs].find(([,job])=>job.finished);if(!first)break;jobs.delete(first[0]);}}
47
+ const inspect=id=>{const job=jobs.get(id);if(!job)return failure('cancelled');return job.state==='running'?{id,state:'running'}:{id,...job.result};};
48
+ function cancel(id){const job=jobs.get(id);if(job?.state==='running'){job.controller.abort();job.state='complete';job.result=failure('cancelled');}return inspect(id);}
49
+ function start(body){
50
+ if(!body||Object.keys(body).some(k=>!['query','archived','kind','status','type','folder','stage'].includes(k))||typeof body.query!=='string'||body.query.length>500||!body.query.trim()||Object.values(body).some(v=>!['string','boolean'].includes(typeof v)||typeof v==='string'&&v.length>512)||body.archived!==undefined&&typeof body.archived!=='boolean'||body.stage!==undefined&&typeof body.stage!=='string')throw new SourceEditError('invalid-request','Supply a semantic query up to 500 characters and supported document filters.');
51
+ clean();if(jobs.size>=16)throw new SourceEditError('search-busy','Semantic search is finishing cancelled work. Retry shortly.');const previous=active;if(active?.state==='running')cancel(active.id);const job={id:randomUUID(),controller:new AbortController(),state:'running',at:Date.now(),result:null,finished:false};jobs.set(job.id,job);active=job;
52
+ const timer=setTimeout(()=>{job.timedOut=true;job.controller.abort();},timeoutMs);timer.unref();
53
+ job.done=(async()=>{await previous?.done;job.controller.signal.throwIfAborted();return run(body,job.controller.signal);})().then(value=>{if(job.state==='running'){job.result=value;job.state='complete';}},error=>{if(job.state==='running'){job.result=failure(job.timedOut?'timeout':job.controller.signal.aborted?'cancelled':error.code??'search_unavailable');job.state='complete';}}).finally(()=>{clearTimeout(timer);job.at=Date.now();job.finished=true;if(active===job)active=null;});
54
+ return {id:job.id,state:'running'};
55
+ }
56
+ async function run(body,signal){
57
+ const selected=installation();if(!selected)return failure(existsSync(settingsFile)?'tool_changed':'tool_missing');
58
+ const checkout=realpathSync(config.repoRoot),all=await library.all();signal.throwIfAborted();
59
+ let roots=(config.docsRoots??[config.docsRoot]).map(p=>realpathSync(p)).filter(p=>inside(checkout,p));
60
+ if(body.folder){const folder=realpathSync(path.resolve(config.repoRoot,body.folder));if(!inside(checkout,folder))return failure('no_coverage');roots=roots.flatMap(root=>inside(root,folder)?[folder]:inside(folder,root)?[root]:[]);}
61
+ if(!roots.length||roots.length>32)return failure('no_coverage');
62
+ const allowed=new Map(all.flatMap(row=>{try{return [[authorizeManagedSource(path.resolve(config.repoRoot,row.path),config).canonicalPath,row]];}catch{return [];}}));
63
+ const scopePaths=[...allowed.keys()].filter(p=>roots.some(root=>inside(root,p))),paths=[];let pathBytes=0;for(const p of scopePaths){pathBytes+=Buffer.byteLength(p)+8;if(paths.length>=2000||pathBytes>512*1024)break;paths.push(p);}
64
+ return connect(selected,checkout,signal,async client=>{
65
+ const status=await client.tool('document_search_status',{paths});
66
+ if(status.state!=='ready')return failure(status.state);
67
+ if(!Number.isSafeInteger(status.generation)||status.generation<1)return failure('embedding_mismatch');
68
+ if(!status.project||!path.isAbsolute(status.project.root)||!inside(status.project.root,checkout)||!Array.isArray(status.covered)||status.covered.length>paths.length)throw new Error('invalid status');
69
+ const sample=new Set(paths);if(status.covered.some(entry=>typeof entry.path!=='string'||!sample.has(entry.path)))throw new Error('invalid coverage');
70
+ if(!status.covered.length&&paths.length===scopePaths.length)return failure('no_coverage');
71
+ const response=await client.tool('semantic_search',{query:body.query.trim(),prefixes:roots});
72
+ if(response.state!=='ready')return failure(response.state);
73
+ if(response.generation!==status.generation)return failure('embedding_mismatch');
74
+ if(response.root!==status.project.root||response.store!==status.project.store||!Array.isArray(response.matches)||response.matches.length>50)throw new Error('invalid results');
75
+ const documents=[],sections=[],seen=new Set();let bytes=0,stale=0;
76
+ for(const match of response.matches){signal.throwIfAborted();let fd;
77
+ try{
78
+ if(typeof match.path!=='string'||!path.isAbsolute(match.path)||!Number.isInteger(match.startLine)||!Number.isInteger(match.endLine)||match.startLine<1||match.endLine<match.startLine)continue;
79
+ const pointer=authorizeManagedSource(match.path,config);if(!inside(checkout,pointer.canonicalPath))continue;
80
+ const row=allowed.get(pointer.canonicalPath);if(!row||seen.has(row.path)||documents.length>=20)continue;
81
+ const file=authorizeManagedSource(match.path,config).path;
82
+ if(path.relative(config.repoRoot,file).split(path.sep).some(p=>p==='prompts'||p==='.git'||config.excludeDirs?.has(p))||file===config.indexPath)continue;
83
+ fd=await open(file,constants.O_RDONLY|(constants.O_NOFOLLOW??0));const before=await fd.stat();if(!before.isFile()||before.size>8*1024*1024||bytes+before.size>128*1024*1024)continue;
84
+ bytes+=before.size;const buffer=Buffer.alloc(before.size);let offset=0;while(offset<buffer.length){signal.throwIfAborted();const read=await fd.read(buffer,offset,buffer.length-offset,offset);if(!read.bytesRead)break;offset+=read.bytesRead;}if(offset!==buffer.length)continue;
85
+ const after=await fd.stat();authorizeManagedSource(file,config);const current=statSync(file);if(before.dev!==current.dev||before.ino!==current.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||current.size!==after.size||current.mtimeMs!==after.mtimeMs)continue;
86
+ const source=buffer.toString('utf8'),parts=extractFrontmatter(source),fm=parseSimpleFrontmatter(parts.frontmatter);
87
+ if(fm.type!==undefined&&typeof fm.type!=='string'||fm.type==='prompt'||/^---\r?\n/.test(source)&&!parts.bodyLineOffset)continue;
88
+ const doc={...row,title:String(fm.title||extractFirstHeading(parts.body)||path.basename(file,'.md')).slice(0,300),type:fm.type??'untyped',status:fm.status??'untyped',kind:libraryKind(fm,row.path)};
89
+ if(doc.kind==='plan'){const stage=readPlanStage(readShipsFrontmatter(parts.frontmatter));doc.stage=stage.word;doc.stageInvalid=stage.invalid;}else{delete doc.stage;delete doc.stageInvalid;}
90
+ const archived=row.path.split('/').includes(path.basename(config.archiveDir??'archived'))||doc.status==='archived';
91
+ if(!matchesLibraryStage(doc,body.stage)||!body.archived&&archived||body.kind&&body.kind!=='all'&&({plans:'plan',hubs:'hub',documents:'document'}[body.kind]??body.kind)!==doc.kind||body.status&&body.status!==doc.status||body.type&&body.type!==doc.type||body.folder&&row.folder!==body.folder&&!row.folder.startsWith(body.folder+'/'))continue;
92
+ const verified=match.hashAlgorithm==='sha256-bytes'&&typeof match.hash==='string'&&/^[a-f0-9]{64}$/.test(match.hash)&&createHash('sha256').update(buffer).digest('hex')===match.hash;
93
+ const lines=normalizeEol(parts.body).split('\n'),at=verified?match.startLine-parts.bodyLineOffset-1:0;
94
+ const excerpt=lines.slice(Math.max(0,at),Math.max(0,at)+4).join(' ').replace(/\s+/g,' ').slice(0,240);
95
+ const revision=sourceRevision(source);documents.push({...doc,excerpt,revision,location:verified?'verified':'changed'});seen.add(row.path);if(!verified){stale++;continue;}
96
+ // Bound the section lookup to ordinary ATX headings; fences/comments never become jump targets.
97
+ let fence=null,comment=false,heading=null;for(let i=0;i<=Math.min(at,lines.length-1);i++){const raw=lines[i];if(raw.includes('<!--'))comment=true;const marker=raw.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);if(marker&&!comment){if(!fence)fence=marker[1];else if(marker[1][0]===fence[0]&&marker[1].length>=fence.length&&!marker[2].trim())fence=null;}const h=!fence&&!comment&&raw.match(/^ {0,3}#{1,6}\s+(.*?)(?:\s+#+\s*)?$/);if(h)heading={path:row.path,title:h[1].slice(0,200),line:i+1,revision};if(raw.includes('-->'))comment=false;}
98
+ if(heading)sections.push(heading);
99
+ }catch{if(signal.aborted)signal.throwIfAborted();}finally{await fd?.close();}
100
+ }
101
+ return {state:'ready',documents,sections,total:documents.length,limited:true,stale,coverage:{partial:paths.length<scopePaths.length||status.covered.length<paths.length||status.coverage?.partial===true||response.indexState?.degraded===true||Number(response.indexState?.failedFiles)>0||response.indexState?.verifying===true},lastIndexed:status.project.lastIndexed??null,indexUpdating:response.indexState?.indexing===true,message:'Top semantic matches from the existing local index.'};
102
+ });
103
+ }
104
+ return {settings,saveSettings,start,inspect,cancel,close(){for(const job of jobs.values())job.controller.abort();return Promise.allSettled([...jobs.values()].map(job=>job.done));}};
105
+ }
@@ -0,0 +1,208 @@
1
+ import { getStageDefinitions } from './stages.mjs';
2
+ import {createAppLifecycle} from './app-lifecycle.mjs';
3
+ import {createAppYardstick} from './app-yardstick.mjs';
4
+ import {createAppFiling} from './app-filing.mjs';
5
+ import {createAppDocuments} from './app-create.mjs';
6
+ import {searchCheckout} from './app-search.mjs';
7
+ import {createAppSemanticSearch} from './app-semantic-search.mjs';
8
+ import {resolveStatusMetadata} from './status-metadata.mjs';
9
+ import {prepareNativeAction} from './native-action.mjs';
10
+ import { createRecordLibrary } from './app-records.mjs';
11
+ import { prepareDecisionAction } from './decision-action.mjs';
12
+ import { createDocumentLibrary, libraryKind } from './app-library.mjs';
13
+ import { createAppGit } from './app-git.mjs';
14
+ import { createAppGitCommits } from './app-git-commits.mjs';
15
+ import { appTemplates } from './new.mjs';
16
+ import { saveTemplate } from './template-store.mjs';
17
+ import { createHash } from 'node:crypto';
18
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { createSourceEditor, SourceEditError, sourceRevision } from './source-editor.mjs';
22
+ import { authorizeManagedSource } from './managed-path.mjs';
23
+ import { extractFrontmatter, parseSimpleFrontmatter } from './frontmatter.mjs';
24
+ import { deriveFlags, flagsFile, readFlagEvents, triageFlag } from './flags.mjs';
25
+ import { readPlanOwnership, ownershipLiveness } from './pickup.mjs';
26
+ import { normalizeEol } from './frontmatter.mjs';
27
+ import { extractFirstHeading } from './extractors.mjs';
28
+
29
+ export const localActor = () => ({ kind: 'human', id: `human:local:${createHash('sha256').update(`${os.userInfo().username}:${os.userInfo().uid}`).digest('hex').slice(0, 24)}`, label: os.userInfo().username });
30
+ function problem(code, message) { throw new SourceEditError(code, message); }
31
+ function relative(filePath, config) { return path.relative(config.repoRoot, filePath).split(path.sep).join('/'); }
32
+ function contained(root, filePath) { const rel = path.relative(root, filePath); return rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel); }
33
+
34
+ function evidenceFor(source, documentPath, config) {
35
+ const flags = deriveFlags(readFlagEvents(flagsFile(config))).filter(flag => {
36
+ if (typeof flag.file !== 'string') return false;
37
+ const id = typeof flag.id === 'string' ? flag.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
38
+ return flag.file === documentPath || (flag.file && source.includes(flag.file)) || (id && new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`).test(source));
39
+ }).slice(-6).reverse();
40
+ return flags.map(flag=>flagEvidence(flag,config));
41
+ }
42
+ function flagEvidence(flag,config) {
43
+ let current = { status: 'unavailable', revision: null, line: null, quote: null };
44
+ const candidate = path.resolve(config.repoRoot, flag.file), ext = path.extname(candidate).toLowerCase();
45
+ // Evidence reads are bounded to this checkout and known document/code
46
+ // types. No arbitrary-file endpoint, prompts, env files, or Git internals.
47
+ if (contained(config.repoRoot, candidate) && !flag.file.split(/[\\/]/).some(p => ['prompts', '.git', 'node_modules'].includes(p)||config.excludeDirs?.has(p))
48
+ && /^(?:\.md|\.mjs|\.cjs|\.js|\.ts|\.tsx|\.jsx|\.py|\.swift|\.rs|\.go|\.sql|\.sh|\.css)$/.test(ext)) {
49
+ try {
50
+ if (lstatSync(candidate).isSymbolicLink() || !contained(realpathSync(config.repoRoot), realpathSync(candidate)) || statSync(candidate).size > 1024 * 1024) throw new Error('unsafe');
51
+ const raw = readFileSync(candidate, 'utf8'), lines = normalizeEol(raw).split('\n');
52
+ const at = Number.isInteger(flag.line) ? flag.line - 1 : null;
53
+ const quote = at !== null ? lines[at]?.trim().slice(0, 200) ?? null : null;
54
+ const matches = flag.quote ? lines.flatMap((line, i) => line.trim().slice(0, 200) === flag.quote ? [i + 1] : []) : [];
55
+ const status = at === null ? 'file' : quote === flag.quote ? 'unchanged' : matches.length === 1 ? 'moved' : matches.length > 1 ? 'ambiguous' : 'changed';
56
+ const line = status === 'moved' ? matches[0] : flag.line ?? null;
57
+ current = { status, revision: sourceRevision(raw), line, quote: line ? lines[line - 1]?.trim().slice(0, 200) ?? null : null };
58
+ } catch { /* Original evidence survives missing or unsafe current files. */ }
59
+ }
60
+ return { id: flag.id, text: flag.text, severity: flag.severity, state: flag.state, triage: flag.triage, original: { file: flag.file, line: flag.line, quote: flag.quote, at: flag.at, by: flag.by, revision: null, revisionReason: 'The legacy flag did not record an observed revision.' }, current };
61
+ }
62
+
63
+ /** Shared checkout operations. Transport adapters supply their own trusted actor. */
64
+ export function createCheckoutService({config,initialPath=null,actor=localActor(),authenticate=()=>actor,semanticOptions={}}) {
65
+ if (actor.kind !== 'human' || !actor.id) problem('unauthenticated', 'A locally identified human is required.');
66
+ const library = createDocumentLibrary(config);
67
+ const semantic=createAppSemanticSearch({config,library,...semanticOptions});
68
+ const git = createAppGit({config,actor,authenticate,library});
69
+ const gitCommits = createAppGitCommits({config,actor,authenticate,git});
70
+ const editor = createSourceEditor({ config, allowStageEdits:true, domainPrepare:prepareNativeAction, legacyTypes:[...(config.validTypes ?? ['plan','doc'])].filter(t=>t!=='prompt'),allowUnconfiguredRead:true, authenticate: req => authenticate(req), authorize: ({ actor: candidate, path: filePath }) => ({ allowed: candidate.kind === 'human' && candidate.id === actor.id && !path.relative(config.repoRoot,filePath).split(path.sep).some(p=>p==='prompts'||p==='.git'||config.excludeDirs?.has(p)) && filePath!==config.indexPath && (config.docsRoots ?? [config.docsRoot]).some(root => contained(path.resolve(root), filePath)) }) });
71
+ function openDocument(req, documentPath) {
72
+ const opened = editor.read(req, { path: documentPath });
73
+ const fm = parseSimpleFrontmatter(extractFrontmatter(opened.source).frontmatter);
74
+ const repoPath = relative(opened.path, config), ownership = readPlanOwnership(repoPath, config);
75
+ return { ...opened, path: repoPath, title: (fm.record_schema==='runlist.record/v1'&&(fm.finding||fm.question)) || fm.title || extractFirstHeading(extractFrontmatter(opened.source).body) || path.basename(opened.path, '.md'), status: fm.status, type:fm.type??'untyped',kind:libraryKind(fm,repoPath),stageDefinitions:getStageDefinitions(config),relationships:library.relations(repoPath,fm,extractFrontmatter(opened.source).body),metadata: fm, claim: ownership ? { corrupt: ownership.corrupt, state: ownership.state, sessionId: ownership.sessionId, liveness: ownershipLiveness(ownership) } : null, flags: evidenceFor(opened.source, repoPath, config) };
76
+ }
77
+ const records=createRecordLibrary({config,library,readSource:(req,path)=>editor.read(req,{path}),evidence:flag=>flagEvidence(flag,config)});
78
+ const lifecycle=createAppLifecycle({config,read:openDocument,actor:req=>authenticate(req)});
79
+ const yardstick=createAppYardstick({config,read:openDocument,actor:req=>authenticate(req),pending:(req,input)=>{
80
+ const retained=editor.listRecovery(req,{path:input});
81
+ if(retained.unavailable)problem('recovery-unavailable','Inspect unavailable editor recovery before recording an assessment.');
82
+ if(retained.items.length)problem('draft-pending','Save or discard this plan’s retained draft, and inspect pending saves, before recording an assessment.');
83
+ const status=lifecycle.information(req,input);
84
+ if(!status.enabled)problem('lifecycle-unavailable',status.reason);
85
+ }});
86
+ const filing=createAppFiling({config,library,readSource:(input,req)=>{
87
+ const file=authorizeManagedSource(path.resolve(config.repoRoot,input),config).path,before=lstatSync(file);
88
+ const opened=editor.read(req,{path:input}),after=lstatSync(file);
89
+ if(['dev','ino','size','mtimeMs','ctimeMs','mode'].some(key=>before[key]!==after[key]))problem('revision-conflict','This source changed while filing evidence was read. Refresh the report.');
90
+ return {...opened,stamp:`${after.dev}:${after.ino}:${after.size}:${after.mtimeMs}:${after.ctimeMs}`};
91
+ }});
92
+ const assertYardstickIdle=(req,input)=>yardstick.assertIdle(req,relative(path.resolve(config.repoRoot,input),config));
93
+ const invalidateReports=input=>{library.invalidate(input);records.invalidate();filing.invalidate();};
94
+ const creation=createAppDocuments({config,actor:req=>authenticate(req),library});
95
+ async function request(req, route, body) {
96
+ if (typeof route !== 'string' || !route.startsWith('/api/')) problem('invalid-request','Choose a supported checkout operation.');
97
+ const url = new URL(route, 'http://checkout.invalid');
98
+ const authenticated = {actor:authenticate(req)};
99
+ if(url.pathname==='/api/filing'||url.pathname.startsWith('/api/yardstick')){if(authenticated.actor?.kind!=='human'||authenticated.actor.id!==actor.id)problem('forbidden','This local checkout authority is required.');}
100
+ if (req.method === 'GET' && url.pathname === '/api/git/status') return git.status(req,url.searchParams);
101
+ if (req.method === 'GET' && url.pathname === '/api/git/diff') return git.diff(req,url.searchParams);
102
+ if (req.method === 'GET' && url.pathname === '/api/git/operation') return gitCommits.inspect(req,url.searchParams.get('id'));
103
+ const send = (_status, value) => { if (_status === 404) problem('not-found',value.message); return value; };
104
+ if (req.method === 'GET' && url.pathname === '/api/templates') return send(200, appTemplates(config));
105
+ if(req.method==='GET'&&url.pathname==='/api/create')return creation.options();
106
+ if(url.pathname.startsWith('/api/semantic/')){
107
+ if(authenticated.actor?.kind!=='human'||authenticated.actor.id!==actor.id)problem('forbidden','This local checkout authority is required.');
108
+ if(req.method==='GET'&&url.pathname==='/api/semantic/settings')return semantic.settings();
109
+ if(req.method==='GET'&&url.pathname==='/api/semantic/result')return semantic.inspect(url.searchParams.get('id'));
110
+ if(req.method==='POST'){if(!body||typeof body!=='object'||Array.isArray(body))problem('invalid-request','Use a JSON object.');
111
+ if(url.pathname==='/api/semantic/settings')return semantic.saveSettings(body);
112
+ if(url.pathname==='/api/semantic/start')return semantic.start(body);
113
+ if(url.pathname==='/api/semantic/cancel'){if(Object.keys(body).some(k=>k!=='id')||typeof body.id!=='string')problem('invalid-request','Choose a semantic request.');return semantic.cancel(body.id);}
114
+ }
115
+ }
116
+ if(req.method==='GET'&&url.pathname==='/api/search')return searchCheckout(config,library,url.searchParams);
117
+ if(req.method==='GET'&&url.pathname==='/api/recovery'){
118
+ const target=url.searchParams.get('path'),groups=[editor.listRecovery(req,{path:target}),...(target?[]:[creation.list(req),lifecycle.list(req),yardstick.list(req),gitCommits.list(req)])];
119
+ const items=groups.flatMap(group=>group.items).map(item=>({...item,path:path.isAbsolute(item.path)?relative(item.path,config):item.path})).sort((a,b)=>String(b.at).localeCompare(String(a.at)));
120
+ const offset=Math.max(0,Math.floor(Number(url.searchParams.get('offset'))||0)),limit=Math.max(1,Math.min(200,Math.floor(Number(url.searchParams.get('limit'))||50)));
121
+ return {items:items.slice(offset,offset+limit),total:items.length,offset,limit,hasMore:offset+limit<items.length,unavailable:groups.reduce((n,g)=>n+g.unavailable,0)};
122
+ }
123
+ if (req.method === 'GET' && ['/api/library','/api/plans'].includes(url.pathname)) { const params=new URLSearchParams(url.searchParams); if(url.pathname==='/api/plans'&&!params.has('kind'))params.set('kind','plans'); const metadata=new URLSearchParams(params);if(params.get('content')==='1')metadata.delete('q');let result=await library.query(metadata);if(params.get('content')==='1'&&params.get('q')?.trim())result={...result,...await searchCheckout(config,library,params)}; return send(200,{checkout:path.basename(config.repoRoot),checkoutPath:config.repoRoot,...result,plans:result.documents,initialPath}); }
124
+ if (req.method === 'GET' && url.pathname === '/api/document') { await library.refresh(); return send(200,openDocument(req,url.searchParams.get('path'))); }
125
+ if (req.method === 'GET' && url.pathname === '/api/link') { await library.refresh(); return send(200,library.resolve(url.searchParams.get('ref'),url.searchParams.get('from')??'')); }
126
+ if(req.method==='GET'&&url.pathname==='/api/settings')return send(200,{checkout:path.basename(config.repoRoot),roots:(config.docsRoots??[config.docsRoot]).map(root=>relative(root,config)),exclusions:[...config.excludeDirs],statuses:resolveStatusMetadata(config),configFile:config.configPath?relative(config.configPath,config):null,sharing:{records:'Markdown in this checkout; share through Git',preferences:'This browser',drafts:'Private local editor state',templates:'runlist.templates.json'}});
127
+ if(req.method==='GET'&&url.pathname==='/api/filing')return filing.request(url.searchParams,req);
128
+ if(req.method==='GET'&&url.pathname==='/api/yardstick')return yardstick.information(req,url.searchParams.get('path'));
129
+ if(req.method==='GET'&&url.pathname==='/api/lifecycle')return send(200,lifecycle.information(req,url.searchParams.get('path')));
130
+ if(req.method==='GET'&&url.pathname==='/api/records'){
131
+ const kind=url.searchParams.get('kind');if(!['flags','decisions'].includes(kind))problem('invalid-request','Choose flags or decisions.');
132
+ return send(200,url.searchParams.has('key')?await records.detail(req,kind,url.searchParams.get('key')):await records.query(req,kind,url.searchParams));
133
+ }
134
+ if (req.method === 'POST') {
135
+ if (!body || typeof body !== 'object' || Array.isArray(body)) problem('invalid-request','Use a JSON object.');
136
+ if(['actor','checkout','repoRoot'].some(key=>key in body))problem('forbidden','Identity and checkout authority belong to the local application.');
137
+ if(url.pathname==='/api/git/commit/preview')return gitCommits.preview(req,body);
138
+ if(url.pathname==='/api/git/commit/start')return gitCommits.start(req,body);
139
+ if(url.pathname==='/api/git/operation/inspect')return gitCommits.inspectJob(req,body);
140
+ if(url.pathname==='/api/git/operation/recover')return gitCommits.recover(req,body);
141
+ if(url.pathname==='/api/git/operation/settle')return gitCommits.settle(req,body);
142
+ if(url.pathname==='/api/git/operation/cancel')return gitCommits.cancel(req,body);
143
+ if(url.pathname==='/api/create/preview')return creation.preview(req,body);
144
+ if(url.pathname==='/api/create/inspect')return creation.inspect(req,body);
145
+ if(url.pathname==='/api/create/discard')return creation.discard(req,body);
146
+ if(url.pathname==='/api/create/commit'){const result=creation.commit(req,body);invalidateReports();return result;}
147
+ if(url.pathname==='/api/yardstick/preview')return yardstick.preview(req,body);
148
+ if(url.pathname==='/api/yardstick/commit'){const result=await yardstick.commit(req,body);invalidateReports();return result;}
149
+ if(url.pathname==='/api/yardstick/inspect')return yardstick.inspect(req,body);
150
+ if(url.pathname==='/api/yardstick/settle')return yardstick.settle(req,body);
151
+ if(url.pathname==='/api/lifecycle/preview'){assertYardstickIdle(req,body.path);return send(200,await lifecycle.preview(req,body));}
152
+ if(url.pathname==='/api/lifecycle/commit'){const retained=lifecycle.inspect(req,body);if(retained)assertYardstickIdle(req,retained.path);const result=await lifecycle.commit(req,body);invalidateReports();return send(200,result);}
153
+ if(url.pathname==='/api/lifecycle/inspect')return send(200,lifecycle.inspect(req,body));
154
+ if(url.pathname==='/api/lifecycle/settle')return send(200,lifecycle.settle(req,body));
155
+ if(url.pathname==='/api/native/preview'){
156
+ assertYardstickIdle(req,body.path);
157
+ const opened=editor.read(req,{path:body.path});
158
+ const claim=readPlanOwnership(relative(opened.path,config),config);
159
+ if(claim?.corrupt||claim?.state==='owned')problem('claim-conflict','This record is claimed or its ownership needs repair through the CLI.');
160
+ if(opened.revision!==body.expectedRevision)problem('revision-conflict','The record changed. Reload and review it again.');
161
+ const request={path:relative(opened.path,config),operationId:body.operationId,expectedRevision:opened.revision,action:body.action,note:body.note,optionId:body.optionId,at:new Date().toISOString()};
162
+ request.source=prepareNativeAction(opened.source,request,authenticated.actor);
163
+ return send(200,{request,before:opened.source});
164
+ }
165
+ if(url.pathname==='/api/native/action'){assertYardstickIdle(req,body.path);const result=editor.nativeAction(req,body);invalidateReports(body.path);return send(200,result);}
166
+ if(url.pathname==='/api/flags/triage'){
167
+ const selected=await records.detail(req,'flags',body.key);
168
+ if(selected.native)problem('record-read-only','Use the native record action for this record.');
169
+ if(!/^sha256:[a-f0-9]{64}$/.test(body.expectedRevision??'')||!body.operationId)problem('invalid-request','Review the current flag and supply its revision and operation ID.');
170
+ if(typeof body.note!=='string'||!body.note.trim()||body.note.length>4000)problem('invalid-request','Give a reason, up to 4,000 characters.');
171
+ const human=authenticated.actor;
172
+ const outcome=triageFlag(config,{id:selected.id,event:body.action,note:body.note,expectedRevision:body.expectedRevision,operationId:body.operationId,by:{kind:'person',name:human.label??human.id,id:human.id}});
173
+ records.invalidate();return send(200,{...outcome,actor:human});
174
+ }
175
+ if(url.pathname==='/api/decisions/preview'){
176
+ const selected=await records.detail(req,'decisions',body.key);
177
+ if(!selected.editable||selected.native)problem('record-read-only','Open this record’s source to edit it.');
178
+ if(selected.revision!==body.expectedRevision)problem('decision-revision-conflict','The source changed. Reload the decision and review it again.');
179
+ const source=prepareDecisionAction(selected.source,{...body,id:selected.id,line:selected.line,actor:authenticated.actor,at:new Date().toISOString()},config.raw?.decisions);
180
+ return send(200,{path:selected.path,source,before:selected.source,expectedRevision:selected.revision});
181
+ }
182
+ if (url.pathname === '/api/templates/save') { saveTemplate(config,body); return send(200,appTemplates(config)); }
183
+ if (!['/api/save','/api/undo','/api/draft/read','/api/draft/write','/api/draft/discard','/api/operation/inspect','/api/operation/settle'].includes(url.pathname)) return send(404, { code:'not-found', message:'No such operation.' });
184
+ if (url.pathname.startsWith('/api/operation/')) {
185
+ const retained = editor.inspectOperation(req, body);
186
+ const current = retained ? openDocument(req, retained.path) : null;
187
+ if (url.pathname.endsWith('settle')) return send(200, editor.settleOperation(req, body));
188
+ return send(200, retained ? { ...retained, currentSource:current.source, currentRevision:current.revision } : null);
189
+ }
190
+ // Save/undo/draft writes validate current source under their own
191
+ // guards. Draft reads without retained state and discard also need a
192
+ // stable source read, but none needs a second rendered document and
193
+ // related evidence before the guarded operation.
194
+ if(['/api/draft/read','/api/draft/discard'].includes(url.pathname))editor.read(req,{path:body.path});
195
+ const methods = { '/api/save':'save', '/api/undo':'undo', '/api/draft/read':'readDraft', '/api/draft/write':'putDraft', '/api/draft/discard':'discardDraft' };
196
+ if(['/api/save','/api/undo','/api/draft/write'].includes(url.pathname))assertYardstickIdle(req,body.path);
197
+ const result = editor[methods[url.pathname]](req, body);
198
+ if(['/api/save','/api/undo'].includes(url.pathname)){invalidateReports(body.path);}
199
+ return send(200, result);
200
+ }
201
+ problem('not-found','No such checkout operation.');
202
+ }
203
+ return {request,close:()=>semantic.close()};
204
+ }
205
+ export function appError(error) {
206
+ const code=error.code || 'request-failed';
207
+ return {code,message:error instanceof SourceEditError || /template|flag-|decision-/.test(code) ? error.message : 'The operation failed. Check the selected checkout.',details:error.details ?? {}};
208
+ }