kibi-cli 2.28.1 → 2.29.0
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/bin/kibi +12 -1
- package/dist/cli-command.d.ts +3 -0
- package/dist/cli-command.d.ts.map +1 -1
- package/dist/cli-command.js +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -2
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +4 -1
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +26 -2
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +67 -3
- package/dist/engine.d.ts +11 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +120 -34
- package/dist/prolog/error-terms.d.ts.map +1 -1
- package/dist/prolog/error-terms.js +15 -9
- package/dist/prolog/store-lock.d.ts +11 -0
- package/dist/prolog/store-lock.d.ts.map +1 -1
- package/dist/prolog/store-lock.js +48 -1
- package/dist/traceability/commit-amend.d.ts +7 -0
- package/dist/traceability/commit-amend.d.ts.map +1 -0
- package/dist/traceability/commit-amend.js +85 -0
- package/dist/traceability/staged-inventory-drift.d.ts +12 -2
- package/dist/traceability/staged-inventory-drift.d.ts.map +1 -1
- package/dist/traceability/staged-inventory-drift.js +22 -5
- package/package.json +1 -1
package/bin/kibi
CHANGED
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
You should have received a copy of the GNU Affero General Public License
|
|
17
17
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
// Kibi needs Node.js 22+ (package "engines"), but npm only warns on a
|
|
20
|
+
// mismatch. Fail before loading anything, with the Node that was found.
|
|
21
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
22
|
+
if (!globalThis.Bun && nodeMajor < 22) {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`kibi requires Node.js 22 or later, but was started with Node.js ${process.versions.node} (${process.execPath}). ` +
|
|
25
|
+
"Put Node.js 22+ first on PATH, including for the host that launches it, and retry.\n",
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { main } = await import("../dist/cli.js");
|
|
20
31
|
|
|
21
32
|
await main();
|
package/dist/cli-command.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ export interface CommandResult {
|
|
|
2
2
|
readonly exitCode?: number;
|
|
3
3
|
}
|
|
4
4
|
export declare function withExitCode<TArgs extends unknown[]>(fn: (...args: TArgs) => Promise<CommandResult | undefined>): (...args: TArgs) => Promise<void>;
|
|
5
|
+
/** Note that a command already printed this error, so the CLI does not again. */
|
|
6
|
+
export declare function markErrorReported(error: unknown): void;
|
|
7
|
+
export declare function errorWasReported(error: unknown): boolean;
|
|
5
8
|
//# sourceMappingURL=cli-command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-command.d.ts","sourceRoot":"","sources":["../src/cli-command.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAGD,wBAAgB,YAAY,CAAC,KAAK,SAAS,OAAO,EAAE,EAClD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,GACzD,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAOnC"}
|
|
1
|
+
{"version":3,"file":"cli-command.d.ts","sourceRoot":"","sources":["../src/cli-command.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAGD,wBAAgB,YAAY,CAAC,KAAK,SAAS,OAAO,EAAE,EAClD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,GACzD,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAOnC;AAID,iFAAiF;AAEjF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEtD;AAGD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIxD"}
|
package/dist/cli-command.js
CHANGED
|
@@ -7,3 +7,14 @@ export function withExitCode(fn) {
|
|
|
7
7
|
}
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
+
const reportedErrors = new WeakSet();
|
|
11
|
+
/** Note that a command already printed this error, so the CLI does not again. */
|
|
12
|
+
// implements REQ-kibi-operation-interface-parity
|
|
13
|
+
export function markErrorReported(error) {
|
|
14
|
+
if (typeof error === "object" && error !== null)
|
|
15
|
+
reportedErrors.add(error);
|
|
16
|
+
}
|
|
17
|
+
// implements REQ-kibi-operation-interface-parity
|
|
18
|
+
export function errorWasReported(error) {
|
|
19
|
+
return (typeof error === "object" && error !== null && reportedErrors.has(error));
|
|
20
|
+
}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUrD;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAIT;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAM7D;AAGD,wBAAgB,YAAY,IAAI,OAAO,CAiBtC;AAGD,wBAAsB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAe3C;AAED,wBAAsB,kBAAkB,CACtC,YAAY,UAAoD,EAChE,KAAK,cAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { readFileSync } from "node:fs";
|
|
|
20
20
|
import { resolve } from "node:path";
|
|
21
21
|
import { pathToFileURL } from "node:url";
|
|
22
22
|
import { Command } from "commander";
|
|
23
|
+
import { errorWasReported } from "./cli-command.js";
|
|
23
24
|
import { registerFoundationCommands } from "./cli-register-foundation.js";
|
|
24
25
|
import { registerJsonOnlyCommands } from "./cli-register-json.js";
|
|
25
26
|
import { registerMaintenanceCommands } from "./cli-register-maintenance.js";
|
|
@@ -41,8 +42,10 @@ export function isCliEntrypoint(argv1, moduleUrl) {
|
|
|
41
42
|
return (argv1 !== undefined && moduleUrl === pathToFileURL(resolve(argv1)).href);
|
|
42
43
|
}
|
|
43
44
|
export function exitCodeFromCliFailure(error) {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (!errorWasReported(error)) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
console.error(message);
|
|
48
|
+
}
|
|
46
49
|
return 1;
|
|
47
50
|
}
|
|
48
51
|
// implements REQ-kibi-operation-interface-parity
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAoF7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AA6hBD,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EACtC,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAC5B,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAC3B,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAMpB;AAGD,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0rB/B;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAoDtB;AAID,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBnB;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,aAAa,EACrB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,EAAE,CAAC,CAiBnB;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAkEtB;AAID,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAmGtB;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyCtB;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAgDtB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CA0BtB;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBtB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBtB;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBtB"}
|
package/dist/commands/check.js
CHANGED
|
@@ -26,6 +26,7 @@ import { escapeAtom, normalizeEntityId, parseTriples, parseViolationRows, } from
|
|
|
26
26
|
import { createSemanticReviewDiagnostics, createSymbolGranularityDiagnostics, createSymbolQualityDiagnostics, hasBlockingImpactDiagnostics, } from "../public/impact-diagnostics.js";
|
|
27
27
|
import { executeCheck } from "../public/operations/check-executor.js";
|
|
28
28
|
import { refreshDetachedSnapshot } from "../runtime/detached-snapshot.js";
|
|
29
|
+
import { amendsRootCommit } from "../traceability/commit-amend.js";
|
|
29
30
|
import { KIBI_NO_IMPACT_DECLARATION, KIBI_SYMBOLS_MANIFEST_PATH, KIBI_SYMBOL_COORDINATES_PATH, } from "../traceability/evidence-model.js";
|
|
30
31
|
import { captureStagedSnapshot } from "../traceability/git-change-snapshot.js";
|
|
31
32
|
import { supportedStagedFiles, } from "../traceability/git-staged.js";
|
|
@@ -617,6 +618,8 @@ export async function checkCommand(options) {
|
|
|
617
618
|
const inventoryDrift = stagedInventoryDriftViolations({
|
|
618
619
|
stagedPaths: new Set(markdownFiles.map((file) => file.path)),
|
|
619
620
|
snapshotEntities,
|
|
621
|
+
judgeNewRequirements: snapshot.headCommit !== null &&
|
|
622
|
+
!amendsRootCommit(process.cwd(), snapshot.headCommit),
|
|
620
623
|
readBase: (filePath) => {
|
|
621
624
|
try {
|
|
622
625
|
return snapshot
|
|
@@ -634,7 +637,7 @@ export async function checkCommand(options) {
|
|
|
634
637
|
coverage: stagedCoverage,
|
|
635
638
|
violations: inventoryDrift,
|
|
636
639
|
messages: [
|
|
637
|
-
`${inventoryDrift.length} staged requirement(s) have a semantic inventory that no longer matches their prose:`,
|
|
640
|
+
`${inventoryDrift.length} staged requirement(s) have a semantic inventory that is missing or no longer matches their prose:`,
|
|
638
641
|
...inventoryDrift.map((violation) => ` ${violation.message}`),
|
|
639
642
|
],
|
|
640
643
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/commands/engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/commands/engine.ts"],"names":[],"mappings":"AA6CA,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BzD;AAcD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBvD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AAEH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiDf;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB3D;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB"}
|
package/dist/commands/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { EngineClient } from "../engine.js";
|
|
3
|
+
import { classifyStoreLockHolder, describeLockHolderPid, lockHolderProcess, readStoreLockOwner, } from "../prolog/store-lock.js";
|
|
3
4
|
import { resolveBranchAttachment, resolveReadBranchAttachment, } from "../utils/branch-resolver.js";
|
|
4
5
|
// Engine maintenance never writes the KB, so a detached HEAD addresses the
|
|
5
6
|
// daemon serving its read-only snapshot.
|
|
@@ -55,14 +56,37 @@ export async function engineStatusCommand() {
|
|
|
55
56
|
await engine.terminate();
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
/** The pid of a live process holding the active branch store, if any. */
|
|
60
|
+
function liveStoreLockHolder(root) {
|
|
61
|
+
const attachment = resolveReadBranchAttachment(root);
|
|
62
|
+
if ("error" in attachment)
|
|
63
|
+
return null;
|
|
64
|
+
const recorded = readStoreLockOwner(attachment.storePath);
|
|
65
|
+
if (recorded === null)
|
|
66
|
+
return null;
|
|
67
|
+
const state = classifyStoreLockHolder(recorded.owner);
|
|
68
|
+
return state === "alive" && recorded.owner.pid !== undefined
|
|
69
|
+
? recorded.owner.pid
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
58
72
|
export async function engineStopCommand() {
|
|
59
73
|
const engine = client();
|
|
60
74
|
try {
|
|
61
75
|
// Stopping an absent daemon must remain a no-op. Starting SWI merely to
|
|
62
76
|
// shut it down defeats the maintenance command and can unexpectedly
|
|
63
77
|
// migrate a legacy branch during an operator status check.
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
const branch = currentBranch(workspaceRoot());
|
|
79
|
+
if (await engine.stop(false)) {
|
|
80
|
+
console.log("Kibi engine stopped");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const { engineSocketPath } = await import("../engine.js");
|
|
84
|
+
console.log(`No Kibi engine answered at ${engineSocketPath(workspaceRoot(), branch)}`);
|
|
85
|
+
const holder = liveStoreLockHolder(workspaceRoot());
|
|
86
|
+
if (holder !== null) {
|
|
87
|
+
const owner = lockHolderProcess(holder);
|
|
88
|
+
console.log(`The branch store is still held by ${describeLockHolderPid(holder)}. That engine was likely started with a different KIBI_RUNTIME_DIR, XDG_RUNTIME_DIR or TMPDIR; stop it from the same environment, or end pid ${owner.pid}.`);
|
|
89
|
+
}
|
|
66
90
|
}
|
|
67
91
|
finally {
|
|
68
92
|
await engine.terminate();
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ import type { PendingSourceReceiptSnapshot } from "./sync/discovery.js";
|
|
|
6
6
|
export declare class SyncError extends Error {
|
|
7
7
|
constructor(message: string);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Sources staged as new files (renames included) that HEAD does not contain yet. An empty branch
|
|
11
|
+
* store compiles its first sync as a semantic baseline, which accepts
|
|
12
|
+
* requirements without an inventory. Once another branch of the workspace has
|
|
13
|
+
* synced, that grace covers committed history only: a new branch must not
|
|
14
|
+
* accept a staged requirement its parent branch would refuse. Without a HEAD
|
|
15
|
+
* commit (or outside Git) there is no history to tell apart, and the set is
|
|
16
|
+
* empty.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stagedNewSourceKeys(workspaceRoot: string): Set<string>;
|
|
9
19
|
export declare function trackedRelationshipFiles(workspaceRoot: string, relationshipsDir: string, recoverMissingPendingSources?: boolean, recoveredPendingReceiptPaths?: PendingSourceReceiptSnapshot[]): string[];
|
|
10
20
|
export interface SyncResult extends SyncSummary {
|
|
11
21
|
exitCode?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA6BjE,OAAO,EAEL,yBAAyB,EAE1B,MAAM,gDAAgD,CAAC;AAGxD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EACL,KAAK,gBAAgB,EAGtB,MAAM,6BAA6B,CAAC;AA2BrC,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAmCxE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAmCD;;;;;;;;GAQG;AAEH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqBtE;AAkCD,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,4BAA4B,UAAQ,EACpC,4BAA4B,GAAE,4BAA4B,EAAO,GAChE,MAAM,EAAE,CAuGV;AAiFD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,yBAAyB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,yBAAyB,GAAG;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE,KACzD,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,aAAa,CAAC;IAC/D,yBAAyB,CAAC,EAAE,OAAO,yBAAyB,CAAC;CAC9D;AA+FD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAGvE;AAGD,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,IAAI,GAChB,IAAI,CAMN;AAED,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,UAAU,EAAE,EACzB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,IAAI,CAIN;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,IAAI,CAMP;AAED,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,UAAU,EAAE,EACzB,oBAAoB,EAAE,OAAO,EAC7B,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAClB,IAAI,CAUN;AAGD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAC;CACZ,EACN,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAmvCrB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/commands/sync.js
CHANGED
|
@@ -19,6 +19,7 @@ import { execSync } from "node:child_process";
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
|
+
import { markErrorReported } from "../cli-command.js";
|
|
22
23
|
import { branchErrorToDiagnostic, createDocsNotIndexedDiagnostic, createInvalidAuthoringDiagnostic, createKbMissingDiagnostic, formatSyncSummary, } from "../diagnostics.js";
|
|
23
24
|
import { EngineClient, acquireEnginePublicationLease, engineSocketPath, ensureJournaledBranchStoreAsync, formatEngineAttachmentMismatch, fsyncJournaledBranchStore, parseEngineAttachmentIdentity, readEngineAttachmentIdentity, } from "../engine.js";
|
|
24
25
|
import { isCliDebugEnabled } from "../env.js";
|
|
@@ -28,7 +29,7 @@ import { analyzeSemanticAdvisorInput } from "../operations/semantic-advisor/anal
|
|
|
28
29
|
import { validateSemanticInventoryBoundary } from "../operations/semantic-advisor/ingestion-boundary.js";
|
|
29
30
|
import { PrologProcess } from "../prolog.js";
|
|
30
31
|
import { detachedHeadWriteRefusal, resolveBranchAttachment, } from "../utils/branch-resolver.js";
|
|
31
|
-
import { branchStoreManifestPath, branchStorePath, ensureBranchStoreManifest, expectedBranchStoreManifest, } from "../utils/branch-store-locator.js";
|
|
32
|
+
import { branchStoreManifestPath, branchStorePath, branchStoresRoot, ensureBranchStoreManifest, expectedBranchStoreManifest, } from "../utils/branch-store-locator.js";
|
|
32
33
|
import { loadEntityPaths } from "../utils/config.js";
|
|
33
34
|
import { SYNC_CACHE_TTL_MS, SYNC_CACHE_VERSION, SYNC_COMPILER_FINGERPRINT, failedSourceKeepsSemanticBaseline, hashFile, hashManifestWithCoordinates, hashNormalized, readSyncCache, toCacheKey, writeSyncCache, } from "./sync/cache.js";
|
|
34
35
|
import { clearRecoveredPendingSourceReceipts, discoverSourceFiles, } from "./sync/discovery.js";
|
|
@@ -75,6 +76,62 @@ function assertNoUnresolvedGitConflicts(workspaceRoot) {
|
|
|
75
76
|
throw new SyncError(`Unresolved Git conflicts block Kibi compilation; resolve these authored files first: ${conflicted.split("\n").join(", ")}`);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Sources staged as new files (renames included) that HEAD does not contain yet. An empty branch
|
|
81
|
+
* store compiles its first sync as a semantic baseline, which accepts
|
|
82
|
+
* requirements without an inventory. Once another branch of the workspace has
|
|
83
|
+
* synced, that grace covers committed history only: a new branch must not
|
|
84
|
+
* accept a staged requirement its parent branch would refuse. Without a HEAD
|
|
85
|
+
* commit (or outside Git) there is no history to tell apart, and the set is
|
|
86
|
+
* empty.
|
|
87
|
+
*/
|
|
88
|
+
// implements REQ-kibi-proposition-complete-ingestion
|
|
89
|
+
export function stagedNewSourceKeys(workspaceRoot) {
|
|
90
|
+
try {
|
|
91
|
+
return new Set(execSync(
|
|
92
|
+
// --no-renames: a staged rename is a new source key too, judged on
|
|
93
|
+
// the parent branch like any other addition.
|
|
94
|
+
"git diff --cached --no-renames --name-only --relative --diff-filter=A -z HEAD", {
|
|
95
|
+
cwd: workspaceRoot,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
})
|
|
100
|
+
.split("\0")
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.map((file) => file.replaceAll("\\", "/")));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return new Set();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Whether a branch store other than `livePath` has a non-empty sync cache. */
|
|
109
|
+
function anotherBranchStoreSynced(workspaceRoot, livePath) {
|
|
110
|
+
const root = branchStoresRoot(workspaceRoot);
|
|
111
|
+
let entries;
|
|
112
|
+
try {
|
|
113
|
+
entries = readdirSync(root);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const storePath = path.join(root, entry);
|
|
120
|
+
if (path.resolve(storePath) === path.resolve(livePath))
|
|
121
|
+
continue;
|
|
122
|
+
try {
|
|
123
|
+
const cache = JSON.parse(readFileSync(path.join(storePath, "sync-cache.json"), "utf8"));
|
|
124
|
+
if (cache.hashes !== null &&
|
|
125
|
+
typeof cache.hashes === "object" &&
|
|
126
|
+
Object.keys(cache.hashes).length > 0)
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// A store without a readable cache has not synced.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
78
135
|
export function trackedRelationshipFiles(workspaceRoot, relationshipsDir, recoverMissingPendingSources = false, recoveredPendingReceiptPaths = []) {
|
|
79
136
|
let tracked;
|
|
80
137
|
try {
|
|
@@ -531,6 +588,10 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
531
588
|
const addedShardRelationships = [];
|
|
532
589
|
const removedShardRelationships = [];
|
|
533
590
|
const initialSemanticBaseline = Object.keys(syncCache.hashes).length === 0;
|
|
591
|
+
const baselineExcludedKeys = initialSemanticBaseline &&
|
|
592
|
+
anotherBranchStoreSynced(workspaceRoot, livePathForEngine)
|
|
593
|
+
? stagedNewSourceKeys(workspaceRoot)
|
|
594
|
+
: new Set();
|
|
534
595
|
const changedMarkdownFiles = [];
|
|
535
596
|
const changedManifestFiles = [];
|
|
536
597
|
const forceEntitySourceKeys = new Set();
|
|
@@ -664,7 +725,8 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
664
725
|
nextExtractionFailures[key] = {
|
|
665
726
|
hash,
|
|
666
727
|
message: failure.message,
|
|
667
|
-
...(initialSemanticBaseline
|
|
728
|
+
...((initialSemanticBaseline && !baselineExcludedKeys.has(key)) ||
|
|
729
|
+
previousFailure?.baseline === true
|
|
668
730
|
? { baseline: true }
|
|
669
731
|
: {}),
|
|
670
732
|
...(semanticHash !== undefined ? { semanticHash } : {}),
|
|
@@ -774,7 +836,8 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
774
836
|
const enforceBoundary = advertisedContract ||
|
|
775
837
|
(previousFailure !== undefined
|
|
776
838
|
? !failedSourceKeepsSemanticBaseline(previousFailure, boundary.sourceHash)
|
|
777
|
-
: (
|
|
839
|
+
: (syncCache.hashes[key] === undefined &&
|
|
840
|
+
(!initialSemanticBaseline || baselineExcludedKeys.has(key))) ||
|
|
778
841
|
syncCache.semanticContracts[key] === true ||
|
|
779
842
|
(previousSemanticHash !== undefined &&
|
|
780
843
|
previousSemanticHash !== boundary.sourceHash));
|
|
@@ -1251,6 +1314,7 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
1251
1314
|
}
|
|
1252
1315
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1253
1316
|
console.error(`Error: ${errorMessage}`);
|
|
1317
|
+
markErrorReported(error);
|
|
1254
1318
|
const commit = getCurrentCommit();
|
|
1255
1319
|
const summary = withOptionalCommit({
|
|
1256
1320
|
branch: currentBranch || "unknown",
|
package/dist/engine.d.ts
CHANGED
|
@@ -55,6 +55,14 @@ export type EnginePublicationLease = Readonly<{
|
|
|
55
55
|
readonly release: () => void;
|
|
56
56
|
}>;
|
|
57
57
|
export declare function acquireEnginePublicationLease(workspaceRoot: string, branch: string): EnginePublicationLease;
|
|
58
|
+
/**
|
|
59
|
+
* The Node binary that hosts the daemon. A Node client starts it with its own
|
|
60
|
+
* runtime, so a host that runs Kibi through Node 22+ by absolute path still
|
|
61
|
+
* gets a Node 22+ daemon when an older `node` comes first on PATH. Bun clients
|
|
62
|
+
* (source checkouts, tests) use `node` from PATH, since the daemon is a Node
|
|
63
|
+
* artifact. KIBI_NODE_PATH overrides both.
|
|
64
|
+
*/
|
|
65
|
+
export declare function daemonNodePath(environment?: NodeJS.ProcessEnv): string;
|
|
58
66
|
/** What a live daemon reports in its handshake. */
|
|
59
67
|
export type EngineDaemonIdentity = Readonly<{
|
|
60
68
|
prologIdentity: string | null;
|
|
@@ -137,7 +145,9 @@ export declare class EngineClient {
|
|
|
137
145
|
private reconcileAttachment;
|
|
138
146
|
private shutdownConnectedDaemon;
|
|
139
147
|
getPid(): number;
|
|
140
|
-
|
|
148
|
+
/** Resolves false when no daemon answered and none was started. */
|
|
149
|
+
stop(startIfMissing?: boolean): Promise<boolean>;
|
|
150
|
+
private daemonProcessAlive;
|
|
141
151
|
terminate(): Promise<void>;
|
|
142
152
|
}
|
|
143
153
|
export declare function ensureJournaledBranchStoreAsync(branchPath: string): Promise<void>;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAgCA,OAAO,EAEL,KAAK,iBAAiB,EAMvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EACV,wBAAwB,EACxB,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,aAAa,EAAmB,MAAM,aAAa,CAAC;AAa7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,sCAAsC,CAAC;AAQ9C,YAAY,EACV,wBAAwB,EACxB,eAAe,EACf,aAAa,GACd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAU5B,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AACrD,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,qEAAqE;AACrE,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAEhD;;;;;GAKG;AAEH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,QAAwB,CAAC;AAE7D;;;;GAIG;AAEH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAQD,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,OAAO,GACtB,MAAM,IAAI,CAIZ;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,GACjB,wBAAwB,GAAG,IAAI,CAgBjC;AAED,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,wBAAwB,GAAG,IAAI,CAyBjC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,wBAAwB,GAAG,IAAI,EACzC,QAAQ,EAAE,wBAAwB,GAAG,IAAI,GACxC,OAAO,CAWT;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,wBAAwB,GAAG,IAAI,EACzC,QAAQ,EAAE,wBAAwB,GAAG,IAAI,GACxC,MAAM,CASR;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -293,10 +293,22 @@ function parseFrames(buffer, onFrame) {
|
|
|
293
293
|
}
|
|
294
294
|
return buffer.subarray(cursor);
|
|
295
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The socket directory every Kibi process for one user must agree on. Linux
|
|
298
|
+
* hosts and agent harnesses often give their children a private TMPDIR, so
|
|
299
|
+
* following it there would hide an engine from the CLI, git hooks and other
|
|
300
|
+
* hosts that share its branch store (they then see only `store_locked`).
|
|
301
|
+
* Linux therefore uses /tmp unless KIBI_RUNTIME_DIR or XDG_RUNTIME_DIR names
|
|
302
|
+
* a directory. macOS keeps its per-user TMPDIR, which launchd sets for every
|
|
303
|
+
* process of the user.
|
|
304
|
+
*/
|
|
305
|
+
function defaultRuntimeDirectory() {
|
|
306
|
+
return path.join(process.platform === "linux" ? "/tmp" : os.tmpdir(), "kibi-runtime");
|
|
307
|
+
}
|
|
296
308
|
export function runtimeDirectory() {
|
|
297
309
|
const configured = process.env.KIBI_RUNTIME_DIR ??
|
|
298
310
|
process.env.XDG_RUNTIME_DIR ??
|
|
299
|
-
|
|
311
|
+
defaultRuntimeDirectory();
|
|
300
312
|
// Windows named pipes have no Unix socket pathname budget. Preserve its
|
|
301
313
|
// filesystem location policy for the associated pid files.
|
|
302
314
|
if (process.platform === "win32") {
|
|
@@ -340,38 +352,62 @@ export function runtimeDirectory() {
|
|
|
340
352
|
const maximumBytes = process.platform === "linux" ? 107 : 103;
|
|
341
353
|
const socketName = `kibi-${"0".repeat(32)}.sock`;
|
|
342
354
|
for (const candidate of candidates) {
|
|
355
|
+
const rejection = runtimeDirectoryRejection(candidate, euid, socketName, maximumBytes);
|
|
356
|
+
if (typeof rejection === "object")
|
|
357
|
+
return rejection.directory;
|
|
358
|
+
if (candidate === process.env.KIBI_RUNTIME_DIR)
|
|
359
|
+
warnRuntimeDirectoryIgnored(candidate, rejection);
|
|
360
|
+
}
|
|
361
|
+
throw new Error("Unable to create a writable Kibi engine runtime directory");
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The canonical runtime directory when `candidate` can hold the engine socket,
|
|
365
|
+
* else why it cannot.
|
|
366
|
+
*/
|
|
367
|
+
function runtimeDirectoryRejection(candidate, euid, socketName, maximumBytes) {
|
|
368
|
+
try {
|
|
343
369
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
if (error.code !== "ENOENT")
|
|
350
|
-
throw error;
|
|
351
|
-
}
|
|
352
|
-
mkdirSync(candidate, { recursive: true, mode: 0o700 });
|
|
353
|
-
const directory = lstatSync(candidate);
|
|
354
|
-
if (!directory.isDirectory() ||
|
|
355
|
-
directory.uid !== euid ||
|
|
356
|
-
(directory.mode & 0o077) !== 0)
|
|
357
|
-
continue;
|
|
358
|
-
const canonical = canonicalFilesystemPath(candidate);
|
|
359
|
-
if (Buffer.byteLength(path.join(canonical, socketName), "utf8") >
|
|
360
|
-
maximumBytes)
|
|
361
|
-
continue;
|
|
362
|
-
// XDG_RUNTIME_DIR can exist but be mounted read-only (for example in a
|
|
363
|
-
// constrained container). Probe the directory before using it for the
|
|
364
|
-
// socket, start lock, and pid files so auto-start fails over cleanly.
|
|
365
|
-
const probe = path.join(canonical, `.kibi-write-${randomUUID()}`);
|
|
366
|
-
writeFileSync(probe, "", { mode: 0o600, flag: "wx" });
|
|
367
|
-
unlinkSync(probe);
|
|
368
|
-
return canonical;
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// Try the private system-temp fallback below.
|
|
370
|
+
if (lstatSync(candidate).isSymbolicLink())
|
|
371
|
+
return "it is a symbolic link";
|
|
372
372
|
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error.code !== "ENOENT")
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
mkdirSync(candidate, { recursive: true, mode: 0o700 });
|
|
378
|
+
const directory = lstatSync(candidate);
|
|
379
|
+
if (!directory.isDirectory())
|
|
380
|
+
return "it is not a directory";
|
|
381
|
+
if (directory.uid !== euid)
|
|
382
|
+
return `it is owned by uid ${directory.uid}, not by the current user (uid ${euid})`;
|
|
383
|
+
if ((directory.mode & 0o077) !== 0)
|
|
384
|
+
return `its mode is ${(directory.mode & 0o777).toString(8).padStart(4, "0")}; it must be private to its owner (chmod 700)`;
|
|
385
|
+
const canonical = canonicalFilesystemPath(candidate);
|
|
386
|
+
if (Buffer.byteLength(path.join(canonical, socketName), "utf8") > maximumBytes)
|
|
387
|
+
return `its path is too long for a Unix socket (at most ${maximumBytes - socketName.length - 1} bytes)`;
|
|
388
|
+
// XDG_RUNTIME_DIR can exist but be mounted read-only (for example in a
|
|
389
|
+
// constrained container). Probe the directory before using it for the
|
|
390
|
+
// socket, start lock, and pid files so auto-start fails over cleanly.
|
|
391
|
+
const probe = path.join(canonical, `.kibi-write-${randomUUID()}`);
|
|
392
|
+
writeFileSync(probe, "", { mode: 0o600, flag: "wx" });
|
|
393
|
+
unlinkSync(probe);
|
|
394
|
+
return { directory: canonical };
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return `it is not usable (${error.message})`;
|
|
373
398
|
}
|
|
374
|
-
|
|
399
|
+
}
|
|
400
|
+
const warnedRuntimeDirectories = new Set();
|
|
401
|
+
/**
|
|
402
|
+
* Processes that disagree on the runtime directory cannot reach each other's
|
|
403
|
+
* engine, so an explicit KIBI_RUNTIME_DIR that Kibi cannot use is reported
|
|
404
|
+
* (once per process) instead of being replaced silently.
|
|
405
|
+
*/
|
|
406
|
+
function warnRuntimeDirectoryIgnored(directory, reason) {
|
|
407
|
+
if (warnedRuntimeDirectories.has(directory))
|
|
408
|
+
return;
|
|
409
|
+
warnedRuntimeDirectories.add(directory);
|
|
410
|
+
process.stderr.write(`kibi: ignoring KIBI_RUNTIME_DIR=${directory} because ${reason}; using a private directory under /tmp instead.\n`);
|
|
375
411
|
}
|
|
376
412
|
function fsyncPath(filePath) {
|
|
377
413
|
if (!existsSync(filePath))
|
|
@@ -669,6 +705,18 @@ function recoverStaleJournalLock(branchPath) {
|
|
|
669
705
|
// race between the liveness probe and removal.
|
|
670
706
|
}
|
|
671
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* The Node binary that hosts the daemon. A Node client starts it with its own
|
|
710
|
+
* runtime, so a host that runs Kibi through Node 22+ by absolute path still
|
|
711
|
+
* gets a Node 22+ daemon when an older `node` comes first on PATH. Bun clients
|
|
712
|
+
* (source checkouts, tests) use `node` from PATH, since the daemon is a Node
|
|
713
|
+
* artifact. KIBI_NODE_PATH overrides both.
|
|
714
|
+
*/
|
|
715
|
+
export function daemonNodePath(environment = process.env) {
|
|
716
|
+
if (environment.KIBI_NODE_PATH)
|
|
717
|
+
return environment.KIBI_NODE_PATH;
|
|
718
|
+
return "Bun" in globalThis ? "node" : process.execPath;
|
|
719
|
+
}
|
|
672
720
|
function daemonEntry() {
|
|
673
721
|
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
674
722
|
// Packed clients keep the daemon beside engine.js. Source-tree repository
|
|
@@ -827,7 +875,7 @@ export class EngineClient {
|
|
|
827
875
|
catch {
|
|
828
876
|
if (!allowSpawn)
|
|
829
877
|
return;
|
|
830
|
-
const nodePath =
|
|
878
|
+
const nodePath = daemonNodePath();
|
|
831
879
|
const entry = daemonEntry();
|
|
832
880
|
const lockPath = engineStartLockPath(this.workspaceRoot, this.branch);
|
|
833
881
|
const startDeadline = Date.now() + this.timeout;
|
|
@@ -915,6 +963,10 @@ export class EngineClient {
|
|
|
915
963
|
}
|
|
916
964
|
attachSocket(socket) {
|
|
917
965
|
this.inputBuffer = Buffer.alloc(0);
|
|
966
|
+
// An idle connection must not keep its client alive: a host that is done
|
|
967
|
+
// with an MCP server or CLI command lets it exit while the shared daemon
|
|
968
|
+
// keeps serving. request() holds the socket while a reply is pending.
|
|
969
|
+
socket.unref();
|
|
918
970
|
socket.on("data", (chunk) => {
|
|
919
971
|
if (this.socket !== socket)
|
|
920
972
|
return;
|
|
@@ -983,10 +1035,13 @@ export class EngineClient {
|
|
|
983
1035
|
release = resolve;
|
|
984
1036
|
});
|
|
985
1037
|
await previous;
|
|
1038
|
+
let held = null;
|
|
986
1039
|
try {
|
|
987
1040
|
const socket = this.socket;
|
|
988
1041
|
if (socket === null || socket.destroyed)
|
|
989
1042
|
throw new Error("Kibi engine is not connected");
|
|
1043
|
+
socket.ref();
|
|
1044
|
+
held = socket;
|
|
990
1045
|
const id = ++this.requestId;
|
|
991
1046
|
const limits = this.readLimits !== undefined &&
|
|
992
1047
|
READ_LIMITED_METHODS.has(request.method)
|
|
@@ -1039,6 +1094,8 @@ export class EngineClient {
|
|
|
1039
1094
|
return value;
|
|
1040
1095
|
}
|
|
1041
1096
|
finally {
|
|
1097
|
+
if (held !== null && this.pending.size === 0)
|
|
1098
|
+
held.unref();
|
|
1042
1099
|
release();
|
|
1043
1100
|
}
|
|
1044
1101
|
}
|
|
@@ -1301,6 +1358,7 @@ export class EngineClient {
|
|
|
1301
1358
|
return 0;
|
|
1302
1359
|
}
|
|
1303
1360
|
}
|
|
1361
|
+
/** Resolves false when no daemon answered and none was started. */
|
|
1304
1362
|
async stop(startIfMissing = true) {
|
|
1305
1363
|
const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
|
|
1306
1364
|
if (!this.isRunning()) {
|
|
@@ -1309,14 +1367,29 @@ export class EngineClient {
|
|
|
1309
1367
|
}
|
|
1310
1368
|
catch {
|
|
1311
1369
|
if (!startIfMissing)
|
|
1312
|
-
return;
|
|
1370
|
+
return false;
|
|
1313
1371
|
throw new Error("Unable to start Kibi engine for stop request");
|
|
1314
1372
|
}
|
|
1315
1373
|
}
|
|
1316
1374
|
if (!this.isRunning() && existsSync(socketPath)) {
|
|
1317
1375
|
throw new Error(`Kibi engine socket remains present but is not reachable: ${socketPath}`);
|
|
1318
1376
|
}
|
|
1377
|
+
// A draining daemon has no listener but still counts as stopped here.
|
|
1378
|
+
const found = this.isRunning() || this.daemonProcessAlive();
|
|
1319
1379
|
await this.shutdownConnectedDaemon();
|
|
1380
|
+
return found;
|
|
1381
|
+
}
|
|
1382
|
+
daemonProcessAlive() {
|
|
1383
|
+
const pid = this.getPid();
|
|
1384
|
+
if (pid <= 0)
|
|
1385
|
+
return false;
|
|
1386
|
+
try {
|
|
1387
|
+
process.kill(pid, 0);
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1320
1393
|
}
|
|
1321
1394
|
async terminate() {
|
|
1322
1395
|
// Closing a client connection releases the engine; the daemon remains
|
|
@@ -1643,7 +1716,15 @@ export async function runEngineDaemon(requestedOptions) {
|
|
|
1643
1716
|
: { maxOutputBytes: options.maxOutputBytes }),
|
|
1644
1717
|
});
|
|
1645
1718
|
const session = new EngineSession(prolog, branchPath);
|
|
1646
|
-
|
|
1719
|
+
try {
|
|
1720
|
+
await session.boot();
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
// A failed boot (store_locked, a module load error) leaves the SWI child
|
|
1724
|
+
// running, which keeps this daemon alive with no socket.
|
|
1725
|
+
await prolog.terminate().catch(() => undefined);
|
|
1726
|
+
throw error;
|
|
1727
|
+
}
|
|
1647
1728
|
let attachedIdentity = readEngineAttachmentIdentity(branchPath);
|
|
1648
1729
|
const serverPrologIdentity = engineSwiplIdentity();
|
|
1649
1730
|
const serverPackageVersions = enginePackageVersions();
|
|
@@ -2099,7 +2180,12 @@ export async function runEngineDaemon(requestedOptions) {
|
|
|
2099
2180
|
return result;
|
|
2100
2181
|
}
|
|
2101
2182
|
case "status":
|
|
2102
|
-
|
|
2183
|
+
// Qualified: kb.pl loads library(http/json), but SWI-Prolog 9.0 does
|
|
2184
|
+
// not autoload it into `user`, where its toplevel then waits on a
|
|
2185
|
+
// "correct to kb:json_write_dict?" prompt instead of answering. A
|
|
2186
|
+
// string binding reaches the client as JSON text; an atom would
|
|
2187
|
+
// arrive Prolog-quoted and fail to parse.
|
|
2188
|
+
return prolog.query("kb_storage_status(Status), with_output_to(string(Json), json:json_write_dict(current_output, Status, [width(0)]))");
|
|
2103
2189
|
case "checkpoint": {
|
|
2104
2190
|
queryCache.clear();
|
|
2105
2191
|
freshnessCache = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-terms.d.ts","sourceRoot":"","sources":["../../src/prolog/error-terms.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"error-terms.d.ts","sourceRoot":"","sources":["../../src/prolog/error-terms.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAG1D,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,cAAc,GACd,mBAAmB,GACnB,kBAAkB,GAClB,sBAAsB,GACtB,eAAe,GACf,kBAAkB,CAAC;AAGvB,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAGH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAGH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE,eAAe,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,SAAS,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC,CAAC,CAAC;AAEH,yEAAyE;AAEzE,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAK1B;AAED,kFAAkF;AAElF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GACf,iBAAiB,GAAG,IAAI,CAsG1B"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import { splitTopLevelGeneral } from "./codec.js";
|
|
19
|
+
import { describeLockHolderPid } from "./store-lock.js";
|
|
19
20
|
/**
|
|
20
21
|
* Structured transport for Prolog errors.
|
|
21
22
|
*
|
|
@@ -88,7 +89,7 @@ export function parsePrologErrorTerm(termText) {
|
|
|
88
89
|
const holder = detail.owner;
|
|
89
90
|
const holderSummary = holder === null
|
|
90
91
|
? "an unidentified holder"
|
|
91
|
-
:
|
|
92
|
+
: `${holder.pid === undefined ? "pid ?" : describeLockHolderPid(holder.pid)}${holder.workspaceRoot !== undefined ? ` for ${holder.workspaceRoot}` : ""}${holder.startedAt !== undefined ? ` since ${holder.startedAt}` : ""}`;
|
|
92
93
|
return {
|
|
93
94
|
code: "permission_denied",
|
|
94
95
|
storeLocked: detail,
|
|
@@ -319,16 +320,21 @@ function parseEntityRole(message) {
|
|
|
319
320
|
return "target";
|
|
320
321
|
return undefined;
|
|
321
322
|
}
|
|
322
|
-
|
|
323
|
+
const PROLOG_ESCAPES = { n: "\n", t: "\t" };
|
|
324
|
+
/**
|
|
325
|
+
* Strip SWI quoted-atom or string quoting and unescape standard character
|
|
326
|
+
* escapes. Strings matter for store_locked: kb.pl reads the owner journal
|
|
327
|
+
* with read_file_to_string, so the owner arrives double-quoted.
|
|
328
|
+
*/
|
|
323
329
|
function unquoteAtom(value) {
|
|
324
330
|
const trimmed = value.trim();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
.
|
|
331
|
-
.replace(/\\
|
|
331
|
+
const quote = trimmed[0];
|
|
332
|
+
if (trimmed.length >= 2 &&
|
|
333
|
+
(quote === "'" || quote === '"') &&
|
|
334
|
+
trimmed.endsWith(quote)) {
|
|
335
|
+
return trimmed
|
|
336
|
+
.slice(1, -1)
|
|
337
|
+
.replace(/\\(.)/gs, (_, char) => PROLOG_ESCAPES[char] ?? char);
|
|
332
338
|
}
|
|
333
339
|
return trimmed;
|
|
334
340
|
}
|
|
@@ -3,6 +3,17 @@ import type { PrologStoreLockOwner } from "./error-terms.js";
|
|
|
3
3
|
/** Linux boot id; guards against PID reuse across reboots. Null elsewhere. */
|
|
4
4
|
export declare function currentBootId(): string | null;
|
|
5
5
|
export declare function isProcessAlive(pid: number): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The process to stop for a store lock holder. The lock records the engine's
|
|
8
|
+
* SWI-Prolog child; its parent (the daemon, or another Kibi process hosting
|
|
9
|
+
* the session) is what a user ends or stops.
|
|
10
|
+
*/
|
|
11
|
+
export declare function lockHolderProcess(pid: number): {
|
|
12
|
+
pid: number;
|
|
13
|
+
prologPid?: number;
|
|
14
|
+
};
|
|
15
|
+
/** "pid N", naming the owning Kibi process when N is its SWI-Prolog child. */
|
|
16
|
+
export declare function describeLockHolderPid(pid: number): string;
|
|
6
17
|
export type StoreLockHolderState = "self" | "alive" | "dead" | "unknown";
|
|
7
18
|
export declare function classifyStoreLockHolder(owner: PrologStoreLockOwner | null): StoreLockHolderState;
|
|
8
19
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-lock.d.ts","sourceRoot":"","sources":["../../src/prolog/store-lock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store-lock.d.ts","sourceRoot":"","sources":["../../src/prolog/store-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { existsSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
19
20
|
import { readFileSync } from "node:fs";
|
|
20
21
|
import { basename, dirname, join } from "node:path";
|
|
@@ -61,6 +62,52 @@ export function isProcessAlive(pid) {
|
|
|
61
62
|
return code === "EPERM";
|
|
62
63
|
}
|
|
63
64
|
}
|
|
65
|
+
function processInfo(pid) {
|
|
66
|
+
try {
|
|
67
|
+
if (process.platform === "linux") {
|
|
68
|
+
// `comm` is in parentheses; the fields after it are state, ppid, ...
|
|
69
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
70
|
+
const open = stat.indexOf("(");
|
|
71
|
+
const close = stat.lastIndexOf(")");
|
|
72
|
+
return {
|
|
73
|
+
command: stat.slice(open + 1, close),
|
|
74
|
+
parent: Number(stat.slice(close + 2).split(" ")[1]),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (process.platform === "win32")
|
|
78
|
+
return null;
|
|
79
|
+
const [parent, ...command] = execFileSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 })
|
|
80
|
+
.trim()
|
|
81
|
+
.split(/\s+/);
|
|
82
|
+
return { parent: Number(parent), command: basename(command.join(" ")) };
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The process to stop for a store lock holder. The lock records the engine's
|
|
90
|
+
* SWI-Prolog child; its parent (the daemon, or another Kibi process hosting
|
|
91
|
+
* the session) is what a user ends or stops.
|
|
92
|
+
*/
|
|
93
|
+
// implements REQ-core-journaled-engine-persistence
|
|
94
|
+
export function lockHolderProcess(pid) {
|
|
95
|
+
const info = processInfo(pid);
|
|
96
|
+
if (info === null ||
|
|
97
|
+
!/^swipl/.test(info.command) ||
|
|
98
|
+
!Number.isInteger(info.parent) ||
|
|
99
|
+
info.parent <= 1)
|
|
100
|
+
return { pid };
|
|
101
|
+
return { pid: info.parent, prologPid: pid };
|
|
102
|
+
}
|
|
103
|
+
/** "pid N", naming the owning Kibi process when N is its SWI-Prolog child. */
|
|
104
|
+
// implements REQ-core-journaled-engine-persistence
|
|
105
|
+
export function describeLockHolderPid(pid) {
|
|
106
|
+
const holder = lockHolderProcess(pid);
|
|
107
|
+
return holder.prologPid === undefined
|
|
108
|
+
? `pid ${holder.pid}`
|
|
109
|
+
: `pid ${holder.pid} (through its SWI-Prolog process, pid ${holder.prologPid})`;
|
|
110
|
+
}
|
|
64
111
|
export function classifyStoreLockHolder(owner) {
|
|
65
112
|
if (owner === null || typeof owner.pid !== "number")
|
|
66
113
|
return "unknown";
|
|
@@ -356,7 +403,7 @@ export async function retryAttachAfterBreakingStaleLock(prolog, kbPath, failed)
|
|
|
356
403
|
if (decision.action !== "break") {
|
|
357
404
|
const holderSummary = storeLocked.owner === null
|
|
358
405
|
? decision.reason
|
|
359
|
-
:
|
|
406
|
+
: `${storeLocked.owner.pid === undefined ? "pid ?" : describeLockHolderPid(storeLocked.owner.pid)} (${decision.reason})`;
|
|
360
407
|
return {
|
|
361
408
|
...failed,
|
|
362
409
|
error: `${failed.error}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the commit being made replaces a root commit (`git commit --amend`
|
|
3
|
+
* on a branch with one commit). Such a commit has no parent either, so the
|
|
4
|
+
* staged gate treats it as a first commit.
|
|
5
|
+
*/
|
|
6
|
+
export declare function amendsRootCommit(workspaceRoot: string, headCommit: string | null): boolean;
|
|
7
|
+
//# sourceMappingURL=commit-amend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-amend.d.ts","sourceRoot":"","sources":["../../src/traceability/commit-amend.ts"],"names":[],"mappings":"AAsEA;;;;GAIG;AAEH,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,OAAO,CAmBT"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { basename } from "node:path";
|
|
21
|
+
/** How far up the process tree to look for the `git commit` running a hook. */
|
|
22
|
+
const MAX_ANCESTORS = 8;
|
|
23
|
+
function processArguments(pid) {
|
|
24
|
+
try {
|
|
25
|
+
if (process.platform === "linux") {
|
|
26
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
27
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
28
|
+
const argv = readFileSync(`/proc/${pid}/cmdline`, "utf8")
|
|
29
|
+
.split("\0")
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
return { parent, argv };
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === "win32")
|
|
34
|
+
return null;
|
|
35
|
+
const [parent, ...argv] = execFileSync("ps", ["-o", "ppid=,args=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 })
|
|
36
|
+
.trim()
|
|
37
|
+
.split(/\s+/);
|
|
38
|
+
return { parent: Number(parent), argv };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether this process runs under `git commit --amend`. Git does not tell a
|
|
46
|
+
* pre-commit hook that it is amending, so the hook's ancestors are read.
|
|
47
|
+
*/
|
|
48
|
+
function runsUnderCommitAmend() {
|
|
49
|
+
let pid = process.ppid;
|
|
50
|
+
for (let depth = 0; depth < MAX_ANCESTORS && pid > 1; depth += 1) {
|
|
51
|
+
const info = processArguments(pid);
|
|
52
|
+
if (info === null)
|
|
53
|
+
return false;
|
|
54
|
+
const [program, ...rest] = info.argv;
|
|
55
|
+
if (program !== undefined && /^git(\.exe)?$/.test(basename(program)))
|
|
56
|
+
return rest.includes("commit") && rest.includes("--amend");
|
|
57
|
+
pid = info.parent;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether the commit being made replaces a root commit (`git commit --amend`
|
|
63
|
+
* on a branch with one commit). Such a commit has no parent either, so the
|
|
64
|
+
* staged gate treats it as a first commit.
|
|
65
|
+
*/
|
|
66
|
+
// implements REQ-kibi-proposition-complete-ingestion
|
|
67
|
+
export function amendsRootCommit(workspaceRoot, headCommit) {
|
|
68
|
+
if (headCommit === null)
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const parents = execFileSync("git", ["rev-list", "--parents", "-n", "1", headCommit], {
|
|
72
|
+
cwd: workspaceRoot,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
75
|
+
})
|
|
76
|
+
.trim()
|
|
77
|
+
.split(/\s+/);
|
|
78
|
+
if (parents.length > 1)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return runsUnderCommitAmend();
|
|
85
|
+
}
|
|
@@ -13,12 +13,22 @@ export type StagedInventoryDriftViolation = Readonly<{
|
|
|
13
13
|
* or in the base tree) whose inventory no longer matches their prose. The
|
|
14
14
|
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
15
15
|
* the gate applies the same test, so drift is caught at the commit that
|
|
16
|
-
* introduces it instead of after a later merge.
|
|
17
|
-
*
|
|
16
|
+
* introduces it instead of after a later merge. A requirement the base tree
|
|
17
|
+
* does not have yet is judged too, as sync judges a source it has never
|
|
18
|
+
* imported, unless the commit has no parent (a first commit, which sync
|
|
19
|
+
* imports as its semantic baseline). So is one whose statement (the prose
|
|
20
|
+
* its inventory covers) differs from the base tree's, as sync judges a
|
|
21
|
+
* source whose semantic hash changed. Other requirements without a contract,
|
|
22
|
+
* such as a Context-only edit, are left to sync's baseline.
|
|
18
23
|
*/
|
|
19
24
|
export declare function stagedInventoryDriftViolations(input: {
|
|
20
25
|
stagedPaths: ReadonlySet<string>;
|
|
21
26
|
snapshotEntities: readonly ExtractionResult[];
|
|
22
27
|
readBase: (path: string) => string | null;
|
|
28
|
+
/**
|
|
29
|
+
* False for a first commit, or an amend of it: sync grandfathers its sources
|
|
30
|
+
* as a baseline, so only declared contracts are judged.
|
|
31
|
+
*/
|
|
32
|
+
judgeNewRequirements?: boolean;
|
|
23
33
|
}): StagedInventoryDriftViolation[];
|
|
24
34
|
//# sourceMappingURL=staged-inventory-drift.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staged-inventory-drift.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-inventory-drift.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,iFAAiF;AAEjF,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAGtE,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,IAAI,EAAE,OAAO,2BAA2B,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"staged-inventory-drift.d.ts","sourceRoot":"","sources":["../../src/traceability/staged-inventory-drift.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,iFAAiF;AAEjF,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAGtE,MAAM,MAAM,6BAA6B,GAAG,QAAQ,CAAC;IACnD,IAAI,EAAE,OAAO,2BAA2B,CAAC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC,CAAC;AA0CH;;;;;;;;;;;;GAYG;AAEH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACpD,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,gBAAgB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,6BAA6B,EAAE,CAsClC"}
|
|
@@ -34,6 +34,11 @@ function payloadOf(result) {
|
|
|
34
34
|
relationships: result.relationships,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
/** The hash of the prose a requirement's inventory must cover. */
|
|
38
|
+
function semanticSourceHashOf(result) {
|
|
39
|
+
const payload = payloadOf(result);
|
|
40
|
+
return validateSemanticInventoryBoundary(payload, result.relationships, analyzeSemanticAdvisorInput({ payload }).receipt).sourceHash;
|
|
41
|
+
}
|
|
37
42
|
function baseResult(path, readBase) {
|
|
38
43
|
const content = readBase(path);
|
|
39
44
|
if (content === null)
|
|
@@ -50,8 +55,13 @@ function baseResult(path, readBase) {
|
|
|
50
55
|
* or in the base tree) whose inventory no longer matches their prose. The
|
|
51
56
|
* next `kibi sync` refuses them under the proposition-complete boundary;
|
|
52
57
|
* the gate applies the same test, so drift is caught at the commit that
|
|
53
|
-
* introduces it instead of after a later merge.
|
|
54
|
-
*
|
|
58
|
+
* introduces it instead of after a later merge. A requirement the base tree
|
|
59
|
+
* does not have yet is judged too, as sync judges a source it has never
|
|
60
|
+
* imported, unless the commit has no parent (a first commit, which sync
|
|
61
|
+
* imports as its semantic baseline). So is one whose statement (the prose
|
|
62
|
+
* its inventory covers) differs from the base tree's, as sync judges a
|
|
63
|
+
* source whose semantic hash changed. Other requirements without a contract,
|
|
64
|
+
* such as a Context-only edit, are left to sync's baseline.
|
|
55
65
|
*/
|
|
56
66
|
// implements REQ-kibi-proposition-complete-ingestion
|
|
57
67
|
export function stagedInventoryDriftViolations(input) {
|
|
@@ -68,16 +78,23 @@ export function stagedInventoryDriftViolations(input) {
|
|
|
68
78
|
if (boundary.errors.length === 0)
|
|
69
79
|
continue;
|
|
70
80
|
const base = baseResult(source, input.readBase);
|
|
71
|
-
const
|
|
81
|
+
const contract = advertisesContract(result.entity) ||
|
|
72
82
|
(base !== null && advertisesContract(base.entity));
|
|
73
|
-
|
|
83
|
+
const judgeUncontracted = input.judgeNewRequirements ?? true;
|
|
84
|
+
const isNew = input.readBase(source) === null && judgeUncontracted;
|
|
85
|
+
const restated = judgeUncontracted &&
|
|
86
|
+
base !== null &&
|
|
87
|
+
semanticSourceHashOf(base) !== boundary.sourceHash;
|
|
88
|
+
if (!contract && !isNew && !restated)
|
|
74
89
|
continue;
|
|
75
90
|
violations.push({
|
|
76
91
|
rule: STAGED_INVENTORY_DRIFT_RULE,
|
|
77
92
|
entityId: result.entity.id,
|
|
78
93
|
source,
|
|
79
94
|
errors: boundary.errors,
|
|
80
|
-
message:
|
|
95
|
+
message: contract
|
|
96
|
+
? `${result.entity.id} (${source}): the semantic inventory does not match the requirement's prose, so the next kibi sync rejects it: ${boundary.errors.join("; ")}. Run 'kibi migrate' to re-derive it, or kb_model mode analyze with the complete prose and keep its inventory contract.`
|
|
97
|
+
: `${result.entity.id} (${source}): ${isNew ? "a new requirement" : "a requirement whose statement changes"} needs a semantic inventory that matches its prose, so the next kibi sync rejects it: ${boundary.errors.join("; ")}. Run kb_model mode analyze (CLI: semantic-advisor) with its id and complete semantic_text, then write the returned inventory contract into it (kb_upsert with semantic_inventory, semantic_inventory_version, semantic_source_field, semantic_source_hash and logic_claims).`,
|
|
81
98
|
});
|
|
82
99
|
}
|
|
83
100
|
return violations.sort((left, right) => left.entityId.localeCompare(right.entityId));
|