@mriekki/project-forge 4.12.0 → 4.13.1

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.
Files changed (22) hide show
  1. package/README.md +2 -0
  2. package/assets/components.json +3 -3
  3. package/assets/core/distribution.json +2 -0
  4. package/assets/deployment/distribution.json +2 -0
  5. package/assets/generated/ASSEMBLY.json +35 -15
  6. package/assets/generated/core/core/instructions/project-forge-local-development.instructions.md +28 -0
  7. package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +7 -5
  8. package/assets/generated/core/core/process/HUMAN_GATES.md +21 -4
  9. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +15 -6
  10. package/assets/generated/core/core/process/LOCAL_DEVELOPMENT_COMMANDS.md +103 -0
  11. package/assets/generated/core/core/skills/build/SKILL.md +10 -5
  12. package/assets/generated/core/core/skills/run/SKILL.md +39 -13
  13. package/assets/generated/core/core/skills/start-work/SKILL.md +15 -6
  14. package/assets/generated/core/tools/vscode_work_item_status/extension/README.md +8 -4
  15. package/assets/generated/core/tools/vscode_work_item_status/extension/extension.js +79 -21
  16. package/assets/generated/core/tools/vscode_work_item_status/extension/package.json +26 -2
  17. package/assets/generated/deployment/docs/RECOVERY_READINESS.md +66 -0
  18. package/assets/generated/deployment/src/project_forge_deployment/recovery_scope.py +116 -0
  19. package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter.py +27 -2
  20. package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter_remote.py +9 -6
  21. package/assets/generated/deployment/src/project_forge_deployment/sqlite_cli.py +23 -3
  22. package/package.json +1 -1
@@ -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.
@@ -165,7 +165,11 @@ PRIMARY review subject is the final proposed implementation plan. Prefer:
165
165
  2. a durable plan document when one exists and is useful;
166
166
  3. otherwise the full substantive plan in the conversation.
167
167
 
168
- If a durable plan is a repository artifact and VS Code can invoke the Project Forge companion extension, use `projectForge.openReviewArtifact` with the exact workspace-relative path. If the host exposes a stronger native plan/review surface, use that instead.
168
+ If a durable plan is a repository artifact, resolve its exact work-item-specific path from the current work-item authority/gate state before opening it. Do not select a global plan, another work item's artifact, or a focus-derived file.
169
+
170
+ In VS Code, when the Project Forge companion review command/tool is available, request presentation of that exact artifact. Record the actual presentation outcome when the host reports one, including rendered Markdown vs editor fallback vs unavailable/failed. If the host exposes a stronger native plan/review surface, use that instead.
171
+
172
+ If automatic opening is unavailable or fails, keep the gate usable: include the exact plan link/path inside the question and provide a deterministic manual fallback. For Markdown in VS Code on Windows, tell the human to open the exact linked file and press `Ctrl+Shift+V`.
169
173
 
170
174
  Before rendering the gate, state explicitly:
171
175
 
@@ -180,7 +184,9 @@ IMPLEMENTATION PLAN REVIEW REQUIRED
180
184
 
181
185
  Work item: <ID / key / concise change>
182
186
  Risk: <STANDARD | ELEVATED>
183
- Review subject: <native plan / exact plan path>
187
+ Approval subject: final proposed implementation plan
188
+ Primary artifact: <exact clickable work-item plan link/path | native plan surface>
189
+ Presentation: <rendered Markdown opened | editor fallback opened | automatic opening unavailable/failed | native plan foregrounded>
184
190
 
185
191
  Plan covers:
186
192
  - approach
@@ -200,9 +206,11 @@ Implementation has NOT started.
200
206
  4. Stop [NO ACTION]
201
207
  ```
202
208
 
203
- When the host exposes structured choice UI, render the actions as a single-select control rather than a numbered menu while preserving the consequence labels. Do not preselect or infer `Approve plan and begin implementation` merely because the plan exists or the human invoked `/start-work`.
209
+ The exact PRIMARY artifact link/path must be present in the approval question itself; do not rely on a link in earlier commentary. The artifact named in the gate must be the same artifact requested/opened for presentation.
210
+
211
+ When the host exposes structured choice UI, render the actions as a single-select control rather than a numbered menu while preserving the consequence labels and the same work-item/artifact context in the question. Do not preselect or infer `Approve plan and begin implementation` merely because the plan exists, was opened, or the human invoked `/start-work`.
204
212
 
205
- Only option 1 (or equivalent explicit free-text authorization) authorizes substantive implementation. Options 2-4 do not.
213
+ Only option 1 (or equivalent explicit free-text authorization) authorizes substantive implementation. Options 2-4 do not. Preview, navigation, cancellation, or a question asking where the plan is never counts as approval.
206
214
 
207
215
  ### Material plan changes after approval
208
216
 
@@ -222,10 +230,11 @@ APPROVED PLAN CHANGED
222
230
 
223
231
  Material change: ...
224
232
  Reason: ...
225
- Revised plan: ...
233
+ Revised PRIMARY artifact: <exact revised plan link/path | native plan surface>
234
+ Presentation: <actual presentation evidence>
226
235
  ```
227
236
 
228
- Foreground the revised PRIMARY plan review surface when supported, then require renewed plan approval. Do not silently continue implementation under stale authorization.
237
+ Foreground the revised PRIMARY plan review surface when supported, then require renewed plan approval using the revised exact link/path inside the question. Do not silently continue implementation under stale authorization or reuse stale presentation evidence.
229
238
 
230
239
  ## 6. Working branch setup
231
240
 
@@ -1,6 +1,6 @@
1
1
  # Project Forge Work Item Status
2
2
 
3
- This optional local VS Code extension decorates Project Forge work-item folders in the Explorer from repository evidence and provides a read-only helper for Project Forge human-review surfaces.
3
+ This optional local VS Code extension decorates Project Forge work-item folders in the Explorer from repository evidence and provides read-only helpers for Project Forge human-review surfaces.
4
4
 
5
5
  ## Work-item status
6
6
 
@@ -18,12 +18,16 @@ Project Forge workflows may use **Project Forge: Open Review Artifact** immediat
18
18
 
19
19
  The command accepts an exact workspace-relative repository artifact path when invoked programmatically. When run manually from the Command Palette without a path, it asks for the workspace-relative path.
20
20
 
21
+ On VS Code hosts that support the Language Model Tool API, the extension also contributes the read-only `projectForge_openReviewArtifact` tool so an agent can request the same exact presentation operation without arbitrary command execution. Older supported VS Code hosts keep the command/manual fallback.
22
+
21
23
  Behavior:
22
24
 
23
- - Markdown files open in rendered Markdown Preview when available;
25
+ - Markdown files request rendered Markdown Preview first;
26
+ - when Markdown Preview is unavailable, the helper reports that fact and falls back to normal editor preview;
24
27
  - other repository artifacts open in the normal VS Code editor preview;
25
28
  - targets outside the currently open workspace are rejected;
26
- - missing or ambiguous multi-root targets are reported rather than guessed;
29
+ - missing or ambiguous multi-root targets are reported rather than guessed from active editor focus;
30
+ - the presentation result distinguishes the resolved URI, requested mode, actual opened mode, cancellation, and failure;
27
31
  - opening/previewing an artifact is read-only presentation and never records approval.
28
32
 
29
- Project Forge should foreground only the PRIMARY review subject automatically. Supporting CONTEXT and TRACEABILITY documents remain available without stealing focus unless the human chooses to open them.
33
+ Project Forge should foreground only the exact PRIMARY review subject automatically. Supporting CONTEXT and TRACEABILITY documents remain available without stealing focus unless the human chooses to open them. The approval question itself must still identify and link the same artifact; a successful open is presentation evidence only.
@@ -13,6 +13,7 @@ const ID_FROM_NAME = /^([A-Za-z][A-Za-z0-9]*-\d+)(?:-|$)/;
13
13
  const ID_FROM_BRANCH = /(?:^|\/)([A-Za-z][A-Za-z0-9]*-\d+)(?:-|\/|$)/i;
14
14
  const MARKER = /<!--\s*project-forge-work-item:\s*([A-Za-z][A-Za-z0-9]*-\d+)\s*-->/gi;
15
15
  const REFRESH_INTERVAL_MS = 120000;
16
+ const REVIEW_TOOL_NAME = 'projectForge_openReviewArtifact';
16
17
 
17
18
  function normalizeId(value) {
18
19
  return value ? value.toUpperCase() : undefined;
@@ -242,32 +243,42 @@ async function resolveReviewUri(target) {
242
243
  throw new Error(`Review artifact was not found in the open workspace: ${requested}`);
243
244
  }
244
245
 
245
- const activeEditor = vscode.window.activeTextEditor;
246
- const activeFolder = activeEditor
247
- ? vscode.workspace.getWorkspaceFolder(activeEditor.document.uri)
248
- : undefined;
249
- const activeMatch = activeFolder
250
- ? matches.find((entry) => entry.folder.uri.fsPath === activeFolder.uri.fsPath)
251
- : undefined;
252
- if (activeMatch) {
253
- return activeMatch.uri;
254
- }
255
-
256
246
  throw new Error(`Review artifact path is ambiguous across workspace folders: ${requested}`);
257
247
  }
258
248
 
259
- async function openReviewArtifact(target) {
249
+ function requestedReviewMode(uri) {
250
+ const extension = path.extname(uri.fsPath).toLowerCase();
251
+ return extension === '.md' || extension === '.markdown'
252
+ ? 'markdown-preview'
253
+ : 'editor-preview';
254
+ }
255
+
256
+ async function presentReviewArtifact(target, options = {}) {
257
+ const notify = options.notify !== false;
258
+ let uri;
260
259
  try {
261
- const uri = await resolveReviewUri(target);
260
+ uri = await resolveReviewUri(target);
262
261
  if (!uri) {
263
- return undefined;
262
+ return {
263
+ status: 'cancelled',
264
+ uri: null,
265
+ requestedMode: null,
266
+ openedMode: null,
267
+ message: 'No review artifact was selected.',
268
+ };
264
269
  }
265
270
 
266
- const extension = path.extname(uri.fsPath).toLowerCase();
267
- if (extension === '.md' || extension === '.markdown') {
271
+ const requestedMode = requestedReviewMode(uri);
272
+ if (requestedMode === 'markdown-preview') {
268
273
  try {
269
274
  await vscode.commands.executeCommand('markdown.showPreview', uri);
270
- return uri.toString();
275
+ return {
276
+ status: 'opened',
277
+ uri: uri.toString(),
278
+ requestedMode,
279
+ openedMode: 'rendered-markdown',
280
+ message: 'Rendered Markdown preview opened.',
281
+ };
271
282
  } catch {
272
283
  // Fall back to the normal editor preview surface when Markdown preview is unavailable.
273
284
  }
@@ -277,11 +288,40 @@ async function openReviewArtifact(target) {
277
288
  preview: true,
278
289
  preserveFocus: false,
279
290
  });
280
- return uri.toString();
291
+ return {
292
+ status: 'opened',
293
+ uri: uri.toString(),
294
+ requestedMode,
295
+ openedMode: 'editor-preview',
296
+ message: requestedMode === 'markdown-preview'
297
+ ? 'Rendered Markdown preview was unavailable; editor preview opened instead.'
298
+ : 'Editor preview opened.',
299
+ };
281
300
  } catch (error) {
282
301
  const message = error && error.message ? error.message : String(error);
283
- await vscode.window.showWarningMessage(`Project Forge review artifact: ${message}`);
284
- return undefined;
302
+ if (notify) {
303
+ await vscode.window.showWarningMessage(`Project Forge review artifact: ${message}`);
304
+ }
305
+ return {
306
+ status: 'failed',
307
+ uri: uri ? uri.toString() : null,
308
+ requestedMode: uri ? requestedReviewMode(uri) : null,
309
+ openedMode: null,
310
+ message,
311
+ };
312
+ }
313
+ }
314
+
315
+ async function openReviewArtifact(target) {
316
+ return presentReviewArtifact(target, { notify: true });
317
+ }
318
+
319
+ class OpenReviewArtifactTool {
320
+ async invoke(options) {
321
+ const result = await presentReviewArtifact(options && options.input, { notify: false });
322
+ return new vscode.LanguageModelToolResult([
323
+ new vscode.LanguageModelTextPart(JSON.stringify(result)),
324
+ ]);
285
325
  }
286
326
  }
287
327
 
@@ -373,6 +413,18 @@ function activate(context) {
373
413
  context.subscriptions.push(
374
414
  vscode.commands.registerCommand('projectForge.openReviewArtifact', openReviewArtifact),
375
415
  );
416
+
417
+ if (
418
+ vscode.lm &&
419
+ typeof vscode.lm.registerTool === 'function' &&
420
+ typeof vscode.LanguageModelToolResult === 'function' &&
421
+ typeof vscode.LanguageModelTextPart === 'function'
422
+ ) {
423
+ context.subscriptions.push(
424
+ vscode.lm.registerTool(REVIEW_TOOL_NAME, new OpenReviewArtifactTool()),
425
+ );
426
+ }
427
+
376
428
  context.subscriptions.push(
377
429
  vscode.window.onDidChangeWindowState((event) => {
378
430
  if (event.focused) {
@@ -406,4 +458,10 @@ function activate(context) {
406
458
 
407
459
  function deactivate() {}
408
460
 
409
- module.exports = { activate, deactivate, openReviewArtifact, resolveReviewUri };
461
+ module.exports = {
462
+ activate,
463
+ deactivate,
464
+ openReviewArtifact,
465
+ presentReviewArtifact,
466
+ resolveReviewUri,
467
+ };
@@ -2,7 +2,7 @@
2
2
  "name": "project-forge-work-item-status",
3
3
  "displayName": "Project Forge Work Item Status",
4
4
  "description": "Shows evidence-backed Project Forge work-item status badges and provides read-only review-surface helpers in VS Code.",
5
- "version": "0.2.0",
5
+ "version": "0.3.0",
6
6
  "publisher": "project-forge",
7
7
  "license": "UNLICENSED",
8
8
  "engines": {
@@ -19,7 +19,8 @@
19
19
  ],
20
20
  "activationEvents": [
21
21
  "workspaceContains:docs/work-items/*",
22
- "onCommand:projectForge.openReviewArtifact"
22
+ "onCommand:projectForge.openReviewArtifact",
23
+ "onLanguageModelTool:projectForge_openReviewArtifact"
23
24
  ],
24
25
  "main": "./extension.js",
25
26
  "extensionKind": [
@@ -41,6 +42,29 @@
41
42
  "command": "projectForge.openReviewArtifact",
42
43
  "title": "Project Forge: Open Review Artifact"
43
44
  }
45
+ ],
46
+ "languageModelTools": [
47
+ {
48
+ "name": "projectForge_openReviewArtifact",
49
+ "displayName": "Open Project Forge Review Artifact",
50
+ "canBeReferencedInPrompt": true,
51
+ "toolReferenceName": "projectForgeReviewArtifact",
52
+ "userDescription": "Open an exact workspace review artifact using the safest read-only review surface available.",
53
+ "modelDescription": "Use immediately before a Project Forge artifact-based human approval gate when the exact review artifact path is already known. Opens only that exact workspace-contained file and returns structured presentation evidence. It never approves, edits, commits, or guesses an ambiguous target.",
54
+ "inputSchema": {
55
+ "type": "object",
56
+ "properties": {
57
+ "path": {
58
+ "type": "string",
59
+ "description": "Exact workspace-relative or absolute workspace-contained path of the PRIMARY review artifact. Do not infer it from editor focus."
60
+ }
61
+ },
62
+ "required": [
63
+ "path"
64
+ ],
65
+ "additionalProperties": false
66
+ }
67
+ }
44
68
  ]
45
69
  }
46
70
  }
@@ -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
+ }