@workflow-cannon/workspace-kit 0.4.0 → 0.5.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/README.md +1 -1
- package/dist/cli.js +10 -6
- package/dist/core/config-cli.d.ts +6 -0
- package/dist/core/config-cli.js +479 -0
- package/dist/core/config-metadata.d.ts +35 -0
- package/dist/core/config-metadata.js +162 -0
- package/dist/core/config-mutations.d.ts +18 -0
- package/dist/core/config-mutations.js +32 -0
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.js +7 -2
- package/dist/core/lineage-contract.d.ts +59 -0
- package/dist/core/lineage-contract.js +9 -0
- package/dist/core/lineage-store.d.ts +16 -0
- package/dist/core/lineage-store.js +74 -0
- package/dist/core/policy.d.ts +12 -2
- package/dist/core/policy.js +37 -2
- package/dist/core/workspace-kit-config.d.ts +9 -1
- package/dist/core/workspace-kit-config.js +52 -4
- package/dist/modules/approvals/decisions-store.d.ts +20 -0
- package/dist/modules/approvals/decisions-store.js +52 -0
- package/dist/modules/approvals/index.js +57 -1
- package/dist/modules/approvals/review-runtime.d.ts +24 -0
- package/dist/modules/approvals/review-runtime.js +146 -0
- package/dist/modules/improvement/confidence.d.ts +23 -0
- package/dist/modules/improvement/confidence.js +50 -0
- package/dist/modules/improvement/generate-recommendations-runtime.d.ts +13 -0
- package/dist/modules/improvement/generate-recommendations-runtime.js +92 -0
- package/dist/modules/improvement/improvement-state.d.ts +11 -0
- package/dist/modules/improvement/improvement-state.js +42 -0
- package/dist/modules/improvement/index.js +47 -1
- package/dist/modules/improvement/ingest.d.ts +18 -0
- package/dist/modules/improvement/ingest.js +223 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/task-engine/index.js +1 -1
- package/dist/modules/task-engine/transitions.js +1 -0
- package/dist/modules/workspace-config/index.js +37 -9
- package/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical metadata for user-facing workspace config keys (Phase 2b).
|
|
3
|
+
* CLI, explain, and generated docs consume this registry.
|
|
4
|
+
*/
|
|
5
|
+
const REGISTRY = {
|
|
6
|
+
"tasks.storeRelativePath": {
|
|
7
|
+
key: "tasks.storeRelativePath",
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "Relative path (from workspace root) to the task engine JSON state file.",
|
|
10
|
+
default: ".workspace-kit/tasks/state.json",
|
|
11
|
+
domainScope: "project",
|
|
12
|
+
owningModule: "task-engine",
|
|
13
|
+
sensitive: false,
|
|
14
|
+
requiresRestart: false,
|
|
15
|
+
requiresApproval: false,
|
|
16
|
+
exposure: "public",
|
|
17
|
+
writableLayers: ["project", "user"]
|
|
18
|
+
},
|
|
19
|
+
"policy.extraSensitiveModuleCommands": {
|
|
20
|
+
key: "policy.extraSensitiveModuleCommands",
|
|
21
|
+
type: "array",
|
|
22
|
+
description: "Additional module command names (e.g. run subcommands) treated as sensitive for policy approval.",
|
|
23
|
+
default: [],
|
|
24
|
+
domainScope: "project",
|
|
25
|
+
owningModule: "workspace-kit",
|
|
26
|
+
sensitive: true,
|
|
27
|
+
requiresRestart: false,
|
|
28
|
+
requiresApproval: true,
|
|
29
|
+
exposure: "maintainer",
|
|
30
|
+
writableLayers: ["project"]
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
export function getConfigKeyMetadata(key) {
|
|
34
|
+
return REGISTRY[key];
|
|
35
|
+
}
|
|
36
|
+
export function listConfigMetadata(options) {
|
|
37
|
+
const exposure = options?.exposure ?? "public";
|
|
38
|
+
const order = ["public", "maintainer", "internal"];
|
|
39
|
+
const maxIdx = exposure === "all" ? 2 : order.indexOf(exposure);
|
|
40
|
+
const allowed = maxIdx < 0 ? new Set(["public"]) : new Set(order.slice(0, maxIdx + 1));
|
|
41
|
+
return Object.values(REGISTRY)
|
|
42
|
+
.filter((m) => allowed.has(m.exposure))
|
|
43
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
44
|
+
}
|
|
45
|
+
export function assertWritableKey(key) {
|
|
46
|
+
const meta = REGISTRY[key];
|
|
47
|
+
if (!meta) {
|
|
48
|
+
const err = new Error(`config-unknown-key: '${key}' is not a registered config key`);
|
|
49
|
+
err.code = "config-unknown-key";
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
if (meta.exposure === "internal") {
|
|
53
|
+
const err = new Error(`config-internal-key: '${key}' is internal and not user-writable`);
|
|
54
|
+
err.code = "config-internal-key";
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
return meta;
|
|
58
|
+
}
|
|
59
|
+
export function validateValueForMetadata(meta, value) {
|
|
60
|
+
if (meta.type === "array") {
|
|
61
|
+
if (!Array.isArray(value)) {
|
|
62
|
+
throw typeError(meta.key, "array", value);
|
|
63
|
+
}
|
|
64
|
+
if (meta.key === "policy.extraSensitiveModuleCommands") {
|
|
65
|
+
for (const item of value) {
|
|
66
|
+
if (typeof item !== "string" || item.trim().length === 0) {
|
|
67
|
+
throw new Error(`config-type-error(${meta.key}): array entries must be non-empty strings`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (meta.type === "string" && typeof value !== "string") {
|
|
74
|
+
throw typeError(meta.key, "string", value);
|
|
75
|
+
}
|
|
76
|
+
if (meta.type === "boolean" && typeof value !== "boolean") {
|
|
77
|
+
throw typeError(meta.key, "boolean", value);
|
|
78
|
+
}
|
|
79
|
+
if (meta.type === "number" && typeof value !== "number") {
|
|
80
|
+
throw typeError(meta.key, "number", value);
|
|
81
|
+
}
|
|
82
|
+
if (meta.type === "object") {
|
|
83
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
84
|
+
throw typeError(meta.key, "object", value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (meta.allowedValues && meta.allowedValues.length > 0) {
|
|
88
|
+
if (!meta.allowedValues.some((v) => deepEqualLoose(v, value))) {
|
|
89
|
+
throw new Error(`config-constraint(${meta.key}): value not in allowed set: ${JSON.stringify(meta.allowedValues)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function typeError(key, expected, got) {
|
|
94
|
+
return new Error(`config-type-error(${key}): expected ${expected}, got ${got === null ? "null" : typeof got}`);
|
|
95
|
+
}
|
|
96
|
+
function deepEqualLoose(a, b) {
|
|
97
|
+
if (a === b)
|
|
98
|
+
return true;
|
|
99
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validate top-level shape of persisted kit config files (strict unknown-key rejection).
|
|
103
|
+
*/
|
|
104
|
+
export function validatePersistedConfigDocument(data, label) {
|
|
105
|
+
const allowed = new Set(["schemaVersion", "core", "tasks", "documentation", "policy", "modules"]);
|
|
106
|
+
for (const k of Object.keys(data)) {
|
|
107
|
+
if (!allowed.has(k)) {
|
|
108
|
+
throw new Error(`config-invalid(${label}): unknown top-level key '${k}'`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (data.schemaVersion !== undefined && typeof data.schemaVersion !== "number") {
|
|
112
|
+
throw new Error(`config-invalid(${label}): schemaVersion must be a number`);
|
|
113
|
+
}
|
|
114
|
+
const tasks = data.tasks;
|
|
115
|
+
if (tasks !== undefined) {
|
|
116
|
+
if (typeof tasks !== "object" || tasks === null || Array.isArray(tasks)) {
|
|
117
|
+
throw new Error(`config-invalid(${label}): tasks must be an object`);
|
|
118
|
+
}
|
|
119
|
+
const t = tasks;
|
|
120
|
+
for (const k of Object.keys(t)) {
|
|
121
|
+
if (k !== "storeRelativePath") {
|
|
122
|
+
throw new Error(`config-invalid(${label}): unknown tasks.${k}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const policy = data.policy;
|
|
127
|
+
if (policy !== undefined) {
|
|
128
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
129
|
+
throw new Error(`config-invalid(${label}): policy must be an object`);
|
|
130
|
+
}
|
|
131
|
+
const p = policy;
|
|
132
|
+
for (const k of Object.keys(p)) {
|
|
133
|
+
if (k !== "extraSensitiveModuleCommands") {
|
|
134
|
+
throw new Error(`config-invalid(${label}): unknown policy.${k}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (p.extraSensitiveModuleCommands !== undefined) {
|
|
138
|
+
validateValueForMetadata(REGISTRY["policy.extraSensitiveModuleCommands"], p.extraSensitiveModuleCommands);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const core = data.core;
|
|
142
|
+
if (core !== undefined) {
|
|
143
|
+
if (typeof core !== "object" || core === null || Array.isArray(core) || Object.keys(core).length > 0) {
|
|
144
|
+
throw new Error(`config-invalid(${label}): core must be an empty object when present`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const doc = data.documentation;
|
|
148
|
+
if (doc !== undefined) {
|
|
149
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc) || Object.keys(doc).length > 0) {
|
|
150
|
+
throw new Error(`config-invalid(${label}): documentation must be an empty object when present`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const mods = data.modules;
|
|
154
|
+
if (mods !== undefined) {
|
|
155
|
+
if (typeof mods !== "object" || mods === null || Array.isArray(mods) || Object.keys(mods).length > 0) {
|
|
156
|
+
throw new Error(`config-invalid(${label}): modules must be absent or an empty object`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function getConfigRegistryExport() {
|
|
161
|
+
return REGISTRY;
|
|
162
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CONFIG_MUTATIONS_SCHEMA_VERSION: 1;
|
|
2
|
+
export type ConfigMutationRecord = {
|
|
3
|
+
schemaVersion: typeof CONFIG_MUTATIONS_SCHEMA_VERSION;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
actor: string;
|
|
6
|
+
key: string;
|
|
7
|
+
layer: "project" | "user";
|
|
8
|
+
operation: "set" | "unset";
|
|
9
|
+
ok: boolean;
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
previousSummary?: string;
|
|
13
|
+
nextSummary?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function appendConfigMutation(workspacePath: string, record: Omit<ConfigMutationRecord, "schemaVersion"> & {
|
|
16
|
+
schemaVersion?: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function summarizeForEvidence(key: string, sensitive: boolean, value: unknown): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const CONFIG_MUTATIONS_SCHEMA_VERSION = 1;
|
|
4
|
+
const CONFIG_DIR = ".workspace-kit/config";
|
|
5
|
+
const MUTATIONS_FILE = "mutations.jsonl";
|
|
6
|
+
function summarizeValue(key, value) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return "(unset)";
|
|
9
|
+
if (key === "policy.extraSensitiveModuleCommands" && Array.isArray(value)) {
|
|
10
|
+
return `array(len=${value.length})`;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return `string(len=${value.length})`;
|
|
14
|
+
}
|
|
15
|
+
return typeof value;
|
|
16
|
+
}
|
|
17
|
+
export async function appendConfigMutation(workspacePath, record) {
|
|
18
|
+
const dir = path.join(workspacePath, CONFIG_DIR);
|
|
19
|
+
const fp = path.join(dir, MUTATIONS_FILE);
|
|
20
|
+
const line = `${JSON.stringify({
|
|
21
|
+
schemaVersion: CONFIG_MUTATIONS_SCHEMA_VERSION,
|
|
22
|
+
...record
|
|
23
|
+
})}\n`;
|
|
24
|
+
await fs.mkdir(dir, { recursive: true });
|
|
25
|
+
await fs.appendFile(fp, line, "utf8");
|
|
26
|
+
}
|
|
27
|
+
export function summarizeForEvidence(key, sensitive, value) {
|
|
28
|
+
if (sensitive) {
|
|
29
|
+
return value === undefined ? "(unset)" : "(redacted)";
|
|
30
|
+
}
|
|
31
|
+
return summarizeValue(key, value);
|
|
32
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { ModuleRegistry, ModuleRegistryError, validateModuleSet, type ModuleRegistryOptions } from "./module-registry.js";
|
|
2
2
|
export { ModuleCommandRouter, ModuleCommandRouterError, type ModuleCommandDescriptor, type ModuleCommandRouterOptions } from "./module-command-router.js";
|
|
3
|
-
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, KIT_CONFIG_DEFAULTS, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, resolveWorkspaceConfigWithLayers, type ConfigLayer, type ConfigLayerId, type EffectiveWorkspaceConfig, type ExplainConfigResult, type ResolveWorkspaceConfigOptions } from "./workspace-kit-config.js";
|
|
4
|
-
export { appendPolicyTrace, getOperationIdForCommand, isSensitiveModuleCommand, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor, type PolicyOperationId, type PolicyTraceRecord } from "./policy.js";
|
|
3
|
+
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, getProjectConfigPath, getUserConfigFilePath, KIT_CONFIG_DEFAULTS, loadUserLayer, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, normalizeConfigForExport, PROJECT_CONFIG_REL, resolveWorkspaceConfigWithLayers, stableStringifyConfig, type ConfigLayer, type ConfigLayerId, type EffectiveWorkspaceConfig, type ExplainConfigResult, type ResolveWorkspaceConfigOptions } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getExtraSensitiveModuleCommandsFromEffective, getOperationIdForCommand, isSensitiveModuleCommand, isSensitiveModuleCommandForEffective, parsePolicyApproval, parsePolicyApprovalFromEnv, POLICY_TRACE_SCHEMA_VERSION, resolveActor, resolvePolicyOperationIdForCommand, type PolicyOperationId, type PolicyTraceRecord, type PolicyTraceRecordInput } from "./policy.js";
|
|
5
|
+
export { assertWritableKey, getConfigKeyMetadata, getConfigRegistryExport, listConfigMetadata, validatePersistedConfigDocument, validateValueForMetadata, type ConfigKeyExposure, type ConfigKeyMetadata, type ConfigValueType } from "./config-metadata.js";
|
|
6
|
+
export { appendConfigMutation, CONFIG_MUTATIONS_SCHEMA_VERSION, summarizeForEvidence, type ConfigMutationRecord } from "./config-mutations.js";
|
|
7
|
+
export { generateConfigReferenceDocs, runWorkspaceConfigCli, type ConfigCliIo } from "./config-cli.js";
|
|
8
|
+
export { LINEAGE_SCHEMA_VERSION, lineageCorrelationRoot, type LineageAppPayload, type LineageCorrPayload, type LineageDecPayload, type LineageEvent, type LineageEventType, type LineageRecPayload } from "./lineage-contract.js";
|
|
9
|
+
export { appendLineageEvent, newLineageEventId, queryLineageChain, readLineageEvents } from "./lineage-store.js";
|
|
5
10
|
export type CoreRuntimeVersion = "0.1";
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { ModuleRegistry, ModuleRegistryError, validateModuleSet } from "./module-registry.js";
|
|
2
2
|
export { ModuleCommandRouter, ModuleCommandRouterError } from "./module-command-router.js";
|
|
3
|
-
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, KIT_CONFIG_DEFAULTS, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, resolveWorkspaceConfigWithLayers } from "./workspace-kit-config.js";
|
|
4
|
-
export { appendPolicyTrace, getOperationIdForCommand, isSensitiveModuleCommand, parsePolicyApproval, parsePolicyApprovalFromEnv, resolveActor } from "./policy.js";
|
|
3
|
+
export { buildBaseConfigLayers, deepMerge, envToConfigOverlay, explainConfigPath, getAtPath, getProjectConfigPath, getUserConfigFilePath, KIT_CONFIG_DEFAULTS, loadUserLayer, mergeConfigLayers, MODULE_CONFIG_CONTRIBUTIONS, normalizeConfigForExport, PROJECT_CONFIG_REL, resolveWorkspaceConfigWithLayers, stableStringifyConfig } from "./workspace-kit-config.js";
|
|
4
|
+
export { appendPolicyTrace, getExtraSensitiveModuleCommandsFromEffective, getOperationIdForCommand, isSensitiveModuleCommand, isSensitiveModuleCommandForEffective, parsePolicyApproval, parsePolicyApprovalFromEnv, POLICY_TRACE_SCHEMA_VERSION, resolveActor, resolvePolicyOperationIdForCommand } from "./policy.js";
|
|
5
|
+
export { assertWritableKey, getConfigKeyMetadata, getConfigRegistryExport, listConfigMetadata, validatePersistedConfigDocument, validateValueForMetadata } from "./config-metadata.js";
|
|
6
|
+
export { appendConfigMutation, CONFIG_MUTATIONS_SCHEMA_VERSION, summarizeForEvidence } from "./config-mutations.js";
|
|
7
|
+
export { generateConfigReferenceDocs, runWorkspaceConfigCli } from "./config-cli.js";
|
|
8
|
+
export { LINEAGE_SCHEMA_VERSION, lineageCorrelationRoot } from "./lineage-contract.js";
|
|
9
|
+
export { appendLineageEvent, newLineageEventId, queryLineageChain, readLineageEvents } from "./lineage-store.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T203: Immutable lineage event contract (append-only store, correlation fields).
|
|
3
|
+
* @see docs/maintainers/TASKS.md T192, T203
|
|
4
|
+
*/
|
|
5
|
+
export declare const LINEAGE_SCHEMA_VERSION: 1;
|
|
6
|
+
export type LineageEventType = "rec" | "dec" | "app" | "corr";
|
|
7
|
+
/** Recommendation enqueued (Task Engine improvement task created). */
|
|
8
|
+
export type LineageRecPayload = {
|
|
9
|
+
recommendationTaskId: string;
|
|
10
|
+
evidenceKey: string;
|
|
11
|
+
title: string;
|
|
12
|
+
confidence: number;
|
|
13
|
+
confidenceTier: string;
|
|
14
|
+
provenanceRefs: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
/** Human approval decision recorded (before or alongside task transition). */
|
|
17
|
+
export type LineageDecPayload = {
|
|
18
|
+
recommendationTaskId: string;
|
|
19
|
+
evidenceKey: string;
|
|
20
|
+
decisionVerb: "accept" | "decline" | "accept_edited";
|
|
21
|
+
actor: string;
|
|
22
|
+
decisionFingerprint: string;
|
|
23
|
+
policyTraceRef?: {
|
|
24
|
+
operationId: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
};
|
|
27
|
+
/** Optional link to a config mutation evidence row when the reviewer supplies it. */
|
|
28
|
+
configMutationRef?: {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
key: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Applied change marker (task reached completed via acceptance). */
|
|
34
|
+
export type LineageAppPayload = {
|
|
35
|
+
recommendationTaskId: string;
|
|
36
|
+
evidenceKey: string;
|
|
37
|
+
decisionFingerprint: string;
|
|
38
|
+
finalTaskStatus: "completed";
|
|
39
|
+
};
|
|
40
|
+
/** Optional correlation enrichment (trace linkage). */
|
|
41
|
+
export type LineageCorrPayload = {
|
|
42
|
+
recommendationTaskId: string;
|
|
43
|
+
evidenceKey: string;
|
|
44
|
+
policySchemaVersion?: number;
|
|
45
|
+
policyOperationId?: string;
|
|
46
|
+
policyTimestamp?: string;
|
|
47
|
+
mutationRecordTimestamp?: string;
|
|
48
|
+
mutationKey?: string;
|
|
49
|
+
};
|
|
50
|
+
export type LineageEvent = {
|
|
51
|
+
schemaVersion: typeof LINEAGE_SCHEMA_VERSION;
|
|
52
|
+
eventId: string;
|
|
53
|
+
eventType: LineageEventType;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
correlationRoot: string;
|
|
56
|
+
payload: LineageRecPayload | LineageDecPayload | LineageAppPayload | LineageCorrPayload;
|
|
57
|
+
};
|
|
58
|
+
/** Stable correlation root: ties chain to recommendation + evidence identity. */
|
|
59
|
+
export declare function lineageCorrelationRoot(recommendationTaskId: string, evidenceKey: string): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T203: Immutable lineage event contract (append-only store, correlation fields).
|
|
3
|
+
* @see docs/maintainers/TASKS.md T192, T203
|
|
4
|
+
*/
|
|
5
|
+
export const LINEAGE_SCHEMA_VERSION = 1;
|
|
6
|
+
/** Stable correlation root: ties chain to recommendation + evidence identity. */
|
|
7
|
+
export function lineageCorrelationRoot(recommendationTaskId, evidenceKey) {
|
|
8
|
+
return `${recommendationTaskId}::${evidenceKey}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LineageEvent, LineageEventType } from "./lineage-contract.js";
|
|
2
|
+
export declare function newLineageEventId(): string;
|
|
3
|
+
export declare function appendLineageEvent(workspacePath: string, input: {
|
|
4
|
+
eventType: LineageEventType;
|
|
5
|
+
recommendationTaskId: string;
|
|
6
|
+
evidenceKey: string;
|
|
7
|
+
payload: LineageEvent["payload"];
|
|
8
|
+
eventId?: string;
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
}): Promise<LineageEvent>;
|
|
11
|
+
export declare function readLineageEvents(workspacePath: string): Promise<LineageEvent[]>;
|
|
12
|
+
/** Reconstruct rec → dec → app chain for a recommendation task id (deterministic sort by timestamp). */
|
|
13
|
+
export declare function queryLineageChain(workspacePath: string, recommendationTaskId: string): Promise<{
|
|
14
|
+
events: LineageEvent[];
|
|
15
|
+
byType: Record<LineageEventType, LineageEvent[]>;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { LINEAGE_SCHEMA_VERSION, lineageCorrelationRoot } from "./lineage-contract.js";
|
|
5
|
+
const LINEAGE_DIR = ".workspace-kit/lineage";
|
|
6
|
+
const EVENTS_FILE = "events.jsonl";
|
|
7
|
+
function eventsPath(workspacePath) {
|
|
8
|
+
return path.join(workspacePath, LINEAGE_DIR, EVENTS_FILE);
|
|
9
|
+
}
|
|
10
|
+
export function newLineageEventId() {
|
|
11
|
+
return `lev-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
12
|
+
}
|
|
13
|
+
export async function appendLineageEvent(workspacePath, input) {
|
|
14
|
+
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
15
|
+
const correlationRoot = lineageCorrelationRoot(input.recommendationTaskId, input.evidenceKey);
|
|
16
|
+
const event = {
|
|
17
|
+
schemaVersion: LINEAGE_SCHEMA_VERSION,
|
|
18
|
+
eventId: input.eventId ?? newLineageEventId(),
|
|
19
|
+
eventType: input.eventType,
|
|
20
|
+
timestamp,
|
|
21
|
+
correlationRoot,
|
|
22
|
+
payload: input.payload
|
|
23
|
+
};
|
|
24
|
+
const fp = eventsPath(workspacePath);
|
|
25
|
+
await fs.mkdir(path.dirname(fp), { recursive: true });
|
|
26
|
+
await fs.appendFile(fp, `${JSON.stringify(event)}\n`, "utf8");
|
|
27
|
+
return event;
|
|
28
|
+
}
|
|
29
|
+
export async function readLineageEvents(workspacePath) {
|
|
30
|
+
const fp = eventsPath(workspacePath);
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await fs.readFile(fp, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e.code === "ENOENT")
|
|
37
|
+
return [];
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const line of raw.split("\n")) {
|
|
42
|
+
const t = line.trim();
|
|
43
|
+
if (!t)
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const ev = JSON.parse(t);
|
|
47
|
+
if (ev.schemaVersion === LINEAGE_SCHEMA_VERSION)
|
|
48
|
+
out.push(ev);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* skip corrupt line */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Reconstruct rec → dec → app chain for a recommendation task id (deterministic sort by timestamp). */
|
|
57
|
+
export async function queryLineageChain(workspacePath, recommendationTaskId) {
|
|
58
|
+
const all = await readLineageEvents(workspacePath);
|
|
59
|
+
const chain = all.filter((e) => {
|
|
60
|
+
const p = e.payload;
|
|
61
|
+
return p.recommendationTaskId === recommendationTaskId;
|
|
62
|
+
});
|
|
63
|
+
chain.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
64
|
+
const byType = {
|
|
65
|
+
rec: [],
|
|
66
|
+
dec: [],
|
|
67
|
+
app: [],
|
|
68
|
+
corr: []
|
|
69
|
+
};
|
|
70
|
+
for (const e of chain) {
|
|
71
|
+
byType[e.eventType].push(e);
|
|
72
|
+
}
|
|
73
|
+
return { events: chain, byType };
|
|
74
|
+
}
|
package/dist/core/policy.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const POLICY_TRACE_SCHEMA_VERSION: 1;
|
|
2
|
+
export type PolicyOperationId = "cli.upgrade" | "cli.init" | "cli.config-mutate" | "policy.dynamic-sensitive" | "doc.document-project" | "doc.generate-document" | "tasks.import-tasks" | "tasks.generate-tasks-md" | "tasks.run-transition" | "approvals.review-item" | "improvement.generate-recommendations";
|
|
2
3
|
export declare function getOperationIdForCommand(commandName: string): PolicyOperationId | undefined;
|
|
4
|
+
export declare function getExtraSensitiveModuleCommandsFromEffective(effective: Record<string, unknown>): string[];
|
|
5
|
+
/** Resolve operation id for tracing, including config-declared sensitive module commands. */
|
|
6
|
+
export declare function resolvePolicyOperationIdForCommand(commandName: string, effective: Record<string, unknown>): PolicyOperationId | undefined;
|
|
3
7
|
/**
|
|
4
8
|
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
5
9
|
*/
|
|
@@ -12,6 +16,7 @@ export declare function parsePolicyApprovalFromEnv(env: NodeJS.ProcessEnv): Poli
|
|
|
12
16
|
export declare function parsePolicyApproval(args: Record<string, unknown>): PolicyApprovalPayload | undefined;
|
|
13
17
|
export declare function resolveActor(workspacePath: string, args: Record<string, unknown>, env: NodeJS.ProcessEnv): string;
|
|
14
18
|
export type PolicyTraceRecord = {
|
|
19
|
+
schemaVersion: number;
|
|
15
20
|
timestamp: string;
|
|
16
21
|
operationId: PolicyOperationId;
|
|
17
22
|
command: string;
|
|
@@ -21,4 +26,9 @@ export type PolicyTraceRecord = {
|
|
|
21
26
|
commandOk?: boolean;
|
|
22
27
|
message?: string;
|
|
23
28
|
};
|
|
24
|
-
|
|
29
|
+
/** Policy sensitivity from built-in map plus `policy.extraSensitiveModuleCommands` on effective config. */
|
|
30
|
+
export declare function isSensitiveModuleCommandForEffective(commandName: string, args: Record<string, unknown>, effective: Record<string, unknown>): boolean;
|
|
31
|
+
export type PolicyTraceRecordInput = Omit<PolicyTraceRecord, "schemaVersion"> & {
|
|
32
|
+
schemaVersion?: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function appendPolicyTrace(workspacePath: string, record: PolicyTraceRecordInput): Promise<void>;
|
package/dist/core/policy.js
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
export const POLICY_TRACE_SCHEMA_VERSION = 1;
|
|
4
5
|
const COMMAND_TO_OPERATION = {
|
|
5
6
|
"document-project": "doc.document-project",
|
|
6
7
|
"generate-document": "doc.generate-document",
|
|
7
8
|
"import-tasks": "tasks.import-tasks",
|
|
8
9
|
"generate-tasks-md": "tasks.generate-tasks-md",
|
|
9
|
-
"run-transition": "tasks.run-transition"
|
|
10
|
+
"run-transition": "tasks.run-transition",
|
|
11
|
+
"review-item": "approvals.review-item",
|
|
12
|
+
"generate-recommendations": "improvement.generate-recommendations"
|
|
10
13
|
};
|
|
11
14
|
export function getOperationIdForCommand(commandName) {
|
|
12
15
|
return COMMAND_TO_OPERATION[commandName];
|
|
13
16
|
}
|
|
17
|
+
export function getExtraSensitiveModuleCommandsFromEffective(effective) {
|
|
18
|
+
const policy = effective.policy;
|
|
19
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const raw = policy.extraSensitiveModuleCommands;
|
|
23
|
+
if (!Array.isArray(raw)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return raw.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
27
|
+
}
|
|
28
|
+
/** Resolve operation id for tracing, including config-declared sensitive module commands. */
|
|
29
|
+
export function resolvePolicyOperationIdForCommand(commandName, effective) {
|
|
30
|
+
const builtin = getOperationIdForCommand(commandName);
|
|
31
|
+
if (builtin)
|
|
32
|
+
return builtin;
|
|
33
|
+
if (getExtraSensitiveModuleCommandsFromEffective(effective).includes(commandName)) {
|
|
34
|
+
return "policy.dynamic-sensitive";
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
14
38
|
/**
|
|
15
39
|
* Sensitive when mutation / write is possible. Documentation commands are exempt when dryRun is true.
|
|
16
40
|
*/
|
|
@@ -91,12 +115,23 @@ export function resolveActor(workspacePath, args, env) {
|
|
|
91
115
|
}
|
|
92
116
|
return "unknown";
|
|
93
117
|
}
|
|
118
|
+
/** Policy sensitivity from built-in map plus `policy.extraSensitiveModuleCommands` on effective config. */
|
|
119
|
+
export function isSensitiveModuleCommandForEffective(commandName, args, effective) {
|
|
120
|
+
if (isSensitiveModuleCommand(commandName, args)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return getExtraSensitiveModuleCommandsFromEffective(effective).includes(commandName);
|
|
124
|
+
}
|
|
94
125
|
const POLICY_DIR = ".workspace-kit/policy";
|
|
95
126
|
const TRACE_FILE = "traces.jsonl";
|
|
96
127
|
export async function appendPolicyTrace(workspacePath, record) {
|
|
97
128
|
const dir = path.join(workspacePath, POLICY_DIR);
|
|
98
129
|
const fp = path.join(workspacePath, POLICY_DIR, TRACE_FILE);
|
|
99
|
-
const
|
|
130
|
+
const full = {
|
|
131
|
+
...record,
|
|
132
|
+
schemaVersion: record.schemaVersion ?? POLICY_TRACE_SCHEMA_VERSION
|
|
133
|
+
};
|
|
134
|
+
const line = `${JSON.stringify(full)}\n`;
|
|
100
135
|
await fs.mkdir(dir, { recursive: true });
|
|
101
136
|
await fs.appendFile(fp, line, "utf8");
|
|
102
137
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ConfigRegistryView } from "../contracts/module-contract.js";
|
|
2
|
-
export type ConfigLayerId = "kit-default" | `module:${string}` | "project" | "env" | "invocation";
|
|
2
|
+
export type ConfigLayerId = "kit-default" | `module:${string}` | "user" | "project" | "env" | "invocation";
|
|
3
3
|
export type ConfigLayer = {
|
|
4
4
|
id: ConfigLayerId;
|
|
5
5
|
data: Record<string, unknown>;
|
|
6
6
|
};
|
|
7
7
|
/** Effective workspace config: domain keys + optional modules map (project file). */
|
|
8
8
|
export type EffectiveWorkspaceConfig = Record<string, unknown>;
|
|
9
|
+
export declare const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
|
|
10
|
+
export declare function getProjectConfigPath(workspacePath: string): string;
|
|
9
11
|
/** Built-in defaults (lowest layer). */
|
|
10
12
|
export declare const KIT_CONFIG_DEFAULTS: Record<string, unknown>;
|
|
11
13
|
/**
|
|
@@ -16,6 +18,9 @@ export declare const MODULE_CONFIG_CONTRIBUTIONS: Record<string, Record<string,
|
|
|
16
18
|
export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
|
|
17
19
|
export declare function getAtPath(root: Record<string, unknown>, dotted: string): unknown;
|
|
18
20
|
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
21
|
+
/** Resolved home for user-level config (`~/.workspace-kit/config.json`). Override with `WORKSPACE_KIT_HOME` (tests). */
|
|
22
|
+
export declare function getUserConfigFilePath(): string;
|
|
23
|
+
export declare function loadUserLayer(): Promise<ConfigLayer>;
|
|
19
24
|
/**
|
|
20
25
|
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
21
26
|
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
@@ -37,6 +42,9 @@ export declare function resolveWorkspaceConfigWithLayers(options: ResolveWorkspa
|
|
|
37
42
|
effective: EffectiveWorkspaceConfig;
|
|
38
43
|
layers: ConfigLayer[];
|
|
39
44
|
}>;
|
|
45
|
+
export declare function normalizeConfigForExport(value: unknown): unknown;
|
|
46
|
+
/** Deterministic JSON for agents and tests (sorted keys, trailing newline). */
|
|
47
|
+
export declare function stableStringifyConfig(value: unknown): string;
|
|
40
48
|
export type ExplainConfigResult = {
|
|
41
49
|
path: string;
|
|
42
50
|
effectiveValue: unknown;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
|
|
5
|
+
import { validatePersistedConfigDocument } from "./config-metadata.js";
|
|
6
|
+
export const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
|
|
7
|
+
export function getProjectConfigPath(workspacePath) {
|
|
8
|
+
return path.join(workspacePath, PROJECT_CONFIG_REL);
|
|
9
|
+
}
|
|
5
10
|
/** Built-in defaults (lowest layer). */
|
|
6
11
|
export const KIT_CONFIG_DEFAULTS = {
|
|
7
12
|
core: {},
|
|
@@ -85,6 +90,25 @@ export function deepEqual(a, b) {
|
|
|
85
90
|
}
|
|
86
91
|
return true;
|
|
87
92
|
}
|
|
93
|
+
/** Resolved home for user-level config (`~/.workspace-kit/config.json`). Override with `WORKSPACE_KIT_HOME` (tests). */
|
|
94
|
+
export function getUserConfigFilePath() {
|
|
95
|
+
const home = process.env.WORKSPACE_KIT_HOME?.trim() || os.homedir();
|
|
96
|
+
return path.join(home, ".workspace-kit", "config.json");
|
|
97
|
+
}
|
|
98
|
+
async function readUserConfigFile() {
|
|
99
|
+
const fp = getUserConfigFilePath();
|
|
100
|
+
if (!existsSync(fp)) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
const raw = await fs.readFile(fp, "utf8");
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
106
|
+
throw new Error(`config-invalid(user): ${fp} must be a JSON object`);
|
|
107
|
+
}
|
|
108
|
+
const obj = parsed;
|
|
109
|
+
validatePersistedConfigDocument(obj, "user config");
|
|
110
|
+
return obj;
|
|
111
|
+
}
|
|
88
112
|
async function readProjectConfigFile(workspacePath) {
|
|
89
113
|
const fp = path.join(workspacePath, PROJECT_CONFIG_REL);
|
|
90
114
|
if (!existsSync(fp)) {
|
|
@@ -96,11 +120,13 @@ async function readProjectConfigFile(workspacePath) {
|
|
|
96
120
|
throw new Error("config-invalid: .workspace-kit/config.json must be a JSON object");
|
|
97
121
|
}
|
|
98
122
|
const obj = parsed;
|
|
99
|
-
|
|
100
|
-
throw new Error("config-invalid: schemaVersion must be a number when present");
|
|
101
|
-
}
|
|
123
|
+
validatePersistedConfigDocument(obj, ".workspace-kit/config.json");
|
|
102
124
|
return obj;
|
|
103
125
|
}
|
|
126
|
+
export async function loadUserLayer() {
|
|
127
|
+
const data = await readUserConfigFile();
|
|
128
|
+
return { id: "user", data };
|
|
129
|
+
}
|
|
104
130
|
/**
|
|
105
131
|
* Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
|
|
106
132
|
* Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
|
|
@@ -186,6 +212,7 @@ export function mergeConfigLayers(layers) {
|
|
|
186
212
|
export async function resolveWorkspaceConfigWithLayers(options) {
|
|
187
213
|
const { workspacePath, registry, env = process.env, invocationConfig } = options;
|
|
188
214
|
const layers = [...buildBaseConfigLayers(registry)];
|
|
215
|
+
layers.push(await loadUserLayer());
|
|
189
216
|
layers.push(await loadProjectLayer(workspacePath));
|
|
190
217
|
layers.push({ id: "env", data: envToConfigOverlay(env) });
|
|
191
218
|
if (invocationConfig && Object.keys(invocationConfig).length > 0) {
|
|
@@ -193,6 +220,27 @@ export async function resolveWorkspaceConfigWithLayers(options) {
|
|
|
193
220
|
}
|
|
194
221
|
return { effective: mergeConfigLayers(layers), layers };
|
|
195
222
|
}
|
|
223
|
+
export function normalizeConfigForExport(value) {
|
|
224
|
+
return sortKeysDeep(value);
|
|
225
|
+
}
|
|
226
|
+
function sortKeysDeep(value) {
|
|
227
|
+
if (value === null || typeof value !== "object") {
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.map(sortKeysDeep);
|
|
232
|
+
}
|
|
233
|
+
const o = value;
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const k of Object.keys(o).sort()) {
|
|
236
|
+
out[k] = sortKeysDeep(o[k]);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/** Deterministic JSON for agents and tests (sorted keys, trailing newline). */
|
|
241
|
+
export function stableStringifyConfig(value) {
|
|
242
|
+
return `${JSON.stringify(sortKeysDeep(value), null, 2)}\n`;
|
|
243
|
+
}
|
|
196
244
|
export function explainConfigPath(dottedPath, layers) {
|
|
197
245
|
const mergedFull = mergeConfigLayers(layers);
|
|
198
246
|
const effectiveValue = getAtPath(mergedFull, dottedPath);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const APPROVAL_DECISION_SCHEMA_VERSION: 1;
|
|
2
|
+
export type ApprovalDecisionRecord = {
|
|
3
|
+
schemaVersion: typeof APPROVAL_DECISION_SCHEMA_VERSION;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
taskId: string;
|
|
6
|
+
evidenceKey: string;
|
|
7
|
+
decisionVerb: "accept" | "decline" | "accept_edited";
|
|
8
|
+
actor: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
editedSummary?: string;
|
|
11
|
+
policyTraceRef?: {
|
|
12
|
+
operationId: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function computeDecisionFingerprint(taskId: string, decisionVerb: string, evidenceKey: string, editedSummary?: string): string;
|
|
17
|
+
export declare function readDecisionFingerprints(workspacePath: string): Promise<Set<string>>;
|
|
18
|
+
export declare function appendDecisionRecord(workspacePath: string, record: Omit<ApprovalDecisionRecord, "schemaVersion" | "timestamp"> & {
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
}): Promise<void>;
|