@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,210 @@
|
|
|
1
|
+
"""Separate unscored documentation observations; no target execution or certification."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import ast
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
from pathlib import Path,PurePosixPath
|
|
8
|
+
from urllib.parse import unquote,urlsplit
|
|
9
|
+
from .scope import Scope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def markdown_body(text):
|
|
13
|
+
"""Remove fenced/indented samples, comments and inline code before link parsing."""
|
|
14
|
+
lines=[];fence=None
|
|
15
|
+
for line in text.splitlines():
|
|
16
|
+
m=re.match(r'^ {0,3}(`{3,}|~{3,})',line)
|
|
17
|
+
if m:
|
|
18
|
+
token=m.group(1)
|
|
19
|
+
if fence is None:fence=token
|
|
20
|
+
elif token[0]==fence[0] and len(token)>=len(fence):fence=None
|
|
21
|
+
lines.append('');continue
|
|
22
|
+
lines.append('' if fence or line.startswith((' ','\t')) else line)
|
|
23
|
+
return re.sub(r'<!--[\s\S]*?-->|(`+).*?\1','', '\n'.join(lines))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def links(text):
|
|
27
|
+
body=markdown_body(text);refs={}
|
|
28
|
+
for m in re.finditer(r'(?m)^ {0,3}\[([^]\n]+)\]:\s*<?([^\s>]+)>?',body):refs[m.group(1).casefold()]=m.group(2)
|
|
29
|
+
body=re.sub(r'(?m)^ {0,3}\[[^]\n]+\]:[^\n]*','',body)
|
|
30
|
+
found=[]
|
|
31
|
+
# Bounded balanced parentheses allow ordinary paths containing parentheses.
|
|
32
|
+
for m in re.finditer(r'!?\[([^]\n]*)\]\(\s*(<[^>\n]+>|(?:[^\s()\\]|\\.|\([^()\n]*\)){1,2048})(?:\s+["\'][^\n]*?["\'])?\s*\)',body):
|
|
33
|
+
found.append((m.group(2).strip('<>'),body.count('\n',0,m.start())+1))
|
|
34
|
+
for m in re.finditer(r'!?\[([^]\n]+)\](?:\[([^]\n]*)\])?(?![(:])',body):
|
|
35
|
+
key=(m.group(2) or m.group(1)).casefold()
|
|
36
|
+
if key in refs:found.append((refs[key],body.count('\n',0,m.start())+1))
|
|
37
|
+
return sorted(set(found),key=lambda x:(x[1],x[0]))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def anchors(text):
|
|
41
|
+
result=set();counts={}
|
|
42
|
+
for line in markdown_body(text).splitlines():
|
|
43
|
+
m=re.match(r'^ {0,3}#{1,6}\s+(.+?)\s*#*$',line)
|
|
44
|
+
if not m:continue
|
|
45
|
+
label=re.sub(r'[^\w\- ]','',m.group(1).lower()).replace(' ','-')
|
|
46
|
+
n=counts.get(label,0);counts[label]=n+1;result.add(label+('-'+str(n) if n else ''))
|
|
47
|
+
result.update(re.findall(r'\bid=["\']([^"\']+)',text))
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def documented_default(node,flag):
|
|
52
|
+
"""Only JSON scalars; never return credential-shaped literal metadata."""
|
|
53
|
+
import math
|
|
54
|
+
if not isinstance(node,ast.Constant):return None,'unknown'
|
|
55
|
+
value=node.value
|
|
56
|
+
if type(value) not in {str,int,float,bool,type(None)} or (type(value) is float and not math.isfinite(value)):
|
|
57
|
+
return None,'unknown'
|
|
58
|
+
sensitive_name=re.search(r'password|passphrase|secret|token|key|credential|auth',flag,re.I)
|
|
59
|
+
sensitive_value=isinstance(value,str) and re.search(r'://[^/\s]+@|(?:api[_-]?key|token|password|secret)=[^&\s]+|^Bearer\s+\S+|^(?:sk-|gh[pousr]_|xox[baprs]-|AKIA)',value,re.I)
|
|
60
|
+
if value is not None and (sensitive_name or sensitive_value):return '[redacted]','redacted'
|
|
61
|
+
return value,'observed'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def interfaces(scope):
|
|
65
|
+
facts=[];unknown=[];read_bytes=0;attempted=0
|
|
66
|
+
for r in scope.selected({'production','source','unknown','tooling'}):
|
|
67
|
+
if r['language']=='other' and r['path'].suffix not in {'.json','.service','.plist'}:continue
|
|
68
|
+
if attempted>=1000 or read_bytes>=16000000:
|
|
69
|
+
unknown.append({'reason':'interface_budget','remaining_coverage':'unknown'});break
|
|
70
|
+
attempted+=1
|
|
71
|
+
raw=scope.raw(r['path'],min(131072,16000000-read_bytes));read_bytes+=len(raw);text=raw.decode('utf8','replace');sha=hashlib.sha256(raw).hexdigest()
|
|
72
|
+
base={'path':r['relative'],'sha256':sha,'runtime_verified':False}
|
|
73
|
+
if r['language']=='python':
|
|
74
|
+
try:tree=ast.parse(text)
|
|
75
|
+
except (SyntaxError,ValueError,RecursionError):unknown.append({**base,'reason':'python_syntax_or_truncated'});continue
|
|
76
|
+
for node in ast.walk(tree):
|
|
77
|
+
if not isinstance(node,ast.Call):continue
|
|
78
|
+
name=node.func.attr if isinstance(node.func,ast.Attribute) else ''
|
|
79
|
+
if name=='add_argument':
|
|
80
|
+
flags=[a.value for a in node.args if isinstance(a,ast.Constant) and isinstance(a.value,str) and a.value.startswith('--')]
|
|
81
|
+
default_node=next((k.value for k in node.keywords if k.arg=='default'),None)
|
|
82
|
+
for flag in flags:
|
|
83
|
+
default,default_state=documented_default(default_node,flag)
|
|
84
|
+
facts.append({**base,'kind':'cli_flag','name':flag,'default':default,'default_state':default_state,'line':node.lineno,'state':'observed'})
|
|
85
|
+
if not flags:unknown.append({**base,'reason':'dynamic_cli_registration','line':node.lineno})
|
|
86
|
+
if name in {'get','post','put','patch','delete','route'} and node.args and isinstance(node.args[0],ast.Constant) and isinstance(node.args[0].value,str) and node.args[0].value.startswith('/'):
|
|
87
|
+
facts.append({**base,'kind':'route_candidate','name':node.args[0].value,'method':name.upper(),'line':node.lineno,'state':'inferred'})
|
|
88
|
+
elif r['language'] in {'javascript','typescript','shell','powershell','cmd'}:
|
|
89
|
+
from .adapters import mask_source
|
|
90
|
+
try:view=mask_source(text,r['language'])[0] if r['language']!='cmd' else text
|
|
91
|
+
except (ValueError,RecursionError):
|
|
92
|
+
unknown.append({**base,'reason':'unsupported_or_truncated_syntax'});continue
|
|
93
|
+
if r['language'] in {'shell','powershell'}:
|
|
94
|
+
for i,line in enumerate(text.splitlines(),1):
|
|
95
|
+
visible=view.splitlines()[i-1] if i<=len(view.splitlines()) else ''
|
|
96
|
+
if not re.match(r'^\s*(?:exec\s+)?(?:python[23]?|node|pwsh)\s+',visible):continue
|
|
97
|
+
m=re.match(r'^\s*(?:exec\s+)?(python[23]?|node|pwsh)\s+([\w./\\-]+)',line)
|
|
98
|
+
if m:facts.append({**base,'kind':'launcher','name':m.group(1),'target':m.group(2),'line':i,'state':'observed','syntax_scope':'literal_launcher_only'})
|
|
99
|
+
if r['language']=='cmd':
|
|
100
|
+
for i,line in enumerate(text.splitlines(),1):
|
|
101
|
+
if re.match(r'(?i)^\s*@?(?:rem\b|::)',line):continue
|
|
102
|
+
m=re.match(r'(?i)^\s*@?(?:call\s+)?(python(?:3|\.exe)?|node(?:\.exe)?|powershell(?:\.exe)?|pwsh|java(?:\.exe)?)\s+([\w./\\-]+)',line)
|
|
103
|
+
if m:facts.append({**base,'kind':'launcher','name':m.group(1),'target':m.group(2),'line':i,'state':'observed','syntax_scope':'literal_cmd_launcher_only'})
|
|
104
|
+
# String-bearing registration requires a call spelling outside strings/comments.
|
|
105
|
+
for m in re.finditer(r'\.(option|command|get|post|put|delete)\s*\(',view):
|
|
106
|
+
tail=text[m.end():m.end()+512];literal=re.match(r'\s*["\']([^"\'\n]{1,256})["\']',tail)
|
|
107
|
+
if literal:
|
|
108
|
+
value=literal.group(1);kind='cli_flag' if m.group(1)=='option' else 'route_candidate'
|
|
109
|
+
if kind=='cli_flag' and not value.startswith('-'):continue
|
|
110
|
+
facts.append({**base,'kind':kind,'name':value.split()[0],'default':None,'state':'inferred','line':text.count('\n',0,m.start())+1})
|
|
111
|
+
else:unknown.append({**base,'reason':'dynamic_registration'})
|
|
112
|
+
if r['path'].suffix in {'.service','.plist'}:facts.append({**base,'kind':'service_definition','name':r['path'].name,'state':'observed'})
|
|
113
|
+
if re.search(r'\b(?:getenv|environ|process\.env)\b',text):unknown.append({**base,'reason':'environment_precedence_requires_review'})
|
|
114
|
+
return facts,unknown
|
|
115
|
+
|
|
116
|
+
EXPECTATIONS={
|
|
117
|
+
'desktop_ui':('end_user','install|installation','state|storage','reset|troubleshoot'),
|
|
118
|
+
'desktop_widget':('end_user','install|installation','state|storage|reset'),
|
|
119
|
+
'windows_service':('administrator','privilege|administrator|account','pair|trust|permission'),
|
|
120
|
+
'browser_python':('learner','worker|stop|cancel','offline|asset|download'),
|
|
121
|
+
'proxy':('operator','auth|token|access','stream','retention|logging'),
|
|
122
|
+
'backup':('operator','restore|recovery','dry.run|preview|verify'),
|
|
123
|
+
'ocr':('reviewer','evidence|confidence|manual.review','provenance|original|source'),
|
|
124
|
+
'pdf_mapping':('content_author','page|mapping','provenance|source|chapter'),
|
|
125
|
+
'apps_script':('administrator','permission|scope|oauth','deploy|installation'),
|
|
126
|
+
'ide_extension':('developer','install|plugin','model|endpoint|privacy')}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check(root,scope=None,generated_routes=(),audience=None):
|
|
130
|
+
owned=scope is None;scope=scope or Scope(root).build();root=scope.root
|
|
131
|
+
try:
|
|
132
|
+
all_docs=[r for r in scope.records if r['path'].suffix.lower() in {'.md','.markdown'}]
|
|
133
|
+
docs=all_docs[:2048];budget=16000000;doc_partial=len(docs)<len(all_docs)
|
|
134
|
+
texts={};hashes={};seen={};duplicates=0;evidence=[];findings=[];finding_keys={}
|
|
135
|
+
for r in docs:
|
|
136
|
+
if budget<=0:doc_partial=True;break
|
|
137
|
+
raw=scope.raw(r['path'],min(262144,budget));budget-=len(raw)
|
|
138
|
+
if len(raw)!=r['size']:doc_partial=True
|
|
139
|
+
sha=hashlib.sha256(raw).hexdigest();texts[r['relative']]=raw.decode('utf8','replace')
|
|
140
|
+
hashes[r['relative']]={'sha256':sha,'bytes':len(raw),'complete_file':len(raw)==r['size']}
|
|
141
|
+
evidence.append({'path':r['relative'],**hashes[r['relative']],'kind':'documentation','audience':audience or 'unknown'})
|
|
142
|
+
if sha in seen:duplicates+=1
|
|
143
|
+
seen[sha]=r['relative']
|
|
144
|
+
def issue(rule,state,r,message,action,**extra):
|
|
145
|
+
h=hashes.get(r['relative'])
|
|
146
|
+
if h is None:
|
|
147
|
+
raw=scope.raw(r['path'],131072);h={'sha256':hashlib.sha256(raw).hexdigest(),'bytes':len(raw),'complete_file':len(raw)==r['size']}
|
|
148
|
+
key=(rule,state,h['sha256'],extra.get('target',extra.get('name',action)))
|
|
149
|
+
if key in finding_keys:
|
|
150
|
+
finding_keys[key].setdefault('copied_occurrences',[]).append(r['relative']);return
|
|
151
|
+
item={'rule_id':rule,'state':state,'path':r['relative'],**h,
|
|
152
|
+
'audience':audience or 'maintainer','consequence':message,'action':action,'priority':'medium' if state=='observed' else 'low',
|
|
153
|
+
'confirmed':False,'runtime_verified':False,**extra}
|
|
154
|
+
findings.append(item);finding_keys[key]=item
|
|
155
|
+
selected={x['relative'] for x in scope.records}
|
|
156
|
+
for r in docs:
|
|
157
|
+
if r['relative'] not in texts:continue
|
|
158
|
+
text=texts[r['relative']]
|
|
159
|
+
for target,line in links(text):
|
|
160
|
+
if re.match(r'^[a-z][a-z0-9+.-]*:',target,re.I) or target.startswith('//'):continue
|
|
161
|
+
parsed=urlsplit(target);link=unquote(parsed.path);fragment=unquote(parsed.fragment)
|
|
162
|
+
if link.startswith('/'):
|
|
163
|
+
if link not in generated_routes:issue('AQD-LINK-003','unknown',r,'Root-relative route has no configured resolution.','Configure the generated route or verify it separately.',target=target,line=line)
|
|
164
|
+
continue
|
|
165
|
+
candidate=r['path'].parent/link if link else r['path']
|
|
166
|
+
candidate=candidate.resolve()
|
|
167
|
+
try:rel=candidate.relative_to(root).as_posix()
|
|
168
|
+
except ValueError:
|
|
169
|
+
issue('AQD-LINK-003','unknown',r,'Link leaves the selected root.','Verify the external local dependency separately.',target=target,line=line);continue
|
|
170
|
+
if rel not in selected and not candidate.is_dir():
|
|
171
|
+
portable=[n for n in selected if unicodedata.normalize('NFC',n).casefold()==unicodedata.normalize('NFC',rel).casefold()]
|
|
172
|
+
if portable:issue('AQD-LINK-002','observed',r,'Link differs in case or Unicode normalization.','Use the exact portable path spelling.',target=target,line=line)
|
|
173
|
+
else:issue('AQD-LINK-001','observed' if not candidate.exists() else 'unknown',r,'Local link target is missing or outside selected scope.','Repair the link or declare its generated/excluded target.',target=target,line=line)
|
|
174
|
+
elif fragment and candidate in scope.by_path:
|
|
175
|
+
if rel not in texts:
|
|
176
|
+
issue('AQD-LINK-003','unknown',r,'Anchor target is outside the documentation read budget or supported Markdown scope.','Verify this fragment separately.',target=target,line=line)
|
|
177
|
+
elif fragment not in anchors(texts[rel]):issue('AQD-LINK-004','inferred',r,'Heading anchor does not match the supported Markdown convention.','Verify renderer anchor rules and correct the fragment.',target=target,line=line)
|
|
178
|
+
interface,unknown=interfaces(scope)
|
|
179
|
+
canonical='\n'.join(t for p,t in texts.items() if not re.search(r'(?i)changelog|release.notes|history',p))
|
|
180
|
+
documented=set(re.findall(r'--[a-zA-Z][a-zA-Z0-9_-]*',canonical));known={x['name'] for x in interface if x['kind']=='cli_flag'}
|
|
181
|
+
for fact in interface:
|
|
182
|
+
if fact['kind']=='route_candidate' and fact['name'] not in canonical:
|
|
183
|
+
issue('AQD-API-001','inferred',scope.record(root/fact['path']),'A static route-shaped registration has no selected documentation mention.','Check whether this is an exposed route and document its method/authentication contract.',name=fact['name'])
|
|
184
|
+
if fact['kind']=='cli_flag' and fact['name'] not in documented:
|
|
185
|
+
r=scope.record(root/fact['path']);issue('AQD-CLI-001','inferred',r,'A static CLI flag is absent from selected current documentation.','Document this flag if it is public.',name=fact['name'])
|
|
186
|
+
if fact['kind']=='cli_flag' and fact.get('default') is not None and fact['default']!='[redacted]':
|
|
187
|
+
for line in canonical.splitlines():
|
|
188
|
+
if fact['name'] in line:
|
|
189
|
+
m=re.search(r'default\s*[:=]\s*[`"\']?([\w./-]+)',line,re.I)
|
|
190
|
+
if m and m.group(1)!=str(fact['default']):
|
|
191
|
+
issue('AQD-CLI-002','inferred',scope.record(root/fact['path']),'Documented default differs from a static literal.','Check precedence and update the public default.',name=fact['name']);break
|
|
192
|
+
for flag in sorted(documented-known):
|
|
193
|
+
if docs:issue('AQD-CLI-003','unknown',docs[0],'Documented flag was not found in supported static registrations.','Check rename, external command or dynamic registration.',name=flag)
|
|
194
|
+
from .facets import facets
|
|
195
|
+
capabilities=facets(scope)
|
|
196
|
+
present={x['facet'] for x in capabilities['facts']}
|
|
197
|
+
for facet,(reader,*patterns) in EXPECTATIONS.items():
|
|
198
|
+
if facet not in present:continue
|
|
199
|
+
for pattern in patterns:
|
|
200
|
+
if audience and audience!=reader:continue
|
|
201
|
+
if not re.search(pattern,canonical,re.I) and scope.records:
|
|
202
|
+
issue('AQD-PRODUCT-001','inferred',scope.records[0],'Product evidence suggests an undocumented reader need: '+facet+'.','Review and document '+pattern+' for '+reader+'.',product_facet=facet,reader=reader)
|
|
203
|
+
for index,item in enumerate(findings):item['id']='doc-'+hashlib.sha256((item['rule_id']+item['path']+str(item.get('target',item.get('name',item['action'])))).encode()).hexdigest()[:20]
|
|
204
|
+
return {'schema_version':1,'kind':'aqaf_documentation_review','root':str(root),'status':'partial' if doc_partial or not scope.selection_complete() or any(x.get('reason') in {'interface_budget','unsupported_or_truncated_syntax','python_syntax_or_truncated'} for x in unknown) else 'complete_within_static_scope',
|
|
205
|
+
'score_effect':'none','certification':False,'coverage':{'documents_selected':len(all_docs),'documents_read':len(texts),'max_documents':2048,'max_document_bytes':16000000,'max_document_file_bytes':262144},'documents':evidence,'duplicate_documents':duplicates,'interfaces':interface,'interface_unknowns':unknown,
|
|
206
|
+
'capabilities':capabilities,'findings':findings,'backlog':[{'id':f['id'],'action':f['action'],'evidence':{'path':f['path'],'sha256':f['sha256']},'state':f['state'],'priority':f['priority']} for f in findings],
|
|
207
|
+
'applicability':[{'facet':facet,'reader':reader,'state':'not_applicable' if audience and audience!=reader else 'inferred' if facet in present else 'unknown'} for facet,(reader,*_) in EXPECTATIONS.items()],
|
|
208
|
+
'scope':scope.report(),'limitations':['Static bounded registrations only; dynamic interfaces and runtime claims require verification.','Document presence and term matches do not establish correctness or readability.','Renderer-specific anchors and complex Markdown remain reviewable unknowns.']}
|
|
209
|
+
finally:
|
|
210
|
+
if owned:scope.cache.close()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Bounded static evidence helpers. Never import or execute assessed project code."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import ast
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
_BLOCK_MARKER = re.compile(r'/\*|\*/')
|
|
9
|
+
|
|
10
|
+
PACKAGE_MARKERS = ('.git', 'package.json', 'pyproject.toml', 'Cargo.toml', 'Package.swift',
|
|
11
|
+
'pom.xml', 'settings.gradle', 'settings.gradle.kts', 'go.mod', 'composer.json')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _metadata_files(root, relative, suffixes):
|
|
15
|
+
"""Known metadata locations only; no symlink traversal or unbounded ancestor walk."""
|
|
16
|
+
base = root
|
|
17
|
+
for part in Path(relative).parts:
|
|
18
|
+
base = base / part
|
|
19
|
+
if base.is_symlink():
|
|
20
|
+
return []
|
|
21
|
+
if base.is_file():
|
|
22
|
+
return [base] if base.suffix.lower() in suffixes else []
|
|
23
|
+
if not base.is_dir():
|
|
24
|
+
return []
|
|
25
|
+
found = []
|
|
26
|
+
visited = 0
|
|
27
|
+
for directory, dirs, names in os.walk(base, followlinks=False):
|
|
28
|
+
visited += 1
|
|
29
|
+
if visited > 256:
|
|
30
|
+
break
|
|
31
|
+
dirs[:] = sorted(d for d in dirs if not d.startswith('.') and not (Path(directory)/d).is_symlink())
|
|
32
|
+
for name in sorted(names):
|
|
33
|
+
path = Path(directory) / name
|
|
34
|
+
if not path.is_symlink() and path.is_file() and path.suffix.lower() in suffixes:
|
|
35
|
+
found.append(path)
|
|
36
|
+
if len(found) == 1000:
|
|
37
|
+
return found
|
|
38
|
+
return found
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _unique_paths(paths):
|
|
42
|
+
# Case-insensitive filesystems can resolve uppercase/lowercase candidates to
|
|
43
|
+
# one file. Preserve the first valid spelling; keep distinct Linux files.
|
|
44
|
+
seen = set(); result = []
|
|
45
|
+
for path in paths:
|
|
46
|
+
try:
|
|
47
|
+
info = path.stat(); identity = (info.st_dev, info.st_ino)
|
|
48
|
+
except OSError:
|
|
49
|
+
continue
|
|
50
|
+
if identity not in seen:
|
|
51
|
+
seen.add(identity); result.append(path)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parent_context(root: Path):
|
|
56
|
+
"""Consider only the immediate owner of a conventional source slice.
|
|
57
|
+
|
|
58
|
+
An independently marked package/repository never inherits its parent's docs.
|
|
59
|
+
Evidence is descriptive file presence, not verified architectural compliance.
|
|
60
|
+
"""
|
|
61
|
+
root = root.resolve()
|
|
62
|
+
result = {'root': None, 'architecture_files': [], 'migration_files': []}
|
|
63
|
+
if root.name.lower() not in {'src', 'app', 'lib'} or any((root/name).exists() or (root/name).is_symlink() for name in PACKAGE_MARKERS):
|
|
64
|
+
return result
|
|
65
|
+
parent = root.parent
|
|
66
|
+
documents = []
|
|
67
|
+
for relative in ('architecture.md', 'ARCHITECTURE.md', 'architecture.rst', 'ARCHITECTURE.rst',
|
|
68
|
+
'docs/architecture.md', 'docs/ARCHITECTURE.md', 'docs/architecture', 'docs/adr'):
|
|
69
|
+
documents.extend(_metadata_files(parent, relative, {'.md', '.rst', '.adoc'}))
|
|
70
|
+
migrations = []
|
|
71
|
+
for relative in ('migrations', 'prisma/migrations', 'alembic/versions', 'db/migrate', 'db/migrations'):
|
|
72
|
+
migrations.extend(_metadata_files(parent, relative, {'.sql', '.py', '.js', '.ts', '.rb', '.php', '.xml', '.json', '.yml', '.yaml'}))
|
|
73
|
+
documents = _unique_paths(documents)
|
|
74
|
+
migrations = _unique_paths(migrations)
|
|
75
|
+
if documents or migrations:
|
|
76
|
+
result = {'root': str(parent),
|
|
77
|
+
'architecture_files': sorted({'../'+p.relative_to(parent).as_posix() for p in documents}),
|
|
78
|
+
'migration_files': sorted({'../'+p.relative_to(parent).as_posix() for p in migrations})}
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def code_tokens(text, rust=False):
|
|
83
|
+
"""Mask comments and literals for narrow call/attribute heuristics, not parsing.
|
|
84
|
+
|
|
85
|
+
Rust block comments nest; Rust raw-string delimiters are preserved while
|
|
86
|
+
finding the closing quote. Template strings are masked as a whole.
|
|
87
|
+
"""
|
|
88
|
+
single = r"'(?:\\.|[^'\\])*'" if not rust else r"'(?:\\[^\n]|[^'\\\n])'"
|
|
89
|
+
raw = r'(?P<raw>(?:br|r)(?P<hashes>#{0,255})")|' if rust else ''
|
|
90
|
+
pattern = re.compile(r'//[^\n]*|(?P<block>/\*)|' + raw + r'"(?:\\[\s\S]|[^"\\])*"|' + single + r'|`(?:\\[\s\S]|[^`\\])*`')
|
|
91
|
+
parts = []; offset = 0
|
|
92
|
+
while True:
|
|
93
|
+
match = pattern.search(text, offset)
|
|
94
|
+
if not match:
|
|
95
|
+
parts.append(text[offset:]); break
|
|
96
|
+
parts.append(text[offset:match.start()]); end = match.end()
|
|
97
|
+
if match.group('block'):
|
|
98
|
+
depth = 1
|
|
99
|
+
while depth:
|
|
100
|
+
marker = _BLOCK_MARKER.search(text, end)
|
|
101
|
+
if not marker:
|
|
102
|
+
end = len(text); break
|
|
103
|
+
depth += 1 if marker.group() == '/*' else -1
|
|
104
|
+
end = marker.end()
|
|
105
|
+
elif rust and match.group('raw'):
|
|
106
|
+
closing = '"' + match.group('hashes')
|
|
107
|
+
position = text.find(closing, end)
|
|
108
|
+
end = len(text) if position < 0 else position + len(closing)
|
|
109
|
+
parts.append(''.join('\n' if c == '\n' else ' ' for c in text[match.start():end]))
|
|
110
|
+
offset = end
|
|
111
|
+
return ''.join(parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def rust_test_count(text):
|
|
115
|
+
if not re.search(r'#\s*\[\s*(?:(?:tokio|async_std)\s*::\s*)?test(?:\s*\([^]]*\))?\s*\]', text):
|
|
116
|
+
return 0
|
|
117
|
+
tokens = code_tokens(text, rust=True)
|
|
118
|
+
return len(re.findall(r'#\s*\[\s*(?:(?:tokio|async_std)\s*::\s*)?test(?:\s*\([^]]*\))?\s*\]\s*(?:#\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+[A-Za-z_]\w*\s*\(', tokens))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def has_warning_call(text, python=False):
|
|
122
|
+
pattern = r'\b(?:console|logger|log)\s*\.\s*warn(?:ing)?\s*\('
|
|
123
|
+
if not re.search(pattern, text): return False
|
|
124
|
+
if python:
|
|
125
|
+
try:
|
|
126
|
+
tree = ast.parse(text)
|
|
127
|
+
except (SyntaxError, RecursionError):
|
|
128
|
+
return False
|
|
129
|
+
return any(isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)
|
|
130
|
+
and n.func.attr in {'warn', 'warning'} and isinstance(n.func.value, ast.Name)
|
|
131
|
+
and n.func.value.id in {'console', 'logger', 'log'} for n in ast.walk(tree))
|
|
132
|
+
return bool(re.search(pattern, code_tokens(text)))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unscored product facets from bounded static evidence, separate from profiles."""
|
|
2
|
+
import re
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
# Spelling evidence is an inferred capability, never proof of complete behavior.
|
|
6
|
+
PATTERNS={
|
|
7
|
+
'browser_python':r'\b(?:loadPyodide|runPythonAsync|pyodide)\b',
|
|
8
|
+
'pseudocode':r'\b(?:parsePseudocode|PseudocodeParser|executePseudocode)\b',
|
|
9
|
+
'ocr':r'\b(?:pytesseract|Tesseract|recognizeText|VNRecognizeTextRequest|OCRResult|OcrResult|ocr|ocr_[A-Za-z0-9_]+)\b',
|
|
10
|
+
'grading':r'\b(?:grade_submission|evaluate_submission|EvaluationResult|criterion_scores)\b',
|
|
11
|
+
'desktop_ui':r'\b(?:QApplication|NSApplication|Tk|WindowGroup|ApplicationWindow|QMainWindow)\b',
|
|
12
|
+
'desktop_widget':r'\b(?:WidgetKit|WidgetConfiguration|AppWidgetProvider)\b',
|
|
13
|
+
'windows_service':r'\b(?:ServiceBase|AddWindowsService|UseWindowsService|ServiceFramework)\b',
|
|
14
|
+
'proxy':r'\b(?:StreamingResponse|ReverseProxy|ProxyPass|proxy_pass|http_proxy)\b',
|
|
15
|
+
'authentication':r'\b(?:OAuth2PasswordBearer|AuthenticationMiddleware|AddAuthentication|login_required|verify_oauth2_token|verify_id_token|OAuth2AuthorizationCodeBearer)\b',
|
|
16
|
+
'backup':r'\b(?:CreateBackup|create_backup|backup_source|BackupFlow|PortBackupSource)\b',
|
|
17
|
+
'pdf_mapping':r'\b(?:page_map|page_mapping|chapter_manifest|build_book_packs)\b',
|
|
18
|
+
'browser_worker':r'\b(?:Worker|postMessage|terminate)\b',
|
|
19
|
+
'apps_script':r'\b(?:SpreadsheetApp|ScriptApp|DocumentApp)\b',
|
|
20
|
+
'ide_extension':r'\b(?:ToolWindowFactory|AnAction|IntelliJPlatform|ProjectActivity)\b',
|
|
21
|
+
'infrastructure':r'\b(?:subprocess|ProcessBuilder|StartProcess|Popen)\b',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def facets(scope,max_files=128,max_bytes=2000000,owner=None):
|
|
25
|
+
from .discovery import code_view,load_engine
|
|
26
|
+
engine=load_engine();facts=[];diagnostics=[];used=0;attempted=0
|
|
27
|
+
records=[r for r in scope.selected({'production','source','unknown','tooling'}) if owner is None or r['owner']==owner]
|
|
28
|
+
for record in records:
|
|
29
|
+
if record['language']=='other':continue
|
|
30
|
+
if attempted>=max_files or used>=max_bytes:break
|
|
31
|
+
raw=scope.raw(record['path'],min(32768,max_bytes-used));used+=len(raw);attempted+=1
|
|
32
|
+
text=raw.decode('utf8','replace')
|
|
33
|
+
try:view=code_view(text,record['language'],engine)
|
|
34
|
+
except (ValueError,RecursionError,SyntaxError):
|
|
35
|
+
diagnostics.append({'path':record['relative'],'reason':'unsupported_or_truncated_syntax'});continue
|
|
36
|
+
for facet,pattern in PATTERNS.items():
|
|
37
|
+
match=re.search(pattern,view)
|
|
38
|
+
if match:facts.append({'facet':facet,'state':'inferred','path':record['relative'],
|
|
39
|
+
'sha256':hashlib.sha256(raw).hexdigest(),'line':view.count('\n',0,match.start())+1,
|
|
40
|
+
'read_bytes':len(raw),'complete_file':len(raw)==record['size'],
|
|
41
|
+
'confirmed':False,'runtime_verified':False})
|
|
42
|
+
present={f['facet'] for f in facts}
|
|
43
|
+
delivery=sorted({kind for facet,kind in [('desktop_ui','desktop'),('browser_python','browser'),('browser_worker','browser'),('desktop_widget','desktop'),('windows_service','service'),('ide_extension','ide_plugin'),('apps_script','cloud_script')] if facet in present})
|
|
44
|
+
return {'delivery':delivery,'runtimes':sorted({r['language'] for r in records if r['language']!='other'}),'source_evidence_state':'bounded_source_sample' if attempted else 'metadata_only_or_unread','platform_constraints':['windows'] if 'windows_service' in present else ['apple'] if 'desktop_widget' in present and any(r['language']=='swift' for r in records) else ['unknown'],
|
|
45
|
+
'diagnostics':diagnostics[:200],'schema_version':1,'facts':facts,'attempted':attempted,'bytes':used,
|
|
46
|
+
'partial':bool(diagnostics) or attempted<sum(r['language']!='other' for r in records) or any(not f['complete_file'] for f in facts),
|
|
47
|
+
'interpretation':'Static capability spellings; absence from bounded evidence means unknown.'}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def classification(scope):
|
|
51
|
+
"""Existing profile IDs, shared selected roots, masked source-only text cues."""
|
|
52
|
+
from .discovery import code_view,load_engine
|
|
53
|
+
from .registry import Registry
|
|
54
|
+
engine=load_engine();records=[r for r in scope.records if r['owner']=='root']
|
|
55
|
+
code=[r['path'] for r in records if r['language']!='other']
|
|
56
|
+
def reader(path,limit=65536):
|
|
57
|
+
text=scope.text(path,limit);lang=scope.record(path).get('language')
|
|
58
|
+
if lang and lang!='other':
|
|
59
|
+
try:return code_view(text,lang,engine)
|
|
60
|
+
except (ValueError,RecursionError):return ''
|
|
61
|
+
return text
|
|
62
|
+
ident,evidence,confidence,alternatives=Registry().detect([r['path'] for r in records],scope.root,code,reader=reader)
|
|
63
|
+
return {'profile':ident,'evidence':evidence,'confidence':confidence,'alternatives':alternatives,'kind':'static_heuristic','confirmed':False}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Bounded .gitignore matching without Git, shell execution, or index access.
|
|
2
|
+
|
|
3
|
+
Patterns apply to scanned files regardless of tracked status. Only ignore files
|
|
4
|
+
inside the authorized root are loaded; global Git config is never consulted.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import fnmatch
|
|
9
|
+
import hashlib
|
|
10
|
+
from pathlib import PurePosixPath
|
|
11
|
+
|
|
12
|
+
MAX_IGNORE_BYTES = 131072
|
|
13
|
+
MAX_RULES = 4096
|
|
14
|
+
MAX_PATTERN = 512
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _unescape(pattern):
|
|
18
|
+
out=[];i=0
|
|
19
|
+
while i<len(pattern):
|
|
20
|
+
if pattern[i]=='\\' and i+1<len(pattern):
|
|
21
|
+
i+=1;c=pattern[i];out.append({'*':'[*]','?':'[?]','[':'[[]'}.get(c,c))
|
|
22
|
+
elif pattern.startswith('[^',i):
|
|
23
|
+
# Git accepts both ! and ^ to negate a character class.
|
|
24
|
+
out.append('[!');i+=2;continue
|
|
25
|
+
else:out.append(pattern[i])
|
|
26
|
+
i+=1
|
|
27
|
+
return ''.join(out)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _components_match(pattern, parts):
|
|
31
|
+
# DP rather than a recursive/backtracking ** regex over an entire path.
|
|
32
|
+
states={0}
|
|
33
|
+
for index,component in enumerate(pattern):
|
|
34
|
+
next_states=set()
|
|
35
|
+
for pos in states:
|
|
36
|
+
if component=='**':
|
|
37
|
+
# A trailing '/**' matches descendants, never the parent itself.
|
|
38
|
+
minimum=1 if index==len(pattern)-1 and len(pattern)>1 else 0
|
|
39
|
+
next_states.update(range(pos+minimum,len(parts)+1))
|
|
40
|
+
elif pos<len(parts) and fnmatch.fnmatchcase(parts[pos],component):next_states.add(pos+1)
|
|
41
|
+
states=next_states
|
|
42
|
+
if not states:return False
|
|
43
|
+
return len(parts) in states
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class IgnoreRule:
|
|
48
|
+
base: tuple
|
|
49
|
+
pattern: tuple
|
|
50
|
+
basename: bool
|
|
51
|
+
directory_only: bool
|
|
52
|
+
negate: bool
|
|
53
|
+
source: str
|
|
54
|
+
line: int
|
|
55
|
+
|
|
56
|
+
def matches(self, relative, is_dir):
|
|
57
|
+
parts=PurePosixPath(relative).parts
|
|
58
|
+
if parts[:len(self.base)]!=self.base:return False
|
|
59
|
+
parts=parts[len(self.base):]
|
|
60
|
+
if not parts:return False
|
|
61
|
+
if self.basename:
|
|
62
|
+
return any(fnmatch.fnmatchcase(part,self.pattern[0]) and (not self.directory_only or i<len(parts)-1 or is_dir) for i,part in enumerate(parts))
|
|
63
|
+
# A matched directory also excludes descendants. The walker prunes
|
|
64
|
+
# excluded parents, so a later child negation cannot rescue them.
|
|
65
|
+
for end in range(1,len(parts)+1):
|
|
66
|
+
if self.directory_only and end==len(parts) and not is_dir:continue
|
|
67
|
+
if _components_match(self.pattern,parts[:end]):return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_ignore(raw, source='.gitignore'):
|
|
72
|
+
"""Return rules plus an explicit diagnostic for unsupported/oversized input."""
|
|
73
|
+
if len(raw)>MAX_IGNORE_BYTES:return [],'oversized_gitignore'
|
|
74
|
+
try:text=raw.decode('utf-8-sig')
|
|
75
|
+
except UnicodeError:return [],'invalid_gitignore_encoding'
|
|
76
|
+
base=PurePosixPath(source).parent.parts
|
|
77
|
+
rules=[];diagnostic=None
|
|
78
|
+
for line_no,line in enumerate(text.splitlines(),1):
|
|
79
|
+
while line.endswith(' '):
|
|
80
|
+
preceding=len(line[:-1])-len(line[:-1].rstrip('\\'))
|
|
81
|
+
if preceding%2:break
|
|
82
|
+
line=line[:-1]
|
|
83
|
+
if not line or line.startswith('#'):continue
|
|
84
|
+
negate=line.startswith('!')
|
|
85
|
+
if negate:line=line[1:]
|
|
86
|
+
if not line:continue
|
|
87
|
+
directory=line.endswith('/')
|
|
88
|
+
if directory:line=line[:-1]
|
|
89
|
+
anchored=line.startswith('/')
|
|
90
|
+
if anchored:line=line[1:]
|
|
91
|
+
if not line:continue
|
|
92
|
+
if len(line)>MAX_PATTERN or len(line.split('/'))>64 or line.count('*')+line.count('?')>32 or '\x00' in line or '[[:' in line:
|
|
93
|
+
diagnostic='unsupported_gitignore_pattern';continue
|
|
94
|
+
basename='/' not in line and not anchored
|
|
95
|
+
pattern=tuple(_unescape(x) for x in line.split('/'))
|
|
96
|
+
rules.append(IgnoreRule(base,pattern,basename,directory,negate,source,line_no))
|
|
97
|
+
if len(rules)>=MAX_RULES:diagnostic='gitignore_rule_limit';break
|
|
98
|
+
return rules,diagnostic
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def ignore_decision(relative,is_dir,rules):
|
|
102
|
+
ignored=False;last=None
|
|
103
|
+
for rule in rules:
|
|
104
|
+
if rule.matches(relative,is_dir):ignored=not rule.negate;last=rule
|
|
105
|
+
return ignored,({'path':last.source,'line':last.line} if ignored and last else None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ignore_evidence(raw, relative):
|
|
109
|
+
return {'path':relative,'sha256':hashlib.sha256(raw).hexdigest()}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Optional, data-only book-informed review questions. Never scored detectors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib
|
|
4
|
+
from importlib import resources
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_knowledge():
|
|
12
|
+
data=json.loads(resources.files('aqaf').joinpath('data/book_review_knowledge.json').read_text(encoding='utf8'))
|
|
13
|
+
return validate_knowledge(data)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_knowledge(data):
|
|
17
|
+
if data.get('schema_version')!=1:raise ValueError('Unsupported review knowledge version')
|
|
18
|
+
books={b['id']:b for b in data['books']}
|
|
19
|
+
if len(books)!=len(data['books']):raise ValueError('Duplicate knowledge book')
|
|
20
|
+
routes=set()
|
|
21
|
+
for b in books.values():
|
|
22
|
+
if not b.get('edition') or not b.get('review_summary') or not b.get('review_contribution') or not b.get('reading_references'):
|
|
23
|
+
raise ValueError('Missing book reading metadata')
|
|
24
|
+
expected='/books/'+b['id']+'/'
|
|
25
|
+
if b.get('website_path')!=expected or not re.fullmatch(r'/books/[a-z0-9-]+/',expected):raise ValueError('Invalid book page')
|
|
26
|
+
for ref in b['reading_references']:
|
|
27
|
+
if ref.get('kind') not in {'chapter','topic','pattern'} or not ref.get('title') or ref.get('edition')!=b['edition']:
|
|
28
|
+
raise ValueError('Invalid reading reference')
|
|
29
|
+
if ref.get('number') is not None and not re.fullmatch(r'[1-9][0-9]*',str(ref['number'])):raise ValueError('Invalid reference number')
|
|
30
|
+
url=ref.get('source_url','');parsed=urlsplit(url)
|
|
31
|
+
if parsed.scheme!='https' or not parsed.hostname or parsed.username is not None or parsed.password is not None or any(c.isspace() or c=='\\' for c in url):raise ValueError('Invalid reference URL')
|
|
32
|
+
if ref.get('verification_status')!='verified_primary' or not re.fullmatch(r'\d{4}-\d{2}-\d{2}',ref.get('verified_on','')) or not ref.get('verification_basis'):
|
|
33
|
+
raise ValueError('Unverified reading reference')
|
|
34
|
+
ids=set()
|
|
35
|
+
for card in data['principles']:
|
|
36
|
+
if card['id'] in ids or not card['book_ids'] or not set(card['book_ids'])<=set(books):raise ValueError('Invalid knowledge references')
|
|
37
|
+
route=card.get('website_path','')
|
|
38
|
+
if not re.fullmatch(r'/learn/[a-z0-9-]+/',route) or route in routes:raise ValueError('Invalid guide page')
|
|
39
|
+
routes.add(route)
|
|
40
|
+
if not all(card.get(k) for k in ('explanation','example','counterexample','editorial_basis','example_basis')):raise ValueError('Incomplete guide')
|
|
41
|
+
ids.add(card['id'])
|
|
42
|
+
if set(card['applies_when'])!={'profiles','facets','languages'}:raise ValueError('Invalid knowledge applicability')
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def review_agenda(scope,profile,paths=None):
|
|
47
|
+
"""Ask context-sensitive questions, without interpreting absence as failure."""
|
|
48
|
+
from .facets import facets
|
|
49
|
+
allowed=set(Path(p) for p in paths) if paths is not None else set(scope.files)
|
|
50
|
+
records=[r for r in scope.records if r['path'] in allowed]
|
|
51
|
+
owners={r['owner'] for r in records}
|
|
52
|
+
context=facets(scope,owner=next(iter(owners)) if len(owners)==1 else None) if records else {'facts':[],'partial':False}
|
|
53
|
+
relative={r['relative'] for r in records}
|
|
54
|
+
facts=[f for f in context['facts'] if f['path'] in relative]
|
|
55
|
+
present={f['facet'] for f in facts};languages={r['language'] for r in records if r['language']!='other'}
|
|
56
|
+
# Evidence is deliberately bounded, contains no source bodies or default values.
|
|
57
|
+
samples=[]
|
|
58
|
+
for role in ({'production','source','unknown','tooling'},{'docs'},{'tests'}):
|
|
59
|
+
candidates=[r for r in records if r['role'] in role]
|
|
60
|
+
for r in candidates[:2]:
|
|
61
|
+
raw=scope.raw(r['path'],8192)
|
|
62
|
+
samples.append({'path':r['relative'],'role':r['role'],'sha256':hashlib.sha256(raw).hexdigest(),
|
|
63
|
+
'bytes':len(raw),'complete_file':len(raw)==r['size'],'state':'observed_inventory_only'})
|
|
64
|
+
data=load_knowledge();books={b['id']:b for b in data['books']};questions=[]
|
|
65
|
+
for card in data['principles']:
|
|
66
|
+
when=card['applies_when'];basis=[]
|
|
67
|
+
if not any(when.values()):basis.append({'kind':'general_review','claim':'Applicability and adequacy require a reviewer; not a missing-feature finding.'})
|
|
68
|
+
if profile in when['profiles']:basis.append({'kind':'profile_hint','profile':profile,'confirmed':False})
|
|
69
|
+
for lang in sorted(languages & set(when['languages'])):basis.append({'kind':'language_presence','language':lang})
|
|
70
|
+
for f in facts:
|
|
71
|
+
if f['facet'] in when['facets']:basis.append({'kind':'inferred_static_facet',**f})
|
|
72
|
+
if not basis:continue
|
|
73
|
+
questions.append({k:card[k] for k in ('id','title','question','required_evidence','verification','limitations','explanation')} | {
|
|
74
|
+
'kind':'manual_verification','state':'unknown','confirmed':False,'runtime_verified':False,'mandatory':False,
|
|
75
|
+
'guide_url':'https://aqaf.focsd.com'+card['website_path'],
|
|
76
|
+
'applicability_basis':basis,'sources':[{'source_id':books[b]['source_id'],'book_id':b,'title':books[b]['title'],
|
|
77
|
+
'url':books[b]['primary_url'],'book_url':'https://aqaf.focsd.com'+books[b]['website_path'],
|
|
78
|
+
'edition':books[b]['edition'],'contribution':books[b]['review_contribution'],
|
|
79
|
+
'reading_references':books[b]['reading_references'],'basis':'secondary_application_notes','notes_path':books[b]['notes_path'],
|
|
80
|
+
'notes_sha256':books[b]['notes_sha256'],'full_book_reviewed':False} for b in card['book_ids']],
|
|
81
|
+
'completion_record':{'reviewer':None,'date':None,'evidence':[],'result':'unknown','not_applicable_reason':None}})
|
|
82
|
+
return {'schema_version':1,'knowledge_version':data['knowledge_version'],'kind':'book_informed_review_agenda','score_effect':'none',
|
|
83
|
+
'profile_hint':profile,'languages':sorted(languages),'questions':questions,'inventory_evidence':samples,
|
|
84
|
+
'context_sampling_partial':context.get('partial',False),'target_code_executed':False,
|
|
85
|
+
'limitations':data['limitations']+['Profile/facet hints select questions; they do not establish a product contract or a defect.',
|
|
86
|
+
'No repository commands, Git history, tests, network checks or hardware actions are executed.',
|
|
87
|
+
'Receiver validation, authentication, runtime behavior and architecture conformance need independent evidence.']}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def markdown_agenda(agenda):
|
|
91
|
+
"""Render current and earlier additive agenda records without online lookups."""
|
|
92
|
+
lines=['## Book-informed review agenda','','Optional human verification questions; unknown until reviewed. No score effect. Application guidance uses secondary notes; selected reading-reference facts were independently checked. These references do not validate detectors.','']
|
|
93
|
+
for q in agenda['questions']:
|
|
94
|
+
lines.extend(['### '+q['id']+' — '+q['title'],q['question']])
|
|
95
|
+
if q.get('explanation'):lines.extend(['',q['explanation']])
|
|
96
|
+
if q.get('guide_url'):lines.extend(['','[Why this matters and how to review it]('+q['guide_url']+')'])
|
|
97
|
+
lines.extend(['','- Evidence to request: '+q['required_evidence'],
|
|
98
|
+
'- Suggested verification (not executed): '+q['verification'],'- Limit: '+q['limitations'],
|
|
99
|
+
'- State: unknown; assign a reviewer and date or explain why not applicable.','', 'Further reading:'])
|
|
100
|
+
for source in q['sources']:
|
|
101
|
+
edition=' — '+source['edition'] if source.get('edition') else ''
|
|
102
|
+
contribution='. '+source['contribution'] if source.get('contribution') else ''
|
|
103
|
+
lines.extend(['- ['+source['title']+']('+source.get('book_url',source['url'])+')'+edition+contribution,
|
|
104
|
+
' Primary resource: ['+source['title']+']('+source['url']+').'])
|
|
105
|
+
for ref in source.get('reading_references',[]):
|
|
106
|
+
label=ref['kind'].capitalize()+(' '+ref['number'] if ref['number'] is not None else '')+': '+ref['title']
|
|
107
|
+
lines.append(' - ['+label+']('+ref['source_url']+') — '+ref['edition']+'; verified '+ref['verified_on']+' (title/number/edition only).')
|
|
108
|
+
lines.append(' Source ID: '+source['source_id']+'. Full book not reviewed.')
|
|
109
|
+
lines.append('')
|
|
110
|
+
return lines
|