Add mkcapsule --resolve: build-time claim registry for capsule block collisions (FABRIC-3.md §XXIV)
Traced what a "Block NNNN" collision actually means before designing a fix for it: capsule_loader.c's block-write path routes through the generic block-subsystem API, which kernel_main.c registers as two devices in a fixed order -- the volatile ramdrive first (LBN 2048-3071), then Artemis's real virtio-blk device immediately after (LBN 3072+, backed by disk/artemis.img). Every capsule this project has lands in Artemis's persistent range, not the ramdrive, and blk_update()'s dirty-marking + repl.c's idle-loop flush write that content through to the real disk file on every boot. A block-number collision is therefore a silent, persistent overwrite of real disk content surviving reboots, not a transient RAM mixup. The existing collision gate (check_block_conflicts(), already a hard non-interactive build failure) already catches capsule-vs-capsule collisions across the whole flat range. The real gap: zero visibility into blocks something other than a capsule owns (Artemis's own non-capsule persistent data), and no device-boundary/capacity awareness at all. Added, scoped step by step before writing any code: - tools/capsule-claims.txt -- derived, auto-created/regenerated, git-ignored. Lets --resolve tell "this capsule's own content changed" apart from "genuinely new collision with something else." - tools/capsule-reserved.txt -- human-authored, git-tracked, seeded with nothing yet rather than guessed at. Checked by both the plain build gate (new check_reserved_conflicts()) and --resolve. - tools/patches/ -- git-tracked, one file per accepted interactive renumber; a structured old->new block list, not a generic diff, since that's the only thing a renumber ever changes. - mkcapsule --resolve <dir> -- the only interactive mkcapsule mode, a deliberate separate invocation from the plain build path (which stays non-interactive so CI never blocks on a prompt). Suggests a renumbering that preserves a capsule's own existing block spacing, prompts y/N, rewrites the .4th source in place on acceptance. Found and fixed a real bug during verification: the registry's empty-block-list case (workload-5.4th, zero Block headers) serialized with a stray trailing space that the reader parsed back as a phantom block 0, causing spurious re-registration every run -- caught by testing idempotency directly, not assuming a clean first run meant it worked. Verified: isolated collision tests confirm both accept and reject paths, confirm a resolved collision doesn't re-prompt the other side, confirm reserved-range collisions are caught by both --resolve and the plain build gate. Full 3-architecture rebuild via the real Makefile.starkernel succeeded clean; amd64 boots with an unchanged dict_hash/capsule_hash from every prior boot this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c8ba8832c4
commit
cd2fda4351
+478
-1
@@ -11,6 +11,18 @@
|
||||
mkcapsule --lint <capsules_dir> lint all .4th files
|
||||
mkcapsule --manifest <capsules_dir> print block manifest to stdout
|
||||
mkcapsule --manifest <capsules_dir> <out> write block manifest to file
|
||||
mkcapsule --resolve <capsules_dir> interactive block-conflict
|
||||
resolver (FABRIC-3.md SXXIV)
|
||||
|
||||
--resolve is the only interactive mode -- never part of the plain build
|
||||
path above, which stays non-interactive so CI/unattended builds never
|
||||
block on a prompt. It maintains tools/capsule-claims.txt (auto-created,
|
||||
derived, git-ignored) and checks against tools/capsule-reserved.txt
|
||||
(human-authored, git-tracked, blocks something other than a capsule
|
||||
owns -- e.g. Artemis's own non-capsule persistent data). On a genuine
|
||||
conflict it suggests a renumbering that preserves the capsule's own
|
||||
existing block spacing, prompts y/N, and on acceptance rewrites the
|
||||
.4th source in place and records the decision in tools/patches/.
|
||||
|
||||
Build mode generates a C source file containing:
|
||||
- capsule_arena[] (all payload bytes concatenated)
|
||||
@@ -40,6 +52,7 @@
|
||||
#include <ftw.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "pkcs8_ed25519.h"
|
||||
#include "starkernel/ed25519.h"
|
||||
@@ -887,6 +900,456 @@ static int check_block_conflicts(void) {
|
||||
return conflicts > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Reserved Ranges (FABRIC-3.md SXXIV, 2026-09-12)
|
||||
*
|
||||
* capsule-reserved.txt is human-authored and git-tracked -- mkcapsule can
|
||||
* discover collisions between capsules on its own (they're all .4th files
|
||||
* it scans), but it has zero visibility into blocks something OTHER than a
|
||||
* capsule owns (Artemis's own non-capsule identity/home-blocks data, e.g.)
|
||||
* unless a human tells it. Never auto-created, never auto-modified: a
|
||||
* missing file just means "nothing reserved yet," not an error.
|
||||
*===========================================================================*/
|
||||
#define RESERVED_FILE_PATH "tools/capsule-reserved.txt"
|
||||
#define CLAIMS_FILE_PATH "tools/capsule-claims.txt"
|
||||
#define PATCHES_DIR_PATH "tools/patches"
|
||||
#define MAX_RESERVED_RANGES 256
|
||||
|
||||
typedef struct {
|
||||
int start;
|
||||
int end; /* inclusive */
|
||||
char desc[256];
|
||||
} ReservedRange;
|
||||
|
||||
static ReservedRange reserved_ranges[MAX_RESERVED_RANGES];
|
||||
static int reserved_count = 0;
|
||||
|
||||
/* load_reserved_ranges: parse "START-END description" lines, '#'-comments
|
||||
* and blank lines ignored. Missing file -> reserved_count stays 0, silently
|
||||
* (this file is optional until a human seeds it). */
|
||||
static void load_reserved_ranges(const char *path) {
|
||||
reserved_count = 0;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return;
|
||||
|
||||
char line[512];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *p = line;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p == '#' || *p == '\n' || *p == '\0') continue;
|
||||
|
||||
int start = 0, end = 0;
|
||||
int consumed = 0;
|
||||
if (sscanf(p, "%d-%d%n", &start, &end, &consumed) != 2) continue;
|
||||
if (reserved_count >= MAX_RESERVED_RANGES) break;
|
||||
|
||||
ReservedRange *r = &reserved_ranges[reserved_count];
|
||||
r->start = start;
|
||||
r->end = end;
|
||||
p += consumed;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
size_t dlen = strcspn(p, "\r\n");
|
||||
if (dlen >= sizeof(r->desc)) dlen = sizeof(r->desc) - 1;
|
||||
memcpy(r->desc, p, dlen);
|
||||
r->desc[dlen] = '\0';
|
||||
reserved_count++;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* reserved_owner_of: returns the reserved range covering lbn, or NULL. */
|
||||
static const ReservedRange *reserved_owner_of(int lbn) {
|
||||
for (int i = 0; i < reserved_count; i++) {
|
||||
if (lbn >= reserved_ranges[i].start && lbn <= reserved_ranges[i].end)
|
||||
return &reserved_ranges[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* check_reserved_conflicts: hard build-gate companion to
|
||||
* check_block_conflicts() -- same manifest_entries scan, checked against
|
||||
* capsule-reserved.txt instead of against each other. Call AFTER
|
||||
* check_block_conflicts() has already populated manifest_entries via its
|
||||
* own nftw scan (avoids scanning the directory twice). */
|
||||
static int check_reserved_conflicts(void) {
|
||||
load_reserved_ranges(RESERVED_FILE_PATH);
|
||||
if (reserved_count == 0) return 0;
|
||||
|
||||
int conflicts = 0;
|
||||
for (int i = 0; i < manifest_count; i++) {
|
||||
for (int j = 0; j < manifest_entries[i].block_count; j++) {
|
||||
int lbn = manifest_entries[i].blocks[j];
|
||||
const ReservedRange *r = reserved_owner_of(lbn);
|
||||
if (r) {
|
||||
fprintf(stderr,
|
||||
"mkcapsule: ERROR: block %d (%s) falls inside reserved "
|
||||
"range %d-%d: %s\n",
|
||||
lbn, manifest_entries[i].name, r->start, r->end, r->desc);
|
||||
conflicts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (conflicts > 0) {
|
||||
fprintf(stderr,
|
||||
"mkcapsule: ERROR: %d capsule block(s) collide with reserved "
|
||||
"ranges in %s\n", conflicts, RESERVED_FILE_PATH);
|
||||
}
|
||||
return conflicts > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Claim Registry + Interactive Resolve (mkcapsule --resolve)
|
||||
*
|
||||
* capsule-claims.txt is auto-created/regenerated -- pure derived state,
|
||||
* never hand-edited, git-ignored. Unlike the plain build gate (which just
|
||||
* re-scans and hard-fails every time, no memory needed), --resolve needs
|
||||
* to tell "this capsule's own content changed" apart from "a genuinely new
|
||||
* collision with something else," which needs the previous run's state.
|
||||
*===========================================================================*/
|
||||
typedef struct {
|
||||
char name[CAPSULE_NAME_MAX];
|
||||
uint64_t hash;
|
||||
int blocks[MAX_BLOCKS_PER_CAPSULE];
|
||||
int block_count;
|
||||
} ClaimEntry;
|
||||
|
||||
static ClaimEntry claim_registry[MAX_CAPSULES];
|
||||
static int claim_count = 0;
|
||||
|
||||
static ClaimEntry *find_claim(const char *name) {
|
||||
for (int i = 0; i < claim_count; i++) {
|
||||
if (strcmp(claim_registry[i].name, name) == 0) return &claim_registry[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* load_claims_registry: parse "<name> <hash-hex> <b1>,<b2>,..." lines.
|
||||
* Missing file -> claim_count stays 0 (fresh baseline, not an error --
|
||||
* this is the auto-create case). */
|
||||
static void load_claims_registry(const char *path) {
|
||||
claim_count = 0;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return;
|
||||
|
||||
char line[2048];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (line[0] == '#' || line[0] == '\n' || line[0] == '\0') continue;
|
||||
if (claim_count >= MAX_CAPSULES) break;
|
||||
|
||||
char name[CAPSULE_NAME_MAX];
|
||||
char hashbuf[32];
|
||||
char blocklist[1600] = "";
|
||||
int nf = sscanf(line, "%511s %31s %1599[^\n]", name, hashbuf, blocklist);
|
||||
if (nf < 2) continue; /* nf==3 has a block list; nf==2 is a valid
|
||||
* empty-block-list entry (raw code capsule,
|
||||
* no "Block N" headers at all) */
|
||||
|
||||
ClaimEntry *e = &claim_registry[claim_count];
|
||||
snprintf(e->name, CAPSULE_NAME_MAX, "%s", name);
|
||||
e->hash = strtoull(hashbuf, NULL, 16);
|
||||
e->block_count = 0;
|
||||
|
||||
char *tok = strtok(blocklist, ",");
|
||||
while (tok && e->block_count < MAX_BLOCKS_PER_CAPSULE) {
|
||||
while (*tok == ' ') tok++; /* defensive: tolerate stray
|
||||
* whitespace from older/
|
||||
* hand-written registry files */
|
||||
if (*tok == '\0') { tok = strtok(NULL, ","); continue; }
|
||||
e->blocks[e->block_count++] = (int)strtol(tok, NULL, 10);
|
||||
tok = strtok(NULL, ",");
|
||||
}
|
||||
claim_count++;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void save_claims_registry(const char *path) {
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
fprintf(stderr, "mkcapsule: WARNING: could not write %s\n", path);
|
||||
return;
|
||||
}
|
||||
fprintf(f, "# DO NOT EDIT — re-run mkcapsule --resolve to refresh.\n"
|
||||
"# This file is derived state, not a source of truth (the\n"
|
||||
"# .4th files themselves are) -- git-ignored on purpose.\n");
|
||||
for (int i = 0; i < claim_count; i++) {
|
||||
ClaimEntry *e = &claim_registry[i];
|
||||
fprintf(f, "%s %016" PRIx64, e->name, e->hash);
|
||||
for (int j = 0; j < e->block_count; j++) {
|
||||
fprintf(f, "%s%d", j == 0 ? " " : ",", e->blocks[j]);
|
||||
}
|
||||
fprintf(f, "\n");
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* blocks_free_at_delta: true if shifting every block in `blocks` by `delta`
|
||||
* lands on numbers that are simultaneously free -- not claimed by any OTHER
|
||||
* current capsule (manifest_entries, ground truth for this run, excluding
|
||||
* `skip_idx` itself) and not inside a reserved range. */
|
||||
static int blocks_free_at_delta(const int *blocks, int block_count, int delta,
|
||||
int skip_idx) {
|
||||
for (int j = 0; j < block_count; j++) {
|
||||
int candidate = blocks[j] + delta;
|
||||
if (candidate < 2048) return 0; /* system-reserved floor */
|
||||
if (reserved_owner_of(candidate)) return 0;
|
||||
for (int i = 0; i < manifest_count; i++) {
|
||||
if (i == skip_idx) continue;
|
||||
for (int k = 0; k < manifest_entries[i].block_count; k++) {
|
||||
if (manifest_entries[i].blocks[k] == candidate) return 0;
|
||||
}
|
||||
}
|
||||
/* also check other candidates within the SAME shifted set don't
|
||||
* collide with each other (only possible if the caller passes a
|
||||
* pathological blocks[] with duplicate relative offsets) */
|
||||
for (int j2 = 0; j2 < j; j2++) {
|
||||
if (blocks[j2] + delta == candidate) return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* suggest_renumbering: preserves the capsule's own existing relative
|
||||
* spacing (workload-3.4th's 4606,4615,4625,... convention, e.g.) by
|
||||
* searching for a single shift delta that lands every block on free
|
||||
* ground at once, rather than reassigning each block independently. */
|
||||
#define MAX_DELTA_SEARCH 20000
|
||||
static int suggest_renumbering(const int *blocks, int block_count,
|
||||
int skip_idx, int *out_delta) {
|
||||
for (int delta = 0; delta <= MAX_DELTA_SEARCH; delta += 1) {
|
||||
if (blocks_free_at_delta(blocks, block_count, delta, skip_idx)) {
|
||||
*out_delta = delta;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* rewrite_block_headers: in-place rewrite of a capsule's own "Block N"
|
||||
* header lines to "Block N+delta", preserving everything else in the file
|
||||
* byte-for-byte (content, comments, formatting). Returns 1 on success. */
|
||||
static int rewrite_block_headers(const char *fpath, int delta) {
|
||||
FILE *f = fopen(fpath, "rb");
|
||||
if (!f) return 0;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
rewind(f);
|
||||
uint8_t *data = malloc((size_t)sz + 1);
|
||||
if (!data || fread(data, 1, (size_t)sz, f) != (size_t)sz) {
|
||||
free(data); fclose(f); return 0;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
FILE *out = fopen(fpath, "wb");
|
||||
if (!out) { free(data); return 0; }
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < (size_t)sz) {
|
||||
size_t start = pos;
|
||||
while (pos < (size_t)sz && data[pos] != '\n') pos++;
|
||||
size_t line_len = pos - start;
|
||||
int has_nl = (pos < (size_t)sz);
|
||||
|
||||
if (line_len >= 7 &&
|
||||
data[start+0]=='B' && data[start+1]=='l' && data[start+2]=='o' &&
|
||||
data[start+3]=='c' && data[start+4]=='k' && data[start+5]==' ') {
|
||||
int bn = (int)strtol((const char *)(data + start + 6), NULL, 10);
|
||||
fprintf(out, "Block %d", bn + delta);
|
||||
} else {
|
||||
fwrite(data + start, 1, line_len, out);
|
||||
}
|
||||
if (has_nl) { fputc('\n', out); pos++; }
|
||||
}
|
||||
fclose(out);
|
||||
free(data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* write_renumber_patch: permanent, git-tracked decision record -- not a
|
||||
* generic diff (the only thing a renumber ever changes is Block header
|
||||
* integers, so a structured before/after list is more directly useful
|
||||
* than a line diff would be). */
|
||||
static void write_renumber_patch(const char *capsule_name, const int *old_blocks,
|
||||
int block_count, int delta,
|
||||
const char *reason) {
|
||||
mkdir(PATCHES_DIR_PATH, 0755); /* ignore EEXIST */
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm *tm_ptr = gmtime(&now);
|
||||
char stamp[32];
|
||||
strftime(stamp, sizeof(stamp), "%Y%m%dT%H%M%SZ", tm_ptr);
|
||||
|
||||
char safe_name[CAPSULE_NAME_MAX];
|
||||
size_t k;
|
||||
for (k = 0; capsule_name[k] && k < sizeof(safe_name) - 1; k++) {
|
||||
char c = capsule_name[k];
|
||||
safe_name[k] = (c == '/' || c == ':') ? '_' : c;
|
||||
}
|
||||
safe_name[k] = '\0';
|
||||
|
||||
char path[MAX_PATH_LEN];
|
||||
snprintf(path, sizeof(path), "%s/%s-%s.patch", PATCHES_DIR_PATH, stamp, safe_name);
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
fprintf(stderr, "mkcapsule: WARNING: could not write patch record %s\n", path);
|
||||
return;
|
||||
}
|
||||
fprintf(f, "# mkcapsule --resolve renumber patch\n");
|
||||
fprintf(f, "# capsule: %s\n", capsule_name);
|
||||
fprintf(f, "# timestamp: %s\n", stamp);
|
||||
fprintf(f, "# reason: %s\n", reason);
|
||||
for (int j = 0; j < block_count; j++) {
|
||||
fprintf(f, "%d -> %d\n", old_blocks[j], old_blocks[j] + delta);
|
||||
}
|
||||
fclose(f);
|
||||
fprintf(stderr, "mkcapsule: wrote patch record %s\n", path);
|
||||
}
|
||||
|
||||
/* prompt_yes_no: reads one line from stdin, true only on 'y'/'Y'. */
|
||||
static int prompt_yes_no(const char *question) {
|
||||
fprintf(stderr, "%s [y/N] ", question);
|
||||
char reply[16];
|
||||
if (!fgets(reply, sizeof(reply), stdin)) return 0;
|
||||
return (reply[0] == 'y' || reply[0] == 'Y');
|
||||
}
|
||||
|
||||
/* cmd_resolve: mkcapsule --resolve <dir> -- see main()'s usage text and
|
||||
* FABRIC-3.md SXXIV for the full design. Deliberately the ONLY interactive
|
||||
* mkcapsule mode; the plain build path (check_block_conflicts/
|
||||
* check_reserved_conflicts) stays non-interactive so CI/unattended builds
|
||||
* never block on a prompt. */
|
||||
static int cmd_resolve(const char *dir) {
|
||||
base_dir = dir;
|
||||
base_dir_len = strlen(base_dir);
|
||||
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') base_dir_len--;
|
||||
|
||||
manifest_count = 0;
|
||||
if (nftw(base_dir, manifest_file, 20, FTW_PHYS) != 0) {
|
||||
fprintf(stderr, "mkcapsule: ERROR: directory scan failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
load_reserved_ranges(RESERVED_FILE_PATH);
|
||||
|
||||
int fresh_registry = (access(CLAIMS_FILE_PATH, F_OK) != 0);
|
||||
load_claims_registry(CLAIMS_FILE_PATH);
|
||||
if (fresh_registry) {
|
||||
fprintf(stderr, "mkcapsule --resolve: %s not found, creating fresh "
|
||||
"baseline from current state\n", CLAIMS_FILE_PATH);
|
||||
}
|
||||
|
||||
int registered = 0, updated = 0, resolved = 0, rejected = 0;
|
||||
|
||||
for (int i = 0; i < manifest_count; i++) {
|
||||
ManifestEntry *m = &manifest_entries[i];
|
||||
ClaimEntry *prior = find_claim(m->name);
|
||||
|
||||
if (prior && prior->hash == m->hash &&
|
||||
prior->block_count == m->block_count &&
|
||||
memcmp(prior->blocks, m->blocks,
|
||||
(size_t)m->block_count * sizeof(int)) == 0) {
|
||||
continue; /* unchanged, nothing to do */
|
||||
}
|
||||
|
||||
/* Does this capsule's CURRENT block set collide with anything else
|
||||
* live right now (ground truth = this run's fresh scan), or with
|
||||
* a reserved range? Fresh-registered-or-updated-in-place capsules
|
||||
* whose own new content simply doesn't collide need no prompt --
|
||||
* only an actual collision needs a human decision. */
|
||||
int collides = 0;
|
||||
for (int j = 0; j < m->block_count && !collides; j++) {
|
||||
int lbn = m->blocks[j];
|
||||
if (reserved_owner_of(lbn)) { collides = 1; break; }
|
||||
for (int k = 0; k < manifest_count; k++) {
|
||||
if (k == i) continue;
|
||||
for (int b = 0; b < manifest_entries[k].block_count; b++) {
|
||||
if (manifest_entries[k].blocks[b] == lbn) { collides = 1; break; }
|
||||
}
|
||||
if (collides) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!collides) {
|
||||
ClaimEntry *dest = prior ? prior : &claim_registry[claim_count++];
|
||||
snprintf(dest->name, CAPSULE_NAME_MAX, "%s", m->name);
|
||||
dest->hash = m->hash;
|
||||
dest->block_count = m->block_count;
|
||||
memcpy(dest->blocks, m->blocks, (size_t)m->block_count * sizeof(int));
|
||||
if (prior) { updated++; fprintf(stderr, "mkcapsule --resolve: %s updated (no conflict)\n", m->name); }
|
||||
else { registered++; fprintf(stderr, "mkcapsule --resolve: %s registered\n", m->name); }
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Real conflict -- try to suggest a renumbering. */
|
||||
fprintf(stderr, "\nmkcapsule --resolve: CONFLICT for %s:\n", m->name);
|
||||
for (int j = 0; j < m->block_count; j++) {
|
||||
int lbn = m->blocks[j];
|
||||
const ReservedRange *r = reserved_owner_of(lbn);
|
||||
if (r) {
|
||||
fprintf(stderr, " block %d is reserved: %s\n", lbn, r->desc);
|
||||
continue;
|
||||
}
|
||||
for (int k = 0; k < manifest_count; k++) {
|
||||
if (k == i) continue;
|
||||
for (int b = 0; b < manifest_entries[k].block_count; b++) {
|
||||
if (manifest_entries[k].blocks[b] == lbn) {
|
||||
fprintf(stderr, " block %d also claimed by %s\n",
|
||||
lbn, manifest_entries[k].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int delta;
|
||||
if (suggest_renumbering(m->blocks, m->block_count, i, &delta)) {
|
||||
fprintf(stderr, " suggested renumbering (shift by %+d):\n", delta);
|
||||
for (int j = 0; j < m->block_count; j++) {
|
||||
fprintf(stderr, " %d -> %d\n", m->blocks[j], m->blocks[j] + delta);
|
||||
}
|
||||
char fpath[MAX_PATH_LEN];
|
||||
snprintf(fpath, sizeof(fpath), "%s/%s", base_dir, m->name);
|
||||
/* capsule name uses ':' for subdirectories; the real path uses '/' */
|
||||
for (char *p = fpath; *p; p++) if (*p == ':') *p = '/';
|
||||
|
||||
if (prompt_yes_no(" Accept this renumbering?")) {
|
||||
if (rewrite_block_headers(fpath, delta)) {
|
||||
char reason[256];
|
||||
snprintf(reason, sizeof(reason),
|
||||
"block conflict, resolved via --resolve");
|
||||
write_renumber_patch(m->name, m->blocks, m->block_count,
|
||||
delta, reason);
|
||||
for (int j = 0; j < m->block_count; j++) m->blocks[j] += delta;
|
||||
ClaimEntry *dest = prior ? prior : &claim_registry[claim_count++];
|
||||
snprintf(dest->name, CAPSULE_NAME_MAX, "%s", m->name);
|
||||
dest->hash = m->hash;
|
||||
dest->block_count = m->block_count;
|
||||
memcpy(dest->blocks, m->blocks, (size_t)m->block_count * sizeof(int));
|
||||
resolved++;
|
||||
} else {
|
||||
fprintf(stderr, " ERROR: could not rewrite %s\n", fpath);
|
||||
rejected++;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, " rejected -- %s left unchanged\n", m->name);
|
||||
rejected++;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, " no free renumbering found within +%d of any "
|
||||
"existing block -- resolve %s by hand\n",
|
||||
MAX_DELTA_SEARCH, m->name);
|
||||
rejected++;
|
||||
}
|
||||
}
|
||||
|
||||
save_claims_registry(CLAIMS_FILE_PATH);
|
||||
|
||||
fprintf(stderr, "\nmkcapsule --resolve: %d registered, %d updated, "
|
||||
"%d resolved, %d rejected/unresolved\n",
|
||||
registered, updated, resolved, rejected);
|
||||
return rejected > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* C Source Generation
|
||||
*===========================================================================*/
|
||||
@@ -1154,6 +1617,15 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --resolve mode: interactive block-conflict resolver (FABRIC-3.md
|
||||
* SXXIV). The ONLY interactive mkcapsule mode -- deliberately a
|
||||
* separate, deliberate invocation, never part of the plain `make`
|
||||
* build path (that stays non-interactive so CI/unattended builds
|
||||
* never block on a prompt). */
|
||||
if (argc == 3 && strcmp(argv[1], "--resolve") == 0) {
|
||||
return cmd_resolve(argv[2]);
|
||||
}
|
||||
|
||||
/* --lint mode: validate all .4th files without generating C output */
|
||||
if (argc == 3 && strcmp(argv[1], "--lint") == 0) {
|
||||
base_dir = argv[2];
|
||||
@@ -1191,6 +1663,8 @@ int main(int argc, char **argv) {
|
||||
" %s --lint <capsules_dir> lint all .4th files\n"
|
||||
" %s --manifest [--sign-key <keyfile>] <capsules_dir> [<out.md>]\n"
|
||||
" print/write block manifest\n"
|
||||
" %s --resolve <capsules_dir> interactive block-conflict\n"
|
||||
" resolver (see file header)\n"
|
||||
"\n"
|
||||
"Lint checks (per .4th file):\n"
|
||||
" - each block opens with 'Block N'\n"
|
||||
@@ -1206,7 +1680,7 @@ int main(int argc, char **argv) {
|
||||
"\n"
|
||||
"Names are colon-separated relative paths (e.g. core:init.4th).\n"
|
||||
"Files whose encoded name exceeds %d bytes are skipped with a warning.\n",
|
||||
argv[0], argv[0], argv[0], CAPSULE_NAME_MAX - 1);
|
||||
argv[0], argv[0], argv[0], argv[0], CAPSULE_NAME_MAX - 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1224,6 +1698,9 @@ int main(int argc, char **argv) {
|
||||
if (check_block_conflicts() != 0) {
|
||||
return 1;
|
||||
}
|
||||
if (check_reserved_conflicts() != 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "mkcapsule: scanning %s\n", base_dir);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user