@mriekki/project-forge 4.12.0 → 4.13.0
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.
- package/README.md +2 -0
- package/assets/components.json +3 -3
- package/assets/core/distribution.json +2 -0
- package/assets/deployment/distribution.json +2 -0
- package/assets/generated/ASSEMBLY.json +28 -8
- package/assets/generated/core/core/instructions/project-forge-local-development.instructions.md +28 -0
- package/assets/generated/core/core/process/LOCAL_DEVELOPMENT_COMMANDS.md +103 -0
- package/assets/generated/core/core/skills/build/SKILL.md +10 -5
- package/assets/generated/core/core/skills/run/SKILL.md +39 -13
- package/assets/generated/deployment/docs/RECOVERY_READINESS.md +66 -0
- package/assets/generated/deployment/src/project_forge_deployment/recovery_scope.py +116 -0
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter.py +27 -2
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter_remote.py +9 -6
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_cli.py +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,8 @@ Generated Projects receive an exact approved package during deterministic genera
|
|
|
113
113
|
|
|
114
114
|
That Project workflow resolves the supported exact package version and preserves the managed-baseline, drift, review, and delivery guardrails. Do not use a floating `@latest` package update as the normal Builder path: npm publication can intentionally occur before the stable public contract is advanced to the new version.
|
|
115
115
|
|
|
116
|
+
Package-managed updates follow the **Exact Managed Set** invariant: after a successful update, every path still managed by the selected package/profile has the package-defined content, and every previously managed path that the new package retires has either been removed or explicitly transferred out of Project Forge ownership. Unmanaged consumer files are not directory-mirrored or pruned. See [`docs/system/PROJECT_FORGE_EXACT_MANAGED_SET.md`](docs/system/PROJECT_FORGE_EXACT_MANAGED_SET.md).
|
|
117
|
+
|
|
116
118
|
The package CLI remains available for exact-version troubleshooting and maintainer verification. Use the exact version recorded by the Project or selected by the Project Forge workflow rather than a floating tag.
|
|
117
119
|
|
|
118
120
|
## Maintainer system reference
|
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.13.0",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"components": {
|
|
15
15
|
"core": {
|
|
16
|
-
"revision": "
|
|
16
|
+
"revision": "27f6824d1a53daa7e0c51bf286cce6767ddca3f7",
|
|
17
17
|
"version": "4.0.0",
|
|
18
18
|
"distribution": "greenfield-workspace",
|
|
19
19
|
"descriptor": "assets/core/distribution.json",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"canonicalDescriptor": "distributions/greenfield-workspace.json"
|
|
22
22
|
},
|
|
23
23
|
"deployment": {
|
|
24
|
-
"revision": "
|
|
24
|
+
"revision": "1119331414ec53e0c48b864403fabfef7998df60",
|
|
25
25
|
"distribution": "greenfield-project",
|
|
26
26
|
"descriptor": "assets/deployment/distribution.json",
|
|
27
27
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"mappings": [
|
|
6
6
|
{"source":"core/agents/reviewer.agent.md","destination":".github/agents/reviewer.agent.md"},
|
|
7
7
|
{"source":"core/instructions/project-forge-workflow.instructions.md","destination":".github/instructions/project-forge-workflow.instructions.md"},
|
|
8
|
+
{"source":"core/instructions/project-forge-local-development.instructions.md","destination":".github/instructions/project-forge-local-development.instructions.md"},
|
|
8
9
|
{"source":"core/instructions/project-forge-docs.instructions.md","destination":".github/instructions/project-forge-docs.instructions.md"},
|
|
9
10
|
{"source":"core/skills/define-feature/SKILL.md","destination":".github/skills/define-feature/SKILL.md"},
|
|
10
11
|
{"source":"core/skills/grill-me/SKILL.md","destination":".github/skills/grill-me/SKILL.md"},
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
{"source":"core/process/OPERATION_JOURNAL.md","destination":"docs/process/OPERATION_JOURNAL.md"},
|
|
49
50
|
{"source":"core/process/PROJECT_REQUIREMENTS_EXPORT.md","destination":"docs/process/PROJECT_REQUIREMENTS_EXPORT.md"},
|
|
50
51
|
{"source":"core/process/APPLICATION_VERSIONING.md","destination":"docs/process/APPLICATION_VERSIONING.md"},
|
|
52
|
+
{"source":"core/process/LOCAL_DEVELOPMENT_COMMANDS.md","destination":"docs/process/LOCAL_DEVELOPMENT_COMMANDS.md"},
|
|
51
53
|
{"source":"core/templates/PULL_REQUEST_TEMPLATE.md","destination":".github/PULL_REQUEST_TEMPLATE.md"},
|
|
52
54
|
{"source":"core/templates/PRODUCT_BRIEF_TEMPLATE.md","destination":"docs/templates/PRODUCT_BRIEF_TEMPLATE.md"},
|
|
53
55
|
{"source":"core/templates/FEATURE_BRIEF_TEMPLATE.md","destination":"docs/templates/FEATURE_BRIEF_TEMPLATE.md"},
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
{"source": "src/project_forge_deployment/profiles.py", "destination": "tools/project-forge-deployment/project_forge_deployment/profiles.py"},
|
|
43
43
|
{"source": "src/project_forge_deployment/project_topology.py", "destination": "tools/project-forge-deployment/project_forge_deployment/project_topology.py"},
|
|
44
44
|
{"source": "src/project_forge_deployment/proof_receipt.py", "destination": "tools/project-forge-deployment/project_forge_deployment/proof_receipt.py"},
|
|
45
|
+
{"source": "src/project_forge_deployment/recovery_scope.py", "destination": "tools/project-forge-deployment/project_forge_deployment/recovery_scope.py"},
|
|
45
46
|
{"source": "src/project_forge_deployment/registry_access.py", "destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py"},
|
|
46
47
|
{"source": "src/project_forge_deployment/registry_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/registry_cli.py"},
|
|
47
48
|
{"source": "src/project_forge_deployment/route_registry.py", "destination": "tools/project-forge-deployment/project_forge_deployment/route_registry.py"},
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
{"source": "docs/WORKSPACE_INSTANCES.md", "destination": "docs/project-forge/WORKSPACE_INSTANCES.md"},
|
|
63
64
|
{"source": "docs/ENVIRONMENT_PROOF.md", "destination": "docs/project-forge/ENVIRONMENT_PROOF.md"},
|
|
64
65
|
{"source": "docs/PROJECT_TOPOLOGY_BINDING.md", "destination": "docs/project-forge/PROJECT_TOPOLOGY_BINDING.md"},
|
|
66
|
+
{"source": "docs/RECOVERY_READINESS.md", "destination": "docs/project-forge/RECOVERY_READINESS.md"},
|
|
65
67
|
{"source": "docs/images/developer-environment-journey.svg", "destination": "docs/project-forge/images/developer-environment-journey.svg"},
|
|
66
68
|
{"source": "docs/images/personal-environment-architecture.svg", "destination": "docs/project-forge/images/personal-environment-architecture.svg"},
|
|
67
69
|
{"source": "docs/images/project-environment-model.svg", "destination": "docs/project-forge/images/project-environment-model.svg"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.13.0",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
|
-
"revision": "
|
|
7
|
+
"revision": "27f6824d1a53daa7e0c51bf286cce6767ddca3f7",
|
|
8
8
|
"version": "4.0.0",
|
|
9
9
|
"distribution": "greenfield-workspace",
|
|
10
10
|
"files": [
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"destination": ".github/instructions/project-forge-workflow.instructions.md",
|
|
24
24
|
"sha256": "dd440f677c9ed476bb8b7486500d8acbe2f796272582baadd4d6a60c3d1e662c"
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
"source": "core/instructions/project-forge-local-development.instructions.md",
|
|
28
|
+
"destination": ".github/instructions/project-forge-local-development.instructions.md",
|
|
29
|
+
"sha256": "e7f884f123877b21913af111cde42fc0ff0ac82abe6fd18b8f4584d749fb6c3a"
|
|
30
|
+
},
|
|
26
31
|
{
|
|
27
32
|
"source": "core/instructions/project-forge-docs.instructions.md",
|
|
28
33
|
"destination": ".github/instructions/project-forge-docs.instructions.md",
|
|
@@ -56,12 +61,12 @@
|
|
|
56
61
|
{
|
|
57
62
|
"source": "core/skills/build/SKILL.md",
|
|
58
63
|
"destination": ".github/skills/build/SKILL.md",
|
|
59
|
-
"sha256": "
|
|
64
|
+
"sha256": "e7ba7663a99603f1212930045bec420c71774e56cad963e38867242605afdad4"
|
|
60
65
|
},
|
|
61
66
|
{
|
|
62
67
|
"source": "core/skills/run/SKILL.md",
|
|
63
68
|
"destination": ".github/skills/run/SKILL.md",
|
|
64
|
-
"sha256": "
|
|
69
|
+
"sha256": "3dfc25cff7b03eafe0b8d64cc2102584c7b90793f434313f62c9e61c1b587c9e"
|
|
65
70
|
},
|
|
66
71
|
{
|
|
67
72
|
"source": "core/skills/export-requirements/SKILL.md",
|
|
@@ -238,6 +243,11 @@
|
|
|
238
243
|
"destination": "docs/process/APPLICATION_VERSIONING.md",
|
|
239
244
|
"sha256": "846abd1085501bebf3ffd8025f875dcf2bfb3f57e67110e684c605ae11c3246b"
|
|
240
245
|
},
|
|
246
|
+
{
|
|
247
|
+
"source": "core/process/LOCAL_DEVELOPMENT_COMMANDS.md",
|
|
248
|
+
"destination": "docs/process/LOCAL_DEVELOPMENT_COMMANDS.md",
|
|
249
|
+
"sha256": "44a62a68cbf211658d0e928f595b357fab3993eebbe5228ffeab9336b79e7ba6"
|
|
250
|
+
},
|
|
241
251
|
{
|
|
242
252
|
"source": "core/templates/PULL_REQUEST_TEMPLATE.md",
|
|
243
253
|
"destination": ".github/PULL_REQUEST_TEMPLATE.md",
|
|
@@ -351,7 +361,7 @@
|
|
|
351
361
|
]
|
|
352
362
|
},
|
|
353
363
|
"deployment": {
|
|
354
|
-
"revision": "
|
|
364
|
+
"revision": "1119331414ec53e0c48b864403fabfef7998df60",
|
|
355
365
|
"distribution": "greenfield-project",
|
|
356
366
|
"files": [
|
|
357
367
|
{
|
|
@@ -549,6 +559,11 @@
|
|
|
549
559
|
"destination": "tools/project-forge-deployment/project_forge_deployment/proof_receipt.py",
|
|
550
560
|
"sha256": "9b4bbb559747fc4ee3ba984a050fc6425e532527ae82f9897c73e04d8d0291b6"
|
|
551
561
|
},
|
|
562
|
+
{
|
|
563
|
+
"source": "src/project_forge_deployment/recovery_scope.py",
|
|
564
|
+
"destination": "tools/project-forge-deployment/project_forge_deployment/recovery_scope.py",
|
|
565
|
+
"sha256": "1773369f26dea95795eb971a8433699897db74d6c38223c0d93c8626c3b9f2aa"
|
|
566
|
+
},
|
|
552
567
|
{
|
|
553
568
|
"source": "src/project_forge_deployment/registry_access.py",
|
|
554
569
|
"destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py",
|
|
@@ -577,17 +592,17 @@
|
|
|
577
592
|
{
|
|
578
593
|
"source": "src/project_forge_deployment/sqlite_adapter.py",
|
|
579
594
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter.py",
|
|
580
|
-
"sha256": "
|
|
595
|
+
"sha256": "ab0fe2a1009ac1883ee7c9344c84240051648ccd1f05df9166cd81f6a216f893"
|
|
581
596
|
},
|
|
582
597
|
{
|
|
583
598
|
"source": "src/project_forge_deployment/sqlite_adapter_remote.py",
|
|
584
599
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter_remote.py",
|
|
585
|
-
"sha256": "
|
|
600
|
+
"sha256": "6c7ba2c49f8ae97a28927f694df23b7f18de1247aac4bc4dce858859fb2199ed"
|
|
586
601
|
},
|
|
587
602
|
{
|
|
588
603
|
"source": "src/project_forge_deployment/sqlite_cli.py",
|
|
589
604
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_cli.py",
|
|
590
|
-
"sha256": "
|
|
605
|
+
"sha256": "37132cc141afe9196a96fd91db55597998886d037b2baded62526c08df148f5d"
|
|
591
606
|
},
|
|
592
607
|
{
|
|
593
608
|
"source": "src/project_forge_deployment/sqlite_import_recovery.py",
|
|
@@ -649,6 +664,11 @@
|
|
|
649
664
|
"destination": "docs/project-forge/PROJECT_TOPOLOGY_BINDING.md",
|
|
650
665
|
"sha256": "50988ac1d797494bcb2d5dd6cece04f6313fb30179ca3132191dea75bcd759cd"
|
|
651
666
|
},
|
|
667
|
+
{
|
|
668
|
+
"source": "docs/RECOVERY_READINESS.md",
|
|
669
|
+
"destination": "docs/project-forge/RECOVERY_READINESS.md",
|
|
670
|
+
"sha256": "ae94a858c9431ad1f432cb75e76b532d83f69c8ca163707ac12d9ea11c60edc1"
|
|
671
|
+
},
|
|
652
672
|
{
|
|
653
673
|
"source": "docs/images/developer-environment-journey.svg",
|
|
654
674
|
"destination": "docs/project-forge/images/developer-environment-journey.svg",
|
package/assets/generated/core/core/instructions/project-forge-local-development.instructions.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Project Forge Local Development Contract
|
|
6
|
+
|
|
7
|
+
Keep Core stack-neutral and Projects operationally deterministic. Follow `docs/process/LOCAL_DEVELOPMENT_COMMANDS.md` when it exists.
|
|
8
|
+
|
|
9
|
+
When an Engineering Enabler or equivalent foundational work item first establishes executable application topology, establish unambiguous repository-owned build and run commands for the normal developer path. Do not require a particular wrapper language or invent placeholder commands before executable topology exists.
|
|
10
|
+
|
|
11
|
+
For Feature Brief and Engineering Enabler implementation/validation, default application execution and persistence operations to the established Project-owned local developer runtime and its associated persistence unless the developer explicitly selects a supported alternative. When the Project defines a local container/database pair as that topology, do not silently substitute a native process or separate checkout database.
|
|
12
|
+
|
|
13
|
+
If the established default runtime/persistence is absent, stopped, stale, or blocked by initialization/schema prerequisites, route through `/setup local` or the repository-owned repair workflow. Do not bypass it by selecting another runtime/database. Preserve `/run`'s no-implicit-setup/no-automatic-migration boundary.
|
|
14
|
+
|
|
15
|
+
When a work item materially changes executable topology, assess and maintain the affected Project-owned engineering interfaces in the same accepted change:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
BUILD CONTRACT IMPACT
|
|
19
|
+
RUN CONTRACT IMPACT
|
|
20
|
+
SETUP CONTRACT IMPACT
|
|
21
|
+
CI CONTRACT IMPACT
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Examples include build-system changes, native/container changes, component composition, persistence ownership/location, launch profiles/ports, transport/authentication requirements, readiness checks, and stop/cleanup mechanics.
|
|
25
|
+
|
|
26
|
+
Prefer the same underlying repository-owned build entry point in CI where practical. Preserve repository-required isolated/disposable test runtimes and databases; never redirect automated tests into mutable developer data merely to reuse the normal developer runtime.
|
|
27
|
+
|
|
28
|
+
A failing or stale established build/run command is a Project contract problem. Do not silently bypass it with ecosystem-native inference. Framework-native discovery remains a fallback only while no Project-owned command contract exists yet or when the developer explicitly selects a supported alternative.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Local Development Command Contract
|
|
2
|
+
|
|
3
|
+
## Principle
|
|
4
|
+
|
|
5
|
+
Project Forge Core is generic; each executable Project should become deterministic.
|
|
6
|
+
|
|
7
|
+
Bootstrap and early discovery do not need to know framework-specific build/run mechanics. Once an Engineering Enabler or equivalent foundational work item establishes executable application topology, the Project should expose unambiguous repository-owned commands for normal developer build and run behavior.
|
|
8
|
+
|
|
9
|
+
This contract does not require literal `build.ps1` or `run.ps1` files and does not introduce a `PROJECT_COMMANDS.json` registry. A repository-owned command may be a wrapper script, package/task script, Compose command, build-tool target, Make target, or another deterministic command already owned by the Project.
|
|
10
|
+
|
|
11
|
+
## Establishment
|
|
12
|
+
|
|
13
|
+
When the first foundational Engineering Enabler makes the application executable, establish the normal local developer contract as applicable:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
Build ........ authoritative Project-owned build command
|
|
17
|
+
Run .......... authoritative Project-owned developer runtime command
|
|
18
|
+
Persistence .. runtime-associated developer persistence, when applicable
|
|
19
|
+
Stop ......... how the normal developer runtime is stopped
|
|
20
|
+
Readiness .... what proves the selected runtime is actually ready
|
|
21
|
+
Transport .... required local HTTP/HTTPS/authentication properties when applicable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If application scaffolding or runtime is intentionally deferred, do not create placeholder commands merely to satisfy this contract. Ecosystem-native inference may remain a temporary fallback until executable topology exists.
|
|
25
|
+
|
|
26
|
+
## Default runtime and persistence
|
|
27
|
+
|
|
28
|
+
For Feature Brief and Engineering Enabler implementation/validation, use the established Project-owned local developer runtime and its associated persistence by default unless the developer explicitly selects a supported alternative.
|
|
29
|
+
|
|
30
|
+
When the Project defines a local container and associated database as its developer topology, that container/database pair is the default. Do not silently substitute a native process or separate checkout database merely because an ecosystem-native command is easier to invoke.
|
|
31
|
+
|
|
32
|
+
If the default runtime is absent, stopped, stale, or blocked by missing/pending schema prerequisites, route through `/setup local` or the repository-owned repair workflow. Do not bypass the problem by selecting another runtime/database.
|
|
33
|
+
|
|
34
|
+
An explicit developer selection may authorize a supported alternative within the relevant operation's existing boundaries. Once selected, inspect and operate only against that runtime and its persistence.
|
|
35
|
+
|
|
36
|
+
Automated tests remain separate: preserve isolated/disposable test databases or runtimes required by the repository's test contract. Never redirect tests into mutable developer data merely to reuse the developer run contract.
|
|
37
|
+
|
|
38
|
+
## Build command lifecycle
|
|
39
|
+
|
|
40
|
+
`/build` prefers the established Project-owned build command. Once that command exists, it is authoritative for the normal build target; a failing or stale wrapper is a Project contract problem, not permission to silently bypass it with framework-native fallback.
|
|
41
|
+
|
|
42
|
+
Where practical, CI should invoke the same underlying repository-owned build entry point so local and CI build semantics do not drift.
|
|
43
|
+
|
|
44
|
+
A work item that materially changes build topology should assess and update the build contract in the same accepted change. Examples include:
|
|
45
|
+
|
|
46
|
+
- changing build systems or package managers;
|
|
47
|
+
- splitting/combining application components;
|
|
48
|
+
- changing generated artifacts or build outputs;
|
|
49
|
+
- moving from native build orchestration to containerized build orchestration;
|
|
50
|
+
- changing required build-time configuration or dependency preparation.
|
|
51
|
+
|
|
52
|
+
## Run command lifecycle
|
|
53
|
+
|
|
54
|
+
`/run` prefers the established Project-owned runtime and associated persistence. A work item that materially changes local runtime topology should assess and update the run contract in the same accepted change. Examples include:
|
|
55
|
+
|
|
56
|
+
- native/container topology changes;
|
|
57
|
+
- frontend/backend composition changes;
|
|
58
|
+
- persistence engine/path/volume changes;
|
|
59
|
+
- launch-profile or port changes;
|
|
60
|
+
- HTTP/HTTPS or authentication transport changes;
|
|
61
|
+
- required initialization/migration/readiness behavior;
|
|
62
|
+
- stop/cleanup mechanics.
|
|
63
|
+
|
|
64
|
+
The run command must not hide setup or destructive repair. `/run` may perform read-only prerequisite checks, but normal installation, forward initialization/migration, container convergence, or repair remain owned by `/setup` or repository-specific setup tooling.
|
|
65
|
+
|
|
66
|
+
## Readiness semantics
|
|
67
|
+
|
|
68
|
+
Process startup is not application readiness.
|
|
69
|
+
|
|
70
|
+
Keep these dimensions distinct:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Process startup
|
|
74
|
+
Artifact/version identity
|
|
75
|
+
Persistence/schema readiness
|
|
76
|
+
Transport readiness
|
|
77
|
+
Functional checks actually performed
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A successful `/version` call, open database connection, or connection-only health endpoint does not prove schema compatibility, authentication usability, OAuth readiness, or another user workflow unless that condition is independently verified.
|
|
81
|
+
|
|
82
|
+
When safe non-mutating verification is unavailable, report the gap instead of inventing confidence.
|
|
83
|
+
|
|
84
|
+
## Command resolution fallback
|
|
85
|
+
|
|
86
|
+
Core remains stack-neutral. `/build` and `/run` may still infer an unambiguous ecosystem-native command when no Project-owned command exists yet. That is a fallback for early/incomplete Projects, not the preferred mature state.
|
|
87
|
+
|
|
88
|
+
Once a Project-owned command exists, do not silently fall through to a different native command because the authoritative command fails. Fix or explicitly change the Project contract through the relevant accepted work item.
|
|
89
|
+
|
|
90
|
+
## Work-item maintenance rule
|
|
91
|
+
|
|
92
|
+
For Feature Brief and Engineering Enabler work that materially changes executable application topology, the implementation/review should assess these impacts before delivery:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
BUILD CONTRACT IMPACT
|
|
96
|
+
RUN CONTRACT IMPACT
|
|
97
|
+
SETUP CONTRACT IMPACT
|
|
98
|
+
CI CONTRACT IMPACT
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Update affected Project-owned commands and documentation in the same work item rather than leaving them for rediscovery in a later session.
|
|
102
|
+
|
|
103
|
+
This is an engineering-interface maintenance rule, not a requirement that every work item touch build/run files.
|
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
|
|
9
9
|
# Build
|
|
10
10
|
|
|
11
|
-
`/build` is a thin local helper.
|
|
11
|
+
`/build` is a thin local helper. Core stays stack-neutral; the Project becomes deterministic by exposing a repository-owned build command once executable application topology exists. Follow `docs/process/LOCAL_DEVELOPMENT_COMMANDS.md` when present.
|
|
12
12
|
|
|
13
13
|
## Contract
|
|
14
14
|
|
|
@@ -16,12 +16,17 @@ Invoking `/build` authorizes one bounded local build attempt for the requested t
|
|
|
16
16
|
|
|
17
17
|
Resolve the build command from current repository evidence in this order:
|
|
18
18
|
|
|
19
|
-
1. an explicit
|
|
20
|
-
2. an explicit
|
|
21
|
-
3. an
|
|
19
|
+
1. an explicit developer-selected supported build target, when one was supplied;
|
|
20
|
+
2. an explicit repository-owned build wrapper or documented command;
|
|
21
|
+
3. an explicit package/task script whose purpose is build;
|
|
22
|
+
4. an unambiguous ecosystem-native build entry point already present in the repository, but only when the Project has not yet established a repository-owned build contract.
|
|
22
23
|
|
|
23
24
|
Do not invent a command because a framework is recognizable. If multiple materially different build targets exist and no default is documented, require a target such as `/build web`, `/build api`, or `/build all`.
|
|
24
25
|
|
|
26
|
+
Once an executable Project has an established repository-owned build command, treat that command as authoritative. If it is missing, stale, or fails because the Project topology changed, report the contract problem; do not silently bypass it with a framework-native fallback. The work item that materially changes build topology should maintain the Project-owned build command in the same accepted change.
|
|
27
|
+
|
|
28
|
+
During an intentionally deferred or first-scaffold phase, ecosystem-native inference remains a fallback. Repeated inference is not the preferred mature Project state.
|
|
29
|
+
|
|
25
30
|
## Preconditions
|
|
26
31
|
|
|
27
32
|
Before execution:
|
|
@@ -40,7 +45,7 @@ If dependencies or workstation tools are missing, recommend `/setup` rather than
|
|
|
40
45
|
Run the selected build visibly and report:
|
|
41
46
|
|
|
42
47
|
- target;
|
|
43
|
-
- resolved repository-owned command identity;
|
|
48
|
+
- resolved repository-owned command identity, or clearly labeled inference fallback when no Project-owned command exists yet;
|
|
44
49
|
- success/failure and exit code when available;
|
|
45
50
|
- produced artifact/output locations when the build contract makes them explicit;
|
|
46
51
|
- effective application release/build identity when the build exposes it;
|
|
@@ -8,47 +8,72 @@ disable-model-invocation: true
|
|
|
8
8
|
|
|
9
9
|
# Run
|
|
10
10
|
|
|
11
|
-
`/run` is the normal convenience entry point for starting the project locally.
|
|
11
|
+
`/run` is the normal convenience entry point for starting the project locally. Core stays stack-neutral; the Project becomes deterministic by exposing a repository-owned local runtime command and runtime/persistence pairing once executable application topology exists. Follow `docs/process/LOCAL_DEVELOPMENT_COMMANDS.md` when present.
|
|
12
12
|
|
|
13
13
|
## Contract
|
|
14
14
|
|
|
15
|
-
Invoking `/run` authorizes one bounded local start attempt for the requested target. It does not authorize setup, source edits, deployment, release publication, Production access, or persistent background services outside the repository's normal local-development contract.
|
|
15
|
+
Invoking `/run` authorizes one bounded local start attempt for the requested target. It does not authorize setup, source edits, migrations, database reset/replacement, deployment, release publication, Production access, or persistent background services outside the repository's normal local-development contract.
|
|
16
16
|
|
|
17
|
-
Resolve the local runtime
|
|
17
|
+
Resolve the local runtime from current repository evidence in this order:
|
|
18
18
|
|
|
19
|
-
1. an explicit
|
|
20
|
-
2.
|
|
21
|
-
3. an
|
|
19
|
+
1. an explicit developer-selected supported runtime/target, when one was supplied;
|
|
20
|
+
2. the established Project-owned default local runtime and its associated persistence, including a project-owned local container/database when the repository defines that as the developer topology;
|
|
21
|
+
3. an explicit repository-owned run/dev wrapper or documented command for that selected runtime;
|
|
22
|
+
4. an explicit package/task script whose purpose is local run/dev;
|
|
23
|
+
5. an unambiguous ecosystem-native local entry point already present in the repository, but only when the Project has not yet established a repository-owned runtime contract.
|
|
22
24
|
|
|
23
25
|
Do not guess. If the repository has multiple independent runtimes and no documented default, require a target such as `/run web`, `/run api`, or `/run all`.
|
|
24
26
|
|
|
27
|
+
For normal Feature Brief and Engineering Enabler development/validation, default to the established Project-owned local runtime and associated persistence unless the developer explicitly selects an alternative. Do not silently start a native process or use a separate checkout database merely because an ecosystem-native command is convenient.
|
|
28
|
+
|
|
29
|
+
Once a repository-owned runtime command exists, treat it as authoritative for that runtime. If it is stale or fails because topology changed, report the contract problem; do not silently bypass it with another runtime/database.
|
|
30
|
+
|
|
25
31
|
## Preconditions
|
|
26
32
|
|
|
27
33
|
Before starting:
|
|
28
34
|
|
|
29
35
|
- identify repository root and current Git state;
|
|
30
36
|
- inspect `PROJECT_BOOTSTRAP.json` and scaffold/readiness evidence when present;
|
|
37
|
+
- resolve the exact selected runtime and, when applicable, the persistence instance it owns or uses;
|
|
31
38
|
- verify the selected runtime entry point exists;
|
|
32
|
-
- inspect
|
|
39
|
+
- inspect documented local ports, health endpoints, launch profiles, and transport/authentication requirements;
|
|
33
40
|
- do not create an intentionally deferred scaffold merely to make `/run` succeed.
|
|
34
41
|
|
|
35
42
|
If runtime source is intentionally deferred or absent, return `RUN NOT AVAILABLE` and identify the owning work item or missing marker when repository evidence provides it.
|
|
36
43
|
|
|
37
44
|
If workstation tools or dependencies are missing, recommend `/setup` instead of silently installing them.
|
|
38
45
|
|
|
46
|
+
### Persistence and schema readiness
|
|
47
|
+
|
|
48
|
+
When the selected runtime requires persistence, inspect repository-owned non-mutating readiness evidence for the **exact selected persistence instance** before claiming functional readiness. This may include installed Project Forge/Deployment Kit status primitives, migration status commands, repository health/doctor commands, or other documented read-only checks.
|
|
49
|
+
|
|
50
|
+
- If initialization or required schema is known to be missing/stale, do not automatically migrate, reset, delete, replace, or switch databases. Report `PERSISTENCE BLOCKED` and route to `/setup local` or the repository-owned repair workflow.
|
|
51
|
+
- If the repository does not expose a safe way to establish schema compatibility, report `PERSISTENCE UNVERIFIED`. Do not turn a successful connection, process start, `/version`, or connection-only health check into an application-readiness claim.
|
|
52
|
+
- If the application has no persistence requirement for the selected runtime, report persistence as `N/A`; do not invent a database gate.
|
|
53
|
+
|
|
54
|
+
A stopped, absent, stale, or migration-pending Project-owned default container/database is a prerequisite problem, not permission to select a different runtime. An explicit developer selection may authorize a supported alternative within the existing `/run` boundary; inspect only that selected runtime and persistence.
|
|
55
|
+
|
|
56
|
+
### Transport/authentication readiness
|
|
57
|
+
|
|
58
|
+
Resolve documented transport prerequisites before choosing a launch profile. If normal authentication development requires HTTPS, secure cookies, or another repository-declared transport property, use the supported Project-owned profile/runtime when available or report the unresolved prerequisite. Do not disable secure-cookie policy, invent TLS/provider settings, or weaken authentication configuration to make `/run` succeed.
|
|
59
|
+
|
|
39
60
|
## Execution
|
|
40
61
|
|
|
41
|
-
Start the process in an observable developer-controlled terminal/process surface. Do not hide a long-running application in an unexplained background process.
|
|
62
|
+
Start the selected process/runtime in an observable developer-controlled terminal/process surface. Do not hide a long-running application in an unexplained background process.
|
|
42
63
|
|
|
43
|
-
Report:
|
|
64
|
+
Report these evidence dimensions separately:
|
|
44
65
|
|
|
45
|
-
- target;
|
|
66
|
+
- target and selected runtime identity;
|
|
46
67
|
- resolved command identity;
|
|
47
68
|
- process/startup result;
|
|
48
69
|
- effective application release/build identity when the running application exposes it;
|
|
49
|
-
-
|
|
70
|
+
- persistence readiness: `READY`, `BLOCKED`, `UNVERIFIED`, or `N/A`;
|
|
71
|
+
- local URL/port and transport evidence only when repository/runtime evidence establishes it;
|
|
72
|
+
- functional checks actually performed and their result;
|
|
50
73
|
- how to stop the process.
|
|
51
74
|
|
|
75
|
+
Do not equate startup, artifact identity, `/version`, or connection-only health with successful user workflows. Do not claim login, OAuth callback/provider readiness, or another functional path was verified unless that exact path was exercised successfully.
|
|
76
|
+
|
|
52
77
|
When `/run all` is explicitly supported, start only the repository-defined set of local components and make each process/endpoint visible.
|
|
53
78
|
|
|
54
79
|
When the project implements `docs/process/APPLICATION_VERSIONING.md`, prefer runtime-observed version evidence over a source-file guess. Do not synthesize a build number or claim that a static source label proves the running artifact's identity.
|
|
@@ -57,7 +82,7 @@ When the project implements `docs/process/APPLICATION_VERSIONING.md`, prefer run
|
|
|
57
82
|
|
|
58
83
|
`/run` may write a concise sanitized startup receipt under `.project-forge/logs/run/` following `docs/process/OPERATION_JOURNAL.md`.
|
|
59
84
|
|
|
60
|
-
Record startup command identity, target, timestamps, startup
|
|
85
|
+
Record selected runtime/persistence identity (without secrets), startup command identity, target, timestamps, process/startup result, persistence readiness, functional checks actually performed, observed application version/build identity, and endpoint when established. Do not duplicate or continuously capture unbounded application stdout/stderr into the Project Forge journal.
|
|
61
86
|
|
|
62
87
|
## Deployment boundary
|
|
63
88
|
|
|
@@ -66,10 +91,11 @@ Record startup command identity, target, timestamps, startup/health result, obse
|
|
|
66
91
|
It must not:
|
|
67
92
|
|
|
68
93
|
- mutate remote environments;
|
|
94
|
+
- perform setup or migration writes implicitly;
|
|
69
95
|
- publish images/packages/releases;
|
|
70
96
|
- change deployment bindings;
|
|
71
97
|
- infer Production configuration;
|
|
72
98
|
- bump or rewrite release version state;
|
|
73
99
|
- stage, commit, push, merge, or clean branches.
|
|
74
100
|
|
|
75
|
-
A successful `/run` proves only
|
|
101
|
+
A successful `/run` proves only the evidence dimensions actually observed for the selected local runtime in that session.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Recovery Readiness Contract V1
|
|
2
|
+
|
|
3
|
+
Project Forge distinguishes a verified database backup from verified application recovery. A successful SQLite backup proves only the database artifact created by the application's SQLite maintenance contract; it does not prove that every dependency required to recover authentication or the full application is available or usable.
|
|
4
|
+
|
|
5
|
+
## Recovery states
|
|
6
|
+
|
|
7
|
+
Reusable evidence must keep these concepts separate:
|
|
8
|
+
|
|
9
|
+
1. `databaseBackupVerified` — the SQLite database backup operation completed successfully.
|
|
10
|
+
2. `verifiedBackupScope` — currently `database` for SQLite backup evidence.
|
|
11
|
+
3. `recoveryScopeDeclared` — the consumer supplied a strict, non-secret recovery declaration.
|
|
12
|
+
4. `unresolvedRequiredDependencies` — required non-database dependencies that still need recovery-time verification.
|
|
13
|
+
5. `authenticationPolicyVerified` — the selected authentication recovery policy has been dynamically verified after restore.
|
|
14
|
+
6. `applicationRecoveryReady` — all required recovery dependencies and the selected policy have been verified for the restored application.
|
|
15
|
+
|
|
16
|
+
Current SQLite backup/import/migration commands never set `applicationRecoveryReady` to true. Deployment Kit does not yet provide a general restore engine.
|
|
17
|
+
|
|
18
|
+
## Recovery manifest
|
|
19
|
+
|
|
20
|
+
`sqlite backup` and existing-database `sqlite migrate` may receive `--recovery-manifest <path>` pointing to UTF-8 JSON with exactly this schema:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"schemaVersion": 1,
|
|
25
|
+
"authenticationPolicy": "invalidate-authentication",
|
|
26
|
+
"dependencies": [
|
|
27
|
+
{
|
|
28
|
+
"id": "data-protection-key-ring",
|
|
29
|
+
"type": "cryptographic-key-ring",
|
|
30
|
+
"required": true
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`authenticationPolicy` may be `preserve-authentication`, `invalidate-authentication`, or `null`. Dependency IDs/types are non-secret identifiers only. Unknown fields fail closed. Do not put paths, keys, credentials, cookies, tokens, passwords, action links, provider secrets, or secret values in the manifest.
|
|
37
|
+
|
|
38
|
+
The normalized manifest is fingerprinted deterministically and the privacy-safe declaration/readiness summary is included in SQLite evidence. Secret contents are neither archived nor printed by this contract. First-install migration, import, interrupted-import recovery, and rollback checking do not accept a recovery manifest in V1.
|
|
39
|
+
|
|
40
|
+
## Authentication recovery policies
|
|
41
|
+
|
|
42
|
+
`preserve-authentication` means a future recovery workflow must prove that required cryptographic/protector dependencies remain usable and that retaining historical authentication state is acceptable.
|
|
43
|
+
|
|
44
|
+
`invalidate-authentication` means a future recovery workflow must prove that sessions and action links from historical state cannot authenticate or mutate accounts after recovery. Merely replacing a key ring is not sufficient evidence because password hashes, security stamps, sessions, token expiry/consumption, and similar state may also exist in the restored database.
|
|
45
|
+
|
|
46
|
+
Neither policy is considered verified by a database backup alone.
|
|
47
|
+
|
|
48
|
+
## Pre-traffic invariant
|
|
49
|
+
|
|
50
|
+
A future restore operation must fail closed and keep the restored application unavailable to normal traffic until all required recovery dependencies and the selected authentication policy have been verified. Restore completion and database integrity must never be treated as equivalent to application recovery readiness.
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- **PF-REC-001** — Database backup verification must never imply whole-application recovery readiness.
|
|
55
|
+
- **PF-REC-002** — Consumers may declare non-database recovery dependencies without exposing secret values.
|
|
56
|
+
- **PF-REC-003** — Required dependencies must be verified before application recovery readiness can pass.
|
|
57
|
+
- **PF-REC-004** — Authentication-bearing consumers must select an explicit post-restore authentication policy when they want application recovery readiness.
|
|
58
|
+
- **PF-REC-005** — Historical restore must remain unavailable to normal traffic until that policy is verified.
|
|
59
|
+
- **PF-REC-006** — Incomplete or failed recovery evidence fails closed.
|
|
60
|
+
- **PF-REC-007** — Existing SQLite-only consumers retain current database backup behavior and database-only success semantics.
|
|
61
|
+
- **PF-REC-008** — Read-only planning/readiness inspection performs no restore, key manipulation, revocation, rotation, or other destructive action.
|
|
62
|
+
- **PF-REC-009** — Dynamic recovery validation uses disposable fixtures and never logs secret material.
|
|
63
|
+
|
|
64
|
+
## Non-scope
|
|
65
|
+
|
|
66
|
+
This contract does not implement generic secret archival, provider-specific secret storage, key transport, key rotation, session/token mutation, or a general database/application restore workflow. Those require separate requirements, plan approval, implementation review, and release/adoption gates.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Reusable recovery-scope declarations and non-secret readiness evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import DeploymentKitError
|
|
11
|
+
|
|
12
|
+
RECOVERY_SCHEMA_VERSION = 1
|
|
13
|
+
AUTHENTICATION_POLICIES = {"preserve-authentication", "invalidate-authentication"}
|
|
14
|
+
_ID_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$")
|
|
15
|
+
_ALLOWED_TOP = {"schemaVersion", "authenticationPolicy", "dependencies"}
|
|
16
|
+
_ALLOWED_DEPENDENCY = {"id", "type", "required"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _identifier(value: Any, field: str) -> str:
|
|
20
|
+
if not isinstance(value, str) or _ID_RE.fullmatch(value) is None:
|
|
21
|
+
raise DeploymentKitError(f"{field} must use lowercase letters, digits, dots, underscores, and internal hyphens.")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_recovery_scope(value: dict[str, Any] | None) -> dict[str, Any]:
|
|
26
|
+
"""Normalize a strict, non-secret recovery declaration.
|
|
27
|
+
|
|
28
|
+
The declaration deliberately has no free-form values, paths, credentials, or
|
|
29
|
+
provider configuration. It describes recovery dependencies without storing
|
|
30
|
+
their contents or teaching generic persistence tooling how to archive them.
|
|
31
|
+
"""
|
|
32
|
+
if value is None:
|
|
33
|
+
return {
|
|
34
|
+
"schemaVersion": RECOVERY_SCHEMA_VERSION,
|
|
35
|
+
"declared": False,
|
|
36
|
+
"authenticationPolicy": None,
|
|
37
|
+
"dependencies": [],
|
|
38
|
+
"manifestSha256": None,
|
|
39
|
+
}
|
|
40
|
+
if not isinstance(value, dict) or set(value) != _ALLOWED_TOP:
|
|
41
|
+
raise DeploymentKitError(
|
|
42
|
+
"Recovery manifest must contain exactly schemaVersion, authenticationPolicy, and dependencies."
|
|
43
|
+
)
|
|
44
|
+
if value["schemaVersion"] != RECOVERY_SCHEMA_VERSION:
|
|
45
|
+
raise DeploymentKitError(f"Recovery manifest schemaVersion must be {RECOVERY_SCHEMA_VERSION}.")
|
|
46
|
+
policy = value["authenticationPolicy"]
|
|
47
|
+
if policy is not None and policy not in AUTHENTICATION_POLICIES:
|
|
48
|
+
raise DeploymentKitError(
|
|
49
|
+
"authenticationPolicy must be preserve-authentication, invalidate-authentication, or null."
|
|
50
|
+
)
|
|
51
|
+
dependencies = value["dependencies"]
|
|
52
|
+
if not isinstance(dependencies, list):
|
|
53
|
+
raise DeploymentKitError("Recovery manifest dependencies must be an array.")
|
|
54
|
+
normalized: list[dict[str, Any]] = []
|
|
55
|
+
seen: set[str] = set()
|
|
56
|
+
for dependency in dependencies:
|
|
57
|
+
if not isinstance(dependency, dict) or set(dependency) != _ALLOWED_DEPENDENCY:
|
|
58
|
+
raise DeploymentKitError("Each recovery dependency must contain exactly id, type, and required.")
|
|
59
|
+
dependency_id = _identifier(dependency["id"], "Recovery dependency id")
|
|
60
|
+
dependency_type = _identifier(dependency["type"], "Recovery dependency type")
|
|
61
|
+
required = dependency["required"]
|
|
62
|
+
if not isinstance(required, bool):
|
|
63
|
+
raise DeploymentKitError("Recovery dependency required must be a boolean.")
|
|
64
|
+
if dependency_id in seen:
|
|
65
|
+
raise DeploymentKitError(f"Duplicate recovery dependency id: {dependency_id}")
|
|
66
|
+
seen.add(dependency_id)
|
|
67
|
+
normalized.append({"id": dependency_id, "type": dependency_type, "required": required})
|
|
68
|
+
normalized.sort(key=lambda item: item["id"])
|
|
69
|
+
canonical = {
|
|
70
|
+
"schemaVersion": RECOVERY_SCHEMA_VERSION,
|
|
71
|
+
"authenticationPolicy": policy,
|
|
72
|
+
"dependencies": normalized,
|
|
73
|
+
}
|
|
74
|
+
fingerprint = hashlib.sha256(
|
|
75
|
+
json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
76
|
+
).hexdigest()
|
|
77
|
+
return {
|
|
78
|
+
**canonical,
|
|
79
|
+
"declared": True,
|
|
80
|
+
"manifestSha256": fingerprint,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def public_recovery_scope(scope: dict[str, Any]) -> dict[str, Any]:
|
|
85
|
+
return {
|
|
86
|
+
"schemaVersion": scope["schemaVersion"],
|
|
87
|
+
"declared": scope["declared"],
|
|
88
|
+
"authenticationPolicy": scope["authenticationPolicy"],
|
|
89
|
+
"dependencies": [dict(item) for item in scope["dependencies"]],
|
|
90
|
+
"manifestSha256": scope["manifestSha256"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def recovery_readiness(scope: dict[str, Any], *, database_backup_verified: bool) -> dict[str, Any]:
|
|
95
|
+
unresolved = [item["id"] for item in scope["dependencies"] if item["required"]]
|
|
96
|
+
return {
|
|
97
|
+
"schemaVersion": RECOVERY_SCHEMA_VERSION,
|
|
98
|
+
"databaseBackupVerified": bool(database_backup_verified),
|
|
99
|
+
"verifiedBackupScope": "database" if database_backup_verified else None,
|
|
100
|
+
"recoveryScopeDeclared": scope["declared"],
|
|
101
|
+
"authenticationPolicy": scope["authenticationPolicy"],
|
|
102
|
+
"authenticationPolicyVerified": False if scope["authenticationPolicy"] is not None else None,
|
|
103
|
+
"unresolvedRequiredDependencies": unresolved,
|
|
104
|
+
"manifestSha256": scope["manifestSha256"],
|
|
105
|
+
"applicationRecoveryReady": False,
|
|
106
|
+
"readinessReason": "restore-policy-not-verified" if scope["declared"] else "recovery-scope-not-declared",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def recovery_evidence(scope: dict[str, Any], *, database_backup_verified: bool) -> dict[str, Any]:
|
|
111
|
+
"""Return the privacy-safe evidence payload persisted with a SQLite operation."""
|
|
112
|
+
readiness = recovery_readiness(scope, database_backup_verified=database_backup_verified)
|
|
113
|
+
return {
|
|
114
|
+
**readiness,
|
|
115
|
+
"dependencies": [dict(item) for item in scope["dependencies"]],
|
|
116
|
+
}
|
|
@@ -14,6 +14,7 @@ from .app_runtime import APP_ROOT
|
|
|
14
14
|
from .box_platform import platform_status
|
|
15
15
|
from .errors import DeploymentKitError
|
|
16
16
|
from .profiles import load_profile
|
|
17
|
+
from .recovery_scope import normalize_recovery_scope, public_recovery_scope, recovery_evidence, recovery_readiness
|
|
17
18
|
from .sqlite_adapter_remote import REMOTE_SCRIPT
|
|
18
19
|
from .ssh import ssh_args, verify_access
|
|
19
20
|
|
|
@@ -111,6 +112,7 @@ def normalize_sqlite_request(
|
|
|
111
112
|
maintenance_environment: dict[str, str] | Iterable[tuple[str, str]],
|
|
112
113
|
run_as: str | None = None, database_file: str | None = None,
|
|
113
114
|
source_database: str | None = None, first_install: bool = False,
|
|
115
|
+
recovery_scope: dict[str, Any] | None = None,
|
|
114
116
|
) -> dict[str, Any]:
|
|
115
117
|
request = {
|
|
116
118
|
"ownerRepo": _owner_repo(owner_repo),
|
|
@@ -122,6 +124,7 @@ def normalize_sqlite_request(
|
|
|
122
124
|
"runAs": _run_as(run_as) if run_as is not None else None,
|
|
123
125
|
"databaseFile": _database_file(database_file) if database_file is not None else None,
|
|
124
126
|
"sourceDatabase": _source_database(source_database) if source_database is not None else None,
|
|
127
|
+
"recoveryScope": normalize_recovery_scope(recovery_scope),
|
|
125
128
|
}
|
|
126
129
|
if request["sourceDatabase"] is not None:
|
|
127
130
|
if request["runAs"] is None or request["databaseFile"] is None:
|
|
@@ -140,6 +143,7 @@ def _b64(value: str) -> str:
|
|
|
140
143
|
def build_remote_script(spec: dict[str, Any] | None = None) -> str:
|
|
141
144
|
spec = spec or {}
|
|
142
145
|
env = spec.get("maintenanceEnvironment", {})
|
|
146
|
+
recovery_payload = json.dumps(spec.get("recoveryEvidence", {}), sort_keys=True, separators=(",", ":"))
|
|
143
147
|
replacements = {
|
|
144
148
|
"__APP_ROOT__": APP_ROOT,
|
|
145
149
|
"__SQLITE_SCHEMA_VERSION__": str(SQLITE_SCHEMA_VERSION),
|
|
@@ -154,6 +158,7 @@ def build_remote_script(spec: dict[str, Any] | None = None) -> str:
|
|
|
154
158
|
"__RESERVATION_SHA__": spec.get("reservationSha256", ""),
|
|
155
159
|
"__ENV_KEYS__": " ".join(f"'{_b64(k)}'" for k in env),
|
|
156
160
|
"__ENV_VALUES__": " ".join(f"'{_b64(v)}'" for v in env.values()),
|
|
161
|
+
"__RECOVERY_EVIDENCE_B64__": _b64(recovery_payload),
|
|
157
162
|
}
|
|
158
163
|
script = REMOTE_SCRIPT
|
|
159
164
|
for placeholder, value in replacements.items():
|
|
@@ -291,6 +296,12 @@ def plan_sqlite_operation(
|
|
|
291
296
|
spec.update(runAs=existing["runAs"], databaseFile=existing["databaseFile"], reservationSha256=existing["reservationSha256"])
|
|
292
297
|
if action in {"backup", "migrate", "rollback-check"} and not existing["databasePresent"]:
|
|
293
298
|
raise DeploymentKitError(f"SQLite {action} requires an existing database.")
|
|
299
|
+
creates_verified_backup = bool(action == "backup" or (action == "migrate" and not request["firstInstall"]))
|
|
300
|
+
recovery_scope = request["recoveryScope"]
|
|
301
|
+
spec["recoveryEvidence"] = recovery_evidence(
|
|
302
|
+
recovery_scope,
|
|
303
|
+
database_backup_verified=creates_verified_backup,
|
|
304
|
+
)
|
|
294
305
|
return {
|
|
295
306
|
"targetName": name, "action": action, "ownerRepo": request["ownerRepo"],
|
|
296
307
|
"environment": request["environment"], "productionClassified": production_classified,
|
|
@@ -299,7 +310,10 @@ def plan_sqlite_operation(
|
|
|
299
310
|
"sourceDatabase": request["sourceDatabase"] if action == "import" else None,
|
|
300
311
|
"firstInstall": bool(request["firstInstall"] if action == "migrate" else False),
|
|
301
312
|
"stopsManagedRuntime": bool(action == "migrate" and existing and existing["managedRuntimeRunning"]),
|
|
302
|
-
"createsVerifiedBackup":
|
|
313
|
+
"createsVerifiedBackup": creates_verified_backup,
|
|
314
|
+
"verifiedBackupScope": "database" if creates_verified_backup else None,
|
|
315
|
+
"recoveryScope": public_recovery_scope(recovery_scope),
|
|
316
|
+
"applicationRecoveryReadyAfterOperation": False,
|
|
303
317
|
"inventorySnapshot": _snapshot(inventory), "expectedReservationSha256": expected_sha, "spec": spec,
|
|
304
318
|
}
|
|
305
319
|
|
|
@@ -319,11 +333,18 @@ def authorization_phrase(plan: dict[str, Any]) -> str:
|
|
|
319
333
|
|
|
320
334
|
|
|
321
335
|
def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
322
|
-
required = {
|
|
336
|
+
required = {
|
|
337
|
+
"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image",
|
|
338
|
+
"databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup",
|
|
339
|
+
"verifiedBackupScope", "recoveryScope", "applicationRecoveryReadyAfterOperation", "inventorySnapshot",
|
|
340
|
+
"expectedReservationSha256", "spec",
|
|
341
|
+
}
|
|
323
342
|
if not isinstance(plan, dict) or set(plan) != required or not isinstance(plan["productionClassified"], bool):
|
|
324
343
|
raise DeploymentKitError("Malformed SQLite operation plan.")
|
|
325
344
|
if plan["environment"] == "production" and not plan["productionClassified"]:
|
|
326
345
|
raise DeploymentKitError("Literal production environment must remain Production-classified.")
|
|
346
|
+
if plan["applicationRecoveryReadyAfterOperation"] is not False:
|
|
347
|
+
raise DeploymentKitError("SQLite operations cannot claim application recovery readiness.")
|
|
327
348
|
_require_ready_platform(plan["targetName"])
|
|
328
349
|
current = sqlite_inventory(plan["targetName"])
|
|
329
350
|
if _snapshot(current) != plan["inventorySnapshot"]:
|
|
@@ -334,6 +355,10 @@ def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
334
355
|
result["mutated"] = True
|
|
335
356
|
result["operation"] = plan["action"]; result["operationOutput"] = output.strip()
|
|
336
357
|
result["productionClassified"] = plan["productionClassified"]
|
|
358
|
+
result["recoveryReadiness"] = recovery_readiness(
|
|
359
|
+
plan["spec"]["recoveryScope"],
|
|
360
|
+
database_backup_verified=plan["createsVerifiedBackup"],
|
|
361
|
+
)
|
|
337
362
|
return result
|
|
338
363
|
|
|
339
364
|
|
|
@@ -19,6 +19,7 @@ FIRST_INSTALL="__FIRST_INSTALL__"
|
|
|
19
19
|
RESERVATION_SHA="__RESERVATION_SHA__"
|
|
20
20
|
ENV_KEYS=(__ENV_KEYS__)
|
|
21
21
|
ENV_VALUES=(__ENV_VALUES__)
|
|
22
|
+
RECOVERY_EVIDENCE_B64="__RECOVERY_EVIDENCE_B64__"
|
|
22
23
|
|
|
23
24
|
b64d() { printf '%s' "$1" | base64 --decode; }
|
|
24
25
|
meta_value() {
|
|
@@ -121,6 +122,7 @@ decode_spec() {
|
|
|
121
122
|
RUN_AS="$(b64d "$RUN_AS_B64")"
|
|
122
123
|
DATABASE_FILE="$(b64d "$DB_FILE_B64")"
|
|
123
124
|
SOURCE_DATABASE="$(b64d "$SOURCE_DB_B64")"
|
|
125
|
+
RECOVERY_EVIDENCE_JSON="$(b64d "$RECOVERY_EVIDENCE_B64")"
|
|
124
126
|
valid_repo "$OWNER" || { echo "Invalid SQLite owner repository." >&2; exit 45; }
|
|
125
127
|
valid_slug "$ENVIRONMENT" || { echo "Invalid SQLite environment." >&2; exit 45; }
|
|
126
128
|
valid_slug "$APP_SLUG" || { echo "Invalid SQLite app slug." >&2; exit 45; }
|
|
@@ -129,6 +131,7 @@ decode_spec() {
|
|
|
129
131
|
valid_db_file "$DATABASE_FILE" || { echo "Invalid SQLite database file name." >&2; exit 45; }
|
|
130
132
|
[[ "$RESERVATION_SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "Invalid SQLite reservation fingerprint." >&2; exit 45; }
|
|
131
133
|
[[ "${#ENV_KEYS[@]}" -eq "${#ENV_VALUES[@]}" && "${#ENV_KEYS[@]}" -gt 0 ]] || { echo "Invalid SQLite maintenance environment payload." >&2; exit 45; }
|
|
134
|
+
[[ "$RECOVERY_EVIDENCE_JSON" == \{*\} ]] || { echo "Invalid SQLite recovery evidence payload." >&2; exit 45; }
|
|
132
135
|
local index key
|
|
133
136
|
for index in "${!ENV_KEYS[@]}"; do
|
|
134
137
|
key="$(b64d "${ENV_KEYS[$index]}")"
|
|
@@ -210,8 +213,8 @@ run_migrate() {
|
|
|
210
213
|
record_evidence() {
|
|
211
214
|
local root="$1" action="$2" outcome="$3" artifact="${4:-}" timestamp
|
|
212
215
|
timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
213
|
-
printf '{"timestamp":"%s","action":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","image":"%s","databaseFile":"%s","reservationSha256":"%s","artifact":"%s","outcome":"%s"}\n' \
|
|
214
|
-
"$timestamp" "$action" "$OWNER" "$ENVIRONMENT" "$APP_SLUG" "$IMAGE" "$DATABASE_FILE" "$RESERVATION_SHA" "$artifact" "$outcome" >> "$root/sqlite-evidence.jsonl"
|
|
216
|
+
printf '{"timestamp":"%s","action":"%s","ownerRepo":"%s","environment":"%s","appSlug":"%s","image":"%s","databaseFile":"%s","reservationSha256":"%s","artifact":"%s","outcome":"%s","recovery":%s}\n' \
|
|
217
|
+
"$timestamp" "$action" "$OWNER" "$ENVIRONMENT" "$APP_SLUG" "$IMAGE" "$DATABASE_FILE" "$RESERVATION_SHA" "$artifact" "$outcome" "$RECOVERY_EVIDENCE_JSON" >> "$root/sqlite-evidence.jsonl"
|
|
215
218
|
chmod 0600 "$root/sqlite-evidence.jsonl"
|
|
216
219
|
}
|
|
217
220
|
|
|
@@ -287,7 +290,7 @@ operate() {
|
|
|
287
290
|
backup_name="backup-$stamp-$DATABASE_FILE"
|
|
288
291
|
run_backup "$DATA" "$BACKUPS" "$backup_name"
|
|
289
292
|
record_evidence "$ROOT" backup backed-up "$backup_name"
|
|
290
|
-
printf 'SQLite verified backup created: %s\n' "$BACKUPS/$backup_name"
|
|
293
|
+
printf 'SQLite verified database backup created: %s\n' "$BACKUPS/$backup_name"
|
|
291
294
|
;;
|
|
292
295
|
migrate)
|
|
293
296
|
if [[ "$FIRST_INSTALL" == true ]]; then
|
|
@@ -311,12 +314,12 @@ operate() {
|
|
|
311
314
|
if [[ "$MANAGED_RUNNING" == true ]]; then docker stop "$MANAGED_CONTAINER" >/dev/null; fi
|
|
312
315
|
if ! run_migrate "$DATA"; then
|
|
313
316
|
record_evidence "$ROOT" migrate failed "$backup_name"
|
|
314
|
-
echo "SQLite migration failed; verified pre-migration backup is retained and managed runtime remains stopped." >&2
|
|
317
|
+
echo "SQLite migration failed; verified pre-migration database backup is retained and managed runtime remains stopped." >&2
|
|
315
318
|
exit 50
|
|
316
319
|
fi
|
|
317
320
|
run_check "$DATA"
|
|
318
321
|
record_evidence "$ROOT" migrate migrated "$backup_name"
|
|
319
|
-
printf 'SQLite migration completed; verified pre-migration backup: %s\n' "$BACKUPS/$backup_name"
|
|
322
|
+
printf 'SQLite migration completed; verified pre-migration database backup: %s\n' "$BACKUPS/$backup_name"
|
|
320
323
|
fi
|
|
321
324
|
;;
|
|
322
325
|
rollback-check)
|
|
@@ -337,4 +340,4 @@ case "$ACTION" in
|
|
|
337
340
|
import|backup|migrate|rollback-check) operate ;;
|
|
338
341
|
*) echo "Unsupported SQLite adapter action: $ACTION" >&2; exit 2 ;;
|
|
339
342
|
esac
|
|
340
|
-
'''
|
|
343
|
+
'''
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI for the SQLite persistence adapter."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import argparse, json, sys
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from typing import Any
|
|
5
6
|
from .errors import DeploymentKitError
|
|
6
7
|
from .sqlite_adapter import authorization_phrase, apply_sqlite_operation, normalize_sqlite_request, plan_sqlite_operation, sqlite_inventory
|
|
@@ -28,6 +29,18 @@ def _parse_env(items: list[str]) -> list[tuple[str,str]]:
|
|
|
28
29
|
return out
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
def _load_recovery_manifest(path: str | None) -> dict[str, Any] | None:
|
|
33
|
+
if path is None:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
value = json.loads(Path(path).read_text(encoding='utf-8'))
|
|
37
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
38
|
+
raise DeploymentKitError(f"Could not read recovery manifest as UTF-8 JSON: {error}") from error
|
|
39
|
+
if not isinstance(value, dict):
|
|
40
|
+
raise DeploymentKitError("Recovery manifest root must be a JSON object.")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
31
44
|
def _confirm(args: argparse.Namespace, phrase: str, action: str) -> None:
|
|
32
45
|
if args.yes: return
|
|
33
46
|
if input(f"Type {phrase} to {action}: ").strip() != phrase:
|
|
@@ -50,6 +63,10 @@ def _common(parser: argparse.ArgumentParser) -> None:
|
|
|
50
63
|
parser.add_argument('--yes', action='store_true')
|
|
51
64
|
|
|
52
65
|
|
|
66
|
+
def _recovery_manifest_arg(parser: argparse.ArgumentParser) -> None:
|
|
67
|
+
parser.add_argument('--recovery-manifest', help='Optional strict non-secret JSON declaration of recovery dependencies and authentication policy')
|
|
68
|
+
|
|
69
|
+
|
|
53
70
|
def _import_args(parser: argparse.ArgumentParser) -> None:
|
|
54
71
|
parser.add_argument('--run-as', required=True)
|
|
55
72
|
parser.add_argument('--database-file', required=True)
|
|
@@ -62,8 +79,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
62
79
|
inv=sub.add_parser('inventory',help='Read SQLite persistence reservations without writes'); inv.add_argument('name'); inv.add_argument('--json',action='store_true')
|
|
63
80
|
imp=sub.add_parser('import',help='Import an existing SQLite DB via the exact image online-backup contract'); _common(imp); _import_args(imp)
|
|
64
81
|
recover=sub.add_parser('recover-import',help='Recover a verified failed import without overwriting or recopying the database'); _common(recover); _import_args(recover)
|
|
65
|
-
backup=sub.add_parser('backup',help='Create a verified SQLite backup with the exact image'); _common(backup)
|
|
66
|
-
migrate=sub.add_parser('migrate',help='Create required backup and run exact-image SQLite migration'); _common(migrate)
|
|
82
|
+
backup=sub.add_parser('backup',help='Create a verified SQLite database backup with the exact image'); _common(backup); _recovery_manifest_arg(backup)
|
|
83
|
+
migrate=sub.add_parser('migrate',help='Create required database backup and run exact-image SQLite migration'); _common(migrate); _recovery_manifest_arg(migrate)
|
|
67
84
|
migrate.add_argument('--first-install',action='store_true'); migrate.add_argument('--run-as'); migrate.add_argument('--database-file')
|
|
68
85
|
check=sub.add_parser('rollback-check',help='Verify that an exact rollback image accepts the current SQLite state'); _common(check)
|
|
69
86
|
return parser
|
|
@@ -73,10 +90,13 @@ def main(argv: list[str]|None=None) -> int:
|
|
|
73
90
|
args=build_parser().parse_args(argv)
|
|
74
91
|
try:
|
|
75
92
|
if args.command=='inventory': _print(sqlite_inventory(args.name),as_json=args.json); return 0
|
|
93
|
+
if args.command=='migrate' and getattr(args,'first_install',False) and getattr(args,'recovery_manifest',None):
|
|
94
|
+
raise DeploymentKitError("--recovery-manifest is supported only for existing-database SQLite migration, not --first-install.")
|
|
76
95
|
production_classified = args.production or args.environment == 'production'
|
|
77
96
|
request=normalize_sqlite_request(owner_repo=args.owner_repo,environment=args.environment,app_slug=args.app_slug,image=args.image,
|
|
78
97
|
maintenance_environment=_parse_env(args.maintenance_env),run_as=getattr(args,'run_as',None),database_file=getattr(args,'database_file',None),
|
|
79
|
-
source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False)
|
|
98
|
+
source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False),
|
|
99
|
+
recovery_scope=_load_recovery_manifest(getattr(args,'recovery_manifest',None)))
|
|
80
100
|
if args.command=='recover-import':
|
|
81
101
|
plan=plan_sqlite_import_recovery(args.name,request,production=production_classified)
|
|
82
102
|
_print({k:v for k,v in plan.items() if k!='request'},as_json=False)
|