语言无关的代码验证协议规范
"任何声称都必须有可验证的实践锚点。"
Status: Working Draft. Components are at different maturity levels — see Maturity.
| Change | Trigger | Section |
|---|---|---|
Stub-based uninstall — zero-dependency removal via pract_stub.py |
photo_screener trial: "卸载后代码还能跑吗?" | 2. Uninstall Guarantee |
| Missing anchor severity layering — I/O functions get INFO, not WARNING | photo_screener trial: 21 warnings, many on load_and_preprocess etc. |
6.1 Severity Layering |
| Anchor registry interop — scanner recognizes out-of-line anchors | photo_screener trial: pract_anchors.py invisible to scanner |
6.2 Registry-Aware Scanning |
| @i_dont_know staleness — auto-escalate after 90 days | 待补充260607: 噪声即课题; unverified unknowns rot | 5.3 Staleness Detection |
| Change | Trigger | Section |
|---|---|---|
| Source provenance — @pt anchors MUST carry source field | RE Framework integration: anchors without trace provenance are unfalsifiable | 5.1 Test Anchor, 5.5 Source Provenance |
| Degraded verification — three operating modes for RE (full/partial/degraded) | RE Framework: lifted .cpp often can't compile standalone | 9. Degraded Verification |
| Uncompilable anchor state — honest declaration when code can't run | RE Framework: binary-internal deps prevent practify test | 5.4 Anchor Health States |
| Verify retry cap — max 3 Lift→Verify cycles before going back to A-layer | RE Framework: prevent process entropy in unfalsifiable loops | 9. Degraded Verification |
Practify is a three-layer code verification protocol for vibe coding workflows:
| Layer | What it does | When it runs | Maturity |
|---|---|---|---|
| Scanner | Detects defensive code patterns via AST analysis | Compile-time (static) | Verified — Python: 38 findings 0 FP. TypeScript: validated on test files. |
| Anchors | Binds verifiable tests to function declarations | Compile-time (declarative) + Runtime (validation) | Experimental — 18/18 tests passed in photo_screener trial. |
| Noise Cards | Accumulates runtime failures as structured knowledge | Runtime (continuous) | Unverified — schema defined, no project has accumulated >0 cards. |
Any claim must be convertible to a verifiable practice test, executable in finite steps with observable results. Otherwise it is invalid for the purpose of pursuing effectiveness.
The protocol does not prohibit. It demands proof.
- Traditional: "You cannot divide by zero." (defensive)
- Practify: "Prove the divisor is non-zero, or handle the zero case." (offensive)
The single allowed defense is i_dont_know — an honest declaration that opens the battlefield for practice feedback.
A protocol must not become a new form of technical debt.
The Practify Protocol guarantees that removing it from a project requires deleting at most two files and optionally one line per source file. Source files with residual anchor lines MUST continue to function correctly after removal.
Each practify-instrumented project contains a single stub file at the project root:
# pract_stub.py
# Generated by `practify init`. Delete this file to disable all anchors.
# Keep this file without practify installed: decorators degrade to no-ops.
try:
from practify import test as _pract_test, i_dont_know as _pract_idk
except ImportError:
# practify not installed — anchors silently become no-ops.
# Code continues to run without modification.
def _pract_test(description, test_fn):
return lambda f: f
def _pract_idk(what):
return lambda f: f
# Public names
test = _pract_test
i_dont_know = _pract_idkSource files import from the stub:
from pract_stub import test as pt, i_dont_know as idk
# @pract: anchors auto-degrade if pract_stub.py or practify/ is removed.
# @pt lines can remain in source — they become dead imports (harmless).
@pt("empty list", lambda: process([]) == [])
@idk("behavior with large files not yet verified")
def process(data):
...| State | pract_stub.py |
practify/ |
Behavior |
|---|---|---|---|
| Full | Yes | Yes | Anchors register and validate. practify test works. |
| Silent | Yes | No | Decorators are no-ops. Code runs. Anchors don't register. |
| Clean | No | No | from pract_stub import fails. Remove that line. Code is pristine. |
1. Delete pract_stub.py
2. Delete practify/ directory (or uninstall practify pip package)
3. (Optional) Remove `from pract_stub import ...` lines from source files.
These lines will cause ImportError if not removed, but:
- A single sed/grep fixes all files: grep -rl "pract_stub" . | xargs sed -i '/pract_stub/d'
- Or leave them — the ImportError is clean and explicit.No AST-level code rewriting is required. No scanning the entire project to strip decorators.
- Python: Decorator-based no-op (as above)
- TypeScript: JSDoc annotations (
/** @pract.test ... */) are comments — removing practify requires zero code changes. The annotations become inert documentation. - Rust: Proc macros expand to no-ops when the
practcrate is removed fromCargo.toml. - All languages: Anchors MUST be removable by deleting a single dependency declaration. Annotations in source MUST be inert when the dependency is absent.
(Unchanged from v0.1 — minor field clarifications)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://practify.dev/noise-card-v0.2.json",
"type": "object",
"required": ["noise_id", "timestamp", "trigger", "function_name", "observed", "expected"],
"properties": {
"noise_id": {
"type": "string",
"description": "Globally unique identifier"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 UTC timestamp of failure observation"
},
"trigger": {
"type": "string",
"description": "Exact input or condition that triggered the failure"
},
"function_name": {
"type": "string",
"description": "Fully qualified function name"
},
"observed": {
"type": "string",
"description": "What actually happened. Specific — stack traces, return values, error messages."
},
"expected": {
"type": "string",
"description": "What should have happened per the function's claimed behavior."
},
"anchor_violated": {
"type": "string",
"description": "Which anchor (test description) was violated, if any."
},
"discovery": {
"type": "string",
"description": "What new knowledge did this failure produce? What did we not know before?"
},
"curriculum": {
"type": "string",
"description": "Concise, reusable lesson for AI context injection. Must be actionable."
},
"converted_to_test": {
"type": "string",
"description": "Description of the regression test created from this noise card."
},
"resolved": {
"type": "boolean",
"default": false
},
"resolved_at": {
"type": "string",
"format": "date-time"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Classification tags"
},
"context_snippet": {
"type": "string",
"description": "Code snippet surrounding the failure point (max 500 chars)"
},
"language": {
"type": "string",
"description": "Origin language — enables cross-language curriculum"
}
}
}(Unchanged from v0.1)
| Property | Description |
|---|---|
| Purpose | A verifiable practice test bound to a function declaration |
| Semantics | "I claim this function behaves correctly under condition X, and here is a reproducible test." |
| Required fields | description (human-readable), test_fn (executable predicate returning boolean) |
| Required field (v0.3) | source (provenance string): MUST record where the test vector data came from — trace_id, register/memory snapshot, observation timestamp. See 5.5 Source Provenance. |
| Compile-time check | Every public function MUST have at least one test anchor OR one i_dont_know anchor |
| Runtime check | Test anchors SHOULD be executable via practify test. If code cannot be independently compiled (e.g., RE-lifted code with internal binary dependencies), see 9. Degraded Verification. |
| Property | Description |
|---|---|
| Purpose | Honest declaration of a cognitive boundary |
| Semantics | "This function has edge cases I haven't verified yet. I am actively inviting practice feedback." |
| Required fields | what (specific description of what is unknown) |
| Optional field (v0.3) | source (provenance string): for RE use cases, records what static analysis prompted this unknown — F5 output, missing trace coverage, etc. Helps distinguish "I don't know because I haven't looked" from "I looked and genuinely can't determine." |
| Difference from TODO | TODO = "I know what to do but haven't done it." I-don't-know = "I don't yet know what the correct behavior is." |
@i_dont_know anchors created more than 90 days ago, on functions that have been modified since the anchor was created, MUST be escalated:
- Scanner severity: INFO → WARNING
- Message: "This cognitive boundary was declared N days ago. Has sufficient practice data accumulated to convert it to a @pract.test?"
Implementations SHOULD record the creation date of each @i_dont_know anchor. The pract_stub.py-based approach records this in the anchor file itself.
| State | Condition | Meaning |
|---|---|---|
healthy |
All tests pass | Function's claimed behavior is verified |
unverified |
Only i_dont_know anchors, no tests | Exploration zone |
degrading |
Has tests but some fail | Previously verified behavior is now broken |
stale_unknown |
i_dont_know > 90 days, function modified | Cognitive boundary overdue for resolution |
skeleton |
No anchors at all | Violation of First Law |
uncompilable (v0.3) |
Has anchors but code cannot be independently compiled | Anchors carry source provenance but practify test cannot execute. Verification deferred to runtime trace comparison. Common in RE use cases. |
Each test anchor must answer: "Where did this data come from?"
Without source provenance, a test anchor is unfalsifiable — it could be derived from the same B1 hypothesis it claims to verify (circular reasoning), or fabricated entirely. Source provenance is the A-layer anchor point that breaks this circle.
Source string format:
source="<source_type>:<binary_or_file>!<function>#<id>, offset=<addr>, <key_observations> observed <ISO8601_timestamp>"Source types:
| Type | Meaning | Allowed for @pt | Allowed for @idk |
|---|---|---|---|
trace |
Dynamic debugging: register/memory snapshot from Frida/x64dbg/etc. | ✅ | ✅ |
memory |
Memory dump: extracted constant tables, vtables, string tables | ✅ | ✅ |
static |
Static analysis inference (F5 output, IDA disassembly, Ghidra decompiler) | ❌ | ✅ |
Rule: @pt MUST use trace or memory source. @pt with static source or without source → INVALID (rejected at review).
Rule: @idk MAY use static source — honestly stating "this unknown was inferred from static analysis, not observed in trace."
Examples:
# Valid @pt — trace-based source
@pt("volume=5 → eax=1",
lambda: speak(Animal(), 5) == 1,
source="trace:foo.dll!speak#002, offset=0x1A, edx=5 eax=1 observed 2026-06-18T10:00:05Z")
# Valid @idk — static-based source, honest about origin
@idk("volume=100 时是否会溢出?",
source="static:foo.dll!speak@0x1400077c0, F5 shows cmp edx,64h but trace never hit edx≥100")
# INVALID — @pt has no source
@pt("volume=5 → eax=1", lambda: speak(Animal(), 5) == 1) # ← rejectedImplementation note: The source parameter is a string. Implementations MAY validate its format but MUST preserve it verbatim. The pract_stub.py-based approach passes source as an additional keyword argument to the decorator; when practify is not installed, the stub silently discards it (the anchor degrades to no-op but the source string remains in source code for audit).
The missing-anchor pattern (P3) MUST be severity-layered based on function characteristics:
| Function Category | Severity | Rationale |
|---|---|---|
| Pure logic, no I/O calls | WARNING | High anchor value — easy to test, high regression risk |
Contains I/O calls (open, requests, Image.open, etc.) |
INFO | Low anchor value — test requires mocking or real resources. Suggest @i_dont_know. |
Name starts with test_ |
SKIP | Already a test function. Don't flag. |
Name starts with _ |
SKIP | Private/internal. Don't flag. |
I/O detection keywords (language-agnostic):
- File:
open,read,write,Path,fs,file - Network:
requests,fetch,http,curl,socket,connect - Image:
Image,PIL,imread,imwrite,decode,encode - Database:
execute,query,cursor,connect,collection
Functions matching ≥2 I/O keywords are classified as I/O-heavy.
The scanner MUST consult the runtime anchor registry before reporting missing-anchor.
When practify test is run, all decorated functions register their anchors. The scanner checks this registry:
- Function found in registry with ≥1 test anchor → do not report
- Function found in registry with only
i_dont_know→ do not report (it's in honest exploration state) - Function found in registry with no anchors → report MISSING_ANCHOR
This enables out-of-line anchor files (pract_anchors.py) — anchors registered on wrapper functions whose names follow the convention _anchor_{function_name} are associated with the target function.
| Property | Value |
|---|---|
| Severity | WARNING (pure logic) / INFO (I/O-heavy) / SKIP (test_ prefix or private) |
| Definition | A public function without a test anchor or i_dont_know declaration, and not found in the anchor registry |
| Cross-language | Same severity layering applies in all implementations |
(Unchanged from v0.1: Swallowed Exception, Bare Exception Handler, Defensive Null Propagation, Trivially True Test, Vague TODO)
| Level | Requirements |
|---|---|
| Level 1 — Scanner | Implements P1-P6 with severity layering (6.1) and registry awareness (6.2). CLI. |
| Level 2 — Anchors | Level 1 + stub-based anchor system (Section 2) with test/i_dont_know decorators. |
| Level 3 — Noise | Level 2 + noise card creation and AI context export. Staleness detection (5.3). |
| Level 4 — Full Protocol | Level 3 + runtime noise card accumulation integrated with test runner. |
| Component | Python | TypeScript | Maturity | Latest Evidence |
|---|---|---|---|---|
| Scanner | ✅ | ✅ | Verified | photo_screener: 38 findings, 0 FP |
| Anchors | ✅ | — | Experimental | photo_screener: 18/18 tests passed, 0 bugs found, 0 regressions |
| Source Provenance (v0.3) | — | — | Conjecture | Field defined in protocol. 0 RE projects have produced sourced anchors. |
| Noise Cards | ✅ | — | Unverified | 0 cards accumulated |
| AI Context | ✅ | — | Conjecture | 0 injection cycles run |
| Stub Uninstall | ✅ | — | Verified | Tested: delete stub + practify, code still runs via no-op fallback |
| Degraded Verification (v0.3) | — | — | Conjecture | Modes defined. No RE project has exercised Partial/Degraded paths. |
Not all anchored code can be independently compiled and run. This is not a failure of the protocol — it is an honest recognition that A-layer verification has material prerequisites. When those prerequisites are absent, the protocol MUST degrade gracefully rather than pretend.
| Mode | Condition | practify test | Confidence auto-promotion | Anchor source requirement |
|---|---|---|---|---|
| Full | Practify installed AND code self-contained (no unresolved external deps) | ✅ Runs | ✅ draft→candidate | @pt MUST have trace/memory source |
| Partial | Practify installed BUT code has unresolved dependencies (common in RE: lifted code depends on internal binary symbols) | ❌ Cannot run | ❌ Manual only | @pt MUST still carry source — the anchor serves as documented hypothesis until runtime verification becomes possible |
| Degraded | Practify not installed | ❌ N/A | ❌ Manual only | Source provenance still required in _anchors.py for audit trail |
Before practify test is invoked, the function MUST be classified:
Self-contained: No calls to external functions, no global variable references, no custom types from outside the translation unit. → Eligible for Full mode.
Has-deps: Calls other functions OR references global state OR uses custom types. → Check dependency resolution:
- All dependencies are themselves self-contained AND lifted → merge and compile → Full mode
- Any dependency is unresolved → Partial mode → record in
uncompilable_functions.yaml
# uncompilable_functions.yaml
- function: speak
source_location: "foo.dll:0x1400077c0"
uncompilable_reason: "depends on AudioDevice::write (0x140008000) and Animal::vftable"
unresolved_deps:
- type: function
name: AudioDevice::write
address: "foo.dll:0x140008000"
lift_status: not_started
- type: vtable
name: Animal::vftable
address: "foo.dll:0x140007000"
suggested_path: "Lift AudioDevice::write first, then retry speak compilation"
anchor_count: 4
anchor_sources_valid: true # all @pt have trace/memory sourceWhen Partial or Degraded mode is active, the Lift→Verify cycle has a hard cap of 3 attempts per function before the methodology forces a return to A-layer data collection (Scout phase / dynamic tracing).
Rationale: repeatedly tweaking B1 hypotheses (Lift code) without new A-layer data (traces) is the definition of process entropy (实践偏离:过程熵增). The cap breaks this cycle by refusing to let B1 iterate in isolation.
When operating in Partial or Degraded mode, tools and exports MUST prefix their output with:
"以下验证结果基于降级模式([部分/降级])。置信度未自动提升。所有 test anchor 均记录了数据来源,但尚未通过可公共观测的实践检验。confirmed 状态需要人工对照 trace 证据后手动授予。"
This is not defensive — it is an honest declaration of the current verification ceiling. The protocol remains useful: anchors document hypotheses with provenance, noise cards accumulate known failures, and the structure preserves everything needed for full verification when A-layer conditions permit.
- Protocol versions are
v{major}.{minor}. - Minor version changes MUST be backward-compatible (old noise cards remain readable).
- Current version: v0.3 — pre-stable. All components subject to change based on practice feedback.
"This specification is a working hypothesis. Its truth will be determined not by argument, but by whether it produces more reliable code in practice. The source provenance requirement (v0.3) is a wager: that requiring test data to carry its A-layer origin will break the circular reasoning that makes AI-generated code verification unfalsifiable. The wager will be settled not in this document, but in real RE projects."
— First Law, Applied Reflexively