pace-engine 0.1.0__tar.gz
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.
- pace_engine-0.1.0/PKG-INFO +35 -0
- pace_engine-0.1.0/README.md +25 -0
- pace_engine-0.1.0/pace/__init__.py +0 -0
- pace_engine-0.1.0/pace/cli/__init__.py +0 -0
- pace_engine-0.1.0/pace/cli/pace.py +253 -0
- pace_engine-0.1.0/pace/contracts/INSTANCE_CONTRACT_0.1.0.pdl +139 -0
- pace_engine-0.1.0/pace/contracts/__init__.py +0 -0
- pace_engine-0.1.0/pace/engines/__init__.py +0 -0
- pace_engine-0.1.0/pace/engines/handoff.py +113 -0
- pace_engine-0.1.0/pace/engines/memory.py +43 -0
- pace_engine-0.1.0/pace/engines/project_creator.py +138 -0
- pace_engine-0.1.0/pace/kernel/__init__.py +0 -0
- pace_engine-0.1.0/pace/kernel/kernel.py +105 -0
- pace_engine-0.1.0/pace/services/__init__.py +0 -0
- pace_engine-0.1.0/pace/services/contract_loader.py +56 -0
- pace_engine-0.1.0/pace/services/fs.py +15 -0
- pace_engine-0.1.0/pace/services/pdl.py +27 -0
- pace_engine-0.1.0/pace/services/pdl_nested.py +64 -0
- pace_engine-0.1.0/pace/services/validate.py +36 -0
- pace_engine-0.1.0/pace/services/version.py +7 -0
- pace_engine-0.1.0/pace_engine.egg-info/PKG-INFO +35 -0
- pace_engine-0.1.0/pace_engine.egg-info/SOURCES.txt +25 -0
- pace_engine-0.1.0/pace_engine.egg-info/dependency_links.txt +1 -0
- pace_engine-0.1.0/pace_engine.egg-info/entry_points.txt +2 -0
- pace_engine-0.1.0/pace_engine.egg-info/top_level.txt +1 -0
- pace_engine-0.1.0/pyproject.toml +24 -0
- pace_engine-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pace-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PACE - an engineering operating system: a versioned, installable engine that preserves and governs a project's knowledge so any AI understands it in seconds.
|
|
5
|
+
Author: Cley Duarte
|
|
6
|
+
Project-URL: Homepage, https://github.com/cleyduartees-dot/PACE
|
|
7
|
+
Keywords: ai,knowledge,engineering,governance,memory,context
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# PACE
|
|
12
|
+
|
|
13
|
+
An engineering operating system: a versioned, installable engine that preserves, organizes, governs and evolves the architectural knowledge of software projects, in collaboration between people and AI.
|
|
14
|
+
|
|
15
|
+
PACE is not a library embedded inside a product. It is installed once and governs any number of independent projects, each carrying only its own `.pace/` memory — never a copy of PACE itself.
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
Founded 2026-07-06. This repository currently contains only this founding commit. The minimal day-1 skeleton (kernel, services, engines, ontology, protocols, governance, policies, standards, contracts, cli, history, docs) is the next step, not yet built.
|
|
20
|
+
|
|
21
|
+
## Founding principle
|
|
22
|
+
|
|
23
|
+
- **PACE** — the engine. Code only, versioned, installable. No knowledge of any specific organization or project.
|
|
24
|
+
- **Organización** — an adopting company's own governance memory (who holds authority, its own policies, its registry of projects), same recursive shape as a project instance, scoped one level up.
|
|
25
|
+
- **Instancia (`.pace/`)** — a single project's own memory: mission, vision, roadmap, sprint, handoff, history, decisions.
|
|
26
|
+
- **Proyecto** — the actual product codebase. Outside PACE's concern beyond hosting a `.pace/` folder at its root.
|
|
27
|
+
|
|
28
|
+
## Roadmap (founding sequence)
|
|
29
|
+
|
|
30
|
+
1. Crear el repositorio oficial PACE. — done
|
|
31
|
+
2. Implementar el esqueleto mínimo.
|
|
32
|
+
3. Implementar `pace init`.
|
|
33
|
+
4. Adoptar el primer proyecto gobernado por PACE.
|
|
34
|
+
5. Implementar `pace create`.
|
|
35
|
+
6. Crear el primer proyecto nuevo nacido desde PACE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# PACE
|
|
2
|
+
|
|
3
|
+
An engineering operating system: a versioned, installable engine that preserves, organizes, governs and evolves the architectural knowledge of software projects, in collaboration between people and AI.
|
|
4
|
+
|
|
5
|
+
PACE is not a library embedded inside a product. It is installed once and governs any number of independent projects, each carrying only its own `.pace/` memory — never a copy of PACE itself.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Founded 2026-07-06. This repository currently contains only this founding commit. The minimal day-1 skeleton (kernel, services, engines, ontology, protocols, governance, policies, standards, contracts, cli, history, docs) is the next step, not yet built.
|
|
10
|
+
|
|
11
|
+
## Founding principle
|
|
12
|
+
|
|
13
|
+
- **PACE** — the engine. Code only, versioned, installable. No knowledge of any specific organization or project.
|
|
14
|
+
- **Organización** — an adopting company's own governance memory (who holds authority, its own policies, its registry of projects), same recursive shape as a project instance, scoped one level up.
|
|
15
|
+
- **Instancia (`.pace/`)** — a single project's own memory: mission, vision, roadmap, sprint, handoff, history, decisions.
|
|
16
|
+
- **Proyecto** — the actual product codebase. Outside PACE's concern beyond hosting a `.pace/` folder at its root.
|
|
17
|
+
|
|
18
|
+
## Roadmap (founding sequence)
|
|
19
|
+
|
|
20
|
+
1. Crear el repositorio oficial PACE. — done
|
|
21
|
+
2. Implementar el esqueleto mínimo.
|
|
22
|
+
3. Implementar `pace init`.
|
|
23
|
+
4. Adoptar el primer proyecto gobernado por PACE.
|
|
24
|
+
5. Implementar `pace create`.
|
|
25
|
+
6. Crear el primer proyecto nuevo nacido desde PACE.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""PACE CLI - thin command layer delegating to the Kernel and Engines.
|
|
2
|
+
|
|
3
|
+
Beyond argument handling, the only logic here is the guided intake: an
|
|
4
|
+
interactive setup that seeds a project's memory from the owner's answers.
|
|
5
|
+
Run as `python cli/pace.py <command> ...`, or install and run `pace ...`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
13
|
+
from pace.kernel.kernel import locate_instance, validate_instance
|
|
14
|
+
from pace.services.pdl import read_pdl
|
|
15
|
+
from pace.engines.project_creator import init_instance, create_project
|
|
16
|
+
from pace.engines.handoff import generate_handoff
|
|
17
|
+
from pace.engines.memory import remember, recall
|
|
18
|
+
|
|
19
|
+
ACTIVE_SECTIONS = [
|
|
20
|
+
("MISSION", "ACTIVE_MISSION"),
|
|
21
|
+
("VISION", "ACTIVE_VISION"),
|
|
22
|
+
("ROADMAP", "ACTIVE_ROADMAP"),
|
|
23
|
+
("SPRINT", "ACTIVE_SPRINT"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cmd_doctor(args) -> int:
|
|
28
|
+
root = locate_instance(Path(args.path) if args.path else None)
|
|
29
|
+
if root is None:
|
|
30
|
+
print("no .pace/ instance found")
|
|
31
|
+
return 1
|
|
32
|
+
violations = validate_instance(root)
|
|
33
|
+
if violations:
|
|
34
|
+
print(f"INVALID - {len(violations)} violation(s):")
|
|
35
|
+
for v in violations:
|
|
36
|
+
print(f" - {v}")
|
|
37
|
+
return 1
|
|
38
|
+
print(f"VALID - {root}")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_context(args) -> int:
|
|
43
|
+
root = locate_instance(Path(args.path) if args.path else None)
|
|
44
|
+
if root is None:
|
|
45
|
+
print("no .pace/ instance found")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
instance = read_pdl(root / "INSTANCE.pdl")
|
|
49
|
+
active = read_pdl(root / "ACTIVE_VERSIONS.pdl")
|
|
50
|
+
|
|
51
|
+
print(f"NAME {instance.get('NAME')}")
|
|
52
|
+
print(f"SLUG {instance.get('SLUG')}")
|
|
53
|
+
print(f"KIND {instance.get('KIND')}")
|
|
54
|
+
print(f"SCHEMA_VERSION {instance.get('SCHEMA_VERSION')}")
|
|
55
|
+
if instance.get("ORG_REF"):
|
|
56
|
+
print(f"ORG_REF {instance.get('ORG_REF')}")
|
|
57
|
+
print()
|
|
58
|
+
|
|
59
|
+
for label, key in ACTIVE_SECTIONS:
|
|
60
|
+
relative_path = active.get(key)
|
|
61
|
+
if not relative_path:
|
|
62
|
+
continue
|
|
63
|
+
full_path = root / relative_path
|
|
64
|
+
print(f"--- {label} ({relative_path}) ---")
|
|
65
|
+
if full_path.is_file():
|
|
66
|
+
print(full_path.read_text(encoding="utf-8").strip())
|
|
67
|
+
else:
|
|
68
|
+
print("(file not found)")
|
|
69
|
+
print()
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _slugify(name: str) -> str:
|
|
74
|
+
slug = "".join(c if c.isalnum() else "-" for c in name.lower())
|
|
75
|
+
while "--" in slug:
|
|
76
|
+
slug = slug.replace("--", "-")
|
|
77
|
+
return slug.strip("-") or "project"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _prompt(text: str, default: str = "") -> str:
|
|
81
|
+
suffix = f" [{default}]" if default else ""
|
|
82
|
+
try:
|
|
83
|
+
value = input(f"{text}{suffix}\n> ").strip()
|
|
84
|
+
except EOFError:
|
|
85
|
+
value = ""
|
|
86
|
+
return value or default
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_guided_intake(kind, name, slug, org_ref):
|
|
90
|
+
"""Interactive setup: ask a few questions and seed mission / vision /
|
|
91
|
+
roadmap / sprint. The AI (or the owner) answers; nothing is invented.
|
|
92
|
+
Returns the resolved identity plus an `extra` dict of seeded content."""
|
|
93
|
+
print("\nPACE guided setup - a few questions to seed your project's memory.")
|
|
94
|
+
print("Press Enter to accept a [default] or to skip a question.\n")
|
|
95
|
+
|
|
96
|
+
if not name:
|
|
97
|
+
name = _prompt("Project name") or "Unnamed project"
|
|
98
|
+
kind_in = _prompt("Kind (PROJECT / ORGANIZATION)", kind or "PROJECT").upper()
|
|
99
|
+
kind = "ORGANIZATION" if kind_in.startswith("O") else "PROJECT"
|
|
100
|
+
if kind == "PROJECT" and not org_ref:
|
|
101
|
+
org_ref = _prompt("Organization it belongs to (org ref)") or "org"
|
|
102
|
+
if not slug:
|
|
103
|
+
slug = _prompt("Slug", _slugify(name))
|
|
104
|
+
|
|
105
|
+
extra = {}
|
|
106
|
+
mission = _prompt("Why does this project exist? (mission)")
|
|
107
|
+
if mission:
|
|
108
|
+
extra["mission"] = mission
|
|
109
|
+
vision = _prompt("Where is it going? (vision)")
|
|
110
|
+
if vision:
|
|
111
|
+
extra["vision"] = vision
|
|
112
|
+
roadmap = _prompt("What is pending? (roadmap - main items)")
|
|
113
|
+
if roadmap:
|
|
114
|
+
extra["roadmap"] = roadmap
|
|
115
|
+
sprint = _prompt("What are you working on right now? (current sprint)")
|
|
116
|
+
if sprint:
|
|
117
|
+
extra["sprint"] = sprint
|
|
118
|
+
return kind, name, slug, org_ref, extra
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_init(args) -> int:
|
|
122
|
+
kind, name, slug, org_ref = args.kind, args.name, args.slug, args.org_ref
|
|
123
|
+
extra = {}
|
|
124
|
+
if args.guided:
|
|
125
|
+
kind, name, slug, org_ref, extra = run_guided_intake(kind, name, slug, org_ref)
|
|
126
|
+
|
|
127
|
+
if not name:
|
|
128
|
+
print("init failed: --name is required (or use --guided)")
|
|
129
|
+
return 1
|
|
130
|
+
if not slug:
|
|
131
|
+
slug = _slugify(name)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
root = init_instance(
|
|
135
|
+
Path(args.path), kind=kind, name=name, slug=slug, org_ref=org_ref, **extra
|
|
136
|
+
)
|
|
137
|
+
except (FileExistsError, ValueError) as error:
|
|
138
|
+
print(f"init failed: {error}")
|
|
139
|
+
return 1
|
|
140
|
+
print(f"created .pace/ at {root}")
|
|
141
|
+
violations = validate_instance(root)
|
|
142
|
+
if violations:
|
|
143
|
+
print("WARNING - the new instance is not structurally valid:")
|
|
144
|
+
for v in violations:
|
|
145
|
+
print(f" - {v}")
|
|
146
|
+
return 1
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_create(args) -> int:
|
|
151
|
+
try:
|
|
152
|
+
root = create_project(
|
|
153
|
+
Path(args.path),
|
|
154
|
+
name=args.name,
|
|
155
|
+
slug=args.slug,
|
|
156
|
+
org_ref=args.org_ref,
|
|
157
|
+
)
|
|
158
|
+
except FileExistsError as error:
|
|
159
|
+
print(f"create failed: {error}")
|
|
160
|
+
return 1
|
|
161
|
+
print(f"created project at {root.parent}")
|
|
162
|
+
violations = validate_instance(root)
|
|
163
|
+
if violations:
|
|
164
|
+
print("WARNING - the new instance is not structurally valid:")
|
|
165
|
+
for v in violations:
|
|
166
|
+
print(f" - {v}")
|
|
167
|
+
return 1
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_handoff(args) -> int:
|
|
172
|
+
root = locate_instance(Path(args.path) if args.path else None)
|
|
173
|
+
if root is None:
|
|
174
|
+
print("no .pace/ instance found")
|
|
175
|
+
return 1
|
|
176
|
+
out = generate_handoff(root)
|
|
177
|
+
print(f"handoff regenerated at {out}\n")
|
|
178
|
+
print(out.read_text(encoding="utf-8"))
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_remember(args) -> int:
|
|
183
|
+
root = locate_instance(Path(args.path) if args.path else None)
|
|
184
|
+
if root is None:
|
|
185
|
+
print("no .pace/ instance found")
|
|
186
|
+
return 1
|
|
187
|
+
out = remember(root, args.text)
|
|
188
|
+
print(f"remembered in {out}")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cmd_recall(args) -> int:
|
|
193
|
+
root = locate_instance(Path(args.path) if args.path else None)
|
|
194
|
+
if root is None:
|
|
195
|
+
print("no .pace/ instance found")
|
|
196
|
+
return 1
|
|
197
|
+
print(recall(root))
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
202
|
+
parser = argparse.ArgumentParser(prog="pace")
|
|
203
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
204
|
+
|
|
205
|
+
doctor = subparsers.add_parser("doctor", help="validate a .pace/ instance structurally")
|
|
206
|
+
doctor.add_argument("path", nargs="?", default=None)
|
|
207
|
+
doctor.set_defaults(func=cmd_doctor)
|
|
208
|
+
|
|
209
|
+
context = subparsers.add_parser("context", help="print a .pace/ instance's current context")
|
|
210
|
+
context.add_argument("path", nargs="?", default=None)
|
|
211
|
+
context.set_defaults(func=cmd_context)
|
|
212
|
+
|
|
213
|
+
init = subparsers.add_parser("init", help="create a new .pace/ instance in an existing project")
|
|
214
|
+
init.add_argument("path")
|
|
215
|
+
init.add_argument("--kind", choices=["PROJECT", "ORGANIZATION"], default="PROJECT")
|
|
216
|
+
init.add_argument("--name", default=None)
|
|
217
|
+
init.add_argument("--slug", default=None)
|
|
218
|
+
init.add_argument("--org-ref", dest="org_ref", default=None)
|
|
219
|
+
init.add_argument("--guided", action="store_true",
|
|
220
|
+
help="interactive guided setup that seeds mission/vision/roadmap/sprint")
|
|
221
|
+
init.set_defaults(func=cmd_init)
|
|
222
|
+
|
|
223
|
+
handoff = subparsers.add_parser("handoff", help="regenerate the AI-onboarding handoff for a .pace/ instance")
|
|
224
|
+
handoff.add_argument("path", nargs="?", default=None)
|
|
225
|
+
handoff.set_defaults(func=cmd_handoff)
|
|
226
|
+
|
|
227
|
+
remember = subparsers.add_parser("remember", help="append a continuity note to the project's working memory")
|
|
228
|
+
remember.add_argument("text")
|
|
229
|
+
remember.add_argument("path", nargs="?", default=None)
|
|
230
|
+
remember.set_defaults(func=cmd_remember)
|
|
231
|
+
|
|
232
|
+
recall = subparsers.add_parser("recall", help="print the project's working/continuity memory")
|
|
233
|
+
recall.add_argument("path", nargs="?", default=None)
|
|
234
|
+
recall.set_defaults(func=cmd_recall)
|
|
235
|
+
|
|
236
|
+
create = subparsers.add_parser("create", help="generate a brand-new project governed by PACE from scratch")
|
|
237
|
+
create.add_argument("path")
|
|
238
|
+
create.add_argument("--name", required=True)
|
|
239
|
+
create.add_argument("--slug", required=True)
|
|
240
|
+
create.add_argument("--org-ref", dest="org_ref", required=True)
|
|
241
|
+
create.set_defaults(func=cmd_create)
|
|
242
|
+
|
|
243
|
+
return parser
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main(argv=None) -> int:
|
|
247
|
+
parser = build_parser()
|
|
248
|
+
args = parser.parse_args(argv)
|
|
249
|
+
return args.func(args)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
CONTRACT_VERSION 0.1.0
|
|
2
|
+
|
|
3
|
+
STATUS DRAFT
|
|
4
|
+
|
|
5
|
+
OWNER President Cley Duarte
|
|
6
|
+
|
|
7
|
+
DEPENDS_ON
|
|
8
|
+
|
|
9
|
+
ontology/PACE_ONTOLOGY_0.1.0.pdl
|
|
10
|
+
protocols/PDL_SPECIFICATION.pdl (not yet written — referenced for future
|
|
11
|
+
format validation, not required for this contract to be a valid design
|
|
12
|
+
artifact today)
|
|
13
|
+
|
|
14
|
+
PURPOSE
|
|
15
|
+
|
|
16
|
+
Define the physical, checkable shape any .pace/ instance (a project or an
|
|
17
|
+
organization hub — same KIND distinction, not two different contracts)
|
|
18
|
+
must have to be valid. This is the representation layer for the entities,
|
|
19
|
+
relations and assertions defined in PACE_ONTOLOGY_0.1.0.
|
|
20
|
+
|
|
21
|
+
ROOT_MANIFEST
|
|
22
|
+
|
|
23
|
+
.pace/INSTANCE.pdl
|
|
24
|
+
|
|
25
|
+
REPRESENTS the GOVERNED_UNIT entity itself and its direct identity
|
|
26
|
+
assertions and relations.
|
|
27
|
+
|
|
28
|
+
REQUIRED_FIELDS
|
|
29
|
+
KIND PROJECT | ORGANIZATION
|
|
30
|
+
NAME (HAS_NAME)
|
|
31
|
+
SLUG (HAS_SLUG)
|
|
32
|
+
SCHEMA_VERSION (HAS_SCHEMA_VERSION — which Contract version this
|
|
33
|
+
instance claims to satisfy)
|
|
34
|
+
PACE_VERSION (HAS_PACE_VERSION — which PACE engine version
|
|
35
|
+
created or last managed this instance. Traceability
|
|
36
|
+
only, permanently: the Kernel never decides what it
|
|
37
|
+
can open based on this field, only on SCHEMA_VERSION
|
|
38
|
+
— the same way a browser interprets HTML by its
|
|
39
|
+
declared version, not by which browser authored it)
|
|
40
|
+
ORG_REF (BELONGS_TO target — required if KIND=PROJECT,
|
|
41
|
+
absent if KIND=ORGANIZATION)
|
|
42
|
+
CREATED_AT (HAS_CREATED_AT)
|
|
43
|
+
|
|
44
|
+
.pace/ACTIVE_VERSIONS.pdl
|
|
45
|
+
|
|
46
|
+
REPRESENTS the IS_CURRENTLY_ACTIVE relation, one entry per versioned
|
|
47
|
+
assertion sequence.
|
|
48
|
+
|
|
49
|
+
REQUIRED_FIELDS
|
|
50
|
+
ACTIVE_MISSION
|
|
51
|
+
ACTIVE_VISION
|
|
52
|
+
ACTIVE_ROADMAP
|
|
53
|
+
ACTIVE_SPRINT
|
|
54
|
+
|
|
55
|
+
SECTIONS
|
|
56
|
+
|
|
57
|
+
mission/, vision/, roadmap/, sprint/
|
|
58
|
+
REPRESENTS HAS_MISSION / HAS_VISION / HAS_ROADMAP / HAS_CURRENT_SPRINT
|
|
59
|
+
FORMAT PDL
|
|
60
|
+
MUTABILITY PROTECTED — superseded, never edited in place; every prior
|
|
61
|
+
version file is kept
|
|
62
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
63
|
+
|
|
64
|
+
history/
|
|
65
|
+
REPRESENTS HISTORY_ENTRY
|
|
66
|
+
FORMAT PDL
|
|
67
|
+
MUTABILITY PROTECTED — append-only
|
|
68
|
+
REQUIRED_AT_V0.1 must exist, with at least one founding entry
|
|
69
|
+
|
|
70
|
+
releases/
|
|
71
|
+
REPRESENTS RELEASE
|
|
72
|
+
FORMAT PDL
|
|
73
|
+
MUTABILITY PROTECTED — append-only
|
|
74
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
75
|
+
|
|
76
|
+
decisions/
|
|
77
|
+
REPRESENTS DECISION
|
|
78
|
+
FORMAT PDL (by explicit decision — consistency over the old system's
|
|
79
|
+
mixed Markdown/PDL convention for this kind of record)
|
|
80
|
+
MUTABILITY PROTECTED — append-only
|
|
81
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
82
|
+
|
|
83
|
+
requests/
|
|
84
|
+
REPRESENTS REQUEST
|
|
85
|
+
FORMAT PDL
|
|
86
|
+
MUTABILITY core fields (WHO, DATE, RAW_TEXT) PROTECTED, immutable once
|
|
87
|
+
logged. STATUS and PROMOTED_TO are the one explicit exception —
|
|
88
|
+
updatable as the request's lifecycle advances.
|
|
89
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
90
|
+
|
|
91
|
+
handoff/
|
|
92
|
+
REPRESENTS no new entity, relation or assertion — a pure representation
|
|
93
|
+
layer: a materialized view over mission/vision/roadmap/sprint and
|
|
94
|
+
ACTIVE_VERSIONS, built for AI-onboarding consumption.
|
|
95
|
+
FORMAT PDL
|
|
96
|
+
MUTABILITY REGENERABLE — fully disposable, rebuilt by the Bootstrap /
|
|
97
|
+
Handoff engine (not yet built)
|
|
98
|
+
REQUIRED_AT_V0.1 folder must exist, empty until that engine ships
|
|
99
|
+
|
|
100
|
+
memory/generated/
|
|
101
|
+
REPRESENTS computed reports (audits, continuity scores) — a regenerable
|
|
102
|
+
view, not an entity of its own
|
|
103
|
+
FORMAT JSON
|
|
104
|
+
MUTABILITY REGENERABLE — free to overwrite or discard
|
|
105
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
106
|
+
|
|
107
|
+
memory/persistent/
|
|
108
|
+
REPRESENTS HAS_CONTINUITY_NOTE
|
|
109
|
+
FORMAT PDL or JSON
|
|
110
|
+
MUTABILITY PROTECTED — refined in place, never silently discarded
|
|
111
|
+
REQUIRED_AT_V0.1 folder must exist; content optional
|
|
112
|
+
|
|
113
|
+
NOT_REPRESENTED_HERE
|
|
114
|
+
|
|
115
|
+
ACTOR entities are not defined per project instance. They live at the
|
|
116
|
+
organization's own governed-unit instance and are referenced by ID from
|
|
117
|
+
any assertion's ASSERTED_BY / APPROVED_BY field — this avoids duplicating
|
|
118
|
+
"who is the President" inside every project instance.
|
|
119
|
+
|
|
120
|
+
The DEPENDS_ON relation, as used for the PACE repository's own 12-component
|
|
121
|
+
build graph, is not represented inside a project instance. It belongs to
|
|
122
|
+
PACE's own repository, not to any .pace/ instance.
|
|
123
|
+
|
|
124
|
+
HARD_RULES
|
|
125
|
+
|
|
126
|
+
1 Every REQUIRED_AT_V0.1 folder must exist structurally, even empty,
|
|
127
|
+
from the moment pace init runs. Empty is valid; missing is not.
|
|
128
|
+
|
|
129
|
+
2 No code, script or executable file of any kind may exist anywhere
|
|
130
|
+
inside .pace/, in any section, ever.
|
|
131
|
+
|
|
132
|
+
3 PROTECTED sections are never edited in place. Any change to the
|
|
133
|
+
content of an existing file is a contract violation — only new files
|
|
134
|
+
or new versions are allowed.
|
|
135
|
+
|
|
136
|
+
4 REGENERABLE sections (handoff/, memory/generated/) may be freely
|
|
137
|
+
rewritten or deleted at any time without violating this contract.
|
|
138
|
+
|
|
139
|
+
END
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Handoff engine - regenerates the AI-onboarding view of a .pace/ instance.
|
|
2
|
+
|
|
3
|
+
Produces .pace/handoff/HANDOFF.md: the single file an AI reads first. It
|
|
4
|
+
names who governs the project (whom to consult), how to work here, and the
|
|
5
|
+
current mission / vision / roadmap / sprint, plus pointers to the deeper
|
|
6
|
+
memory. The handoff/ section is REGENERABLE: this rebuilds it from the
|
|
7
|
+
authoritative sources, never the other way around.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
14
|
+
from pace.services.pdl import read_pdl
|
|
15
|
+
|
|
16
|
+
ACTIVE_SECTIONS = [
|
|
17
|
+
("Mission", "ACTIVE_MISSION"),
|
|
18
|
+
("Vision", "ACTIVE_VISION"),
|
|
19
|
+
("Roadmap", "ACTIVE_ROADMAP"),
|
|
20
|
+
("Current sprint", "ACTIVE_SPRINT"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_section(root: Path, relative_path):
|
|
25
|
+
if not relative_path:
|
|
26
|
+
return "(not set)"
|
|
27
|
+
full = root / relative_path
|
|
28
|
+
if not full.is_file():
|
|
29
|
+
return "(file not found)"
|
|
30
|
+
return full.read_text(encoding="utf-8").strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count_pdl(root: Path, section: str) -> int:
|
|
34
|
+
d = root / section
|
|
35
|
+
if not d.is_dir():
|
|
36
|
+
return 0
|
|
37
|
+
return sum(1 for p in d.iterdir() if p.is_file() and p.suffix == ".pdl")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _root_authority(root: Path):
|
|
41
|
+
actors_dir = Path(root) / "actors"
|
|
42
|
+
if not actors_dir.is_dir():
|
|
43
|
+
return None
|
|
44
|
+
for p in sorted(actors_dir.glob("*.pdl")):
|
|
45
|
+
data = read_pdl(p)
|
|
46
|
+
if str(data.get("IS_ROOT_AUTHORITY", "")).strip().lower() == "true":
|
|
47
|
+
label = f"{data.get('ROLE', '')} {data.get('NAME', '')}".strip()
|
|
48
|
+
return label or data.get("NAME")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def generate_handoff(root: Path) -> Path:
|
|
53
|
+
"""Rebuild .pace/handoff/HANDOFF.md from the instance. Returns its path."""
|
|
54
|
+
root = Path(root)
|
|
55
|
+
instance = read_pdl(root / "INSTANCE.pdl")
|
|
56
|
+
active = read_pdl(root / "ACTIVE_VERSIONS.pdl")
|
|
57
|
+
name = instance.get("NAME", "this project")
|
|
58
|
+
kind = instance.get("KIND", "PROJECT")
|
|
59
|
+
authority = _root_authority(root)
|
|
60
|
+
|
|
61
|
+
out = []
|
|
62
|
+
out.append(f"# PACE Handoff - {name}")
|
|
63
|
+
out.append("")
|
|
64
|
+
out.append("You are an AI joining this project. Read this first: it is the")
|
|
65
|
+
out.append("project's own memory, so you do not have to ask what we are doing")
|
|
66
|
+
out.append("or why. This file is regenerated by PACE - do not edit it by hand.")
|
|
67
|
+
out.append("")
|
|
68
|
+
out.append("## How to work here")
|
|
69
|
+
out.append("")
|
|
70
|
+
if authority:
|
|
71
|
+
out.append(f"- The ROOT_AUTHORITY of this project is **{authority}**.")
|
|
72
|
+
out.append(" You ADVISE and AUDIT; they decide. Propose to them, never")
|
|
73
|
+
out.append(" override. Confirm before anything becomes official.")
|
|
74
|
+
else:
|
|
75
|
+
out.append("- You ADVISE and AUDIT; the project owner (ROOT_AUTHORITY) decides.")
|
|
76
|
+
out.append(" Propose, never override. Confirm with the owner first.")
|
|
77
|
+
out.append("- Every permanent correction is logged as a request and, once")
|
|
78
|
+
out.append(" approved, becomes a rule you must follow. Do not re-ask what an")
|
|
79
|
+
out.append(" approved rule already settles.")
|
|
80
|
+
out.append("")
|
|
81
|
+
out.append("## Identity")
|
|
82
|
+
out.append("")
|
|
83
|
+
out.append(f"- Name: {name}")
|
|
84
|
+
out.append(f"- Kind: {kind}")
|
|
85
|
+
if instance.get("ORG_REF"):
|
|
86
|
+
out.append(f"- Belongs to: {instance.get('ORG_REF')}")
|
|
87
|
+
if authority:
|
|
88
|
+
out.append(f"- Root authority: {authority}")
|
|
89
|
+
out.append("")
|
|
90
|
+
|
|
91
|
+
for label, key in ACTIVE_SECTIONS:
|
|
92
|
+
out.append(f"## {label}")
|
|
93
|
+
out.append("")
|
|
94
|
+
out.append("```")
|
|
95
|
+
out.append(_read_section(root, active.get(key)))
|
|
96
|
+
out.append("```")
|
|
97
|
+
out.append("")
|
|
98
|
+
|
|
99
|
+
out.append("## Where the rest of the memory lives")
|
|
100
|
+
out.append("")
|
|
101
|
+
out.append(f"- Decisions: .pace/decisions/ ({_count_pdl(root, 'decisions')} recorded)")
|
|
102
|
+
out.append(f"- History: .pace/history/ ({_count_pdl(root, 'history')} entries)")
|
|
103
|
+
out.append(f"- Requests: .pace/requests/ ({_count_pdl(root, 'requests')} logged in intake)")
|
|
104
|
+
out.append("")
|
|
105
|
+
out.append("Read those for the WHY behind decisions and the reasoning that is")
|
|
106
|
+
out.append("not in the code - including paths that were tried and discarded.")
|
|
107
|
+
out.append("")
|
|
108
|
+
|
|
109
|
+
handoff_dir = root / "handoff"
|
|
110
|
+
handoff_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
path = handoff_dir / "HANDOFF.md"
|
|
112
|
+
path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
113
|
+
return path
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Working-memory engine - the continuity notes an AI keeps so it does not
|
|
2
|
+
lose the thread within a long chat or across chats (the anti-"dementia"
|
|
3
|
+
layer). `remember` appends a note; `recall` prints the accumulated memory.
|
|
4
|
+
|
|
5
|
+
Lives in .pace/memory/persistent/CONTINUITY.md - refined in place, never
|
|
6
|
+
silently discarded (HAS_CONTINUITY_NOTE). Agreements that become permanent
|
|
7
|
+
graduate from here into decisions/ or history/; this file is the running
|
|
8
|
+
working memory, not the system of record.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
MEMORY_FILE = "memory/persistent/CONTINUITY.md"
|
|
15
|
+
|
|
16
|
+
_HEADER = (
|
|
17
|
+
"# Continuity memory\n\n"
|
|
18
|
+
"Running notes so an AI keeps the thread - objective, decisions, open\n"
|
|
19
|
+
"threads, latest state. Newest last. Read this every turn; do not lose\n"
|
|
20
|
+
"what was already agreed.\n\n"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _memory_path(root: Path) -> Path:
|
|
25
|
+
return Path(root) / MEMORY_FILE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def remember(root: Path, text: str) -> Path:
|
|
29
|
+
path = _memory_path(root)
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
if not path.exists():
|
|
32
|
+
path.write_text(_HEADER, encoding="utf-8")
|
|
33
|
+
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
|
|
34
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
35
|
+
handle.write(f"- [{stamp}] {text}\n")
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def recall(root: Path) -> str:
|
|
40
|
+
path = _memory_path(root)
|
|
41
|
+
if not path.is_file():
|
|
42
|
+
return '(no continuity memory yet - add notes with: pace remember "...")'
|
|
43
|
+
return path.read_text(encoding="utf-8").strip()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Project Creator engine — init and create modes.
|
|
2
|
+
|
|
3
|
+
init_instance() attaches a valid, structurally minimal .pace/ instance to
|
|
4
|
+
a project directory that already exists (its own code lives elsewhere,
|
|
5
|
+
untouched).
|
|
6
|
+
|
|
7
|
+
create_project() generates a brand-new project from scratch: a fresh
|
|
8
|
+
directory, a git repository, a minimal README, and a .pace/ instance —
|
|
9
|
+
nothing stack-specific (no framework, no language scaffolding). That is
|
|
10
|
+
a separate, later capability (templates/, not yet built), not part of
|
|
11
|
+
this minimal create mode.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
20
|
+
from pace.services.contract_loader import load_instance_contract
|
|
21
|
+
from pace.services.version import PACE_VERSION
|
|
22
|
+
|
|
23
|
+
CONTRACT_PATH = Path(__file__).resolve().parent.parent / "contracts" / "INSTANCE_CONTRACT_0.1.0.pdl"
|
|
24
|
+
CONTRACT_VERSION = "0.1.0"
|
|
25
|
+
|
|
26
|
+
PLACEHOLDER = "Not yet defined."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_pdl(path: Path, fields: dict) -> None:
|
|
30
|
+
lines = [f"{key} {value}".rstrip() for key, value in fields.items()]
|
|
31
|
+
lines.append("END")
|
|
32
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def init_instance(
|
|
37
|
+
target_dir: Path,
|
|
38
|
+
kind: str,
|
|
39
|
+
name: str,
|
|
40
|
+
slug: str,
|
|
41
|
+
org_ref: str = None,
|
|
42
|
+
mission: str = PLACEHOLDER,
|
|
43
|
+
vision: str = PLACEHOLDER,
|
|
44
|
+
roadmap: str = PLACEHOLDER,
|
|
45
|
+
sprint: str = PLACEHOLDER,
|
|
46
|
+
) -> Path:
|
|
47
|
+
"""Create a minimal, structurally valid .pace/ instance inside
|
|
48
|
+
`target_dir`. Raises if one already exists there."""
|
|
49
|
+
target_dir = Path(target_dir)
|
|
50
|
+
root = target_dir / ".pace"
|
|
51
|
+
if root.exists():
|
|
52
|
+
raise FileExistsError(f".pace/ already exists at {root}")
|
|
53
|
+
|
|
54
|
+
if kind == "PROJECT" and not org_ref:
|
|
55
|
+
raise ValueError("KIND=PROJECT requires org_ref")
|
|
56
|
+
|
|
57
|
+
contract = load_instance_contract(CONTRACT_PATH)
|
|
58
|
+
|
|
59
|
+
for section in sorted(contract["sections"]):
|
|
60
|
+
(root / section).mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
63
|
+
|
|
64
|
+
instance_fields = {
|
|
65
|
+
"KIND": kind,
|
|
66
|
+
"NAME": name,
|
|
67
|
+
"SLUG": slug,
|
|
68
|
+
"SCHEMA_VERSION": CONTRACT_VERSION,
|
|
69
|
+
"PACE_VERSION": PACE_VERSION,
|
|
70
|
+
"CREATED_AT": created_at,
|
|
71
|
+
}
|
|
72
|
+
if org_ref:
|
|
73
|
+
instance_fields["ORG_REF"] = org_ref
|
|
74
|
+
_write_pdl(root / "INSTANCE.pdl", instance_fields)
|
|
75
|
+
|
|
76
|
+
_write_pdl(root / "mission" / "MISSION_1.0.0.pdl", {
|
|
77
|
+
"MISSION_VERSION": "1.0.0", "STATUS": "APPROVED", "MISSION": mission,
|
|
78
|
+
})
|
|
79
|
+
_write_pdl(root / "vision" / "VISION_1.0.0.pdl", {
|
|
80
|
+
"VISION_VERSION": "1.0.0", "STATUS": "APPROVED", "VISION": vision,
|
|
81
|
+
})
|
|
82
|
+
_write_pdl(root / "roadmap" / "ROADMAP_1.0.0.pdl", {
|
|
83
|
+
"ROADMAP_VERSION": "1.0.0", "STATUS": "APPROVED", "ROADMAP": roadmap,
|
|
84
|
+
})
|
|
85
|
+
_write_pdl(root / "sprint" / "SPRINT_1.pdl", {
|
|
86
|
+
"SPRINT_VERSION": "1", "STATUS": "ACTIVE", "SPRINT": sprint,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
_write_pdl(root / "ACTIVE_VERSIONS.pdl", {
|
|
90
|
+
"ACTIVE_MISSION": "mission/MISSION_1.0.0.pdl",
|
|
91
|
+
"ACTIVE_VISION": "vision/VISION_1.0.0.pdl",
|
|
92
|
+
"ACTIVE_ROADMAP": "roadmap/ROADMAP_1.0.0.pdl",
|
|
93
|
+
"ACTIVE_SPRINT": "sprint/SPRINT_1.pdl",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
_write_pdl(root / "history" / "HISTORY-0001-FOUNDING.pdl", {
|
|
97
|
+
"HISTORY_VERSION": "1.0.0",
|
|
98
|
+
"TYPE": "Founding",
|
|
99
|
+
"STATUS": "APPROVED",
|
|
100
|
+
"TITLE": f"{name} founded as a PACE-governed instance.",
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return root
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_project(
|
|
107
|
+
target_dir: Path,
|
|
108
|
+
name: str,
|
|
109
|
+
slug: str,
|
|
110
|
+
org_ref: str,
|
|
111
|
+
mission: str = PLACEHOLDER,
|
|
112
|
+
vision: str = PLACEHOLDER,
|
|
113
|
+
roadmap: str = PLACEHOLDER,
|
|
114
|
+
sprint: str = PLACEHOLDER,
|
|
115
|
+
) -> Path:
|
|
116
|
+
"""Generate a brand-new project from scratch: creates `target_dir`
|
|
117
|
+
(must not already exist or must be empty), initializes a git
|
|
118
|
+
repository, writes a minimal README, and attaches a .pace/ instance.
|
|
119
|
+
Stack-specific scaffolding is deliberately out of scope."""
|
|
120
|
+
target_dir = Path(target_dir)
|
|
121
|
+
if target_dir.exists() and any(target_dir.iterdir()):
|
|
122
|
+
raise FileExistsError(f"{target_dir} already exists and is not empty")
|
|
123
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["git", "init"], cwd=target_dir, check=True,
|
|
127
|
+
capture_output=True, text=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
(target_dir / "README.md").write_text(
|
|
131
|
+
f"# {name}\n\nGoverned by PACE. See .pace/ for its mission, vision, roadmap and history.\n",
|
|
132
|
+
encoding="utf-8",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return init_instance(
|
|
136
|
+
target_dir, kind="PROJECT", name=name, slug=slug, org_ref=org_ref,
|
|
137
|
+
mission=mission, vision=vision, roadmap=roadmap, sprint=sprint,
|
|
138
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""PACE Kernel — locates a .pace/ instance and validates it structurally
|
|
2
|
+
against contracts/INSTANCE_CONTRACT_0.1.0.pdl, loaded at runtime from the
|
|
3
|
+
actual file, not a hand transcription of its rules.
|
|
4
|
+
|
|
5
|
+
Deliberately minimal: this is structural validation only (does the
|
|
6
|
+
required shape exist, is SCHEMA_VERSION supported). Deeper, semantic
|
|
7
|
+
validation belongs to the future Doctor engine, not the Kernel.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
14
|
+
from pace.services.fs import find_upward
|
|
15
|
+
from pace.services.pdl import read_pdl
|
|
16
|
+
from pace.services.contract_loader import load_instance_contract
|
|
17
|
+
from pace.services.validate import (
|
|
18
|
+
require_fields,
|
|
19
|
+
require_dirs,
|
|
20
|
+
require_non_empty_dir,
|
|
21
|
+
forbid_file_suffixes,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
CONTRACT_PATH = Path(__file__).resolve().parent.parent / "contracts" / "INSTANCE_CONTRACT_0.1.0.pdl"
|
|
25
|
+
|
|
26
|
+
# The schema dialects THIS Kernel build knows how to interpret — the way
|
|
27
|
+
# a browser understands a set of HTML versions, not "the one current
|
|
28
|
+
# version, everything else is outdated". Growing this set over time (as
|
|
29
|
+
# PACE adds Contract versions) means the Kernel learns to read more
|
|
30
|
+
# dialects; it does not mean older ones stop being valid. Whether a
|
|
31
|
+
# mismatch means "this instance needs pace migrate" or "this Kernel
|
|
32
|
+
# needs updating" depends on which side is actually behind — the Kernel
|
|
33
|
+
# cannot assume it's always the instance's fault.
|
|
34
|
+
SUPPORTED_SCHEMA_VERSIONS = {"0.1.0"}
|
|
35
|
+
|
|
36
|
+
# HARD_RULE 2 ("no code, script or executable file") is prose, not a
|
|
37
|
+
# machine-enumerable list — this is the Kernel's own codified reading of
|
|
38
|
+
# that rule, not something extracted from the contract text.
|
|
39
|
+
FORBIDDEN_SUFFIXES = {".py", ".ts", ".js", ".sh", ".exe", ".ps1", ".bat"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def locate_instance(start: Path = None):
|
|
43
|
+
"""Find the nearest .pace/ directory walking upward from `start`."""
|
|
44
|
+
return find_upward(".pace", start)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_instance(root: Path) -> list:
|
|
48
|
+
"""Structural validation only. Returns a list of violations; an
|
|
49
|
+
empty list means the instance is structurally valid."""
|
|
50
|
+
violations = []
|
|
51
|
+
contract = load_instance_contract(CONTRACT_PATH)
|
|
52
|
+
|
|
53
|
+
instance_file = root / "INSTANCE.pdl"
|
|
54
|
+
if not instance_file.is_file():
|
|
55
|
+
violations.append("missing INSTANCE.pdl")
|
|
56
|
+
else:
|
|
57
|
+
instance = read_pdl(instance_file)
|
|
58
|
+
# ORG_REF is excluded here: the contract marks it conditionally
|
|
59
|
+
# required (only when KIND=PROJECT), not unconditionally like
|
|
60
|
+
# the other fields — enforced explicitly just below instead.
|
|
61
|
+
required = [
|
|
62
|
+
field for field in contract["root_manifest"].get(".pace/INSTANCE.pdl", [])
|
|
63
|
+
if field != "ORG_REF"
|
|
64
|
+
]
|
|
65
|
+
violations += require_fields(instance, required, "INSTANCE.pdl")
|
|
66
|
+
if instance.get("KIND") == "PROJECT" and not instance.get("ORG_REF"):
|
|
67
|
+
violations.append("INSTANCE.pdl KIND=PROJECT requires ORG_REF")
|
|
68
|
+
schema_version = instance.get("SCHEMA_VERSION")
|
|
69
|
+
if schema_version and schema_version not in SUPPORTED_SCHEMA_VERSIONS:
|
|
70
|
+
violations.append(
|
|
71
|
+
f"SCHEMA_VERSION {schema_version} is not understood by "
|
|
72
|
+
f"this PACE engine (understands: {sorted(SUPPORTED_SCHEMA_VERSIONS)}) "
|
|
73
|
+
"— either this instance needs pace migrate, or this "
|
|
74
|
+
"Kernel needs updating; pace migrate is not yet built"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
active_versions_file = root / "ACTIVE_VERSIONS.pdl"
|
|
78
|
+
if not active_versions_file.is_file():
|
|
79
|
+
violations.append("missing ACTIVE_VERSIONS.pdl")
|
|
80
|
+
else:
|
|
81
|
+
active_versions = read_pdl(active_versions_file)
|
|
82
|
+
required = contract["root_manifest"].get(".pace/ACTIVE_VERSIONS.pdl", [])
|
|
83
|
+
violations += require_fields(active_versions, required, "ACTIVE_VERSIONS.pdl")
|
|
84
|
+
|
|
85
|
+
violations += require_dirs(root, sorted(contract["sections"]))
|
|
86
|
+
violations += require_non_empty_dir(root, "history")
|
|
87
|
+
violations += forbid_file_suffixes(root, FORBIDDEN_SUFFIXES)
|
|
88
|
+
|
|
89
|
+
return violations
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else None
|
|
94
|
+
root = locate_instance(target)
|
|
95
|
+
if root is None:
|
|
96
|
+
print("no .pace/ instance found")
|
|
97
|
+
raise SystemExit(1)
|
|
98
|
+
print(f"instance located at {root}")
|
|
99
|
+
violations = validate_instance(root)
|
|
100
|
+
if violations:
|
|
101
|
+
print(f"INVALID - {len(violations)} violation(s):")
|
|
102
|
+
for v in violations:
|
|
103
|
+
print(f" - {v}")
|
|
104
|
+
raise SystemExit(1)
|
|
105
|
+
print("VALID")
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Loads contracts/INSTANCE_CONTRACT_*.pdl at runtime, instead of the
|
|
2
|
+
Kernel hardcoding a hand transcription of its rules.
|
|
3
|
+
|
|
4
|
+
Purpose-built for the Instance Contract's known document shape, not a
|
|
5
|
+
fully general PDL engine — see protocols/PDL_SPECIFICATION_0.2.0.pdl.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pace.services.pdl_nested import split_top_level_sections, indented_field_names
|
|
11
|
+
|
|
12
|
+
TOP_LEVEL_SECTIONS = [
|
|
13
|
+
"DEPENDS_ON", "PURPOSE", "ROOT_MANIFEST", "SECTIONS",
|
|
14
|
+
"NOT_REPRESENTED_HERE", "HARD_RULES",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
ROOT_MANIFEST_FILES = [".pace/INSTANCE.pdl", ".pace/ACTIVE_VERSIONS.pdl"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_root_manifest(root_manifest_text: str) -> dict:
|
|
21
|
+
files = split_top_level_sections(root_manifest_text, ROOT_MANIFEST_FILES)
|
|
22
|
+
return {
|
|
23
|
+
name: indented_field_names(body, "REQUIRED_FIELDS", base_indent=4)
|
|
24
|
+
for name, body in files.items()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_sections(sections_text: str) -> dict:
|
|
29
|
+
"""A SECTIONS entry header is a line at indentation 0, one or more
|
|
30
|
+
comma-separated paths (e.g. 'mission/, vision/, roadmap/, sprint/'
|
|
31
|
+
or a single 'history/'). Every section in Contract 0.1.0 requires
|
|
32
|
+
its folder to exist, so this only needs to collect the paths."""
|
|
33
|
+
sections = {}
|
|
34
|
+
for line in sections_text.splitlines():
|
|
35
|
+
if not line.strip():
|
|
36
|
+
continue
|
|
37
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
38
|
+
if indent == 0:
|
|
39
|
+
for raw_path in line.strip().split(","):
|
|
40
|
+
path = raw_path.strip().rstrip("/")
|
|
41
|
+
if path:
|
|
42
|
+
sections[path] = {"required": True}
|
|
43
|
+
return sections
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_instance_contract(path: Path) -> dict:
|
|
47
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
48
|
+
first_line = text.splitlines()[0]
|
|
49
|
+
contract_version = first_line.split(" ", 1)[1].strip() if " " in first_line else ""
|
|
50
|
+
|
|
51
|
+
top = split_top_level_sections(text, TOP_LEVEL_SECTIONS)
|
|
52
|
+
return {
|
|
53
|
+
"contract_version": contract_version,
|
|
54
|
+
"root_manifest": parse_root_manifest(top.get("ROOT_MANIFEST", "")),
|
|
55
|
+
"sections": parse_sections(top.get("SECTIONS", "")),
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generic filesystem helpers shared by the Kernel and, later, Engines."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_upward(name: str, start: Path = None):
|
|
7
|
+
"""Walk upward from `start` (default: cwd) looking for a directory
|
|
8
|
+
called `name` — the way Git locates `.git/` from anywhere inside a
|
|
9
|
+
repository. Returns the matching Path, or None if not found."""
|
|
10
|
+
current = Path(start or Path.cwd()).resolve()
|
|
11
|
+
for directory in [current, *current.parents]:
|
|
12
|
+
candidate = directory / name
|
|
13
|
+
if candidate.is_dir():
|
|
14
|
+
return candidate
|
|
15
|
+
return None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Minimal PDL parser for flat key-value documents.
|
|
2
|
+
|
|
3
|
+
Implements the grammar in protocols/PDL_SPECIFICATION_0.1.0.pdl: one
|
|
4
|
+
KEY VALUE pair per line, blank lines ignored, terminated by END.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_pdl(text: str) -> dict:
|
|
11
|
+
fields = {}
|
|
12
|
+
for raw_line in text.splitlines():
|
|
13
|
+
line = raw_line.strip()
|
|
14
|
+
if not line:
|
|
15
|
+
continue
|
|
16
|
+
if line == "END":
|
|
17
|
+
break
|
|
18
|
+
if " " in line:
|
|
19
|
+
key, value = line.split(" ", 1)
|
|
20
|
+
else:
|
|
21
|
+
key, value = line, ""
|
|
22
|
+
fields[key] = value.strip()
|
|
23
|
+
return fields
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_pdl(path: Path) -> dict:
|
|
27
|
+
return parse_pdl(Path(path).read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Nested-section extraction for PDL documents that mix structural
|
|
2
|
+
fields with hand-wrapped prose (governance/ontology/contracts documents),
|
|
3
|
+
on top of pdl.py's flat grammar.
|
|
4
|
+
|
|
5
|
+
See protocols/PDL_SPECIFICATION_0.2.0.pdl for why this deliberately does
|
|
6
|
+
not auto-detect section headers: a document that uses indentation both
|
|
7
|
+
for structure and for visual line-wrap alignment cannot be told apart
|
|
8
|
+
generically without guessing. Instead, the caller supplies the small,
|
|
9
|
+
fixed vocabulary of top-level section names it expects — the same
|
|
10
|
+
technique the old system's BOOTSTRAP_CONTEXT.pdl already used via its
|
|
11
|
+
own READ_ORDER list.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def split_top_level_sections(text: str, known_sections) -> dict:
|
|
16
|
+
"""Split a document into named top-level blocks. A line is treated
|
|
17
|
+
as the start of a new section only if, once stripped, it exactly
|
|
18
|
+
matches one of `known_sections`. Everything up to the next known
|
|
19
|
+
section name (or END) becomes that section's raw body text."""
|
|
20
|
+
known = set(known_sections)
|
|
21
|
+
sections = {}
|
|
22
|
+
current_name = None
|
|
23
|
+
current_lines = []
|
|
24
|
+
|
|
25
|
+
def flush():
|
|
26
|
+
if current_name is not None:
|
|
27
|
+
sections[current_name] = "\n".join(current_lines).strip("\n")
|
|
28
|
+
|
|
29
|
+
for line in text.splitlines():
|
|
30
|
+
stripped = line.strip()
|
|
31
|
+
if stripped in known:
|
|
32
|
+
flush()
|
|
33
|
+
current_name = stripped
|
|
34
|
+
current_lines = []
|
|
35
|
+
elif stripped == "END":
|
|
36
|
+
break
|
|
37
|
+
elif current_name is not None:
|
|
38
|
+
current_lines.append(line)
|
|
39
|
+
flush()
|
|
40
|
+
return sections
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def indented_field_names(text: str, header: str, base_indent: int) -> list:
|
|
44
|
+
"""Within `text`, find the block introduced by a line equal to
|
|
45
|
+
`header`, then collect the first token of every subsequent line
|
|
46
|
+
indented at exactly `base_indent`. Lines indented deeper than
|
|
47
|
+
`base_indent` are continuations of the previous field's wrapped
|
|
48
|
+
value, not new fields, and are ignored here."""
|
|
49
|
+
names = []
|
|
50
|
+
in_block = False
|
|
51
|
+
for line in text.splitlines():
|
|
52
|
+
stripped = line.strip()
|
|
53
|
+
if not stripped:
|
|
54
|
+
continue
|
|
55
|
+
indent = len(line) - len(line.lstrip(" "))
|
|
56
|
+
if not in_block:
|
|
57
|
+
if stripped == header:
|
|
58
|
+
in_block = True
|
|
59
|
+
continue
|
|
60
|
+
if indent < base_indent:
|
|
61
|
+
break
|
|
62
|
+
if indent == base_indent:
|
|
63
|
+
names.append(stripped.split()[0])
|
|
64
|
+
return names
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Small, reusable validation helpers — generic enough for the Kernel
|
|
2
|
+
today and for a future Doctor/Validator engine, so validation logic
|
|
3
|
+
doesn't live inline inside any one caller."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require_fields(data: dict, fields, context: str) -> list:
|
|
9
|
+
violations = []
|
|
10
|
+
for field in fields:
|
|
11
|
+
if not data.get(field):
|
|
12
|
+
violations.append(f"{context} missing required field {field}")
|
|
13
|
+
return violations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require_dirs(root: Path, paths, label: str = "missing required section") -> list:
|
|
17
|
+
violations = []
|
|
18
|
+
for path in paths:
|
|
19
|
+
if not (root / path).is_dir():
|
|
20
|
+
violations.append(f"{label} {path}/")
|
|
21
|
+
return violations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def require_non_empty_dir(root: Path, path: str) -> list:
|
|
25
|
+
directory = root / path
|
|
26
|
+
if directory.is_dir() and not any(directory.iterdir()):
|
|
27
|
+
return [f"{path}/ exists but has no founding entry"]
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def forbid_file_suffixes(root: Path, suffixes) -> list:
|
|
32
|
+
violations = []
|
|
33
|
+
for path in root.rglob("*"):
|
|
34
|
+
if path.is_file() and path.suffix in suffixes:
|
|
35
|
+
violations.append(f"forbidden code/script file inside .pace/: {path.relative_to(root)}")
|
|
36
|
+
return violations
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""PACE's own engine version — the single source of truth referenced by
|
|
2
|
+
Engines (when writing a new instance) and the Kernel (when reasoning
|
|
3
|
+
about instance compatibility). Independent of SCHEMA_VERSION: the engine
|
|
4
|
+
can evolve without the Instance Contract changing, and vice versa.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
PACE_VERSION = "0.1.0"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pace-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PACE - an engineering operating system: a versioned, installable engine that preserves and governs a project's knowledge so any AI understands it in seconds.
|
|
5
|
+
Author: Cley Duarte
|
|
6
|
+
Project-URL: Homepage, https://github.com/cleyduartees-dot/PACE
|
|
7
|
+
Keywords: ai,knowledge,engineering,governance,memory,context
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# PACE
|
|
12
|
+
|
|
13
|
+
An engineering operating system: a versioned, installable engine that preserves, organizes, governs and evolves the architectural knowledge of software projects, in collaboration between people and AI.
|
|
14
|
+
|
|
15
|
+
PACE is not a library embedded inside a product. It is installed once and governs any number of independent projects, each carrying only its own `.pace/` memory — never a copy of PACE itself.
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
Founded 2026-07-06. This repository currently contains only this founding commit. The minimal day-1 skeleton (kernel, services, engines, ontology, protocols, governance, policies, standards, contracts, cli, history, docs) is the next step, not yet built.
|
|
20
|
+
|
|
21
|
+
## Founding principle
|
|
22
|
+
|
|
23
|
+
- **PACE** — the engine. Code only, versioned, installable. No knowledge of any specific organization or project.
|
|
24
|
+
- **Organización** — an adopting company's own governance memory (who holds authority, its own policies, its registry of projects), same recursive shape as a project instance, scoped one level up.
|
|
25
|
+
- **Instancia (`.pace/`)** — a single project's own memory: mission, vision, roadmap, sprint, handoff, history, decisions.
|
|
26
|
+
- **Proyecto** — the actual product codebase. Outside PACE's concern beyond hosting a `.pace/` folder at its root.
|
|
27
|
+
|
|
28
|
+
## Roadmap (founding sequence)
|
|
29
|
+
|
|
30
|
+
1. Crear el repositorio oficial PACE. — done
|
|
31
|
+
2. Implementar el esqueleto mínimo.
|
|
32
|
+
3. Implementar `pace init`.
|
|
33
|
+
4. Adoptar el primer proyecto gobernado por PACE.
|
|
34
|
+
5. Implementar `pace create`.
|
|
35
|
+
6. Crear el primer proyecto nuevo nacido desde PACE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pace/__init__.py
|
|
4
|
+
pace/cli/__init__.py
|
|
5
|
+
pace/cli/pace.py
|
|
6
|
+
pace/contracts/INSTANCE_CONTRACT_0.1.0.pdl
|
|
7
|
+
pace/contracts/__init__.py
|
|
8
|
+
pace/engines/__init__.py
|
|
9
|
+
pace/engines/handoff.py
|
|
10
|
+
pace/engines/memory.py
|
|
11
|
+
pace/engines/project_creator.py
|
|
12
|
+
pace/kernel/__init__.py
|
|
13
|
+
pace/kernel/kernel.py
|
|
14
|
+
pace/services/__init__.py
|
|
15
|
+
pace/services/contract_loader.py
|
|
16
|
+
pace/services/fs.py
|
|
17
|
+
pace/services/pdl.py
|
|
18
|
+
pace/services/pdl_nested.py
|
|
19
|
+
pace/services/validate.py
|
|
20
|
+
pace/services/version.py
|
|
21
|
+
pace_engine.egg-info/PKG-INFO
|
|
22
|
+
pace_engine.egg-info/SOURCES.txt
|
|
23
|
+
pace_engine.egg-info/dependency_links.txt
|
|
24
|
+
pace_engine.egg-info/entry_points.txt
|
|
25
|
+
pace_engine.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pace
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pace-engine"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PACE - an engineering operating system: a versioned, installable engine that preserves and governs a project's knowledge so any AI understands it in seconds."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Cley Duarte" }]
|
|
12
|
+
keywords = ["ai", "knowledge", "engineering", "governance", "memory", "context"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/cleyduartees-dot/PACE"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pace = "pace.cli.pace:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["pace", "pace.cli", "pace.kernel", "pace.services", "pace.engines", "pace.contracts"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
"pace.contracts" = ["*.pdl"]
|