@guanlin4924/devflow-pi 0.1.79 → 0.1.81
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/package.json +1 -1
- package/plugins/devflow-intel/extensions/devflow-intel.ts +52 -3
- package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +14 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +311 -60
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +10 -2
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/domain_registry.py +168 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/hybrid_query.py +367 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/source_content.py +88 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/writers/sqlite_writer.py +5 -2
- package/src/constants.ts +1 -1
- package/src/initialization-store.ts +1 -0
- package/src/project-initializer.ts +8 -3
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Append-only project scoped mapping bundles, separate from code snapshots.
|
|
2
|
+
|
|
3
|
+
Approval import is an explicit local reviewer attestation, not an authorization service.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
SCHEMA = '''
|
|
10
|
+
CREATE TABLE IF NOT EXISTS domain_mapping_revisions(
|
|
11
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,code_generation TEXT NOT NULL,
|
|
12
|
+
requirement_revision TEXT,status TEXT NOT NULL,previous_revision TEXT,bundle_json TEXT NOT NULL,
|
|
13
|
+
created_at TEXT NOT NULL,PRIMARY KEY(project_id,mapping_revision));
|
|
14
|
+
CREATE TABLE IF NOT EXISTS domain_mapping_current(project_id TEXT PRIMARY KEY,current_revision TEXT NOT NULL);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS domain_term_versions(
|
|
16
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,term_id TEXT NOT NULL,canonical_name TEXT NOT NULL,
|
|
17
|
+
PRIMARY KEY(project_id,mapping_revision,term_id));
|
|
18
|
+
CREATE TABLE IF NOT EXISTS domain_term_alias_versions(
|
|
19
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,term_id TEXT NOT NULL,alias TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY(project_id,mapping_revision,term_id,alias));
|
|
21
|
+
CREATE TABLE IF NOT EXISTS domain_feature_versions(
|
|
22
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,feature_id TEXT NOT NULL,canonical_name TEXT NOT NULL,
|
|
23
|
+
PRIMARY KEY(project_id,mapping_revision,feature_id));
|
|
24
|
+
CREATE TABLE IF NOT EXISTS domain_feature_terms(
|
|
25
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,feature_id TEXT NOT NULL,term_id TEXT NOT NULL,
|
|
26
|
+
PRIMARY KEY(project_id,mapping_revision,feature_id,term_id));
|
|
27
|
+
CREATE TABLE IF NOT EXISTS domain_feature_bindings(
|
|
28
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,feature_id TEXT NOT NULL,entity_id TEXT NOT NULL,
|
|
29
|
+
entity_version_id TEXT NOT NULL,code_generation TEXT NOT NULL,
|
|
30
|
+
PRIMARY KEY(project_id,mapping_revision,feature_id,entity_version_id));
|
|
31
|
+
CREATE TABLE IF NOT EXISTS domain_mapping_decisions(
|
|
32
|
+
project_id TEXT NOT NULL,mapping_revision TEXT NOT NULL,reviewer TEXT NOT NULL,reason TEXT NOT NULL,
|
|
33
|
+
requirement_revision TEXT NOT NULL,decision TEXT NOT NULL,PRIMARY KEY(project_id,mapping_revision));
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def import_bundle(path, project, bundle, expected, code_conn, generation):
|
|
38
|
+
if not project or len(project) > 200:
|
|
39
|
+
raise ValueError('project_id required (max 200 characters)')
|
|
40
|
+
if not isinstance(bundle, dict) or len(json.dumps(bundle)) > 2_000_000:
|
|
41
|
+
raise ValueError('invalid or oversized mapping bundle')
|
|
42
|
+
revision = bundle.get('mapping_revision')
|
|
43
|
+
if not isinstance(revision, str) or not revision or len(revision) > 200:
|
|
44
|
+
raise ValueError('mapping_revision required (max 200 characters)')
|
|
45
|
+
if bundle.get('code_generation') != generation:
|
|
46
|
+
raise ValueError('mapping code_generation mismatch')
|
|
47
|
+
status = bundle.get('status', 'candidate')
|
|
48
|
+
if status not in {'candidate', 'pending_review', 'approved', 'rejected', 'historical_unmapped'}:
|
|
49
|
+
raise ValueError('invalid mapping lifecycle status')
|
|
50
|
+
decision = bundle.get('decision', {})
|
|
51
|
+
if status in {'approved', 'rejected'} and not all(isinstance(v, str) and v.strip() for v in [
|
|
52
|
+
decision.get('reviewer'), decision.get('reason'), bundle.get('requirement_revision')]):
|
|
53
|
+
raise ValueError('review decision requires reviewer, reason and requirement_revision')
|
|
54
|
+
terms = bundle.get('terms', [])
|
|
55
|
+
features = bundle.get('features', [])
|
|
56
|
+
if not terms or not features or len(terms) + len(features) > 1000:
|
|
57
|
+
raise ValueError('bundle requires 1..1000 terms and features')
|
|
58
|
+
term_ids = {term['id'] for term in terms}
|
|
59
|
+
feature_ids = {feature['id'] for feature in features}
|
|
60
|
+
if len(term_ids) != len(terms) or len(feature_ids) != len(features):
|
|
61
|
+
raise ValueError('duplicate term or feature id')
|
|
62
|
+
for feature in features:
|
|
63
|
+
if not set(feature.get('term_ids', [])).issubset(term_ids):
|
|
64
|
+
raise ValueError('unknown term reference')
|
|
65
|
+
for binding in feature.get('bindings', []):
|
|
66
|
+
row = code_conn.execute('SELECT entity_version_id,content_hash FROM entity_versions WHERE snapshot_id=? AND logical_id=?', (generation, binding['entity_id'])).fetchone()
|
|
67
|
+
if row is None or binding.get('entity_version_id') != row[0] or binding.get('content_hash') != row[1]:
|
|
68
|
+
raise ValueError('binding entity version or content hash mismatch')
|
|
69
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
conn = sqlite3.connect(path)
|
|
71
|
+
try:
|
|
72
|
+
conn.executescript(SCHEMA)
|
|
73
|
+
if 'decision' not in {row[1] for row in conn.execute('PRAGMA table_info(domain_mapping_decisions)')}:
|
|
74
|
+
conn.execute("ALTER TABLE domain_mapping_decisions ADD COLUMN decision TEXT NOT NULL DEFAULT 'approved'")
|
|
75
|
+
immutable_tables = ['domain_mapping_revisions', 'domain_term_versions', 'domain_term_alias_versions', 'domain_feature_versions', 'domain_feature_terms', 'domain_feature_bindings', 'domain_mapping_decisions']
|
|
76
|
+
for table in immutable_tables:
|
|
77
|
+
for operation in ['UPDATE','DELETE']:
|
|
78
|
+
conn.execute(f"CREATE TRIGGER IF NOT EXISTS immutable_{table}_{operation} BEFORE {operation} ON {table} BEGIN SELECT RAISE(ABORT, 'mapping revisions are append-only'); END")
|
|
79
|
+
revision_children = ['domain_term_versions', 'domain_term_alias_versions', 'domain_feature_versions', 'domain_feature_terms', 'domain_feature_bindings', 'domain_mapping_decisions']
|
|
80
|
+
for table in revision_children:
|
|
81
|
+
conn.execute(f'''CREATE TRIGGER IF NOT EXISTS sealed_{table}_insert BEFORE INSERT ON {table}
|
|
82
|
+
WHEN EXISTS(SELECT 1 FROM domain_mapping_revisions r
|
|
83
|
+
WHERE r.project_id=NEW.project_id AND r.mapping_revision=NEW.mapping_revision)
|
|
84
|
+
BEGIN SELECT RAISE(ABORT, 'mapping revision is sealed'); END''')
|
|
85
|
+
for operation in ['INSERT','UPDATE']:
|
|
86
|
+
conn.execute(f'''CREATE TRIGGER IF NOT EXISTS approved_current_{operation} BEFORE {operation} ON domain_mapping_current
|
|
87
|
+
WHEN NOT EXISTS(SELECT 1 FROM domain_mapping_revisions r JOIN domain_mapping_decisions d
|
|
88
|
+
ON d.project_id=r.project_id AND d.mapping_revision=r.mapping_revision
|
|
89
|
+
WHERE r.project_id=NEW.project_id AND r.mapping_revision=NEW.current_revision
|
|
90
|
+
AND r.status='approved' AND d.decision='approved' AND length(d.reviewer)>0 AND length(d.reason)>0)
|
|
91
|
+
BEGIN SELECT RAISE(ABORT, 'current must reference an approved revision with decision'); END''')
|
|
92
|
+
conn.commit()
|
|
93
|
+
conn.execute('BEGIN IMMEDIATE')
|
|
94
|
+
current = conn.execute('SELECT current_revision FROM domain_mapping_current WHERE project_id=?', (project,)).fetchone()
|
|
95
|
+
if (current[0] if current else None) != expected:
|
|
96
|
+
raise ValueError('mapping current CAS conflict')
|
|
97
|
+
previous = bundle.get('previous_revision')
|
|
98
|
+
if previous:
|
|
99
|
+
prior = conn.execute('SELECT status,bundle_json FROM domain_mapping_revisions WHERE project_id=? AND mapping_revision=?', (project,previous)).fetchone()
|
|
100
|
+
if prior is None:
|
|
101
|
+
raise ValueError('previous mapping revision does not exist in project')
|
|
102
|
+
transitions = {'candidate': {'candidate','pending_review','rejected','historical_unmapped'}, 'pending_review': {'approved','rejected','candidate'}, 'approved': {'candidate','historical_unmapped'}, 'rejected': {'candidate'}, 'historical_unmapped': {'candidate'}}
|
|
103
|
+
if status not in transitions[prior[0]]:
|
|
104
|
+
raise ValueError('invalid mapping status transition')
|
|
105
|
+
if status == 'approved':
|
|
106
|
+
prior_bundle = json.loads(prior[1])
|
|
107
|
+
for field in ['terms','features','code_generation','requirement_revision']:
|
|
108
|
+
if bundle.get(field) != prior_bundle.get(field):
|
|
109
|
+
raise ValueError('approved mapping differs from pending review bundle')
|
|
110
|
+
elif status not in {'candidate','historical_unmapped'}:
|
|
111
|
+
raise ValueError('review states require a previous mapping revision')
|
|
112
|
+
for term in terms:
|
|
113
|
+
conn.execute('INSERT INTO domain_term_versions VALUES(?,?,?,?)', (project,revision,term['id'],term['name']))
|
|
114
|
+
for alias in set(term.get('aliases', [])):
|
|
115
|
+
conn.execute('INSERT INTO domain_term_alias_versions VALUES(?,?,?,?)', (project,revision,term['id'],alias))
|
|
116
|
+
for feature in features:
|
|
117
|
+
conn.execute('INSERT INTO domain_feature_versions VALUES(?,?,?,?)', (project,revision,feature['id'],feature['name']))
|
|
118
|
+
for term in feature.get('term_ids', []):
|
|
119
|
+
conn.execute('INSERT INTO domain_feature_terms VALUES(?,?,?,?)', (project,revision,feature['id'],term))
|
|
120
|
+
for binding in feature.get('bindings', []):
|
|
121
|
+
conn.execute('INSERT INTO domain_feature_bindings VALUES(?,?,?,?,?,?)', (project,revision,feature['id'],binding['entity_id'],binding['entity_version_id'],generation))
|
|
122
|
+
if status in {'approved','rejected'}:
|
|
123
|
+
conn.execute('INSERT INTO domain_mapping_decisions VALUES(?,?,?,?,?,?)', (project,revision,decision['reviewer'],decision['reason'],bundle['requirement_revision'],status))
|
|
124
|
+
# The revision row is the seal: child rows are staged first in this
|
|
125
|
+
# transaction and no child INSERT is allowed after the seal exists.
|
|
126
|
+
conn.execute('INSERT INTO domain_mapping_revisions VALUES(?,?,?,?,?,?,?,?)', (
|
|
127
|
+
project,revision,generation,bundle.get('requirement_revision'),status,previous,
|
|
128
|
+
json.dumps(bundle, ensure_ascii=False, sort_keys=True),datetime.now(timezone.utc).isoformat()))
|
|
129
|
+
if status == 'approved':
|
|
130
|
+
conn.execute('INSERT INTO domain_mapping_current VALUES(?,?) ON CONFLICT(project_id) DO UPDATE SET current_revision=excluded.current_revision', (project,revision))
|
|
131
|
+
conn.commit()
|
|
132
|
+
finally:
|
|
133
|
+
conn.close()
|
|
134
|
+
return {'mapping_revision': revision, 'status': status, 'project_id': project}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def resolve(path, project, terms, generation, revision='approved-current', statuses=None, requirement=None):
|
|
138
|
+
if not path.is_file():
|
|
139
|
+
return {'status': 'capability_unavailable', 'mappings': [], 'mapping_revision': None}
|
|
140
|
+
statuses = statuses or ['approved']
|
|
141
|
+
if not set(statuses).issubset({'approved', 'candidate', 'pending_review', 'rejected', 'historical_unmapped'}):
|
|
142
|
+
raise ValueError('invalid mapping statusFilter')
|
|
143
|
+
conn = sqlite3.connect(path.as_uri() + '?mode=ro', uri=True)
|
|
144
|
+
conn.row_factory = sqlite3.Row
|
|
145
|
+
try:
|
|
146
|
+
conn.execute('BEGIN')
|
|
147
|
+
if revision == 'approved-current':
|
|
148
|
+
row = conn.execute('SELECT current_revision FROM domain_mapping_current WHERE project_id=?', (project,)).fetchone()
|
|
149
|
+
revision = row[0] if row else None
|
|
150
|
+
row = conn.execute('SELECT * FROM domain_mapping_revisions WHERE project_id=? AND mapping_revision=?', (project,revision)).fetchone()
|
|
151
|
+
result = {'status': 'no_domain_match', 'mapping_revision': revision, 'mappings': [], 'requirement_revision': row['requirement_revision'] if row else None}
|
|
152
|
+
if row is None or row['status'] not in statuses:
|
|
153
|
+
return result
|
|
154
|
+
if requirement and requirement != row['requirement_revision']:
|
|
155
|
+
raise ValueError('requirement revision mismatch')
|
|
156
|
+
if row['code_generation'] != generation:
|
|
157
|
+
result['status'] = 'mapping_snapshot_mismatch'
|
|
158
|
+
return result
|
|
159
|
+
bundle = json.loads(row['bundle_json'])
|
|
160
|
+
requested = {term.casefold() for term in terms}
|
|
161
|
+
matched = {term['id'] for term in bundle['terms'] if requested.intersection(value.casefold() for value in [term['name'], *term.get('aliases', [])])}
|
|
162
|
+
result['mappings'] = [{**feature, 'mapping_status': row['status'], 'mapping_revision': revision, 'code_generation': generation,
|
|
163
|
+
'provenance': {'source_type': 'reviewer_attestation' if row['status'] == 'approved' else 'candidate_import', 'decision': bundle.get('decision')}}
|
|
164
|
+
for feature in bundle['features'] if matched.intersection(feature.get('term_ids', []))]
|
|
165
|
+
result['status'] = 'ambiguous' if len(result['mappings']) > 1 else 'ok' if result['mappings'] else 'no_domain_match'
|
|
166
|
+
return result
|
|
167
|
+
finally:
|
|
168
|
+
conn.close()
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Bounded structured query executor; callers never supply SQL."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import sqlite3
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from adapters.capabilities import confidence_for_parser
|
|
8
|
+
from source_content import verify_span
|
|
9
|
+
from domain_registry import resolve
|
|
10
|
+
|
|
11
|
+
PHASES = ['resolve_domain', 'search_symbols', 'search_content', 'rank_seeds', 'traverse_relations', 'impact_change', 'verify_evidence', 'compose_result']
|
|
12
|
+
FIELDS = {'codeGeneration','mappingRevision','requirementRevision','featureTerms','identifiers','repositoryFilter','pathPrefix','phases','operations','seedEntityIds','changeType','coveragePolicy','direction','depth','limit','statusFilter','includeHeuristic','language','matchKind'}
|
|
13
|
+
CHANGES = {'remove_field','rename_field','change_type','optional_to_required','required_to_optional','change_nullability','change_enum'}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_plan(plan):
|
|
17
|
+
if not isinstance(plan, dict) or set(plan) - FIELDS:
|
|
18
|
+
raise ValueError('QueryPlan contains unknown fields')
|
|
19
|
+
plan = dict(plan)
|
|
20
|
+
phases = plan.get('phases', plan.get('operations', ['resolve_domain', 'search_symbols', 'search_content', 'rank_seeds', 'verify_evidence', 'compose_result']))
|
|
21
|
+
if not isinstance(phases, list) or any(p not in PHASES for p in phases) or len(phases) != len(set(phases)):
|
|
22
|
+
raise ValueError('invalid query phases')
|
|
23
|
+
plan['phases'] = [p for p in PHASES if p in phases]
|
|
24
|
+
for key in ['featureTerms', 'identifiers', 'seedEntityIds', 'repositoryFilter', 'statusFilter']:
|
|
25
|
+
value = plan.get(key, ['approved'] if key == 'statusFilter' else [])
|
|
26
|
+
maximum = 16 if key != 'seedEntityIds' else 20
|
|
27
|
+
if not isinstance(value, list) or len(value) > maximum or any(not isinstance(v, str) or not v or len(v) > (1000 if key == 'seedEntityIds' else 256) or '\x00' in v for v in value):
|
|
28
|
+
raise ValueError('invalid or oversized ' + key)
|
|
29
|
+
plan[key] = value
|
|
30
|
+
for key, default, maximum in [('limit', 20, 100), ('depth', 2, 5)]:
|
|
31
|
+
value = plan.get(key, default)
|
|
32
|
+
if type(value) is not int or not 0 <= value <= maximum or key == 'limit' and value == 0:
|
|
33
|
+
raise ValueError('invalid ' + key)
|
|
34
|
+
plan[key] = value
|
|
35
|
+
prefix = plan.get('pathPrefix', '')
|
|
36
|
+
if not isinstance(prefix, str) or len(prefix) > 1000 or '\\' in prefix or '\x00' in prefix or PurePosixPath(prefix).is_absolute() or '..' in PurePosixPath(prefix).parts:
|
|
37
|
+
raise ValueError('pathPrefix must be a repository-relative path without traversal')
|
|
38
|
+
plan['pathPrefix'] = prefix.strip('/')
|
|
39
|
+
if plan.get('direction', 'outgoing') not in {'incoming','outgoing','both'}:
|
|
40
|
+
raise ValueError('invalid direction')
|
|
41
|
+
if plan.get('changeType') is not None and plan['changeType'] not in CHANGES:
|
|
42
|
+
raise ValueError('invalid changeType')
|
|
43
|
+
if 'impact_change' in phases and plan.get('changeType') not in CHANGES:
|
|
44
|
+
raise ValueError('impact_change requires changeType')
|
|
45
|
+
if plan.get('coveragePolicy', 'local_only') != 'local_only':
|
|
46
|
+
raise ValueError('only local_only coverage is supported')
|
|
47
|
+
if plan.get('matchKind', 'literal') not in {'literal','substring','identifier','exact_phrase'}:
|
|
48
|
+
raise ValueError('invalid matchKind')
|
|
49
|
+
if plan.get('language') is not None and (not isinstance(plan['language'], str) or len(plan['language']) > 80):
|
|
50
|
+
raise ValueError('invalid language')
|
|
51
|
+
if not set(plan['statusFilter']).issubset({'approved','candidate','pending_review','rejected','historical_unmapped'}):
|
|
52
|
+
raise ValueError('invalid statusFilter')
|
|
53
|
+
search = bool({'search_symbols','search_content'}.intersection(phases))
|
|
54
|
+
domain = 'resolve_domain' in phases
|
|
55
|
+
has_seed = bool(plan['seedEntityIds'])
|
|
56
|
+
if not phases:
|
|
57
|
+
raise ValueError('QueryPlan requires at least one phase')
|
|
58
|
+
if 'rank_seeds' in phases and not (search or domain or has_seed):
|
|
59
|
+
raise ValueError('rank_seeds requires search, domain resolution or explicit seeds')
|
|
60
|
+
if {'traverse_relations','impact_change'}.intersection(phases) and not (has_seed or domain or (search and 'rank_seeds' in phases)):
|
|
61
|
+
raise ValueError('traversal requires explicit seeds, resolve_domain, or search + rank_seeds')
|
|
62
|
+
if 'verify_evidence' in phases and not (search or domain or {'traverse_relations','impact_change'}.intersection(phases)):
|
|
63
|
+
raise ValueError('verify_evidence requires a result-producing phase')
|
|
64
|
+
if phases == ['compose_result']:
|
|
65
|
+
raise ValueError('compose_result requires a result-producing phase')
|
|
66
|
+
return plan
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read_connection(path):
|
|
70
|
+
conn = sqlite3.connect(path.as_uri() + '?mode=ro&immutable=1', uri=True)
|
|
71
|
+
conn.row_factory = sqlite3.Row
|
|
72
|
+
conn.execute('PRAGMA query_only=ON')
|
|
73
|
+
deadline = time.monotonic() + 3
|
|
74
|
+
conn.set_progress_handler(lambda: int(time.monotonic() > deadline), 1000)
|
|
75
|
+
conn.execute('BEGIN')
|
|
76
|
+
return conn
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _scope(plan, snapshot):
|
|
80
|
+
scope = snapshot.get('workspace', {}).get('scanScope', {})
|
|
81
|
+
repos = scope.get('codebases', [])
|
|
82
|
+
known = {r['name']: r for r in repos}
|
|
83
|
+
if plan['repositoryFilter'] and (not known or not set(plan['repositoryFilter']).issubset(known)):
|
|
84
|
+
raise ValueError('repositoryFilter is outside snapshot scan scope')
|
|
85
|
+
prefixes = []
|
|
86
|
+
selected = plan['repositoryFilter'] or ([r['name'] for r in repos] if plan['pathPrefix'] else [])
|
|
87
|
+
for name in selected:
|
|
88
|
+
base = known[name].get('workspacePath', '')
|
|
89
|
+
prefix = '/'.join(p for p in [base.strip('/'), plan['pathPrefix']] if p and p != '.')
|
|
90
|
+
prefixes.append((name, prefix))
|
|
91
|
+
if not prefixes:
|
|
92
|
+
prefixes = [(None, plan['pathPrefix'])]
|
|
93
|
+
return prefixes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _where(plan, prefixes, alias='s'):
|
|
97
|
+
parts, args = [], []
|
|
98
|
+
for repository, prefix in prefixes:
|
|
99
|
+
p, values = [], []
|
|
100
|
+
if repository:
|
|
101
|
+
p.append(f'{alias}.codebase=?'); values.append(repository)
|
|
102
|
+
if prefix:
|
|
103
|
+
p.append(f'({alias}.path=? OR substr({alias}.path,1,?)=?)')
|
|
104
|
+
values.extend([prefix, len(prefix) + 1, prefix + '/'])
|
|
105
|
+
parts.append('(' + (' AND '.join(p) or '1') + ')'); args.extend(values)
|
|
106
|
+
where = '(' + ' OR '.join(parts) + ')'
|
|
107
|
+
if plan.get('language'):
|
|
108
|
+
where += f' AND {alias}.language=?'; args.append(plan['language'])
|
|
109
|
+
return where, args
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _decorate(item, generation, kind):
|
|
113
|
+
item['snapshot'] = generation
|
|
114
|
+
item['entity_id'] = (item.get('id') or item.get('entity_id')) if kind == 'symbol' else item.get('entity_id')
|
|
115
|
+
item['entity_version_id'] = generation + ':' + item['entity_id'] if item.get('entity_id') else None
|
|
116
|
+
item['parser'] = item.get('parser') or 'unknown'
|
|
117
|
+
item['confidence'] = item.get('confidence') or ('exact' if kind == 'content' else confidence_for_parser(item['parser'], 'symbol'))
|
|
118
|
+
item['result_type'] = kind
|
|
119
|
+
item['provenance'] = {'relation_parser': item.get('relation_parser'), 'source_file_parser': item.get('source_file_parser', item['parser']), 'source': 'snapshot_source' if kind == 'content' else 'code_index', 'snapshot': generation, 'parser': item['parser'], 'content_hash': item.get('content_hash')}
|
|
120
|
+
return item
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def execute(conn, snapshot, plan, registry_path, project):
|
|
124
|
+
plan = validate_plan(plan)
|
|
125
|
+
generation = snapshot['generation']
|
|
126
|
+
phases = plan['phases']; limit = plan['limit']; prefixes = _scope(plan, snapshot)
|
|
127
|
+
tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
128
|
+
terms = list(dict.fromkeys([*plan['featureTerms'], *plan['identifiers']]))
|
|
129
|
+
conn.create_function('identifier_match', 2, lambda content, term: int(bool(re.search(r'(?<!\w)' + re.escape(term) + r'(?!\w)', content))), deterministic=True)
|
|
130
|
+
results, phase_status, seeds = {}, {}, set(plan['seedEntityIds'])
|
|
131
|
+
truncated = False
|
|
132
|
+
mappings = {'mapping_revision': None, 'requirement_revision': None, 'mappings': []}
|
|
133
|
+
if 'resolve_domain' in phases:
|
|
134
|
+
if not project:
|
|
135
|
+
raise ValueError('resolve_domain requires project_id')
|
|
136
|
+
mappings = resolve(registry_path, project, terms, generation, plan.get('mappingRevision', 'approved-current'), plan['statusFilter'], plan.get('requirementRevision'))
|
|
137
|
+
phase_status['resolve_domain'] = mappings['status']
|
|
138
|
+
if mappings['status'] == 'ok':
|
|
139
|
+
for feature in mappings['mappings']:
|
|
140
|
+
# Candidate mapping is visible but never silently becomes a traversal seed.
|
|
141
|
+
if feature['mapping_status'] == 'approved':
|
|
142
|
+
seeds.update(binding['entity_id'] for binding in feature.get('bindings', []))
|
|
143
|
+
where, scope_args = _where(plan, prefixes)
|
|
144
|
+
for seed in sorted(seeds)[:20]:
|
|
145
|
+
row = conn.execute(f'SELECT s.* FROM symbols s WHERE {where} AND s.id=?', (*scope_args,seed)).fetchone()
|
|
146
|
+
if row and any(seed == binding['entity_id'] for feature in mappings['mappings'] for binding in feature.get('bindings', [])):
|
|
147
|
+
item = _decorate(dict(row), generation, 'symbol')
|
|
148
|
+
item.update(match_kind='approved_domain_binding', score=100, matched_terms=terms, score_contributions=[{'kind':'approved_domain_binding','score':100,'reason':'approved domain feature bound to this entity version'}])
|
|
149
|
+
results[('symbol',item['id'])] = item
|
|
150
|
+
if 'search_symbols' in phases:
|
|
151
|
+
count = 0
|
|
152
|
+
for term in terms:
|
|
153
|
+
rows = conn.execute(f'''SELECT s.* FROM symbols s WHERE {where}
|
|
154
|
+
AND (instr(lower(s.name),lower(?))>0 OR instr(lower(s.qualified_name),lower(?))>0 OR instr(lower(s.path),lower(?))>0)
|
|
155
|
+
ORDER BY CASE WHEN lower(s.name)=lower(?) THEN 0
|
|
156
|
+
WHEN instr(lower(s.name),lower(?))>0 OR instr(lower(s.qualified_name),lower(?))>0 THEN 1
|
|
157
|
+
ELSE 2 END, s.path,s.line_start,s.id LIMIT ?''',
|
|
158
|
+
(*scope_args,term,term,term,term,term,term,limit + 1)).fetchall()
|
|
159
|
+
truncated |= len(rows) > limit
|
|
160
|
+
for row in rows[:limit]:
|
|
161
|
+
item = _decorate(dict(row), generation, 'symbol')
|
|
162
|
+
if item['name'].casefold() == term.casefold() and term in plan['identifiers']:
|
|
163
|
+
kind, score = 'identifier_exact', 90
|
|
164
|
+
elif item['name'].casefold() == term.casefold():
|
|
165
|
+
kind, score = 'symbol_exact', 80
|
|
166
|
+
elif term.casefold() in item['name'].casefold() or term.casefold() in (item.get('qualified_name') or '').casefold():
|
|
167
|
+
kind, score = 'symbol_substring', 60
|
|
168
|
+
else:
|
|
169
|
+
kind, score = 'path_match', 50
|
|
170
|
+
item['match_kind'] = kind
|
|
171
|
+
item['score'] = score
|
|
172
|
+
item['score_contributions'] = [{'kind':kind,'term':term,'score':score,'reason':'literal match against indexed symbol or path'}]
|
|
173
|
+
item['matched_terms'] = [term]
|
|
174
|
+
key = ('symbol', item['id'])
|
|
175
|
+
if key in results:
|
|
176
|
+
results[key]['matched_terms'].append(term)
|
|
177
|
+
results[key]['score'] = max(results[key]['score'], item['score'])
|
|
178
|
+
results[key]['score_contributions'].extend(item['score_contributions'])
|
|
179
|
+
else:
|
|
180
|
+
results[key] = item
|
|
181
|
+
count += 1
|
|
182
|
+
phase_status['search_symbols'] = 'ok' if count else 'no_symbol_match'
|
|
183
|
+
if 'search_content' in phases:
|
|
184
|
+
if 'source_content_chunks' not in tables:
|
|
185
|
+
phase_status['search_content'] = 'capability_unavailable'
|
|
186
|
+
else:
|
|
187
|
+
count = 0
|
|
188
|
+
for term in terms:
|
|
189
|
+
fts = 'source_content_fts' in tables and len(term) >= 3
|
|
190
|
+
match = ' AND c.rowid IN (SELECT rowid FROM source_content_fts WHERE source_content_fts MATCH ?)' if fts else ''
|
|
191
|
+
identifier = ' AND identifier_match(c.content,?)=1' if plan.get('matchKind') == 'identifier' else ''
|
|
192
|
+
params = [*scope_args, term]
|
|
193
|
+
if identifier:
|
|
194
|
+
params.append(term)
|
|
195
|
+
if fts:
|
|
196
|
+
params.append('"' + term.replace('"', '""') + '"')
|
|
197
|
+
rows = conn.execute(f'SELECT c.*,s.codebase,s.parser FROM source_content_chunks c JOIN code_files s ON s.id=c.code_file_id WHERE {where} AND instr(c.content,?)>0{identifier}{match} ORDER BY c.path,c.chunk_no LIMIT ?', (*params,limit + 1)).fetchall()
|
|
198
|
+
truncated |= len(rows) > limit
|
|
199
|
+
for row in rows[:limit]:
|
|
200
|
+
item = dict(row)
|
|
201
|
+
position = re.search(r'(?<!\w)' + re.escape(term) + r'(?!\w)', item['content']).start() if identifier else item['content'].find(term)
|
|
202
|
+
before = item['content'][:position]
|
|
203
|
+
item['line_start'] += before.count('\n')
|
|
204
|
+
item['line_end'] = item['line_start'] + term.count('\n')
|
|
205
|
+
item['matchedText'] = term
|
|
206
|
+
item.pop('content', None)
|
|
207
|
+
owner = conn.execute('''SELECT id FROM symbols
|
|
208
|
+
WHERE codebase=? AND path=? AND line_start<=? AND line_end>=? AND kind<>?
|
|
209
|
+
ORDER BY line_end-line_start,id LIMIT 1''',
|
|
210
|
+
(item['codebase'],item['path'],item['line_start'],item['line_start'],'file')).fetchone()
|
|
211
|
+
if owner:
|
|
212
|
+
item['entity_id'] = owner[0]
|
|
213
|
+
else:
|
|
214
|
+
# File-level content (constants/configuration outside a
|
|
215
|
+
# symbol) still needs a stable snapshot-bound identity.
|
|
216
|
+
item['entity_id'] = item['code_file_id']
|
|
217
|
+
item = _decorate(item, generation, 'content')
|
|
218
|
+
item['match_kind'] = plan.get('matchKind', 'literal')
|
|
219
|
+
item['score'] = 40
|
|
220
|
+
item['score_contributions'] = [{'kind':'content_' + item['match_kind'],'term':term,'score':40,'reason':'literal occurrence in immutable source chunk'}]
|
|
221
|
+
results[('content', item['chunk_id'], term)] = item
|
|
222
|
+
count += 1
|
|
223
|
+
phase_status['search_content'] = 'ok' if count else 'no_content_match'
|
|
224
|
+
ranked = sorted(results.values(), key=lambda x: (-x['score'], x['path'], x['line_start']))
|
|
225
|
+
if 'rank_seeds' in phases:
|
|
226
|
+
seeds.update(item['id'] for item in ranked if item['result_type'] == 'symbol' and item.get('kind') != 'file')
|
|
227
|
+
for item in ranked:
|
|
228
|
+
if item['result_type'] == 'content':
|
|
229
|
+
rows = conn.execute('SELECT id FROM symbols WHERE codebase=? AND path=? AND line_start<=? AND line_end>=? AND kind<>? ORDER BY line_end-line_start,id LIMIT 2', (item['codebase'],item['path'],item['line_start'],item['line_start'],'file')).fetchall()
|
|
230
|
+
seeds.update(row[0] for row in rows)
|
|
231
|
+
# Validate and scope every explicit, domain, and traversal seed against the pinned index.
|
|
232
|
+
valid_seeds = []
|
|
233
|
+
for seed in sorted(seeds):
|
|
234
|
+
if 'impact_change' in phases and 'entity_versions' in tables:
|
|
235
|
+
row = conn.execute(f'SELECT e.logical_id FROM entity_versions e JOIN code_files s ON s.path=e.path AND s.codebase=e.codebase WHERE {where} AND e.logical_id=? AND e.snapshot_id=?', (*scope_args, seed, generation)).fetchone()
|
|
236
|
+
else:
|
|
237
|
+
row = conn.execute(f'SELECT s.id FROM symbols s WHERE {where} AND s.id=?', (*scope_args, seed)).fetchone()
|
|
238
|
+
if row:
|
|
239
|
+
valid_seeds.append(seed)
|
|
240
|
+
truncated |= len(valid_seeds) > 20
|
|
241
|
+
valid_seeds = valid_seeds[:20]
|
|
242
|
+
edges = []
|
|
243
|
+
for phase in ['traverse_relations','impact_change']:
|
|
244
|
+
if phase not in phases:
|
|
245
|
+
continue
|
|
246
|
+
table = 'relations' if phase == 'traverse_relations' else 'semantic_flows'
|
|
247
|
+
if table not in tables:
|
|
248
|
+
phase_status[phase] = 'capability_unavailable'; continue
|
|
249
|
+
if mappings.get('status') == 'ambiguous' and not plan['seedEntityIds']:
|
|
250
|
+
phase_status[phase] = 'ambiguous'; continue
|
|
251
|
+
frontier = list(valid_seeds); visited = set(frontier); seen_edges = set()
|
|
252
|
+
before_count = len(edges)
|
|
253
|
+
for depth in range(1, plan['depth'] + 1):
|
|
254
|
+
next_frontier = []
|
|
255
|
+
for seed in frontier:
|
|
256
|
+
source, target = ('from_id','to_id') if table == 'relations' else ('source_id','target_id')
|
|
257
|
+
direction = plan.get('direction','outgoing')
|
|
258
|
+
condition = f'{source}=?' if direction == 'outgoing' else f'{target}=?' if direction == 'incoming' else f'({source}=? OR {target}=?)'
|
|
259
|
+
args = [seed,seed] if direction == 'both' else [seed]
|
|
260
|
+
if not plan.get('includeHeuristic', False):
|
|
261
|
+
condition += " AND confidence <> 'heuristic'"
|
|
262
|
+
rows = conn.execute(f'SELECT rowid AS _edge_rowid,* FROM {table} WHERE {condition} ORDER BY rowid LIMIT 102', args).fetchall()
|
|
263
|
+
truncated |= len(rows) > 100
|
|
264
|
+
for row in rows[:100]:
|
|
265
|
+
item = dict(row)
|
|
266
|
+
key = (table, item.pop('_edge_rowid'))
|
|
267
|
+
if key in seen_edges:
|
|
268
|
+
continue
|
|
269
|
+
seen_edges.add(key)
|
|
270
|
+
path = item.get('evidence_path') or item.get('source_path')
|
|
271
|
+
if table == 'relations':
|
|
272
|
+
owner = conn.execute('SELECT codebase FROM symbols WHERE id=?', (item[source],)).fetchone()
|
|
273
|
+
else:
|
|
274
|
+
owner = conn.execute('SELECT codebase FROM entity_versions WHERE snapshot_id=? AND logical_id=?', (generation,item[source])).fetchone()
|
|
275
|
+
if not owner:
|
|
276
|
+
continue
|
|
277
|
+
file = conn.execute(f'SELECT s.parser,s.content_hash FROM code_files s WHERE {where} AND s.codebase=? AND s.path=?', (*scope_args,owner[0],path)).fetchone()
|
|
278
|
+
if not file:
|
|
279
|
+
continue
|
|
280
|
+
item['relation_parser'] = item.get('parser') or item.get('extractor') or 'unknown'
|
|
281
|
+
item['source_file_parser'] = file['parser']
|
|
282
|
+
item['content_hash'] = file['content_hash']
|
|
283
|
+
item['parser'] = item['relation_parser']
|
|
284
|
+
item.update(path=path, depth=depth, line_start=item.get('evidence_line') or item.get('line_start'), result_type=phase)
|
|
285
|
+
item = _decorate(item, generation, phase)
|
|
286
|
+
if phase == 'impact_change':
|
|
287
|
+
item['impact_kind'] = plan['changeType']
|
|
288
|
+
edges.append(item)
|
|
289
|
+
node = item[target] if item[source] == seed else item[source]
|
|
290
|
+
if node and node not in visited:
|
|
291
|
+
if table == 'relations':
|
|
292
|
+
scoped_node = conn.execute(f'SELECT s.id FROM symbols s WHERE {where} AND s.id=?', (*scope_args,node)).fetchone()
|
|
293
|
+
else:
|
|
294
|
+
scoped_node = conn.execute(f'SELECT e.logical_id FROM entity_versions e JOIN code_files s ON s.path=e.path AND s.codebase=e.codebase WHERE {where} AND e.logical_id=? AND e.snapshot_id=?', (*scope_args,node,generation)).fetchone()
|
|
295
|
+
if scoped_node:
|
|
296
|
+
visited.add(node); next_frontier.append(node)
|
|
297
|
+
if len(edges) >= limit:
|
|
298
|
+
truncated = True; break
|
|
299
|
+
if len(edges) >= limit:
|
|
300
|
+
break
|
|
301
|
+
frontier = next_frontier
|
|
302
|
+
if not frontier or len(edges) >= limit:
|
|
303
|
+
break
|
|
304
|
+
phase_status[phase] = 'ok' if len(edges) > before_count else 'no_index_evidence'
|
|
305
|
+
if 'verify_evidence' in phases:
|
|
306
|
+
for item in [*ranked, *edges]:
|
|
307
|
+
if 'source_artifacts' in tables:
|
|
308
|
+
verify_span(conn, item)
|
|
309
|
+
else:
|
|
310
|
+
item['evidence_status'] = 'evidence_unavailable'
|
|
311
|
+
raw_combined = [*ranked, *edges]
|
|
312
|
+
combined, by_entity = [], {}
|
|
313
|
+
for item in raw_combined:
|
|
314
|
+
version = item.get('entity_version_id')
|
|
315
|
+
if not version:
|
|
316
|
+
combined.append(item)
|
|
317
|
+
continue
|
|
318
|
+
evidence = {key: item.get(key) for key in ('result_type','match_kind','path','line_start','line_end','matchedText','content_hash','score','evidence_status') if item.get(key) is not None}
|
|
319
|
+
if version not in by_entity:
|
|
320
|
+
item['evidence_sources'] = [evidence]
|
|
321
|
+
by_entity[version] = item
|
|
322
|
+
combined.append(item)
|
|
323
|
+
continue
|
|
324
|
+
primary = by_entity[version]
|
|
325
|
+
primary['evidence_sources'].append(evidence)
|
|
326
|
+
primary['score_contributions'] = [*primary.get('score_contributions', []), *item.get('score_contributions', [])]
|
|
327
|
+
primary['matched_terms'] = list(dict.fromkeys([*primary.get('matched_terms', []), *item.get('matched_terms', [])]))
|
|
328
|
+
if item.get('score', 0) > primary.get('score', 0):
|
|
329
|
+
position = combined.index(primary)
|
|
330
|
+
item['evidence_sources'] = primary['evidence_sources']
|
|
331
|
+
item['score_contributions'] = primary['score_contributions']
|
|
332
|
+
item['matched_terms'] = primary['matched_terms']
|
|
333
|
+
combined[position] = item
|
|
334
|
+
by_entity[version] = item
|
|
335
|
+
combined.sort(key=lambda x: (-x.get('score', 0), x.get('path') or '', x.get('line_start') or 0))
|
|
336
|
+
truncated |= len(combined) > limit
|
|
337
|
+
status = 'ok' if combined else 'no_index_evidence'
|
|
338
|
+
if ranked and all(r['result_type'] == 'content' for r in ranked) and not edges:
|
|
339
|
+
status = 'content_match_only'
|
|
340
|
+
if not combined and len(phase_status) == 1:
|
|
341
|
+
status = next(iter(phase_status.values()))
|
|
342
|
+
if any(value == 'capability_unavailable' for value in phase_status.values()):
|
|
343
|
+
status = 'partial' if combined else 'capability_unavailable'
|
|
344
|
+
if truncated:
|
|
345
|
+
status = 'partial'
|
|
346
|
+
if mappings.get('status') == 'ambiguous':
|
|
347
|
+
status = 'ambiguous'
|
|
348
|
+
if any(item.get('evidence_status') == 'evidence_unavailable' or any(source.get('evidence_status') == 'evidence_unavailable' for source in item.get('evidence_sources', [])) for item in combined[:limit]):
|
|
349
|
+
status = 'evidence_unavailable'
|
|
350
|
+
coverage = {'policy': 'local_only','status': 'not_assessed'}
|
|
351
|
+
if 'impact_change' in phases and 'coverage_metrics' in tables:
|
|
352
|
+
coverage_row = conn.execute('SELECT * FROM coverage_metrics WHERE snapshot_id=?', (generation,)).fetchone()
|
|
353
|
+
if coverage_row:
|
|
354
|
+
coverage = dict(coverage_row)
|
|
355
|
+
coverage['policy'] = 'local_only'
|
|
356
|
+
coverage['status'] = 'partial' if coverage.get('files_parsed', 0) < coverage.get('files_scanned', 0) or coverage.get('dynamic_limitations', 0) else 'complete'
|
|
357
|
+
if coverage['status'] == 'partial' and status == 'ok':
|
|
358
|
+
status = 'partial'
|
|
359
|
+
# Revision consistency outranks result/evidence availability: callers must
|
|
360
|
+
# never mistake code hits for facts from an incompatible mapping snapshot.
|
|
361
|
+
if mappings.get('status') == 'mapping_snapshot_mismatch':
|
|
362
|
+
status = 'mapping_snapshot_mismatch'
|
|
363
|
+
return {'results': combined[:limit], 'mappings': mappings['mappings'], '_meta': {
|
|
364
|
+
'status': status,'snapshot': generation,'mapping_revision': mappings.get('mapping_revision'),
|
|
365
|
+
'requirement_revision': mappings.get('requirement_revision'), 'scope_hash': snapshot.get('workspace', {}).get('scopeHash'),
|
|
366
|
+
'phases': phase_status,'seedEntityIds': valid_seeds,'truncated': truncated,'planSchemaVersion': 1,
|
|
367
|
+
'coverage': coverage, 'resolvedPlan': {**plan,'codeGeneration': generation,'mappingRevision': mappings.get('mapping_revision')}}}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Snapshot-owned source evidence. No query reads the live working tree."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def digest(data):
|
|
9
|
+
return 'sha256:' + hashlib.sha256(data).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_content(conn, root, files, generation, scope):
|
|
13
|
+
conn.executescript('''
|
|
14
|
+
CREATE TABLE snapshot_metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
15
|
+
CREATE TABLE source_artifacts(content_hash TEXT PRIMARY KEY, content BLOB NOT NULL);
|
|
16
|
+
CREATE TABLE source_content_chunks(
|
|
17
|
+
chunk_id TEXT PRIMARY KEY, code_generation TEXT NOT NULL, code_file_id TEXT NOT NULL,
|
|
18
|
+
path TEXT NOT NULL, language TEXT NOT NULL, chunk_no INTEGER NOT NULL,
|
|
19
|
+
line_start INTEGER NOT NULL, line_end INTEGER NOT NULL, byte_start INTEGER NOT NULL,
|
|
20
|
+
byte_end INTEGER NOT NULL, content_hash TEXT NOT NULL, content TEXT NOT NULL);
|
|
21
|
+
CREATE INDEX source_chunks_file ON source_content_chunks(code_file_id, line_start);
|
|
22
|
+
''')
|
|
23
|
+
mode = 'trigram'
|
|
24
|
+
try:
|
|
25
|
+
conn.execute("CREATE VIRTUAL TABLE source_content_fts USING fts5(content, content='source_content_chunks', content_rowid='rowid', tokenize='trigram')")
|
|
26
|
+
except sqlite3.OperationalError:
|
|
27
|
+
mode = 'bounded_literal_scan'
|
|
28
|
+
for record in files:
|
|
29
|
+
data = (Path(root) / record['path']).read_bytes()
|
|
30
|
+
if digest(data) != record['content_hash']:
|
|
31
|
+
raise ValueError('source changed during snapshot build: ' + record['path'])
|
|
32
|
+
conn.execute('INSERT OR IGNORE INTO source_artifacts VALUES(?,?)', (record['content_hash'], data))
|
|
33
|
+
lines = data.splitlines(keepends=True)
|
|
34
|
+
offset = 0
|
|
35
|
+
for number, start in enumerate(range(0, len(lines), 80)):
|
|
36
|
+
chunk = b''.join(lines[start:start + 80])
|
|
37
|
+
conn.execute('INSERT INTO source_content_chunks VALUES(?,?,?,?,?,?,?,?,?,?,?,?)', (
|
|
38
|
+
generation + ':' + record['id'] + ':' + str(number), generation, record['id'],
|
|
39
|
+
record['path'], record['language'], number, start + 1, min(start + 80, len(lines)),
|
|
40
|
+
offset, offset + len(chunk), record['content_hash'], chunk.decode('utf-8', errors='replace')))
|
|
41
|
+
offset += len(chunk)
|
|
42
|
+
if mode == 'trigram':
|
|
43
|
+
conn.execute("INSERT INTO source_content_fts(source_content_fts) VALUES('rebuild')")
|
|
44
|
+
conn.execute("INSERT INTO source_content_fts(source_content_fts, rank) VALUES('integrity-check', 1)")
|
|
45
|
+
capability = {'version': 1, 'tokenizer': mode, 'shortTerms': 'bounded_literal_scan', 'historicalEvidence': True}
|
|
46
|
+
conn.executemany('INSERT INTO snapshot_metadata VALUES(?,?)', [
|
|
47
|
+
('scanScope', json.dumps(scope, sort_keys=True)), ('contentSearch', json.dumps(capability))])
|
|
48
|
+
conn.commit() # Flush FTS pending segments before fingerprinting its immutable shadow tables.
|
|
49
|
+
conn.execute('INSERT INTO snapshot_metadata VALUES(?,?)', ('contentIntegrity', json.dumps(content_integrity(conn), sort_keys=True)))
|
|
50
|
+
return capability
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_span(conn, item):
|
|
54
|
+
row = conn.execute('SELECT content FROM source_artifacts WHERE content_hash=?', (item.get('content_hash'),)).fetchone()
|
|
55
|
+
if row is None or digest(row[0]) != item.get('content_hash'):
|
|
56
|
+
item['evidence_status'] = 'evidence_unavailable'
|
|
57
|
+
return item
|
|
58
|
+
lines = row[0].decode('utf-8', errors='replace').splitlines()
|
|
59
|
+
start = max(1, item.get('line_start') or item.get('evidence_line') or 1)
|
|
60
|
+
end = min(item.get('line_end') or start, start + 19)
|
|
61
|
+
item['evidence'] = '\n'.join(lines[start - 1:end])[:4000]
|
|
62
|
+
item['evidence_status'] = 'verified'
|
|
63
|
+
return item
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def content_integrity(conn):
|
|
67
|
+
"""Fingerprint the build-time integrity-checked source/FTS projection without writes."""
|
|
68
|
+
tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
69
|
+
checks = {}
|
|
70
|
+
for table in ['source_content_chunks', 'source_content_fts_data', 'source_content_fts_idx', 'source_content_fts_docsize', 'source_content_fts_config']:
|
|
71
|
+
if table not in tables:
|
|
72
|
+
continue
|
|
73
|
+
checksum = hashlib.sha256()
|
|
74
|
+
count = 0
|
|
75
|
+
for row in conn.execute(f'SELECT * FROM {table} ORDER BY 1,2'):
|
|
76
|
+
for value in row:
|
|
77
|
+
data = value if isinstance(value, bytes) else str(value).encode('utf-8')
|
|
78
|
+
checksum.update(len(data).to_bytes(8, 'big')); checksum.update(data)
|
|
79
|
+
count += 1
|
|
80
|
+
checks[table] = {'count': count, 'sha256': checksum.hexdigest()}
|
|
81
|
+
rowids = hashlib.sha256()
|
|
82
|
+
for row in conn.execute('SELECT rowid FROM source_content_chunks ORDER BY rowid'):
|
|
83
|
+
rowids.update(str(row[0]).encode() + b'\0')
|
|
84
|
+
checks['chunk_rowids'] = rowids.hexdigest()
|
|
85
|
+
if 'source_content_fts_docsize' in tables:
|
|
86
|
+
invalid = conn.execute('SELECT count(*) FROM source_content_chunks c LEFT JOIN source_content_fts_docsize f ON f.id=c.rowid WHERE f.id IS NULL').fetchone()[0]
|
|
87
|
+
checks['missing_fts_rowids'] = invalid
|
|
88
|
+
return checks
|