@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/bin/aqaf.js +12 -0
  4. package/index.js +51 -0
  5. package/package.json +51 -4
  6. package/vendor/aqaf/__init__.py +2 -0
  7. package/vendor/aqaf/__main__.py +3 -0
  8. package/vendor/aqaf/adapters.py +583 -0
  9. package/vendor/aqaf/artifacts.py +28 -0
  10. package/vendor/aqaf/calibration.py +29 -0
  11. package/vendor/aqaf/catalogue.py +257 -0
  12. package/vendor/aqaf/cli.py +721 -0
  13. package/vendor/aqaf/cursor.py +56 -0
  14. package/vendor/aqaf/data/book_review_knowledge.json +1574 -0
  15. package/vendor/aqaf/data/catalog.json +562 -0
  16. package/vendor/aqaf/data/documentation_checks.json +52 -0
  17. package/vendor/aqaf/data/packs/automation.json +16 -0
  18. package/vendor/aqaf/data/packs/mobile.json +13 -0
  19. package/vendor/aqaf/data/packs/systems.json +17 -0
  20. package/vendor/aqaf/data/packs/web.json +16 -0
  21. package/vendor/aqaf/data/profiles/ai_pipeline.json +39 -0
  22. package/vendor/aqaf/data/profiles/android.json +34 -0
  23. package/vendor/aqaf/data/profiles/api_backend.json +44 -0
  24. package/vendor/aqaf/data/profiles/browser_extension.json +29 -0
  25. package/vendor/aqaf/data/profiles/cli_automation.json +34 -0
  26. package/vendor/aqaf/data/profiles/desktop_utility.json +39 -0
  27. package/vendor/aqaf/data/profiles/embedded_iot.json +34 -0
  28. package/vendor/aqaf/data/profiles/google_apps_script.json +34 -0
  29. package/vendor/aqaf/data/profiles/mcp_agent_server.json +34 -0
  30. package/vendor/aqaf/data/profiles/multirepo_system.json +29 -0
  31. package/vendor/aqaf/data/profiles/npm_library.json +39 -0
  32. package/vendor/aqaf/data/profiles/rust_app.json +34 -0
  33. package/vendor/aqaf/data/profiles/saas_or_web_app.json +44 -0
  34. package/vendor/aqaf/data/profiles/scraper_data_pipeline.json +39 -0
  35. package/vendor/aqaf/data/profiles/swift_app.json +34 -0
  36. package/vendor/aqaf/data/profiles/tauri_electron.json +34 -0
  37. package/vendor/aqaf/data/profiles/unknown_or_general_app.json +25 -0
  38. package/vendor/aqaf/data/profiles/windows_client_server.json +34 -0
  39. package/vendor/aqaf/data/profiles/wordpress_plugin.json +39 -0
  40. package/vendor/aqaf/data/review_checks.json +319 -0
  41. package/vendor/aqaf/data/source_registry.json +814 -0
  42. package/vendor/aqaf/discovery.py +580 -0
  43. package/vendor/aqaf/documentation.py +210 -0
  44. package/vendor/aqaf/evidence.py +132 -0
  45. package/vendor/aqaf/facets.py +63 -0
  46. package/vendor/aqaf/ignore.py +109 -0
  47. package/vendor/aqaf/knowledge.py +110 -0
  48. package/vendor/aqaf/markup.py +96 -0
  49. package/vendor/aqaf/matching.py +22 -0
  50. package/vendor/aqaf/portfolio.py +145 -0
  51. package/vendor/aqaf/project_commands.py +132 -0
  52. package/vendor/aqaf/projects.py +157 -0
  53. package/vendor/aqaf/registry.py +125 -0
  54. package/vendor/aqaf/review.py +446 -0
  55. package/vendor/aqaf/scope.py +401 -0
  56. package/vendor/aqaf/swift_review.py +99 -0
@@ -0,0 +1,29 @@
1
+ """Data-only calibration. Conformance and independently reviewed defects differ."""
2
+ from collections import Counter
3
+ import re
4
+
5
+
6
+ def evaluate(report,worksheet):
7
+ if not isinstance(worksheet,dict) or worksheet.get('schema_version')!=1:raise ValueError('Unsupported calibration worksheet')
8
+ labels=worksheet.get('labels',[])
9
+ if not isinstance(labels,list) or len(labels)>10000:raise ValueError('Calibration labels must be a bounded list')
10
+ signals={s['fingerprint']:s for s in report.get('review',{}).get('signals',[])};counts=Counter();stale=[];seen=set()
11
+ for row in labels:
12
+ if not isinstance(row,dict):raise ValueError('Invalid calibration row')
13
+ fingerprint=row.get('fingerprint');label=row.get('label','unreviewed')
14
+ if not isinstance(fingerprint,str) or not re.fullmatch('[0-9a-f]{64}',fingerprint) or fingerprint in seen:raise ValueError('Invalid or duplicate calibration fingerprint')
15
+ seen.add(fingerprint)
16
+ if label not in {'true_positive','false_positive','false_negative','ambiguous','unreviewed'}:raise ValueError('Invalid calibration label')
17
+ signal=signals.get(fingerprint)
18
+ if signal is None or row.get('source_digest')!=signal.get('source_digest'):stale.append(fingerprint);continue
19
+ if label=='unreviewed':counts[label]+=1;continue
20
+ reviewer=row.get('reviewer');author=worksheet.get('detector_author')
21
+ if not isinstance(reviewer,str) or not reviewer.strip() or not author or reviewer==author or row.get('independent_review') is not True:raise ValueError('Labeled evidence requires an explicit independent reviewer distinct from detector author')
22
+ if not row.get('rationale') or not row.get('characterization_evidence'):raise ValueError('Labeled evidence requires rationale and characterization evidence')
23
+ counts[label]+=1
24
+ counts['unreviewed']+=len(signals.keys()-seen)
25
+ tp=counts['true_positive'];fp=counts['false_positive']
26
+ return {'schema_version':1,'conformance':'separate_fixture_result','status':'independent_labels_present' if tp+fp+counts['ambiguous'] else 'independent_review_pending',
27
+ 'counts':dict(counts),'stale_labels':stale,'precision_on_reviewed_signals':tp/(tp+fp) if tp+fp else None,
28
+ 'recall':None,'recall_reason':'Requires an independently established exhaustive expected-concern corpus; observation worksheets cannot establish recall.',
29
+ 'promotion_authorized':False}
@@ -0,0 +1,257 @@
1
+ """Opt-in host-local portfolio storage. Scanned trees are always read-only."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sqlite3
8
+ from datetime import datetime,timezone
9
+ from difflib import SequenceMatcher
10
+ import re
11
+ from .scope import Scope
12
+
13
+ SCHEMA=1
14
+
15
+ def now():return datetime.now(timezone.utc).isoformat().replace('+00:00','Z')
16
+ def encode(value):return json.dumps(value,sort_keys=True,ensure_ascii=True)
17
+ def stable(value):return hashlib.sha256(value.encode()).hexdigest()
18
+ def default_database():
19
+ import sys
20
+ if sys.platform=='darwin':base=Path.home()/'Library/Application Support'
21
+ elif os.name=='nt':base=Path(os.environ.get('LOCALAPPDATA',Path.home()/'AppData/Local'))
22
+ else:base=Path(os.environ.get('XDG_DATA_HOME',Path.home()/'.local/share'))
23
+ return base/'AQAF/projects.sqlite3'
24
+
25
+ def engine_identity():
26
+ root=Path(__file__).parent;h=hashlib.sha256()
27
+ for p in sorted(root.rglob('*')):
28
+ if p.suffix in {'.py','.json'} and '__pycache__' not in p.parts:
29
+ h.update(p.relative_to(root).as_posix().encode()+b'\0'+p.read_bytes())
30
+ return h.hexdigest()
31
+
32
+ def snapshot(scope,max_bytes=256000000,engine=None):
33
+ entries=[];used=0;complete=True
34
+ for r in sorted(scope.records,key=lambda x:x['relative']):
35
+ if used+r['size']>max_bytes:complete=False;break
36
+ raw=scope.raw(r['path']);used+=len(raw)
37
+ if len(raw)!=r['size']:complete=False
38
+ entries.append({'path':r['relative'],'sha256':hashlib.sha256(raw).hexdigest(),
39
+ 'normalized_newlines_sha256':hashlib.sha256(raw.replace(b'\r\n',b'\n').replace(b'\r',b'\n')).hexdigest() if b'\x00' not in raw else None,
40
+ 'role':r['role'],'language':r['language'],'bytes':len(raw)})
41
+ controls=list(scope.ignore_sources)
42
+ if scope.config_control:controls.append(scope.config_control)
43
+ for name in ('.aqaf.json',):
44
+ p=scope.root/name
45
+ if p.is_symlink():
46
+ complete=False;controls.append({'path':name,'state':'symlink_control_unread'})
47
+ elif p.is_file():
48
+ raw=scope.cache.read(p,131073);whole=len(raw)==p.stat().st_size;complete=complete and whole
49
+ controls.append({'path':name,'sha256':hashlib.sha256(raw).hexdigest(),'bytes':len(raw),'complete_file':whole})
50
+ identity={'root_context':str(scope.root),'entries':entries,'controls':controls,'config':scope.config,'policy':scope.policy,
51
+ 'respect_gitignore':scope.respect_gitignore,'engine':engine or engine_identity()}
52
+ return {'schema_version':1,'digest':stable(encode(identity)),'entries':entries,'controls':controls,
53
+ 'engine':identity['engine'],'scope':scope.report(),'bytes':used,
54
+ 'complete':complete and scope.selection_complete(),
55
+ 'integrity_scope':'selected_inputs_and_controls_not_whole_folder'}
56
+
57
+ class Catalogue:
58
+ def __init__(self,path=None):
59
+ self.path=Path(path or default_database()).expanduser().absolute()
60
+ if self.path.is_symlink():raise ValueError('Catalogue symlink rejected')
61
+ self.path.parent.mkdir(parents=True,exist_ok=True)
62
+ if not self.path.exists():self.path.touch(mode=0o600)
63
+ self.connection=sqlite3.connect(str(self.path));self.connection.row_factory=sqlite3.Row
64
+ try:
65
+ version=self.connection.execute('pragma user_version').fetchone()[0]
66
+ if version>SCHEMA:raise ValueError('Catalogue schema is newer than this engine')
67
+ if version<SCHEMA:
68
+ if self.path.stat().st_size:
69
+ backup=self.path.with_name(self.path.name+'.before-v1.bak')
70
+ if backup.exists():raise ValueError('Migration backup already exists')
71
+ dest=sqlite3.connect(str(backup));self.connection.backup(dest);dest.close()
72
+ with self.connection:
73
+ self.connection.execute('BEGIN IMMEDIATE')
74
+ for statement in [
75
+ 'create table if not exists projects(id text primary key,name text not null)',
76
+ 'create table if not exists volumes(id text primary key,label text)',
77
+ '''create table if not exists instances(id text primary key,project_id text,path text unique,volume_id text,
78
+ source_id text,included_at text,included_at_basis text,first_seen text,last_seen text,last_verified text,
79
+ status text,metadata text)''',
80
+ 'create table if not exists snapshots(id integer primary key,instance_id text,created_at text,digest text,document text)',
81
+ 'create table if not exists runs(id integer primary key,instance_id text,created_at text,kind text,status text,document text)',
82
+ 'create table if not exists capability_facts(id text primary key,instance_id text,document text)',
83
+ 'create table if not exists facts(cache_key text primary key,document text)',
84
+ 'create table if not exists relationships(id text primary key,source_id text,target_id text,kind text,document text)',
85
+ 'create table if not exists findings(id text primary key,instance_id text,document text)',
86
+ 'create table if not exists suggestions(id text primary key,instance_id text,document text)',
87
+ 'create table if not exists decisions(instance_id text,key text,value text,primary key(instance_id,key))']:
88
+ self.connection.execute(statement)
89
+ for table,columns in {'projects':['id','name'],'instances':['id','project_id','path','included_at','status','metadata'],'snapshots':['id','instance_id','document'],'capability_facts':['id','instance_id','document']}.items():
90
+ existing={r[1] for r in self.connection.execute('pragma table_info('+table+')')}
91
+ if not set(columns)<=existing:raise ValueError('Incompatible catalogue table: '+table)
92
+ self.connection.execute('pragma user_version=1')
93
+ except BaseException:
94
+ self.connection.close();raise
95
+ def close(self):self.connection.close()
96
+ def register(self,record,kind='registered'):
97
+ path=Path(record['path']).expanduser()
98
+ if not path.is_absolute():raise ValueError('Absolute source path required')
99
+ path=path.absolute()
100
+ ident='instance-'+stable(str(path))[:24];project='project-'+stable(str(path))[:24]
101
+ old=self.connection.execute('select id from instances where path=?',(str(path),)).fetchone()
102
+ if old:
103
+ existing=self.show(old['id']);merged={**existing['metadata'],**record}
104
+ with self.connection:
105
+ self.connection.execute("update instances set metadata=?,included_at=coalesce(included_at,?),included_at_basis=case when included_at is null then ? else included_at_basis end,status=case when status='candidate' and ?='registered' then 'registered' else status end where id=?",
106
+ (encode(merged),record.get('included_at'),record.get('included_at_basis','unknown'),kind,old['id']))
107
+ return old['id']
108
+ volume=str(Path(*path.parts[:3])) if path.parts[1:2]==('Volumes',) else 'host'
109
+ date=now()
110
+ with self.connection:
111
+ self.connection.execute('insert or ignore into volumes values (?,?)',(stable(volume),volume))
112
+ self.connection.execute('insert into projects values (?,?)',(project,record.get('display_name',path.name)))
113
+ self.connection.execute('insert into instances values (?,?,?,?,?,?,?,?,?,?,?,?)',
114
+ (ident,project,str(path),stable(volume),record.get('id'),record.get('included_at'),
115
+ record.get('included_at_basis','unknown'),date,None,None,kind,encode(record)))
116
+ return ident
117
+ def decide(self,ident,key,value):
118
+ if not self.connection.execute('select 1 from instances where id=?',(ident,)).fetchone():raise ValueError('Unknown instance')
119
+ if key not in {'label','canonical','ownership','purpose','logical_project','excluded','approved'}:raise ValueError('Unknown decision key')
120
+ with self.connection:
121
+ self.connection.execute('insert or replace into decisions values (?,?,?)',(ident,key,str(value)))
122
+ if key=='logical_project':
123
+ if not self.connection.execute('select 1 from projects where id=?',(value,)).fetchone():raise ValueError('Unknown logical project')
124
+ self.connection.execute('update instances set project_id=? where id=?',(value,ident))
125
+ def relocate(self,ident,path):
126
+ before=self.show(ident);path=Path(path).expanduser().absolute()
127
+ if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError('Source symlink rejected')
128
+ volume=str(Path(*path.parts[:3])) if path.parts[1:2]==('Volumes',) else 'host'
129
+ with self.connection:
130
+ self.connection.execute('insert or ignore into volumes values (?,?)',(stable(volume),volume))
131
+ self.connection.execute('update instances set path=?,volume_id=?,status=? where id=?',(str(path),stable(volume),'relocated_unverified',ident))
132
+ self.connection.execute('insert into runs(instance_id,created_at,kind,status,document) values (?,?,?,?,?)',(ident,now(),'relocation','unverified',encode({'from':before['path'],'to':str(path)})))
133
+ return self.show(ident)
134
+
135
+ def list(self):
136
+ return [dict(x) for x in self.connection.execute('select i.*,p.name from instances i join projects p on p.id=i.project_id order by path')]
137
+ def show(self,ident):
138
+ row=self.connection.execute('select * from instances where id=?',(ident,)).fetchone()
139
+ if not row:raise ValueError('Unknown instance: '+ident)
140
+ result=dict(row);result['metadata']=json.loads(result['metadata'])
141
+ result['relationships']=[json.loads(x['document']) for x in self.connection.execute('select document from relationships where source_id=?',(ident,))]
142
+ result['decisions']={x['key']:x['value'] for x in self.connection.execute('select * from decisions where instance_id=?',(ident,))}
143
+ for key,table in [('snapshots','snapshots'),('runs','runs')]:
144
+ result[key]=[{**dict(x),'document':json.loads(x['document'])} for x in self.connection.execute('select * from '+table+' where instance_id=? order by id',(ident,))]
145
+ return result
146
+ def import_evidence(self,path,kind='historical'):
147
+ path=Path(path);count=0
148
+ if path.is_dir():
149
+ datefile=path/'.aqaf-source-dates.json'
150
+ dates=json.loads(datefile.read_text()).get('sources',{}) if datefile.is_file() and not datefile.is_symlink() else {}
151
+ for p in sorted(path.iterdir()):
152
+ if p.is_symlink():continue
153
+ if p.name.endswith('.aqaf-source.json'):count+=self.import_evidence(p)
154
+ elif p.is_dir() and not p.name.startswith('.'):
155
+ self.register({'id':p.name,'path':str(p.absolute()),'provenance':str(path),**dates.get(p.name,{})},'registered');count+=1
156
+ return count
157
+ if path.is_symlink() or path.stat().st_size>64000000:raise ValueError('Evidence must be bounded regular JSON')
158
+ doc=json.loads(path.read_text());records=[]
159
+ if isinstance(doc,list):records=doc
160
+ elif not isinstance(doc,dict):raise ValueError('Evidence must be a JSON object or record list')
161
+ elif doc.get('kind')=='aqaf-source-reference':records=[doc]
162
+ elif isinstance(doc.get('projects'),list):records=doc['projects']
163
+ elif isinstance(doc.get('projects'),dict):
164
+ records=[{'id':i,'path':doc.get('source_paths',{}).get(i),'assessment':v} for i,v in doc['projects'].items()]
165
+ elif isinstance(doc.get('sources'),list):records=doc['sources']
166
+ else:raise ValueError('Unrecognized evidence document')
167
+ for record in records:
168
+ if not isinstance(record,dict) or not isinstance(record.get('path'),str) or not Path(record['path']).is_absolute():continue
169
+ item={**record,'imported_from':str(path),'imported_at':now(),'evidence_freshness':'historical_unverified'}
170
+ state='candidate' if 'root_probability' in record and not record.get('included_at') else 'registered'
171
+ ident=self.register(item,state)
172
+ with self.connection:self.connection.execute('insert into runs(instance_id,created_at,kind,status,document) values (?,?,?,?,?)',(ident,now(),kind,'historical_unverified',encode(item)))
173
+ count+=1
174
+ return count
175
+ def refresh(self,ids=None,config=None):
176
+ from .facets import facets,classification
177
+ from .documentation import check
178
+ from .portfolio import dependencies
179
+ outcomes=[];engine=engine_identity()
180
+ for row in self.list():
181
+ ident=row['id']
182
+ if ids and ident not in ids:continue
183
+ detail=self.show(ident);date=now();scope=None
184
+ if detail['decisions'].get('excluded')=='true':status='excluded'
185
+ elif detail['decisions'].get('approved')=='false':status='candidate'
186
+ elif row['status']=='candidate' and detail['decisions'].get('approved')!='true':status='candidate'
187
+ elif not Path(row['path']).exists():status='unavailable'
188
+ else:
189
+ try:
190
+ path=Path(row['path'])
191
+ if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError('Source symlink rejected')
192
+ if self.path==path or path in self.path.parents:raise ValueError('Keep catalogue outside source tree')
193
+ scope=Scope(path,config=config).build();snap=snapshot(scope,engine=engine)
194
+ cached=self.connection.execute('select document from facts where cache_key=?',(snap['digest'],)).fetchone()
195
+ facts=json.loads(cached[0]) if cached and snap['complete'] else {'profile':classification(scope),'facets':facets(scope),'documentation':check(path,scope=scope),'dependencies':dependencies(scope)}
196
+ status='verified' if snap['complete'] else 'partial'
197
+ old=detail['snapshots'][-1]['digest'] if detail['snapshots'] else None
198
+ with self.connection:
199
+ if snap['complete']:self.connection.execute('insert or replace into facts values (?,?)',(snap['digest'],encode(facts)))
200
+ self.connection.execute('insert into snapshots(instance_id,created_at,digest,document) values (?,?,?,?)',(ident,date,snap['digest'],encode(snap)))
201
+ self.connection.execute('insert into runs(instance_id,created_at,kind,status,document) values (?,?,?,?,?)',(ident,date,'static_refresh',status,encode({'snapshot':snap['digest'],'facts':facts,'cache_hit':bool(cached),'changed':old is not None and old!=snap['digest']})))
202
+ for table in ('capability_facts','findings'):
203
+ self.connection.execute('delete from '+table+' where instance_id=?',(ident,))
204
+ for fact in facts['facets']['facts']:
205
+ self.connection.execute('insert or replace into capability_facts values (?,?,?)',(stable(ident+encode(fact)),ident,encode({**fact,'verified_at':date,'snapshot':snap['digest']})))
206
+ for finding in facts['documentation']['findings']:
207
+ self.connection.execute('insert or replace into findings values (?,?,?)',(stable(ident+finding['id']),ident,encode(finding)))
208
+ self.connection.execute('delete from relationships where source_id=?',(ident,))
209
+ for edge in facts.get('dependencies',[]):
210
+ self.connection.execute('insert or replace into relationships values (?,?,?,?,?)',(stable(ident+encode(edge)),ident,edge['path'],edge['kind'],encode({**edge,'verified_at':date})))
211
+ self.connection.execute('update instances set last_seen=?,last_verified=case when ? then ? else last_verified end where id=?',(date,snap['complete'],date,ident))
212
+ except (OSError,ValueError,sqlite3.Error) as exc:
213
+ status='unreadable'
214
+ with self.connection:self.connection.execute('insert into runs(instance_id,created_at,kind,status,document) values (?,?,?,?,?)',(ident,date,'static_refresh',status,encode({'error':type(exc).__name__,'reason':str(exc)[:512]})))
215
+ finally:
216
+ if scope:scope.cache.close()
217
+ with self.connection:self.connection.execute('update instances set status=? where id=?',(status,ident))
218
+ outcomes.append({'id':ident,'status':status})
219
+ return outcomes
220
+ def compare(self,a,b):
221
+ aa=self.show(a);bb=self.show(b)
222
+ sa=aa['snapshots'][-1]['document'] if aa['snapshots'] else None
223
+ sb=bb['snapshots'][-1]['document'] if bb['snapshots'] else None
224
+ def selected(s):return {e['path']:e['sha256'] for e in s['entries'] if e['role']!='docs' and (e['language']!='other' or Path(e['path']).suffix.lower() in {'.json','.toml','.yaml','.yml','.ini','.cfg','.xml','.csproj','.gradle'})}
225
+ equal=selected(sa)==selected(sb) if sa and sb and sa['complete'] and sb['complete'] and selected(sa) and selected(sb) else None
226
+ x=re.sub(r'[^a-z0-9]+',' ',Path(aa['path']).name.lower()).strip();y=re.sub(r'[^a-z0-9]+',' ',Path(bb['path']).name.lower()).strip()
227
+ from .matching import name_features
228
+ names=name_features(Path(aa['path']).name,Path(bb['path']).name)
229
+ return {'name_features':names,'schema_version':1,'a':a,'b':b,'selected_source_equal':equal,
230
+ 'normalized_newlines_equal':({e['path']:e.get('normalized_newlines_sha256') for e in sa['entries'] if e['path'] in selected(sa)}=={e['path']:e.get('normalized_newlines_sha256') for e in sb['entries'] if e['path'] in selected(sb)}) if sa and sb else None,
231
+ 'compared_files':len(selected(sa)) if sa else 0,'noncode_selected_equal':({e['path']:e['sha256'] for e in sa['entries']}=={e['path']:e['sha256'] for e in sb['entries']}) if sa and sb else None,
232
+ 'whole_project_equal':None,'name_similarity':round(SequenceMatcher(None,x,y).ratio(),4),
233
+ 'state':'candidate','canonical':'user_decision_required','mtime_used_as_quality':False,
234
+ 'scope':'Selected authored source/config; assets and excluded content may differ.',
235
+ 'freshness':{a:aa['last_verified'],b:bb['last_verified']}}
236
+ def candidates(self,ident,limit=20):
237
+ values=[self.compare(ident,r['id']) for r in self.list() if r['id']!=ident]
238
+ return sorted([v for v in values if (v['selected_source_equal'] and v['compared_files']>0) or (v['name_similarity']>=.55 and not v['name_features']['generic_name'])],key=lambda v:(not bool(v['selected_source_equal']),-v['name_similarity'],v['b']))[:limit]
239
+
240
+ def view(self):
241
+ instances=[];engine=engine_identity()
242
+ for row in self.list():
243
+ detail=self.show(row['id'])
244
+ runs=[r for r in detail['runs'] if r['kind']=='static_refresh' and 'facts' in r['document']]
245
+ facts=runs[-1]['document']['facts'] if runs else {}
246
+ metadata=detail['metadata'];profile=facts.get('profile',metadata.get('type',metadata.get('finder_type')))
247
+ instances.append({**{k:v for k,v in row.items() if k!='metadata'},'decisions':detail['decisions'],
248
+ 'metadata':{'type':profile,'family':metadata.get('family'),'historical_type':not bool(facts.get('profile'))},
249
+ 'canonical_path':facts.get('documentation',{}).get('root'),'engine_matches_current':bool(detail['snapshots'] and detail['snapshots'][-1]['document']['engine']==engine),
250
+ 'snapshots':[{'created_at':x['created_at'],'digest':x['digest']} for x in detail['snapshots']],
251
+ 'relationships':detail['relationships'],
252
+ 'suggestions':[json.loads(x[0]) for x in self.connection.execute('select document from suggestions where instance_id=?',(row['id'],))],
253
+ 'runs':[{'created_at':r['created_at'],'status':r['status']} for r in detail['runs'][:-1]]+
254
+ ([{'created_at':runs[-1]['created_at'],'status':runs[-1]['status'],'document':{'facts':{'facets':facts.get('facets'), 'dependencies':facts.get('dependencies')}}}] if runs else [])})
255
+ return {'schema_version':SCHEMA,'generated_at':now(),'instances':instances}
256
+
257
+ def export(self):return {'schema_version':SCHEMA,'generated_at':now(),'instances':[self.show(r['id']) for r in self.list()]}