robisie 0.1.0 → 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/CHANGELOG.md +69 -0
- package/README.md +76 -12
- package/dist/active-recovery.d.ts +58 -0
- package/dist/active-recovery.js +143 -0
- package/dist/agents.d.ts +34 -0
- package/dist/agents.js +77 -16
- package/dist/attempt-identity.d.ts +21 -0
- package/dist/attempt-identity.js +39 -0
- package/dist/ci-failure-result.d.ts +23 -0
- package/dist/ci-failure-result.js +90 -0
- package/dist/claude-headless.d.ts +7 -0
- package/dist/claude-headless.js +28 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +55 -15
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +190 -85
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +3 -3
- package/dist/context.d.ts +24 -0
- package/dist/context.js +22 -6
- package/dist/dependency-readiness.d.ts +13 -0
- package/dist/dependency-readiness.js +80 -0
- package/dist/dispatch-scope.d.ts +18 -0
- package/dist/dispatch-scope.js +46 -0
- package/dist/done-evidence.d.ts +31 -0
- package/dist/done-evidence.js +216 -0
- package/dist/engine-config.d.ts +65 -0
- package/dist/engine-config.js +139 -0
- package/dist/fsops.d.ts +87 -0
- package/dist/fsops.js +117 -24
- package/dist/helpers.d.ts +15 -0
- package/dist/helpers.js +262 -45
- package/dist/intent-rules/admission.d.ts +10 -0
- package/dist/intent-rules/admission.js +35 -0
- package/dist/intent-rules/done-when-roles.d.ts +21 -0
- package/dist/intent-rules/done-when-roles.js +101 -0
- package/dist/intent-rules/hooks.d.ts +29 -0
- package/dist/intent-rules/hooks.js +23 -0
- package/dist/k3-codex-role.d.ts +20 -0
- package/dist/k3-codex-role.js +68 -0
- package/dist/manifest.d.ts +41 -0
- package/dist/manifest.js +59 -14
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +89 -39
- package/dist/opencode-result.d.ts +19 -0
- package/dist/opencode-result.js +136 -0
- package/dist/opencode-review-guard.d.ts +1 -0
- package/dist/opencode-review-guard.js +19 -0
- package/dist/opencode-session-export.d.ts +5 -0
- package/dist/opencode-session-export.js +28 -0
- package/dist/outcome-moves.d.ts +15 -0
- package/dist/outcome-moves.js +39 -0
- package/dist/planer-client.d.ts +214 -0
- package/dist/planer-client.js +351 -0
- package/dist/restart-recovery.d.ts +47 -0
- package/dist/restart-recovery.js +70 -0
- package/dist/review-source-packet.d.ts +17 -0
- package/dist/review-source-packet.js +44 -0
- package/dist/role-registry.d.ts +19 -0
- package/dist/role-registry.js +83 -0
- package/dist/role-results.d.ts +15 -0
- package/dist/role-results.js +42 -0
- package/dist/roles.d.ts +30 -0
- package/dist/roles.js +20 -16
- package/dist/seed-validation.d.ts +28 -0
- package/dist/seed-validation.js +294 -0
- package/dist/selection-result.d.ts +44 -0
- package/dist/selection-result.js +144 -0
- package/dist/sparse-policy.d.ts +8 -0
- package/dist/sparse-policy.js +49 -0
- package/dist/tester-identity.d.ts +11 -0
- package/dist/tester-identity.js +53 -0
- package/dist/worker-env.d.ts +5 -0
- package/dist/worker-env.js +34 -0
- package/docs/ENGINE-API.md +78 -0
- package/docs/ENGINE-CONFIG.md +115 -0
- package/docs/README.en.md +24 -15
- package/docs/README.md +26 -15
- package/docs/RELEASE-0.1.0.md +81 -8
- package/docs/RELEASE-0.1.1.md +110 -0
- package/helpers/ceo-client-launcher-mcp.mjs +36 -8
- package/helpers/client-spawn-session.sh +181 -0
- package/helpers/client-spawn.sh +825 -0
- package/package.json +9 -4
- package/roles/ceo-client.md +3 -2
- package/roles/manager-builder-client.md +18 -4
- package/roles/quality-officer-client.md +20 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CardLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
details?: Record<string, unknown> | null;
|
|
5
|
+
};
|
|
6
|
+
export declare const SCOPE_DETAILS_KEYS: readonly ["scope"];
|
|
7
|
+
/** Ścieżka przed opisem i numerem linii; porównujemy całe segmenty katalogu. */
|
|
8
|
+
export declare function scopePaths(card: CardLike): string[];
|
|
9
|
+
export declare function scopesOverlap(a: readonly string[], b: readonly string[]): boolean;
|
|
10
|
+
/** Only promotion/dispatch collisions ignore tests; write permissions and land diffs stay complete. */
|
|
11
|
+
export declare function collisionScope(scope: readonly string[]): string[];
|
|
12
|
+
/** Dispatch only, normalized paths. ownerChanges: omitted = candidate, array = owner delta, null = unknown IO. */
|
|
13
|
+
export declare function dispatchScope(paths: readonly string[], confirmedTestDirectories: readonly string[], ownerChanges?: readonly string[] | null): string[];
|
|
14
|
+
export declare function pickDispatchable<T extends CardLike>(candidatesByTrack: ReadonlyMap<string, readonly T[]>, activeScopes: readonly string[][], slots: number, projectScope?: (card: T) => readonly string[]): {
|
|
15
|
+
picked: T[];
|
|
16
|
+
waitingScope: T[];
|
|
17
|
+
waitingWip: T[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
export const SCOPE_DETAILS_KEYS = ["scope"];
|
|
3
|
+
/** Ścieżka przed opisem i numerem linii; porównujemy całe segmenty katalogu. */
|
|
4
|
+
export function scopePaths(card) {
|
|
5
|
+
const details = card.details ?? {};
|
|
6
|
+
const raw = details.scope;
|
|
7
|
+
return Array.isArray(raw) ? raw.filter((p) => typeof p === "string")
|
|
8
|
+
.map(p => p.trim().split(/\s+/, 1)[0].replace(/`/g, "").replace(/:\d+(?:[-:]\d+)*$/, ""))
|
|
9
|
+
.filter(Boolean).map(p => posix.normalize(p).replace(/\/$/, "")) : [];
|
|
10
|
+
}
|
|
11
|
+
export function scopesOverlap(a, b) {
|
|
12
|
+
return a.some(x => b.some(y => x === "." || y === "." || x === y || x.startsWith(`${y}/`) || y.startsWith(`${x}/`)));
|
|
13
|
+
}
|
|
14
|
+
/** Only promotion/dispatch collisions ignore tests; write permissions and land diffs stay complete. */
|
|
15
|
+
export function collisionScope(scope) {
|
|
16
|
+
return scope.filter(file => !/(^|\/)(?:__tests__|tests)(?:\/|$)|\.(?:test|spec)\.[^/]+$/.test(file));
|
|
17
|
+
}
|
|
18
|
+
/** Dispatch only, normalized paths. ownerChanges: omitted = candidate, array = owner delta, null = unknown IO. */
|
|
19
|
+
export function dispatchScope(paths, confirmedTestDirectories, ownerChanges) {
|
|
20
|
+
// Unknown owner evidence keeps the broad reservation; an unknown scope blocks all paths.
|
|
21
|
+
if (ownerChanges !== undefined && !paths.length)
|
|
22
|
+
return ["."];
|
|
23
|
+
if (ownerChanges === null)
|
|
24
|
+
return [...paths];
|
|
25
|
+
const directories = new Set(confirmedTestDirectories);
|
|
26
|
+
const reserved = paths.filter(p => !directories.has(p)
|
|
27
|
+
|| !(p === "tests" || p.startsWith("tests/") || p.split("/").includes("__tests__")));
|
|
28
|
+
return [...new Set([...reserved, ...(ownerChanges ?? [])])];
|
|
29
|
+
}
|
|
30
|
+
export function pickDispatchable(candidatesByTrack, activeScopes, slots, projectScope) {
|
|
31
|
+
const picked = [], waitingScope = [], waitingWip = [];
|
|
32
|
+
const occupied = activeScopes.map(collisionScope);
|
|
33
|
+
for (const queue of candidatesByTrack.values())
|
|
34
|
+
for (const card of queue) {
|
|
35
|
+
const scope = collisionScope(projectScope ? projectScope(card) : scopePaths(card));
|
|
36
|
+
if (occupied.some(active => scopesOverlap(scope, active)))
|
|
37
|
+
waitingScope.push(card);
|
|
38
|
+
else if (picked.length >= slots)
|
|
39
|
+
waitingWip.push(card);
|
|
40
|
+
else {
|
|
41
|
+
picked.push(card);
|
|
42
|
+
occupied.push(scope);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { picked, waitingScope, waitingWip };
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type Details = Record<string, unknown> | null | undefined;
|
|
2
|
+
type RequiredDoneEvidenceKey = (typeof REQUIRED_DONE_EVIDENCE_KEYS)[number];
|
|
3
|
+
declare const REQUIRED_DONE_EVIDENCE_KEYS: readonly ["enabled_evidence", "real_use_evidence"];
|
|
4
|
+
export declare function acceptorRole(by: string): string;
|
|
5
|
+
/** Exact landed evidence only; carry/rebase/attempts are not receipts. */
|
|
6
|
+
export declare function machineProcessTraces(details: Details): {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
missing: ("test" | "review" | "ci")[];
|
|
9
|
+
};
|
|
10
|
+
/** Only source fields belong to this tuple; missing/invalid text has one representation. */
|
|
11
|
+
export declare function doneRetrySnapshot(details: Record<string, unknown>): (string | (string | null)[] | null)[];
|
|
12
|
+
export declare function doneEvidenceFingerprint(details: Record<string, unknown>): string;
|
|
13
|
+
/** CR supplies the receipt; a role or model name is never provider evidence. */
|
|
14
|
+
export declare function acceptanceIndependenceValid(details: Details): boolean;
|
|
15
|
+
/** Factory cards with a canonical criterion list need a separate per-criterion acceptance. */
|
|
16
|
+
export declare function doneAcceptanceProblems(persistedDetails: Details, effectiveDetails: Details): string[];
|
|
17
|
+
export declare function declaredCardClassChanged(persistedDetails: Details, effectiveDetails: Details): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Factory-only rollout: an absent/explicitly non-executable class stays on the legacy path. For an
|
|
20
|
+
* executable card, both proofs must be structured objects with non-empty what/when/ref fields.
|
|
21
|
+
*/
|
|
22
|
+
export declare function missingExecutableDoneEvidence(persistedDetails: Details, effectiveDetails: Details): RequiredDoneEvidenceKey[];
|
|
23
|
+
/** True when a patch needs a locked read before merging a done-gate control field. */
|
|
24
|
+
export declare function touchesDoneInvariant(patch: Record<string, unknown>): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Compare the required proof payload, not object identity or unrelated details. This lets callers
|
|
27
|
+
* update other metadata while an executable card remains done, but changing/removing what/when/ref
|
|
28
|
+
* requires a deliberate reopen to delivered first.
|
|
29
|
+
*/
|
|
30
|
+
export declare function executableDoneEvidenceChanged(persistedDetails: Details, effectiveDetails: Details): boolean;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const REQUIRED_DONE_EVIDENCE_KEYS = [
|
|
3
|
+
"enabled_evidence",
|
|
4
|
+
"real_use_evidence",
|
|
5
|
+
];
|
|
6
|
+
const DONE_EVIDENCE_FIELDS = ["what", "when", "ref"];
|
|
7
|
+
const DONE_INVARIANT_DETAIL_KEYS = new Set([
|
|
8
|
+
"card_class",
|
|
9
|
+
"acceptance",
|
|
10
|
+
"done_when",
|
|
11
|
+
...REQUIRED_DONE_EVIDENCE_KEYS,
|
|
12
|
+
]);
|
|
13
|
+
function objectDetails(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
function nonEmptyText(value) {
|
|
19
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
20
|
+
}
|
|
21
|
+
function legacyAcceptorRole(by) {
|
|
22
|
+
return by.trim().split(/[\s(]/, 1)[0].toLowerCase().replace(/-\d+$/, "");
|
|
23
|
+
}
|
|
24
|
+
export function acceptorRole(by) {
|
|
25
|
+
const embedded = by.match(/\b([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)-\d+\b/);
|
|
26
|
+
if (embedded)
|
|
27
|
+
return embedded[1].toLowerCase();
|
|
28
|
+
return legacyAcceptorRole(by);
|
|
29
|
+
}
|
|
30
|
+
function recordOrEmpty(value) { return objectDetails(value) ?? {}; }
|
|
31
|
+
/** Exact landed evidence only; carry/rebase/attempts are not receipts. */
|
|
32
|
+
export function machineProcessTraces(details) {
|
|
33
|
+
const sha = recordOrEmpty(details?.landed).sha, ci = recordOrEmpty(details?.ci_receipt);
|
|
34
|
+
const valid = nonEmptyText(sha) && /^[a-f0-9]{7,40}$/i.test(sha);
|
|
35
|
+
const missing = [];
|
|
36
|
+
for (const key of ["test", "review"]) {
|
|
37
|
+
const trace = recordOrEmpty(details?.[key]);
|
|
38
|
+
if (!valid || trace.sha !== sha || trace.verdict !== "PASS")
|
|
39
|
+
missing.push(key);
|
|
40
|
+
}
|
|
41
|
+
if (!valid || ci.sha !== sha || ci.conclusion !== "success" || ci.check !== "Verify (lint + build:check)"
|
|
42
|
+
|| !["push-main", "pr-head"].includes(String(ci.source)) || !nonEmptyText(ci.head_sha) || !/^[a-f0-9]{7,40}$/i.test(ci.head_sha)
|
|
43
|
+
|| typeof ci.check_run_url !== "string" || !/^https:\/\/api\.github\.com\/repos\/chuppa12\/robisie-app\/check-runs\/[1-9]\d*$/.test(ci.check_run_url)
|
|
44
|
+
|| ci.source === "push-main" && ci.head_sha !== sha
|
|
45
|
+
|| ci.source === "pr-head" && (ci.merge_commit_sha !== sha || ci.base_ref !== "main" || !Number.isSafeInteger(ci.pr_number) || Number(ci.pr_number) <= 0
|
|
46
|
+
|| typeof ci.merged_at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(ci.merged_at) || !Number.isFinite(Date.parse(ci.merged_at)))
|
|
47
|
+
|| typeof ci.checked_at !== "string"
|
|
48
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(ci.checked_at) || !Number.isFinite(Date.parse(ci.checked_at))
|
|
49
|
+
|| typeof ci.run_url !== "string" || !/^https:\/\/github\.com\/chuppa12\/robisie-app\/actions\/runs\/[1-9]\d*$/.test(ci.run_url))
|
|
50
|
+
missing.push("ci");
|
|
51
|
+
return { ok: !missing.length, missing };
|
|
52
|
+
}
|
|
53
|
+
/** Only source fields belong to this tuple; missing/invalid text has one representation. */
|
|
54
|
+
export function doneRetrySnapshot(details) {
|
|
55
|
+
const text = (v) => nonEmptyText(v) ? v : null;
|
|
56
|
+
const proof = (v) => ["what", "when", "ref"].map(k => text(recordOrEmpty(v)[k]));
|
|
57
|
+
const authors = recordOrEmpty(details.seed_provenance);
|
|
58
|
+
return [text(recordOrEmpty(details.landed).sha), proof(details.enabled_evidence), proof(details.real_use_evidence),
|
|
59
|
+
Array.isArray(details.done_when) ? details.done_when.map(text) : null,
|
|
60
|
+
["by", "proposal_by", "packager_by"].map(k => text(authors[k]))];
|
|
61
|
+
}
|
|
62
|
+
export function doneEvidenceFingerprint(details) {
|
|
63
|
+
return createHash("sha256").update(JSON.stringify(doneRetrySnapshot(details))).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
/** CR supplies the receipt; a role or model name is never provider evidence. */
|
|
66
|
+
export function acceptanceIndependenceValid(details) {
|
|
67
|
+
const a = recordOrEmpty(details?.acceptance), i = recordOrEmpty(a.independence);
|
|
68
|
+
const authors = recordOrEmpty(i.authoring_providers), sha = recordOrEmpty(details?.landed).sha;
|
|
69
|
+
return nonEmptyText(sha) && /^[a-f0-9]{7,40}$/i.test(sha) && a.sha === sha && i.sha === sha
|
|
70
|
+
&& nonEmptyText(i.ref) && nonEmptyText(i.receiver_provider)
|
|
71
|
+
&& a.evidence_fingerprint === doneEvidenceFingerprint(details ?? {})
|
|
72
|
+
&& ["by", "proposal_by", "packager_by", "worker"].every(k => nonEmptyText(authors[k])
|
|
73
|
+
&& String(authors[k]).trim().toLowerCase() !== String(i.receiver_provider).trim().toLowerCase());
|
|
74
|
+
}
|
|
75
|
+
/** Factory cards with a canonical criterion list need a separate per-criterion acceptance. */
|
|
76
|
+
export function doneAcceptanceProblems(persistedDetails, effectiveDetails) {
|
|
77
|
+
const isExecutable = declaredCardClass(persistedDetails) === "executable" ||
|
|
78
|
+
declaredCardClass(effectiveDetails) === "executable";
|
|
79
|
+
const isCohort = persistedDetails?.["origin"] === "reksio/propozycja" ||
|
|
80
|
+
effectiveDetails?.["origin"] === "reksio/propozycja";
|
|
81
|
+
const doneWhen = effectiveDetails?.["done_when"];
|
|
82
|
+
if (!isExecutable)
|
|
83
|
+
return [];
|
|
84
|
+
if (!Array.isArray(doneWhen) ||
|
|
85
|
+
doneWhen.length === 0 ||
|
|
86
|
+
!doneWhen.every(nonEmptyText))
|
|
87
|
+
return isCohort
|
|
88
|
+
? ["done_when puste lub niepoprawne — kohorta reksio/propozycja wymaga mierzalnych kryteriów przy done"]
|
|
89
|
+
: [];
|
|
90
|
+
const problems = [];
|
|
91
|
+
const persistedSeedProvenance = objectDetails(persistedDetails?.["seed_provenance"]);
|
|
92
|
+
const effectiveSeedProvenance = objectDetails(effectiveDetails?.["seed_provenance"]);
|
|
93
|
+
const seedField = (key) => {
|
|
94
|
+
const persisted = persistedSeedProvenance?.[key];
|
|
95
|
+
return nonEmptyText(persisted) ? persisted : effectiveSeedProvenance?.[key];
|
|
96
|
+
};
|
|
97
|
+
// updateTask replaces details wholesale; authors must come from the value being saved.
|
|
98
|
+
const proposalBy = effectiveSeedProvenance?.["proposal_by"];
|
|
99
|
+
const packagerBy = effectiveSeedProvenance?.["packager_by"];
|
|
100
|
+
const independent = acceptanceIndependenceValid(effectiveDetails);
|
|
101
|
+
if (isCohort && !independent) {
|
|
102
|
+
if (!nonEmptyText(effectiveSeedProvenance?.by))
|
|
103
|
+
problems.push("brak details.seed_provenance.by");
|
|
104
|
+
if (!nonEmptyText(proposalBy))
|
|
105
|
+
problems.push("brak details.seed_provenance.proposal_by");
|
|
106
|
+
if (!nonEmptyText(packagerBy))
|
|
107
|
+
problems.push("brak details.seed_provenance.packager_by");
|
|
108
|
+
}
|
|
109
|
+
const acceptance = objectDetails(effectiveDetails?.["acceptance"]);
|
|
110
|
+
if (!acceptance) {
|
|
111
|
+
problems.push("brak details.acceptance");
|
|
112
|
+
return problems;
|
|
113
|
+
}
|
|
114
|
+
if (acceptance.independence !== undefined && !independent)
|
|
115
|
+
problems.push("acceptance.independence: brak niezależnego receiptu bieżącego SHA/fingerprintu");
|
|
116
|
+
const by = acceptance["by"];
|
|
117
|
+
if (!nonEmptyText(by))
|
|
118
|
+
problems.push("acceptance.by pusty");
|
|
119
|
+
if (!nonEmptyText(acceptance["at"]))
|
|
120
|
+
problems.push("acceptance.at pusty");
|
|
121
|
+
const criteria = acceptance["criteria"];
|
|
122
|
+
if (!Array.isArray(criteria) ||
|
|
123
|
+
criteria.length !== doneWhen.length ||
|
|
124
|
+
!criteria.every((criterion, i) => criterion === doneWhen[i]))
|
|
125
|
+
problems.push("kryteria zmienione po odbiorze (acceptance.criteria ≠ done_when)");
|
|
126
|
+
const results = acceptance["results"];
|
|
127
|
+
if (!Array.isArray(results) || results.length !== doneWhen.length) {
|
|
128
|
+
const resultCount = Array.isArray(results) ? results.length : 0;
|
|
129
|
+
problems.push(`acceptance.results: ${Array.isArray(results) ? resultCount : "brak"} zamiast ${doneWhen.length}`);
|
|
130
|
+
problems.push(resultCount < doneWhen.length
|
|
131
|
+
? `kryterium ${resultCount + 1}: brak wyniku`
|
|
132
|
+
: `kryterium ${doneWhen.length + 1}: wynik nadmiarowy`);
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(results)) {
|
|
135
|
+
for (let i = 0; i < results.length; i += 1) {
|
|
136
|
+
const result = objectDetails(results[i]);
|
|
137
|
+
if (result?.basis === "machine-process") {
|
|
138
|
+
for (const key of machineProcessTraces(effectiveDetails).missing) {
|
|
139
|
+
if (!problems.includes(`brak śladu: ${key}`))
|
|
140
|
+
problems.push(`brak śladu: ${key}`);
|
|
141
|
+
}
|
|
142
|
+
if (result.ref !== `machine-process:${recordOrEmpty(effectiveDetails?.ci_receipt).run_url}`)
|
|
143
|
+
problems.push(`kryterium ${i + 1}: obcy ref machine-process`);
|
|
144
|
+
}
|
|
145
|
+
const verdict = result?.["verdict"];
|
|
146
|
+
if (verdict !== "PASS")
|
|
147
|
+
problems.push(`kryterium ${i + 1}: werdykt ${verdict ?? "brak"}`);
|
|
148
|
+
if (!nonEmptyText(result?.["ref"]))
|
|
149
|
+
problems.push(`kryterium ${i + 1}: pusty ref`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const seederBy = seedField("by");
|
|
153
|
+
const seederRole = isCohort ? acceptorRole : legacyAcceptorRole;
|
|
154
|
+
if (nonEmptyText(by) && nonEmptyText(seederBy) && seederRole(by) === seederRole(seederBy)) {
|
|
155
|
+
problems.push(`odbierający = siewca (rola ${seederRole(by)})`);
|
|
156
|
+
}
|
|
157
|
+
if (isCohort && nonEmptyText(by)) {
|
|
158
|
+
if ([proposalBy, seedField("proposal_by")].some(v => nonEmptyText(v) && acceptorRole(by) === acceptorRole(v))) {
|
|
159
|
+
problems.push(`odbierający = proponujący (rola ${acceptorRole(by)})`);
|
|
160
|
+
}
|
|
161
|
+
if ([packagerBy, seedField("packager_by")].some(v => nonEmptyText(v) && acceptorRole(by) === acceptorRole(v))) {
|
|
162
|
+
problems.push(`odbierający = opracowujący (rola ${acceptorRole(by)})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return problems;
|
|
166
|
+
}
|
|
167
|
+
function declaredCardClass(details) {
|
|
168
|
+
const value = details?.["card_class"];
|
|
169
|
+
return value === "executable" || value === "non_executable" ? value : undefined;
|
|
170
|
+
}
|
|
171
|
+
function hasCompleteDoneEvidence(value) {
|
|
172
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
173
|
+
return false;
|
|
174
|
+
const evidence = value;
|
|
175
|
+
return DONE_EVIDENCE_FIELDS.every((key) => typeof evidence[key] === "string" && evidence[key].trim().length > 0);
|
|
176
|
+
}
|
|
177
|
+
function evidenceField(value, key) {
|
|
178
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
179
|
+
return undefined;
|
|
180
|
+
return value[key];
|
|
181
|
+
}
|
|
182
|
+
export function declaredCardClassChanged(persistedDetails, effectiveDetails) {
|
|
183
|
+
const persistedClass = declaredCardClass(persistedDetails);
|
|
184
|
+
return persistedClass !== undefined && declaredCardClass(effectiveDetails) !== persistedClass;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Factory-only rollout: an absent/explicitly non-executable class stays on the legacy path. For an
|
|
188
|
+
* executable card, both proofs must be structured objects with non-empty what/when/ref fields.
|
|
189
|
+
*/
|
|
190
|
+
export function missingExecutableDoneEvidence(persistedDetails, effectiveDetails) {
|
|
191
|
+
const isExecutable = declaredCardClass(persistedDetails) === "executable" ||
|
|
192
|
+
declaredCardClass(effectiveDetails) === "executable";
|
|
193
|
+
if (!isExecutable)
|
|
194
|
+
return [];
|
|
195
|
+
return REQUIRED_DONE_EVIDENCE_KEYS.filter((key) => !hasCompleteDoneEvidence(effectiveDetails?.[key]));
|
|
196
|
+
}
|
|
197
|
+
/** True when a patch needs a locked read before merging a done-gate control field. */
|
|
198
|
+
export function touchesDoneInvariant(patch) {
|
|
199
|
+
return Object.keys(patch).some((key) => DONE_INVARIANT_DETAIL_KEYS.has(key));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compare the required proof payload, not object identity or unrelated details. This lets callers
|
|
203
|
+
* update other metadata while an executable card remains done, but changing/removing what/when/ref
|
|
204
|
+
* requires a deliberate reopen to delivered first.
|
|
205
|
+
*/
|
|
206
|
+
export function executableDoneEvidenceChanged(persistedDetails, effectiveDetails) {
|
|
207
|
+
if (declaredCardClass(persistedDetails) !== "executable")
|
|
208
|
+
return false;
|
|
209
|
+
if (objectDetails(persistedDetails?.["acceptance"])) {
|
|
210
|
+
if (JSON.stringify(persistedDetails?.["acceptance"]) !== JSON.stringify(effectiveDetails?.["acceptance"]) ||
|
|
211
|
+
JSON.stringify(persistedDetails?.["done_when"]) !== JSON.stringify(effectiveDetails?.["done_when"]))
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return REQUIRED_DONE_EVIDENCE_KEYS.some((evidenceKey) => DONE_EVIDENCE_FIELDS.some((field) => evidenceField(persistedDetails?.[evidenceKey], field) !==
|
|
215
|
+
evidenceField(effectiveDetails?.[evidenceKey], field)));
|
|
216
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface TrackConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
/** Rola instancji: nazwa wyświetlana i dokładne formy słowa (odmiana), po których reguły rozpoznają ją w tekście. */
|
|
6
|
+
export interface RoleConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
/** Dosłowne formy (z wielkością liter), np. „Control Room”, „CR”. Dopasowanie całych słów, bez ignorowania wielkości. */
|
|
9
|
+
forms: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface RolesConfig {
|
|
12
|
+
/** Koordynator zasiewu (w oryginale Control Room): właściciel karty (`card_owner`) i odbiorca zobowiązania zasiewu. */
|
|
13
|
+
coordinator: RoleConfig & {
|
|
14
|
+
id: string;
|
|
15
|
+
};
|
|
16
|
+
/** Właściciel decyzji (człowiek); `genitive` trafia do markera „Efekt dla <genitive>”. */
|
|
17
|
+
owner: RoleConfig & {
|
|
18
|
+
genitive: string;
|
|
19
|
+
};
|
|
20
|
+
/** Planista: ustala intencję, zakres, design i kryteria. */
|
|
21
|
+
planner: RoleConfig;
|
|
22
|
+
/** Doradca: doradza, nie planuje i nie wykonuje. */
|
|
23
|
+
advisor: RoleConfig;
|
|
24
|
+
}
|
|
25
|
+
/** Domyślne role produktu — neutralne, bez nazw ról konkretnej instalacji. */
|
|
26
|
+
export declare const DEFAULT_ROLES: RolesConfig;
|
|
27
|
+
/** Formy wszystkich ról innych niż Worker — wejście reguły done_when (src/done-when-roles.ts). */
|
|
28
|
+
export declare function nonWorkerRoleForms(roles: RolesConfig): string[];
|
|
29
|
+
export interface InstanceConfig {
|
|
30
|
+
/** UUID projektu w Planerze, do którego trafia szkic. */
|
|
31
|
+
project: string;
|
|
32
|
+
/** Zweryfikowane tory projektu: kompilator przyjmuje dokładny UUID albo dokładną nazwę. */
|
|
33
|
+
tracks: TrackConfig[];
|
|
34
|
+
/** Katalog stanu instancji (bezwzględny); domyślne miejsce zapisu payloadu. */
|
|
35
|
+
stateDir: string;
|
|
36
|
+
/** Role instancji (domyślnie DEFAULT_ROLES). */
|
|
37
|
+
roles: RolesConfig;
|
|
38
|
+
/** Opcjonalny UUID projektu oczekiwań, w którym `termin` musi być datą maszynową (machineFieldProblem). */
|
|
39
|
+
expectationProject?: string;
|
|
40
|
+
/** Plik konfiguracji, z którego wczytano ustawienia (bezwzględny). */
|
|
41
|
+
configPath: string;
|
|
42
|
+
}
|
|
43
|
+
/** K2: dane silnika w tym samym pliku co K4; parsowanie niczego nie uruchamia. */
|
|
44
|
+
export interface EngineConfig extends InstanceConfig {
|
|
45
|
+
engine: {
|
|
46
|
+
/** Repo docelowe (bezwzględne); ścieżka wejściowa względem pliku konfiguracji. */
|
|
47
|
+
repo: string;
|
|
48
|
+
/** Główna gałąź repo docelowego. */
|
|
49
|
+
branch: string;
|
|
50
|
+
/** Każda komenda to niepuste argv: program, potem argumenty; bez interpretacji powłoki. */
|
|
51
|
+
commands: {
|
|
52
|
+
test: string[];
|
|
53
|
+
lint: string[];
|
|
54
|
+
build: string[];
|
|
55
|
+
};
|
|
56
|
+
requiredChecks: string[];
|
|
57
|
+
delivery: 'delivered' | 'pr';
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export declare const CONFIG_FILE = "robisie.config.json";
|
|
61
|
+
export declare function parseConfig(raw: unknown, configPath: string): InstanceConfig;
|
|
62
|
+
export declare function parseRoles(raw: unknown, label: string): RolesConfig;
|
|
63
|
+
export declare function loadConfig(path: string): InstanceConfig;
|
|
64
|
+
export declare function parseEngineConfig(raw: unknown, configPath: string): EngineConfig;
|
|
65
|
+
export declare function loadEngineConfig(path: string): EngineConfig;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// robisie.config.json — minimalna konfiguracja instancji dla kompilatora intencji (K4).
|
|
2
|
+
// Zastępuje wbite w oryginał UUID projektu i torów oraz katalog zapisu (`tools/`).
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
5
|
+
/** Wszystkie warianty „Control Room” z oryginału (`[Cc]ontrol[ -][Rr]oom`) i skrót „CR” (tylko wielkimi). */
|
|
6
|
+
const CONTROL_ROOM_FORMS = ['C', 'c'].flatMap(c => [' ', '-'].flatMap(s => ['R', 'r'].map(r => `${c}ontrol${s}${r}oom`))).concat('CR');
|
|
7
|
+
const both = (...words) => words.flatMap(word => [word, word.charAt(0).toUpperCase() + word.slice(1)]);
|
|
8
|
+
/** Domyślne role produktu — neutralne, bez nazw ról konkretnej instalacji. */
|
|
9
|
+
export const DEFAULT_ROLES = {
|
|
10
|
+
coordinator: { id: 'control-room', name: 'Control Room', forms: CONTROL_ROOM_FORMS },
|
|
11
|
+
owner: { name: 'Właściciel', genitive: 'właściciela', forms: both('właściciel', 'właściciela', 'właścicielowi', 'właścicielem', 'właścicielu') },
|
|
12
|
+
planner: { name: 'Planista', forms: both('planista', 'planisty', 'planiście', 'planistą', 'planisto') },
|
|
13
|
+
advisor: { name: 'Doradca', forms: both('doradca', 'doradcy', 'doradcę', 'doradcą', 'doradco') },
|
|
14
|
+
};
|
|
15
|
+
/** Formy wszystkich ról innych niż Worker — wejście reguły done_when (src/done-when-roles.ts). */
|
|
16
|
+
export function nonWorkerRoleForms(roles) {
|
|
17
|
+
return [...new Set([roles.coordinator, roles.owner, roles.planner, roles.advisor].flatMap(role => role.forms))];
|
|
18
|
+
}
|
|
19
|
+
export const CONFIG_FILE = 'robisie.config.json';
|
|
20
|
+
function fail(path, message) {
|
|
21
|
+
throw new Error(`${path}: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
export function parseConfig(raw, configPath) {
|
|
24
|
+
const label = configPath;
|
|
25
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
26
|
+
fail(label, 'wymagany obiekt JSON');
|
|
27
|
+
const value = raw;
|
|
28
|
+
if (typeof value.project !== 'string' || !value.project.trim())
|
|
29
|
+
fail(label, 'project: wymagany niepusty UUID projektu');
|
|
30
|
+
if (!Array.isArray(value.tracks) || !value.tracks.length)
|
|
31
|
+
fail(label, 'tracks: wymagana niepusta tablica {id, name}');
|
|
32
|
+
const tracks = value.tracks.map((track, index) => {
|
|
33
|
+
if (!track || typeof track !== 'object' || Array.isArray(track))
|
|
34
|
+
fail(label, `tracks[${index}]: wymagany obiekt {id, name}`);
|
|
35
|
+
const { id, name } = track;
|
|
36
|
+
if (typeof id !== 'string' || !id.trim())
|
|
37
|
+
fail(label, `tracks[${index}].id: wymagany niepusty UUID toru`);
|
|
38
|
+
if (typeof name !== 'string' || !name.trim())
|
|
39
|
+
fail(label, `tracks[${index}].name: wymagana niepusta nazwa toru`);
|
|
40
|
+
return { id, name };
|
|
41
|
+
});
|
|
42
|
+
if (typeof value.stateDir !== 'string' || !value.stateDir.trim())
|
|
43
|
+
fail(label, 'stateDir: wymagana ścieżka katalogu stanu instancji');
|
|
44
|
+
const stateDir = isAbsolute(value.stateDir) ? value.stateDir : resolve(dirname(configPath), value.stateDir);
|
|
45
|
+
if (value.expectationProject !== undefined && (typeof value.expectationProject !== 'string' || !value.expectationProject.trim()))
|
|
46
|
+
fail(label, 'expectationProject: gdy podane — niepusty UUID projektu');
|
|
47
|
+
return {
|
|
48
|
+
project: value.project, tracks, stateDir, roles: parseRoles(value.roles, label), configPath,
|
|
49
|
+
...(typeof value.expectationProject === 'string' ? { expectationProject: value.expectationProject } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function text(value, path, label) {
|
|
53
|
+
if (typeof value !== 'string' || !value.trim())
|
|
54
|
+
fail(label, `${path}: wymagany niepusty tekst`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function role(raw, key, label) {
|
|
58
|
+
const fallback = DEFAULT_ROLES[key];
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return fallback;
|
|
61
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
62
|
+
fail(label, `roles.${key}: wymagany obiekt {name, forms}`);
|
|
63
|
+
const value = raw;
|
|
64
|
+
const name = value.name === undefined ? fallback.name : text(value.name, `roles.${key}.name`, label);
|
|
65
|
+
let forms = fallback.forms;
|
|
66
|
+
if (value.forms !== undefined) {
|
|
67
|
+
if (!Array.isArray(value.forms) || !value.forms.length)
|
|
68
|
+
fail(label, `roles.${key}.forms: wymagana niepusta tablica form słowa`);
|
|
69
|
+
forms = value.forms.map((form, index) => text(form, `roles.${key}.forms[${index}]`, label));
|
|
70
|
+
}
|
|
71
|
+
else if (value.name !== undefined)
|
|
72
|
+
forms = [name];
|
|
73
|
+
return { name, forms };
|
|
74
|
+
}
|
|
75
|
+
export function parseRoles(raw, label) {
|
|
76
|
+
if (raw === undefined)
|
|
77
|
+
return DEFAULT_ROLES;
|
|
78
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
79
|
+
fail(label, 'roles: wymagany obiekt {coordinator, owner, planner, advisor}');
|
|
80
|
+
const value = raw;
|
|
81
|
+
for (const key of Object.keys(value))
|
|
82
|
+
if (!(key in DEFAULT_ROLES))
|
|
83
|
+
fail(label, `roles: nieznana rola (dozwolone: ${Object.keys(DEFAULT_ROLES).join(', ')})`);
|
|
84
|
+
const coordinator = value.coordinator;
|
|
85
|
+
const owner = value.owner;
|
|
86
|
+
return {
|
|
87
|
+
coordinator: { ...role(coordinator, 'coordinator', label),
|
|
88
|
+
id: coordinator?.id === undefined ? DEFAULT_ROLES.coordinator.id : text(coordinator.id, 'roles.coordinator.id', label) },
|
|
89
|
+
owner: { ...role(owner, 'owner', label),
|
|
90
|
+
genitive: owner?.genitive !== undefined ? text(owner.genitive, 'roles.owner.genitive', label)
|
|
91
|
+
: owner?.name !== undefined ? text(owner.name, 'roles.owner.name', label) : DEFAULT_ROLES.owner.genitive },
|
|
92
|
+
planner: role(value.planner, 'planner', label),
|
|
93
|
+
advisor: role(value.advisor, 'advisor', label),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function readConfig(path) {
|
|
97
|
+
const configPath = realpathSync(resolve(path));
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = JSON.parse(readFileSync(configPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new Error(error instanceof SyntaxError ? `${configPath}: niepoprawny JSON` : `${configPath}: niepoprawny JSON: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
return { raw, configPath };
|
|
106
|
+
}
|
|
107
|
+
export function loadConfig(path) {
|
|
108
|
+
const { raw, configPath } = readConfig(path);
|
|
109
|
+
return parseConfig(raw, configPath);
|
|
110
|
+
}
|
|
111
|
+
function textList(value, path, label) {
|
|
112
|
+
if (!Array.isArray(value) || !value.length)
|
|
113
|
+
fail(label, `${path}: wymagana niepusta tablica tekstów`);
|
|
114
|
+
return value.map((item, index) => text(item, `${path}[${index}]`, label));
|
|
115
|
+
}
|
|
116
|
+
export function parseEngineConfig(raw, configPath) {
|
|
117
|
+
const config = parseConfig(raw, resolve(configPath));
|
|
118
|
+
const label = config.configPath;
|
|
119
|
+
const engine = raw.engine;
|
|
120
|
+
if (!engine || typeof engine !== 'object' || Array.isArray(engine))
|
|
121
|
+
fail(label, 'engine: wymagany obiekt');
|
|
122
|
+
const value = engine;
|
|
123
|
+
const repo = resolve(dirname(label), text(value.repo, 'engine.repo', label));
|
|
124
|
+
const branch = text(value.branch, 'engine.branch', label);
|
|
125
|
+
if (!value.commands || typeof value.commands !== 'object' || Array.isArray(value.commands))
|
|
126
|
+
fail(label, 'engine.commands: wymagany obiekt {test, lint, build}');
|
|
127
|
+
const commands = value.commands;
|
|
128
|
+
const test = textList(commands.test, 'engine.commands.test', label);
|
|
129
|
+
const lint = textList(commands.lint, 'engine.commands.lint', label);
|
|
130
|
+
const build = textList(commands.build, 'engine.commands.build', label);
|
|
131
|
+
const requiredChecks = textList(value.requiredChecks, 'engine.requiredChecks', label);
|
|
132
|
+
if (value.delivery !== 'delivered' && value.delivery !== 'pr')
|
|
133
|
+
fail(label, 'engine.delivery: wymagane delivered|pr');
|
|
134
|
+
return { ...config, engine: { repo, branch, commands: { test, lint, build }, requiredChecks, delivery: value.delivery } };
|
|
135
|
+
}
|
|
136
|
+
export function loadEngineConfig(path) {
|
|
137
|
+
const { raw, configPath } = readConfig(path);
|
|
138
|
+
return parseEngineConfig(raw, configPath);
|
|
139
|
+
}
|
package/dist/fsops.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Role files can be restored; shared configurations lose robisie entries. Backups are permanent. */
|
|
2
|
+
export type CommentStyle = "hash" | "html";
|
|
3
|
+
export type Op = {
|
|
4
|
+
kind: "file";
|
|
5
|
+
path: string;
|
|
6
|
+
content: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "block";
|
|
9
|
+
path: string;
|
|
10
|
+
comment: CommentStyle;
|
|
11
|
+
body: string;
|
|
12
|
+
/** Remove conflicting server definitions before adding the managed block. */
|
|
13
|
+
clean?: (text: string) => string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "json";
|
|
16
|
+
path: string;
|
|
17
|
+
keyPath: readonly string[];
|
|
18
|
+
value: unknown;
|
|
19
|
+
private: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type Entry = {
|
|
22
|
+
kind: "file";
|
|
23
|
+
path: string;
|
|
24
|
+
backup: string | null;
|
|
25
|
+
/** Digest of the role text init wrote: uninstall removes the file only while it still holds exactly this. */
|
|
26
|
+
fingerprint?: string;
|
|
27
|
+
mode: number | null;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "block";
|
|
30
|
+
path: string;
|
|
31
|
+
comment: CommentStyle;
|
|
32
|
+
created: boolean;
|
|
33
|
+
backup: string | null;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "json";
|
|
36
|
+
path: string;
|
|
37
|
+
keyPath: string[];
|
|
38
|
+
created: boolean;
|
|
39
|
+
backup: string | null;
|
|
40
|
+
/** Digest of what init wrote (doctor). */
|
|
41
|
+
fingerprint: string;
|
|
42
|
+
mode: number | null;
|
|
43
|
+
};
|
|
44
|
+
export interface Store {
|
|
45
|
+
backupDir: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Name for the copy the hint tells the user to make: the first `<path>.kopia[-N]` that nothing occupies, so
|
|
49
|
+
* `cp` can never overwrite the user's own earlier copy or write through a symlink standing at that name.
|
|
50
|
+
*/
|
|
51
|
+
export declare function freeCopyPath(path: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* A private op carries the literal key (`~/.claude.json`): never written through a symlink, because its
|
|
54
|
+
* target is typically a tracked dotfile (`~/dotfiles/claude.json`) and the key would land in git history.
|
|
55
|
+
*/
|
|
56
|
+
export declare function refusePrivateSymlink(op: Op): void;
|
|
57
|
+
/** Stable digest of a JSON value — the manifest's proof that a key still holds what init wrote (no secret stored). */
|
|
58
|
+
export declare function fingerprint(value: unknown): string;
|
|
59
|
+
export declare function renderBlock(comment: CommentStyle, body: string): string;
|
|
60
|
+
/** Remove every complete marker block without trimming neighbouring user text. */
|
|
61
|
+
export declare function stripBlock(text: string, comment: CommentStyle): string;
|
|
62
|
+
export declare function hasBlock(text: string, comment: CommentStyle): boolean;
|
|
63
|
+
/** Parser checks the effect independently of the byte-preserving cleaner. Never include config values in errors. */
|
|
64
|
+
export declare function assertCodexChange(before: string, after: string, body?: string): void;
|
|
65
|
+
/** Shared by command preflight and the actual write; validate before creating a backup. */
|
|
66
|
+
export declare function prepareBlock(current: string | null, comment: CommentStyle, clean?: (text: string) => string, body?: string): string;
|
|
67
|
+
/** What a path (at its write target) held before an op touched it — enough to put it back exactly. */
|
|
68
|
+
export interface Captured {
|
|
69
|
+
path: string;
|
|
70
|
+
bytes: Buffer | null;
|
|
71
|
+
mode: number | null;
|
|
72
|
+
}
|
|
73
|
+
export declare function capture(path: string): Captured;
|
|
74
|
+
/** Put a captured path back (bytes and mode), or remove it when it did not exist. A symlink is kept. */
|
|
75
|
+
export declare function restore(captured: Captured): void;
|
|
76
|
+
/** Apply one op. `prior` is this path's entry from an earlier init (idempotence: no second backup). */
|
|
77
|
+
export declare function applyOp(op: Op, prior: Entry | undefined, store: Store): {
|
|
78
|
+
entry: Entry;
|
|
79
|
+
changed: boolean;
|
|
80
|
+
};
|
|
81
|
+
export declare function preserveFileStep(path: string): string;
|
|
82
|
+
/** Read-only preflight for advice: do not send init into a predictable refusal. */
|
|
83
|
+
export declare function checkRecovery(op: Op, prior: Entry | undefined): void;
|
|
84
|
+
/** Whether what init wrote is still in place (doctor). */
|
|
85
|
+
export declare function entryPresent(entry: Entry): boolean;
|
|
86
|
+
/** Revert one entry. A file robisie no longer recognises is left untouched, with a warning. */
|
|
87
|
+
export declare function undoEntry(entry: Entry, clean?: (text: string) => string): string | null;
|