release-skill 0.9.22 → 0.9.24
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/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +61 -2
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +53 -32
- package/README.zh-CN.md +44 -27
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +50 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/skills/release-assess/SKILL.md +17 -3
- package/adapters/claude/skills/release-config/SKILL.md +6 -0
- package/adapters/claude/skills/release-docs/SKILL.md +6 -0
- package/adapters/claude/skills/release-help/SKILL.md +7 -2
- package/adapters/claude/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +8 -2
- package/adapters/claude/skills/release-publish/SKILL.md +7 -1
- package/adapters/claude/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/claude/skills/release-setup/SKILL.md +9 -1
- package/adapters/claude/skills/release-verify/SKILL.md +6 -0
- package/adapters/claude/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/claude/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/claude/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/claude/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/claude/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/claude/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/claude/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/claude/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +50 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/skills/release-assess/SKILL.md +17 -3
- package/adapters/codex/skills/release-config/SKILL.md +6 -0
- package/adapters/codex/skills/release-docs/SKILL.md +6 -0
- package/adapters/codex/skills/release-help/SKILL.md +9 -3
- package/adapters/codex/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +8 -2
- package/adapters/codex/skills/release-publish/SKILL.md +7 -1
- package/adapters/codex/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/codex/skills/release-setup/SKILL.md +9 -1
- package/adapters/codex/skills/release-verify/SKILL.md +6 -0
- package/adapters/codex/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/codex/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/codex/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/codex/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/codex/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/codex/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/codex/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/codex/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/consumer-contract-vectors.json +7 -7
- package/adapters/cursor/bin/error-codes.json +1 -1
- package/adapters/cursor/bin/foundation-resource-binding.json +1 -1
- package/adapters/cursor/bin/registry.json +50 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/cursor/bin/rules.json +1 -1
- package/adapters/cursor/skills/release-assess/SKILL.md +17 -3
- package/adapters/cursor/skills/release-config/SKILL.md +6 -0
- package/adapters/cursor/skills/release-docs/SKILL.md +6 -0
- package/adapters/cursor/skills/release-help/SKILL.md +9 -3
- package/adapters/cursor/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/cursor/skills/release-prepare/SKILL.md +8 -2
- package/adapters/cursor/skills/release-publish/SKILL.md +7 -1
- package/adapters/cursor/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/cursor/skills/release-setup/SKILL.md +9 -1
- package/adapters/cursor/skills/release-verify/SKILL.md +6 -0
- package/adapters/cursor/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/cursor/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/cursor/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/cursor/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/cursor/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/cursor/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/cursor/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/cursor/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +50 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/skills/release-assess/SKILL.md +17 -3
- package/adapters/kimi/skills/release-config/SKILL.md +6 -0
- package/adapters/kimi/skills/release-docs/SKILL.md +6 -0
- package/adapters/kimi/skills/release-help/SKILL.md +9 -3
- package/adapters/kimi/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +8 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +7 -1
- package/adapters/kimi/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +9 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +6 -0
- package/adapters/kimi/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/kimi/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/kimi/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/kimi/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/kimi/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/kimi/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/kimi/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/kimi/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/consumer-contract-vectors.json +7 -7
- package/adapters/qoder/bin/error-codes.json +1 -1
- package/adapters/qoder/bin/foundation-resource-binding.json +1 -1
- package/adapters/qoder/bin/registry.json +50 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/qoder/bin/rules.json +1 -1
- package/adapters/qoder/skills/release-assess/SKILL.md +17 -3
- package/adapters/qoder/skills/release-config/SKILL.md +6 -0
- package/adapters/qoder/skills/release-docs/SKILL.md +6 -0
- package/adapters/qoder/skills/release-help/SKILL.md +9 -3
- package/adapters/qoder/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/qoder/skills/release-prepare/SKILL.md +8 -2
- package/adapters/qoder/skills/release-publish/SKILL.md +7 -1
- package/adapters/qoder/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/qoder/skills/release-setup/SKILL.md +9 -1
- package/adapters/qoder/skills/release-verify/SKILL.md +6 -0
- package/adapters/qoder/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/qoder/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/qoder/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/qoder/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/qoder/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/qoder/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/qoder/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/qoder/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +50 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2680 -2180
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/skills/release-assess/SKILL.md +17 -3
- package/adapters/workbuddy/skills/release-config/SKILL.md +6 -0
- package/adapters/workbuddy/skills/release-docs/SKILL.md +6 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +7 -2
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +6 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +8 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +7 -1
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +6 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +9 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -0
- package/adapters/workbuddy/src/schemas/file-set-apply-request.schema.json +78 -0
- package/adapters/workbuddy/src/schemas/file-set-event-payload.schema.json +55 -0
- package/adapters/workbuddy/src/schemas/file-set-prune-request.schema.json +17 -0
- package/adapters/workbuddy/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/adapters/workbuddy/src/schemas/file-set-result.schema.json +79 -0
- package/adapters/workbuddy/src/schemas/professional-conclusion.schema.json +132 -0
- package/adapters/workbuddy/src/schemas/project-manifest.schema.json +91 -0
- package/adapters/workbuddy/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +50 -1
- package/bin/release-skill-cli.mjs +80 -5
- package/bin/release-skill.bundle.mjs +2680 -2180
- package/bin/rules.json +1 -1
- package/package.json +4 -4
- package/platform-manifest.json +4 -4
- package/references/05-evidence-and-errors.md +2 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/sync-public-files.mjs +1 -1
- package/skills/release-assess/SKILL.md +17 -3
- package/skills/release-config/SKILL.md +6 -0
- package/skills/release-docs/SKILL.md +6 -0
- package/skills/release-help/SKILL.md +7 -2
- package/skills/release-marketplace/SKILL.md +6 -0
- package/skills/release-prepare/SKILL.md +8 -2
- package/skills/release-publish/SKILL.md +7 -1
- package/skills/release-reconcile/SKILL.md +6 -0
- package/skills/release-setup/SKILL.md +9 -1
- package/skills/release-verify/SKILL.md +6 -0
- package/skills-src/release-assess/SKILL.md +17 -3
- package/skills-src/release-config/SKILL.md +6 -0
- package/skills-src/release-docs/SKILL.md +6 -0
- package/skills-src/release-help/SKILL.md +7 -2
- package/skills-src/release-marketplace/SKILL.md +6 -0
- package/skills-src/release-prepare/SKILL.md +8 -2
- package/skills-src/release-publish/SKILL.md +7 -1
- package/skills-src/release-reconcile/SKILL.md +6 -0
- package/skills-src/release-setup/SKILL.md +9 -1
- package/skills-src/release-verify/SKILL.md +6 -0
- package/src/adapters/plugin-marketplace.mjs +7 -12
- package/src/artifacts/producer-registry.mjs +4 -3
- package/src/commands/assess.mjs +23 -3
- package/src/commands/post-release-local.mjs +2 -2
- package/src/commands/prepare.mjs +5 -30
- package/src/commands/professional-proof.mjs +506 -0
- package/src/commands/setup.mjs +16 -5
- package/src/commands/verify-records.mjs +14 -0
- package/src/core/baseline.mjs +9 -1
- package/src/core/derived-artifact-gates.mjs +38 -9
- package/src/core/redact.mjs +12 -5
- package/src/core/source-authority.mjs +28 -3
- package/src/npm/npm-entry-closure.mjs +71 -9
- package/src/producers/build-adapters.mjs +18 -5
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/producers/render-public-assets.mjs +2 -1
- package/src/producers/sync-skills.mjs +2 -1
- package/src/schemas/file-set-apply-request.schema.json +78 -0
- package/src/schemas/file-set-event-payload.schema.json +55 -0
- package/src/schemas/file-set-prune-request.schema.json +17 -0
- package/src/schemas/file-set-recovery-request.schema.json +29 -0
- package/src/schemas/file-set-result.schema.json +79 -0
- package/src/schemas/professional-conclusion.schema.json +132 -0
- package/src/schemas/project-manifest.schema.json +91 -0
- package/src/schemas/state-store-recovery-observation.schema.json +129 -0
- package/src/snapshot/frozen.mjs +19 -14
- package/src/core/run-retention.mjs +0 -227
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.skill-family.example/v1/professional-conclusion.json",
|
|
4
|
+
"title": "ProfessionalConclusion",
|
|
5
|
+
"description": "Minimal exchange contract for a provider-owned professional conclusion. Foundation fixes the common shape and completion semantics without interpreting the provider's domain code or details.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "kind", "provider", "subject", "scope", "outcome"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"kind": { "const": "skill-family.professional-conclusion" },
|
|
12
|
+
"provider": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["id", "version", "entry"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"id": { "type": "string", "minLength": 1 },
|
|
18
|
+
"version": { "type": "string", "minLength": 1 },
|
|
19
|
+
"entry": { "type": "string", "minLength": 1 }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"subject": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"required": ["ref"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"ref": { "type": "string", "minLength": 1 },
|
|
28
|
+
"revision": { "type": "string", "minLength": 1 }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scope": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["checked", "limitations"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"checked": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string", "minLength": 1 }
|
|
39
|
+
},
|
|
40
|
+
"limitations": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": { "type": "string", "minLength": 1 }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"outcome": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"required": ["completion", "code", "summary"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"completion": { "enum": ["complete", "partial", "not-performed"] },
|
|
52
|
+
"code": { "type": "string", "minLength": 1 },
|
|
53
|
+
"summary": { "type": "string", "minLength": 1 }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"details": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"description": "Provider-owned JSON object. Foundation does not interpret its domain fields."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"allOf": [
|
|
62
|
+
{
|
|
63
|
+
"if": {
|
|
64
|
+
"properties": {
|
|
65
|
+
"outcome": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": { "completion": { "const": "complete" } },
|
|
68
|
+
"required": ["completion"]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": ["outcome"]
|
|
72
|
+
},
|
|
73
|
+
"then": {
|
|
74
|
+
"properties": {
|
|
75
|
+
"scope": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": { "checked": { "type": "array", "minItems": 1 } },
|
|
78
|
+
"required": ["checked"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"if": {
|
|
85
|
+
"properties": {
|
|
86
|
+
"outcome": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": { "completion": { "const": "partial" } },
|
|
89
|
+
"required": ["completion"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": ["outcome"]
|
|
93
|
+
},
|
|
94
|
+
"then": {
|
|
95
|
+
"properties": {
|
|
96
|
+
"scope": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"checked": { "type": "array", "minItems": 1 },
|
|
100
|
+
"limitations": { "type": "array", "minItems": 1 }
|
|
101
|
+
},
|
|
102
|
+
"required": ["checked", "limitations"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"if": {
|
|
109
|
+
"properties": {
|
|
110
|
+
"outcome": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": { "completion": { "const": "not-performed" } },
|
|
113
|
+
"required": ["completion"]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"required": ["outcome"]
|
|
117
|
+
},
|
|
118
|
+
"then": {
|
|
119
|
+
"properties": {
|
|
120
|
+
"scope": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"checked": { "type": "array", "maxItems": 0 },
|
|
124
|
+
"limitations": { "type": "array", "minItems": 1 }
|
|
125
|
+
},
|
|
126
|
+
"required": ["checked", "limitations"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
@@ -58,10 +58,101 @@
|
|
|
58
58
|
"pattern": "^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$"
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
+
"engineering": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"minProperties": 1,
|
|
65
|
+
"properties": {
|
|
66
|
+
"releaseUnits": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"minItems": 1,
|
|
69
|
+
"uniqueItems": true,
|
|
70
|
+
"items": { "$ref": "#/$defs/releaseUnit" }
|
|
71
|
+
},
|
|
72
|
+
"entries": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"minItems": 1,
|
|
75
|
+
"uniqueItems": true,
|
|
76
|
+
"items": { "$ref": "#/$defs/engineeringEntry" }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
61
80
|
"updatedAt": {
|
|
62
81
|
"type": "string",
|
|
63
82
|
"format": "date-time",
|
|
64
83
|
"description": "RFC 3339 date-time of the last manifest update; format enforcement is part of the contracts dialect support."
|
|
65
84
|
}
|
|
85
|
+
},
|
|
86
|
+
"$defs": {
|
|
87
|
+
"versionReference": {
|
|
88
|
+
"oneOf": [
|
|
89
|
+
{
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"required": ["path", "format"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"path": { "$ref": "#/$defs/projectRelativePath" },
|
|
95
|
+
"format": { "const": "text" }
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "object",
|
|
100
|
+
"additionalProperties": false,
|
|
101
|
+
"required": ["path", "format", "jsonPointer"],
|
|
102
|
+
"properties": {
|
|
103
|
+
"path": { "$ref": "#/$defs/projectRelativePath" },
|
|
104
|
+
"format": { "const": "json" },
|
|
105
|
+
"jsonPointer": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"pattern": "^(/([^/~]|~[01])*)+$",
|
|
108
|
+
"description": "Non-empty RFC 6901 JSON Pointer."
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"releaseUnit": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": false,
|
|
117
|
+
"required": ["id", "versionSource"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"id": { "type": "string", "minLength": 1 },
|
|
120
|
+
"versionSource": { "$ref": "#/$defs/versionReference" },
|
|
121
|
+
"versionMirrors": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"uniqueItems": true,
|
|
124
|
+
"items": { "$ref": "#/$defs/versionReference" }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"engineeringEntry": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"required": ["name", "form", "physicalRef"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"name": { "type": "string", "minLength": 1 },
|
|
134
|
+
"form": { "enum": ["skill_md", "file"] },
|
|
135
|
+
"physicalRef": { "$ref": "#/$defs/projectRelativePath" }
|
|
136
|
+
},
|
|
137
|
+
"allOf": [
|
|
138
|
+
{
|
|
139
|
+
"if": {
|
|
140
|
+
"properties": { "form": { "const": "skill_md" } },
|
|
141
|
+
"required": ["form"]
|
|
142
|
+
},
|
|
143
|
+
"then": {
|
|
144
|
+
"properties": {
|
|
145
|
+
"physicalRef": { "type": "string", "pattern": "(^|/)SKILL\\.md$" }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
"projectRelativePath": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"minLength": 1,
|
|
154
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*(?:^|/)\\.(?:/|$))(?!.*\\\\)(?!.*\\u0000)[^/\\\\\\u0000]+(?:/[^/\\\\\\u0000]+)*$",
|
|
155
|
+
"description": "A non-empty path relative to the caller-provided project root. Containment and file identity are mechanism concerns."
|
|
156
|
+
}
|
|
66
157
|
}
|
|
67
158
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.skill-family.example/v1/state-store-recovery-observation.json",
|
|
4
|
+
"title": "State store recovery observation",
|
|
5
|
+
"description": "Read-only maintenance observation. The runtime rechecks identities and original bytes before any takeover; confirmation does not itself provide exclusion.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "kind", "root", "directories", "controls"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"kind": { "const": "skill-family.state-store-recovery-observation" },
|
|
12
|
+
"root": { "type": "string", "pattern": "^/(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*[\\\\\\u0000])(?:[^/]+(?:/[^/]+)*)?$" },
|
|
13
|
+
"directories": {
|
|
14
|
+
"type": "array", "minItems": 1, "maxItems": 3, "uniqueItems": true,
|
|
15
|
+
"items": { "$ref": "#/$defs/directory" },
|
|
16
|
+
"contains": { "type": "object", "properties": { "path": { "const": "." }, "exists": { "const": true } }, "required": ["path", "exists"] },
|
|
17
|
+
"minContains": 1, "maxContains": 1
|
|
18
|
+
},
|
|
19
|
+
"controls": {
|
|
20
|
+
"type": "array", "minItems": 3, "maxItems": 3,
|
|
21
|
+
"prefixItems": [
|
|
22
|
+
{ "$ref": "#/$defs/control", "properties": { "path": { "const": "writer.lock" }, "record": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/writerRecord" }] } }, "type": "object" },
|
|
23
|
+
{ "$ref": "#/$defs/control", "properties": { "path": { "const": "writer-mutation.lock" }, "record": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/mutationRecord" }] } }, "type": "object" },
|
|
24
|
+
{ "$ref": "#/$defs/control", "properties": { "path": { "const": "writer-recovery.lock" }, "record": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/recoveryRecord" }] } }, "type": "object" }
|
|
25
|
+
],
|
|
26
|
+
"items": false
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"$defs": {
|
|
30
|
+
"identity": {
|
|
31
|
+
"type": "object", "additionalProperties": false,
|
|
32
|
+
"required": ["device", "inode", "mode", "type"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"device": { "type": "string", "pattern": "^(0|[1-9][0-9]*)$" },
|
|
35
|
+
"inode": { "type": "string", "pattern": "^(0|[1-9][0-9]*)$" },
|
|
36
|
+
"mode": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
|
37
|
+
"type": { "enum": ["file", "directory"] }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"directory": {
|
|
41
|
+
"type": "object", "additionalProperties": false,
|
|
42
|
+
"required": ["path", "exists", "identity"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"path": { "enum": [".", "events", "snapshots"] },
|
|
45
|
+
"exists": { "type": "boolean" },
|
|
46
|
+
"identity": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/identity" }] }
|
|
47
|
+
},
|
|
48
|
+
"allOf": [{
|
|
49
|
+
"if": { "properties": { "exists": { "const": true } }, "required": ["exists"] },
|
|
50
|
+
"then": { "properties": { "identity": { "$ref": "#/$defs/identity", "type": "object", "properties": { "type": { "const": "directory" } } } } },
|
|
51
|
+
"else": { "properties": { "identity": { "type": "null" } } }
|
|
52
|
+
}]
|
|
53
|
+
},
|
|
54
|
+
"control": {
|
|
55
|
+
"type": "object", "additionalProperties": false,
|
|
56
|
+
"required": ["path", "exists", "identity", "nlink", "size", "sha256", "record"],
|
|
57
|
+
"properties": {
|
|
58
|
+
"path": { "enum": ["writer.lock", "writer-mutation.lock", "writer-recovery.lock"] },
|
|
59
|
+
"exists": { "type": "boolean" },
|
|
60
|
+
"identity": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/identity" }] },
|
|
61
|
+
"nlink": { "enum": [null, 1] },
|
|
62
|
+
"size": { "type": ["integer", "null"], "minimum": 0, "maximum": 9007199254740991 },
|
|
63
|
+
"sha256": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" },
|
|
64
|
+
"record": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/record" }] }
|
|
65
|
+
},
|
|
66
|
+
"allOf": [{
|
|
67
|
+
"if": { "properties": { "exists": { "const": true } }, "required": ["exists"] },
|
|
68
|
+
"then": { "properties": {
|
|
69
|
+
"identity": { "$ref": "#/$defs/identity", "type": "object", "properties": { "type": { "const": "file" } } },
|
|
70
|
+
"nlink": { "const": 1 }, "size": { "type": "integer" }, "sha256": { "type": "string" }, "record": { "$ref": "#/$defs/record" }
|
|
71
|
+
} },
|
|
72
|
+
"else": { "properties": { "identity": { "type": "null" }, "nlink": { "type": "null" }, "size": { "type": "null" }, "sha256": { "type": "null" }, "record": { "type": "null" } } }
|
|
73
|
+
}]
|
|
74
|
+
},
|
|
75
|
+
"record": {
|
|
76
|
+
"type": "object", "additionalProperties": false,
|
|
77
|
+
"required": ["status", "value"],
|
|
78
|
+
"properties": { "status": { "enum": ["valid", "partial", "unsupported"] }, "value": { "type": ["object", "null"] } },
|
|
79
|
+
"allOf": [{
|
|
80
|
+
"if": { "properties": { "status": { "const": "valid" } }, "required": ["status"] },
|
|
81
|
+
"then": { "properties": { "value": { "type": "object" } } },
|
|
82
|
+
"else": { "properties": { "value": { "type": "null" } } }
|
|
83
|
+
}]
|
|
84
|
+
},
|
|
85
|
+
"writerRecord": {
|
|
86
|
+
"$ref": "#/$defs/record", "type": "object",
|
|
87
|
+
"allOf": [{
|
|
88
|
+
"if": { "properties": { "status": { "const": "valid" } }, "required": ["status"] },
|
|
89
|
+
"then": { "properties": { "value": {
|
|
90
|
+
"type": "object", "additionalProperties": false,
|
|
91
|
+
"required": ["schemaVersion", "owner", "fencing", "acquisitionId", "acquiredAt"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"schemaVersion": { "const": 1 }, "owner": { "type": "string", "minLength": 1 },
|
|
94
|
+
"fencing": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
|
|
95
|
+
"acquisitionId": { "type": "string", "pattern": "^[0-9a-f]{24}$" },
|
|
96
|
+
"acquiredAt": { "type": "string", "format": "date-time" }
|
|
97
|
+
}
|
|
98
|
+
} } }
|
|
99
|
+
}]
|
|
100
|
+
},
|
|
101
|
+
"mutationRecord": {
|
|
102
|
+
"$ref": "#/$defs/record", "type": "object",
|
|
103
|
+
"allOf": [{
|
|
104
|
+
"if": { "properties": { "status": { "const": "valid" } }, "required": ["status"] },
|
|
105
|
+
"then": { "properties": { "value": {
|
|
106
|
+
"type": "object", "additionalProperties": false,
|
|
107
|
+
"required": ["schemaVersion", "owner", "fencing", "acquisitionId", "operation", "mutationId"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"schemaVersion": { "const": 1 }, "owner": { "type": "string", "minLength": 1 },
|
|
110
|
+
"fencing": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
|
|
111
|
+
"acquisitionId": { "type": "string", "pattern": "^(?:[0-9a-f]{24}|recovery-claim)$" },
|
|
112
|
+
"operation": { "type": "string", "minLength": 1 },
|
|
113
|
+
"mutationId": { "type": "string", "pattern": "^[0-9a-f]{24}$" }
|
|
114
|
+
}
|
|
115
|
+
} } }
|
|
116
|
+
}]
|
|
117
|
+
},
|
|
118
|
+
"recoveryRecord": {
|
|
119
|
+
"$ref": "#/$defs/record", "type": "object",
|
|
120
|
+
"allOf": [{
|
|
121
|
+
"if": { "properties": { "status": { "const": "valid" } }, "required": ["status"] },
|
|
122
|
+
"then": { "properties": { "value": {
|
|
123
|
+
"type": "object", "additionalProperties": false, "required": ["schemaVersion", "recoveryId"],
|
|
124
|
+
"properties": { "schemaVersion": { "const": 1 }, "recoveryId": { "type": "string", "pattern": "^[0-9a-f]{24}$" } }
|
|
125
|
+
} } }
|
|
126
|
+
}]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/snapshot/frozen.mjs
CHANGED
|
@@ -199,9 +199,10 @@ export async function verifyFrozenDirectoryStructure(directory, label = 'frozen
|
|
|
199
199
|
*
|
|
200
200
|
* Consumption only: this never rebuilds objects and never reads the wall
|
|
201
201
|
* clock. When `commitTimestamp` (the plan's `frozenSnapshot.commitTimestamp`)
|
|
202
|
-
* is provided, the
|
|
203
|
-
*
|
|
204
|
-
*
|
|
202
|
+
* is provided, the plan canonical form remains `+00:00`. Git may read the
|
|
203
|
+
* same UTC instant back as `Z` via `%aI`/`%cI`. Both sides pass through
|
|
204
|
+
* `normalizeGitTimestamp` before comparison, so a commit carrying any other
|
|
205
|
+
* author/committer instant can never pass pre-publish verification.
|
|
205
206
|
*/
|
|
206
207
|
export async function verifyFrozenGitRepository({
|
|
207
208
|
root,
|
|
@@ -225,13 +226,15 @@ export async function verifyFrozenGitRepository({
|
|
|
225
226
|
['--git-dir', gitDir, 'show', '-s', '--format=%aI%n%cI', commit],
|
|
226
227
|
{ shell: false },
|
|
227
228
|
);
|
|
228
|
-
const [
|
|
229
|
+
const [rawAuthorDate, rawCommitterDate] = datesOut.trim().split('\n');
|
|
230
|
+
const authorDate = normalizeGitTimestamp(rawAuthorDate, 'frozen commit author date');
|
|
231
|
+
const committerDate = normalizeGitTimestamp(rawCommitterDate, 'frozen commit committer date');
|
|
229
232
|
if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
|
|
230
233
|
throw frozenError('frozen git commit dates do not match the plan freeze timestamp', {
|
|
231
234
|
commit,
|
|
232
235
|
expectedTimestamp,
|
|
233
|
-
observedAuthorDate:
|
|
234
|
-
observedCommitterDate:
|
|
236
|
+
observedAuthorDate: rawAuthorDate,
|
|
237
|
+
observedCommitterDate: rawCommitterDate,
|
|
235
238
|
});
|
|
236
239
|
}
|
|
237
240
|
}
|
|
@@ -314,9 +317,10 @@ const GIT_ISO8601_STRICT_RE =
|
|
|
314
317
|
*
|
|
315
318
|
* The value must be a non-empty, strict ISO 8601 timestamp with an explicit UTC
|
|
316
319
|
* offset and a parseable calendar date. It is normalized to UTC with second
|
|
317
|
-
* precision and a `+00:00` offset
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
+
* precision and a `+00:00` offset. The plan canonical form remains `+00:00`.
|
|
321
|
+
* Git may read the same UTC instant back as `Z` via `%aI`/`%cI`; both sides
|
|
322
|
+
* pass through `normalizeGitTimestamp` before comparison. Missing, empty,
|
|
323
|
+
* malformed, or out-of-range values throw a closed `GATE_FAILED` error.
|
|
320
324
|
*
|
|
321
325
|
* Normalization is idempotent: re-normalizing an already-canonical value returns
|
|
322
326
|
* the identical string, so prepare and the Git builder always agree byte-for-byte.
|
|
@@ -399,11 +403,12 @@ function publicGitRemote({ repo, githubHost = 'github.com' }) {
|
|
|
399
403
|
* `commitTimestamp` is the plan freeze timestamp sampled exactly once during
|
|
400
404
|
* production prepare. It is strictly validated BEFORE any filesystem or Git
|
|
401
405
|
* write (missing, empty, malformed, or impossible values fail closed), then
|
|
402
|
-
* written verbatim to both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
|
|
403
|
-
*
|
|
404
|
-
* `
|
|
405
|
-
* yield identical tree and commit
|
|
406
|
-
*
|
|
406
|
+
* written verbatim to both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE. The plan
|
|
407
|
+
* canonical form remains `+00:00`. Git may read the same UTC instant back as
|
|
408
|
+
* `Z` via `%aI`/`%cI`; both sides pass through `normalizeGitTimestamp` before
|
|
409
|
+
* comparison. Identical inputs therefore yield identical tree and commit
|
|
410
|
+
* identifiers, and a different freeze timestamp alone yields a different
|
|
411
|
+
* commit with an identical tree.
|
|
407
412
|
*/
|
|
408
413
|
export async function buildFrozenGitRepository({
|
|
409
414
|
snapshotDir,
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/** Best-effort cleanup of superseded, complete production run lineages. */
|
|
2
|
-
|
|
3
|
-
import { lstat, readFile, readdir, realpath, rm as fsRm } from 'node:fs/promises';
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { resolveContained } from 'skill-family-harness-node';
|
|
6
|
-
import { assertImmutablePlanAuthority, computePlanDigest, validatePlan } from './plan.mjs';
|
|
7
|
-
import { loadRun, validateRunCheckpointMapping, validateRunLineage, validateRunPlanDigest } from './run.mjs';
|
|
8
|
-
import { normalizePostPublishView, postPublishActionId } from './postpublish.mjs';
|
|
9
|
-
|
|
10
|
-
const NON_TERMINAL = new Set(['PUBLISHING', 'PARTIAL', 'NEEDS_INPUT', 'BLOCKED', 'DISTRIBUTING']);
|
|
11
|
-
const COMPLETED_CHECKPOINTS = new Set(['succeeded', 'skipped']);
|
|
12
|
-
|
|
13
|
-
function validDirectName(name) {
|
|
14
|
-
return typeof name === 'string' && name.length > 0 && name !== '.' && name !== '..'
|
|
15
|
-
&& !isAbsolute(name) && !name.includes('/') && !name.includes('\\');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function jsonFile(path) {
|
|
19
|
-
try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function physicalContained(root, declaredPath) {
|
|
23
|
-
if (typeof declaredPath !== 'string' || !isAbsolute(declaredPath)) return null;
|
|
24
|
-
const lexicalRoot = resolve(root);
|
|
25
|
-
const physicalRoot = await realpath(lexicalRoot);
|
|
26
|
-
const lexical = relative(lexicalRoot, resolve(declaredPath));
|
|
27
|
-
if (!lexical || isAbsolute(lexical) || lexical === '..' || lexical.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) return null;
|
|
28
|
-
try {
|
|
29
|
-
const contained = await resolveContained(lexicalRoot, lexical);
|
|
30
|
-
const target = await realpath(contained);
|
|
31
|
-
const targetRel = relative(physicalRoot, target);
|
|
32
|
-
if (!targetRel || isAbsolute(targetRel) || targetRel === '..' || targetRel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)) return null;
|
|
33
|
-
return target;
|
|
34
|
-
} catch { return null; }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function loadPlanFromRun(releaseDir, run) {
|
|
38
|
-
if (!run?.planPath) return null;
|
|
39
|
-
const planPath = await physicalContained(releaseDir, run.planPath);
|
|
40
|
-
if (!planPath || basename(dirname(planPath)) !== 'plans') return null;
|
|
41
|
-
const plan = await jsonFile(planPath);
|
|
42
|
-
if (!plan) return null;
|
|
43
|
-
try {
|
|
44
|
-
validatePlan(plan);
|
|
45
|
-
assertImmutablePlanAuthority(planPath, plan);
|
|
46
|
-
const digest = computePlanDigest(plan);
|
|
47
|
-
if (run.planDigest !== digest || basename(planPath) !== `${digest}.json`) return null;
|
|
48
|
-
return { plan, planPath, digest };
|
|
49
|
-
} catch { return null; }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function postVerifyActions(plan) {
|
|
53
|
-
return normalizePostPublishView(plan).flatMap((declaration) => (
|
|
54
|
-
(declaration.hooks ?? []).filter((hook) => hook.phase === 'postVerify').map((hook) => ({
|
|
55
|
-
id: postPublishActionId({ planVersion: plan.planVersion, unitId: declaration.unitId, localId: hook.id }),
|
|
56
|
-
type: 'postpublish-hook',
|
|
57
|
-
}))
|
|
58
|
-
));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function postVerificationComplete(run) {
|
|
62
|
-
return run.checkpoints.every((checkpoint) => (
|
|
63
|
-
checkpoint.status === 'succeeded' || checkpoint.status === 'NO_CHANGE'
|
|
64
|
-
));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function publicationComplete(run) {
|
|
68
|
-
return run.checkpoints.every((cp) => COMPLETED_CHECKPOINTS.has(cp.status)
|
|
69
|
-
|| (['failed', 'deferred'].includes(cp.status)
|
|
70
|
-
&& ['claude-marketplace-install', 'codex-marketplace-install', 'kimi-marketplace-install', 'codebuddy-marketplace-install', 'qoder-marketplace-install'].includes(cp.actionType)));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function lineagePathsContained(run, runsRoot) {
|
|
74
|
-
let cursor = run;
|
|
75
|
-
for (let depth = 0; depth < 32; depth += 1) {
|
|
76
|
-
if (!cursor.sourceRunPath) return true;
|
|
77
|
-
const sourcePath = await physicalContained(runsRoot, cursor.sourceRunPath);
|
|
78
|
-
if (!sourcePath) return false;
|
|
79
|
-
cursor = await loadRun(sourcePath, { requireDigest: true });
|
|
80
|
-
}
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function validatePublicationCheckpoints(run, plan, runsRoot) {
|
|
85
|
-
let cursor = run;
|
|
86
|
-
for (let depth = 0; depth < 32; depth += 1) {
|
|
87
|
-
if (cursor.command === 'publish') {
|
|
88
|
-
validateRunCheckpointMapping(cursor, plan.externalActions ?? []);
|
|
89
|
-
return cursor.status === 'PUBLISHED' ? publicationComplete(cursor) : true;
|
|
90
|
-
}
|
|
91
|
-
if (!cursor.sourceRunPath) return false;
|
|
92
|
-
const sourcePath = await physicalContained(runsRoot, cursor.sourceRunPath);
|
|
93
|
-
if (!sourcePath) return false;
|
|
94
|
-
cursor = await loadRun(sourcePath, { requireDigest: true });
|
|
95
|
-
if (cursor.command === 'reconcile') validateRunCheckpointMapping(cursor, plan.externalActions ?? []);
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function validateTerminal({ candidate, planInfo, runsRoot }) {
|
|
101
|
-
const { run, runPath } = candidate;
|
|
102
|
-
if (!run || !planInfo) return false;
|
|
103
|
-
const { plan, planPath } = planInfo;
|
|
104
|
-
try {
|
|
105
|
-
if (!(await lineagePathsContained(run, runsRoot))) return false;
|
|
106
|
-
await validateRunLineage(run, { plan, planPath, runPath, production: Boolean(plan.production) });
|
|
107
|
-
validateRunPlanDigest(run, plan, { planPath });
|
|
108
|
-
if (run.command === 'verify' && run.status === 'VERIFIED') {
|
|
109
|
-
return validatePublicationCheckpoints(run, plan, runsRoot);
|
|
110
|
-
}
|
|
111
|
-
if (run.command !== 'postverify' || run.status !== 'DISTRIBUTED') return false;
|
|
112
|
-
validateRunCheckpointMapping(run, postVerifyActions(plan));
|
|
113
|
-
return postVerificationComplete(run)
|
|
114
|
-
&& validatePublicationCheckpoints(run, plan, runsRoot);
|
|
115
|
-
} catch { return false; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function scanCandidate(runsRoot, name) {
|
|
119
|
-
const dir = join(runsRoot, name);
|
|
120
|
-
const summary = await jsonFile(join(dir, 'summary.json'));
|
|
121
|
-
const runPath = join(dir, 'release-run.json');
|
|
122
|
-
let run = null;
|
|
123
|
-
let runExists = false;
|
|
124
|
-
let runCorrupt = false;
|
|
125
|
-
try {
|
|
126
|
-
const stat = await lstat(runPath);
|
|
127
|
-
runExists = true;
|
|
128
|
-
if (stat.isSymbolicLink() || !stat.isFile()) runCorrupt = true;
|
|
129
|
-
else run = await loadRun(runPath, { requireDigest: true });
|
|
130
|
-
} catch { if (runExists) runCorrupt = true; }
|
|
131
|
-
const statesExists = await lstat(join(dir, 'states')).then(() => true).catch(() => false);
|
|
132
|
-
let sealedFailedPrepare = false;
|
|
133
|
-
if (summary?.status === 'FAILED' && !runExists) {
|
|
134
|
-
try {
|
|
135
|
-
const evidence = await readFile(join(dir, 'evidence.jsonl'), 'utf8');
|
|
136
|
-
sealedFailedPrepare = evidence.split('\n').some((line) => JSON.parse(line)?.command === 'prepare');
|
|
137
|
-
} catch { sealedFailedPrepare = false; }
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
dir, name, summary, run, runPath, runExists, runCorrupt, statesExists, sealedFailedPrepare,
|
|
141
|
-
planDigest: run?.planDigest ?? summary?.planDigest ?? null,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function deleteCandidate(runsRoot, candidate, rmFn) {
|
|
146
|
-
const contained = await physicalContained(runsRoot, join(runsRoot, candidate.name));
|
|
147
|
-
const physicalRoot = await realpath(runsRoot);
|
|
148
|
-
if (!contained || dirname(contained) !== physicalRoot) throw new Error('run path containment failed');
|
|
149
|
-
const stat = await lstat(contained);
|
|
150
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('run candidate is not a real directory');
|
|
151
|
-
await rmFn(contained, { recursive: true, force: false });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** @returns {Promise<{scanned:number,deleted:number,errors:number,diagnostics:Array}>} */
|
|
155
|
-
export async function cleanupRunRetention({ releaseDir, currentRunDir, rmFn = fsRm } = {}) {
|
|
156
|
-
const result = { scanned: 0, deleted: 0, errors: 0, diagnostics: [] };
|
|
157
|
-
if (!releaseDir) return result;
|
|
158
|
-
const runsRoot = resolve(releaseDir, 'runs');
|
|
159
|
-
let rootStat;
|
|
160
|
-
try { rootStat = await lstat(runsRoot); } catch { return result; }
|
|
161
|
-
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return result;
|
|
162
|
-
const physicalRunsRoot = await realpath(runsRoot).catch(() => null);
|
|
163
|
-
if (!physicalRunsRoot) return result;
|
|
164
|
-
let entries;
|
|
165
|
-
try { entries = await readdir(physicalRunsRoot, { withFileTypes: true }); } catch { return result; }
|
|
166
|
-
const candidates = [];
|
|
167
|
-
for (const entry of entries) {
|
|
168
|
-
result.scanned += 1;
|
|
169
|
-
if (!entry.isDirectory() || entry.isSymbolicLink() || !validDirectName(entry.name)) continue;
|
|
170
|
-
candidates.push(await scanCandidate(physicalRunsRoot, entry.name));
|
|
171
|
-
}
|
|
172
|
-
const protectedNames = new Set();
|
|
173
|
-
if (currentRunDir) protectedNames.add(basename(resolve(currentRunDir)));
|
|
174
|
-
const currentPlan = await jsonFile(join(releaseDir, 'release-plan.json'));
|
|
175
|
-
let currentPlanDigest = null;
|
|
176
|
-
try { if (currentPlan) currentPlanDigest = computePlanDigest(currentPlan); } catch { currentPlanDigest = null; }
|
|
177
|
-
const groups = new Map();
|
|
178
|
-
for (const candidate of candidates) {
|
|
179
|
-
if (!candidate.planDigest) continue;
|
|
180
|
-
if (!groups.has(candidate.planDigest)) groups.set(candidate.planDigest, []);
|
|
181
|
-
groups.get(candidate.planDigest).push(candidate);
|
|
182
|
-
if (currentPlanDigest && candidate.planDigest === currentPlanDigest) protectedNames.add(candidate.name);
|
|
183
|
-
}
|
|
184
|
-
const completeGroups = [];
|
|
185
|
-
for (const [planDigest, group] of groups) {
|
|
186
|
-
const authority = group.find((item) => item.run?.planDigest === planDigest);
|
|
187
|
-
const planInfo = authority ? await loadPlanFromRun(releaseDir, authority.run) : null;
|
|
188
|
-
const requiresPostVerify = Boolean(planInfo && normalizePostPublishView(planInfo.plan)
|
|
189
|
-
.some((declaration) => (declaration.hooks ?? []).some((hook) => hook.phase === 'postVerify')));
|
|
190
|
-
const terminals = group.filter((item) => requiresPostVerify
|
|
191
|
-
? item.run?.status === 'DISTRIBUTED'
|
|
192
|
-
: item.run?.status === 'VERIFIED');
|
|
193
|
-
const terminalResults = planInfo
|
|
194
|
-
? await Promise.all(terminals.map((item) => validateTerminal({ candidate: item, planInfo, runsRoot })))
|
|
195
|
-
: [];
|
|
196
|
-
const terminalValid = terminalResults.some(Boolean);
|
|
197
|
-
const unsafe = group.some((item) => item.runCorrupt
|
|
198
|
-
|| (!item.run && item.summary?.status !== 'PREPARED')
|
|
199
|
-
|| (item.run && NON_TERMINAL.has(item.run.status))
|
|
200
|
-
|| (item.run?.status === 'PUBLISHED' && !publicationComplete(item.run))
|
|
201
|
-
|| (item.run && ((requiresPostVerify && item.run.status === 'DISTRIBUTED')
|
|
202
|
-
|| (!requiresPostVerify && item.run.status === 'VERIFIED'))
|
|
203
|
-
&& !terminalResults[terminals.indexOf(item)]));
|
|
204
|
-
if (!terminalValid || unsafe) {
|
|
205
|
-
for (const item of group) protectedNames.add(item.name);
|
|
206
|
-
} else {
|
|
207
|
-
completeGroups.push({ group, createdAt: planInfo.plan.createdAt ?? '', planDigest });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
completeGroups.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)) || b.planDigest.localeCompare(a.planDigest));
|
|
211
|
-
for (const old of completeGroups.slice(1)) {
|
|
212
|
-
for (const item of old.group) {
|
|
213
|
-
if (protectedNames.has(item.name)) continue;
|
|
214
|
-
try { await deleteCandidate(physicalRunsRoot, item, rmFn); result.deleted += 1; }
|
|
215
|
-
catch (error) { result.errors += 1; result.diagnostics.push({ name: item.name, code: error?.code ?? 'RUN_RETENTION_DELETE_FAILED' }); }
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const candidate of candidates) {
|
|
219
|
-
const hasPlanTrace = Boolean(candidate.planDigest || candidate.summary?.planPath || candidate.summary?.externalActions || candidate.summary?.checkpoints);
|
|
220
|
-
const isFailedPrepare = candidate.name.startsWith('prepare-') && candidate.summary?.status === 'FAILED'
|
|
221
|
-
&& candidate.sealedFailedPrepare && !candidate.runExists && !candidate.statesExists && !hasPlanTrace;
|
|
222
|
-
if (!isFailedPrepare || protectedNames.has(candidate.name)) continue;
|
|
223
|
-
try { await deleteCandidate(physicalRunsRoot, candidate, rmFn); result.deleted += 1; }
|
|
224
|
-
catch (error) { result.errors += 1; result.diagnostics.push({ name: candidate.name, code: error?.code ?? 'RUN_RETENTION_DELETE_FAILED' }); }
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
}
|