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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown format-preserving three-way merge.
|
|
3
|
+
*
|
|
4
|
+
* Splits the file into frontmatter, managed regions (declared by start/end
|
|
5
|
+
* markers), and unmanaged body ranges. Unmanaged ranges are byte-preserved
|
|
6
|
+
* from the current side. Managed regions are merged via text diff3 on the
|
|
7
|
+
* region content. Frontmatter is merged via text diff3 on the YAML block.
|
|
8
|
+
*
|
|
9
|
+
* No full-file parse or stringify is ever performed — all edits are
|
|
10
|
+
* localised byte-range replacements.
|
|
11
|
+
*
|
|
12
|
+
* @module artifacts/merge/markdown
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseManagedRegions } from './regions.mjs';
|
|
16
|
+
import { mergeText } from './text.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect YAML frontmatter in a Markdown buffer.
|
|
20
|
+
*
|
|
21
|
+
* Frontmatter starts with `---\n` at offset 0 and ends at the next `---\n`.
|
|
22
|
+
*
|
|
23
|
+
* @param {Buffer} bytes
|
|
24
|
+
* @returns {{ endOffset: number }|null} End offset (exclusive) of the frontmatter
|
|
25
|
+
* block including the closing `---\n`, or null if no frontmatter.
|
|
26
|
+
*/
|
|
27
|
+
function detectFrontmatter(bytes) {
|
|
28
|
+
const text = bytes.toString('utf8');
|
|
29
|
+
if (!text.startsWith('---\n') && !text.startsWith('---\r\n')) return null;
|
|
30
|
+
|
|
31
|
+
// Find the closing ---
|
|
32
|
+
const afterFirstDelimiter = text.startsWith('---\r\n') ? 5 : 4;
|
|
33
|
+
const rest = text.substring(afterFirstDelimiter);
|
|
34
|
+
const closeIdx = rest.indexOf('\n---\n');
|
|
35
|
+
if (closeIdx < 0) return null;
|
|
36
|
+
|
|
37
|
+
// end offset = afterFirstDelimiter + closeIdx + length of "\n---\n"
|
|
38
|
+
return { endOffset: afterFirstDelimiter + closeIdx + 5 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Merge two Markdown modifications of the same base content.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} options
|
|
45
|
+
* @param {Buffer} options.base - Base Markdown content.
|
|
46
|
+
* @param {Buffer} options.current - Current (human) Markdown content.
|
|
47
|
+
* @param {Buffer} options.generated - Generated (producer) Markdown content.
|
|
48
|
+
* @param {Array<{ id: string, start: string, end: string }>} [options.managedRegions]
|
|
49
|
+
* Managed-region declarations. Each region is delimited by start/end markers.
|
|
50
|
+
* @returns {{ status: 'MERGEABLE'|'CONFLICT'|'STRUCTURE_INVALID', bytes?: Buffer, conflicts: object[] }}
|
|
51
|
+
*/
|
|
52
|
+
export function mergeMarkdown({ base, current, generated, managedRegions = [] } = {}) {
|
|
53
|
+
// Null/missing inputs → CONFLICT
|
|
54
|
+
if (!base || !current || !generated) {
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
status: 'CONFLICT',
|
|
57
|
+
bytes: undefined,
|
|
58
|
+
conflicts: Object.freeze([{ reason: 'missing input' }]),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Parse managed regions from the CURRENT side (authoritative layout)
|
|
63
|
+
let currentRanges;
|
|
64
|
+
try {
|
|
65
|
+
currentRanges = parseManagedRegions(current, managedRegions);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code === 'STRUCTURE_INVALID') {
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
status: 'STRUCTURE_INVALID',
|
|
70
|
+
bytes: undefined,
|
|
71
|
+
conflicts: Object.freeze([{ reason: err.message }]),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Also parse from base and generated to verify consistency
|
|
78
|
+
let baseRanges;
|
|
79
|
+
try {
|
|
80
|
+
baseRanges = parseManagedRegions(base, managedRegions);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.code === 'STRUCTURE_INVALID') {
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
status: 'STRUCTURE_INVALID',
|
|
85
|
+
bytes: undefined,
|
|
86
|
+
conflicts: Object.freeze([{ reason: `base: ${err.message}` }]),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let generatedRanges;
|
|
93
|
+
try {
|
|
94
|
+
generatedRanges = parseManagedRegions(generated, managedRegions);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.code === 'STRUCTURE_INVALID') {
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
status: 'STRUCTURE_INVALID',
|
|
99
|
+
bytes: undefined,
|
|
100
|
+
conflicts: Object.freeze([{ reason: `generated: ${err.message}` }]),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Detect frontmatter on all three sides
|
|
107
|
+
const baseFm = detectFrontmatter(base);
|
|
108
|
+
const currentFm = detectFrontmatter(current);
|
|
109
|
+
const genFm = detectFrontmatter(generated);
|
|
110
|
+
|
|
111
|
+
// Build output by walking unmanaged and managed ranges in order
|
|
112
|
+
const output = [];
|
|
113
|
+
let cursor = 0;
|
|
114
|
+
|
|
115
|
+
// Process frontmatter first (if present on any side).
|
|
116
|
+
// Three-way frontmatter rules for all 8 presence combinations:
|
|
117
|
+
// base current generated → action
|
|
118
|
+
// 0 0 0 → no FM
|
|
119
|
+
// 0 0 1 → include generated FM (generated-only addition)
|
|
120
|
+
// 0 1 0 → preserve current FM (human addition)
|
|
121
|
+
// 0 1 1 → preserve current FM (human overrides)
|
|
122
|
+
// 1 0 0 → accept removal
|
|
123
|
+
// 1 0 1 → if changed → CONFLICT; else accept removal
|
|
124
|
+
// 1 1 0 → preserve current FM (generated removed, human kept)
|
|
125
|
+
// 1 1 1 → three-way merge
|
|
126
|
+
if (currentFm || baseFm || genFm) {
|
|
127
|
+
const baseFmEnd = baseFm?.endOffset ?? 0;
|
|
128
|
+
const currentFmEnd = currentFm?.endOffset ?? 0;
|
|
129
|
+
const genFmEnd = genFm?.endOffset ?? 0;
|
|
130
|
+
|
|
131
|
+
if (baseFm && currentFm && genFm) {
|
|
132
|
+
// All three have frontmatter — three-way merge
|
|
133
|
+
const baseFmBytes = base.subarray(0, baseFmEnd);
|
|
134
|
+
const currentFmBytes = current.subarray(0, currentFmEnd);
|
|
135
|
+
const genFmBytes = generated.subarray(0, genFmEnd);
|
|
136
|
+
|
|
137
|
+
const fmResult = mergeText({
|
|
138
|
+
base: baseFmBytes,
|
|
139
|
+
current: currentFmBytes,
|
|
140
|
+
generated: genFmBytes,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (fmResult.status === 'CONFLICT') {
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
status: 'CONFLICT',
|
|
146
|
+
bytes: undefined,
|
|
147
|
+
conflicts: Object.freeze(fmResult.conflicts.map((c) =>
|
|
148
|
+
Object.freeze({ ...c, regionId: 'frontmatter' }),
|
|
149
|
+
)),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
output.push(fmResult.bytes);
|
|
154
|
+
cursor = currentFmEnd;
|
|
155
|
+
} else if (!baseFm && !currentFm && genFm) {
|
|
156
|
+
// Generated-only new frontmatter — include it
|
|
157
|
+
output.push(generated.subarray(0, genFmEnd));
|
|
158
|
+
cursor = 0; // no frontmatter consumed from current
|
|
159
|
+
} else if (currentFm) {
|
|
160
|
+
// Current has frontmatter — preserve it (covers 0/1/0, 0/1/1, 1/1/0)
|
|
161
|
+
output.push(current.subarray(0, currentFmEnd));
|
|
162
|
+
cursor = currentFmEnd;
|
|
163
|
+
} else if (baseFm && genFm) {
|
|
164
|
+
// Base and generated have frontmatter, current removed it (1/0/1)
|
|
165
|
+
const baseFmContent = base.subarray(0, baseFmEnd);
|
|
166
|
+
const genFmContent = generated.subarray(0, genFmEnd);
|
|
167
|
+
if (!baseFmContent.equals(genFmContent)) {
|
|
168
|
+
// Generated changed frontmatter but current removed it → conflict
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
status: 'CONFLICT',
|
|
171
|
+
bytes: undefined,
|
|
172
|
+
conflicts: Object.freeze([{
|
|
173
|
+
reason: 'frontmatter removed by current but modified by generated',
|
|
174
|
+
regionId: 'frontmatter',
|
|
175
|
+
}]),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Generated didn't change frontmatter — accept current's removal (1/0/0)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Sort regions by start offset for ordered processing
|
|
183
|
+
const sortedRegions = [...currentRanges.entries()]
|
|
184
|
+
.sort(([, a], [, b]) => a.start - b.start);
|
|
185
|
+
|
|
186
|
+
for (const [regionId, currentRange] of sortedRegions) {
|
|
187
|
+
// Emit unmanaged bytes before this region (from current)
|
|
188
|
+
if (cursor < currentRange.start) {
|
|
189
|
+
output.push(current.subarray(cursor, currentRange.start));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get the corresponding range in base and generated
|
|
193
|
+
const baseRange = baseRanges.get(regionId);
|
|
194
|
+
const genRange = generatedRanges.get(regionId);
|
|
195
|
+
|
|
196
|
+
if (!baseRange || !genRange) {
|
|
197
|
+
// Region missing in base or generated — structural mismatch
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
status: 'STRUCTURE_INVALID',
|
|
200
|
+
bytes: undefined,
|
|
201
|
+
conflicts: Object.freeze([{
|
|
202
|
+
reason: `managed region '${regionId}' has inconsistent marker positions across sides`,
|
|
203
|
+
}]),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract region content from each side (between markers, exclusive)
|
|
208
|
+
const baseContent = base.subarray(baseRange.start, baseRange.end);
|
|
209
|
+
const currentContent = current.subarray(currentRange.start, currentRange.end);
|
|
210
|
+
const generatedContent = generated.subarray(genRange.start, genRange.end);
|
|
211
|
+
|
|
212
|
+
// Text merge the region content
|
|
213
|
+
const regionResult = mergeText({
|
|
214
|
+
base: baseContent,
|
|
215
|
+
current: currentContent,
|
|
216
|
+
generated: generatedContent,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (regionResult.status === 'CONFLICT') {
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
status: 'CONFLICT',
|
|
222
|
+
bytes: undefined,
|
|
223
|
+
conflicts: Object.freeze(regionResult.conflicts.map((c) =>
|
|
224
|
+
Object.freeze({ ...c, regionId }),
|
|
225
|
+
)),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Emit merged region bytes
|
|
230
|
+
output.push(regionResult.bytes);
|
|
231
|
+
cursor = currentRange.end;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Emit any trailing unmanaged bytes after the last region
|
|
235
|
+
if (cursor < current.length) {
|
|
236
|
+
output.push(current.subarray(cursor));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const bytes = Buffer.concat(output);
|
|
240
|
+
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
status: 'MERGEABLE',
|
|
243
|
+
bytes,
|
|
244
|
+
conflicts: Object.freeze([]),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed region parser for structured merge drivers.
|
|
3
|
+
*
|
|
4
|
+
* Parses managed-region declarations (start/end marker pairs) from raw
|
|
5
|
+
* byte content and returns a projection map of region id → byte range.
|
|
6
|
+
*
|
|
7
|
+
* Enforces:
|
|
8
|
+
* - No duplicate start or end markers for the same region id.
|
|
9
|
+
* - No nested or overlapping regions.
|
|
10
|
+
* - Every declared region must have both start and end markers present.
|
|
11
|
+
*
|
|
12
|
+
* All operations are byte-level; no full-file parse or stringify.
|
|
13
|
+
*
|
|
14
|
+
* @module artifacts/merge/regions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ReleaseError, STRUCTURE_INVALID } from '../../core/errors.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {{ start: number, end: number }} ByteRange
|
|
21
|
+
* start is inclusive byte offset of the start marker; end is the exclusive
|
|
22
|
+
* byte offset past the end marker (i.e., end marker last byte + 1).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse managed-region declarations from raw bytes.
|
|
27
|
+
*
|
|
28
|
+
* Returns a Map of region id → { start, end } byte ranges.
|
|
29
|
+
*
|
|
30
|
+
* @param {Buffer} bytes - Raw file content.
|
|
31
|
+
* @param {Array<{ id: string, start: string, end: string }>} declarations
|
|
32
|
+
* @returns {Map<string, ByteRange>}
|
|
33
|
+
* @throws {ReleaseError} with code STRUCTURE_INVALID on duplicate/nested/missing markers.
|
|
34
|
+
*/
|
|
35
|
+
export function parseManagedRegions(bytes, declarations) {
|
|
36
|
+
const result = new Map();
|
|
37
|
+
|
|
38
|
+
// Reject empty marker strings upfront to prevent infinite loops
|
|
39
|
+
for (const decl of declarations) {
|
|
40
|
+
if (!decl.start || !decl.end) {
|
|
41
|
+
throw new ReleaseError(
|
|
42
|
+
STRUCTURE_INVALID,
|
|
43
|
+
`managed region '${decl.id}' has empty start or end marker`,
|
|
44
|
+
{ regionId: decl.id },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Reject duplicate declaration ids
|
|
50
|
+
const seenIds = new Set();
|
|
51
|
+
for (const decl of declarations) {
|
|
52
|
+
if (seenIds.has(decl.id)) {
|
|
53
|
+
throw new ReleaseError(
|
|
54
|
+
STRUCTURE_INVALID,
|
|
55
|
+
`duplicate declaration id '${decl.id}' in managed region declarations`,
|
|
56
|
+
{ regionId: decl.id },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
seenIds.add(decl.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {Array<{ id: string, kind: 'start'|'end', offset: number }>} */
|
|
63
|
+
const markers = [];
|
|
64
|
+
|
|
65
|
+
// Collect all marker occurrences
|
|
66
|
+
for (const decl of declarations) {
|
|
67
|
+
let offset = 0;
|
|
68
|
+
while (offset <= bytes.length) {
|
|
69
|
+
const idx = bytes.indexOf(decl.start, offset, 'utf8');
|
|
70
|
+
if (idx < 0) break;
|
|
71
|
+
markers.push({ id: decl.id, kind: 'start', offset: idx });
|
|
72
|
+
offset = idx + Buffer.byteLength(decl.start, 'utf8');
|
|
73
|
+
// Guard: if marker is empty this would be an infinite loop (handled above)
|
|
74
|
+
}
|
|
75
|
+
offset = 0;
|
|
76
|
+
while (offset <= bytes.length) {
|
|
77
|
+
const idx = bytes.indexOf(decl.end, offset, 'utf8');
|
|
78
|
+
if (idx < 0) break;
|
|
79
|
+
markers.push({ id: decl.id, kind: 'end', offset: idx });
|
|
80
|
+
offset = idx + Buffer.byteLength(decl.end, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reject declared regions whose markers are completely absent from content
|
|
85
|
+
for (const decl of declarations) {
|
|
86
|
+
const hasStart = markers.some((m) => m.id === decl.id && m.kind === 'start');
|
|
87
|
+
const hasEnd = markers.some((m) => m.id === decl.id && m.kind === 'end');
|
|
88
|
+
if (!hasStart && !hasEnd) {
|
|
89
|
+
throw new ReleaseError(
|
|
90
|
+
STRUCTURE_INVALID,
|
|
91
|
+
`managed region '${decl.id}' markers not found in content`,
|
|
92
|
+
{ regionId: decl.id },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Sort by offset, then 'start' before 'end' at same offset
|
|
98
|
+
markers.sort((a, b) => a.offset - b.offset || (a.kind === 'start' ? -1 : 1));
|
|
99
|
+
|
|
100
|
+
// Validate and extract ranges
|
|
101
|
+
/** @type {Map<string, { startOffset: number }>} */
|
|
102
|
+
const openRegions = new Map();
|
|
103
|
+
|
|
104
|
+
for (const marker of markers) {
|
|
105
|
+
if (marker.kind === 'start') {
|
|
106
|
+
if (openRegions.has(marker.id)) {
|
|
107
|
+
throw new ReleaseError(
|
|
108
|
+
STRUCTURE_INVALID,
|
|
109
|
+
`duplicate start marker for managed region '${marker.id}'`,
|
|
110
|
+
{ regionId: marker.id, offset: marker.offset },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
// Check for nesting: any currently-open region with a different id
|
|
114
|
+
// whose start is after this offset indicates overlap
|
|
115
|
+
for (const [openId, openInfo] of openRegions) {
|
|
116
|
+
if (openId !== marker.id && openInfo.startOffset < marker.offset) {
|
|
117
|
+
// There's an open region that started before this one — overlapping
|
|
118
|
+
throw new ReleaseError(
|
|
119
|
+
STRUCTURE_INVALID,
|
|
120
|
+
`managed region '${marker.id}' overlaps with open region '${openId}'`,
|
|
121
|
+
{ regionId: marker.id, overlappingWith: openId },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
openRegions.set(marker.id, { startOffset: marker.offset });
|
|
126
|
+
} else {
|
|
127
|
+
// end marker
|
|
128
|
+
if (!openRegions.has(marker.id)) {
|
|
129
|
+
throw new ReleaseError(
|
|
130
|
+
STRUCTURE_INVALID,
|
|
131
|
+
`end marker for managed region '${marker.id}' found without matching start`,
|
|
132
|
+
{ regionId: marker.id, offset: marker.offset },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const openInfo = openRegions.get(marker.id);
|
|
136
|
+
const decl = declarations.find((d) => d.id === marker.id);
|
|
137
|
+
const endMarkerLen = Buffer.byteLength(decl.end, 'utf8');
|
|
138
|
+
result.set(marker.id, {
|
|
139
|
+
start: openInfo.startOffset,
|
|
140
|
+
end: marker.offset + endMarkerLen,
|
|
141
|
+
});
|
|
142
|
+
openRegions.delete(marker.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Any remaining open regions are missing their end markers
|
|
147
|
+
for (const [id] of openRegions) {
|
|
148
|
+
throw new ReleaseError(
|
|
149
|
+
STRUCTURE_INVALID,
|
|
150
|
+
`managed region '${id}' has start marker but no end marker`,
|
|
151
|
+
{ regionId: id },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|