@spectie/adapter-javascript 0.0.0-stage → 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 spectie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # rf.javascript 1.0.0
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 当前接入使用协议 1:`spectie adapter add /本包目录`,自动注册并精确选择;Node 包先在本包目录执行 `npm ci --ignore-scripts`。共享运行时、依赖声明及完整流程见 [扩展说明](https://github.com/fanglinwei/spectie/blob/main/docs/ADAPTERS.md)。
4
+
5
+ Independent adapter, protocol 1 / manifest schema 1. Requires Node.js >=22; actual parser versions are in adapter.json and package-lock.json. Dependencies belong to this package, never the analyzed application.
6
+
7
+ For source installation: run `npm ci --ignore-scripts --no-audit --no-fund --prefix /absolute/package-directory`. A complete offline artifact already includes those dependencies and needs no npm install.
8
+
9
+ Explicit registration (executes only this chosen adapter's self-check):
10
+
11
+ ```sh
12
+ python3 /absolute/path/rf.py --project /absolute/project adapter-register --manifest /absolute/package-directory/adapter.json
13
+ ```
14
+
15
+ Then use the core's adapter-select command to explicitly lock `rf.javascript@1.0.0` in the project. Registration alone does not select or approve any requirement. Multiple versions may coexist; no auto-updates. The executable reads one JSON request from stdin and writes one JSON response to stdout; diagnostics use stderr. Subprocess separation is not an OS sandbox.
16
+
17
+ Capabilities: language.js-ts. Category: language. All observations are static source facts, never business verification. Detailed supported concrete versions and limitations are recorded by the package manifest and [adapter guide](https://github.com/fanglinwei/spectie/blob/main/docs/ADAPTERS.md).
18
+
19
+ This adapter proves local component registration against the default exported configuration. Vue-provided template-expression units reuse this package's TypeScript parser and return template_use facts with resolved declaration spans or explicit unknown diagnostics; template local scopes remain unknown. No expression is executed.
20
+
21
+ 保留同名重载事实及歧义诊断,impact 退回文件粒度,不据缺失事实误报符号增删;默认导出配置中的静态本地组件注册也记录绑定,无法定位目标时保留 unknown 并阻止同名自动导入。
22
+
23
+ 复用每份源码的 UTF-8 字节位置索引,减少重复扫描;事实、原文跨度和指纹算法保持不变。
package/adapter.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "manifest_schema": "1",
3
+ "adapter_id": "rf.javascript",
4
+ "version": "1.0.0",
5
+ "category": "language",
6
+ "protocol": "1",
7
+ "capabilities": [
8
+ {
9
+ "id": "language.js-ts",
10
+ "phase": "language",
11
+ "input_types": [
12
+ ".js",
13
+ ".jsx",
14
+ ".ts",
15
+ ".tsx",
16
+ ".mjs",
17
+ ".cjs"
18
+ ],
19
+ "requires": []
20
+ }
21
+ ],
22
+ "entry": [
23
+ "node",
24
+ "{package}/adapter.mjs"
25
+ ],
26
+ "runtime": {
27
+ "name": "node",
28
+ "minimum": "22"
29
+ },
30
+ "requires": [],
31
+ "parsers": {
32
+ "typescript": "5.9.3"
33
+ },
34
+ "algorithms": [
35
+ "web-raw-span-v1"
36
+ ],
37
+ "limits": [
38
+ "static_only",
39
+ "no_project_execution"
40
+ ],
41
+ "resolution_extensions": [
42
+ ".js",
43
+ ".jsx",
44
+ ".ts",
45
+ ".tsx",
46
+ ".mjs",
47
+ ".cjs"
48
+ ]
49
+ }
package/adapter.mjs ADDED
@@ -0,0 +1,190 @@
1
+ import path from 'node:path/posix';
2
+ import {serve, language, documents, diagnostic, span} from '@spectie/adapter';
3
+ await serve(async metadata => {
4
+ const {default:ts} = await import('typescript');
5
+ if(ts.version !== '5.9.3') throw Error('TypeScript version mismatch');
6
+ function parse(source,filename,inputType) {
7
+ const kind=inputType.endsWith('x')?(inputType==='.tsx'?ts.ScriptKind.TSX:ts.ScriptKind.JSX):inputType==='.ts'?ts.ScriptKind.TS:ts.ScriptKind.JS;
8
+ const sf=ts.createSourceFile(filename,source,ts.ScriptTarget.Latest,true,kind);
9
+ const program=ts.createProgram([filename],{noLib:true,noResolve:true,allowJs:true,allowNonTsExtensions:true,target:ts.ScriptTarget.Latest}, {
10
+ getSourceFile:n=>n===filename?sf:undefined,getDefaultLibFileName:()=>'',writeFile:()=>{},getCurrentDirectory:()=>'',getDirectories:()=>[],fileExists:n=>n===filename,readFile:n=>n===filename?source:undefined,getCanonicalFileName:n=>n,useCaseSensitiveFileNames:()=>true,getNewLine:()=> '\n'
11
+ });
12
+ return {sf,checker:program.getTypeChecker()};
13
+ }
14
+ const analyzeScripts = await language({
15
+ analyze(doc) {
16
+ const out={facts:[],diagnostics:[]}, source=doc.text;
17
+ const {sf,checker}=parse(source,doc.path,doc.inputType);
18
+ if(sf.parseDiagnostics.length) {diagnostic(out,'parse_error',doc.path,'TypeScript syntax diagnostics; file fallback');return {...out,complete:false};}
19
+ const imports=new Map(), importSymbols=new Map(), names=new Map();
20
+ const at=n=>doc.span(n.getStart(sf,false),n.end);
21
+ const add=(kind,n,extra={})=>{const f={kind,...at(n),...extra};out.facts.push(f);return f;};
22
+ const ref=(n,specifier,name='import',data={})=>add('reference',n,{specifier,name,data});
23
+ const bind=(name,decl,specifier,imported)=>{
24
+ imports.set(name,{specifier,imported}); importSymbols.set(checker.getSymbolAtLocation(decl),{name,specifier,imported});
25
+ add('binding',decl,{name,specifier,data:{role:'import',imported,unit_role:doc.role}});
26
+ };
27
+ for(const stmt of sf.statements) {
28
+ if(ts.isImportDeclaration(stmt)&&ts.isStringLiteral(stmt.moduleSpecifier)) {
29
+ const spec=stmt.moduleSpecifier.text;ref(stmt.moduleSpecifier,spec,/\.(css|scss|sass|less|styl|svg|png|jpg|woff2)(?:[?#]|$)/.test(spec)?'resource_import':'import');const c=stmt.importClause;
30
+ if(c?.name)bind(c.name.text,c.name,spec,'default');
31
+ if(c?.namedBindings) {
32
+ if(ts.isNamespaceImport(c.namedBindings))bind(c.namedBindings.name.text,c.namedBindings.name,spec,'*');
33
+ else for(const b of c.namedBindings.elements)bind(b.name.text,b.name,spec,b.propertyName?.text||b.name.text);
34
+ }
35
+ }
36
+ if(ts.isExportDeclaration(stmt)) {
37
+ if(stmt.moduleSpecifier&&ts.isStringLiteral(stmt.moduleSpecifier))ref(stmt.moduleSpecifier,stmt.moduleSpecifier.text,'reexport');
38
+ add('export',stmt,{name:stmt.exportClause?.getText(sf)||'*'});
39
+ }
40
+ }
41
+ function owner(n) {
42
+ const parts=[]; for(let p=n.parent;p&&!ts.isSourceFile(p);p=p.parent) {
43
+ if((ts.isClassDeclaration(p)||ts.isFunctionDeclaration(p)||ts.isMethodDeclaration(p)||ts.isPropertyAssignment(p)||ts.isVariableDeclaration(p))&&p.name&& (ts.isIdentifier(p.name)||ts.isStringLiteral(p.name)))parts.unshift(p.name.text);
44
+ }
45
+ return parts;
46
+ }
47
+ function symbol(n,name,range=n) {
48
+ const key=[...owner(n),name].join('.');
49
+ const fact=doc.symbol({start:range.getStart(sf,false),end:range.end,name:key,data:{unit_role:doc.role}});
50
+ out.facts.push(fact);
51
+ names.set(key,[...(names.get(key)||[]),fact]);
52
+ }
53
+ function imported(n) {
54
+ return ts.isIdentifier(n)?importSymbols.get(checker.getSymbolAtLocation(n)):null;
55
+ }
56
+ const propertyName=n=>n?.name&&(ts.isIdentifier(n.name)||ts.isStringLiteral(n.name))?n.name.text:null;
57
+ function object(n,seen=new Set()) {
58
+ if(!n||seen.has(n))return null;seen.add(n);
59
+ if(ts.isObjectLiteralExpression(n)){
60
+ const keys=n.properties.map(propertyName);
61
+ return keys.every(k=>k!==null)&&new Set(keys).size===keys.length?n:null;
62
+ }
63
+ if(ts.isParenthesizedExpression(n)||ts.isAsExpression(n)||ts.isSatisfiesExpression(n))return object(n.expression,seen);
64
+ if(ts.isIdentifier(n)){
65
+ const d=checker.getSymbolAtLocation(n)?.valueDeclaration;
66
+ if(d&&ts.isVariableDeclaration(d)&&(d.parent.flags&ts.NodeFlags.Const))return object(d.initializer,seen);
67
+ }
68
+ if(ts.isCallExpression(n)){
69
+ const b=imported(n.expression),e=n.expression;
70
+ if((b?.specifier==='vue'&&b.imported==='defineComponent')||(ts.isPropertyAccessExpression(e)&&e.name.text==='extend'&&imported(e.expression)?.specifier==='vue'&&imported(e.expression)?.imported==='default'))return object(n.arguments[0],seen);
71
+ }
72
+ return null;
73
+ }
74
+ const options=new Set(sf.statements.filter(n=>ts.isExportAssignment(n)&&!n.isExportEquals).map(n=>object(n.expression)).filter(Boolean));
75
+ const expose=(n,name)=>add('binding',n,{name,data:{role:'template_declaration',unit_role:doc.role}});
76
+ if(doc.role==='script-setup')for(const stmt of sf.statements){
77
+ if((ts.isFunctionDeclaration(stmt)||ts.isClassDeclaration(stmt))&&stmt.name)expose(stmt.name,stmt.name.text);
78
+ if(ts.isVariableStatement(stmt))for(const d of stmt.declarationList.declarations)if(ts.isIdentifier(d.name))expose(d.name,d.name.text);
79
+ }
80
+ for(const option of options)for(const prop of option.properties){
81
+ const name=propertyName(prop);
82
+ if(['methods','computed','props'].includes(name)&&ts.isPropertyAssignment(prop)){
83
+ const value=object(prop.initializer);
84
+ if(value)for(const member of value.properties)if(member.name&&(ts.isIdentifier(member.name)||ts.isStringLiteral(member.name)))expose(member.name,member.name.text);
85
+ if(name==='props'&&ts.isArrayLiteralExpression(prop.initializer))for(const item of prop.initializer.elements)if(ts.isStringLiteral(item))expose(item,item.text);
86
+ }
87
+ if(['data','setup'].includes(name)){
88
+ const fn=ts.isMethodDeclaration(prop)?prop:ts.isPropertyAssignment(prop)?prop.initializer:null;
89
+ if(fn?.body&&ts.isBlock(fn.body))for(const stmt of fn.body.statements)if(ts.isReturnStatement(stmt)){
90
+ const value=object(stmt.expression);
91
+ if(value)for(const member of value.properties)if(member.name&&(ts.isIdentifier(member.name)||ts.isStringLiteral(member.name)))expose(member.name,member.name.text);
92
+ }
93
+ }
94
+ }
95
+ function walk(n) {
96
+ if((ts.isFunctionDeclaration(n)||ts.isClassDeclaration(n))&&!n.name)diagnostic(out,"anonymous_owner_fallback",doc.path,"Anonymous declaration; enclosing file fallback");
97
+ if((ts.isFunctionDeclaration(n)||ts.isClassDeclaration(n)||ts.isMethodDeclaration(n)||ts.isGetAccessor(n)||ts.isSetAccessor(n))&&n.name) {
98
+ if(ts.isIdentifier(n.name)||ts.isStringLiteral(n.name))symbol(n,n.name.text);
99
+ else diagnostic(out,'ambiguous_symbol',doc.path,'Computed declaration name; file fallback');
100
+ }
101
+ if(ts.isVariableStatement(n)) for(const d of n.declarationList.declarations) {
102
+ if(ts.isIdentifier(d.name))symbol(d,d.name.text,n);
103
+ else diagnostic(out,'ambiguous_symbol',doc.path,'Destructured variable; file fallback');
104
+ }
105
+ if(ts.isPropertyAssignment(n)&&n.name&&(ts.isArrowFunction(n.initializer)||ts.isFunctionExpression(n.initializer))) {
106
+ if(ts.isIdentifier(n.name)||ts.isStringLiteral(n.name))symbol(n,n.name.text);
107
+ }
108
+ if((ts.isArrowFunction(n)||ts.isFunctionExpression(n))&&!ts.isVariableDeclaration(n.parent)&&!ts.isPropertyAssignment(n.parent))diagnostic(out,'anonymous_owner_fallback',doc.path,'Anonymous callback retained by enclosing span/file');
109
+ if(ts.isIdentifier(n)) {
110
+ const b=imported(n);
111
+ if(b && !ts.isImportSpecifier(n.parent)&&!ts.isImportClause(n.parent)&&!ts.isNamespaceImport(n.parent))ref(n,b.specifier,'use',{local:b.name,imported:b.imported});
112
+ }
113
+ if(ts.isObjectLiteralExpression(n)) for(const prop of n.properties) {
114
+ const value=ts.isShorthandPropertyAssignment(prop)?prop.name:ts.isPropertyAssignment(prop)?prop.initializer:null;
115
+ const b=value&&(ts.isShorthandPropertyAssignment(prop)?importSymbols.get(checker.getShorthandAssignmentValueSymbol(prop)):imported(value));
116
+ const owner=ts.isPropertyAssignment(n.parent)?propertyName(n.parent):'',component_export=owner==='components'&&options.has(n.parent.parent)&&object(n)===n;
117
+ if((b||component_export)&&prop.name&&(ts.isIdentifier(prop.name)||ts.isStringLiteral(prop.name))) {
118
+ add('binding',prop,{name:prop.name.text,...(b?{specifier:b.specifier}:{}),data:{role:'object_member',owner,component_export,...(b?{local:b.name,imported:b.imported}:{})}});
119
+ }
120
+ }
121
+ if(ts.isCallExpression(n)) {
122
+ if(n.expression.kind===ts.SyntaxKind.ImportKeyword||n.expression.getText(sf)==='require') {
123
+ diagnostic(out,'dynamic_loading_unknown',doc.path,'Dynamic import/require not resolved');
124
+ }
125
+ if(ts.isPropertyAccessExpression(n.expression)&&n.expression.name.text==='component') {
126
+ const [name,value]=n.arguments,b=value&&imported(value);
127
+ let receiver=n.expression.expression, proven=false, api='';
128
+ const root=imported(receiver);
129
+ if(root?.specifier==='vue' && root.imported==='default') {proven=true;api='Vue.component';}
130
+ if(ts.isIdentifier(receiver)) {
131
+ const decl=checker.getSymbolAtLocation(receiver)?.valueDeclaration;
132
+ if(decl&&ts.isVariableDeclaration(decl)&&decl.initializer&&ts.isCallExpression(decl.initializer)) {
133
+ const factory=imported(decl.initializer.expression);
134
+ if(factory?.specifier==='vue'&&factory.imported==='createApp'){proven=true;api='app.component';}
135
+ }
136
+ }
137
+ if(proven&&name&&ts.isStringLiteral(name)&&b)add('binding',n,{name:name.text,specifier:b.specifier,data:{role:'global_component',api}});
138
+ else diagnostic(out,'dynamic_registration_unknown',doc.path,'Component registration cannot be proved statically');
139
+ }
140
+ }
141
+ if(ts.isPropertyAccessExpression(n)&&ts.isPropertyAccessExpression(n.expression)&&['import.meta.env','process.env'].includes(n.expression.getText(sf)))add('environment_key',n.name,{name:n.name.text});
142
+ if(ts.isElementAccessExpression(n)&&['import.meta.env','process.env'].includes(n.expression.getText(sf))) {
143
+ if(n.argumentExpression&&ts.isStringLiteral(n.argumentExpression))add('environment_key',n,{name:n.argumentExpression.text});
144
+ else diagnostic(out,'dynamic_environment_key',doc.path,'Environment values never read');
145
+ }
146
+ ts.forEachChild(n,walk);
147
+ }
148
+ walk(sf);
149
+ for(const [name,fs] of names)if(fs.length>1)diagnostic(out,'ambiguous_symbol',doc.path,`Duplicate/overloaded symbol ${name}; file fallback`);
150
+ return out;
151
+ },
152
+ })(metadata);
153
+ return async input => {
154
+ const out=await analyzeScripts({...input,units:input.units.filter(u=>!u.role.startsWith('template-expression'))});
155
+ const docs=documents(input);
156
+ for(const unit of input.units.filter(u=>u.role.startsWith('template-expression'))){
157
+ const doc=docs.get(unit.path);
158
+ if(doc.text===null)continue;
159
+ const source=new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(doc.raw.subarray(unit.start,unit.end));
160
+ const unknown=()=>{
161
+ out.facts.push({kind:'template_use',path:unit.path,start:unit.start,end:unit.end,name:source,data:{role:'binding_use',status:'unknown'}});
162
+ diagnostic(out,'template_binding_unknown',unit.path,'Template scope or expression cannot be proved statically: '+source);
163
+ };
164
+ // shortcut: template local scopes remain unknown until lexical template scope resolution is supported.
165
+ if(unit.role.endsWith('-scoped')){unknown();out.processed.push(unit);continue;}
166
+ let prefix=1,{sf,checker}=parse('('+source+')',unit.path,unit.input_type);
167
+ if(sf.parseDiagnostics.length){prefix=0;({sf,checker}=parse(source,unit.path,unit.input_type));}
168
+ if(sf.parseDiagnostics.length){unknown();continue;}
169
+ const resolveSource=f=>path.normalize(path.join(path.dirname(f.path),f.specifier));
170
+ const owners=new Set([unit.path,...input.facts.filter(f=>f.kind==='reference'&&f.name==='sfc_external_source'&&f.data?.role==='template'&&resolveSource(f)===unit.path).map(f=>f.path)]);
171
+ const scripts=new Set([...owners,...input.facts.filter(f=>f.kind==='reference'&&f.name==='sfc_external_source'&&f.data?.role==='script'&&owners.has(f.path)).map(resolveSource)]);
172
+ const bindings=out.facts.filter(f=>f.kind==='binding'&&scripts.has(f.path)&&(f.data?.role==='template_declaration'||(f.data?.role==='import'&&f.data.unit_role==='script-setup')));
173
+ function uses(n){
174
+ if(ts.isIdentifier(n)){
175
+ const p=n.parent,shorthand=ts.isShorthandPropertyAssignment(p);
176
+ const property=(ts.isPropertyAccessExpression(p)&&p.name===n)||(ts.isPropertyAssignment(p)&&p.name===n);
177
+ const local=shorthand?checker.getShorthandAssignmentValueSymbol(p):checker.getSymbolAtLocation(n);
178
+ if(!property&&!local?.valueDeclaration&&!(n.text==='$event'&&unit.role.includes('-event'))){
179
+ const candidates=bindings.filter(f=>f.name===n.text),binding=candidates.length===1?candidates[0]:null;
180
+ out.facts.push({kind:'template_use',...span(unit.path,source,n.getStart(sf)-prefix,n.end-prefix,unit.start),name:n.text,data:{role:'binding_use',status:binding?'resolved':'unknown',...(binding?{binding}:{} )}});
181
+ if(!binding)diagnostic(out,'template_binding_unknown',unit.path,'No unique exposed declaration for '+n.text);
182
+ }
183
+ }
184
+ ts.forEachChild(n,uses);
185
+ }
186
+ uses(sf);out.processed.push(unit);
187
+ }
188
+ out.processed=[...new Set(out.processed)];return out;
189
+ };
190
+ }, import.meta.url);
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 spectie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ # @spectie/adapter
2
+
3
+ 协议 1 的 Node 22+ 适配运行时,独立于 Python 核心。
4
+
5
+ - `run(language({analyze, offsets, load}), import.meta.url)`:普通语言包。
6
+ - `serve(async metadata => analyzeBatch, import.meta.url)`:跨文件、多阶段包。
7
+ - `documents(input)`:核对文件 SHA-256 并解码,非 UTF-8 的 text 为 null。
8
+ - `doc.span(start, end)`:按当前文档坐标生成原文件范围。
9
+ - `span(path, text, start, end, byteOffset=0)`:UTF-16 到 UTF-8 位置转换。
10
+ - `output()`、`diagnostic(...)`:批量结果与诊断构造。
11
+
12
+ package.json 的 spectie 字段声明 capabilities,可选 parsers、algorithms 等;不必重复版本。运行时从 adapter.json(若存在)或 package.json 读取清单。详见 [公共 Interface](../../docs/ADAPTERS.md)。language 的 analyze 必须返回对象;{} 是成功空结果,complete:false 表示部分处理,抛异常不会记录完整覆盖。
13
+
14
+ 开发时使用 `file:../../adapter-runtime/node` 依赖并配置 `install-links=true`,修改共享运行时后需重新安装本地副本。四个现有 Node 包 bundle 运行时,npm/offline 包不依赖仓库外部相对路径。扩展也可使用 registry 中已发布的准确运行时版本;从 GitHub Release 获取 SDK tgz 后,可用 `npm install /包/spectie-adapter-1.0.0.tgz` 安装。
15
+
16
+ 打包本运行时:`npm pack --pack-destination /tmp`。只导出 index.mjs,不导入核心私有实现。
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs';
2
+ import crypto from 'node:crypto';
3
+
4
+ // Same normalization as the core; handshake checks the resulting manifest exactly.
5
+ export function manifest(url) {
6
+ let source;
7
+ try { source = JSON.parse(fs.readFileSync(new URL('./adapter.json', url), 'utf8')); }
8
+ catch (error) {
9
+ if (error.code !== 'ENOENT') throw error;
10
+ const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', url), 'utf8'));
11
+ source = {adapter_id: pkg.name.replace(/^@/, '').replace('/', '.'), ...pkg.spectie, version: pkg.version};
12
+ }
13
+ const value = {
14
+ manifest_schema:'1', category:'language', protocol:'1',
15
+ entry:['node','{package}/adapter.mjs'], runtime:{name:'node',minimum:'22'},
16
+ requires:[], parsers:{}, algorithms:['raw-span-v1'], limits:[], ...source,
17
+ };
18
+ value.capabilities = value.capabilities.map(cap => ({id:value.adapter_id, phase:'language', requires:[], ...cap}));
19
+ value.resolution_extensions ??= [...new Set(value.capabilities.flatMap(cap => cap.input_types).filter(t => t !== '*'))];
20
+ return value;
21
+ }
22
+
23
+ export function documents(input) {
24
+ const result = new Map();
25
+ for (const file of input.files) {
26
+ const raw = Buffer.from(file.content, 'base64');
27
+ if (result.has(file.path) || crypto.createHash('sha256').update(raw).digest('hex') !== file.sha256)
28
+ throw Error('input digest mismatch or duplicate file');
29
+ let text = null;
30
+ try { text = new TextDecoder('utf-8', {fatal:true, ignoreBOM:true}).decode(raw); } catch {}
31
+ result.set(file.path, {...file, raw, text});
32
+ }
33
+ return result;
34
+ }
35
+ export function output() { return {facts:[], embeddings:[], diagnostics:[], processed:[]}; }
36
+ export function diagnostic(out, code, path, message) { out.diagnostics.push({code,path,message}); }
37
+ // Indexes live only for this single-request adapter process.
38
+ const byteOffsets = new Map();
39
+ export function span(path, source, start, end, offset=0) {
40
+ let offsets = byteOffsets.get(source);
41
+ if (!offsets) {
42
+ offsets = new Uint32Array(source.length + 1);
43
+ for (let i = 0; i < source.length; i++) {
44
+ const code = source.charCodeAt(i), next = source.charCodeAt(i + 1);
45
+ if (code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
46
+ offsets[i + 1] = offsets[i] + 3; // A prefix ending inside a pair encodes a replacement character.
47
+ offsets[i + 2] = offsets[i] + 4;
48
+ i++;
49
+ } else offsets[i + 1] = offsets[i] + (code < 0x80 ? 1 : code < 0x800 ? 2 : 3);
50
+ }
51
+ byteOffsets.set(source, offsets);
52
+ }
53
+ return {path,start:offset+offsets[start],end:offset+offsets[end]};
54
+ }
55
+
56
+ export async function serve(load, url) {
57
+ let request, metadata;
58
+ try {
59
+ metadata = manifest(url);
60
+ request = JSON.parse(fs.readFileSync(0, 'utf8'));
61
+ if (request.protocol_version !== '1' || request.adapter_id !== metadata.adapter_id || request.adapter_version !== metadata.version)
62
+ throw Error('incompatible request');
63
+ if (!['handshake','analyze'].includes(request.action) || (request.action === 'handshake' && Object.keys(request.input).length))
64
+ throw Error('unsupported action or handshake input');
65
+ if (Number(process.versions.node.split('.')[0]) < 22) throw Error('Node >=22 required');
66
+ const handler = await load(metadata); // dependency checks also run during handshake
67
+ const result = request.action === 'handshake' ? metadata : await handler(request.input);
68
+ const {protocol_version, request_id, adapter_id, adapter_version, input_digest} = request;
69
+ process.stdout.write(JSON.stringify({protocol_version,request_id,adapter_id,adapter_version,input_digest,
70
+ status:result.diagnostics?.length?'partial':'complete',result}));
71
+ } catch (error) {
72
+ if (!request || !metadata) { process.stderr.write(String(error)); process.exitCode=2; return; }
73
+ const {protocol_version, request_id, input_digest} = request;
74
+ process.stdout.write(JSON.stringify({protocol_version,request_id,adapter_id:metadata.adapter_id,adapter_version:metadata.version,input_digest,
75
+ status:'failure',result:{code:['ERR_MODULE_NOT_FOUND','MODULE_NOT_FOUND'].includes(error.code)?'missing_dependency':'adapter_analysis_failure',message:String(error)}}));
76
+ }
77
+ }
78
+
79
+ // The batch interface remains available through serve for cross-file and phased adapters.
80
+ export function language({analyze, offsets='utf16', load=async()=>{}}) {
81
+ if (!['utf16','bytes'].includes(offsets)) throw Error('offsets must be utf16 or bytes');
82
+ return async metadata => {
83
+ await load(metadata);
84
+ return async input => {
85
+ const out = output(), docs = documents(input);
86
+ for (const unit of input.units) {
87
+ const file = docs.get(unit.path), raw = file.raw.subarray(unit.start, unit.end);
88
+ let text;
89
+ try { text = new TextDecoder('utf-8', {fatal:true,ignoreBOM:true}).decode(raw); }
90
+ catch { diagnostic(out,'unsupported_encoding',unit.path,'Only strict UTF-8 supported'); continue; }
91
+ const at = (start,end) => {
92
+ const size = offsets === 'bytes' ? raw.length : text.length;
93
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || start > end || end > size)
94
+ throw Error('invalid source range');
95
+ if (offsets === 'bytes') return {path:unit.path,start:unit.start+start,end:unit.start+end};
96
+ // Parsers must never split a surrogate pair, even though JS permits slicing one.
97
+ for (const n of [start,end]) if (n > 0 && /[\uD800-\uDBFF]/.test(text[n-1]) && /[\uDC00-\uDFFF]/.test(text[n] ?? ''))
98
+ throw Error('range splits a surrogate pair');
99
+ return span(unit.path,text,start,end,unit.start);
100
+ };
101
+ const document = {path:unit.path,text,raw,inputType:unit.input_type,role:unit.role,span:at,
102
+ symbol:({start,end,algorithm=metadata.algorithms[0],...rest}) => ({...rest,kind:'symbol',...at(start,end),algorithm}),
103
+ region:({start,end,algorithm=metadata.algorithms[0],...rest}) => ({...rest,kind:'region',...at(start,end),algorithm}),
104
+ reference:({start,end,...rest}) => ({...rest,kind:'reference',...at(start,end)})};
105
+ try {
106
+ const result = await analyze(document, input.config);
107
+ if (!result || typeof result !== 'object' || Array.isArray(result)) throw Error('return a result object; use {} for a successful empty result');
108
+ if (Object.keys(result).some(key => !['facts','embeddings','diagnostics','complete'].includes(key))) throw Error('unsupported language result field');
109
+ for (const key of ['facts','embeddings','diagnostics']) {
110
+ if (result[key] !== undefined && !Array.isArray(result[key])) throw Error(key+' must be an array');
111
+ }
112
+ if (result.complete !== undefined && typeof result.complete !== 'boolean') throw Error('complete must be boolean');
113
+ for (const key of ['facts','embeddings','diagnostics']) out[key].push(...(result[key] ?? []));
114
+ if (result.complete !== false) out.processed.push(unit);
115
+ else diagnostic(out,'partial_parse',unit.path,'Parser did not complete this unit');
116
+ } catch (error) { diagnostic(out,'parse_error',unit.path,String(error)); }
117
+ }
118
+ return out;
119
+ };
120
+ };
121
+ }
122
+ export function run(adapter, url) { return serve(adapter, url); }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@spectie/adapter",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": "./index.mjs",
6
+ "files": [
7
+ "index.mjs"
8
+ ],
9
+ "license": "MIT",
10
+ "engines": {
11
+ "node": ">=22"
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@spectie/adapter-javascript",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "files": [
10
+ "adapter.mjs",
11
+ "adapter.json",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "dependencies": {
16
+ "typescript": "5.9.3",
17
+ "@spectie/adapter": "file:../../adapter-runtime/node"
18
+ },
19
+ "bundleDependencies": [
20
+ "@spectie/adapter"
21
+ ]
22
+ }