kibi-cli 2.28.0 → 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/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +6 -7
- 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/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +3 -5
- package/dist/extractors/symbol-coordinates.d.ts.map +1 -1
- package/dist/extractors/symbol-coordinates.js +3 -5
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +3 -4
- package/dist/operations/mutation/symbol-granularity.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-granularity.js +4 -4
- package/dist/operations/mutation/symbol-refresh.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-refresh.js +4 -3
- package/dist/operations/mutation/validation.js +11 -0
- package/dist/plugins/approved-source-analyzers.generated.d.ts +6 -6
- package/dist/plugins/approved-source-analyzers.generated.js +6 -6
- 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/dist/utils/repo-relative-path.d.ts +7 -0
- package/dist/utils/repo-relative-path.d.ts.map +1 -1
- package/dist/utils/repo-relative-path.js +26 -0
- package/package.json +4 -4
- package/schema/entities.pl +2 -1
- package/schema/validation.pl +15 -0
|
@@ -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));
|
|
@@ -15,4 +15,11 @@
|
|
|
15
15
|
* (the workspace snapshot), the caller compares it in NFC.
|
|
16
16
|
*/
|
|
17
17
|
export declare function normalizeRepoRelativePath(workspaceRoot: string, candidate: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a recorded source path to an absolute path that is lexically (and,
|
|
20
|
+
* when the file exists, via realpath) confined under `workspaceRoot`.
|
|
21
|
+
* Returns null for escapes, empty input, or symlink targets outside the root.
|
|
22
|
+
* Callers must treat null as "do not read".
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveWorkspaceSourceAbsolute(workspaceRoot: string, sourceFile: string): string | null;
|
|
18
25
|
//# sourceMappingURL=repo-relative-path.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo-relative-path.d.ts","sourceRoot":"","sources":["../../src/utils/repo-relative-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"repo-relative-path.d.ts","sourceRoot":"","sources":["../../src/utils/repo-relative-path.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;GAeG;AAEH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAsBf;AAED;;;;;GAKG;AAEH,wBAAgB,8BAA8B,CAC5C,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAcf"}
|
|
@@ -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 { realpathSync } from "node:fs";
|
|
18
19
|
import path from "node:path";
|
|
19
20
|
/**
|
|
20
21
|
* Canonical repository-relative form of a recorded path, as proof freshness
|
|
@@ -56,3 +57,28 @@ export function normalizeRepoRelativePath(workspaceRoot, candidate) {
|
|
|
56
57
|
}
|
|
57
58
|
return normalized;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a recorded source path to an absolute path that is lexically (and,
|
|
62
|
+
* when the file exists, via realpath) confined under `workspaceRoot`.
|
|
63
|
+
* Returns null for escapes, empty input, or symlink targets outside the root.
|
|
64
|
+
* Callers must treat null as "do not read".
|
|
65
|
+
*/
|
|
66
|
+
// implements REQ-symbol-sourcefile-workspace-confinement
|
|
67
|
+
export function resolveWorkspaceSourceAbsolute(workspaceRoot, sourceFile) {
|
|
68
|
+
const relative = normalizeRepoRelativePath(workspaceRoot, sourceFile);
|
|
69
|
+
if (relative === null)
|
|
70
|
+
return null;
|
|
71
|
+
const absolute = path.resolve(workspaceRoot, relative);
|
|
72
|
+
try {
|
|
73
|
+
const realRoot = realpathSync.native(path.resolve(workspaceRoot));
|
|
74
|
+
const realFile = realpathSync.native(absolute);
|
|
75
|
+
const confined = path.relative(realRoot, realFile);
|
|
76
|
+
if (confined.startsWith("..") || path.isAbsolute(confined))
|
|
77
|
+
return null;
|
|
78
|
+
return realFile;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Missing path: lexical confinement from normalizeRepoRelativePath is enough.
|
|
82
|
+
return absolute;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -235,13 +235,13 @@
|
|
|
235
235
|
"fast-glob": "^3.2.12",
|
|
236
236
|
"gray-matter": "^4.0.3",
|
|
237
237
|
"js-yaml": "^4.1.0",
|
|
238
|
-
"kibi-core": "^0.22.
|
|
239
|
-
"kibi-plugin-builtin": "^0.5.
|
|
238
|
+
"kibi-core": "^0.22.4",
|
|
239
|
+
"kibi-plugin-builtin": "^0.5.3",
|
|
240
240
|
"kibi-plugin-sdk": "^0.5.0",
|
|
241
241
|
"kibi-swipl": "^1.0.1",
|
|
242
242
|
"saxes": "^6.0.0",
|
|
243
243
|
"ignore": "^5.3.0",
|
|
244
|
-
"yaml": "^2.9.
|
|
244
|
+
"yaml": "^2.9.1"
|
|
245
245
|
},
|
|
246
246
|
"devDependencies": {
|
|
247
247
|
"@types/node": "latest",
|
package/schema/entities.pl
CHANGED
|
@@ -29,7 +29,8 @@ entity_property(_, priority, atom).
|
|
|
29
29
|
entity_property(_, severity, atom).
|
|
30
30
|
entity_property(_, links, list).
|
|
31
31
|
entity_property(_, text_ref, uri).
|
|
32
|
-
|
|
32
|
+
% Workspace-relative source paths only (no absolute / .. escapes).
|
|
33
|
+
entity_property(_, sourceFile, workspace_relpath).
|
|
33
34
|
% Provenance of any authored entity: a JSON object (kind, ref, approved_by,
|
|
34
35
|
% recorded_at) naming who authored it and on what authority.
|
|
35
36
|
entity_property(_, origin, list_or_json).
|
package/schema/validation.pl
CHANGED
|
@@ -50,11 +50,26 @@ check_kind(datetime, V) :- string(V). % accept ISO strings for now
|
|
|
50
50
|
check_kind(list, V) :- is_list(V).
|
|
51
51
|
check_kind(list_or_json, V) :- is_list(V) ; string(V) ; atom(V).
|
|
52
52
|
check_kind(uri, V) :- string(V).
|
|
53
|
+
% Non-empty workspace-relative path: reject absolute forms and `..` segments.
|
|
54
|
+
check_kind(workspace_relpath, V) :-
|
|
55
|
+
string(V),
|
|
56
|
+
string_length(V, Len),
|
|
57
|
+
Len > 0,
|
|
58
|
+
\+ workspace_relpath_absolute(V),
|
|
59
|
+
split_string(V, "/\\", "", Parts),
|
|
60
|
+
\+ member("..", Parts).
|
|
53
61
|
check_kind(integer, V) :- integer(V).
|
|
54
62
|
check_kind(number, V) :- number(V).
|
|
55
63
|
check_kind(boolean, true).
|
|
56
64
|
check_kind(boolean, false).
|
|
57
65
|
|
|
66
|
+
workspace_relpath_absolute(V) :- sub_string(V, 0, 1, _, "/").
|
|
67
|
+
workspace_relpath_absolute(V) :-
|
|
68
|
+
string_length(V, Len),
|
|
69
|
+
Len >= 2,
|
|
70
|
+
sub_string(V, 1, 1, _, ":").
|
|
71
|
+
workspace_relpath_absolute(V) :- sub_string(V, 0, 2, _, "\\\\").
|
|
72
|
+
|
|
58
73
|
% Fallback false
|
|
59
74
|
check_kind(_, _) :- fail.
|
|
60
75
|
|