@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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.12.0",
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": "ed6a52538a03cb92850cafe0cb8404c39b8e5b89",
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": "8df115fe5c27fcf6647d7e87258166616e8e2e53",
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.12.0",
4
+ "version": "4.13.0",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "ed6a52538a03cb92850cafe0cb8404c39b8e5b89",
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": "0659ad88cac5ca652eedba648d10a39ca1d51d65db638ce41b392067a3fd6128"
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": "6bdf23f46609d1b85147347d313d7c4bf13bd8bfc91d4b782fc7a311189ffbe3"
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": "8df115fe5c27fcf6647d7e87258166616e8e2e53",
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": "011615339f739316fb1350a7d4f1385a841744cf09c66da61c10e46f444c059f"
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": "a21006aec6bfa235535fdde4441cf658bb1cec32bd27a0b3840babe77440c2fb"
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": "abee3a7fbef21f1a02b1511f5d050f90d9ef51afcd6714046c25b786568fb833"
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",
@@ -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. It hides framework-specific command details while preserving the repository as the authority for how the project builds.
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 repository-owned build wrapper or documented build command;
20
- 2. an explicit package/task script whose purpose is build;
21
- 3. an unambiguous ecosystem-native build entry point already present in the repository.
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. It resolves and delegates to repository-owned runtime commands rather than requiring the developer to remember framework-specific launch syntax.
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 command from current repository evidence in this order:
17
+ Resolve the local runtime from current repository evidence in this order:
18
18
 
19
- 1. an explicit repository-owned run/dev wrapper or documented command;
20
- 2. an explicit package/task script whose purpose is local run/dev;
21
- 3. an unambiguous ecosystem-native local entry point already present in the repository.
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 any documented local ports/health endpoints;
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
- - local URL/port or health evidence only when repository/runtime evidence establishes it;
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/health result, observed application version/build identity, and endpoint when established. Do not duplicate or continuously capture unbounded application stdout/stderr into the Project Forge journal.
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 that the requested local runtime started with the evidence observed in that session.
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": bool(action == "backup" or (action == "migrate" and not request["firstInstall"])),
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 = {"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {