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.
Files changed (125) hide show
  1. package/.agents/plugins/marketplace.json +23 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +26 -0
  5. package/CHANGELOG.md +68 -0
  6. package/CODE_OF_CONDUCT.md +76 -0
  7. package/CONTRIBUTING.md +49 -0
  8. package/INSTALL.md +182 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +25 -0
  11. package/README.md +501 -0
  12. package/README.zh-CN.md +463 -0
  13. package/SECURITY.md +48 -0
  14. package/adapters/claude/.claude-plugin/marketplace.json +16 -0
  15. package/adapters/claude/.claude-plugin/plugin.json +10 -0
  16. package/adapters/claude/skills/release-assess/SKILL.md +52 -0
  17. package/adapters/claude/skills/release-help/SKILL.md +60 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
  19. package/adapters/claude/skills/release-publish/SKILL.md +55 -0
  20. package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
  21. package/adapters/claude/skills/release-verify/SKILL.md +70 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +26 -0
  23. package/adapters/codex/skills/release-assess/SKILL.md +52 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +60 -0
  25. package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
  26. package/adapters/codex/skills/release-publish/SKILL.md +55 -0
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
  28. package/adapters/codex/skills/release-verify/SKILL.md +70 -0
  29. package/bin/release-skill.mjs +743 -0
  30. package/native/safe-write/binding.gyp +40 -0
  31. package/native/safe-write/prebuilds.json +4 -0
  32. package/native/safe-write/src/safe_write.cc +2023 -0
  33. package/package.json +75 -0
  34. package/references/.render-manifest.json +33 -0
  35. package/references/00-target-state.md +124 -0
  36. package/references/01-state-machine.md +155 -0
  37. package/references/02-project-config.md +217 -0
  38. package/references/03-readme-quality.md +136 -0
  39. package/references/04-supply-chain.md +147 -0
  40. package/references/05-evidence-and-errors.md +164 -0
  41. package/references/06-adapter-contract.md +178 -0
  42. package/schemas/.render-manifest.json +37 -0
  43. package/schemas/approval-record.schema.json +115 -0
  44. package/schemas/artifact-lock.schema.json +111 -0
  45. package/schemas/artifact-plan.schema.json +52 -0
  46. package/schemas/artifact-policy.schema.json +76 -0
  47. package/schemas/evidence-event.schema.json +89 -0
  48. package/schemas/release-plan.schema.json +369 -0
  49. package/schemas/release-project.schema.json +359 -0
  50. package/schemas/release-run.schema.json +195 -0
  51. package/skills/release-assess/SKILL.md +52 -0
  52. package/skills/release-help/SKILL.md +60 -0
  53. package/skills/release-prepare/SKILL.md +71 -0
  54. package/skills/release-publish/SKILL.md +55 -0
  55. package/skills/release-reconcile/SKILL.md +73 -0
  56. package/skills/release-verify/SKILL.md +70 -0
  57. package/skills-src/release-assess/SKILL.md +52 -0
  58. package/skills-src/release-help/SKILL.md +60 -0
  59. package/skills-src/release-prepare/SKILL.md +71 -0
  60. package/skills-src/release-publish/SKILL.md +55 -0
  61. package/skills-src/release-reconcile/SKILL.md +73 -0
  62. package/skills-src/release-verify/SKILL.md +70 -0
  63. package/src/adapters/contract.mjs +214 -0
  64. package/src/adapters/git-github.mjs +214 -0
  65. package/src/adapters/npm.mjs +947 -0
  66. package/src/adapters/plugin-marketplace.mjs +1365 -0
  67. package/src/adapters/push-snapshot.mjs +216 -0
  68. package/src/artifacts/adoption.mjs +743 -0
  69. package/src/artifacts/artifact-plan.mjs +162 -0
  70. package/src/artifacts/entry.mjs +240 -0
  71. package/src/artifacts/git-authority.mjs +637 -0
  72. package/src/artifacts/graph.mjs +189 -0
  73. package/src/artifacts/inspect.mjs +520 -0
  74. package/src/artifacts/inventory.mjs +192 -0
  75. package/src/artifacts/merge/binary.mjs +77 -0
  76. package/src/artifacts/merge/entry-merge.mjs +228 -0
  77. package/src/artifacts/merge/json.mjs +641 -0
  78. package/src/artifacts/merge/markdown.mjs +246 -0
  79. package/src/artifacts/merge/regions.mjs +156 -0
  80. package/src/artifacts/merge/text.mjs +432 -0
  81. package/src/artifacts/merge/tree.mjs +202 -0
  82. package/src/artifacts/merge/yaml.mjs +669 -0
  83. package/src/artifacts/path-key.mjs +94 -0
  84. package/src/artifacts/policy.mjs +319 -0
  85. package/src/artifacts/producer-registry.mjs +439 -0
  86. package/src/artifacts/project-lock.mjs +732 -0
  87. package/src/artifacts/resolution.mjs +658 -0
  88. package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
  89. package/src/artifacts/safe-fs.mjs +72 -0
  90. package/src/artifacts/state.mjs +495 -0
  91. package/src/artifacts/transaction-journal.mjs +983 -0
  92. package/src/artifacts/transaction.mjs +1361 -0
  93. package/src/commands/approve.mjs +280 -0
  94. package/src/commands/artifacts.mjs +627 -0
  95. package/src/commands/assess.mjs +838 -0
  96. package/src/commands/prepare.mjs +1377 -0
  97. package/src/commands/publish.mjs +883 -0
  98. package/src/commands/reconcile.mjs +1255 -0
  99. package/src/commands/verify.mjs +915 -0
  100. package/src/core/approval.mjs +332 -0
  101. package/src/core/baseline.mjs +272 -0
  102. package/src/core/blackbox-hard-gates.mjs +142 -0
  103. package/src/core/config.mjs +448 -0
  104. package/src/core/digest.mjs +90 -0
  105. package/src/core/errors.mjs +113 -0
  106. package/src/core/evidence.mjs +167 -0
  107. package/src/core/hooks.mjs +241 -0
  108. package/src/core/node-version.mjs +64 -0
  109. package/src/core/plan.mjs +735 -0
  110. package/src/core/previous-public-baseline.mjs +204 -0
  111. package/src/core/run.mjs +681 -0
  112. package/src/core/state-machine.mjs +76 -0
  113. package/src/core/version-consistency.mjs +111 -0
  114. package/src/producers/build-adapters.mjs +231 -0
  115. package/src/producers/render-public-assets.mjs +152 -0
  116. package/src/producers/sync-skills.mjs +96 -0
  117. package/src/readme/contract.mjs +297 -0
  118. package/src/readme/examples.mjs +288 -0
  119. package/src/readme/parity.mjs +122 -0
  120. package/src/snapshot/export.mjs +99 -0
  121. package/src/snapshot/frozen.mjs +401 -0
  122. package/src/snapshot/manifest.mjs +207 -0
  123. package/src/snapshot/public-map.mjs +1459 -0
  124. package/src/snapshot/public-path.mjs +110 -0
  125. 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
+ }