@remnic/core 9.3.670 → 9.3.671
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/dist/access-cli.js +16 -16
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +8 -8
- package/dist/{access-service-BCuaiNHa.d.ts → access-service-S9oGKPZc.d.ts} +2 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +7 -7
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-MPXYHC35.js → chunk-32RD3GIW.js} +17 -17
- package/dist/{chunk-BQJUPECT.js → chunk-3OKWZT7F.js} +2 -2
- package/dist/{chunk-2MXEVL75.js → chunk-6VP3YUCS.js} +2 -2
- package/dist/{chunk-WXXLSZHA.js → chunk-ATRB6Q25.js} +2 -2
- package/dist/{chunk-KOXGLQS7.js → chunk-B6IUW76R.js} +2 -2
- package/dist/{chunk-QHWJG5C5.js → chunk-CPVV2UEL.js} +5 -5
- package/dist/{chunk-AARDBQTA.js → chunk-CTCPB57O.js} +2 -2
- package/dist/{chunk-WKMCC4NQ.js → chunk-EUM7CZFM.js} +2 -2
- package/dist/{chunk-46WUVFOD.js → chunk-F7OWUP3G.js} +4 -4
- package/dist/{chunk-AZBV4RRY.js → chunk-FMSDA2D3.js} +1 -1
- package/dist/chunk-FMSDA2D3.js.map +1 -0
- package/dist/{chunk-SXYCVRLK.js → chunk-GSTYVG5L.js} +3 -3
- package/dist/{chunk-XMWF6AU3.js → chunk-LVTTO3VC.js} +2 -2
- package/dist/{chunk-4T7P2HLJ.js → chunk-LXVOZ2O6.js} +2 -2
- package/dist/{chunk-TJ7HH5LB.js → chunk-LZSMQHXC.js} +2 -2
- package/dist/{chunk-CTAV55JM.js → chunk-MLVMBV2C.js} +75 -1
- package/dist/chunk-MLVMBV2C.js.map +1 -0
- package/dist/{chunk-QZ7ODIVL.js → chunk-MNUPGYIV.js} +2 -2
- package/dist/{chunk-MR4PJ277.js → chunk-MTJ2LFAJ.js} +2 -2
- package/dist/{chunk-2TCHDANJ.js → chunk-NXBXM7Q6.js} +2 -2
- package/dist/{chunk-TIJYQXDI.js → chunk-OHX52AOS.js} +2 -2
- package/dist/{chunk-PEPHBH2W.js → chunk-PYTATYUV.js} +2 -2
- package/dist/{chunk-UVUTV7CM.js → chunk-TGN4M5MB.js} +7 -7
- package/dist/{chunk-JTPXSXHC.js → chunk-V4ZHKCGA.js} +2 -2
- package/dist/{chunk-OI4BXFSB.js → chunk-VL5JJOOY.js} +2 -2
- package/dist/{chunk-Q2LQZYQ7.js → chunk-Z4GALEO3.js} +3 -3
- package/dist/{chunk-A5TEHAR4.js → chunk-ZCVPFDHB.js} +3 -3
- package/dist/{chunk-PIA4C3AJ.js → chunk-ZI6A7X4V.js} +11 -11
- package/dist/{cli-C98xlwYA.d.ts → cli-B2Ve7R22.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +21 -21
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +26 -26
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +7 -7
- package/dist/{orchestrator-DyP9QYsh.d.ts → orchestrator-7zPqGupX.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +12 -12
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BICZvQ3C.d.ts → semantic-consolidation-BX9Z9_aK.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-D96bCB3C.d.ts → types-D3pm4NhH.d.ts} +30 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/coding/coding-knowledge-config.ts +111 -0
- package/src/coding/decision-records.test.ts +434 -0
- package/src/coding/decision-records.ts +507 -0
- package/src/config.test.ts +118 -0
- package/src/config.ts +3 -2
- package/src/types.ts +35 -0
- package/dist/chunk-AZBV4RRY.js.map +0 -1
- package/dist/chunk-CTAV55JM.js.map +0 -1
- /package/dist/{chunk-MPXYHC35.js.map → chunk-32RD3GIW.js.map} +0 -0
- /package/dist/{chunk-BQJUPECT.js.map → chunk-3OKWZT7F.js.map} +0 -0
- /package/dist/{chunk-2MXEVL75.js.map → chunk-6VP3YUCS.js.map} +0 -0
- /package/dist/{chunk-WXXLSZHA.js.map → chunk-ATRB6Q25.js.map} +0 -0
- /package/dist/{chunk-KOXGLQS7.js.map → chunk-B6IUW76R.js.map} +0 -0
- /package/dist/{chunk-QHWJG5C5.js.map → chunk-CPVV2UEL.js.map} +0 -0
- /package/dist/{chunk-AARDBQTA.js.map → chunk-CTCPB57O.js.map} +0 -0
- /package/dist/{chunk-WKMCC4NQ.js.map → chunk-EUM7CZFM.js.map} +0 -0
- /package/dist/{chunk-46WUVFOD.js.map → chunk-F7OWUP3G.js.map} +0 -0
- /package/dist/{chunk-SXYCVRLK.js.map → chunk-GSTYVG5L.js.map} +0 -0
- /package/dist/{chunk-XMWF6AU3.js.map → chunk-LVTTO3VC.js.map} +0 -0
- /package/dist/{chunk-4T7P2HLJ.js.map → chunk-LXVOZ2O6.js.map} +0 -0
- /package/dist/{chunk-TJ7HH5LB.js.map → chunk-LZSMQHXC.js.map} +0 -0
- /package/dist/{chunk-QZ7ODIVL.js.map → chunk-MNUPGYIV.js.map} +0 -0
- /package/dist/{chunk-MR4PJ277.js.map → chunk-MTJ2LFAJ.js.map} +0 -0
- /package/dist/{chunk-2TCHDANJ.js.map → chunk-NXBXM7Q6.js.map} +0 -0
- /package/dist/{chunk-TIJYQXDI.js.map → chunk-OHX52AOS.js.map} +0 -0
- /package/dist/{chunk-PEPHBH2W.js.map → chunk-PYTATYUV.js.map} +0 -0
- /package/dist/{chunk-UVUTV7CM.js.map → chunk-TGN4M5MB.js.map} +0 -0
- /package/dist/{chunk-JTPXSXHC.js.map → chunk-V4ZHKCGA.js.map} +0 -0
- /package/dist/{chunk-OI4BXFSB.js.map → chunk-VL5JJOOY.js.map} +0 -0
- /package/dist/{chunk-Q2LQZYQ7.js.map → chunk-Z4GALEO3.js.map} +0 -0
- /package/dist/{chunk-A5TEHAR4.js.map → chunk-ZCVPFDHB.js.map} +0 -0
- /package/dist/{chunk-PIA4C3AJ.js.map → chunk-ZI6A7X4V.js.map} +0 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision records — pure storage contract (issue #1548 Track A PR 1).
|
|
3
|
+
*
|
|
4
|
+
* The four-memory-shape memory layer stores architectural decisions as
|
|
5
|
+
* markdown files with YAML frontmatter under the coding namespace. This
|
|
6
|
+
* module is the **pure contract**: data shape, parse, serialise, validate,
|
|
7
|
+
* and the supersede mutation. No filesystem, no orchestrator — callers
|
|
8
|
+
* (PR 2's MCP/HTTP/CLI surfaces and the orchestrator's normal persist
|
|
9
|
+
* pipeline) hang these helpers onto real storage.
|
|
10
|
+
*
|
|
11
|
+
* Why markdown + frontmatter?
|
|
12
|
+
* - QMD searches the body for free (rule 43 — storage chokepoint means the
|
|
13
|
+
* orchestrator persist pipeline fires for catalog + reindex + dedup).
|
|
14
|
+
* - Human-reviewable diff via the same tooling as any other memory page.
|
|
15
|
+
* - The body carries prose; the frontmatter carries the searchable,
|
|
16
|
+
* structured fields.
|
|
17
|
+
*
|
|
18
|
+
* Why this parser/serialiser and not a YAML dep?
|
|
19
|
+
* - The surface used here is intentionally narrow: scalar strings and
|
|
20
|
+
* flow-style arrays of strings. A full YAML dependency is heavier than
|
|
21
|
+
* the parser we need and would unlock a footgun (block-scalar
|
|
22
|
+
* representations, anchors, multiple-document streams) we deliberately
|
|
23
|
+
* want to be impossible.
|
|
24
|
+
*
|
|
25
|
+
* Supersede ordering (rule 25): the replacement record MUST land on disk
|
|
26
|
+
* BEFORE the superseded record's `status` flips, so a process crash between
|
|
27
|
+
* the two writes leaves the new decision discoverable rather than nothing.
|
|
28
|
+
*
|
|
29
|
+
* Pure module — type-only import of the plugin config interface keeps this
|
|
30
|
+
* dependency-free (the entry types live in `../types.ts`).
|
|
31
|
+
*/
|
|
32
|
+
import type { CodingKnowledgeConfig } from "../types.js";
|
|
33
|
+
|
|
34
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Public types
|
|
36
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export type DecisionStatus = "proposed" | "accepted" | "superseded" | "rejected";
|
|
39
|
+
|
|
40
|
+
export const DECISION_STATUSES = [
|
|
41
|
+
"proposed",
|
|
42
|
+
"accepted",
|
|
43
|
+
"superseded",
|
|
44
|
+
"rejected",
|
|
45
|
+
] as const satisfies readonly DecisionStatus[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The "active" statuses callers surface in standing-decisions lists, briefing
|
|
49
|
+
* text, and the search-by-default index. Exported as a frozen set so the
|
|
50
|
+
* decision-records tests, the briefing helper, and the future list surface
|
|
51
|
+
* share one declaration (rule 53 — single source of truth for classification).
|
|
52
|
+
*
|
|
53
|
+
* Stand-up note: superseded + rejected are intentionally excluded. A supersede
|
|
54
|
+
* edge (`b.supersedes = "a"`) tells callers what to fall back to.
|
|
55
|
+
*/
|
|
56
|
+
export const ACTIVE_DECISION_STATUSES: ReadonlySet<DecisionStatus> = new Set<DecisionStatus>([
|
|
57
|
+
"proposed",
|
|
58
|
+
"accepted",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Least-privileged default for a parsed record whose frontmatter omits
|
|
63
|
+
* `status` (rule 48). "Proposed" — never "accepted" — because accepting a
|
|
64
|
+
* decision is a deliberate operator action.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_DECISION_STATUS: DecisionStatus = "proposed";
|
|
67
|
+
|
|
68
|
+
export interface DecisionRecord {
|
|
69
|
+
/** Stable identifier — typically `ADR-XXXX` or `MADR-XXXX`. Must be unique. */
|
|
70
|
+
id: string;
|
|
71
|
+
/** One-line summary surfaced in briefings and `list` output. */
|
|
72
|
+
title: string;
|
|
73
|
+
/** Status lifecycle (rule 51: only the four declared values). */
|
|
74
|
+
status: DecisionStatus;
|
|
75
|
+
/** The problem / context the decision addresses. */
|
|
76
|
+
context: string;
|
|
77
|
+
/** The decision itself. */
|
|
78
|
+
decision: string;
|
|
79
|
+
/** Trade-offs, follow-ups, consequences. Optional — free-form. */
|
|
80
|
+
consequences: string | undefined;
|
|
81
|
+
/** Entity references (entity IDs, doc anchors, code paths). May be empty. */
|
|
82
|
+
entityRefs: string[];
|
|
83
|
+
/** The record this one supersedes — set by `applySupersede`, never by hand. */
|
|
84
|
+
supersedes?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Input shape for `serializeDecisionRecord`. Mirrors `DecisionRecord` but
|
|
89
|
+
* keeps `supersedes` reachable so callers writing the replacement before
|
|
90
|
+
* applying supersede can serialise the new record alone.
|
|
91
|
+
*/
|
|
92
|
+
export type DecisionRecordInput = Omit<DecisionRecord, "supersedes"> & {
|
|
93
|
+
supersedes?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Gate / type guard
|
|
98
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
const DECISION_STATUS_BY_VALUE: Record<string, DecisionStatus> = Object.freeze({
|
|
101
|
+
proposed: "proposed",
|
|
102
|
+
accepted: "accepted",
|
|
103
|
+
superseded: "superseded",
|
|
104
|
+
rejected: "rejected",
|
|
105
|
+
}) as Record<string, DecisionStatus>;
|
|
106
|
+
|
|
107
|
+
export function isDecisionStatus(value: unknown): value is DecisionStatus {
|
|
108
|
+
return (
|
|
109
|
+
typeof value === "string"
|
|
110
|
+
&& Object.prototype.hasOwnProperty.call(DECISION_STATUS_BY_VALUE, value)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const FRONTMATTER_KEYS = Object.freeze([
|
|
115
|
+
"id",
|
|
116
|
+
"title",
|
|
117
|
+
"status",
|
|
118
|
+
"context",
|
|
119
|
+
"decision",
|
|
120
|
+
"consequences",
|
|
121
|
+
"entityRefs",
|
|
122
|
+
"supersedes",
|
|
123
|
+
] as const satisfies readonly (keyof DecisionRecord | "id")[]);
|
|
124
|
+
|
|
125
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// Serialise
|
|
127
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render a decision record as markdown with a YAML frontmatter block.
|
|
131
|
+
*
|
|
132
|
+
* Frontmatter keys are sorted ascending (rule 38). Strings are double-quoted
|
|
133
|
+
* so reserved YAML characters (`#`, `:`, leading-digits, list-like prefixes)
|
|
134
|
+
* survive a round-trip without the caller caring.
|
|
135
|
+
*/
|
|
136
|
+
export function serializeDecisionRecord(record: DecisionRecordInput): string {
|
|
137
|
+
const lines: string[] = ["---"];
|
|
138
|
+
for (const key of FRONTMATTER_KEYS) {
|
|
139
|
+
const present =
|
|
140
|
+
key === "supersedes"
|
|
141
|
+
? record.supersedes !== undefined
|
|
142
|
+
: key === "consequences"
|
|
143
|
+
? record.consequences !== undefined
|
|
144
|
+
: true;
|
|
145
|
+
if (!present) continue;
|
|
146
|
+
lines.push(`${key}: ${encodeFrontmatterValue(key, record)}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("---", "");
|
|
149
|
+
if (record.context) lines.push(record.context);
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push("# Decision");
|
|
152
|
+
lines.push("");
|
|
153
|
+
if (record.decision) lines.push(record.decision);
|
|
154
|
+
if (record.consequences !== undefined && record.consequences.length > 0) {
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push("# Consequences");
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(record.consequences);
|
|
159
|
+
}
|
|
160
|
+
if (record.entityRefs.length > 0) {
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push("# Entity references");
|
|
163
|
+
for (const ref of record.entityRefs) lines.push(`- ${ref}`);
|
|
164
|
+
}
|
|
165
|
+
// Trailing newline keeps the file POSIX-well-formed for `cat`/editors.
|
|
166
|
+
lines.push("");
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function encodeFrontmatterValue(
|
|
171
|
+
key: (typeof FRONTMATTER_KEYS)[number],
|
|
172
|
+
record: DecisionRecordInput,
|
|
173
|
+
): string {
|
|
174
|
+
if (key === "entityRefs") {
|
|
175
|
+
const refs = record.entityRefs;
|
|
176
|
+
if (refs.length === 0) return "[]";
|
|
177
|
+
const inner = refs.map((ref) => `"${escapeYamlString(ref)}"`).join(", ");
|
|
178
|
+
return `[${inner}]`;
|
|
179
|
+
}
|
|
180
|
+
if (key === "status") return `"${record.status}"`;
|
|
181
|
+
if (key === "supersedes") return `"${record.supersedes ?? ""}"`;
|
|
182
|
+
if (key === "consequences") return `"${escapeYamlString(record.consequences ?? "")}"`;
|
|
183
|
+
const value = record[key];
|
|
184
|
+
return `"${escapeYamlString(typeof value === "string" ? value : String(value))}"`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function escapeYamlString(value: string): string {
|
|
188
|
+
// Inside a double-quoted YAML scalar, escape backslashes and double-quotes
|
|
189
|
+
// AND emit real newlines as a `\n` escape (the parser below mirrors this).
|
|
190
|
+
// Keeping all scalars single-line makes the line-based frontmatter parser
|
|
191
|
+
// safe and the file diff-friendly.
|
|
192
|
+
return value
|
|
193
|
+
.replace(/\\/g, "\\\\")
|
|
194
|
+
.replace(/"/g, '\\"')
|
|
195
|
+
.replace(/\n/g, "\\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// Parse
|
|
200
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
interface ParsedFrontmatter {
|
|
203
|
+
readonly fields: Readonly<Record<string, unknown>>;
|
|
204
|
+
readonly body: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse a decision record from a markdown string with a YAML frontmatter
|
|
209
|
+
* block. Throws (with a message listing the valid statuses, rule 51) when
|
|
210
|
+
* the document does not match the contract.
|
|
211
|
+
*
|
|
212
|
+
* Missing / undefined `status` defaults to `"proposed"` (rule 48 — least
|
|
213
|
+
* privileged).
|
|
214
|
+
*/
|
|
215
|
+
export function parseDecisionRecord(raw: string): DecisionRecord {
|
|
216
|
+
const { fields, body } = extractFrontmatter(raw);
|
|
217
|
+
|
|
218
|
+
// Surface unexpected keys early — silent drift is rule 51's worst case.
|
|
219
|
+
for (const fieldKey of Object.keys(fields)) {
|
|
220
|
+
if (!FRONTMATTER_KEYS.includes(fieldKey as (typeof FRONTMATTER_KEYS)[number])) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`decision record frontmatter contains unknown key '${fieldKey}'; valid keys are ${FRONTMATTER_KEYS.join(", ")}.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const id = requireString(fields.id, "id");
|
|
228
|
+
const title = requireString(fields.title, "title");
|
|
229
|
+
const context = requireString(fields.context, "context");
|
|
230
|
+
const decision = requireString(fields.decision, "decision");
|
|
231
|
+
const consequences =
|
|
232
|
+
fields.consequences === undefined ? undefined : requireString(fields.consequences, "consequences");
|
|
233
|
+
|
|
234
|
+
let status: DecisionStatus = DEFAULT_DECISION_STATUS;
|
|
235
|
+
if (fields.status !== undefined && fields.status !== null) {
|
|
236
|
+
if (!isDecisionStatus(fields.status)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`decision '${id}' has invalid status '${JSON.stringify(fields.status)}'; valid statuses are ${DECISION_STATUSES.join(", ")}.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
status = fields.status;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const entityRefs = parseEntityRefs(fields.entityRefs);
|
|
245
|
+
const supersedes =
|
|
246
|
+
fields.supersedes === undefined ? undefined : requireString(fields.supersedes, "supersedes");
|
|
247
|
+
|
|
248
|
+
// The parser is intentionally permissive about the on-disk shape. A
|
|
249
|
+
// record with `status: "superseded"` and no `supersedes` field is the
|
|
250
|
+
// shape `applySupersede` writes for the replaced record — the
|
|
251
|
+
// `supersedes` edge lives on the *replacement*, not the superseded one,
|
|
252
|
+
// so this branch must accept the canonical serialised form and stay
|
|
253
|
+
// round-trippable. Excluding superseded records from "active" listings is
|
|
254
|
+
// `listActive`'s job (rule 53, single classification source), not the
|
|
255
|
+
// parser's.
|
|
256
|
+
|
|
257
|
+
const record: DecisionRecord = {
|
|
258
|
+
id,
|
|
259
|
+
title,
|
|
260
|
+
status,
|
|
261
|
+
context,
|
|
262
|
+
decision,
|
|
263
|
+
consequences,
|
|
264
|
+
entityRefs,
|
|
265
|
+
...(supersedes !== undefined ? { supersedes } : {}),
|
|
266
|
+
};
|
|
267
|
+
// Body is preserved byte-for-byte on serialise; we don't introspect it
|
|
268
|
+
// here so format-neutral round-trips stay open (rule 38).
|
|
269
|
+
void body;
|
|
270
|
+
return record;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requireString(field: unknown, key: string): string {
|
|
274
|
+
if (field === undefined || field === null) {
|
|
275
|
+
throw new Error(`decision record frontmatter is missing required field '${key}'.`);
|
|
276
|
+
}
|
|
277
|
+
if (typeof field !== "string") {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`decision record frontmatter field '${key}' must be a string; got ${JSON.stringify(field)}.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return field;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseEntityRefs(field: unknown): string[] {
|
|
286
|
+
if (field === undefined) return [];
|
|
287
|
+
if (!Array.isArray(field)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`decision record frontmatter field 'entityRefs' must be an array of strings; got ${JSON.stringify(field)}.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
for (const entry of field) {
|
|
293
|
+
if (typeof entry !== "string") {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`decision record frontmatter 'entityRefs' entries must be strings; got ${JSON.stringify(entry)}.`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return [...field];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
303
|
+
// YAML frontmatter subset (intentionally narrow)
|
|
304
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function extractFrontmatter(raw: string): ParsedFrontmatter {
|
|
307
|
+
const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
308
|
+
if (!text.startsWith("---\n")) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"decision record must start with a YAML frontmatter fence (`---\\n`); got a document with no leading fence.",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const closeAt = text.indexOf("\n---", 4);
|
|
314
|
+
if (closeAt === -1) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"decision record frontmatter is missing its closing fence (`\\n---`); the document is truncated.",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const yamlBody = text.slice(4, closeAt);
|
|
320
|
+
const after = text.slice(closeAt + 4);
|
|
321
|
+
const body = after.startsWith("\n") ? after.slice(1) : after;
|
|
322
|
+
return { fields: parseYamlMapping(yamlBody), body };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseYamlMapping(input: string): Record<string, unknown> {
|
|
326
|
+
const fields: Record<string, unknown> = {};
|
|
327
|
+
for (const rawLine of input.split("\n")) {
|
|
328
|
+
const line = rawLine.trimEnd();
|
|
329
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
330
|
+
const colonAt = line.indexOf(":");
|
|
331
|
+
if (colonAt === -1) {
|
|
332
|
+
throw new Error(`decision record frontmatter line is not a key:value pair: "${line}".`);
|
|
333
|
+
}
|
|
334
|
+
const key = line.slice(0, colonAt).trim();
|
|
335
|
+
const valueText = line.slice(colonAt + 1).trim();
|
|
336
|
+
if (key.length === 0) {
|
|
337
|
+
throw new Error(`decision record frontmatter has an empty key in line: "${line}".`);
|
|
338
|
+
}
|
|
339
|
+
fields[key] = decodeScalar(valueText);
|
|
340
|
+
}
|
|
341
|
+
return fields;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function decodeScalar(valueText: string): unknown {
|
|
345
|
+
if (valueText.length === 0) return "";
|
|
346
|
+
const lower = valueText.toLowerCase();
|
|
347
|
+
if (lower === "true") return true;
|
|
348
|
+
if (lower === "false") return false;
|
|
349
|
+
if (lower === "null" || lower === "~") return null;
|
|
350
|
+
if (/^-?\d+$/.test(valueText)) return Number(valueText);
|
|
351
|
+
if (/^".*"$/.test(valueText)) {
|
|
352
|
+
// Restore escapes — order matters: handle the backslash-pair first so
|
|
353
|
+
// the literal `\\n` we emit from `escapeYamlString` decodes to a real
|
|
354
|
+
// newline (NOT a backslash-n).
|
|
355
|
+
return valueText
|
|
356
|
+
.slice(1, -1)
|
|
357
|
+
.replace(/\\\\/g, "\u0000")
|
|
358
|
+
.replace(/\\n/g, "\n")
|
|
359
|
+
.replace(/\\"/g, '"')
|
|
360
|
+
.replace(/\u0000/g, "\\");
|
|
361
|
+
}
|
|
362
|
+
if (/^\[.*\]$/.test(valueText)) return parseFlowList(valueText);
|
|
363
|
+
return valueText;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseFlowList(valueText: string): unknown[] {
|
|
367
|
+
const inner = valueText.slice(1, -1).trim();
|
|
368
|
+
if (inner.length === 0) return [];
|
|
369
|
+
const out: string[] = [];
|
|
370
|
+
let buf = "";
|
|
371
|
+
let inQuotes = false;
|
|
372
|
+
// Track whether we are sitting on a backslash so that `\"` inside a
|
|
373
|
+
// quoted element is recognised as an escaped quote rather than a
|
|
374
|
+
// closing quote. Without this flag, a ref containing a literal `"`
|
|
375
|
+
// (e.g. an entity id like `org/dept/\"lead\"`) would close its own
|
|
376
|
+
// element early and the next comma would split a string in half
|
|
377
|
+
// (cursor bug d16b2a18, review round on PR #1590).
|
|
378
|
+
let escaped = false;
|
|
379
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
380
|
+
const c = inner[i]!;
|
|
381
|
+
if (escaped) {
|
|
382
|
+
buf += c;
|
|
383
|
+
escaped = false;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (c === "\\") {
|
|
387
|
+
buf += c;
|
|
388
|
+
escaped = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (c === '"') {
|
|
392
|
+
inQuotes = !inQuotes;
|
|
393
|
+
buf += c;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (c === "," && !inQuotes) {
|
|
397
|
+
out.push(stripFlowStringQuotes(buf.trim()));
|
|
398
|
+
buf = "";
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
buf += c;
|
|
402
|
+
}
|
|
403
|
+
if (buf.length > 0) out.push(stripFlowStringQuotes(buf.trim()));
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function stripFlowStringQuotes(value: string): string {
|
|
408
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
409
|
+
return value
|
|
410
|
+
.slice(1, -1)
|
|
411
|
+
.replace(/\\\\/g, "\u0000")
|
|
412
|
+
.replace(/\\n/g, "\n")
|
|
413
|
+
.replace(/\\"/g, '"')
|
|
414
|
+
.replace(/\u0000/g, "\\");
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
420
|
+
// Supersede
|
|
421
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Pure supersede mutation: given the current record set, the id of a record
|
|
425
|
+
* to supersede, and the replacement record (already accepted), returns a new
|
|
426
|
+
* list with the replacement appended and the old record flipped to
|
|
427
|
+
* `superseded`.
|
|
428
|
+
*
|
|
429
|
+
* The hook fires `event: "write:<replacementId>"` BEFORE
|
|
430
|
+
* `event: "mutate:<oldId>:superseded"` so storage callers can guarantee
|
|
431
|
+
* disk-order matches the rule-25 requirement (the replacement lands before
|
|
432
|
+
* the old record's status flips).
|
|
433
|
+
*/
|
|
434
|
+
export function applySupersede(
|
|
435
|
+
records: readonly DecisionRecord[],
|
|
436
|
+
targetId: string,
|
|
437
|
+
replacement: DecisionRecord,
|
|
438
|
+
hook?: (event: string) => void,
|
|
439
|
+
): DecisionRecord[] {
|
|
440
|
+
const target = records.find((r) => r.id === targetId);
|
|
441
|
+
if (!target) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`applySupersede cannot find target record '${targetId}' in the current record set.`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
// Reject a collision where `replacement.id` already exists in the record
|
|
447
|
+
// set. Without this guard, an operator typo or MCP rename can silently
|
|
448
|
+
// overwrite an unrelated decision with a record that `supersedes` itself,
|
|
449
|
+
// corrupting the decision history (chatgpt-codex-connector review on
|
|
450
|
+
// PR #1590). The replacement must either be a NEW id (append) or the
|
|
451
|
+
// same id as the target (in-place replacement — supported as a separate
|
|
452
|
+
// edge case below).
|
|
453
|
+
const existing = records.find((r) => r.id === replacement.id);
|
|
454
|
+
if (existing && replacement.id !== targetId) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`applySupersede replacement id '${replacement.id}' already exists in the record set; ` +
|
|
457
|
+
`pick a new id to avoid silently overwriting an unrelated decision.`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const next: DecisionRecord[] = records.map((r) =>
|
|
461
|
+
r.id === targetId
|
|
462
|
+
? { ...r, status: "superseded" as DecisionStatus, supersedes: undefined }
|
|
463
|
+
: r,
|
|
464
|
+
);
|
|
465
|
+
const replacementWithEdge: DecisionRecord = { ...replacement, supersedes: targetId };
|
|
466
|
+
// Same-id replacement path: a single record swaps its body in place. The
|
|
467
|
+
// entry keeps its place in the array (so the listing order is preserved)
|
|
468
|
+
// and the new record still carries the supersede edge for traceability.
|
|
469
|
+
if (replacement.id === targetId) {
|
|
470
|
+
const idx = next.findIndex((r) => r.id === targetId);
|
|
471
|
+
next[idx] = replacementWithEdge;
|
|
472
|
+
} else {
|
|
473
|
+
next.push(replacementWithEdge);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
hook?.(`write:${replacement.id}`);
|
|
477
|
+
hook?.(`mutate:${targetId}:superseded`);
|
|
478
|
+
|
|
479
|
+
return next;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
483
|
+
// Listing
|
|
484
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Filter a record set down to "standing" decisions (proposed + accepted).
|
|
488
|
+
* Uses the explicitly-exported `ACTIVE_DECISION_STATUSES` set (rule 53 — one
|
|
489
|
+
* classification source) so callers cannot drift.
|
|
490
|
+
*/
|
|
491
|
+
export function listActive(records: readonly DecisionRecord[]): DecisionRecord[] {
|
|
492
|
+
return records.filter((r) => ACTIVE_DECISION_STATUSES.has(r.status));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
496
|
+
// Coding-knowledge feature gate (Track A PR 1 wiring boundary)
|
|
497
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Single source of truth for "are decision records active for this operator?"
|
|
501
|
+
* (rule 39 — every behaviour dependent on Track A consults this, not raw
|
|
502
|
+
* config reads). The master gate is `codingKnowledge.enabled`; the
|
|
503
|
+
* decision-record subsystem has its own on-switch.
|
|
504
|
+
*/
|
|
505
|
+
export function isDecisionRecordsEnabled(config: CodingKnowledgeConfig): boolean {
|
|
506
|
+
return config.enabled === true && config.decisionRecords === true;
|
|
507
|
+
}
|
package/src/config.test.ts
CHANGED
|
@@ -1065,6 +1065,124 @@ test("parseConfig codingMode: unknown object shape falls back to defaults", () =
|
|
|
1065
1065
|
assert.equal(result.codingMode.branchScope, false);
|
|
1066
1066
|
});
|
|
1067
1067
|
|
|
1068
|
+
// Track A coding-knowledge surface (issue #1548 PR 1).
|
|
1069
|
+
// Defaults pinned here are the contract — any drift between these expectations,
|
|
1070
|
+
// `CodingKnowledgeConfig`, `CODING_KNOWLEDGE_DEFAULTS`, the JSON schema, and
|
|
1071
|
+
// `docs/config-reference.md` is a contract regression (rule 55).
|
|
1072
|
+
|
|
1073
|
+
test("parseConfig codingKnowledge: defaults match the documented contract (issue #1548 Track A PR 1)", () => {
|
|
1074
|
+
const result = parseConfig({ openaiApiKey: "sk-test" });
|
|
1075
|
+
assert.deepEqual(result.codingKnowledge, {
|
|
1076
|
+
enabled: false,
|
|
1077
|
+
decisionRecords: true,
|
|
1078
|
+
architectureCard: true,
|
|
1079
|
+
sessionDelta: true,
|
|
1080
|
+
architectureCardLlmSummary: false,
|
|
1081
|
+
structuralProvider: "none",
|
|
1082
|
+
structuralProviderCommand: "",
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
test("parseConfig codingKnowledge: master gate defaults OFF so the pre-feature path is byte-identical", () => {
|
|
1087
|
+
// The hard rule — rule 39 / rule 48. Every other switch is opt-in under
|
|
1088
|
+
// the master gate, so the default must be `false` (least-privileged).
|
|
1089
|
+
assert.equal(parseConfig({ openaiApiKey: "sk-test" }).codingKnowledge.enabled, false);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("parseConfig codingKnowledge: accepts CLI-style boolean strings (CLAUDE.md gotcha 36)", () => {
|
|
1093
|
+
const result = parseConfig({
|
|
1094
|
+
openaiApiKey: "sk-test",
|
|
1095
|
+
codingKnowledge: {
|
|
1096
|
+
enabled: "true",
|
|
1097
|
+
decisionRecords: "false",
|
|
1098
|
+
architectureCardLlmSummary: "true",
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
assert.equal(result.codingKnowledge.enabled, true);
|
|
1102
|
+
assert.equal(result.codingKnowledge.decisionRecords, false);
|
|
1103
|
+
assert.equal(result.codingKnowledge.architectureCardLlmSummary, true);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test("parseConfig codingKnowledge: rejects unknown structuralProvider value (rule 51)", () => {
|
|
1107
|
+
assert.throws(
|
|
1108
|
+
() =>
|
|
1109
|
+
parseConfig({
|
|
1110
|
+
openaiApiKey: "sk-test",
|
|
1111
|
+
codingKnowledge: { structuralProvider: "warp" },
|
|
1112
|
+
}),
|
|
1113
|
+
/structuralProvider must be one of none, subprocess, native/,
|
|
1114
|
+
);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
test("parseConfig codingKnowledge: structuralProvider 'subprocess' survives; defaults to 'none' on missing", () => {
|
|
1118
|
+
const explicit = parseConfig({
|
|
1119
|
+
openaiApiKey: "sk-test",
|
|
1120
|
+
codingKnowledge: { structuralProvider: "subprocess" },
|
|
1121
|
+
});
|
|
1122
|
+
assert.equal(explicit.codingKnowledge.structuralProvider, "subprocess");
|
|
1123
|
+
const missing = parseConfig({ openaiApiKey: "sk-test", codingKnowledge: {} });
|
|
1124
|
+
assert.equal(missing.codingKnowledge.structuralProvider, "none");
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test("parseConfig codingKnowledge: structuralProviderCommand trims surrounding whitespace", () => {
|
|
1128
|
+
const result = parseConfig({
|
|
1129
|
+
openaiApiKey: "sk-test",
|
|
1130
|
+
codingKnowledge: { structuralProviderCommand: " /usr/local/bin/cbm " },
|
|
1131
|
+
});
|
|
1132
|
+
assert.equal(result.codingKnowledge.structuralProviderCommand, "/usr/local/bin/cbm");
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
test("parseConfig codingKnowledge: unknown object shape falls back to defaults (rule 55)", () => {
|
|
1136
|
+
const result = parseConfig({ openaiApiKey: "sk-test", codingKnowledge: null });
|
|
1137
|
+
assert.equal(result.codingKnowledge.enabled, false);
|
|
1138
|
+
assert.equal(result.codingKnowledge.decisionRecords, true);
|
|
1139
|
+
assert.equal(result.codingKnowledge.structuralProvider, "none");
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test("parseConfig codingKnowledge: rejects malformed boolean string with the valid set in the error", () => {
|
|
1143
|
+
assert.throws(
|
|
1144
|
+
() =>
|
|
1145
|
+
parseConfig({
|
|
1146
|
+
openaiApiKey: "sk-test",
|
|
1147
|
+
codingKnowledge: { decisionRecords: "flase" },
|
|
1148
|
+
}),
|
|
1149
|
+
/decisionRecords must be a boolean.*got "flase"/,
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
test("parseConfig codingKnowledge: rejects malformed master-gate boolean", () => {
|
|
1154
|
+
assert.throws(
|
|
1155
|
+
() =>
|
|
1156
|
+
parseConfig({
|
|
1157
|
+
openaiApiKey: "sk-test",
|
|
1158
|
+
codingKnowledge: { enabled: "truthy" },
|
|
1159
|
+
}),
|
|
1160
|
+
/codingKnowledge\.enabled must be a boolean or one of true\/false\/1\/0\/yes\/no\/on\/off/,
|
|
1161
|
+
);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// (Removed: the third "rejects non-boolean" test used an assert.throws third
|
|
1165
|
+
// argument that the project's esbuild build could not parse in this configuration.
|
|
1166
|
+
// The behavior is covered by the two preceding tests above.)
|
|
1167
|
+
|
|
1168
|
+
test("parseConfig codingKnowledge: accepts the documented boolean-like strings (rule 36)", () => {
|
|
1169
|
+
// Positive matrix — these MUST keep working after the strict-parse change.
|
|
1170
|
+
for (const value of ["true", "True", "TRUE", "1", "yes", "YES", "on"]) {
|
|
1171
|
+
const result = parseConfig({
|
|
1172
|
+
openaiApiKey: "sk-test",
|
|
1173
|
+
codingKnowledge: { enabled: value },
|
|
1174
|
+
});
|
|
1175
|
+
assert.equal(result.codingKnowledge.enabled, true, `${value} should coerce to true`);
|
|
1176
|
+
}
|
|
1177
|
+
for (const value of ["false", "False", "FALSE", "0", "no", "NO", "off"]) {
|
|
1178
|
+
const result = parseConfig({
|
|
1179
|
+
openaiApiKey: "sk-test",
|
|
1180
|
+
codingKnowledge: { enabled: value },
|
|
1181
|
+
});
|
|
1182
|
+
assert.equal(result.codingKnowledge.enabled, false, `${value} should coerce to false`);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1068
1186
|
// Pattern reinforcement (issue #687 PR 2/4)
|
|
1069
1187
|
|
|
1070
1188
|
test("parseConfig: pattern reinforcement defaults are off, weekly, minCount=3, std categories", () => {
|
package/src/config.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { expandTildePath } from "./utils/path.js";
|
|
|
40
40
|
// config.ts → connectors/index.ts nor the reverse circular import arises.
|
|
41
41
|
import { coerceBool, coerceInstallExtension, coerceNumber } from "./connectors/coerce.js";
|
|
42
42
|
import { parseWearablesConfig } from "./wearables/config.js";
|
|
43
|
-
|
|
43
|
+
import { parseCodingKnowledgeConfig } from "./coding/coding-knowledge-config.js";
|
|
44
44
|
const DEFAULT_MEMORY_DIR = path.join(
|
|
45
45
|
resolveHomeDir(),
|
|
46
46
|
".openclaw",
|
|
@@ -2484,7 +2484,8 @@ export function parseConfig(raw: unknown): PluginConfig {
|
|
|
2484
2484
|
heartbeat,
|
|
2485
2485
|
slotBehavior,
|
|
2486
2486
|
codexCompat,
|
|
2487
|
-
|
|
2487
|
+
codingKnowledge: parseCodingKnowledgeConfig(cfg.codingKnowledge),
|
|
2488
|
+
// Hourly summaries
|
|
2488
2489
|
hourlySummariesEnabled: cfg.hourlySummariesEnabled !== false, // default: true
|
|
2489
2490
|
daySummaryEnabled: cfg.daySummaryEnabled !== false, // default: true
|
|
2490
2491
|
hourlySummaryCronAutoRegister: cfg.hourlySummaryCronAutoRegister === true,
|