@mriekki/project-forge 4.4.0 → 4.4.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.
package/README.md CHANGED
@@ -126,6 +126,15 @@ npx @mriekki/project-forge@latest check
126
126
 
127
127
  Managed updates verify baselines and refuse unsafe drift or conflicts before modifying Project Forge-managed files.
128
128
 
129
+ ## Maintainer system reference
130
+
131
+ For cross-repository architecture, ownership, lifecycle, provenance, deployment, support-evidence, and governance questions, start with:
132
+
133
+ - [`docs/system/PROJECT_FORGE_SYSTEM_REQUIREMENTS.md`](docs/system/PROJECT_FORGE_SYSTEM_REQUIREMENTS.md) — normative cross-repository invariants;
134
+ - [`docs/system/PROJECT_FORGE_SYSTEM_DESIGN.md`](docs/system/PROJECT_FORGE_SYSTEM_DESIGN.md) — explanatory system map and pointers to detailed owning-repository contracts.
135
+
136
+ The system reference deliberately does not duplicate current component SHAs, package/catalog versions, support status, or implementation-level CLI/schema detail. Resolve mutable facts from the owning repositories and machine-readable Platform/Bootstrap/consumer provenance.
137
+
129
138
  ## Maintainer release process
130
139
 
131
140
  Project Forge maintainers use a gated release workflow that resolves accepted component commits, verifies the assembled package, creates the package pull request, verifies CI, and stops at the explicit publication gate.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.4.0",
4
+ "version": "4.4.1",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "deployment": {
25
25
  "repository": "mriekki/project-forge-deployment-kit",
26
- "commit": "eb157c8ae2c638ae1cd08bf27446d430835adc80",
26
+ "commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
27
27
  "distribution": "greenfield-project",
28
28
  "descriptor": "assets/deployment/distribution.json",
29
29
  "sourceRoot": "assets/generated/deployment",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.4.0",
4
+ "version": "4.4.1",
5
5
  "components": {
6
6
  "core": {
7
7
  "repository": "mriekki/project-forge-core",
@@ -328,7 +328,7 @@
328
328
  },
329
329
  "deployment": {
330
330
  "repository": "mriekki/project-forge-deployment-kit",
331
- "commit": "eb157c8ae2c638ae1cd08bf27446d430835adc80",
331
+ "commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
332
332
  "distribution": "greenfield-project",
333
333
  "files": [
334
334
  {
@@ -519,7 +519,7 @@
519
519
  {
520
520
  "source": "src/project_forge_deployment/sqlite_adapter.py",
521
521
  "destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter.py",
522
- "sha256": "b6f5fd33b776a1fb02d6b6a00fbab0b95827946e5fee51aafef9446fa084f42b"
522
+ "sha256": "011615339f739316fb1350a7d4f1385a841744cf09c66da61c10e46f444c059f"
523
523
  },
524
524
  {
525
525
  "source": "src/project_forge_deployment/sqlite_adapter_remote.py",
@@ -529,12 +529,12 @@
529
529
  {
530
530
  "source": "src/project_forge_deployment/sqlite_cli.py",
531
531
  "destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_cli.py",
532
- "sha256": "e56fe51fe92110704f709f9b4a3e89f667aa4f827dbf562262b3f89ab7637dfb"
532
+ "sha256": "abee3a7fbef21f1a02b1511f5d050f90d9ef51afcd6714046c25b786568fb833"
533
533
  },
534
534
  {
535
535
  "source": "src/project_forge_deployment/sqlite_import_recovery.py",
536
536
  "destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_import_recovery.py",
537
- "sha256": "417096927ae2f36d19df90e26ed88a1f8659a83538155d3538f660a887077f4e"
537
+ "sha256": "b047494e403963120d8667e189f20ed6310526870b56adb1f433a9b0154c7636"
538
538
  },
539
539
  {
540
540
  "source": "src/project_forge_deployment/ssh.py",
@@ -252,9 +252,18 @@ def _reservation_from_request(request: dict[str, Any]) -> dict[str, Any]:
252
252
  }
253
253
 
254
254
 
255
- def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> dict[str, Any]:
255
+ def plan_sqlite_operation(
256
+ name: str,
257
+ action: str,
258
+ request: dict[str, Any],
259
+ *,
260
+ production: bool = False,
261
+ ) -> dict[str, Any]:
256
262
  if action not in {"import", "backup", "migrate", "rollback-check"}:
257
263
  raise DeploymentKitError("Unsupported SQLite plan action.")
264
+ if not isinstance(production, bool):
265
+ raise DeploymentKitError("SQLite Production classification must be a boolean.")
266
+ production_classified = production or request["environment"] == "production"
258
267
  _require_ready_platform(name)
259
268
  inventory = sqlite_inventory(name)
260
269
  existing = _find(inventory, request["appSlug"], request["environment"])
@@ -284,7 +293,8 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
284
293
  raise DeploymentKitError(f"SQLite {action} requires an existing database.")
285
294
  return {
286
295
  "targetName": name, "action": action, "ownerRepo": request["ownerRepo"],
287
- "environment": request["environment"], "appSlug": request["appSlug"], "image": request["image"],
296
+ "environment": request["environment"], "productionClassified": production_classified,
297
+ "appSlug": request["appSlug"], "image": request["image"],
288
298
  "databaseFile": spec["databaseFile"], "runAs": spec["runAs"],
289
299
  "sourceDatabase": request["sourceDatabase"] if action == "import" else None,
290
300
  "firstInstall": bool(request["firstInstall"] if action == "migrate" else False),
@@ -295,7 +305,9 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
295
305
 
296
306
 
297
307
  def authorization_phrase(plan: dict[str, Any]) -> str:
298
- production = plan["environment"] == "production"
308
+ production = plan.get("productionClassified")
309
+ if not isinstance(production, bool):
310
+ raise DeploymentKitError("Malformed SQLite operation plan Production classification.")
299
311
  mapping = {
300
312
  "import": (SQLITE_IMPORT_WRITE_PHRASE, PRODUCTION_SQLITE_IMPORT_WRITE_PHRASE),
301
313
  "backup": (SQLITE_BACKUP_WRITE_PHRASE, PRODUCTION_SQLITE_BACKUP_WRITE_PHRASE),
@@ -307,9 +319,11 @@ def authorization_phrase(plan: dict[str, Any]) -> str:
307
319
 
308
320
 
309
321
  def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
310
- required = {"targetName", "action", "ownerRepo", "environment", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
311
- if not isinstance(plan, dict) or set(plan) != required:
322
+ required = {"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
323
+ if not isinstance(plan, dict) or set(plan) != required or not isinstance(plan["productionClassified"], bool):
312
324
  raise DeploymentKitError("Malformed SQLite operation plan.")
325
+ if plan["environment"] == "production" and not plan["productionClassified"]:
326
+ raise DeploymentKitError("Literal production environment must remain Production-classified.")
313
327
  _require_ready_platform(plan["targetName"])
314
328
  current = sqlite_inventory(plan["targetName"])
315
329
  if _snapshot(current) != plan["inventorySnapshot"]:
@@ -319,6 +333,7 @@ def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
319
333
  result = sqlite_inventory(plan["targetName"])
320
334
  result["mutated"] = True
321
335
  result["operation"] = plan["action"]; result["operationOutput"] = output.strip()
336
+ result["productionClassified"] = plan["productionClassified"]
322
337
  return result
323
338
 
324
339
 
@@ -332,4 +347,4 @@ def assert_app_spec_matches_persistence(name: str, app_spec: dict[str, Any]) ->
332
347
  if existing["runAs"] != app_spec["runAs"]:
333
348
  raise DeploymentKitError("Application runAs does not match the prepared SQLite persistence reservation.")
334
349
  if app_spec.get("persistence", {}).get("mode") != "directory":
335
- raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
350
+ raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
@@ -38,6 +38,11 @@ def _common(parser: argparse.ArgumentParser) -> None:
38
38
  parser.add_argument('name')
39
39
  parser.add_argument('--owner-repo', required=True)
40
40
  parser.add_argument('--environment', required=True)
41
+ parser.add_argument(
42
+ '--production',
43
+ action='store_true',
44
+ help="Classify this named environment as Production even when its name is not literally 'production'",
45
+ )
41
46
  parser.add_argument('--app-slug', required=True)
42
47
  parser.add_argument('--image', required=True)
43
48
  parser.add_argument('--maintenance-env', action='append', default=[], help='Non-secret image maintenance environment value in NAME=value form; repeat as needed')
@@ -68,19 +73,20 @@ def main(argv: list[str]|None=None) -> int:
68
73
  args=build_parser().parse_args(argv)
69
74
  try:
70
75
  if args.command=='inventory': _print(sqlite_inventory(args.name),as_json=args.json); return 0
76
+ production_classified = args.production or args.environment == 'production'
71
77
  request=normalize_sqlite_request(owner_repo=args.owner_repo,environment=args.environment,app_slug=args.app_slug,image=args.image,
72
78
  maintenance_environment=_parse_env(args.maintenance_env),run_as=getattr(args,'run_as',None),database_file=getattr(args,'database_file',None),
73
79
  source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False))
74
80
  if args.command=='recover-import':
75
- plan=plan_sqlite_import_recovery(args.name,request)
81
+ plan=plan_sqlite_import_recovery(args.name,request,production=production_classified)
76
82
  _print({k:v for k,v in plan.items() if k!='request'},as_json=False)
77
83
  phrase=recovery_authorization_phrase(plan)
78
84
  _confirm(args,phrase,f"recover SQLite import for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
79
85
  _print(apply_sqlite_import_recovery(plan),as_json=args.json); return 0
80
- plan=plan_sqlite_operation(args.name,args.command,request)
86
+ plan=plan_sqlite_operation(args.name,args.command,request,production=production_classified)
81
87
  _print({k:v for k,v in plan.items() if k!='spec'},as_json=False)
82
88
  phrase=authorization_phrase(plan)
83
89
  _confirm(args,phrase,f"run SQLite {args.command} for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
84
90
  _print(apply_sqlite_operation(plan),as_json=args.json); return 0
85
91
  except DeploymentKitError as error:
86
- print(f"ERROR: {error}",file=sys.stderr); return 2
92
+ print(f"ERROR: {error}",file=sys.stderr); return 2
@@ -356,13 +356,22 @@ def _snapshot(inspection: dict[str, Any]) -> str:
356
356
  return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
357
357
 
358
358
 
359
- def plan_sqlite_import_recovery(name: str, request: dict[str, Any]) -> dict[str, Any]:
359
+ def plan_sqlite_import_recovery(
360
+ name: str,
361
+ request: dict[str, Any],
362
+ *,
363
+ production: bool = False,
364
+ ) -> dict[str, Any]:
365
+ if not isinstance(production, bool):
366
+ raise DeploymentKitError("SQLite Production classification must be a boolean.")
367
+ production_classified = production or request["environment"] == "production"
360
368
  inspection = inspect_sqlite_import_recovery(name, request)
361
369
  return {
362
370
  "targetName": name,
363
371
  "action": "recover-import",
364
372
  "ownerRepo": request["ownerRepo"],
365
373
  "environment": request["environment"],
374
+ "productionClassified": production_classified,
366
375
  "appSlug": request["appSlug"],
367
376
  "image": request["image"],
368
377
  "runAs": request["runAs"],
@@ -381,22 +390,32 @@ def plan_sqlite_import_recovery(name: str, request: dict[str, Any]) -> dict[str,
381
390
 
382
391
 
383
392
  def authorization_phrase(plan: dict[str, Any]) -> str:
393
+ production = plan.get("productionClassified")
394
+ if not isinstance(production, bool):
395
+ raise DeploymentKitError("Malformed SQLite import recovery Production classification.")
384
396
  return (
385
397
  PRODUCTION_SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
386
- if plan["environment"] == "production"
398
+ if production
387
399
  else SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
388
400
  )
389
401
 
390
402
 
391
403
  def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
392
404
  required = {
393
- "targetName", "action", "ownerRepo", "environment", "appSlug", "image", "runAs",
405
+ "targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "runAs",
394
406
  "databaseFile", "sourceDatabase", "databaseSha256", "sourceSha256", "databaseBytes",
395
407
  "stateSnapshot", "overwritesDatabase", "deletesPersistentData", "createsReservation",
396
408
  "reverifiesDatabase", "request",
397
409
  }
398
- if not isinstance(plan, dict) or set(plan) != required or plan["action"] != "recover-import":
410
+ if (
411
+ not isinstance(plan, dict)
412
+ or set(plan) != required
413
+ or plan["action"] != "recover-import"
414
+ or not isinstance(plan["productionClassified"], bool)
415
+ ):
399
416
  raise DeploymentKitError("Malformed SQLite import recovery plan.")
417
+ if plan["environment"] == "production" and not plan["productionClassified"]:
418
+ raise DeploymentKitError("Literal production environment must remain Production-classified.")
400
419
  current = inspect_sqlite_import_recovery(plan["targetName"], plan["request"])
401
420
  if _snapshot(current) != plan["stateSnapshot"]:
402
421
  raise DeploymentKitError(
@@ -434,4 +453,5 @@ def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
434
453
  inventory["mutated"] = True
435
454
  inventory["operation"] = "recover-import"
436
455
  inventory["operationOutput"] = output.strip()
456
+ inventory["productionClassified"] = plan["productionClassified"]
437
457
  return inventory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.4.0",
3
+ "version": "4.4.1",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {