Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ contract or a policy always come with a version bump of that contract.

Offline Repository Analysis, target E4, issue #72.

- Release-readiness repairs: reject ambiguous/malformed current CI inventory
before it can remove carried failures; preserve immutable revision time;
require exact bounded attempt identities and retain UNKNOWN history. Activity
shares current-record admission and exposes rejected verification inputs.
- Replay native source-selection predicates instead of accepting a recognized
omission reason alone. Operational verification explicitly distinguishes
proved source/tree bytes from unproven omitted sizes and history ancestry;
canonical v1 artifact bytes and existing lineages remain unchanged.
- Reconcile the October 21 release state/sequence and add a tested direct
first-report path; the v0.6.0 installation remains conditional on publication.

- New stdlib-only `code` companion: immutable local Git snapshot, Python 3.12
AST structure, six scoped findings, declared-module import cycles and bounded
recent non-merge change hotspots. No source execution or forge dependency.
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,27 @@ For this unreleased checkout:
pip install -e .
```

After the 0.5.0 release tag is published:
After the 0.6.0 release tag is published:

```bash
pip install git+https://github.com/drevendev/devostasis@v0.6.0
```

Observe one repository (a GitHub token is read from `DEVOSTASIS_GITHUB_TOKEN`,
Produce your first seven-Vital report without a configuration file:

```bash
devostasis run --repo drevendev/Devostasis --store ./history-local
devostasis verify --bundle history-local/projects/github.com/drevendev/devostasis/latest
```

Open `history-local/projects/github.com/drevendev/devostasis/latest/report.md`.
The first successful run is a `BASELINE`; later runs compare with the previous
verified bundle. Unavailable API evidence remains explicit, and planning/debt
without configured registers or mappings remains undeclared/uninstrumented.

A GitHub token is read from `DEVOSTASIS_GITHUB_TOKEN`,
`GITHUB_TOKEN`, `GH_TOKEN` or `gh auth token`; public repositories work
without one, with a low rate limit):
without one, with a low rate limit. For separate observation and evaluation:

```bash
devostasis observe --repo owner/name --out observations.json
Expand Down Expand Up @@ -332,6 +344,9 @@ gauges, accepted band ordering and executable conformance vectors. The
pending 0.2.0 adoption adds the accepted Vital repairs and durable Integrity
history; 0.3.0 adds the separate Evidence to Action companion and 0.4.0 adds
reproducible consumer handoff and accepted receipt/compatibility contracts.
0.5.0 adds bound consumer operations and portable private history; 0.6.0 adds
native offline Python analysis and verified source packets. The cumulative
release is still under review, including inherited history-admission repairs.
Its GitLab work collector does not yet implement the core GitLab Vital
adapter or the accepted Instruments carrier.

Expand Down
66 changes: 65 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,71 @@ Ordered by what unblocks what, not by what is interesting. Every item names
who or what blocks it, so a reader can tell the difference between work not
started and work that cannot start.

## Release state and next steps — 2026-10-08
## Release state and next steps — 2026-10-21

The project is at **release stabilization**, with Phase A closed and Phase B
five of seven items implemented. B1 still has 62 named cases without executable
proof, and B7 still needs sustained outside-consumer evidence. The latest
published version is **v0.1.9**; 0.2.0 through 0.6.0 remain unreleased.

The audit began with a clean working tree. All local and fetched remote heads
are ancestors of upstream `release/0.6.0` at `22a2894`, including the original
maintenance commits and the Dependabot correction merged through #79. Nine
earlier PRs remain open: #36/#37/#50/#53/#57/#59/#64/#69/#75. There is no
unrepresented development branch to recover. Inclusion in the release branch
does not publish its code on `master` or create a release tag.

Upstream CI for the 0.6 source head `09d497f` passed on Python 3.12/3.13/3.14
in [run 39030444073](https://github.com/drevendev/Devostasis/actions/runs/39030444073).
The Dependabot merge head `22a2894` also passed
[CI](https://github.com/drevendev/Devostasis/actions/runs/39169319950).
The old `action_required` run is historical evidence, not the current CI state.
These successful checks do not resolve the October 14/16/18 admission reviews
on #57/#75 or approve a later repaired head.

### Delivery order

1. **Finish the admission repairs before release.** Current malformed or
duplicate revision records must not erase carried failures or inflate the
sample. Admit timestamps before reconciliation, preserve immutable revision
time, check exact attempt identities without allocation by their declared
maximum, and retain UNKNOWN attempt evidence. Exercise actual four-bundle
evaluator/store chains and current/historical replay. Source omissions must
satisfy path/mode/scope and deterministic budget predicates; omitted blob
sizes remain acquisition claims, explicitly unproven by a tree proof.
2. **Propagate and review every final release head.** Preserve the prepared
sequence: corrected #57 into `release/0.2.0`, then #53, #59, #64, #69 and #75
into `master`, with their respective tags under CONTRIBUTING.md. Carry the
history repair into descendants, include #79's Dependabot correction in
0.6, and verify each changed head independently. Reconcile #36/#37/#50 after
their delivered commits land. A green matrix is evidence, not acceptance.
3. **Make the existing product straightforward to try.** The README now starts
with a direct single-repository report and its actual path (#78). Retain
source-install instructions until publication is confirmed. #63 still needs
actual scheduled Dependabot execution; the configuration correction alone
does not complete that operational acceptance.
4. **Complete assurance before increasing reach.** Work the outstanding
timestamp/lineage/store audit families (#35), the remaining exact B1 cases
(#20/#23) and independently proved source-history ancestry/counts (#73).
Keep partial coverage, historical compatibility and evidence limits visible.

### Proposed next major code increment

After releasing 0.6, the highest-impact code-only increment is **independently
verifiable history and executable assurance**: bounded commit/parent/tree
proofs for #73, hostile rehashed-history controls, and the missing executable
conformance boundaries for B1. This strengthens the proof objects on which
both work routing and native hotspots depend. It is a proposal, not acceptance
of a new research contract, a new release number, or completion of B1.

The larger product milestone remains a real outside consumer completing work
from verified scopes with retained results and calendar history. Deployment
and scheduling remain owner-deferred in #71; they are not silently reinstated
by this audit. Once B7 and existing compatibility/review gates are met, extend
reach with the accepted core GitLab adapter and Instrument carrier. The
existing GitLab work collector does not implement that core adapter.

## Historical 0.6 implementation snapshot — 2026-10-08

**0.6.0 Offline Repository Analysis (E4) is prepared on
`zendreven/release-0.6.0`, carrying the pending 0.2–0.5 ancestry.**
Expand Down
6 changes: 6 additions & 0 deletions docs/code-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ The local identity derives from the recorded root commit IDs, not a forge URL;
shallow or unrelated roots and changed policy make comparisons INCOMPARABLE.
History counts and root traversal are acquisition observations; their complete
ancestry is not independently proved by the source tree proof (issue #73).
Likewise, a tree binds an omitted blob's identity, not its separately recorded
size. Verification checks the deterministic selection predicates and marks
omitted blob sizes `UNPROVEN` in its operational `proof_scope` receipt. Byte-cap
reasons that depend on those sizes remain acquisition claims; `VERIFIED` means
the admitted proofs and recorded bundle replay, not proof of those omissions.
The receipt is outside the immutable artifact and does not rewrite v1 bundles.

Compare two verified bundles under the same policy:

Expand Down
12 changes: 12 additions & 0 deletions docs/spec/code-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ UNSUPPORTED/LANGUAGE_UNSUPPORTED. UNAVAILABLE reasons are UNSAFE_PATH,
SOURCE_LINK_OR_SUBMODULE, FILE_BYTE_CAP, FILE_COUNT_CAP and TOTAL_BYTE_CAP.
`sources` maps exactly admitted paths to canonical base64 of unmodified bytes.

Offline admission applies the collector's same ordered predicates: safe path,
policy scope, regular blob mode, supported language, per-file bytes, preceding
admitted-file count and preceding verified bytes plus current declared size.
A recognized reason alone is insufficient to omit an otherwise eligible file.
Path/mode and policy/count predicates are independently checked. A tree does
not authenticate a blob's size when its bytes are omitted; byte-based omission
predicates can only be replayed against that acquisition claim. They do not
prove that the omission was necessary. `code verify` reports `proof_scope`
outside the canonical members: source tree and admitted blobs VERIFIED,
omitted blob sizes UNPROVEN (or NOT_APPLICABLE), history ancestry UNPROVEN.
No unbounded blob read or new compatibility/migration rule is implied.

`proof` contains the raw base64 commit object and an object-ID-to-base64 map
of every reachable tree. Verification computes framed Git object hashes,
checks the commit/tree link, expands all trees, checks the entire inventory and
Expand Down
19 changes: 19 additions & 0 deletions docs/spec/integrity-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ When the current series is partial or unusable, carried records are kept, so
history crosses a bundle whose verification evidence could not be read. A
current verdict is never carried (HIST-16).

Current inventory admission validates every row before treating absence as
branch departure. Duplicate current ids are diagnosed as
`REVISION_RECORD_DUPLICATE:<revision>`; malformed rows and contradictory
coverage prevent a completeness claim. Ambiguous current records cannot
replace carried facts. Current timestamps are parsed before reconciliation;
a different instant for a known revision yields
`IMMUTABLE_COMMIT_TIME_MISMATCH` and preserves its prior time and attempts.
Equivalent timezone representations name the same instant.

Attempt completeness requires exactly the observed identities 1..latest,
with work bounded by the observed cardinality. A missing or extra identity
cannot establish completeness. Unknown state vocabulary is malformed input;
the legitimate `UNKNOWN` outcome is retained as incomplete history and never
proves absence of failure. Successive states of one attempt, including
`VERIFY_UNRESOLVED` then `VERIFY_PASS`, remain valid observations. An observed
failure remains absorbing even when the rest of history is incomplete.
These are admission repairs within the existing history lineage and accepted
rules, not new thresholds, external-check semantics or a migration.

**Lineage.** A carried union of `devostasis.ci-history.v2` is read as it is.
The revision records a 0.1.x bundle keeps in `ci.revision_verdicts_14d`
(`devostasis.ci-history.v1`) are replayed from the attempts they kept, never
Expand Down
7 changes: 6 additions & 1 deletion src/devostasis/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .normalize import CI_REVISIONS, INV_COMMITS, INV_CRS, INV_ISSUES, INV_RELEASES
from .observations import ObservationSet
from .policy import ACTIVITY
from .revision_history import _series_records

BASIS_PREVIOUS = "PREVIOUS_BUNDLE"
BASIS_WINDOW = "OBSERVATION_WINDOW_28D"
Expand Down Expand Up @@ -130,7 +131,11 @@ def build_activity(
verification_items: list[dict[str, Any]] = []
verified = failed = unresolved = 0
if ci is not None and ci.has_value:
for rev in ci.value:
admitted, _, problems = _series_records(ci)
coverage_notes.extend(f"VERIFICATION_INPUT_INADMISSIBLE:{problem}" for problem in problems)
if admitted is None:
coverage_notes.append(f"VERIFICATION:{ci.status}:{ci.freshness}")
for rev in admitted or []:
if not rev.get("parents") or not _in_interval(rev.get("committed_at"), start, end):
continue
verified += 1
Expand Down
12 changes: 11 additions & 1 deletion src/devostasis/codeanalysis/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..workscope.operations import no_links
from . import CONTRACT, ENGINE
from .engine import RULES, evaluate, policy
from .git import object_digest
from .git import object_digest, source_selection

MEMBERS = ("inputs.json", "policy.json", "analysis.json", "findings.sarif", "report.md")
MAX_BUNDLE_BYTES = 128 * 1024 * 1024
Expand Down Expand Up @@ -92,6 +92,8 @@ def admit(raw, config):
require(entry["status"] in ("ADMITTED", "UNAVAILABLE", "UNSUPPORTED", "OUT_OF_SCOPE"), "inventory availability unknown")
if entry["kind"] == "blob": number(entry["size"], 0, 2**63 - 1)
else: require(entry["size"] is None, "submodule must not claim blob size")
selection = source_selection(safe, entry["mode"], entry["kind"], entry["size"], config, len(admitted), source_total)
require((entry["status"], entry["reason"]) == selection, "unjustified source selection")
included = safe is not None and (not config["include"] or any(safe == p or safe.startswith(p + "/") for p in config["include"]))
excluded = safe is not None and any(safe == p or safe.startswith(p + "/") for p in config["exclude"])
if entry["status"] == "ADMITTED":
Expand Down Expand Up @@ -210,6 +212,14 @@ def verify(root):
return verify_content(read(root))


def proof_scope(decoded):
"""Operational verification limits, outside the immutable v1 artifact bytes."""
omitted = any(e["kind"] == "blob" and e["status"] != "ADMITTED" for e in decoded["inputs.json"]["entries"])
return {"source_tree": "VERIFIED", "admitted_blobs": "VERIFIED",
"omitted_blob_sizes": "UNPROVEN" if omitted else "NOT_APPLICABLE",
"history_ancestry": "UNPROVEN"}


def publish(root, content):
verify_content(content); root = no_links(root)
root.parent.mkdir(parents=True, exist_ok=True); no_links(root.parent)
Expand Down
5 changes: 3 additions & 2 deletions src/devostasis/codeanalysis/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ..canonical import canonical_bytes, loads
from ..workscope.model import require
from .bundle import MAX_BUNDLE_BYTES, build, publish, verify
from .bundle import MAX_BUNDLE_BYTES, build, proof_scope, publish, verify
from .consumer import compare, packet, verify_packet, work_evidence
from .engine import default_policy
from .git import collect
Expand Down Expand Up @@ -42,7 +42,8 @@ def cmd(args):
if args.operation == "compare":
emit(compare(verify(args.before), verify(args.after)), args.output); return 0
decoded = verify(args.bundle)
if args.operation == "verify": emit({"status": "VERIFIED", "bundle_id": decoded["manifest.json"]["bundle_id"]})
if args.operation == "verify": emit({"status": "VERIFIED", "bundle_id": decoded["manifest.json"]["bundle_id"],
"proof_scope": proof_scope(decoded)})
elif args.operation == "show": emit(decoded["analysis.json"], args.output)
elif args.operation == "sarif": emit(decoded["findings.sarif"], args.output)
elif args.operation == "packet": emit(packet(decoded, args.path, args.max_files, args.max_bytes), args.output)
Expand Down
42 changes: 25 additions & 17 deletions src/devostasis/codeanalysis/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ def blob_digest(data, object_format):
return object_digest(data, object_format)


def source_selection(name, mode, kind, size, config, count, total):
"""Deterministic acquisition priority; omitted blob sizes remain caller observations."""
if name is None:
return "UNAVAILABLE", "UNSAFE_PATH"
included = not config["include"] or any(name == p or name.startswith(p + "/") for p in config["include"])
excluded = any(name == p or name.startswith(p + "/") for p in config["exclude"])
if not included or excluded:
return "OUT_OF_SCOPE", "POLICY_EXCLUSION"
if mode not in ("100644", "100755") or kind != "blob":
return "UNAVAILABLE", "SOURCE_LINK_OR_SUBMODULE"
if not name.endswith(".py"):
return "UNSUPPORTED", "LANGUAGE_UNSUPPORTED"
if size > config["budget"]["max_file_bytes"]:
return "UNAVAILABLE", "FILE_BYTE_CAP"
if count >= config["budget"]["max_files"]:
return "UNAVAILABLE", "FILE_COUNT_CAP"
if total + size > config["budget"]["max_total_bytes"]:
return "UNAVAILABLE", "TOTAL_BYTE_CAP"
return "ADMITTED", None


class Git:
def __init__(self, directory, seconds=120):
self.directory = Path(directory).resolve()
Expand Down Expand Up @@ -133,23 +154,10 @@ def collect(directory, ref, config, at=None):
entry["path"] = path(filename.decode("utf-8"))
except (UnicodeError, ValueError):
entries.append(entry); continue
name = entry["path"]
included = not config["include"] or any(name == p or name.startswith(p + "/") for p in config["include"])
excluded = any(name == p or name.startswith(p + "/") for p in config["exclude"])
if not included or excluded:
entry.update(status="OUT_OF_SCOPE", reason="POLICY_EXCLUSION")
elif mode not in (b"100644", b"100755") or kind != b"blob":
entry.update(reason="SOURCE_LINK_OR_SUBMODULE")
elif not name.endswith(".py"):
entry.update(status="UNSUPPORTED", reason="LANGUAGE_UNSUPPORTED")
elif entry["size"] > config["budget"]["max_file_bytes"]:
entry.update(reason="FILE_BYTE_CAP")
elif len(selected) >= config["budget"]["max_files"]:
entry.update(reason="FILE_COUNT_CAP")
elif total + entry["size"] > config["budget"]["max_total_bytes"]:
entry.update(reason="TOTAL_BYTE_CAP")
else:
entry.update(status="ADMITTED", reason=None); selected.append(entry); total += entry["size"]
status, reason = source_selection(entry["path"], entry["mode"], entry["kind"], entry["size"], config, len(selected), total)
entry.update(status=status, reason=reason)
if status == "ADMITTED":
selected.append(entry); total += entry["size"]
entries.append(entry)
sources = {}
if selected:
Expand Down
Loading