instruction-drift 0.1.0__py3-none-any.whl
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.
- instruction_drift/__init__.py +3 -0
- instruction_drift/__main__.py +3 -0
- instruction_drift/audit.py +118 -0
- instruction_drift/cli.py +33 -0
- instruction_drift/extract.py +52 -0
- instruction_drift-0.1.0.dist-info/METADATA +110 -0
- instruction_drift-0.1.0.dist-info/RECORD +11 -0
- instruction_drift-0.1.0.dist-info/WHEEL +5 -0
- instruction_drift-0.1.0.dist-info/entry_points.txt +2 -0
- instruction_drift-0.1.0.dist-info/licenses/LICENSE +21 -0
- instruction_drift-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Deterministic, offline comparison of lexical claims with disk contents."""
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from fnmatch import fnmatchcase
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from .extract import extract
|
|
8
|
+
|
|
9
|
+
DEFAULT_FILES = ('AGENTS.md', 'CLAUDE.md', 'MUSE.md', 'CONTRIBUTING.md')
|
|
10
|
+
SKIP = {'node_modules', 'dist', '__pycache__', 'venv'}
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Finding:
|
|
14
|
+
file: str
|
|
15
|
+
line: int
|
|
16
|
+
kind: str
|
|
17
|
+
claim: str
|
|
18
|
+
severity: str
|
|
19
|
+
detail: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def imports(root, excluded):
|
|
23
|
+
identifiers = set()
|
|
24
|
+
for directory, dirs, files in os.walk(root):
|
|
25
|
+
dirs[:] = sorted(d for d in dirs if not d.startswith('.') and d not in SKIP
|
|
26
|
+
and not (Path(directory) / d).is_symlink())
|
|
27
|
+
for name in sorted(files):
|
|
28
|
+
path = Path(directory) / name
|
|
29
|
+
if path.resolve() in excluded or path.suffix not in ('.py', '.js', '.ts'):
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
content = path.read_text(encoding='utf-8')
|
|
33
|
+
except (OSError, UnicodeError):
|
|
34
|
+
continue
|
|
35
|
+
if path.suffix == '.py':
|
|
36
|
+
for match in re.finditer(r'^\s*(?:from\s+([\w.]+)\s+import\b|import\s+([^\n;#]+))', content, re.M):
|
|
37
|
+
if match.group(1):
|
|
38
|
+
identifiers.add(match.group(1))
|
|
39
|
+
else:
|
|
40
|
+
identifiers.update(part.strip().split()[0] for part in
|
|
41
|
+
match.group(2).split(',') if part.strip())
|
|
42
|
+
else:
|
|
43
|
+
identifiers.update(re.findall(r'''(?:require\(\s*|\bfrom\s*)['"]([^'"]+)['"]''', content))
|
|
44
|
+
return identifiers
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def audit(repo='.', files=None, ignore=(), fail_on='high'):
|
|
48
|
+
root = Path(repo).resolve()
|
|
49
|
+
names = list(DEFAULT_FILES if files is None else files)
|
|
50
|
+
report = dict(files_read=[], findings=[], ignored=0, parse_errors=[], truncated=False)
|
|
51
|
+
if not root.is_dir():
|
|
52
|
+
report['parse_errors'].append({'file': str(repo), 'detail': 'Repository is not a directory'})
|
|
53
|
+
excluded = {(root / name).resolve() for name in names}
|
|
54
|
+
used = imports(root, excluded) if root.is_dir() else set()
|
|
55
|
+
targets = None
|
|
56
|
+
seen = set()
|
|
57
|
+
threshold_hit = False
|
|
58
|
+
for name in names if root.is_dir() else []:
|
|
59
|
+
path = root / name
|
|
60
|
+
if files is None and not path.exists():
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
content = path.read_text(encoding='utf-8')
|
|
64
|
+
except (OSError, UnicodeError) as error:
|
|
65
|
+
report['parse_errors'].append({'file': name, 'detail': str(error)})
|
|
66
|
+
continue
|
|
67
|
+
report['files_read'].append(name)
|
|
68
|
+
for item in extract(content):
|
|
69
|
+
key = (name, item.line, item.kind, item.claim)
|
|
70
|
+
if key in seen:
|
|
71
|
+
continue
|
|
72
|
+
seen.add(key)
|
|
73
|
+
if any(fnmatchcase(item.claim, pattern) for pattern in ignore):
|
|
74
|
+
report['ignored'] += 1
|
|
75
|
+
continue
|
|
76
|
+
severity, detail = 'HIGH', ''
|
|
77
|
+
if item.kind == 'prohibition-contradiction':
|
|
78
|
+
if item.claim not in used:
|
|
79
|
+
continue
|
|
80
|
+
severity = 'MEDIUM'
|
|
81
|
+
detail = 'Identifier appears in source imports; verify manually.'
|
|
82
|
+
elif item.claim.startswith('make '):
|
|
83
|
+
makefile = root / 'Makefile'
|
|
84
|
+
if not makefile.exists():
|
|
85
|
+
continue
|
|
86
|
+
if targets is None:
|
|
87
|
+
try:
|
|
88
|
+
targets = set(re.findall(r'^([A-Za-z0-9_.-]+)\s*:(?!=)', makefile.read_text(encoding='utf-8'), re.M))
|
|
89
|
+
except (OSError, UnicodeError) as error:
|
|
90
|
+
report['parse_errors'].append({'file': 'Makefile', 'detail': str(error)})
|
|
91
|
+
targets = set()
|
|
92
|
+
if item.claim[5:] in targets:
|
|
93
|
+
continue
|
|
94
|
+
detail = 'Target not found in root Makefile.'
|
|
95
|
+
else:
|
|
96
|
+
candidate = root / item.claim
|
|
97
|
+
try:
|
|
98
|
+
candidate.resolve().relative_to(root)
|
|
99
|
+
except ValueError:
|
|
100
|
+
continue
|
|
101
|
+
exists = candidate.is_dir() if item.claim.endswith('/') else candidate.exists()
|
|
102
|
+
if exists:
|
|
103
|
+
continue
|
|
104
|
+
detail = 'Referenced path does not exist' + (' as a directory.' if item.claim.endswith('/') else '.')
|
|
105
|
+
threshold_hit = threshold_hit or severity == 'HIGH' or fail_on == 'medium'
|
|
106
|
+
if len(report['findings']) < 500:
|
|
107
|
+
report['findings'].append(asdict(Finding(name, item.line, item.kind, item.claim, severity, detail)))
|
|
108
|
+
else:
|
|
109
|
+
report['truncated'] = True
|
|
110
|
+
findings = report['findings']
|
|
111
|
+
report['totals'] = {'kind': {k: sum(f['kind'] == k for f in findings) for k in
|
|
112
|
+
('referenced-file', 'script-in-command', 'prohibition-contradiction')},
|
|
113
|
+
'severity': {s: sum(f['severity'] == s for f in findings) for s in ('HIGH', 'MEDIUM')}}
|
|
114
|
+
failed = report['parse_errors'] or threshold_hit
|
|
115
|
+
report['exit_code'] = 2 if failed else (1 if findings else 0)
|
|
116
|
+
report['verdict'] = {0: 'PASS', 1: 'WARN', 2: 'FAIL'}[report['exit_code']]
|
|
117
|
+
report['parse_error_count'] = len(report['parse_errors'])
|
|
118
|
+
return report
|
instruction_drift/cli.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Command-line interface; commands mentioned in instructions are never run."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from . import __version__
|
|
5
|
+
from .audit import audit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv=None):
|
|
9
|
+
parser = argparse.ArgumentParser(prog='instruction-drift', description='Check repository instruction claims offline.')
|
|
10
|
+
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
|
|
11
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
12
|
+
check = sub.add_parser('audit', help='Audit instruction claims against repository files')
|
|
13
|
+
check.add_argument('repo', nargs='?', default='.')
|
|
14
|
+
check.add_argument('--files', nargs='+', action='extend')
|
|
15
|
+
check.add_argument('--ignore', nargs='+', action='extend', default=[])
|
|
16
|
+
check.add_argument('--json', action='store_true')
|
|
17
|
+
check.add_argument('--fail-on', choices=('high', 'medium'), default='high')
|
|
18
|
+
args = parser.parse_args(argv)
|
|
19
|
+
report = audit(args.repo, args.files, args.ignore, args.fail_on)
|
|
20
|
+
if args.json:
|
|
21
|
+
print(json.dumps(report, indent=2))
|
|
22
|
+
else:
|
|
23
|
+
print('Instruction files read: ' + (', '.join(report['files_read']) or 'none (no instruction files read)'))
|
|
24
|
+
for finding in report['findings']:
|
|
25
|
+
print('{file}:{line}: {severity} {kind}: {claim} — {detail}'.format(**finding))
|
|
26
|
+
for error in report['parse_errors']:
|
|
27
|
+
print('{file}: parse error: {detail}'.format(**error))
|
|
28
|
+
print('Totals: ' + json.dumps(report['totals'], sort_keys=True))
|
|
29
|
+
print('Ignored: {}; parse errors: {}'.format(report['ignored'], report['parse_error_count']))
|
|
30
|
+
if report['truncated']:
|
|
31
|
+
print('Report truncated at 500 findings.')
|
|
32
|
+
print(report['verdict'])
|
|
33
|
+
return report['exit_code']
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Small, deliberately lexical instruction-claim extractors."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
EXTENSIONS = ('.py', '.js', '.ts', '.md', '.json', '.yaml', '.yml',
|
|
6
|
+
'.toml', '.sh', '.go', '.rs', '.txt')
|
|
7
|
+
ROOTS = ('src/', 'tests/', 'docs/', 'examples/', 'scripts/', 'bin/')
|
|
8
|
+
TOKEN = re.compile(r"[^\s`\"'<>|;()]+")
|
|
9
|
+
BACKTICK = re.compile(r'`([^`]+)`')
|
|
10
|
+
PROHIBITION = re.compile(
|
|
11
|
+
r'\b(?:do not use|never use|must not use|avoid using)\s+`?([\w@][\w.\-@]*)',
|
|
12
|
+
re.IGNORECASE)
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Claim:
|
|
16
|
+
line: int
|
|
17
|
+
kind: str
|
|
18
|
+
claim: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def path_token(token):
|
|
22
|
+
"""Return a normalized path claim, or None for unsupported tokens."""
|
|
23
|
+
token = token.lstrip(',:!?[]{}').rstrip('.,:!?[]{}')
|
|
24
|
+
if '*' in token or '://' in token:
|
|
25
|
+
return None
|
|
26
|
+
bare = token[2:] if token.startswith('./') else token
|
|
27
|
+
if '/' in bare and (bare.endswith(EXTENSIONS) or bare.startswith(ROOTS)):
|
|
28
|
+
return token
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract(text):
|
|
33
|
+
"""Extract claims in line order; command paths have a single kind."""
|
|
34
|
+
result = []
|
|
35
|
+
for number, line in enumerate(text.splitlines(), 1):
|
|
36
|
+
spans = list(BACKTICK.finditer(line))
|
|
37
|
+
for match in TOKEN.finditer(line):
|
|
38
|
+
path = path_token(match.group())
|
|
39
|
+
if path:
|
|
40
|
+
command = any(s.start(1) <= match.start() < s.end(1)
|
|
41
|
+
and (len(s.group(1).split()) > 1 or s.group(1).startswith('./'))
|
|
42
|
+
for s in spans)
|
|
43
|
+
result.append(Claim(number, 'script-in-command' if command else
|
|
44
|
+
'referenced-file', path))
|
|
45
|
+
for span in spans:
|
|
46
|
+
words = span.group(1).split()
|
|
47
|
+
if len(words) >= 2 and words[0] == 'make' and not words[1].startswith('-'):
|
|
48
|
+
result.append(Claim(number, 'script-in-command', 'make ' + words[1]))
|
|
49
|
+
for match in PROHIBITION.finditer(line):
|
|
50
|
+
result.append(Claim(number, 'prohibition-contradiction',
|
|
51
|
+
match.group(1).rstrip('.')))
|
|
52
|
+
return list(dict.fromkeys(result))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: instruction-drift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline heuristic checks for stale repository instructions
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# instruction-drift
|
|
12
|
+
|
|
13
|
+
An offline, deterministic, stdlib-only Python CLI that checks concrete instruction
|
|
14
|
+
claims against repository files. A finding means **check this claim**. It does
|
|
15
|
+
not establish that an instruction is wrong. No daemon, LLM, network, or keys.
|
|
16
|
+
|
|
17
|
+
Requires Python 3.9+. Install a locally built wheel with `pip install
|
|
18
|
+
/path/to/instruction_drift-0.1.0-py3-none-any.whl`.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
instruction-drift audit .
|
|
22
|
+
instruction-drift audit examples/sample-repo --json
|
|
23
|
+
instruction-drift audit . --files AGENTS.md CONTRIBUTING.md --ignore 'docs/*'
|
|
24
|
+
instruction-drift audit . --fail-on medium
|
|
25
|
+
instruction-drift --version
|
|
26
|
+
instruction-drift --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The repository defaults to the current directory. Default instruction files,
|
|
30
|
+
in order, are root AGENTS.md, CLAUDE.md, MUSE.md, and CONTRIBUTING.md. Missing
|
|
31
|
+
default files are skipped; an empty repository passes with a note. `--files`
|
|
32
|
+
replaces that list with paths relative to the repository. Missing explicitly
|
|
33
|
+
requested files, unreadable files, and invalid UTF-8 produce parse errors.
|
|
34
|
+
`--files` and `--ignore` accept multiple arguments and can be repeated.
|
|
35
|
+
|
|
36
|
+
Checks are heuristic:
|
|
37
|
+
|
|
38
|
+
- **referenced-file (HIGH):** missing path tokens containing `/` and ending in
|
|
39
|
+
.py, .js, .ts, .md, .json, .yaml, .yml, .toml, .sh, .go, .rs, or .txt;
|
|
40
|
+
also directory references rooted under src/, tests/, docs/, examples/,
|
|
41
|
+
scripts/, or bin/. Trailing `/` requires a directory. Paths resolve relative
|
|
42
|
+
to the repository root. URLs, tokens containing `*`, and paths resolving
|
|
43
|
+
outside the repository are skipped.
|
|
44
|
+
- **script-in-command (HIGH):** missing path tokens in backticked commands;
|
|
45
|
+
missing `make target` targets when a root Makefile exists. Simple literal
|
|
46
|
+
target definitions are recognized; Makefile variables and comments are not
|
|
47
|
+
definitions. A command path is reported once rather than also as a reference.
|
|
48
|
+
- **prohibition-contradiction (MEDIUM):** “do not use”, “never use”, “must not
|
|
49
|
+
use”, or “avoid using” followed by an identifier also found in a Python
|
|
50
|
+
import/from import or JS/TS require/from import. Verify manually: exceptions
|
|
51
|
+
and test fixtures may be intentional. Instruction files are excluded from
|
|
52
|
+
source search. Hidden directories, .git, node_modules, dist, __pycache__,
|
|
53
|
+
.venv, and venv are skipped, as are symlinked directories.
|
|
54
|
+
|
|
55
|
+
The report includes files read, file:line locations, kinds, claims, severities,
|
|
56
|
+
details, totals by kind and severity, ignored count, parse-error count, and a
|
|
57
|
+
PASS/WARN/FAIL verdict. `--json` exposes the same information as valid JSON.
|
|
58
|
+
Claims matching `--ignore` glob patterns are excluded and counted before
|
|
59
|
+
checking their validity. Findings are deduplicated by file, line, kind, and
|
|
60
|
+
claim. At most 500 findings are retained in instruction-file and line order;
|
|
61
|
+
additional findings set the truncation flag. Totals describe retained findings. Failure thresholds still apply to findings
|
|
62
|
+
beyond the display cap.
|
|
63
|
+
|
|
64
|
+
Exit codes:
|
|
65
|
+
|
|
66
|
+
| Code | Meaning |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| 0 | PASS: no findings and no parse errors |
|
|
69
|
+
| 1 | WARN: findings exist, but none reach the failure threshold |
|
|
70
|
+
| 2 | FAIL: a finding reaches the threshold, or any parse error occurs |
|
|
71
|
+
|
|
72
|
+
The default threshold is HIGH; `--fail-on medium` also fails MEDIUM findings.
|
|
73
|
+
For example, the synthetic sample repo cites a renamed script and moved test
|
|
74
|
+
directory, and prohibits an identifier imported in src/app.py. Auditing it
|
|
75
|
+
returns two HIGH findings, one MEDIUM finding, and exit code 2. Its illustrative
|
|
76
|
+
import does not require an installed dependency because commands are never run.
|
|
77
|
+
|
|
78
|
+
## Limitations
|
|
79
|
+
|
|
80
|
+
This is pattern matching, not semantic judgement. Paraphrased claims are not
|
|
81
|
+
checked. A MEDIUM contradiction needs human review. Commands are not executed;
|
|
82
|
+
only path tokens and simple Makefile target names are checked. Dynamic imports,
|
|
83
|
+
complex shell syntax, generated paths, multiline imports, Makefile includes and
|
|
84
|
+
computed targets, and language-specific resolution are not interpreted.
|
|
85
|
+
Lexical import patterns may match comments or strings and may miss real imports.
|
|
86
|
+
Source files that cannot be decoded or read are skipped; this can hide import
|
|
87
|
+
matches. Instruction-file read failures always produce parse errors. A truncated
|
|
88
|
+
report is incomplete. Filesystem changes during a scan can affect its results.
|
|
89
|
+
|
|
90
|
+
## Differentiation
|
|
91
|
+
|
|
92
|
+
- **startup-trust-guard** audits TRUST: can an attacker plant or rewrite the
|
|
93
|
+
instruction file? instruction-drift audits TRUTH: is what it says still true
|
|
94
|
+
about the on-disk repository?
|
|
95
|
+
- **rules-doctor** audits rule-file health and structure. This tool cross-checks
|
|
96
|
+
instruction claims against the on-disk repo.
|
|
97
|
+
- **edit-guard** blocks edits made on stale reads. This tool finds stale
|
|
98
|
+
instructions that can cause those edits.
|
|
99
|
+
- **context-mode / heavy runtimes:** this is an offline stdlib CLI with no
|
|
100
|
+
daemon, no LLM, and no network.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
106
|
+
python3 -m build
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Building requires setuptools, wheel, and the build frontend already installed
|
|
110
|
+
for offline use. Runtime and tests use only the Python standard library.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
instruction_drift/__init__.py,sha256=U81StD9LNclhgLJ9bgtdeFUzA4XVRYGOmqgLTpxL3S0,74
|
|
2
|
+
instruction_drift/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
instruction_drift/audit.py,sha256=QYPE3oOOA0NTXto2cdOvoJJ3hxLMrmIulc1bV_v5Vkk,5316
|
|
4
|
+
instruction_drift/cli.py,sha256=4LsT_OHgZjO3fB4ddp_Upj6h95QA7BKAB0TXtRVCJTA,1745
|
|
5
|
+
instruction_drift/extract.py,sha256=TgMFTf_rQk_AQziMb47Yjwv0zJ-6yjgxnu1a8jqKLJQ,2105
|
|
6
|
+
instruction_drift-0.1.0.dist-info/licenses/LICENSE,sha256=BHibCVQHNQhBYTj-25q7SphGxQHnM5-dmo37W0YLcm8,1087
|
|
7
|
+
instruction_drift-0.1.0.dist-info/METADATA,sha256=FBT_t2D87L80K4EuwvgAJ5IpWexU8kzqch9rmjzFBiM,5338
|
|
8
|
+
instruction_drift-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
instruction_drift-0.1.0.dist-info/entry_points.txt,sha256=21cQWcFwc_6LI_eRPGTqjBD2lRcw2zvFdMVOuODEy2I,65
|
|
10
|
+
instruction_drift-0.1.0.dist-info/top_level.txt,sha256=Am-FIfrhTWa7BaRGUmYqhcgYx8SQa1FoGBsaa-4sAZs,18
|
|
11
|
+
instruction_drift-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 instruction-drift contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
instruction_drift
|