Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Preserve output-only terminal recording bytes for scriptreplay.
docs/demos/stable-vocabulary/output.log -text whitespace=cr-at-eol
docs/demos/single-occurrence.log whitespace=cr-at-eol
15 changes: 15 additions & 0 deletions docs/demos/stable-vocabulary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Stable vocabulary builder replay

Run `python3 docs/demos/stable-vocabulary/demo.py`. Identical private definition,
start, transition and registration inputs use old vocabulary on base
`0b6b8492497c6f9e83027efb624f7e3a6dc13188` and published vocabulary on the candidate
(the containing revision). The four builders with old vocabulary become zero;
the run identity and signature bytes stay unchanged. Unknown term ownership
refuses. This invokes pure builders with a stub public key; it proves no installed
or live-store behavior and performs no network, graph, key or scheduler operation.

[Output](output.log) and [timing](timing.log) are an output-only terminal recording.
Download both and replay with
`scriptreplay --log-out output.log --log-timing timing.log`.
No inline playback is claimed. These small sanitized files stay in this existing
demo tree with source history.
58 changes: 58 additions & 0 deletions docs/demos/stable-vocabulary/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Private before/after RDF builder replay; no network, keys or graph writes."""
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tempfile
from unittest import mock

ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from shuttle import export, signing

BASE = '0b6b8492497c6f9e83027efb624f7e3a6dc13188'
OLD = 'http://aegis.gastown.local/ontology/'


def load(directory, name):
path = directory / (name + '.py')
path.write_bytes(subprocess.check_output(
['git', '-C', str(ROOT), 'show', BASE + ':shuttle/' + name + '.py']))
spec = importlib.util.spec_from_file_location('shuttle._baseline_' + name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def measure(label, writer, signer):
record = {'run': 'example', 'definition': 'example', 'state': 'open',
'at': '2026-11-13T00:00:00Z', 'agent': 'fixture', 'step': 'claim',
'from': 'open', 'to': 'claimed', 'signature': 'ab' * 64}
with mock.patch.object(signer, 'public_key_hex', return_value='cd' * 32):
bodies = [writer.definition_turtle({'name': 'example', 'transitions': [{'step': 'claim'}]}),
writer.start_turtle(record), writer.transition_turtle(record, 3),
signer.registration_turtle('fixture', 'urn:shuttle:workflow:example')]
old = sum(OLD in body for body in bodies)
result = {'source': label, 'builders': len(bodies), 'builders_with_legacy_vocabulary': old,
'run_identity': writer.run_iri('example'),
'signature_hex_unchanged': record['signature'] in bodies[2]}
print(json.dumps(result, sort_keys=True), flush=True)
assert old == (4 if label == 'base' else 0)
assert result['run_identity'] == 'urn:shuttle:run:example'
assert result['signature_hex_unchanged']


with tempfile.TemporaryDirectory() as directory, \
mock.patch.dict('os.environ', {'SHUTTLE_ENTITY_NS': 'urn:shuttle:'}):
print('Same private records: definition, start, transition and registration builders.')
measure('base', load(Path(directory), 'export'), load(Path(directory), 'signing'))
measure('candidate', export, signing)
from shuttle import vocabulary
try:
vocabulary.term('UnknownTerm')
except KeyError:
print('PASS: unknown vocabulary owner refuses; no legacy fallback.')
else:
raise AssertionError('unknown term accepted')
7 changes: 7 additions & 0 deletions docs/demos/stable-vocabulary/output.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Script started on 2026-11-13 11:56:44-05:00 [COMMAND="python3 docs/demos/stable-vocabulary/demo.py" <not executed on terminal>]
Same private records: definition, start, transition and registration builders.
{"builders": 4, "builders_with_legacy_vocabulary": 4, "run_identity": "urn:shuttle:run:example", "signature_hex_unchanged": true, "source": "base"}
{"builders": 4, "builders_with_legacy_vocabulary": 0, "run_identity": "urn:shuttle:run:example", "signature_hex_unchanged": true, "source": "candidate"}
PASS: unknown vocabulary owner refuses; no legacy fallback.

Script done on 2026-11-13 11:56:44-05:00 [COMMAND_EXIT_CODE="0"]
4 changes: 4 additions & 0 deletions docs/demos/stable-vocabulary/timing.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0.083690 80
0.006088 149
0.000153 154
0.000024 61
4 changes: 2 additions & 2 deletions docs/knowledge/repository-knowledge.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"source": "shuttle/cli.py",
"subject": "http://aegis.gastown.local/ontology/code/shuttle/shuttle%2Fcli.py",
"summary": "The CLI defines and advances workflows, exports history, verifies signatures and freezes windows. It has no HTTP or MCP server.",
"source_sha256": "0460cc7819309e7a87fcc4d4dfbca79f8307bb1a5fcad946e934f4f3fb3d1d4a",
"source_sha256": "33d945358af48e0aceea367e4bc969bdb70757e93ca7d3e59ca02ddb92631d83",
"properties": {
"interfaceKind": "CLI",
"publicName": "shuttle"
Expand All @@ -38,7 +38,7 @@
"source": "shuttle/signing.py",
"subject": "http://aegis.gastown.local/ontology/code/shuttle/shuttle%2Fsigning.py",
"summary": "SHUTTLE_KEY_DIR selects agent key storage; require-existing-key mode refuses missing or unsafe key files and does not create replacements.",
"source_sha256": "4e9a268d068030989e5131bb8d4d9e83e671b3c9e497aaa8fbd10449acc246e3",
"source_sha256": "28846cac3874b68524c3d60f041cce1091517b7e247dbdd7641f5d0ebda171fe",
"properties": {
"interfaceKind": "config",
"publicName": "SHUTTLE_KEY_DIR / existing_keys_only"
Expand Down
36 changes: 36 additions & 0 deletions docs/stable-vocabulary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Stable vocabulary and historical run identity

Fresh workflow, start, transition and verifier-registration Turtle uses the
published Intigraph vocabulary. The 19-term crosswalk comes from
[ownership.json](https://intigraph.ai/ids/ownership.json), SHA-256
`a389be53b74259d0c515ff6db16d602474dc5fe38dc6cc47f0f54a2d1347d34e`.
Five classes belong to `https://w3id.org/intigraph/quipu/ns#`; fourteen properties
belong to `https://w3id.org/intigraph/camayoc/core/ns#`.
Unknown term names refuse instead of falling back to another vocabulary.

Vocabulary ownership is separate from instance identity. The existing neutral
`urn:shuttle:` instance default and explicit `SHUTTLE_ENTITY_NS` contract remain.
Run, workflow, step, event and registration identities, signature messages, local
logs and watermarks retain their existing bytes. This change allocates no new
identity scheme, invokes no scoped mint, enrolls no verifier and rewrites no
historical graph. An explicitly configured historical instance base remains an
operator-controlled identity parameter; it must not be changed to migrate terms.

Verification and signed fixed/calendar history readers query both the published
and historical literal predicates using explicit UNION branches, retaining the
established native-reader compatibility form rather than alternate predicate
paths for literal objects. Known vocabulary terms are normalized only
for comparison; subject and reference identities and signatures remain exact.
Duplicate aliases remain visible to existing cardinality checks and refuse.
No inference or `sameAs` activation is required for these readers.

The legacy constant in `vocabulary.py` is a read-compatibility identifier.
Repository-share builders and queries retain their separate former-name contract
and private intermediate-index behavior; their old IRIs are not rewritten by
the runtime workflow writer change.

Source tests prove the fresh default builders emit no old vocabulary IRIs and
historical signed facts remain readable. They do not establish installed,
released or live-store acceptance. Deployment still needs compatible loaded
shapes, normal release/adoption, one smoke check and an explicit rollback.
Source rollback is a normal revert; preserve existing outboxes and identities.
13 changes: 7 additions & 6 deletions shuttle/calendar_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
from zoneinfo import ZoneInfo

from . import export, windows
from . import export, windows, vocabulary
from .scheduled import ScheduledRefused, _rows
from .scheduled_history import ScheduledHistory

Expand Down Expand Up @@ -98,12 +98,13 @@ def measure(self, *, now, occurrence, positive):
return {'verdict': 'NOT_DUE', 'slot': None}
if any(v.eligible(dict(v.binding), s) is not True for s in (control, slot)):
raise ScheduledRefused('current declaration eligibility unavailable')
a = export.AEGIS
registration = _rows(v.query(
f'SELECT ?verifier ?pk ?workflow (isIRI(?workflow) AS ?scopeIRI) WHERE {{ '
f'<{v.registration}> a ?type ; <{a}verifier> ?verifier ; '
f'<{a}publicKey> ?pk ; <{a}attests> ?workflow . '
f'FILTER(?type = <{a}VerifierRegistration>) }} LIMIT 8',
f'<{v.registration}> a ?type . '
f'{vocabulary.pattern("<"+v.registration+">", "verifier", "?verifier")} '
f'{vocabulary.pattern("<"+v.registration+">", "publicKey", "?pk")} '
f'{vocabulary.pattern("<"+v.registration+">", "attests", "?workflow")} '
f'{vocabulary.type_filter("?type", "VerifierRegistration")} }} LIMIT 8',
graph=v.identity_graph), 8)
if registration != [{'verifier': v.binding['agent'], 'pk': v.public_key,
'workflow': v.binding['workflow'], 'scopeIRI': True}] or \
Expand All @@ -115,7 +116,7 @@ def measure(self, *, now, occurrence, positive):
'(STR(?raw) AS ?value) (isIRI(?raw) AS ?iri) '
'(STR(DATATYPE(?raw)) AS ?datatype) WHERE { VALUES ?run { '
+ ' '.join('<'+export.run_iri(run)+'>' for run in runs) + ' } '
+ f'?event <{a}inRun> ?run ; ?predicate ?raw }} LIMIT 64',
+ f'{vocabulary.pattern("?event", "inRun", "?run")} ?event ?predicate ?raw }} LIMIT 64',
graph=windows.window_iri(month)), 64)
grouped = [[r for r in rows if r.get('ev') in
(export.event_iri(run, 2), export.event_iri(run, 3))] for run in runs]
Expand Down
14 changes: 8 additions & 6 deletions shuttle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,13 @@ def cmd_verify(args) -> int:
defs, runs = _index()
_, run, _ = _run_state(defs, runs, args.run)
r = export_mod.run_iri(args.run)
a = export_mod.AEGIS
v = export_mod.vocabulary
rows = qc.query(
f"SELECT ?ev ?step ?from ?to ?at ?agent ?sig WHERE {{ "
f"?ev <{a}inRun> <{r}> ; <{a}atStep> ?step ; <{a}fromState> ?from ; "
f"<{a}toState> ?to ; <{export_mod.PROV}endedAtTime> ?at ; "
f"<{a}performedBy> ?agent ; <{a}signature> ?sig . }}",
f"{v.pattern('?ev', 'inRun', '<'+r+'>')} {v.pattern('?ev', 'atStep', '?step')} "
f"{v.pattern('?ev', 'fromState', '?from')} {v.pattern('?ev', 'toState', '?to')} "
f"?ev <{export_mod.PROV}endedAtTime> ?at . "
f"{v.pattern('?ev', 'performedBy', '?agent')} {v.pattern('?ev', 'signature', '?sig')} }}",
graph=windows.window_iri(run["window"]),
).get("rows", [])
if not rows:
Expand All @@ -259,8 +260,9 @@ def cmd_verify(args) -> int:
f"{run['window']} — export first, or the window moved"
)
regs = qc.query(
f"SELECT ?verifier ?pk WHERE {{ ?reg a <{a}VerifierRegistration> ; "
f"<{a}verifier> ?verifier ; <{a}publicKey> ?pk . }}",
f"SELECT ?verifier ?pk WHERE {{ ?reg a ?registrationType . "
f"{v.pattern('?reg', 'verifier', '?verifier')} {v.pattern('?reg', 'publicKey', '?pk')} "
f"{v.type_filter('?registrationType', 'VerifierRegistration')} }}",
graph=args.identity_graph,
).get("rows", [])
keys = {row["verifier"]: row["pk"] for row in regs}
Expand Down
50 changes: 25 additions & 25 deletions shuttle/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
What a transition exports (the camayoc workflow slice,
`competency/workflow-and-archive.md` Q1–Q8):

- an `aegis:TransitionEvent` with step, from/to states, time,
`aegis:performedBy` and `aegis:signature` (hex ed25519 over the canonical
- an `qp:TransitionEvent` with step, from/to states, time,
`core:performedBy` and `core:signature` (hex ed25519 over the canonical
`shuttle-transition-v1` message — re-derivable from these facts alone);
- a re-asserted `aegis:currentState` on the run — derived convenience; the
- a re-asserted `core:currentState` on the run — derived convenience; the
events are the truth, nothing is mutated in place.

Pre-export, shuttle verifies its OWN signature (fail fast on key drift);
Expand All @@ -26,7 +26,7 @@
import os

from . import quipu_client as qc
from . import signing, state, windows
from . import signing, state, windows, vocabulary


class ExportError(RuntimeError):
Expand All @@ -49,35 +49,35 @@ def event_iri(run_id: str, seq: int) -> str:
return f"{entity_ns()}event:{run_id}:{seq}"


AEGIS = "http://aegis.gastown.local/ontology/"
AEGIS = vocabulary.LEGACY # compatibility only; new facts use exact term owners
PROV = "http://www.w3.org/ns/prov#"


def _prefixes() -> str:
return f"@prefix aegis: <{AEGIS}> .\n@prefix prov: <{PROV}> .\n"
return vocabulary.prefixes() + f"@prefix prov: <{PROV}> .\n"


def definition_turtle(rec: dict) -> str:
"""A WorkflowDefinition and its declared steps — durable knowledge shape,
but exported beside the runs so the window is self-describing."""
wf = workflow_iri(rec["name"])
lines = [_prefixes(), f"<{wf}> a aegis:WorkflowDefinition ."]
lines = [_prefixes(), f"<{wf}> a qp:WorkflowDefinition ."]
for t in rec["transitions"]:
step = f"{wf}/step/{t['step']}"
lines.append(f"<{wf}> aegis:hasStep <{step}> .")
lines.append(f"<{step}> a aegis:WorkflowStep .")
lines.append(f"<{wf}> core:hasStep <{step}> .")
lines.append(f"<{step}> a qp:WorkflowStep .")
return "\n".join(lines) + "\n"


def start_turtle(rec: dict) -> str:
r = run_iri(rec["run"])
return (
_prefixes()
+ f"<{r}> a aegis:WorkflowRun ;\n"
+ f' aegis:runOf <{workflow_iri(rec["definition"])}> ;\n'
+ f' aegis:currentState "{rec["state"]}" ;\n'
+ f' aegis:createdAt "{rec["at"]}" ;\n'
+ f' aegis:sourceKind "observed" .\n'
+ f"<{r}> a qp:WorkflowRun ;\n"
+ f' core:runOf <{workflow_iri(rec["definition"])}> ;\n'
+ f' core:currentState "{rec["state"]}" ;\n'
+ f' core:createdAt "{rec["at"]}" ;\n'
+ f' core:sourceKind "observed" .\n'
)


Expand All @@ -87,16 +87,16 @@ def transition_turtle(rec: dict, seq: int) -> str:
wf_step = f'{workflow_iri(rec["definition"])}/step/{rec["step"]}'
return (
_prefixes()
+ f"<{ev}> a aegis:TransitionEvent ;\n"
+ f" aegis:inRun <{r}> ;\n"
+ f" aegis:atStep <{wf_step}> ;\n"
+ f' aegis:fromState "{rec["from"]}" ;\n'
+ f' aegis:toState "{rec["to"]}" ;\n'
+ f"<{ev}> a qp:TransitionEvent ;\n"
+ f" core:inRun <{r}> ;\n"
+ f" core:atStep <{wf_step}> ;\n"
+ f' core:fromState "{rec["from"]}" ;\n'
+ f' core:toState "{rec["to"]}" ;\n'
+ f' prov:endedAtTime "{rec["at"]}" ;\n'
+ f' aegis:performedBy <{entity_ns()}agent:{rec["agent"]}> ;\n'
+ f' aegis:signature "{rec["signature"]}" ;\n'
+ f' aegis:sourceKind "observed" .\n'
+ f'<{r}> aegis:currentState "{rec["to"]}" .\n'
+ f' core:performedBy <{entity_ns()}agent:{rec["agent"]}> ;\n'
+ f' core:signature "{rec["signature"]}" ;\n'
+ f' core:sourceKind "observed" .\n'
+ f'<{r}> core:currentState "{rec["to"]}" .\n'
)


Expand All @@ -115,7 +115,7 @@ def _definitions_by_name(recs: list[dict]) -> dict[str, dict]:


def _workflow_of(rec: dict) -> str | None:
"""The workflow a record asserts `aegis:runOf` against, if any."""
"""The workflow a record asserts `core:runOf` against, if any."""
kind = rec.get("type")
if kind in ("start", "transition"):
return rec.get("definition")
Expand Down Expand Up @@ -182,7 +182,7 @@ def export(root=None, strict: bool = False) -> dict:
A `define` record exports into the window of the month it was RECORDED.
Runs export into the window of the month they RAN. Those are the same
month exactly once — the month a workflow is introduced. Every month
afterwards, a run asserts `aegis:runOf` against a workflow that the new
afterwards, a run asserts `core:runOf` against a workflow that the new
window has never heard of, SHACL's class constraint refuses it, and one
refusal stops the whole drain.

Expand Down
Loading
Loading