@saasontools/strauss-kb 0.1.3 → 0.1.4
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 +25 -1
- package/dist/{chunk-HYNAEAPM.js → chunk-EDH43Z7J.js} +67 -13
- package/dist/chunk-EDH43Z7J.js.map +1 -0
- package/dist/{chunk-FSI4Q2FD.js → chunk-TS26G7TL.js} +2 -2
- package/dist/{chunk-QLTB77W4.js → chunk-Y5C7Z2HG.js} +2 -2
- package/dist/cli-main.cjs +66 -12
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +66 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +3 -3
- package/dist/mcp-main.cjs +66 -12
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HYNAEAPM.js.map +0 -1
- /package/dist/{chunk-FSI4Q2FD.js.map → chunk-TS26G7TL.js.map} +0 -0
- /package/dist/{chunk-QLTB77W4.js.map → chunk-Y5C7Z2HG.js.map} +0 -0
package/README.md
CHANGED
|
@@ -151,7 +151,31 @@ before publishing, which narrows the lost-update window rather than closing it.
|
|
|
151
151
|
[ARCHITECTURE.md](./ARCHITECTURE.md) says why a lock was rejected.
|
|
152
152
|
|
|
153
153
|
`supersede` writes both directions, so a backlink cannot drift in normal use and
|
|
154
|
-
`validate` drops to catching hand-edits.
|
|
154
|
+
`validate` drops to catching hand-edits. A `write` (or `write-decision`) that
|
|
155
|
+
carries `supersedes` does the same: the new record publishes first, then each
|
|
156
|
+
prior record it names is marked superseded in turn — a crash between the two
|
|
157
|
+
leaves an old record with no backlink, which `validate` already reports as
|
|
158
|
+
"is not marked superseded", never a silent drift. A `supersedes` id naming a
|
|
159
|
+
record that does not exist yet is legal and does not fail the write; `validate`
|
|
160
|
+
is what reports a target that never resolves. A `supersedes` id naming the
|
|
161
|
+
record's own concept id is a no-op rather than an error, duplicate ids mark
|
|
162
|
+
once, and the array is capped at 32 entries.
|
|
163
|
+
|
|
164
|
+
A concurrent writer marking the same target races the compare-and-swap check;
|
|
165
|
+
that's retried a few times before giving up, and giving up is reported the
|
|
166
|
+
same way as a target that doesn't exist yet — left out of `supersededIds` for
|
|
167
|
+
`validate` to catch, not thrown, since the calling record is already
|
|
168
|
+
published by that point. If two different records both name the same target
|
|
169
|
+
in `supersedes`, the target's backlink points at whichever wrote last;
|
|
170
|
+
`validate` doesn't see this as a problem because the target genuinely is
|
|
171
|
+
superseded, but `kb_query`/`kb_load`'s adjudication surfaces the resulting
|
|
172
|
+
fork as a warning at read time.
|
|
173
|
+
|
|
174
|
+
`kb_write` and `kb_write_decision` return
|
|
175
|
+
`{ conceptId, action: "created" | "superseded-prior", supersededIds }` —
|
|
176
|
+
`supersededIds` is only the ids actually marked, not every id the input named.
|
|
177
|
+
A 409 from a concept-id collision carries `action: "refused"` in its `details`,
|
|
178
|
+
alongside the `conceptId`.
|
|
155
179
|
|
|
156
180
|
Records are never deleted. Superseding keeps the earlier reasoning inspectable,
|
|
157
181
|
which is what a later `trace` reads.
|
|
@@ -180,7 +180,7 @@ var composeInputSchema = z2.object({
|
|
|
180
180
|
/** Concept ids this record relates to; rendered as body links. */
|
|
181
181
|
relatedConceptIds: z2.array(kbConceptIdSchema).optional(),
|
|
182
182
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
183
|
-
supersedes: z2.array(kbConceptIdSchema).optional(),
|
|
183
|
+
supersedes: z2.array(kbConceptIdSchema).max(32).optional(),
|
|
184
184
|
materiality: z2.enum(KB_MATERIALITIES).optional(),
|
|
185
185
|
confidence: z2.enum(KB_CONFIDENCES).optional(),
|
|
186
186
|
owner: z2.string().min(1).optional()
|
|
@@ -1561,7 +1561,11 @@ var writeCommand = define({
|
|
|
1561
1561
|
composeRecord(type, input, actor, now()),
|
|
1562
1562
|
actor
|
|
1563
1563
|
);
|
|
1564
|
-
return {
|
|
1564
|
+
return {
|
|
1565
|
+
conceptId: record.conceptId,
|
|
1566
|
+
action: record.action,
|
|
1567
|
+
supersededIds: record.supersededIds
|
|
1568
|
+
};
|
|
1565
1569
|
}
|
|
1566
1570
|
});
|
|
1567
1571
|
|
|
@@ -1591,7 +1595,11 @@ var writeDecisionCommand = define({
|
|
|
1591
1595
|
composeDecisionRecord(input, actor, now()),
|
|
1592
1596
|
actor
|
|
1593
1597
|
);
|
|
1594
|
-
return {
|
|
1598
|
+
return {
|
|
1599
|
+
conceptId: record.conceptId,
|
|
1600
|
+
action: record.action,
|
|
1601
|
+
supersededIds: record.supersededIds
|
|
1602
|
+
};
|
|
1595
1603
|
}
|
|
1596
1604
|
});
|
|
1597
1605
|
|
|
@@ -1690,7 +1698,7 @@ var KbRecordAlreadyExistsError = class extends BaseError {
|
|
|
1690
1698
|
fault: "User" /* User */,
|
|
1691
1699
|
retriable: false,
|
|
1692
1700
|
reportToUser: true,
|
|
1693
|
-
details: { conceptId: conceptId2 }
|
|
1701
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
1694
1702
|
});
|
|
1695
1703
|
this.conceptId = conceptId2;
|
|
1696
1704
|
}
|
|
@@ -1882,13 +1890,27 @@ var KbStore = class {
|
|
|
1882
1890
|
conceptId: conceptId2,
|
|
1883
1891
|
by: actor
|
|
1884
1892
|
});
|
|
1893
|
+
const targets = new Set(frontmatter.strauss_supersedes ?? []);
|
|
1894
|
+
targets.delete(conceptId2);
|
|
1895
|
+
const supersededIds = [];
|
|
1896
|
+
for (const old of targets) {
|
|
1897
|
+
if (await this.markSupersededRetrying(bundlePath2, old, conceptId2, actor)) {
|
|
1898
|
+
supersededIds.push(old);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1885
1901
|
this.logger.info?.({
|
|
1886
1902
|
operation: "kb.write",
|
|
1887
1903
|
bundlePath: root,
|
|
1888
1904
|
conceptId: conceptId2,
|
|
1889
1905
|
anchors: frontmatter.strauss_anchors?.length ?? 0
|
|
1890
1906
|
});
|
|
1891
|
-
return {
|
|
1907
|
+
return {
|
|
1908
|
+
conceptId: conceptId2,
|
|
1909
|
+
frontmatter,
|
|
1910
|
+
body: input.body,
|
|
1911
|
+
action: supersededIds.length ? "superseded-prior" : "created",
|
|
1912
|
+
supersededIds
|
|
1913
|
+
};
|
|
1892
1914
|
}
|
|
1893
1915
|
/** One record by concept id, or null when it does not exist. */
|
|
1894
1916
|
async read(bundlePath2, conceptId2) {
|
|
@@ -1952,15 +1974,11 @@ var KbStore = class {
|
|
|
1952
1974
|
async supersede(bundlePath2, conceptId2, replacementId, actor = "unknown") {
|
|
1953
1975
|
const replacement = await this.read(bundlePath2, replacementId);
|
|
1954
1976
|
if (!replacement) throw new KbRecordNotFoundError(replacementId);
|
|
1955
|
-
const superseded = await this.
|
|
1977
|
+
const superseded = await this.markSuperseded(
|
|
1956
1978
|
bundlePath2,
|
|
1957
1979
|
conceptId2,
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
strauss_status: "superseded",
|
|
1961
|
-
strauss_superseded_by: replacementId
|
|
1962
|
-
}),
|
|
1963
|
-
{ operation: "supersede", by: actor, target: replacementId }
|
|
1980
|
+
replacementId,
|
|
1981
|
+
actor
|
|
1964
1982
|
);
|
|
1965
1983
|
await this.mutate(
|
|
1966
1984
|
bundlePath2,
|
|
@@ -2124,6 +2142,42 @@ ${answer}
|
|
|
2124
2142
|
}
|
|
2125
2143
|
return result;
|
|
2126
2144
|
}
|
|
2145
|
+
/**
|
|
2146
|
+
* `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
|
|
2147
|
+
* a missing target (a broken link, legal per compose.ts) or a CAS conflict
|
|
2148
|
+
* from a concurrent writer touching the same target. A conflict is retried
|
|
2149
|
+
* a bounded number of times — each attempt re-reads the target fresh — and
|
|
2150
|
+
* on the last, `false` reports "not marked" rather than throwing: the
|
|
2151
|
+
* caller's own record is already published, so failing here would leave
|
|
2152
|
+
* that publish unreported instead of undone. kb_validate's existing
|
|
2153
|
+
* "not marked superseded" check is what surfaces the residue.
|
|
2154
|
+
*/
|
|
2155
|
+
async markSupersededRetrying(bundlePath2, conceptId2, replacementId, actor, retries = 3) {
|
|
2156
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2157
|
+
try {
|
|
2158
|
+
await this.markSuperseded(bundlePath2, conceptId2, replacementId, actor);
|
|
2159
|
+
return true;
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
if (error instanceof KbRecordNotFoundError) return false;
|
|
2162
|
+
if (!(error instanceof KbWriteConflictError)) throw error;
|
|
2163
|
+
if (attempt === retries) return false;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
/** The one-directional half of `supersede`: marks `conceptId` superseded. */
|
|
2169
|
+
async markSuperseded(bundlePath2, conceptId2, replacementId, actor) {
|
|
2170
|
+
return this.mutate(
|
|
2171
|
+
bundlePath2,
|
|
2172
|
+
conceptId2,
|
|
2173
|
+
(frontmatter) => ({
|
|
2174
|
+
...frontmatter,
|
|
2175
|
+
strauss_status: "superseded",
|
|
2176
|
+
strauss_superseded_by: replacementId
|
|
2177
|
+
}),
|
|
2178
|
+
{ operation: "supersede", by: actor, target: replacementId }
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2127
2181
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2128
2182
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2129
2183
|
const before = await readFile3(target, "utf8").catch(() => null);
|
|
@@ -2320,4 +2374,4 @@ export {
|
|
|
2320
2374
|
KB_DIR,
|
|
2321
2375
|
KbStore
|
|
2322
2376
|
};
|
|
2323
|
-
//# sourceMappingURL=chunk-
|
|
2377
|
+
//# sourceMappingURL=chunk-EDH43Z7J.js.map
|