prizmkit 1.1.159 → 1.1.160

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 (77) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/codebuddy/settings-adapter.js +76 -25
  3. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +9 -2
  4. package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +42 -15
  5. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  6. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +1 -1
  7. package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +6 -6
  8. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +4 -3
  9. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +2 -2
  10. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -2
  11. package/bundled/dev-pipeline/tests/test_runtime_commit.py +68 -13
  12. package/bundled/dev-pipeline/tests/test_unified_cli.py +10 -6
  13. package/bundled/rules/prizm/prizm-commit-workflow.md +7 -7
  14. package/bundled/rules/prizm/prizm-documentation.md +24 -65
  15. package/bundled/rules/prizm/prizm-progressive-loading.md +9 -8
  16. package/bundled/skills/_metadata.json +1 -1
  17. package/bundled/skills/prizmkit-code-review/SKILL.md +1 -1
  18. package/bundled/skills/prizmkit-committer/SKILL.md +48 -41
  19. package/bundled/skills/prizmkit-deploy/SKILL.md +172 -198
  20. package/bundled/skills/prizmkit-deploy/references/capability-contract.md +137 -0
  21. package/bundled/skills/prizmkit-deploy/references/deploy-record-schema.json +1053 -0
  22. package/bundled/skills/prizmkit-deploy/references/deployment-declaration-schema.json +291 -0
  23. package/bundled/skills/prizmkit-deploy/references/legacy-migration.md +90 -0
  24. package/bundled/skills/prizmkit-deploy/references/policy-and-execution.md +170 -0
  25. package/bundled/skills/prizmkit-deploy/references/real-resource-test-policy.md +73 -0
  26. package/bundled/skills/prizmkit-deploy/references/verification-and-recovery.md +164 -0
  27. package/bundled/skills/prizmkit-deploy/scripts/validate-deploy-artifact.py +371 -0
  28. package/bundled/skills/prizmkit-implement/SKILL.md +2 -2
  29. package/bundled/skills/prizmkit-init/SKILL.md +17 -17
  30. package/bundled/skills/prizmkit-init/references/update-supplement.md +5 -5
  31. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +17 -11
  32. package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +95 -155
  33. package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +6 -5
  34. package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +4 -2
  35. package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +9 -10
  36. package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +6 -5
  37. package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +13 -12
  38. package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -3
  39. package/bundled/skills/prizmkit-test/SKILL.md +26 -13
  40. package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +8 -4
  41. package/bundled/skills/prizmkit-test/references/independent-test-review.md +7 -3
  42. package/bundled/skills/prizmkit-test/references/test-coverage-model.md +33 -10
  43. package/bundled/skills/prizmkit-test/references/test-report-template.md +33 -11
  44. package/bundled/skills/prizmkit-workflow/SKILL.md +5 -7
  45. package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +5 -6
  46. package/bundled/templates/hooks/commit-intent-status.py +9 -41
  47. package/bundled/templates/hooks/commit-intent.json +3 -3
  48. package/bundled/templates/hooks/diff-prizm-docs.py +3 -1
  49. package/bundled/templates/hooks/prizm-pre-commit.py +32 -6
  50. package/bundled/templates/hooks/run-python-hook.cjs +44 -0
  51. package/bundled/templates/hooks/validate-prizm-docs.py +399 -173
  52. package/bundled/templates/project-memory-template.md +19 -19
  53. package/package.json +1 -1
  54. package/src/clean.js +6 -7
  55. package/src/gitignore-template.js +8 -12
  56. package/src/scaffold.js +125 -71
  57. package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +0 -89
  58. package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +0 -145
  59. package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +0 -109
  60. package/bundled/skills/prizmkit-deploy/references/data-safety-examples.md +0 -120
  61. package/bundled/skills/prizmkit-deploy/references/database-setup.md +0 -65
  62. package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +0 -150
  63. package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +0 -78
  64. package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +0 -49
  65. package/bundled/skills/prizmkit-deploy/references/deployment-modes.md +0 -50
  66. package/bundled/skills/prizmkit-deploy/references/direct-upload.md +0 -26
  67. package/bundled/skills/prizmkit-deploy/references/dns-setup.md +0 -42
  68. package/bundled/skills/prizmkit-deploy/references/docker-deploy.md +0 -31
  69. package/bundled/skills/prizmkit-deploy/references/firewall-setup.md +0 -37
  70. package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +0 -21
  71. package/bundled/skills/prizmkit-deploy/references/nginx-blue-green.md +0 -59
  72. package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +0 -164
  73. package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +0 -221
  74. package/bundled/skills/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
  75. package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
  76. package/bundled/skills/prizmkit-deploy/references/ssh-takeover.md +0 -20
  77. package/bundled/skills/prizmkit-deploy/references/ssl-setup.md +0 -56
@@ -0,0 +1,164 @@
1
+ # Deployment Verification and Recovery Contract
2
+
3
+ Use this contract after each planned step, after any failure or interruption, and before selecting a terminal status. Verification observes actual state; it does not infer success from a command exit, API acceptance, or user action alone.
4
+
5
+ ## Layered Verification
6
+
7
+ Evaluate all five layers and record each named layer exactly once:
8
+
9
+ ```text
10
+ PASSED, FAILED, INACCESSIBLE, NOT_APPLICABLE
11
+ ```
12
+
13
+ | Layer | Evidence | Applicability rule |
14
+ |---|---|---|
15
+ | `command_or_platform` | Exit/result plus target-native operation identity and completion state, not merely request acceptance | Applicable to every executed automated action; manual-only operations explain `NOT_APPLICABLE`. |
16
+ | `runtime_or_release` | Intended process/service/container/release exists, has expected identity/version, and is in the required state | Applicable when the operation creates, starts, stops, restarts, promotes, or rolls back a runtime/release. |
17
+ | `configured_health` | Every configured required project health check meets its explicit expectation | Applicable when a health boundary exists; absence is reported and never replaced with an invented endpoint. |
18
+ | `external_url` | Intended externally observed endpoint reaches the expected release/behavior through the real traffic path | Applicable when an external URL is configured and accessible from the controller boundary. |
19
+ | `startup_logs` | Bounded, redacted project-owned startup window contains no planned fatal condition | Applicable when startup output exists and access is authorized; specify time and/or line bound. |
20
+
21
+ For every layer store whether it is required and a concise redacted evidence statement. `INACCESSIBLE` and `NOT_APPLICABLE` are not passes. A required layer must be `PASSED` for `SUCCEEDED`; required `FAILED`, `INACCESSIBLE`, or `NOT_APPLICABLE` prevents success. Optional inaccessible or inapplicable layers remain visible in residual risk. Before persisting or trusting the result, validate the complete record with both `${SKILL_DIR}/references/deploy-record-schema.json` and `${SKILL_DIR}/scripts/validate-deploy-artifact.py --kind record` so omitted, duplicate, or contradictory layers fail closed.
22
+
23
+ A configured required health check failing after an otherwise successful command makes the operation `FAILED`. Do not downgrade it to a warning.
24
+
25
+ ## Evidence Rules
26
+
27
+ - Re-observe state after the action; do not reuse pre-action evidence as proof of a postcondition.
28
+ - Bind release/runtime observations to the intended target profile and release identity.
29
+ - Distinguish controller result, target-control-plane result, and target workload state.
30
+ - Use actual configured ports/endpoints and target-native identities.
31
+ - Bound all output. Persist only concise summaries, never raw command output or full logs.
32
+ - Redact Secret-bearing URLs, headers, environment assignments, tokens, keys, cookies, connection strings, and user-identified patterns before display or storage.
33
+ - If evidence may be sensitive and reliable redaction is unavailable, mark the layer `INACCESSIBLE` and provide a manual verification action.
34
+
35
+ ## Terminal Status Decision
36
+
37
+ Exactly one terminal status is returned:
38
+
39
+ | Status | Required condition |
40
+ |---|---|
41
+ | `SUCCEEDED` | All planned steps completed, all required applicable verification layers are `PASSED`, no effect remains unknown, and the required sole-record update was verified. |
42
+ | `FAILED` | Confirmed execution occurred but a step, required verification layer, rollback attempt, cleanup required for correctness, or required local-record update failed. Report actual live state. |
43
+ | `BLOCKED` | Safe continuation cannot be established because capability/evidence/authorization is absent, input conflicts, plan validity changed, an unknown effect remains indeterminate, or destructive impact cannot be bounded. |
44
+ | `WAITING_USER` | Execution is safely paused for an exact confirmation, login, MFA, browser approval, manual action, or separately authorized test boundary. Minimal recovery is available when its write was confirmed. |
45
+ | `CANCELLED` | The operator cancelled; no further unconfirmed action ran, and confirmed safe cleanup plus residual-resource reporting completed. |
46
+
47
+ Partial completion is never `SUCCEEDED`. A release can be live while the operation is `FAILED`; say both. A control-plane “accepted,” queued, or zero-exit response is not success until required layers pass.
48
+
49
+ ## Final Result
50
+
51
+ Return and, when confirmed, persist the complete terminal result defined by `${SKILL_DIR}/references/deploy-record-schema.json` and accepted by the shipped semantic validator:
52
+
53
+ - terminal status and operation;
54
+ - target profile and immutable plan identity;
55
+ - start/finish boundary and concise summary;
56
+ - observed live state and release identity;
57
+ - rollback availability: `VERIFIED`, `UNVERIFIED`, `ABSENT`, `UNKNOWN`, or `NOT_APPLICABLE`;
58
+ - every verification layer with required flag, status, and evidence;
59
+ - residual releases, processes, artifacts, locks, migrations, traffic state, and other project-owned resources;
60
+ - local-record update result;
61
+ - exact recovery guidance and relevant next operations.
62
+
63
+ Report inaccessible or inapplicable layers explicitly. Never fill missing evidence with `PASSED`.
64
+
65
+ ## Failure Handling
66
+
67
+ At the first failed step or required layer:
68
+
69
+ 1. stop dependent actions and plan expansion;
70
+ 2. bound and redact the failure evidence;
71
+ 3. observe current target/release/runtime/data/traffic state read-only;
72
+ 4. classify every planned write as proven applied, proven not applied, or indeterminate;
73
+ 5. perform only cleanup already authorized by the immutable plan and safe in the observed state;
74
+ 6. determine rollback availability from current evidence;
75
+ 7. select `FAILED`, `BLOCKED`, or `WAITING_USER` truthfully;
76
+ 8. preserve residual resources needed for diagnosis unless confirmed cleanup says otherwise.
77
+
78
+ Never automatically replay an indeterminate write. Follow the `unknown_outcome_inspection` step. Only proven read-only or idempotent actions may use bounded retry.
79
+
80
+ ### Failure Before Any Write
81
+
82
+ If a precondition or capability fails before execution, live state remains unchanged. Return `BLOCKED` when continuation lacks evidence/authorization or `FAILED` when confirmed execution itself failed. Do not create a misleading release/history event file.
83
+
84
+ ### Failure After a Write
85
+
86
+ State the applied/unknown effects, current live release, health, traffic, data state, residual resources, and whether old/new units coexist. A healthy previous release does not erase a failed candidate operation.
87
+
88
+ ### Failed Health
89
+
90
+ Required health failure always fails the operation. If traffic/live selection changed, inspect actual live state and apply only a pre-confirmed verified rollback; otherwise stop and offer the verified rollback or manual recovery.
91
+
92
+ ### Local Record Failure
93
+
94
+ If the live target changed but the required `.prizmkit/deploy/deploy.json` update or read-back validation failed:
95
+
96
+ - return `FAILED`;
97
+ - report that the target may be live and local state is stale;
98
+ - preserve the candidate temporary/legacy evidence without overwriting uncertain files;
99
+ - require read-only target rediscovery before any later record repair or deployment;
100
+ - never replay the deployment to repair the record.
101
+
102
+ ## Rollback
103
+
104
+ A rollback is `VERIFIED` only when current evidence establishes:
105
+
106
+ - exact prior release/configuration/data identity;
107
+ - required artifacts/state still exist and match the target;
108
+ - rollback action and authorization are available;
109
+ - compatibility with the current persistent-data state;
110
+ - rollback verification and restore-on-rollback-failure behavior.
111
+
112
+ `UNVERIFIED`, `UNKNOWN`, or `ABSENT` rollback is not offered as an automated safety guarantee.
113
+
114
+ A rollback may run automatically only when it was explicitly part of the confirmed immutable failure plan and its preconditions still match. Otherwise show the exact rollback plan/effects and obtain confirmation. Rollback itself uses all applicable verification layers. Failed rollback leaves the operation `FAILED` and reports both attempted states.
115
+
116
+ When rollback is absent, do not block a deploy that received exact `NO_RELIABLE_ROLLBACK` risk acceptance. On failure, provide live-state evidence, residual resources, the earliest safe manual stop point, and a concrete manual recovery procedure. Do not claim the old release can be restored.
117
+
118
+ ## Interruption and Unknown Remote State
119
+
120
+ A process/session interruption does not reveal whether the target wrote state. On recovery:
121
+
122
+ 1. validate the sole local record with both schema and semantic validation without dropping unknown data;
123
+ 2. load only `plan_id`, Secret-free plan digest, operation/profile, last proven step, minimal confirmation receipts, pending checkpoint, and bounded observations from `recovery`;
124
+ 3. rediscover project revision, target identity, authentication identity, tool capability/version, native configuration, actual ports, live/release/runtime/data/traffic state, health boundary, and rollback evidence affected by the plan;
125
+ 4. recompile the complete candidate plan from current authoritative evidence and compare its Secret-free digest with the stored immutable digest; never reconstruct actions from history summaries;
126
+ 5. inspect each possibly executed write before replay;
127
+ 6. continue only from the first unproven step when earlier postconditions are proven and every required confirmation receipt matches the digest; when a receipt is absent or cannot be validated, request fresh confirmation;
128
+ 7. compile and preview a new plan when identity, command/action, effect, risk, scope, or evidence assumption changed;
129
+ 8. clear recovery only after a verified terminal result is safely recorded.
130
+
131
+ Do not reconstruct commands from history summaries. Do not infer “not applied” from a missing client response. Do not claim concurrent session safety; this procedure assumes one operator.
132
+
133
+ ## Human Checkpoint Recovery
134
+
135
+ For login, MFA, browser approval, confirmation, or another manual action:
136
+
137
+ - record `WAITING_USER`, exact non-sensitive action, and expected postcondition;
138
+ - never store a password, token, challenge, browser cookie, one-time code, approval URL containing credentials, or raw authentication output;
139
+ - after the user reports completion, re-run the planned read-only identity/authorization/postcondition probes;
140
+ - remain `WAITING_USER` or become `BLOCKED` when revalidation fails;
141
+ - resume only when the immutable plan and target identity still match.
142
+
143
+ The user's “done” is a signal to revalidate, not verification itself.
144
+
145
+ ## Bounded Retry
146
+
147
+ Retry only when the immutable plan identifies the action as proven read-only or idempotent, the error as transient, the maximum attempts as greater than the current attempt, and the stop condition as unmet. Report each attempt without a duration promise.
148
+
149
+ Never retry automatically after authentication denial, invalid input, migration uncertainty, destructive failure, required health failure, plan mismatch, or an indeterminate write.
150
+
151
+ ## Deterministic Recovery Cases
152
+
153
+ | Observed case | Terminal/result behavior |
154
+ |---|---|
155
+ | Command result passed; required configured health failed | `FAILED`; report actual live state and verified rollback availability |
156
+ | External URL required but controller cannot access it | `FAILED` or `BLOCKED` according to whether execution occurred; layer is `INACCESSIBLE`, never `PASSED` |
157
+ | Startup logs do not exist for this target | Layer is `NOT_APPLICABLE` with reason |
158
+ | Login or browser approval is required | `WAITING_USER`; persist minimal checkpoint only after confirmed record write |
159
+ | Possible write timed out; inspection proves applied | Do not replay; continue verification from the proven postcondition |
160
+ | Possible write timed out; inspection proves not applied | Retry only within confirmed idempotent policy |
161
+ | Possible write timed out; inspection is indeterminate | `BLOCKED` or `WAITING_USER`; replay needs dedicated confirmation |
162
+ | Rollback is verified after failed required health | Offer or run only according to confirmed rollback policy, then verify rollback |
163
+ | Rollback is absent after partial release | `FAILED` with live state, residual resources, and manual recovery |
164
+ | Live release is healthy but sole-record write failed | `FAILED`; state target-live/local-record-stale and rediscover before repair |
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env python3
2
+ """Validate non-sensitive semantic invariants for PrizmKit Deploy artifacts.
3
+
4
+ This companion validator is intentionally read-only and standard-library-only.
5
+ JSON Schema validation remains mandatory; this script enforces cross-field and
6
+ host-neutral path rules that are awkward to express or easy to regress in a
7
+ schema alone. Diagnostics contain stable codes and JSON paths, never values.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import re
15
+ import stat
16
+ import sys
17
+ import unicodedata
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any, Iterable, Mapping, Sequence
21
+
22
+ MAX_INPUT_BYTES = 1024 * 1024
23
+ MAX_ERRORS = 50
24
+ VERIFICATION_LAYERS = {
25
+ "command_or_platform",
26
+ "runtime_or_release",
27
+ "configured_health",
28
+ "external_url",
29
+ "startup_logs",
30
+ }
31
+ FORBIDDEN_FIELDS = {
32
+ "value",
33
+ "secret_value",
34
+ "password",
35
+ "token",
36
+ "private_key",
37
+ "connection_string",
38
+ "raw_output",
39
+ "raw_logs",
40
+ }
41
+ _SAFE_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,63}$")
42
+ _DRIVE_PREFIX = re.compile(r"^[A-Za-z]:")
43
+
44
+
45
+ class DuplicateKeyError(ValueError):
46
+ """Raised for a duplicate object key without retaining its key or value."""
47
+
48
+
49
+ @dataclass(frozen=True, order=True)
50
+ class ValidationError:
51
+ code: str
52
+ path: str
53
+
54
+ def as_dict(self) -> dict[str, str]:
55
+ return {"code": self.code, "path": self.path}
56
+
57
+
58
+ class ErrorCollector:
59
+ """Collect a bounded deterministic set of non-sensitive diagnostics."""
60
+
61
+ def __init__(self) -> None:
62
+ self._errors: set[ValidationError] = set()
63
+
64
+ def add(self, code: str, path: str) -> None:
65
+ if len(self._errors) >= MAX_ERRORS:
66
+ return
67
+ self._errors.add(ValidationError(code=code, path=path))
68
+
69
+ def result(self) -> list[dict[str, str]]:
70
+ return [error.as_dict() for error in sorted(self._errors)]
71
+
72
+
73
+ def _strict_object(pairs: Sequence[tuple[str, Any]]) -> dict[str, Any]:
74
+ result: dict[str, Any] = {}
75
+ for key, value in pairs:
76
+ if key in result:
77
+ raise DuplicateKeyError("duplicate JSON key")
78
+ result[key] = value
79
+ return result
80
+
81
+
82
+ def _load_json(path: Path) -> tuple[Any | None, list[dict[str, str]]]:
83
+ try:
84
+ metadata = path.lstat()
85
+ except OSError:
86
+ return None, [{"code": "INPUT_UNREADABLE", "path": "$"}]
87
+ if path.is_symlink() or not stat.S_ISREG(metadata.st_mode):
88
+ return None, [{"code": "INPUT_NOT_REGULAR_FILE", "path": "$"}]
89
+ if metadata.st_size > MAX_INPUT_BYTES:
90
+ return None, [{"code": "INPUT_TOO_LARGE", "path": "$"}]
91
+ try:
92
+ content = path.read_text(encoding="utf-8")
93
+ return json.loads(content, object_pairs_hook=_strict_object), []
94
+ except DuplicateKeyError:
95
+ return None, [{"code": "DUPLICATE_KEY", "path": "$"}]
96
+ except (OSError, UnicodeError, json.JSONDecodeError, RecursionError):
97
+ return None, [{"code": "INVALID_JSON", "path": "$"}]
98
+
99
+
100
+ def _is_project_relative_path(value: Any, *, allow_root: bool = False) -> bool:
101
+ if not isinstance(value, str) or not value or len(value) > 512:
102
+ return False
103
+ if value == ".":
104
+ return allow_root
105
+ if value.startswith("/") or _DRIVE_PREFIX.match(value) or "\\" in value:
106
+ return False
107
+ if any(unicodedata.category(character) in {"Cc", "Cs"} for character in value):
108
+ return False
109
+ parts = value.split("/")
110
+ return all(part not in {"", ".", ".."} for part in parts)
111
+
112
+
113
+ def _is_legacy_deploy_path(value: Any) -> bool:
114
+ return _is_project_relative_path(value) and str(value).startswith(".prizmkit/deploy/")
115
+
116
+
117
+ def _iter_list(value: Any) -> Iterable[tuple[int, Any]]:
118
+ if not isinstance(value, list):
119
+ return ()
120
+ return enumerate(value)
121
+
122
+
123
+ def _check_path_list(
124
+ value: Any,
125
+ path: str,
126
+ errors: ErrorCollector,
127
+ *,
128
+ legacy: bool = False,
129
+ ) -> None:
130
+ predicate = _is_legacy_deploy_path if legacy else _is_project_relative_path
131
+ for index, item in _iter_list(value):
132
+ if not predicate(item):
133
+ errors.add("UNSAFE_MANAGED_PATH", f"{path}[{index}]")
134
+
135
+
136
+ def _check_forbidden_fields(value: Any, path: str, errors: ErrorCollector) -> None:
137
+ pending: list[tuple[Any, str]] = [(value, path)]
138
+ while pending:
139
+ current, current_path = pending.pop()
140
+ if isinstance(current, Mapping):
141
+ for key, child in current.items():
142
+ child_path = (
143
+ f"{current_path}.{key}"
144
+ if _SAFE_KEY.fullmatch(str(key))
145
+ else f"{current_path}.[unknown]"
146
+ )
147
+ if str(key).lower() in FORBIDDEN_FIELDS:
148
+ errors.add("FORBIDDEN_SENSITIVE_FIELD", child_path)
149
+ pending.append((child, child_path))
150
+ elif isinstance(current, list):
151
+ pending.extend(
152
+ (child, f"{current_path}[{index}]")
153
+ for index, child in enumerate(current)
154
+ )
155
+
156
+
157
+ def _check_unique_names(
158
+ value: Any,
159
+ path: str,
160
+ field: str,
161
+ code: str,
162
+ errors: ErrorCollector,
163
+ ) -> set[str]:
164
+ seen: set[str] = set()
165
+ for index, item in _iter_list(value):
166
+ if not isinstance(item, Mapping):
167
+ continue
168
+ identity = item.get(field)
169
+ if not isinstance(identity, str):
170
+ continue
171
+ if identity in seen:
172
+ errors.add(code, f"{path}[{index}].{field}")
173
+ seen.add(identity)
174
+ return seen
175
+
176
+
177
+ def _check_profile_reference(
178
+ value: Any,
179
+ path: str,
180
+ profiles: set[str],
181
+ errors: ErrorCollector,
182
+ ) -> None:
183
+ if value is not None and isinstance(value, str) and value not in profiles:
184
+ errors.add("DANGLING_PROFILE_REFERENCE", path)
185
+
186
+
187
+ def _check_terminal_result(value: Any, path: str, errors: ErrorCollector) -> None:
188
+ if not isinstance(value, Mapping):
189
+ return
190
+ verification = value.get("verification")
191
+ if not isinstance(verification, list):
192
+ errors.add("VERIFICATION_LAYER_SET", f"{path}.verification")
193
+ return
194
+
195
+ layers: list[str] = []
196
+ for item in verification:
197
+ if isinstance(item, Mapping) and isinstance(item.get("layer"), str):
198
+ layers.append(item["layer"])
199
+ if len(verification) != len(VERIFICATION_LAYERS) or set(layers) != VERIFICATION_LAYERS:
200
+ errors.add("VERIFICATION_LAYER_SET", f"{path}.verification")
201
+ if len(layers) != len(set(layers)):
202
+ errors.add("DUPLICATE_VERIFICATION_LAYER", f"{path}.verification")
203
+
204
+ if value.get("status") == "SUCCEEDED":
205
+ for index, item in _iter_list(verification):
206
+ if (
207
+ isinstance(item, Mapping)
208
+ and item.get("required") is True
209
+ and item.get("status") != "PASSED"
210
+ ):
211
+ errors.add(
212
+ "SUCCEEDED_REQUIRED_LAYER_NOT_PASSED",
213
+ f"{path}.verification[{index}]",
214
+ )
215
+
216
+
217
+ def _validate_record(data: Any, errors: ErrorCollector) -> None:
218
+ if not isinstance(data, Mapping):
219
+ errors.add("ROOT_NOT_OBJECT", "$")
220
+ return
221
+
222
+ project = data.get("project")
223
+ if isinstance(project, Mapping):
224
+ _check_path_list(
225
+ project.get("native_config_files"),
226
+ "$.project.native_config_files",
227
+ errors,
228
+ )
229
+
230
+ profiles_value = data.get("profiles")
231
+ profiles = _check_unique_names(
232
+ profiles_value,
233
+ "$.profiles",
234
+ "id",
235
+ "DUPLICATE_PROFILE_ID",
236
+ errors,
237
+ )
238
+ for index, profile in _iter_list(profiles_value):
239
+ if isinstance(profile, Mapping):
240
+ _check_path_list(
241
+ profile.get("native_config_files"),
242
+ f"$.profiles[{index}].native_config_files",
243
+ errors,
244
+ )
245
+
246
+ _check_unique_names(
247
+ data.get("secret_references"),
248
+ "$.secret_references",
249
+ "name",
250
+ "DUPLICATE_SECRET_REFERENCE",
251
+ errors,
252
+ )
253
+
254
+ current = data.get("current")
255
+ if isinstance(current, Mapping):
256
+ _check_profile_reference(
257
+ current.get("profile_id"),
258
+ "$.current.profile_id",
259
+ profiles,
260
+ errors,
261
+ )
262
+
263
+ latest = data.get("latest_result")
264
+ if isinstance(latest, Mapping):
265
+ _check_profile_reference(
266
+ latest.get("profile_id"),
267
+ "$.latest_result.profile_id",
268
+ profiles,
269
+ errors,
270
+ )
271
+ _check_terminal_result(latest, "$.latest_result", errors)
272
+
273
+ for index, history in _iter_list(data.get("history")):
274
+ if isinstance(history, Mapping):
275
+ _check_profile_reference(
276
+ history.get("profile_id"),
277
+ f"$.history[{index}].profile_id",
278
+ profiles,
279
+ errors,
280
+ )
281
+
282
+ recovery = data.get("recovery")
283
+ if isinstance(recovery, Mapping):
284
+ _check_profile_reference(
285
+ recovery.get("profile_id"),
286
+ "$.recovery.profile_id",
287
+ profiles,
288
+ errors,
289
+ )
290
+
291
+ legacy = data.get("legacy_migration")
292
+ if isinstance(legacy, Mapping):
293
+ sources = legacy.get("sources")
294
+ for index, source in _iter_list(sources):
295
+ if isinstance(source, Mapping) and not _is_legacy_deploy_path(source.get("path")):
296
+ errors.add("UNSAFE_MANAGED_PATH", f"$.legacy_migration.sources[{index}].path")
297
+ _check_path_list(
298
+ legacy.get("preserved_paths"),
299
+ "$.legacy_migration.preserved_paths",
300
+ errors,
301
+ legacy=True,
302
+ )
303
+
304
+
305
+ def _validate_declaration(data: Any, errors: ErrorCollector) -> None:
306
+ if not isinstance(data, Mapping):
307
+ errors.add("ROOT_NOT_OBJECT", "$")
308
+ return
309
+
310
+ _check_path_list(data.get("native_config_files"), "$.native_config_files", errors)
311
+ _check_unique_names(
312
+ data.get("profile_hints"),
313
+ "$.profile_hints",
314
+ "id",
315
+ "DUPLICATE_PROFILE_ID",
316
+ errors,
317
+ )
318
+ _check_unique_names(
319
+ data.get("secret_references"),
320
+ "$.secret_references",
321
+ "name",
322
+ "DUPLICATE_SECRET_REFERENCE",
323
+ errors,
324
+ )
325
+
326
+ for index, command in _iter_list(data.get("commands")):
327
+ if (
328
+ isinstance(command, Mapping)
329
+ and not _is_project_relative_path(command.get("working_directory"), allow_root=True)
330
+ ):
331
+ errors.add("UNSAFE_MANAGED_PATH", f"$.commands[{index}].working_directory")
332
+
333
+ for index, hint in _iter_list(data.get("capability_hints")):
334
+ if isinstance(hint, Mapping):
335
+ _check_path_list(
336
+ hint.get("evidence_files"),
337
+ f"$.capability_hints[{index}].evidence_files",
338
+ errors,
339
+ )
340
+
341
+
342
+ def validate(kind: str, data: Any) -> list[dict[str, str]]:
343
+ errors = ErrorCollector()
344
+ _check_forbidden_fields(data, "$", errors)
345
+ if kind == "record":
346
+ _validate_record(data, errors)
347
+ else:
348
+ _validate_declaration(data, errors)
349
+ return errors.result()
350
+
351
+
352
+ def _parser() -> argparse.ArgumentParser:
353
+ parser = argparse.ArgumentParser(
354
+ description="Validate semantic invariants for one PrizmKit Deploy artifact."
355
+ )
356
+ parser.add_argument("--kind", choices=("record", "declaration"), required=True)
357
+ parser.add_argument("path", type=Path)
358
+ return parser
359
+
360
+
361
+ def main(argv: Sequence[str] | None = None) -> int:
362
+ args = _parser().parse_args(argv)
363
+ data, load_errors = _load_json(args.path)
364
+ errors = load_errors or validate(args.kind, data)
365
+ output = {"valid": not errors, "errors": errors}
366
+ print(json.dumps(output, ensure_ascii=False, separators=(",", ":")))
367
+ return 0 if not errors else 1
368
+
369
+
370
+ if __name__ == "__main__":
371
+ raise SystemExit(main())
@@ -36,8 +36,8 @@ Before editing:
36
36
 
37
37
  1. Read `plan.md`, `spec.md`, and only relevant companion artifacts.
38
38
  2. Read `.prizmkit/prizm-docs/root.prizm` when present.
39
- 3. Follow its pointers to only the relevant module and detail documentation.
40
- 4. If relevant detail documentation is missing, read the target source files as fallback and record that detailed context was unavailable. Context loading does not create or modify documentation.
39
+ 3. Follow its pointers to only the relevant direct-child module index and nested detail documentation. Before modifying source, read the complete relevant detail plus the complete parent/child documents needed to resolve its pointers; grep-only fragments are insufficient modification context.
40
+ 4. If relevant detail documentation is missing, inspect only bounded target source files and narrowly implicated callers/contracts as fallback, record that detailed context was unavailable, and proceed without creating a placeholder. Context loading never creates or modifies documentation.
41
41
  5. Read referenced layer rules when present. If a rule conflicts with the plan, stop and ask the user unless the plan clearly supersedes the rule.
42
42
 
43
43
  ## Optional Inline Delegation
@@ -25,7 +25,7 @@ Recommended project initialization skill. Scans any brownfield or greenfield pro
25
25
 
26
26
  ### Artifact Ownership
27
27
 
28
- All paths under `.prizmkit/` are framework-internal and remain outside automated task commits. Initialization must not stage or commit them. Root instruction or platform-support files written during setup also remain support data rather than task-owned source.
28
+ `.prizmkit/**` is the PrizmKit framework directory. Initialization may read and write only its declared managed artifacts, but it does not stage, commit, force-add, change or interpret `.gitignore`, require Git history, or decide whether those paths are ignored, untracked, or tracked. The project controls Git visibility; initialization behavior is otherwise identical. Root instruction and platform-support files remain support data rather than initialization-owned commit output.
29
29
 
30
30
  ## Host-Neutral Interaction and Discovery
31
31
 
@@ -81,8 +81,8 @@ BROWNFIELD WORKFLOW (existing project):
81
81
  2. Map directory structure using a TWO-TIER model — flat structures lose the nesting relationships that AI needs to navigate the codebase:
82
82
  - TOP-LEVEL modules: directories directly under project root that contain source files or sub-directories with source files (e.g. `src/`, `internal/`, `lib/`)
83
83
  - SUB-MODULES: directories INSIDE a top-level module (e.g. `src/routes/`, `src/models/`)
84
- - A sub-module maps to `.prizmkit/prizm-docs/<M>/<S>.prizm`, never to `.prizmkit/prizm-docs/<S>.prizm` flattening would create ambiguous paths when two modules have identically-named sub-modules
85
- - Exclude: `.git/`, `node_modules/`, `vendor/`, `build/`, `dist/`, `__pycache__/`, `target/`, `bin/`, `.agents/`, `.codex/`, `.claude/`, `.codebuddy/`, `.prizmkit/`
84
+ - Exact `root.prizm` is L0; top-level module M maps to direct-child `.prizmkit/prizm-docs/<M>.prizm` (L1); sub-module S maps to nested `.prizmkit/prizm-docs/<M>/<S>.prizm` (L2). Deeper source ownership remains L2 and never creates L3.
85
+ - Exclude: `.git/`, `node_modules/`, `vendor/`, `build/`, `dist/`, `__pycache__/`, `target/`, `bin/`, `.agents/`, `.codex/`, `.claude/`, `.codebuddy/`, `.pi/`, and the complete `.prizmkit/` framework directory. This excludes installed `.prizmkit/dev-pipeline/` artifacts but not canonical top-level `dev-pipeline/` source.
86
86
  - **Bounded discovery contract** — use the current Host's available directory-listing or file-search capability to enumerate at most two directory levels, apply the exclusions above before analysis, preserve parent/child relationships, and report the resulting project-relative structure. Do not require a POSIX shell pipeline or any other operating-system-specific command.
87
87
  3. Identify entry points by language convention
88
88
  4. Catalog dependencies (external packages)
@@ -179,12 +179,12 @@ After Infrastructure Quick Scan completes, check if `detected_layers` is non-emp
179
179
 
180
180
  **Phase 5: Prizm Documentation Generation**
181
181
  Invoke prizmkit-prizm-docs (Init operation), passing the two-tier module structure from Phase 4:
182
- - Create `.prizmkit/prizm-docs/` directory structure mirroring the source tree (sub-module dirs become subdirectories under `.prizmkit/prizm-docs/<top-level>/`)
183
- - Generate `root.prizm` (L0) with project meta and MODULE_INDEX listing only top-level modules. If module count > 15, use MODULE_GROUPS format instead (group by functional domain).
184
- - For each module entry in MODULE_INDEX/MODULE_GROUPS, include keyword tags extracted from the module's source files scan for: exported symbols, imported packages, domain terms in file/directory names. Format: `- module-name [tag1, tag2, tag3]: ...`. Tags help AI match user intent to relevant modules.
185
- - Generate L1 docs for top-level modules at `.prizmkit/prizm-docs/<M>.prizm` and for sub-modules at `.prizmkit/prizm-docs/<M>/<S>.prizm`
186
- - Skip L2 (lazy generation) L2 is created later by `/prizmkit-retrospective` when a changed module has meaningful behavior or durable knowledge. During implementation, if an affected L2 is missing, `/prizmkit-implement` reads the target source files as fallback instead of blocking.
187
- - Do not create auxiliary `changelog.prizm`, CHANGELOG sections/files, UPDATED/date metadata, feature/bug/refactor/task/session/run/pipeline/workflow IDs, branch names, absolute worktree paths, or `.prizmkit/specs` / `.prizmkit/dev-pipeline` artifact paths; git history is the source of change history
182
+ - Create only documentation directories needed by complete documents; do not create placeholder sub-module files.
183
+ - Generate exact `root.prizm` (L0) with `PRIZM_VERSION: 4`, project meta, and MODULE_INDEX listing only top-level modules. If module count > 15 or measured capacity requires it, use MODULE_GROUPS.
184
+ - For each module entry in MODULE_INDEX/MODULE_GROUPS, include only high-value intent tags and a pointer to one direct-child L1 at `.prizmkit/prizm-docs/<M>.prizm`.
185
+ - Generate one direct-child L1 structural index for each top-level module. Record source submodules structurally, but emit a SUBDIRS pointer only when its complete nested L2 exists.
186
+ - Skip all L2 during Init. Before later source modification, read a complete existing relevant L2 and complete resolving pointer documents; if absent, inspect bounded relevant source as fallback without creating a placeholder. `/prizmkit-retrospective` may later create a complete Value-Gate-qualified L2.
187
+ - Do not create auxiliary `changelog.prizm`, CHANGELOG sections/files, UPDATED/date metadata, feature/bug/refactor/task/session/run/pipeline/workflow IDs, branch names, absolute worktree paths, or `.prizmkit/specs` / `.prizmkit/dev-pipeline` artifact paths; temporal history is outside Prizm memory
188
188
 
189
189
  **Phase 6: Workspace Initialization**
190
190
  6a. Create `.prizmkit/` directory structure (if missing):
@@ -308,15 +308,15 @@ Rules Quick Entry:
308
308
  Matched layers: frontend (React), backend (Express.js), database (Prisma) → no rule profiles enabled yet
309
309
 
310
310
  Modules discovered:
311
- src/routes/ → .prizmkit/prizm-docs/routes.prizm (12 files)
312
- src/models/ → .prizmkit/prizm-docs/models.prizm (8 files)
313
- src/services/ .prizmkit/prizm-docs/services.prizm (15 files)
314
- src/middleware/ .prizmkit/prizm-docs/middleware.prizm (5 files)
311
+ src/ → .prizmkit/prizm-docs/src.prizm (top-level L1)
312
+ src/routes/ → nested source submodule (no placeholder L2 during Init)
313
+ src/models/ nested source submodule (no placeholder L2 during Init)
314
+ src/services/ nested source submodule (no placeholder L2 during Init)
315
315
 
316
316
  Project brief: inferred from codebase → confirmed by user
317
317
  → .prizmkit/plans/project-brief.md
318
318
 
319
- Generated: root.prizm + 4 L1 docs
319
+ Generated: root.prizm + 1 direct-child L1 doc
320
320
  Saved: .prizmkit/config.json (tech_stack + detected_layers + enabled_rule_profiles recorded)
321
321
 
322
322
  Next: Use /prizmkit-plan to start your first feature
@@ -345,9 +345,9 @@ Tech stack changes detected:
345
345
  = frontend: React (unchanged)
346
346
 
347
347
  Documentation gap-fill:
348
- + app/share/[token].prizm (L2) — created (3 source files, meaningful logic)
349
- = routes.prizm (L1) up to date
350
- ~ models.prizm (L1) — FILES count updated (8 → 10)
348
+ = app.prizm (direct-child L1) — up to date
349
+ source fallback: app/share/[token] (nested L2 absent; no placeholder created)
350
+ ~ app.prizm (L1) — concise source summary updated
351
351
 
352
352
  Project brief: inferred from codebase → confirmed by user
353
353
  → .prizmkit/plans/project-brief.md (generated)
@@ -2,8 +2,8 @@
2
2
 
3
3
  Runs after tech stack merge in Update mode:
4
4
 
5
- 1. **Module scan**: Re-scan project directories using the same TWO-TIER model from Step 1. Compare discovered modules against existing MODULE_INDEX in root.prizm.
6
- 2. **Missing L1 check**: For any discovered module with no corresponding L1 `.prizm` doc create L1 immediately and add to MODULE_INDEX.
7
- 3. **Missing L2 check**: For any module/sub-module that has source files with meaningful logic but no L2 `.prizm` doc create L2 using the L2 GENERATION TEMPLATE. Judgment call: skip trivial wrapper directories or single-config modules.
8
- 4. **Stale L1 check**: For existing L1 docs, verify FILES count and KEY_FILES are still accurate. Update if source directory contents have changed significantly.
9
- 5. **Report**: Include in the Update report: modules added, L1 docs created, L2 docs created, stale docs refreshed.
5
+ 1. **Bounded module scan**: Re-scan only the registered top-level source modules and concrete structural gaps using the same two-tier model. Exclude the complete `.prizmkit/` framework directory, including installed `.prizmkit/dev-pipeline/`, but do not exclude a repository's canonical top-level `dev-pipeline/` source module.
6
+ 2. **Missing L1 check**: For a discovered top-level module with no direct-child L1, build one complete Value-Gate-filtered structural candidate at `.prizmkit/prizm-docs/<M>.prizm` and add a resolving root pointer only after the candidate validates.
7
+ 3. **Missing L2 context**: For an affected source submodule with no nested L2, inspect only the bounded source files and narrowly implicated contracts needed to determine the gap. Do not create a placeholder. A complete L2 may be created only when current durable facts pass the Value Gate and mirrored/semantic identity, ownership, pointer, format, and capacity checks all pass.
8
+ 4. **Existing target reconciliation**: Read every complete affected target and complete resolving parent/child pointer document. Apply Cleanup by semantic meaning, preserve protected knowledge, build candidates bottom-up, skip byte-identical replacements, and re-read actual bytes after writes.
9
+ 5. **Failure and report**: If any post-write check fails, restore exact pre-write bytes and remove invalid new targets before reporting a blocker. Report modules/details added, skipped source fallbacks, stale summaries reconciled, and capacity/pointer validation. Never stage, commit, force-add, change or interpret `.gitignore`, require Git history, or classify tracking state as documentation health.