@aqaf/cli 0.0.0-stage → 1.1.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.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/bin/aqaf.js +12 -0
- package/index.js +51 -0
- package/package.json +51 -4
- package/vendor/aqaf/__init__.py +2 -0
- package/vendor/aqaf/__main__.py +3 -0
- package/vendor/aqaf/adapters.py +583 -0
- package/vendor/aqaf/artifacts.py +28 -0
- package/vendor/aqaf/calibration.py +29 -0
- package/vendor/aqaf/catalogue.py +257 -0
- package/vendor/aqaf/cli.py +721 -0
- package/vendor/aqaf/cursor.py +56 -0
- package/vendor/aqaf/data/book_review_knowledge.json +1574 -0
- package/vendor/aqaf/data/catalog.json +562 -0
- package/vendor/aqaf/data/documentation_checks.json +52 -0
- package/vendor/aqaf/data/packs/automation.json +16 -0
- package/vendor/aqaf/data/packs/mobile.json +13 -0
- package/vendor/aqaf/data/packs/systems.json +17 -0
- package/vendor/aqaf/data/packs/web.json +16 -0
- package/vendor/aqaf/data/profiles/ai_pipeline.json +39 -0
- package/vendor/aqaf/data/profiles/android.json +34 -0
- package/vendor/aqaf/data/profiles/api_backend.json +44 -0
- package/vendor/aqaf/data/profiles/browser_extension.json +29 -0
- package/vendor/aqaf/data/profiles/cli_automation.json +34 -0
- package/vendor/aqaf/data/profiles/desktop_utility.json +39 -0
- package/vendor/aqaf/data/profiles/embedded_iot.json +34 -0
- package/vendor/aqaf/data/profiles/google_apps_script.json +34 -0
- package/vendor/aqaf/data/profiles/mcp_agent_server.json +34 -0
- package/vendor/aqaf/data/profiles/multirepo_system.json +29 -0
- package/vendor/aqaf/data/profiles/npm_library.json +39 -0
- package/vendor/aqaf/data/profiles/rust_app.json +34 -0
- package/vendor/aqaf/data/profiles/saas_or_web_app.json +44 -0
- package/vendor/aqaf/data/profiles/scraper_data_pipeline.json +39 -0
- package/vendor/aqaf/data/profiles/swift_app.json +34 -0
- package/vendor/aqaf/data/profiles/tauri_electron.json +34 -0
- package/vendor/aqaf/data/profiles/unknown_or_general_app.json +25 -0
- package/vendor/aqaf/data/profiles/windows_client_server.json +34 -0
- package/vendor/aqaf/data/profiles/wordpress_plugin.json +39 -0
- package/vendor/aqaf/data/review_checks.json +319 -0
- package/vendor/aqaf/data/source_registry.json +814 -0
- package/vendor/aqaf/discovery.py +580 -0
- package/vendor/aqaf/documentation.py +210 -0
- package/vendor/aqaf/evidence.py +132 -0
- package/vendor/aqaf/facets.py +63 -0
- package/vendor/aqaf/ignore.py +109 -0
- package/vendor/aqaf/knowledge.py +110 -0
- package/vendor/aqaf/markup.py +96 -0
- package/vendor/aqaf/matching.py +22 -0
- package/vendor/aqaf/portfolio.py +145 -0
- package/vendor/aqaf/project_commands.py +132 -0
- package/vendor/aqaf/projects.py +157 -0
- package/vendor/aqaf/registry.py +125 -0
- package/vendor/aqaf/review.py +446 -0
- package/vendor/aqaf/scope.py +401 -0
- package/vendor/aqaf/swift_review.py +99 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Strictly data-only architecture profiles and packs. Does not import or execute extension code."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from importlib import resources
|
|
7
|
+
|
|
8
|
+
IDENT = re.compile(r'^[a-z][a-z0-9_]{1,63}$')
|
|
9
|
+
SIGNALS = {'file','extension','path','text','json_key'}
|
|
10
|
+
|
|
11
|
+
class ProfileError(ValueError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate(doc, category):
|
|
16
|
+
if not isinstance(doc, dict) or doc.get('schema_version') != 1 or not IDENT.fullmatch(str(doc.get('id',''))):
|
|
17
|
+
raise ProfileError('Expected schema_version=1 and lowercase snake_case id')
|
|
18
|
+
if category == 'profile':
|
|
19
|
+
for key in ('name','product_type','target_architecture','status'):
|
|
20
|
+
if not isinstance(doc.get(key),str) or not doc[key].strip():
|
|
21
|
+
raise ProfileError(f'Profile {doc["id"]}: missing {key}')
|
|
22
|
+
if not isinstance(doc.get('packs'),list) or not all(IDENT.fullmatch(str(x)) for x in doc['packs']):
|
|
23
|
+
raise ProfileError(f'Profile {doc["id"]}: invalid packs')
|
|
24
|
+
if not isinstance(doc.get('detection'),list): raise ProfileError('detection must be a list')
|
|
25
|
+
for item in doc['detection']:
|
|
26
|
+
if not isinstance(item,dict) or item.get('kind') not in SIGNALS or not isinstance(item.get('value'),str) or not 0 <= item.get('weight',-1) <= 150:
|
|
27
|
+
raise ProfileError(f'Invalid declarative detection in {doc["id"]}')
|
|
28
|
+
if len(item['value']) > 200 or '..' in item['value']:
|
|
29
|
+
raise ProfileError('Unsafe/oversized detection value')
|
|
30
|
+
elif category == 'pack':
|
|
31
|
+
if not isinstance(doc.get('name'),str) or not isinstance(doc.get('profiles'),list):
|
|
32
|
+
raise ProfileError(f'Pack {doc["id"]}: invalid fields')
|
|
33
|
+
if not all(IDENT.fullmatch(str(p)) for p in doc['profiles']):
|
|
34
|
+
raise ProfileError('Invalid profile ID in pack')
|
|
35
|
+
return doc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_doc(path: Path, category):
|
|
39
|
+
if path.is_symlink() or not path.is_file() or path.stat().st_size > 65536:
|
|
40
|
+
raise ProfileError(f'Invalid or oversized manifest: {path}')
|
|
41
|
+
try: return validate(json.loads(path.read_text(encoding='utf-8')), category)
|
|
42
|
+
except (OSError, ValueError) as e: raise ProfileError(f'{path}: {e}') from e
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Registry:
|
|
46
|
+
def __init__(self, extra_dirs=(), extra_pack_dirs=()):
|
|
47
|
+
self.profiles = {}
|
|
48
|
+
self.packs = {}
|
|
49
|
+
pkg = resources.files('aqaf').joinpath('data')
|
|
50
|
+
for category, name, store in [('profile','profiles',self.profiles),('pack','packs',self.packs)]:
|
|
51
|
+
for item in sorted(pkg.joinpath(name).iterdir(), key=lambda p:p.name):
|
|
52
|
+
if item.name.endswith('.json'):
|
|
53
|
+
value = validate(json.loads(item.read_text(encoding='utf-8')),category)
|
|
54
|
+
if value['id'] in store: raise ProfileError('Duplicate built-in ID')
|
|
55
|
+
store[value['id']] = value
|
|
56
|
+
for folder in extra_dirs:
|
|
57
|
+
path=Path(folder)
|
|
58
|
+
if not path.is_dir(): raise ProfileError(f'Profile directory not found: {path}')
|
|
59
|
+
for item in sorted(path.glob('*.json')):
|
|
60
|
+
value=read_doc(item,'profile')
|
|
61
|
+
if value['id'] in self.profiles:
|
|
62
|
+
raise ProfileError(f'External profile may not override built-in ID {value["id"]}')
|
|
63
|
+
self.profiles[value['id']] = value
|
|
64
|
+
for folder in extra_pack_dirs:
|
|
65
|
+
path=Path(folder)
|
|
66
|
+
if not path.is_dir(): raise ProfileError(f'Pack directory not found: {path}')
|
|
67
|
+
for item in sorted(path.glob('*.json')):
|
|
68
|
+
value=read_doc(item,'pack')
|
|
69
|
+
if value['id'] in self.packs:
|
|
70
|
+
raise ProfileError(f'External pack may not override built-in ID {value["id"]}')
|
|
71
|
+
self.packs[value['id']] = value
|
|
72
|
+
for pack in self.packs.values():
|
|
73
|
+
for p in pack['profiles']:
|
|
74
|
+
if p not in self.profiles: raise ProfileError(f'Pack {pack["id"]}: unknown profile {p}')
|
|
75
|
+
|
|
76
|
+
def get_profile(self, id):
|
|
77
|
+
if id not in self.profiles: raise ProfileError(f'Unknown product profile {id}; run aqaf profiles list')
|
|
78
|
+
return self.profiles[id]
|
|
79
|
+
|
|
80
|
+
def get_pack(self,id):
|
|
81
|
+
if id not in self.packs: raise ProfileError(f'Unknown pack {id}; run aqaf packs list')
|
|
82
|
+
return self.packs[id]
|
|
83
|
+
|
|
84
|
+
def detect(self, files, root, code, pack='auto', reader=None):
|
|
85
|
+
read=reader or _read_small
|
|
86
|
+
if pack!='auto': allowed=set(self.get_pack(pack)['profiles']) | {pid for pid,p in self.profiles.items() if pack in p['packs']}
|
|
87
|
+
else: allowed=set(self.profiles)
|
|
88
|
+
filenames={p.name.lower() for p in files}
|
|
89
|
+
extensions={p.suffix.lower() for p in code}
|
|
90
|
+
paths='\n'.join(str(p.relative_to(root)).replace('\\','/').lower() for p in files)
|
|
91
|
+
# Text signal sampling is bounded; this detector never executes target files.
|
|
92
|
+
snippets='\n'.join(read(p,65536)[:1500].lower() for p in code[:350])
|
|
93
|
+
candidates=[]
|
|
94
|
+
for id,profile in self.profiles.items():
|
|
95
|
+
if id not in allowed or id=='unknown_or_general_app': continue
|
|
96
|
+
evidence=[]; total=0
|
|
97
|
+
for s in profile['detection']:
|
|
98
|
+
kind=s['kind']; val=s['value']; weight=s['weight']; matched=False
|
|
99
|
+
if kind=='file': matched=val.lower() in filenames
|
|
100
|
+
elif kind=='extension': matched=val.lower() in extensions
|
|
101
|
+
elif kind=='path': matched=val.lower() in paths
|
|
102
|
+
elif kind=='text': matched=val.lower() in snippets
|
|
103
|
+
elif kind=='json_key':
|
|
104
|
+
file_name,key=val.split(':',1) if ':' in val else ('','')
|
|
105
|
+
for p in files:
|
|
106
|
+
if p.name.lower()==file_name.lower():
|
|
107
|
+
try:
|
|
108
|
+
d=json.loads(read(p,65536)); matched= key in d if isinstance(d,dict) else False
|
|
109
|
+
except (ValueError,UnicodeError): pass
|
|
110
|
+
if matched: break
|
|
111
|
+
if matched: total+=weight; evidence.append(kind+':'+val)
|
|
112
|
+
if evidence: candidates.append((total,id,evidence))
|
|
113
|
+
if not candidates: return 'unknown_or_general_app',[], 'low', []
|
|
114
|
+
candidates.sort(key=lambda x:(-x[0],x[1]))
|
|
115
|
+
best=candidates[0]
|
|
116
|
+
# Don't treat weak, generic evidence as an authoritative classification.
|
|
117
|
+
confidence='medium' if best[0]>=60 and (len(candidates)==1 or best[0]-candidates[1][0]>=20) else 'low'
|
|
118
|
+
summary=[{'profile':id,'signal_score':score,'signals':evidence} for score,id,evidence in candidates[:5]]
|
|
119
|
+
return best[1], best[2],confidence,summary
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _read_small(p,limit=65536):
|
|
123
|
+
try:
|
|
124
|
+
with p.open('rb') as handle: return handle.read(limit).decode('utf-8','replace')
|
|
125
|
+
except OSError: return ''
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Opt-in, bounded JS/TS source observations. No execution or credential decoding.
|
|
2
|
+
|
|
3
|
+
This deliberately limited lexer/structural reader is not a compiler, taint engine,
|
|
4
|
+
or proof of races, authentication failures, validation quality or data loss.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from collections import namedtuple
|
|
8
|
+
import hashlib
|
|
9
|
+
from importlib import resources
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
Token = namedtuple('Token', 'kind value start end line column')
|
|
15
|
+
EXTENSIONS = {'.js', '.ts', '.mjs', '.mts', '.cjs', '.cts', '.gs'}
|
|
16
|
+
EXCLUDED = {'node_modules','vendor','target','build','dist','coverage','generated',
|
|
17
|
+
'tests','test','__tests__','fixtures','__fixtures__','examples','docs',
|
|
18
|
+
'references','reference','archive','archives','cache'}
|
|
19
|
+
MAX_FILE_BYTES = 256000
|
|
20
|
+
MAX_TOKENS = 20000
|
|
21
|
+
MAX_SCOPES = 128
|
|
22
|
+
MAX_FILES = 1000
|
|
23
|
+
MAX_TOTAL_BYTES = 20000000
|
|
24
|
+
_ATOM = re.compile(r'[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|===|!==|=>|\+\+|--|\+=|-=|&&|\|\||\?\?|==|!=|<=|>=|\?\.')
|
|
25
|
+
CHECK_IDS = ('AQR-BROWSER-001','AQR-CRED-001','AQR-STATE-001','AQR-JOB-001','AQR-VALID-001','AQR-LIFE-001')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def catalog():
|
|
29
|
+
return json.loads(resources.files('aqaf').joinpath('data/review_checks.json').read_text(encoding='utf8'))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def excluded(path):
|
|
33
|
+
p=Path(path)
|
|
34
|
+
return (p.name.startswith('.') or any(x in EXCLUDED or x.startswith('.') for x in p.parts[:-1])
|
|
35
|
+
or bool(re.search(r'(?:\.test\.|\.spec\.|\.min\.|^test_|\.d\.ts$)',p.name)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lex(text):
|
|
39
|
+
tokens=[];i=0;line=1;last_newline=-1
|
|
40
|
+
while i<len(text):
|
|
41
|
+
start=i;kind=None;value=None;c=text[i]
|
|
42
|
+
if c.isspace():
|
|
43
|
+
i+=1
|
|
44
|
+
elif text.startswith('//',i):
|
|
45
|
+
end=text.find('\n',i);i=len(text) if end<0 else end
|
|
46
|
+
elif text.startswith('/*',i):
|
|
47
|
+
end=text.find('*/',i+2)
|
|
48
|
+
if end<0:raise ValueError('unsupported_syntax')
|
|
49
|
+
i=end+2
|
|
50
|
+
elif c in '\"\'`':
|
|
51
|
+
quote=c;i+=1
|
|
52
|
+
while i<len(text):
|
|
53
|
+
if text[i]=='\\':i+=2;continue
|
|
54
|
+
if text[i]==quote:break
|
|
55
|
+
i+=1
|
|
56
|
+
if i>=len(text):raise ValueError('unsupported_syntax')
|
|
57
|
+
i+=1;kind='opaque' if quote=='`' else 'string';value='' if quote=='`' else text[start+1:i-1]
|
|
58
|
+
elif c=='/' and (not tokens or tokens[-1].value in {'=', '(', '[', '{', ',', ':', 'return', '=>', '!', '?', '&&', '||', ';'}):
|
|
59
|
+
i+=1;in_class=False
|
|
60
|
+
while i<len(text):
|
|
61
|
+
if text[i]=='\\':i+=2;continue
|
|
62
|
+
if text[i]=='[':in_class=True
|
|
63
|
+
elif text[i]==']':in_class=False
|
|
64
|
+
elif text[i]=='/' and not in_class:break
|
|
65
|
+
elif text[i]=='\n':raise ValueError('unsupported_syntax')
|
|
66
|
+
i+=1
|
|
67
|
+
if i>=len(text):raise ValueError('unsupported_syntax')
|
|
68
|
+
i+=1
|
|
69
|
+
while i<len(text) and text[i].isalpha():i+=1
|
|
70
|
+
kind='opaque';value=''
|
|
71
|
+
else:
|
|
72
|
+
match=_ATOM.match(text,i)
|
|
73
|
+
if match:
|
|
74
|
+
value=match.group();i+=len(value);kind='name' if re.match('[A-Za-z_$]',value) else 'symbol'
|
|
75
|
+
else:kind='symbol';value=c;i+=1
|
|
76
|
+
if kind:
|
|
77
|
+
tokens.append(Token(kind,value,start,i,line,start-last_newline))
|
|
78
|
+
if len(tokens)>MAX_TOKENS:raise ValueError('unsupported_limit')
|
|
79
|
+
segment=text[start:i]
|
|
80
|
+
count=segment.count('\n')
|
|
81
|
+
if count:line+=count;last_newline=text.rfind('\n',start,i)
|
|
82
|
+
return tokens
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Structure:
|
|
86
|
+
def __init__(self,tokens):
|
|
87
|
+
self.t=tokens;self.pairs={};stack=[]
|
|
88
|
+
for i,t in enumerate(tokens):
|
|
89
|
+
if t.kind!='symbol':continue
|
|
90
|
+
if t.value in ('(', '[', '{'):stack.append(i)
|
|
91
|
+
elif t.value in (')',']','}'):
|
|
92
|
+
if not stack or {'(':')','[':']','{':'}'}[tokens[stack[-1]].value]!=t.value:raise ValueError('unsupported_syntax')
|
|
93
|
+
other=stack.pop();self.pairs[other]=i;self.pairs[i]=other
|
|
94
|
+
if stack:raise ValueError('unsupported_syntax')
|
|
95
|
+
self.scopes=[{'start':-1,'end':len(tokens),'params':[],'parent':None}]
|
|
96
|
+
for i,t in enumerate(tokens):
|
|
97
|
+
body=None;params=[]
|
|
98
|
+
if t.kind=='name' and t.value=='function':
|
|
99
|
+
j=i+1
|
|
100
|
+
if j<len(tokens) and tokens[j].value=='*':j+=1
|
|
101
|
+
if j<len(tokens) and tokens[j].kind=='name':j+=1
|
|
102
|
+
if j in self.pairs and tokens[j].value=='(':
|
|
103
|
+
close=self.pairs[j];params=list(range(j+1,close));k=close+1
|
|
104
|
+
while k<len(tokens) and k<close+32 and tokens[k].value not in {'{',';','=>'}:k+=1
|
|
105
|
+
if k in self.pairs and tokens[k].value=='{':body=k
|
|
106
|
+
elif t.kind=='symbol' and t.value=='=>' and i+1 in self.pairs and tokens[i+1].value=='{':
|
|
107
|
+
body=i+1
|
|
108
|
+
if i-1 in self.pairs and tokens[i-1].value==')':params=list(range(self.pairs[i-1]+1,i-1))
|
|
109
|
+
elif i:params=[i-1]
|
|
110
|
+
if body is not None:self.scopes.append({'start':body,'end':self.pairs[body],'params':params,'parent':None})
|
|
111
|
+
self.scopes[1:]=sorted(self.scopes[1:],key=lambda s:s['start'])
|
|
112
|
+
if len(self.scopes)>MAX_SCOPES:raise ValueError('unsupported_limit')
|
|
113
|
+
starts={s['start']:i for i,s in enumerate(self.scopes) if i};active=[0];self.owner=[]
|
|
114
|
+
for i in range(len(tokens)):
|
|
115
|
+
while len(active)>1 and i>self.scopes[active[-1]]['end']:active.pop()
|
|
116
|
+
if i in starts:
|
|
117
|
+
n=starts[i];self.scopes[n]['parent']=active[-1];active.append(n)
|
|
118
|
+
self.owner.append(active[-1])
|
|
119
|
+
self.by_owner={}
|
|
120
|
+
for i,owner in enumerate(self.owner):self.by_owner.setdefault(owner,[]).append(i)
|
|
121
|
+
self.defs={}
|
|
122
|
+
for owner,s in enumerate(self.scopes):
|
|
123
|
+
for chunk in self.split_indices(s['params']):
|
|
124
|
+
if chunk and tokens[chunk[0]].kind=='name':self.defs[(owner,tokens[chunk[0]].value)]=(chunk[0],None)
|
|
125
|
+
for i,t in enumerate(tokens[:-2]):
|
|
126
|
+
if t.kind=='name' and t.value in {'const','let','var'} and tokens[i+1].kind=='name' and tokens[i+2].value=='=':
|
|
127
|
+
end=self.expression_end(i+3)
|
|
128
|
+
self.defs[(self.owner[i],tokens[i+1].value)]=(i,list(range(i+3,end)))
|
|
129
|
+
# Straight-line identifier assignments supersede earlier local definitions.
|
|
130
|
+
for i,token in enumerate(tokens[:-2]):
|
|
131
|
+
if token.kind!='name' or tokens[i+1].value!='=' or (i and tokens[i-1].value in {'.','?.','const','let','var'}):continue
|
|
132
|
+
origin,_=self.definition(token.value,self.owner[i],i)
|
|
133
|
+
if origin is not None:self.defs[(origin,token.value)]=(i,list(range(i+2,self.expression_end(i+2))))
|
|
134
|
+
self.calls=[]
|
|
135
|
+
for i,t in enumerate(tokens[:-1]):
|
|
136
|
+
if t.kind!='name' or i+1 not in self.pairs or tokens[i+1].value!='(':continue
|
|
137
|
+
start=i;chain=[t.value]
|
|
138
|
+
while start>=2 and tokens[start-1].value in {'.','?.'} and tokens[start-2].kind=='name':
|
|
139
|
+
start-=2;chain.insert(0,tokens[start].value)
|
|
140
|
+
self.calls.append({'start':start,'end':self.pairs[i+1], 'name':chain,
|
|
141
|
+
'args':self.split_range(i+2,self.pairs[i+1]),'owner':self.owner[start],
|
|
142
|
+
'new':start>0 and tokens[start-1].value=='new'})
|
|
143
|
+
|
|
144
|
+
def split_indices(self,indices):
|
|
145
|
+
if not indices:return []
|
|
146
|
+
return self.split_range(indices[0],indices[-1]+1)
|
|
147
|
+
|
|
148
|
+
def split_range(self,start,end):
|
|
149
|
+
result=[];chunk=[];i=start
|
|
150
|
+
while i<end:
|
|
151
|
+
if self.t[i].kind=='symbol' and self.t[i].value==',':result.append(chunk);chunk=[];i+=1;continue
|
|
152
|
+
if i in self.pairs and self.pairs[i]>i:
|
|
153
|
+
close=self.pairs[i];chunk.extend(range(i,close+1));i=close+1
|
|
154
|
+
else:chunk.append(i);i+=1
|
|
155
|
+
if chunk:result.append(chunk)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
def expression_end(self,start):
|
|
159
|
+
i=start
|
|
160
|
+
while i<len(self.t):
|
|
161
|
+
if self.t[i].kind=='symbol' and self.t[i].value in {';', '}'}:return i
|
|
162
|
+
if i>start and self.t[i].line>self.t[i-1].line and self.t[i].value in {'const','let','var','function','return'}:return i
|
|
163
|
+
if i in self.pairs and self.pairs[i]>i:i=self.pairs[i]+1
|
|
164
|
+
else:i+=1
|
|
165
|
+
return i
|
|
166
|
+
|
|
167
|
+
def values(self,indices):return [self.t[i].value for i in indices]
|
|
168
|
+
def direct(self,owner):return self.by_owner.get(owner,[])
|
|
169
|
+
def definition(self,name,owner,before):
|
|
170
|
+
while owner is not None:
|
|
171
|
+
result=self.defs.get((owner,name))
|
|
172
|
+
if result and result[0]<before:return owner,result[1]
|
|
173
|
+
owner=self.scopes[owner]['parent']
|
|
174
|
+
return None,None
|
|
175
|
+
|
|
176
|
+
def properties(self,indices):
|
|
177
|
+
if not indices or indices[0] not in self.pairs or self.t[indices[0]].value!='{':return None
|
|
178
|
+
if self.pairs[indices[0]]!=indices[-1]:return None
|
|
179
|
+
out={}
|
|
180
|
+
for entry in self.split_range(indices[0]+1,indices[-1]):
|
|
181
|
+
if self.t[entry[0]].kind not in {'name','string'}:return None
|
|
182
|
+
if len(entry)>1 and self.t[entry[1]].value==':':out[self.t[entry[0]].value]=entry[2:]
|
|
183
|
+
elif len(entry)==1:out[self.t[entry[0]].value]=entry
|
|
184
|
+
else:return None
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _review_source(text,path,apply_exclusions=True):
|
|
189
|
+
"""Analyze a bounded source string; output contains no source snippets/literals."""
|
|
190
|
+
result={'status':'assessed','signals':[]}
|
|
191
|
+
if Path(path).is_absolute() or '..' in Path(path).parts or '\\' in str(path) or '\x00' in str(path):result['status']='unsupported_path';return result
|
|
192
|
+
if Path(path).suffix.lower() not in EXTENSIONS:result['status']='unsupported_language';return result
|
|
193
|
+
if apply_exclusions and excluded(path):result['status']='excluded_scope';return result
|
|
194
|
+
if len(text.encode('utf8'))>MAX_FILE_BYTES:result['status']='unsupported_limit';return result
|
|
195
|
+
try:s=Structure(lex(text))
|
|
196
|
+
except (ValueError,RecursionError) as error:
|
|
197
|
+
result['status']='unsupported_limit' if str(error)=='unsupported_limit' else 'unsupported_syntax';return result
|
|
198
|
+
t=s.t;checks={c['id']:c for c in catalog()['checks']};source_digest=hashlib.sha256(text.encode('utf8')).hexdigest()
|
|
199
|
+
seen=set(); budget=[5000]
|
|
200
|
+
def spend():
|
|
201
|
+
budget[0]-=1
|
|
202
|
+
if budget[0]<0:raise ValueError('unsupported_limit')
|
|
203
|
+
def emit(check,variant,index,fact,kind='review_candidate',controls=()):
|
|
204
|
+
key=(check,variant,index)
|
|
205
|
+
if key in seen:return
|
|
206
|
+
if len(result['signals'])>=100:
|
|
207
|
+
result['status']='assessed_limited';return
|
|
208
|
+
seen.add(key);token=t[index];entry=checks[check]
|
|
209
|
+
result['signals'].append({'check_id':check,'variant':variant,'kind':kind,'confirmed':False,'runtime_verified':False,
|
|
210
|
+
'origin':'static_review_heuristic','review_state':'unreviewed','confidence':'medium' if kind=='observed_control' else 'low',
|
|
211
|
+
'locations':[{'path':str(path),'line':token.line,'column':token.column}],
|
|
212
|
+
'observed_fact':fact,'observed_controls':list(controls),
|
|
213
|
+
'unknowns':entry['unknowns'],'next_test':entry['next_test'],'source_ids':entry['source_ids'],
|
|
214
|
+
'source_digest':source_digest,'fingerprint':hashlib.sha256((check+'\0'+variant+'\0'+str(path)+'\0'+str(token.line)+'\0'+source_digest).encode()).hexdigest()})
|
|
215
|
+
# Worker/browser error transport is counterevidence, never a missing-logging claim.
|
|
216
|
+
for call in s.calls:
|
|
217
|
+
if call['name'][-1]=='postMessage' and call['args']:
|
|
218
|
+
props=s.properties(call['args'][0])
|
|
219
|
+
if props and any(t[i].kind=='string' and t[i].value=='error' for i in props.get('type',[])):
|
|
220
|
+
emit('AQR-BROWSER-001','error_message_channel',call['start'],
|
|
221
|
+
'An explicit error-type message is sent through a browser/worker message channel.', 'observed_control',
|
|
222
|
+
['Explicit error transport is visible; absence of console logging is not absence of error reporting.'])
|
|
223
|
+
# Encoded literal ancestry: allowlisted syntax only, no decoding or evaluation.
|
|
224
|
+
def embedded(indices,owner,before,depth=0,visited=None):
|
|
225
|
+
spend()
|
|
226
|
+
if depth>4 or len(indices)>4000:return False
|
|
227
|
+
visited=set() if visited is None else visited
|
|
228
|
+
for i in indices:
|
|
229
|
+
value=t[i].value
|
|
230
|
+
if t[i].kind=='string' and value and value not in {'base64','base64url','utf8','utf-8','hex'} and not re.search(r'(?i)todo|example|placeholder|fake|test.only|changeme',value):return True
|
|
231
|
+
if t[i].kind=='name' and value not in visited:
|
|
232
|
+
origin,expr=s.definition(value,owner,before)
|
|
233
|
+
if expr is not None and (origin,value) not in visited:
|
|
234
|
+
visited.add((origin,value))
|
|
235
|
+
if embedded(expr,origin,before,depth+1,visited):return True
|
|
236
|
+
return False
|
|
237
|
+
def decoded(indices,owner,before,depth=0):
|
|
238
|
+
spend()
|
|
239
|
+
if depth>4 or len(indices)>4000:return False
|
|
240
|
+
inside=[c for c in s.calls if c['start'] in indices and (c['name']==['Buffer','from'] or c['name'][-1]=='atob')]
|
|
241
|
+
for call in inside:
|
|
242
|
+
encoded=call['name'][-1]=='atob' or any(t[i].kind=='string' and t[i].value in {'base64','base64url'} for arg in call['args'][1:] for i in arg)
|
|
243
|
+
data=[i for i in indices if not (t[i].kind=='string' and t[i].value in {'base64','base64url','utf8','hex'})]
|
|
244
|
+
if encoded and embedded(data,owner,before):return True
|
|
245
|
+
for i in indices:
|
|
246
|
+
if t[i].kind=='name':
|
|
247
|
+
origin,expr=s.definition(t[i].value,owner,before)
|
|
248
|
+
if expr is not None and decoded(expr,origin,before,depth+1):return True
|
|
249
|
+
return False
|
|
250
|
+
for i,token in enumerate(t):
|
|
251
|
+
if token.value not in {'||','??'}:continue
|
|
252
|
+
end=s.expression_end(i+1);right=list(range(i+1,end));owner=s.owner[i]
|
|
253
|
+
# Only a fallback assignment to a header/auth-like binding; no general dataflow claim.
|
|
254
|
+
start=max(0,i-10);eq=next((j for j in range(i-1,start-1,-1) if t[j].value=='='),None)
|
|
255
|
+
if eq is None:continue
|
|
256
|
+
names=[t[j].value.lower() for j in range(start,eq) if t[j].kind in {'name','string'}]
|
|
257
|
+
if not any(re.search(r'header|authorization|credential|api_?key|token|password',name) for name in names):continue
|
|
258
|
+
if decoded(right,owner,i):emit('AQR-CRED-001','encoded_fallback_binding',i,
|
|
259
|
+
'Supported lexical assignments link an encoded-literal construction to a fallback in a header/authentication-like binding. Branches, authentication use and validity are unverified; values are omitted.')
|
|
260
|
+
# Durable-state sequences: publication and recovery are reviewed independently.
|
|
261
|
+
for call in s.calls:
|
|
262
|
+
if call['name'][-1] in {'writeFile','writeFileSync'} and len(call['args'])>=2:
|
|
263
|
+
serial=any(c['name']==['JSON','stringify'] and c['start'] in call['args'][1] for c in s.calls)
|
|
264
|
+
if not serial:continue
|
|
265
|
+
path_arg=s.values(call['args'][0]);rename=next((c for c in s.calls if c['owner']==call['owner'] and c['start']>call['end'] and c['name'][-1] in {'rename','renameSync'} and len(c['args'])>=2 and s.values(c['args'][0])==path_arg and s.values(c['args'][1])!=path_arg),None)
|
|
266
|
+
if rename:emit('AQR-STATE-001','publication_sequence',call['start'],'A JSON write is followed by rename from the same temporary binding. Filesystem durability and all paths remain unverified.','observed_control')
|
|
267
|
+
else:emit('AQR-STATE-001','direct_json_write',call['start'],'A JSON serialization is written directly with no rename publication observed in the same function; wrapper behavior is unknown.')
|
|
268
|
+
for owner,scope in enumerate(s.scopes):
|
|
269
|
+
body=s.direct(owner);calls=[c for c in s.calls if c['owner']==owner]
|
|
270
|
+
parses=any(c['name']==['JSON','parse'] for c in calls)
|
|
271
|
+
for i in body:
|
|
272
|
+
if t[i].value!='catch' or not parses:continue
|
|
273
|
+
j=i+1
|
|
274
|
+
if j<len(t) and t[j].value=='(':j=s.pairs[j]+1
|
|
275
|
+
if j>=len(t) or t[j].value!='{':continue
|
|
276
|
+
part=list(range(j+1,s.pairs[j]));values=s.values(part)
|
|
277
|
+
empty=any(t[k].value=='return' and k+1<len(t) and t[k+1].value in {'{','['} and s.pairs.get(k+1)==k+2 for k in part)
|
|
278
|
+
if empty and 'throw' not in values and not any(c['start'] in part and c['name'][-1] in {'rename','renameSync','copyFile','copyFileSync'} for c in calls):
|
|
279
|
+
emit('AQR-STATE-001','empty_recovery_default',i,'A JSON-parse function catches failure and returns an empty container; preservation of corrupt state is not visible in this catch block.')
|
|
280
|
+
# Admission counter used before an await but incremented afterward.
|
|
281
|
+
for i in body:
|
|
282
|
+
if t[i].value!='while' or i+1>=len(t) or t[i+1].value!='(':continue
|
|
283
|
+
close=s.pairs[i+1];j=close+1
|
|
284
|
+
if j>=len(t) or t[j].value!='{':continue
|
|
285
|
+
loop=list(range(j+1,s.pairs[j]));condition={t[k].value for k in range(i+2,close) if t[k].kind=='name'}
|
|
286
|
+
claims=[c for c in calls if c['start'] in loop and c['name'][-1] in {'claim','claimJob','takeJob','dequeue','nextJob'} and c['start'] and t[c['start']-1].value=='await']
|
|
287
|
+
for claim in claims:
|
|
288
|
+
increments=[k for k in loop if t[k].value in condition and k+1<len(t) and t[k+1].value in {'++','+='}]
|
|
289
|
+
if increments and min(increments)>claim['end']:emit('AQR-JOB-001','admission_after_await',claim['start'],'A loop-condition counter is incremented after an awaited claim-like call. Overlapping admission and synchronization require characterization.')
|
|
290
|
+
# Unknown/any input asserted into an application type without a visible guard.
|
|
291
|
+
unknown={t[k].value for k in scope['params'] if t[k].kind=='name' and k+2<len(t) and t[k+1].value==':' and t[k+2].value in {'unknown','any'}}
|
|
292
|
+
for i in body:
|
|
293
|
+
if t[i].value!='return' or i+2>=len(t) or t[i+1].value not in unknown or t[i+2].value!='as':continue
|
|
294
|
+
name=t[i+1].value
|
|
295
|
+
guards=[]
|
|
296
|
+
for call in calls:
|
|
297
|
+
if call['start']>=i or not any(name in s.values(arg) for arg in call['args']):continue
|
|
298
|
+
for k in body:
|
|
299
|
+
if t[k].value!='if' or k+1>=len(t) or t[k+1].value!='(':continue
|
|
300
|
+
close=s.pairs[k+1];j=close+1
|
|
301
|
+
if k<call['start']<close and j<len(t) and t[j].value=='{' and s.pairs[j]<i and 'throw' in s.values(list(range(j+1,s.pairs[j]))):guards.append(call)
|
|
302
|
+
if guards:emit('AQR-VALID-001','validation_control',i,'A check of the same unknown input precedes a type assertion; validator semantics and control-flow coverage remain unverified.','observed_control')
|
|
303
|
+
else:emit('AQR-VALID-001','unknown_type_assertion',i,'A function returns an unknown/any input through a TypeScript assertion. Assertions alone do not perform runtime validation.')
|
|
304
|
+
for call in s.calls:
|
|
305
|
+
if call['name'][-1] not in {'update','updateMany'} or not call['args']:continue
|
|
306
|
+
props=s.properties(call['args'][0])
|
|
307
|
+
if not props:continue
|
|
308
|
+
where=s.properties(props.get('where',[]));data=s.properties(props.get('data',[]))
|
|
309
|
+
if where is None or not data or not any(t[i].kind=='string' and t[i].value in {'completed','failed','cancelled','canceled'} for i in data.get('status',[])):continue
|
|
310
|
+
if set(where)=={'id'}:emit('AQR-JOB-001','terminal_id_only',call['start'],'A terminal-state update has only an ID in its inline where clause; ownership, state and transaction fencing may exist elsewhere.')
|
|
311
|
+
elif 'id' in where:emit('AQR-JOB-001','terminal_guard_fields',call['start'],'A terminal update includes additional inline ownership/state filter fields; their semantics and database enforcement remain unverified.','observed_control')
|
|
312
|
+
for call in s.calls:
|
|
313
|
+
if call['name']!=['Worker'] or not call['new'] or call['owner']==0:continue
|
|
314
|
+
owner=call['owner'];start=call['start'];binding=t[start-2].value if start>=2 and t[start-1].value=='new' else ''
|
|
315
|
+
# Declaration is NAME = new Worker(...).
|
|
316
|
+
if start>=3 and t[start-1].value=='new' and t[start-2].value=='=':binding=t[start-3].value
|
|
317
|
+
terminate=False;error=False
|
|
318
|
+
for c in s.calls:
|
|
319
|
+
if c['name'][0]==binding:
|
|
320
|
+
origin,_=s.definition(binding,c['owner'],c['start'])
|
|
321
|
+
if origin==owner and c['name'][-1]=='terminate':terminate=True
|
|
322
|
+
if origin==owner and c['name'][-1]=='addEventListener' and c['args'] and any(t[k].value=='error' for k in c['args'][0]):error=True
|
|
323
|
+
for k in range(start+1,len(t)-3):
|
|
324
|
+
if [t[x].value for x in range(k,k+4)]==[binding,'.','onerror','=']:
|
|
325
|
+
origin,_=s.definition(binding,s.owner[k],k)
|
|
326
|
+
if origin==owner:error=True
|
|
327
|
+
if terminate and error:emit('AQR-LIFE-001','worker_controls',start,'A function-created worker has visible error and termination controls; lifecycle coverage and all exit paths remain unverified.','observed_control')
|
|
328
|
+
else:emit('AQR-LIFE-001','function_worker_ownership',start,'A worker is constructed inside a function; local error/termination coverage is incomplete or delegated.',controls=[label for yes,label in [(terminate,'Termination call observed.'),(error,'Error handler observed.')] if yes])
|
|
329
|
+
for call in s.calls:
|
|
330
|
+
if call['name'][-1]!='save' or not call['start'] or t[call['start']-1].value!='await':continue
|
|
331
|
+
owner=call['owner'];body=s.direct(owner)
|
|
332
|
+
for i in body:
|
|
333
|
+
if i<=call['end'] or t[i].value!='status' or i+2>=len(t) or t[i+1].value!='=' or t[i+2].kind!='string' or t[i+2].value.lower()!='saved':continue
|
|
334
|
+
guarded=False
|
|
335
|
+
for k in body:
|
|
336
|
+
if t[k].value!='if' or k+1>=len(t) or t[k+1].value!='(':continue
|
|
337
|
+
close=s.pairs[k+1];values=s.values(list(range(k+2,close)));j=close+1
|
|
338
|
+
if j<len(t) and t[j].value=='{' and j<i<s.pairs[j] and any(v in {'===','==','!==','!='} for v in values) and any(re.search(r'(?i)revision|version|generation',v) for v in values):guarded=True
|
|
339
|
+
if guarded:emit('AQR-LIFE-001','save_revision_control',i,'A revision/version comparison guards the saved-status assignment; revision ownership and acknowledgement semantics remain unverified.','observed_control')
|
|
340
|
+
else:emit('AQR-LIFE-001','saved_after_await',i,'A saved status is assigned after an awaited save without a visible revision guard on that assignment; concurrent edits require characterization.')
|
|
341
|
+
result['signals'].sort(key=lambda x:(x['locations'][0]['line'],x['locations'][0]['column'],x['check_id'],x['variant']))
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def review_source(text,path):
|
|
346
|
+
from .adapters import analyze_source
|
|
347
|
+
# Direct callers retain the conservative path-role exclusions.
|
|
348
|
+
if Path(path).is_absolute() or '..' in Path(path).parts or '\\' in str(path) or '\x00' in str(path):return {'status':'unsupported_path','signals':[]}
|
|
349
|
+
if excluded(path):return {'status':'excluded_scope','signals':[]}
|
|
350
|
+
return analyze_source(text,path)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def review_files(files,root,scope=None,max_files=MAX_FILES,max_total_bytes=MAX_TOTAL_BYTES,max_file_bytes=MAX_FILE_BYTES,offset=0):
|
|
354
|
+
"""Round-robin components/languages; preserve evidence and explicit omissions."""
|
|
355
|
+
from collections import Counter,defaultdict,deque
|
|
356
|
+
import unicodedata
|
|
357
|
+
from .adapters import analyze_source,language,support_matrix,FAMILIES
|
|
358
|
+
metadata=catalog();signals=[];counts=Counter();buckets=defaultdict(list);groups={};outcomes=[];digests=defaultdict(list)
|
|
359
|
+
roles=(scope.config.get('review_roles',['production','source','unknown','tooling']) if scope else ['production','source','unknown','tooling'])
|
|
360
|
+
def group(record,lang):
|
|
361
|
+
key=(record.get('owner','root'),lang)
|
|
362
|
+
if key not in groups:groups[key]={'owner':key[0],'language':lang,'files_collected':0,'eligible':0,'attempted':0,'assessed':0,'outcomes':Counter()}
|
|
363
|
+
return groups[key]
|
|
364
|
+
feasibility=Counter();pool=0
|
|
365
|
+
for path in files:
|
|
366
|
+
relative=path.relative_to(root).as_posix();record=scope.record(path) if scope else {'relative':relative,'role':'unknown','priority':relative}
|
|
367
|
+
lang=language(relative) or (record.get('language') if record.get('language_basis')=='bounded_shebang' else None);g=group(record,lang or record.get('language','other'));g['files_collected']+=1
|
|
368
|
+
if not lang:
|
|
369
|
+
counts['unsupported_language']+=1;g['outcomes']['unsupported_language']+=1
|
|
370
|
+
if record.get('language') not in {'other',None}:feasibility[record['language']]+=1
|
|
371
|
+
continue
|
|
372
|
+
if record.get('role') not in roles or (scope is None and excluded(relative)):
|
|
373
|
+
counts['excluded_scope']+=1;g['outcomes']['excluded_scope']+=1;continue
|
|
374
|
+
pool+=1;g['eligible']+=1;buckets[(g['owner'],lang)].append((path,relative,record,g))
|
|
375
|
+
# Deterministic rotation prevents large PHP/JS trees from starving smaller components.
|
|
376
|
+
queue=deque(deque(sorted(rows,key=lambda r:r[2].get('priority',r[1]))) for _,rows in sorted(buckets.items()))
|
|
377
|
+
ordered=[]
|
|
378
|
+
while queue:
|
|
379
|
+
rows=queue.popleft();ordered.append(rows.popleft())
|
|
380
|
+
if rows:queue.append(rows)
|
|
381
|
+
attempted=0;total=0;assessed=0
|
|
382
|
+
for ordinal,(path,relative,record,g) in enumerate(ordered):
|
|
383
|
+
status=None;result=None;raw=None
|
|
384
|
+
if ordinal<offset:status='before_offset_unassessed'
|
|
385
|
+
elif attempted>=max_files:status='file_budget'
|
|
386
|
+
elif total>=max_total_bytes:status='byte_budget'
|
|
387
|
+
else:
|
|
388
|
+
attempted+=1;g['attempted']+=1
|
|
389
|
+
try:
|
|
390
|
+
if scope:size=record['size']
|
|
391
|
+
else:
|
|
392
|
+
if path.is_symlink():raise ValueError('excluded_symlink')
|
|
393
|
+
size=path.stat().st_size
|
|
394
|
+
if size>max_file_bytes:status='unsupported_limit'
|
|
395
|
+
elif total+size>max_total_bytes:status='byte_budget'
|
|
396
|
+
else:
|
|
397
|
+
raw=scope.raw(path,max_file_bytes+1) if scope else path.read_bytes()[:max_file_bytes+1]
|
|
398
|
+
total+=len(raw)
|
|
399
|
+
if len(raw)!=size:status='changed_or_unreadable'
|
|
400
|
+
elif len(raw)>max_file_bytes:status='unsupported_limit'
|
|
401
|
+
else:
|
|
402
|
+
text=raw.decode('utf8');context={'role':record.get('role','unknown'),'owner':record.get('owner','root'),'sql_dialect':scope.config.get('sql_dialect','unknown') if scope else 'unknown'}
|
|
403
|
+
result=analyze_source(text,relative,context);status=result['status']
|
|
404
|
+
digest=hashlib.sha256(raw).hexdigest();digests[digest].append(relative)
|
|
405
|
+
if status.startswith('assessed'):assessed+=1;g['assessed']+=1
|
|
406
|
+
available=max(0,5000-len(signals));new=result['signals'][:available]
|
|
407
|
+
for signal in new:
|
|
408
|
+
# Evidence identity is content-sensitive; concern identity is separate.
|
|
409
|
+
loc=signal['locations'][0];logical='\0'.join([signal['check_id'],signal['variant'],record.get('owner','root'),relative,str(loc['line']),str(loc['column'])])
|
|
410
|
+
signal['logical_concern_id']=hashlib.sha256(logical.encode()).hexdigest()
|
|
411
|
+
signal['display_path']=unicodedata.normalize('NFC',relative);signal['source_digest']=digest
|
|
412
|
+
signals.extend(new)
|
|
413
|
+
if len(result['signals'])>available:counts['signal_budget']+=1
|
|
414
|
+
except UnicodeError:status='unsupported_encoding'
|
|
415
|
+
except OSError:status='unreadable'
|
|
416
|
+
except ValueError as error:status=str(error) if str(error)=='excluded_symlink' else 'unsupported_syntax'
|
|
417
|
+
counts[status]+=1;g['outcomes'][status]+=1
|
|
418
|
+
if len(outcomes)<10000:outcomes.append({'path':relative,'owner':g['owner'],'language':g['language'],'status':status,'ordinal':ordinal,**({'diagnostics':result['diagnostics']} if result and result.get('diagnostics') else {})})
|
|
419
|
+
sources=json.loads(resources.files('aqaf').joinpath('data/source_registry.json').read_text(encoding='utf8'))
|
|
420
|
+
ids={sid for check in metadata['checks'] for sid in check['source_ids']}|{sid for signal in signals for sid in signal['source_ids']}
|
|
421
|
+
for g in groups.values():g['outcomes']=dict(sorted(g['outcomes'].items()))
|
|
422
|
+
secondary=[]
|
|
423
|
+
for lang,n in sorted(feasibility.items()):
|
|
424
|
+
examples=[{'path':record['relative'],'owner':record['owner'],'role':record['role']} for record in (scope.records if scope else []) if record.get('language')==lang][:8]
|
|
425
|
+
secondary.append({'language':lang,'collected_candidate_files':n,'examples':examples,'ownership':'filtered_owned_candidates_not_certified_authorship',
|
|
426
|
+
'decision':'deferred','candidate_slice':'Passive publication/error/resource ownership facts, with language-specific sources and positive/control/adversarial fixtures before enabling.',
|
|
427
|
+
'validation_effort':'Separate bounded adapter proposal plus independent applicability review; no automatic parser/compiler installation.',
|
|
428
|
+
'reason':'No calibrated bounded adapter; authored intent and useful characterization require independent review. Presence is not support.'})
|
|
429
|
+
selected_checks=sorted({c for g in groups.values() if g['assessed'] for c in FAMILIES.get(g['language'],[])})
|
|
430
|
+
return {'schema_version':2,'status':'experimental','score_impact':0,'affects_baseline_gates':False,'checks':metadata['checks'],'signals':signals,
|
|
431
|
+
'support_matrix':support_matrix(),'secondary_language_feasibility':secondary,
|
|
432
|
+
'coverage_groups':[{'owner':g['owner'],'language':g['language'],'check_id':check,'files_eligible':g['eligible'],'files_assessed_partial_syntax':g['assessed'],'status':'partial_source_assessed' if g['assessed'] else 'unassessed'} for g in groups.values() for check in FAMILIES.get(g['language'],[])],
|
|
433
|
+
'coverage':[{'check_id':check['id'],'status':'observed' if any(s['check_id']==check['id'] for s in signals) else 'no_observation_in_assessed_subset' if check['id'] in selected_checks else 'unassessed'} for check in metadata['checks']],
|
|
434
|
+
'scope':{'languages':sorted(__import__('aqaf.adapters',fromlist=['LANGUAGE_EXTENSIONS']).LANGUAGE_EXTENSIONS),'review_roles':roles,
|
|
435
|
+
'max_file_bytes':max_file_bytes,'max_files':max_files,'max_total_bytes':max_total_bytes,'files_eligible':pool,'files_attempted':attempted,'files_assessed':assessed,'bytes_read':total,'offset':offset,
|
|
436
|
+
'scheduler':'round_robin_owner_language_then_source_priority','coverage_complete':counts['assessed']==pool and not counts.get('signal_budget'),
|
|
437
|
+
'zero_attempt_reason':None if attempted else 'no_supported_owned_files_in_selected_roles' if not pool else 'offset_beyond_pool',
|
|
438
|
+
'outcomes':dict(sorted(counts.items())),'groups':[groups[k] for k in sorted(groups)],'file_outcomes':outcomes,'file_outcomes_truncated':len(ordered)>len(outcomes),
|
|
439
|
+
'resume':'Offset is an independent partial selection, not a validated merged run. No prior results or human labels are silently reused.'},
|
|
440
|
+
'exact_source_copies':[{'sha256':sha,'paths':paths,'assessment':'Every owned path retains ownership and evidence; copies are not silently deduplicated.'} for sha,paths in sorted(digests.items()) if len(paths)>1][:2000],
|
|
441
|
+
'limitations':['Partial experimental adapters recognize declared bounded syntax, not complete languages or frameworks.',
|
|
442
|
+
'Observations are unscored, unconfirmed and not runtime verification. No observation does not establish correctness.',
|
|
443
|
+
'No target execution, imports, build hooks, macros, credential decoding, provider calls or source excerpts.',
|
|
444
|
+
'Per-language support is partial; dynamic calls, aliases, caller controls, complex markup and unrecognized syntax remain unresolved.',
|
|
445
|
+
'Independent defect precision and human calibration remain pending; fixture conformance is not defect accuracy.'],
|
|
446
|
+
'source_registry':[s for s in sources if s['id'] in ids]}
|