@astrosheep/keiyaku 2.9.8 → 2.9.10
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/README.md +50 -182
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +10 -7
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +242 -79
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +35 -337
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +125 -127
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
package/build/core/draft.js
CHANGED
|
@@ -3,8 +3,9 @@ import { parseToAST } from "./markdown/parser.js";
|
|
|
3
3
|
import { extractListItems, renderSectionContent } from "./markdown/render.js";
|
|
4
4
|
import { normalizeSectionTitle } from "./markdown/titles.js";
|
|
5
5
|
import { markdownSourceSections, renderScopeFragment, validateScopeFragmentLines } from "./scope.js";
|
|
6
|
-
const KNOWN_SECTIONS = new Set(["objective", "scope", "
|
|
7
|
-
const
|
|
6
|
+
const KNOWN_SECTIONS = new Set(["context", "objective", "design", "scope", "criteria", "verification"]);
|
|
7
|
+
const RETIRED_OR_MALFORMED_CORE_SECTIONS = new Set(["title", "name", "checks"]);
|
|
8
|
+
const BIND_DRAFT_SECTION_ORDER = ["title", "context", "objective", "design", "scope", "criteria", "verification"];
|
|
8
9
|
function invalidBindDraft(message) {
|
|
9
10
|
throw new FlowError("INVALID_KEIYAKU_DRAFT", `INVALID_BIND_DRAFT: ${message}`);
|
|
10
11
|
}
|
|
@@ -50,26 +51,26 @@ function findUnexpectedListOnlyLine(section) {
|
|
|
50
51
|
}
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
|
-
function
|
|
54
|
+
function extractSingleLineDeclarations(section, heading) {
|
|
54
55
|
const unexpected = findUnexpectedListOnlyLine(section);
|
|
55
56
|
if (unexpected) {
|
|
56
|
-
invalidBindDraft(
|
|
57
|
+
invalidBindDraft(`${heading} must be a bullet list; found '${unexpected}'`);
|
|
57
58
|
}
|
|
58
59
|
const items = extractListItems(section);
|
|
59
60
|
if (items.length === 0) {
|
|
60
61
|
const hasListNode = section.children.some((child) => child.type === "list");
|
|
61
62
|
if (hasListNode) {
|
|
62
|
-
invalidBindDraft(
|
|
63
|
+
invalidBindDraft(`empty ${heading.toLowerCase()} declaration in bind draft`);
|
|
63
64
|
}
|
|
64
|
-
invalidBindDraft(
|
|
65
|
+
invalidBindDraft(`${heading} must be a bullet list; found empty ${heading} section`);
|
|
65
66
|
}
|
|
66
67
|
return items.map((item) => {
|
|
67
68
|
const trimmed = item.trim();
|
|
68
69
|
if (trimmed.length === 0) {
|
|
69
|
-
invalidBindDraft(
|
|
70
|
+
invalidBindDraft(`empty ${heading.toLowerCase()} declaration in bind draft`);
|
|
70
71
|
}
|
|
71
72
|
if (item.includes("\n") || item.includes("\r")) {
|
|
72
|
-
invalidBindDraft(
|
|
73
|
+
invalidBindDraft(`${heading} declarations must be a single line`);
|
|
73
74
|
}
|
|
74
75
|
return trimmed;
|
|
75
76
|
});
|
|
@@ -85,7 +86,7 @@ function collectBindDraftNodes(content) {
|
|
|
85
86
|
structuralContent = `${structuralContent.slice(0, source.contentStart)}\`\`\`\nscope\n\`\`\`\n${structuralContent.slice(source.end)}`;
|
|
86
87
|
}
|
|
87
88
|
const ast = parseToAST(structuralContent);
|
|
88
|
-
let
|
|
89
|
+
let title;
|
|
89
90
|
let seenH1 = false;
|
|
90
91
|
const sections = new Map();
|
|
91
92
|
const extensions = [];
|
|
@@ -97,12 +98,16 @@ function collectBindDraftNodes(content) {
|
|
|
97
98
|
invalidBindDraft("bind draft allows exactly one H1 heading");
|
|
98
99
|
}
|
|
99
100
|
seenH1 = true;
|
|
100
|
-
|
|
101
|
+
title = node.title.trim() || undefined;
|
|
101
102
|
continue;
|
|
102
103
|
}
|
|
103
104
|
if (node.level !== 2)
|
|
104
105
|
continue;
|
|
105
106
|
const normalized = normalizeSectionName(node.title);
|
|
107
|
+
if (RETIRED_OR_MALFORMED_CORE_SECTIONS.has(normalized)) {
|
|
108
|
+
const replacement = normalized === "checks" ? "Criteria and Verification" : "the H1 Title";
|
|
109
|
+
invalidBindDraft(`obsolete or malformed core section '${node.title}'; use ${replacement}`);
|
|
110
|
+
}
|
|
106
111
|
if (KNOWN_SECTIONS.has(normalized)) {
|
|
107
112
|
if (sections.has(normalized)) {
|
|
108
113
|
invalidBindDraft(`duplicate section '${node.title}' in bind draft`);
|
|
@@ -115,7 +120,7 @@ function collectBindDraftNodes(content) {
|
|
|
115
120
|
content: normalizeExtensionContent(renderSectionContent(node)),
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
|
-
return { ...(
|
|
123
|
+
return { ...(title === undefined ? {} : { title }), sections, extensions };
|
|
119
124
|
}
|
|
120
125
|
function parseScopeSection(content) {
|
|
121
126
|
const sourceSections = markdownSourceSections(content).filter((section) => section.level === 2 && normalizeSectionTitle(section.title) === "scope");
|
|
@@ -141,52 +146,68 @@ function parseScopeSection(content) {
|
|
|
141
146
|
export function parsePartialBindDraft(content) {
|
|
142
147
|
if (content.trim().length === 0)
|
|
143
148
|
return {};
|
|
144
|
-
const {
|
|
149
|
+
const { title, sections, extensions } = collectBindDraftNodes(content);
|
|
145
150
|
const partial = {};
|
|
146
|
-
if (
|
|
147
|
-
partial.
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
if (title !== undefined)
|
|
152
|
+
partial.title = title;
|
|
153
|
+
for (const sectionName of ["context", "objective", "design"]) {
|
|
154
|
+
const section = sections.get(sectionName);
|
|
155
|
+
if (!section)
|
|
156
|
+
continue;
|
|
157
|
+
const text = renderSectionContent(section).trim();
|
|
158
|
+
const heading = `${sectionName[0].toUpperCase()}${sectionName.slice(1)}`;
|
|
159
|
+
if (!text)
|
|
160
|
+
invalidBindDraft(`${heading} section cannot be empty`);
|
|
161
|
+
partial[sectionName] = text;
|
|
154
162
|
}
|
|
155
163
|
const scope = sections.get("scope");
|
|
156
164
|
if (scope) {
|
|
157
165
|
partial.scope = parseScopeSection(content);
|
|
158
166
|
}
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
partial.
|
|
167
|
+
const criteria = sections.get("criteria");
|
|
168
|
+
if (criteria)
|
|
169
|
+
partial.criteria = extractSingleLineDeclarations(criteria, "Criteria");
|
|
170
|
+
const verification = sections.get("verification");
|
|
171
|
+
if (verification)
|
|
172
|
+
partial.verification = extractSingleLineDeclarations(verification, "Verification");
|
|
162
173
|
if (extensions.length > 0)
|
|
163
174
|
partial.extensions = extensions;
|
|
164
175
|
return partial;
|
|
165
176
|
}
|
|
166
177
|
export function mergeBindDraft(base, override) {
|
|
167
178
|
return {
|
|
168
|
-
...(base.
|
|
179
|
+
...(base.title !== undefined ? { title: base.title } : {}),
|
|
180
|
+
...(base.context !== undefined ? { context: base.context } : {}),
|
|
169
181
|
...(base.objective !== undefined ? { objective: base.objective } : {}),
|
|
182
|
+
...(base.design !== undefined ? { design: base.design } : {}),
|
|
170
183
|
...(base.scope !== undefined ? { scope: base.scope } : {}),
|
|
171
|
-
...(base.
|
|
184
|
+
...(base.criteria !== undefined ? { criteria: base.criteria } : {}),
|
|
185
|
+
...(base.verification !== undefined ? { verification: base.verification } : {}),
|
|
172
186
|
...(base.extensions !== undefined ? { extensions: base.extensions } : {}),
|
|
173
|
-
...(override.
|
|
187
|
+
...(override.title !== undefined ? { title: override.title } : {}),
|
|
188
|
+
...(override.context !== undefined ? { context: override.context } : {}),
|
|
174
189
|
...(override.objective !== undefined ? { objective: override.objective } : {}),
|
|
190
|
+
...(override.design !== undefined ? { design: override.design } : {}),
|
|
175
191
|
...(override.scope !== undefined ? { scope: override.scope } : {}),
|
|
176
|
-
...(override.
|
|
192
|
+
...(override.criteria !== undefined ? { criteria: override.criteria } : {}),
|
|
193
|
+
...(override.verification !== undefined ? { verification: override.verification } : {}),
|
|
177
194
|
...(override.extensions !== undefined ? { extensions: override.extensions } : {}),
|
|
178
195
|
};
|
|
179
196
|
}
|
|
180
197
|
export function missingBindDraftSections(partial) {
|
|
181
198
|
const missing = [];
|
|
182
|
-
if (partial.
|
|
183
|
-
missing.push("
|
|
199
|
+
if (partial.title === undefined || partial.title.trim().length === 0)
|
|
200
|
+
missing.push("title");
|
|
201
|
+
if (partial.context === undefined || partial.context.trim().length === 0)
|
|
202
|
+
missing.push("context");
|
|
184
203
|
if (partial.objective === undefined || partial.objective.trim().length === 0)
|
|
185
204
|
missing.push("objective");
|
|
205
|
+
if (partial.design === undefined || partial.design.trim().length === 0)
|
|
206
|
+
missing.push("design");
|
|
186
207
|
if (partial.scope === undefined || partial.scope.length === 0)
|
|
187
208
|
missing.push("scope");
|
|
188
|
-
if (partial.
|
|
189
|
-
missing.push("
|
|
209
|
+
if (partial.criteria === undefined || partial.criteria.length === 0)
|
|
210
|
+
missing.push("criteria");
|
|
190
211
|
return missing;
|
|
191
212
|
}
|
|
192
213
|
/** Paste-ready partial markdown for only the missing required sections. */
|
|
@@ -195,19 +216,31 @@ export function renderMissingBindSectionsTemplate(missing) {
|
|
|
195
216
|
for (const section of BIND_DRAFT_SECTION_ORDER) {
|
|
196
217
|
if (!missing.includes(section))
|
|
197
218
|
continue;
|
|
198
|
-
if (section === "
|
|
199
|
-
lines.push("# <
|
|
219
|
+
if (section === "title") {
|
|
220
|
+
lines.push("# <title>");
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (section === "context") {
|
|
224
|
+
lines.push("## Context", "<established facts and constraints>");
|
|
200
225
|
continue;
|
|
201
226
|
}
|
|
202
227
|
if (section === "objective") {
|
|
203
228
|
lines.push("## Objective", "<what the commission should accomplish>");
|
|
204
229
|
continue;
|
|
205
230
|
}
|
|
231
|
+
if (section === "design") {
|
|
232
|
+
lines.push("## Design", "<settled owner, boundaries, flow, state, and failure behavior>");
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
206
235
|
if (section === "scope") {
|
|
207
236
|
lines.push("## Scope", "~~~", "<pattern>", "<another-pattern>", "~~~");
|
|
208
237
|
continue;
|
|
209
238
|
}
|
|
210
|
-
|
|
239
|
+
if (section === "criteria") {
|
|
240
|
+
lines.push("## Criteria", "- <declarative acceptance criterion>");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
lines.push("## Verification", "- <executable command or testimony declaration>");
|
|
211
244
|
}
|
|
212
245
|
return lines.join("\n");
|
|
213
246
|
}
|
|
@@ -226,46 +259,70 @@ export function materializeBindDraft(partial) {
|
|
|
226
259
|
invalidBindDraft(`missing ${describeMissingBindSections(missing)} in bind draft`);
|
|
227
260
|
}
|
|
228
261
|
return {
|
|
229
|
-
|
|
262
|
+
title: partial.title.trim(),
|
|
263
|
+
context: partial.context.trim(),
|
|
230
264
|
objective: partial.objective.trim(),
|
|
265
|
+
design: partial.design.trim(),
|
|
231
266
|
scope: partial.scope,
|
|
232
|
-
|
|
267
|
+
criteria: partial.criteria,
|
|
268
|
+
verification: partial.verification ?? [],
|
|
233
269
|
extensions: partial.extensions ?? [],
|
|
234
270
|
};
|
|
235
271
|
}
|
|
236
272
|
export function parseBindDraft(content) {
|
|
237
|
-
const {
|
|
238
|
-
if (!
|
|
239
|
-
invalidBindDraft("missing H1 in bind draft; expected '# <
|
|
273
|
+
const { title, sections, extensions } = collectBindDraftNodes(content);
|
|
274
|
+
if (!title) {
|
|
275
|
+
invalidBindDraft("missing H1 Title in bind draft; expected '# <title>' followed by Context, Objective, Design, Scope, and Criteria; Verification is optional");
|
|
240
276
|
}
|
|
277
|
+
const context = sections.get("context");
|
|
241
278
|
const objective = sections.get("objective");
|
|
279
|
+
const design = sections.get("design");
|
|
242
280
|
const scope = sections.get("scope");
|
|
243
|
-
const
|
|
281
|
+
const criteria = sections.get("criteria");
|
|
282
|
+
const verification = sections.get("verification");
|
|
283
|
+
if (!context)
|
|
284
|
+
invalidBindDraft("missing Context section in bind draft");
|
|
244
285
|
if (!objective)
|
|
245
286
|
invalidBindDraft("missing Objective section in bind draft");
|
|
287
|
+
if (!design)
|
|
288
|
+
invalidBindDraft("missing Design section in bind draft");
|
|
246
289
|
if (!scope)
|
|
247
290
|
invalidBindDraft("missing Scope section in bind draft");
|
|
248
291
|
const parsedScope = parseScopeSection(content);
|
|
249
|
-
if (!
|
|
250
|
-
invalidBindDraft("missing
|
|
292
|
+
if (!criteria)
|
|
293
|
+
invalidBindDraft("missing Criteria section in bind draft");
|
|
294
|
+
const contextText = renderSectionContent(context).trim();
|
|
251
295
|
const objectiveText = renderSectionContent(objective).trim();
|
|
296
|
+
const designText = renderSectionContent(design).trim();
|
|
297
|
+
if (!contextText)
|
|
298
|
+
invalidBindDraft("Context section cannot be empty");
|
|
252
299
|
if (!objectiveText)
|
|
253
300
|
invalidBindDraft("Objective section cannot be empty");
|
|
301
|
+
if (!designText)
|
|
302
|
+
invalidBindDraft("Design section cannot be empty");
|
|
254
303
|
return {
|
|
255
|
-
|
|
304
|
+
title,
|
|
305
|
+
context: contextText,
|
|
256
306
|
objective: objectiveText,
|
|
307
|
+
design: designText,
|
|
257
308
|
scope: parsedScope,
|
|
258
|
-
|
|
309
|
+
criteria: extractSingleLineDeclarations(criteria, "Criteria"),
|
|
310
|
+
verification: verification ? extractSingleLineDeclarations(verification, "Verification") : [],
|
|
259
311
|
extensions,
|
|
260
312
|
};
|
|
261
313
|
}
|
|
262
314
|
export function renderBindDraft(draft) {
|
|
263
315
|
const sections = [
|
|
264
|
-
`# ${draft.
|
|
316
|
+
`# ${draft.title}`,
|
|
317
|
+
`## Context\n${draft.context}`,
|
|
265
318
|
`## Objective\n${draft.objective}`,
|
|
319
|
+
`## Design\n${draft.design}`,
|
|
266
320
|
`## Scope\n${renderScopeFragment(draft.scope)}`,
|
|
267
|
-
`##
|
|
321
|
+
`## Criteria\n${draft.criteria.map((criterion) => `- ${criterion}`).join("\n")}`,
|
|
268
322
|
];
|
|
323
|
+
if (draft.verification.length > 0) {
|
|
324
|
+
sections.push(`## Verification\n${draft.verification.map((declaration) => `- ${declaration}`).join("\n")}`);
|
|
325
|
+
}
|
|
269
326
|
for (const extension of draft.extensions) {
|
|
270
327
|
const content = normalizeExtensionContent(extension.content);
|
|
271
328
|
if (content.length > 0) {
|
|
@@ -279,21 +336,32 @@ export function renderBindDraft(draft) {
|
|
|
279
336
|
}
|
|
280
337
|
export function compileBindDraftFlags(input) {
|
|
281
338
|
for (const [flag, value] of [
|
|
282
|
-
["--
|
|
339
|
+
["--title", input.title],
|
|
340
|
+
["--context", input.context],
|
|
283
341
|
["--objective", input.objective],
|
|
342
|
+
["--design", input.design],
|
|
284
343
|
["--scope", input.scope],
|
|
285
|
-
["--
|
|
344
|
+
["--criteria", input.criteria],
|
|
345
|
+
["--verification", input.verification],
|
|
286
346
|
]) {
|
|
287
|
-
if (value.includes("\n") || value.includes("\r")) {
|
|
347
|
+
if (value !== undefined && (value.includes("\n") || value.includes("\r"))) {
|
|
288
348
|
throw new FlowError("EMPTY_PARAM", `${flag} must be a single line`);
|
|
289
349
|
}
|
|
290
350
|
}
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
351
|
+
const sections = [];
|
|
352
|
+
if (input.title !== undefined)
|
|
353
|
+
sections.push(`# ${input.title}`);
|
|
354
|
+
if (input.context !== undefined)
|
|
355
|
+
sections.push(`## Context\n${input.context}`);
|
|
356
|
+
if (input.objective !== undefined)
|
|
357
|
+
sections.push(`## Objective\n${input.objective}`);
|
|
358
|
+
if (input.design !== undefined)
|
|
359
|
+
sections.push(`## Design\n${input.design}`);
|
|
360
|
+
if (input.scope !== undefined)
|
|
361
|
+
sections.push(`## Scope\n~~~\n${input.scope}\n~~~`);
|
|
362
|
+
if (input.criteria !== undefined)
|
|
363
|
+
sections.push(`## Criteria\n- ${input.criteria}`);
|
|
364
|
+
if (input.verification !== undefined)
|
|
365
|
+
sections.push(`## Verification\n- ${input.verification}`);
|
|
366
|
+
return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
|
|
299
367
|
}
|
package/build/core/forfeit.js
CHANGED
|
@@ -1,67 +1,123 @@
|
|
|
1
1
|
import { FlowError } from "../flow-error.js";
|
|
2
2
|
import { assertCleanTrackedWorkingTree } from "../git/worktree.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
async function
|
|
9
|
-
const stableRoot = await stableRepoRoot(cwd);
|
|
10
|
-
await releaseScope(stableRoot, { contractId, actor, at });
|
|
11
|
-
return await cleanupContractWorkspace(stableRoot, contractId);
|
|
12
|
-
}
|
|
13
|
-
export async function forfeitContract(input, deps = {}) {
|
|
3
|
+
import { runLifecycle } from "./lifecycle-runner.js";
|
|
4
|
+
import { currentLifecycleState } from "./repository-ledger/read-model.js";
|
|
5
|
+
import { isTerminalState } from "./status/lifecycle.js";
|
|
6
|
+
import { terminalCleanupFacts } from "./worktree-path.js";
|
|
7
|
+
import { repositoryDeliveryRef } from "./repository-ledger/codec.js";
|
|
8
|
+
export async function forfeitContract(input) {
|
|
14
9
|
const at = new Date(input.nowMs).toISOString();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const ledger = await readLedger(stableRoot, input.contractId);
|
|
19
|
-
if (ledger) {
|
|
20
|
-
const state = deriveContractState(ledger.entries);
|
|
21
|
-
if (state.state === "forfeited") {
|
|
22
|
-
await reconcileTaskForfeit({
|
|
23
|
-
cwd: stableRoot,
|
|
24
|
-
contractId: input.contractId,
|
|
25
|
-
publishLedger: async () => undefined,
|
|
26
|
-
});
|
|
27
|
-
const cleanup = await (deps.cleanupForfeit ?? cleanupForfeit)(stableRoot, input.contractId, input.actor, at);
|
|
28
|
-
return { contractId: input.contractId, cleanup };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (!ledger) {
|
|
32
|
-
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
33
|
-
}
|
|
34
|
-
const state = deriveContractState(ledger.entries);
|
|
35
|
-
if (state.state === "corrupt" || isTerminalState(state)) {
|
|
36
|
-
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} cannot forfeit from state ${state.state}`);
|
|
37
|
-
}
|
|
38
|
-
await reconcileTaskForfeit({
|
|
39
|
-
cwd: stableRoot,
|
|
10
|
+
const requestedNote = input.note?.trim() || undefined;
|
|
11
|
+
const execution = await runLifecycle({
|
|
12
|
+
cwd: input.cwd,
|
|
40
13
|
contractId: input.contractId,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
14
|
+
mutation: {
|
|
15
|
+
verb: "forfeit",
|
|
16
|
+
},
|
|
17
|
+
signal: input.signal,
|
|
18
|
+
...(input.terminalWorkspaceCleanup ? { terminalWorkspaceCleanup: input.terminalWorkspaceCleanup } : {}),
|
|
19
|
+
admit: async (session, replay) => {
|
|
20
|
+
const state = currentLifecycleState(session.lifecycle);
|
|
21
|
+
if (state.state !== "forfeited" && isTerminalState(state))
|
|
22
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} cannot forfeit from state ${state.state}`);
|
|
23
|
+
if (state.state === "forfeited") {
|
|
24
|
+
const transaction = replay.state === "verb-committed" ? replay.transaction : undefined;
|
|
25
|
+
const existing = transaction?.entries.at(-1);
|
|
26
|
+
if (!existing || existing.kind !== "forfeit")
|
|
27
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} forfeited without an accepted forfeit transaction`);
|
|
28
|
+
return {
|
|
29
|
+
state,
|
|
30
|
+
delivery: { status: "absent" },
|
|
31
|
+
existing: { transactionObjectId: transaction.transactionObjectId, entry: existing },
|
|
32
|
+
...(session.target.status === "available" ? { targetRoot: session.target.root } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (session.target.status === "available")
|
|
36
|
+
await assertCleanTrackedWorkingTree(session.target.root);
|
|
37
|
+
if (session.lifecycle.petition.state === "awaiting-verdict") {
|
|
38
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has a pending settlement`);
|
|
39
|
+
}
|
|
40
|
+
if (session.lifecycle.petition.state === "approved" && session.lifecycle.petition.intent !== "forfeit") {
|
|
41
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has an approved non-forfeit settlement`);
|
|
42
|
+
}
|
|
43
|
+
if (!session.pins.delivery.head)
|
|
44
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} delivery ref is absent before forfeit`);
|
|
45
|
+
return {
|
|
46
|
+
state,
|
|
47
|
+
delivery: { status: "present", head: session.pins.delivery.head },
|
|
48
|
+
...(session.target.status === "available" ? { targetRoot: session.target.root } : {}),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
prepare: (admission) => ({
|
|
52
|
+
...(admission.business.existing ? { existing: admission.business.existing } : {}),
|
|
53
|
+
entry: admission.business.existing?.entry ?? {
|
|
54
|
+
v: 2,
|
|
55
|
+
kind: "forfeit",
|
|
56
|
+
contract: input.contractId,
|
|
57
|
+
at,
|
|
58
|
+
actor: input.actor,
|
|
59
|
+
data: {
|
|
60
|
+
reason: input.reason,
|
|
48
61
|
actor: input.actor,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
finalHead: admission.business.delivery.status === "present"
|
|
63
|
+
? admission.business.delivery.head
|
|
64
|
+
: admission.business.existing.entry.data.finalHead,
|
|
65
|
+
...(requestedNote ? { note: requestedNote } : {}),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
revalidate: async ({ session, admission, prepared }) => {
|
|
70
|
+
const state = currentLifecycleState(session.lifecycle);
|
|
71
|
+
if (prepared.existing) {
|
|
72
|
+
return session.repository.topology.nodes.some((node) => node.objectId === prepared.existing.transactionObjectId)
|
|
73
|
+
? undefined
|
|
74
|
+
: { code: "FORFEIT_STATE_CHANGED", message: "accepted forfeit entry changed during replay preparation" };
|
|
75
|
+
}
|
|
76
|
+
if (session.target.status === "available")
|
|
77
|
+
await assertCleanTrackedWorkingTree(session.target.root);
|
|
78
|
+
if (admission.business.delivery.status !== "present" || session.pins.delivery.head !== admission.business.delivery.head) {
|
|
79
|
+
return { code: "FORFEIT_DELIVERY_CHANGED", message: "contract delivery changed during forfeit preparation" };
|
|
80
|
+
}
|
|
81
|
+
return state.state === admission.business.state.state
|
|
82
|
+
? undefined
|
|
83
|
+
: { code: "FORFEIT_STATE_CHANGED", message: "contract lifecycle changed during forfeit preparation" };
|
|
84
|
+
},
|
|
85
|
+
effect: async ({ context, admission, prepared }) => {
|
|
86
|
+
const publication = prepared.existing
|
|
87
|
+
? await context.replayCommitted({ transactionObjectId: prepared.existing.transactionObjectId, carrier: "retire" })
|
|
88
|
+
: await context.publishAuthoritative({
|
|
89
|
+
verb: "forfeit",
|
|
90
|
+
admittedLedgerHead: admission.ledgerHead,
|
|
91
|
+
acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
|
|
92
|
+
entries: [prepared.entry],
|
|
93
|
+
delivery: {
|
|
94
|
+
ref: repositoryDeliveryRef(input.contractId),
|
|
95
|
+
expectedOid: admission.business.delivery.status === "present" ? admission.business.delivery.head : null,
|
|
53
96
|
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
97
|
+
});
|
|
98
|
+
if (publication.status === "indeterminate") {
|
|
99
|
+
return { kind: "precommit-effects-only", authoritativeHead: publication.currentHead ?? admission.ledgerHead };
|
|
100
|
+
}
|
|
101
|
+
if (publication.status !== "committed") {
|
|
102
|
+
return { kind: "refused-before-effects", authoritativeHead: publication.currentHead ?? admission.ledgerHead, refusal: { code: "LEDGER_CAS_CONFLICT", message: `contract ${input.contractId} changed during forfeit` } };
|
|
103
|
+
}
|
|
104
|
+
const published = publication;
|
|
105
|
+
await context.session.completeReplay("retire");
|
|
106
|
+
const cleanupRow = context.journal.all().find((row) => row.effect === "worktree-projection" && row.status === "succeeded");
|
|
107
|
+
const cleanupFacts = cleanupRow?.status === "succeeded" ? cleanupRow.receipt?.facts : undefined;
|
|
108
|
+
const deliveryRefRow = context.journal.all().find((row) => row.effect === "delivery-ref-projection" && row.status === "succeeded");
|
|
109
|
+
const deliveryRefFacts = deliveryRefRow?.status === "succeeded" ? deliveryRefRow.receipt?.facts : undefined;
|
|
110
|
+
const cleanup = terminalCleanupFacts(cleanupFacts, deliveryRefFacts);
|
|
111
|
+
if (!cleanup)
|
|
112
|
+
throw new FlowError("INTERNAL_STATE", "terminal replay completed without typed cleanup facts");
|
|
113
|
+
return { kind: "committed", commit: published, entryKind: "forfeit", value: { cleanup } };
|
|
60
114
|
},
|
|
61
115
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
116
|
+
if (execution.kind === "noncommitted")
|
|
117
|
+
return execution.outcome;
|
|
118
|
+
if (execution.kind === "contained")
|
|
119
|
+
return { ...execution.outcome, contractId: input.contractId };
|
|
120
|
+
return { ...execution.outcome, contractId: input.contractId, cleanup: execution.payload.cleanup, carrier: execution.carrier };
|
|
65
121
|
}
|
|
66
122
|
export async function forfeitContractCommand(input) {
|
|
67
123
|
return await forfeitContract({
|
|
@@ -71,5 +127,6 @@ export async function forfeitContractCommand(input) {
|
|
|
71
127
|
nowMs: input.nowMs,
|
|
72
128
|
reason: "manual",
|
|
73
129
|
note: input.note,
|
|
130
|
+
signal: input.signal,
|
|
74
131
|
});
|
|
75
132
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
const SLUG_CHARACTER = /[\p{Letter}\p{Number}\p{Mark}]/u;
|
|
2
3
|
const SLUG_LETTER_OR_NUMBER = /[\p{Letter}\p{Number}]/u;
|
|
3
4
|
const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
|
|
@@ -6,8 +7,8 @@ const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
|
|
|
6
7
|
* collision ordinals, extensions, and temporary-write bookkeeping.
|
|
7
8
|
*/
|
|
8
9
|
export const IDENTIFIER_SLUG_MAX_UTF8_BYTES = 120;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export const TASK_IDENTIFIER_MAX_UTF8_BYTES = 48;
|
|
11
|
+
const TASK_IDENTIFIER_DIGEST_HEX_LENGTH = 8;
|
|
11
12
|
export class IdentifierSlugError extends Error {
|
|
12
13
|
code = "INVALID_IDENTIFIER_SLUG";
|
|
13
14
|
constructor(message, options) {
|
|
@@ -18,23 +19,14 @@ export class IdentifierSlugError extends Error {
|
|
|
18
19
|
function utf8Bytes(value) {
|
|
19
20
|
return Buffer.byteLength(value, "utf8");
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
* Normalize human text into one Unicode-safe, portable identifier slug.
|
|
23
|
-
* NFKC makes canonically and compatibly equivalent input deterministic. All
|
|
24
|
-
* non-letter/number/mark runs become one ASCII hyphen, then the result is
|
|
25
|
-
* truncated at a Unicode code-point boundary to the shared UTF-8 byte bound.
|
|
26
|
-
*/
|
|
27
|
-
export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
|
|
28
|
-
if (!Number.isSafeInteger(maxUtf8Bytes) || maxUtf8Bytes < 1 || maxUtf8Bytes > IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
|
|
29
|
-
throw new IdentifierSlugError(`slug byte bound must be from 1 through ${IDENTIFIER_SLUG_MAX_UTF8_BYTES}`);
|
|
30
|
-
}
|
|
22
|
+
function normalizedIdentifierSlug(value, maxUtf8Bytes) {
|
|
31
23
|
let slug = "";
|
|
32
24
|
let pendingSeparator = false;
|
|
33
25
|
for (const character of value.normalize("NFKC").toLowerCase()) {
|
|
34
26
|
if (SLUG_CHARACTER.test(character)) {
|
|
35
27
|
const addition = `${pendingSeparator && slug ? "-" : ""}${character}`;
|
|
36
28
|
if (utf8Bytes(slug) + utf8Bytes(addition) > maxUtf8Bytes)
|
|
37
|
-
|
|
29
|
+
return { slug, truncated: true };
|
|
38
30
|
slug += addition;
|
|
39
31
|
pendingSeparator = false;
|
|
40
32
|
}
|
|
@@ -42,6 +34,18 @@ export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MA
|
|
|
42
34
|
pendingSeparator = true;
|
|
43
35
|
}
|
|
44
36
|
}
|
|
37
|
+
return { slug, truncated: false };
|
|
38
|
+
}
|
|
39
|
+
function truncateNormalizedIdentifierSlug(value, maxUtf8Bytes) {
|
|
40
|
+
let prefix = "";
|
|
41
|
+
for (const character of value) {
|
|
42
|
+
if (utf8Bytes(prefix) + utf8Bytes(character) > maxUtf8Bytes)
|
|
43
|
+
break;
|
|
44
|
+
prefix += character;
|
|
45
|
+
}
|
|
46
|
+
return prefix.replace(/-+$/u, "");
|
|
47
|
+
}
|
|
48
|
+
function validatedIdentifierSlug(slug, maxUtf8Bytes) {
|
|
45
49
|
if (!slug || !SLUG_LETTER_OR_NUMBER.test(slug)) {
|
|
46
50
|
throw new IdentifierSlugError("text cannot be converted to a portable identifier slug; use at least one Unicode letter or number");
|
|
47
51
|
}
|
|
@@ -54,6 +58,52 @@ export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MA
|
|
|
54
58
|
}
|
|
55
59
|
return slug;
|
|
56
60
|
}
|
|
61
|
+
function completeNormalizedIdentifierSlug(value) {
|
|
62
|
+
let slug = "";
|
|
63
|
+
let pendingSeparator = false;
|
|
64
|
+
for (const character of value.normalize("NFKC").toLowerCase()) {
|
|
65
|
+
if (SLUG_CHARACTER.test(character)) {
|
|
66
|
+
slug += `${pendingSeparator && slug ? "-" : ""}${character}`;
|
|
67
|
+
pendingSeparator = false;
|
|
68
|
+
}
|
|
69
|
+
else if (slug) {
|
|
70
|
+
pendingSeparator = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return validatedIdentifierSlug(slug, Number.MAX_SAFE_INTEGER);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Normalize human text into one Unicode-safe, portable identifier slug.
|
|
77
|
+
* NFKC makes canonically and compatibly equivalent input deterministic. All
|
|
78
|
+
* non-letter/number/mark runs become one ASCII hyphen, then the result is
|
|
79
|
+
* truncated at a Unicode code-point boundary to the shared UTF-8 byte bound.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
|
|
82
|
+
if (!Number.isSafeInteger(maxUtf8Bytes) || maxUtf8Bytes < 1 || maxUtf8Bytes > IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
|
|
83
|
+
throw new IdentifierSlugError(`slug byte bound must be from 1 through ${IDENTIFIER_SLUG_MAX_UTF8_BYTES}`);
|
|
84
|
+
}
|
|
85
|
+
return validatedIdentifierSlug(normalizedIdentifierSlug(value, maxUtf8Bytes).slug, maxUtf8Bytes);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Allocate a newly generated task ID. Persisted task documents remain
|
|
89
|
+
* addressable at their original ID; this limit applies only to new IDs.
|
|
90
|
+
*/
|
|
91
|
+
export function taskIdentifierSlug(value, collisionSuffix = "") {
|
|
92
|
+
const suffixBytes = utf8Bytes(collisionSuffix);
|
|
93
|
+
const availableBytes = TASK_IDENTIFIER_MAX_UTF8_BYTES - suffixBytes;
|
|
94
|
+
if (availableBytes < TASK_IDENTIFIER_DIGEST_HEX_LENGTH + 2) {
|
|
95
|
+
throw new IdentifierSlugError("task collision suffix leaves no room for a bounded identifier");
|
|
96
|
+
}
|
|
97
|
+
const normalized = completeNormalizedIdentifierSlug(value);
|
|
98
|
+
if (utf8Bytes(normalized) <= TASK_IDENTIFIER_MAX_UTF8_BYTES) {
|
|
99
|
+
return `${truncateNormalizedIdentifierSlug(normalized, availableBytes)}${collisionSuffix}`;
|
|
100
|
+
}
|
|
101
|
+
const prefixBytes = availableBytes - TASK_IDENTIFIER_DIGEST_HEX_LENGTH - 1;
|
|
102
|
+
const prefix = truncateNormalizedIdentifierSlug(normalized, prefixBytes);
|
|
103
|
+
const digest = crypto.createHash("sha256").update(normalized, "utf8").digest("hex")
|
|
104
|
+
.slice(0, TASK_IDENTIFIER_DIGEST_HEX_LENGTH);
|
|
105
|
+
return `${prefix}-${digest}${collisionSuffix}`;
|
|
106
|
+
}
|
|
57
107
|
export function isNormalizedIdentifierSlug(value) {
|
|
58
108
|
if (!value || utf8Bytes(value) > IDENTIFIER_SLUG_MAX_UTF8_BYTES)
|
|
59
109
|
return false;
|
package/build/core/ids.js
CHANGED
|
@@ -16,13 +16,13 @@ export class ContractIdError extends Error {
|
|
|
16
16
|
this.name = "ContractIdError";
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
19
|
+
export function slugifyContractTitle(title) {
|
|
20
20
|
try {
|
|
21
|
-
return normalizeIdentifierSlug(
|
|
21
|
+
return normalizeIdentifierSlug(title);
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
24
24
|
if (error instanceof IdentifierSlugError) {
|
|
25
|
-
throw new ContractIdError(`contract
|
|
25
|
+
throw new ContractIdError(`contract title ${error.message}`, { cause: error });
|
|
26
26
|
}
|
|
27
27
|
throw error;
|
|
28
28
|
}
|
|
@@ -77,7 +77,7 @@ export function createUlid(nowMs, randomBytes) {
|
|
|
77
77
|
return ulid;
|
|
78
78
|
}
|
|
79
79
|
export function buildContractId(input) {
|
|
80
|
-
return `${
|
|
80
|
+
return `${slugifyContractTitle(input.title)}.${createUlid(input.nowMs, input.randomBytes)}`;
|
|
81
81
|
}
|
|
82
82
|
export async function assertValidContractId(cwd, id) {
|
|
83
83
|
const git = createGit(cwd);
|