release-skill 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +23 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +26 -0
- package/CHANGELOG.md +68 -0
- package/CODE_OF_CONDUCT.md +76 -0
- package/CONTRIBUTING.md +49 -0
- package/INSTALL.md +182 -0
- package/LICENSE +21 -0
- package/NOTICE +25 -0
- package/README.md +501 -0
- package/README.zh-CN.md +463 -0
- package/SECURITY.md +48 -0
- package/adapters/claude/.claude-plugin/marketplace.json +16 -0
- package/adapters/claude/.claude-plugin/plugin.json +10 -0
- package/adapters/claude/skills/release-assess/SKILL.md +52 -0
- package/adapters/claude/skills/release-help/SKILL.md +60 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
- package/adapters/claude/skills/release-publish/SKILL.md +55 -0
- package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/claude/skills/release-verify/SKILL.md +70 -0
- package/adapters/codex/.codex-plugin/plugin.json +26 -0
- package/adapters/codex/skills/release-assess/SKILL.md +52 -0
- package/adapters/codex/skills/release-help/SKILL.md +60 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
- package/adapters/codex/skills/release-publish/SKILL.md +55 -0
- package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/codex/skills/release-verify/SKILL.md +70 -0
- package/bin/release-skill.mjs +743 -0
- package/native/safe-write/binding.gyp +40 -0
- package/native/safe-write/prebuilds.json +4 -0
- package/native/safe-write/src/safe_write.cc +2023 -0
- package/package.json +75 -0
- package/references/.render-manifest.json +33 -0
- package/references/00-target-state.md +124 -0
- package/references/01-state-machine.md +155 -0
- package/references/02-project-config.md +217 -0
- package/references/03-readme-quality.md +136 -0
- package/references/04-supply-chain.md +147 -0
- package/references/05-evidence-and-errors.md +164 -0
- package/references/06-adapter-contract.md +178 -0
- package/schemas/.render-manifest.json +37 -0
- package/schemas/approval-record.schema.json +115 -0
- package/schemas/artifact-lock.schema.json +111 -0
- package/schemas/artifact-plan.schema.json +52 -0
- package/schemas/artifact-policy.schema.json +76 -0
- package/schemas/evidence-event.schema.json +89 -0
- package/schemas/release-plan.schema.json +369 -0
- package/schemas/release-project.schema.json +359 -0
- package/schemas/release-run.schema.json +195 -0
- package/skills/release-assess/SKILL.md +52 -0
- package/skills/release-help/SKILL.md +60 -0
- package/skills/release-prepare/SKILL.md +71 -0
- package/skills/release-publish/SKILL.md +55 -0
- package/skills/release-reconcile/SKILL.md +73 -0
- package/skills/release-verify/SKILL.md +70 -0
- package/skills-src/release-assess/SKILL.md +52 -0
- package/skills-src/release-help/SKILL.md +60 -0
- package/skills-src/release-prepare/SKILL.md +71 -0
- package/skills-src/release-publish/SKILL.md +55 -0
- package/skills-src/release-reconcile/SKILL.md +73 -0
- package/skills-src/release-verify/SKILL.md +70 -0
- package/src/adapters/contract.mjs +214 -0
- package/src/adapters/git-github.mjs +214 -0
- package/src/adapters/npm.mjs +947 -0
- package/src/adapters/plugin-marketplace.mjs +1365 -0
- package/src/adapters/push-snapshot.mjs +216 -0
- package/src/artifacts/adoption.mjs +743 -0
- package/src/artifacts/artifact-plan.mjs +162 -0
- package/src/artifacts/entry.mjs +240 -0
- package/src/artifacts/git-authority.mjs +637 -0
- package/src/artifacts/graph.mjs +189 -0
- package/src/artifacts/inspect.mjs +520 -0
- package/src/artifacts/inventory.mjs +192 -0
- package/src/artifacts/merge/binary.mjs +77 -0
- package/src/artifacts/merge/entry-merge.mjs +228 -0
- package/src/artifacts/merge/json.mjs +641 -0
- package/src/artifacts/merge/markdown.mjs +246 -0
- package/src/artifacts/merge/regions.mjs +156 -0
- package/src/artifacts/merge/text.mjs +432 -0
- package/src/artifacts/merge/tree.mjs +202 -0
- package/src/artifacts/merge/yaml.mjs +669 -0
- package/src/artifacts/path-key.mjs +94 -0
- package/src/artifacts/policy.mjs +319 -0
- package/src/artifacts/producer-registry.mjs +439 -0
- package/src/artifacts/project-lock.mjs +732 -0
- package/src/artifacts/resolution.mjs +658 -0
- package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
- package/src/artifacts/safe-fs.mjs +72 -0
- package/src/artifacts/state.mjs +495 -0
- package/src/artifacts/transaction-journal.mjs +983 -0
- package/src/artifacts/transaction.mjs +1361 -0
- package/src/commands/approve.mjs +280 -0
- package/src/commands/artifacts.mjs +627 -0
- package/src/commands/assess.mjs +838 -0
- package/src/commands/prepare.mjs +1377 -0
- package/src/commands/publish.mjs +883 -0
- package/src/commands/reconcile.mjs +1255 -0
- package/src/commands/verify.mjs +915 -0
- package/src/core/approval.mjs +332 -0
- package/src/core/baseline.mjs +272 -0
- package/src/core/blackbox-hard-gates.mjs +142 -0
- package/src/core/config.mjs +448 -0
- package/src/core/digest.mjs +90 -0
- package/src/core/errors.mjs +113 -0
- package/src/core/evidence.mjs +167 -0
- package/src/core/hooks.mjs +241 -0
- package/src/core/node-version.mjs +64 -0
- package/src/core/plan.mjs +735 -0
- package/src/core/previous-public-baseline.mjs +204 -0
- package/src/core/run.mjs +681 -0
- package/src/core/state-machine.mjs +76 -0
- package/src/core/version-consistency.mjs +111 -0
- package/src/producers/build-adapters.mjs +231 -0
- package/src/producers/render-public-assets.mjs +152 -0
- package/src/producers/sync-skills.mjs +96 -0
- package/src/readme/contract.mjs +297 -0
- package/src/readme/examples.mjs +288 -0
- package/src/readme/parity.mjs +122 -0
- package/src/snapshot/export.mjs +99 -0
- package/src/snapshot/frozen.mjs +401 -0
- package/src/snapshot/manifest.mjs +207 -0
- package/src/snapshot/public-map.mjs +1459 -0
- package/src/snapshot/public-path.mjs +110 -0
- package/src/snapshot/scan.mjs +419 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot manifest: content-addressable inventory of a snapshot directory.
|
|
3
|
+
*
|
|
4
|
+
* A {@link SnapshotManifest} records every file in a snapshot directory with its
|
|
5
|
+
* individual SHA-256 hash, the aggregate content hash of all file data, the
|
|
6
|
+
* total file count, total byte size, and the generation timestamp.
|
|
7
|
+
*
|
|
8
|
+
* Hash algorithm is identical to {@link module:digest.sha256Hex}: SHA-256 via
|
|
9
|
+
* Node.js `node:crypto` `createHash`, output as lowercase hex.
|
|
10
|
+
*
|
|
11
|
+
* The manifest is designed so that:
|
|
12
|
+
* - Two runs over identical content always produce the same `contentHash`.
|
|
13
|
+
* - `verifyManifest` can detect any modification (addition, removal, content
|
|
14
|
+
* change) to the snapshot directory after generation.
|
|
15
|
+
*
|
|
16
|
+
* @module snapshot/manifest
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
20
|
+
import { relative, resolve, join } from 'node:path';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Types (documented via JSDoc)
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} FileEntry
|
|
29
|
+
* @property {string} path - Relative path within the snapshot directory.
|
|
30
|
+
* @property {string} sha256 - Lowercase hex SHA-256 of the file content.
|
|
31
|
+
* @property {number} size - File size in bytes.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} SnapshotManifest
|
|
36
|
+
* @property {string} version - Manifest schema version (`"1"`).
|
|
37
|
+
* @property {string} createdAt - ISO-8601 generation timestamp.
|
|
38
|
+
* @property {string} dir - The snapshot directory that was scanned.
|
|
39
|
+
* @property {number} fileCount - Number of files in the snapshot.
|
|
40
|
+
* @property {number} totalSize - Sum of all file sizes in bytes.
|
|
41
|
+
* @property {string} contentHash - Aggregate SHA-256 hex over all file
|
|
42
|
+
* contents, sorted by path, with each file's contribution separated by its
|
|
43
|
+
* relative path (ensuring order-independence between traversal strategies).
|
|
44
|
+
* @property {FileEntry[]} files - Per-file entries sorted by `path`.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Internal helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recursively collect all regular file paths in a directory, returning
|
|
53
|
+
* paths relative to `root`. Symlinks are resolved; directories named
|
|
54
|
+
* `.git` are skipped.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} root - Absolute directory to walk.
|
|
57
|
+
* @returns {Promise<string[]>} Sorted array of relative paths.
|
|
58
|
+
*/
|
|
59
|
+
async function walkFiles(root) {
|
|
60
|
+
const result = [];
|
|
61
|
+
|
|
62
|
+
/** @param {string} dir */
|
|
63
|
+
async function walk(dir) {
|
|
64
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.name === '.git') continue;
|
|
67
|
+
const full = join(dir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
await walk(full);
|
|
70
|
+
} else if (entry.isFile()) {
|
|
71
|
+
result.push(relative(root, full));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await walk(root);
|
|
77
|
+
result.sort();
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Public API
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate a {@link SnapshotManifest} for a directory.
|
|
87
|
+
*
|
|
88
|
+
* The content hash is computed deterministically:
|
|
89
|
+
* 1. Each file is hashed individually (SHA-256).
|
|
90
|
+
* 2. The aggregate content hash is the SHA-256 of the concatenation of
|
|
91
|
+
* `path + "\0" + fileSha256` for every file, in sorted path order.
|
|
92
|
+
* This design makes the hash independent of filesystem traversal order
|
|
93
|
+
* and the number of bytes in each file, while still binding the hash to
|
|
94
|
+
* the file path *and* content.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} snapshotDir - Absolute path to the snapshot directory.
|
|
97
|
+
* @returns {Promise<SnapshotManifest>}
|
|
98
|
+
*/
|
|
99
|
+
export async function generateManifest(snapshotDir) {
|
|
100
|
+
const dir = resolve(snapshotDir);
|
|
101
|
+
|
|
102
|
+
// Verify the directory exists
|
|
103
|
+
const dirStat = await stat(dir);
|
|
104
|
+
if (!dirStat.isDirectory()) {
|
|
105
|
+
throw new Error(`Not a directory: ${dir}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const filePaths = await walkFiles(dir);
|
|
109
|
+
|
|
110
|
+
const files = [];
|
|
111
|
+
let totalSize = 0;
|
|
112
|
+
|
|
113
|
+
// Hash each file and accumulate
|
|
114
|
+
const aggregateHash = createHash('sha256');
|
|
115
|
+
|
|
116
|
+
for (const relPath of filePaths) {
|
|
117
|
+
const absPath = join(dir, relPath);
|
|
118
|
+
const content = await readFile(absPath);
|
|
119
|
+
const fileHash = createHash('sha256').update(content).digest('hex');
|
|
120
|
+
|
|
121
|
+
files.push({
|
|
122
|
+
path: relPath,
|
|
123
|
+
sha256: fileHash,
|
|
124
|
+
size: content.length,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
totalSize += content.length;
|
|
128
|
+
|
|
129
|
+
// Feed path + hash into aggregate (not file bytes), so the aggregate
|
|
130
|
+
// hash is order-invariant with respect to the actual byte sequence but
|
|
131
|
+
// still bound to path and content hash.
|
|
132
|
+
aggregateHash.update(relPath);
|
|
133
|
+
aggregateHash.update('\0');
|
|
134
|
+
aggregateHash.update(fileHash);
|
|
135
|
+
aggregateHash.update('\0');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const contentHash = aggregateHash.digest('hex');
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
version: '1',
|
|
142
|
+
createdAt: new Date().toISOString(),
|
|
143
|
+
dir,
|
|
144
|
+
fileCount: files.length,
|
|
145
|
+
totalSize,
|
|
146
|
+
contentHash,
|
|
147
|
+
files,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Verify that a snapshot directory matches an existing manifest.
|
|
153
|
+
*
|
|
154
|
+
* Returns `true` if every file in the manifest exists in the directory with
|
|
155
|
+
* the same SHA-256 hash, the file count and total size match, and no extra
|
|
156
|
+
* files are present.
|
|
157
|
+
*
|
|
158
|
+
* Returns `false` if any mismatch is detected (file added, removed, content
|
|
159
|
+
* changed, or size/count differs).
|
|
160
|
+
*
|
|
161
|
+
* This function never throws on verification mismatches -- it returns `false`
|
|
162
|
+
* instead. It only throws if the directory cannot be read or the manifest is
|
|
163
|
+
* structurally invalid.
|
|
164
|
+
*
|
|
165
|
+
* @param {SnapshotManifest} manifest - A previously generated manifest.
|
|
166
|
+
* @param {string} [snapshotDir] - Directory to verify. Defaults to
|
|
167
|
+
* `manifest.dir`.
|
|
168
|
+
* @returns {Promise<boolean>}
|
|
169
|
+
*/
|
|
170
|
+
export async function verifyManifest(manifest, snapshotDir) {
|
|
171
|
+
const dir = resolve(snapshotDir ?? manifest.dir);
|
|
172
|
+
|
|
173
|
+
if (!manifest || !Array.isArray(manifest.files)) {
|
|
174
|
+
throw new Error('Invalid manifest: missing files array');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Generate a fresh manifest for the directory
|
|
178
|
+
const fresh = await generateManifest(dir);
|
|
179
|
+
|
|
180
|
+
// Quick structural checks
|
|
181
|
+
if (fresh.fileCount !== manifest.fileCount) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (fresh.totalSize !== manifest.totalSize) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Compare files entry-by-entry (both are sorted by path)
|
|
189
|
+
if (fresh.files.length !== manifest.files.length) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < fresh.files.length; i++) {
|
|
194
|
+
const a = fresh.files[i];
|
|
195
|
+
const b = manifest.files[i];
|
|
196
|
+
if (a.path !== b.path || a.sha256 !== b.sha256 || a.size !== b.size) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Content hash comparison (deterministic re-derivation)
|
|
202
|
+
if (fresh.contentHash !== manifest.contentHash) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
}
|