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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/codebuddy/settings-adapter.js +76 -25
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +9 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_commit.py +42 -15
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +1 -1
- package/bundled/dev-pipeline/templates/sections/runtime-commit-handoff.md +6 -6
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +4 -3
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +2 -2
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +2 -2
- package/bundled/dev-pipeline/tests/test_runtime_commit.py +68 -13
- package/bundled/dev-pipeline/tests/test_unified_cli.py +10 -6
- package/bundled/rules/prizm/prizm-commit-workflow.md +7 -7
- package/bundled/rules/prizm/prizm-documentation.md +24 -65
- package/bundled/rules/prizm/prizm-progressive-loading.md +9 -8
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/prizmkit-code-review/SKILL.md +1 -1
- package/bundled/skills/prizmkit-committer/SKILL.md +48 -41
- package/bundled/skills/prizmkit-deploy/SKILL.md +172 -198
- package/bundled/skills/prizmkit-deploy/references/capability-contract.md +137 -0
- package/bundled/skills/prizmkit-deploy/references/deploy-record-schema.json +1053 -0
- package/bundled/skills/prizmkit-deploy/references/deployment-declaration-schema.json +291 -0
- package/bundled/skills/prizmkit-deploy/references/legacy-migration.md +90 -0
- package/bundled/skills/prizmkit-deploy/references/policy-and-execution.md +170 -0
- package/bundled/skills/prizmkit-deploy/references/real-resource-test-policy.md +73 -0
- package/bundled/skills/prizmkit-deploy/references/verification-and-recovery.md +164 -0
- package/bundled/skills/prizmkit-deploy/scripts/validate-deploy-artifact.py +371 -0
- package/bundled/skills/prizmkit-implement/SKILL.md +2 -2
- package/bundled/skills/prizmkit-init/SKILL.md +17 -17
- package/bundled/skills/prizmkit-init/references/update-supplement.md +5 -5
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +17 -11
- package/bundled/skills/prizmkit-prizm-docs/assets/prizm-docs-format.md +95 -155
- package/bundled/skills/prizmkit-prizm-docs/references/op-init.md +6 -5
- package/bundled/skills/prizmkit-prizm-docs/references/op-rebuild.md +4 -2
- package/bundled/skills/prizmkit-prizm-docs/references/op-status.md +9 -10
- package/bundled/skills/prizmkit-prizm-docs/references/op-update.md +6 -5
- package/bundled/skills/prizmkit-prizm-docs/references/op-validate.md +13 -12
- package/bundled/skills/prizmkit-retrospective/SKILL.md +3 -3
- package/bundled/skills/prizmkit-test/SKILL.md +26 -13
- package/bundled/skills/prizmkit-test/references/boundary-contract-and-test-double-guidance.md +8 -4
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +7 -3
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +33 -10
- package/bundled/skills/prizmkit-test/references/test-report-template.md +33 -11
- package/bundled/skills/prizmkit-workflow/SKILL.md +5 -7
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +5 -6
- package/bundled/templates/hooks/commit-intent-status.py +9 -41
- package/bundled/templates/hooks/commit-intent.json +3 -3
- package/bundled/templates/hooks/diff-prizm-docs.py +3 -1
- package/bundled/templates/hooks/prizm-pre-commit.py +32 -6
- package/bundled/templates/hooks/run-python-hook.cjs +44 -0
- package/bundled/templates/hooks/validate-prizm-docs.py +399 -173
- package/bundled/templates/project-memory-template.md +19 -19
- package/package.json +1 -1
- package/src/clean.js +6 -7
- package/src/gitignore-template.js +8 -12
- package/src/scaffold.js +125 -71
- package/bundled/skills/prizmkit-deploy/assets/deploy-document-template.md +0 -89
- package/bundled/skills/prizmkit-deploy/references/ci-cd-workflows.md +0 -145
- package/bundled/skills/prizmkit-deploy/references/cloud-platform-deploy.md +0 -109
- package/bundled/skills/prizmkit-deploy/references/data-safety-examples.md +0 -120
- package/bundled/skills/prizmkit-deploy/references/database-setup.md +0 -65
- package/bundled/skills/prizmkit-deploy/references/deploy-config-schema.md +0 -150
- package/bundled/skills/prizmkit-deploy/references/deploy-history-schema.md +0 -78
- package/bundled/skills/prizmkit-deploy/references/deploy-metadata-schema.json +0 -49
- package/bundled/skills/prizmkit-deploy/references/deployment-modes.md +0 -50
- package/bundled/skills/prizmkit-deploy/references/direct-upload.md +0 -26
- package/bundled/skills/prizmkit-deploy/references/dns-setup.md +0 -42
- package/bundled/skills/prizmkit-deploy/references/docker-deploy.md +0 -31
- package/bundled/skills/prizmkit-deploy/references/firewall-setup.md +0 -37
- package/bundled/skills/prizmkit-deploy/references/live-validation-notes.md +0 -21
- package/bundled/skills/prizmkit-deploy/references/nginx-blue-green.md +0 -59
- package/bundled/skills/prizmkit-deploy/references/pending-input-schema.json +0 -164
- package/bundled/skills/prizmkit-deploy/references/ssh-adapter-flow.md +0 -221
- package/bundled/skills/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
- package/bundled/skills/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
- package/bundled/skills/prizmkit-deploy/references/ssh-takeover.md +0 -20
- 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,
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
183
|
-
- Generate `root.prizm` (L0) with project meta and MODULE_INDEX listing only top-level modules. If module count > 15
|
|
184
|
-
- For each module entry in MODULE_INDEX/MODULE_GROUPS, include
|
|
185
|
-
- Generate L1
|
|
186
|
-
- Skip 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;
|
|
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/
|
|
312
|
-
src/
|
|
313
|
-
src/
|
|
314
|
-
src/
|
|
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 +
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
~
|
|
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. **
|
|
6
|
-
2. **Missing L1 check**: For
|
|
7
|
-
3. **Missing L2
|
|
8
|
-
4. **
|
|
9
|
-
5. **
|
|
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.
|