@davesheffer/hunch 1.45.2 → 1.47.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/dist/cli/index.js +290 -353
- package/dist/cli/invocation.d.ts +22 -13
- package/dist/cli/invocation.js +49 -29
- package/dist/cli/verify.d.ts +28 -0
- package/dist/cli/verify.js +265 -0
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/experiment.d.ts +1 -1
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/agenthook.js +26 -6
- package/dist/core/boundedSpawn.d.ts +57 -0
- package/dist/core/boundedSpawn.js +321 -0
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +1 -0
- package/dist/core/correction.js +9 -9
- package/dist/core/delivery.d.ts +6 -1
- package/dist/core/delivery.js +71 -5
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/migrate.d.ts +10 -1
- package/dist/core/migrate.js +17 -6
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/projectDna.js +11 -1
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/relativeImports.js +6 -2
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/taskSelection.js +48 -6
- package/dist/core/testSelection.d.ts +122 -0
- package/dist/core/testSelection.js +588 -0
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +8 -23
- package/dist/extractors/git.js +77 -95
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +168 -184
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +37 -37
- package/dist/extractors/languages.js +83 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +27 -13
- package/dist/extractors/parse.js +120 -56
- package/dist/extractors/workspaces.js +3 -1
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/health.d.ts +6 -0
- package/dist/integrations/health.js +33 -12
- package/dist/integrations/hooks.d.ts +13 -16
- package/dist/integrations/hooks.js +31 -30
- package/dist/integrations/mergeDriver.js +18 -17
- package/dist/integrations/optout.d.ts +3 -0
- package/dist/integrations/optout.js +17 -0
- package/dist/integrations/providers.d.ts +6 -2
- package/dist/integrations/providers.js +19 -2
- package/dist/integrations/team.js +4 -2
- package/dist/integrations/uninstall.d.ts +10 -4
- package/dist/integrations/uninstall.js +22 -18
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +107 -81
- package/dist/serve/app.js +9 -1
- package/dist/serve/config.js +5 -0
- package/dist/serve/writelock.d.ts +3 -1
- package/dist/serve/writelock.js +17 -3
- package/dist/store/hunchStore.d.ts +2 -1
- package/dist/store/hunchStore.js +20 -23
- package/dist/store/jsonStore.d.ts +13 -3
- package/dist/store/jsonStore.js +37 -13
- package/dist/store/stateBinding.js +17 -4
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +93 -154
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +33 -50
- package/package.json +3 -3
- package/server.json +2 -2
package/dist/store/hunchStore.js
CHANGED
|
@@ -1341,8 +1341,8 @@ export class HunchStore {
|
|
|
1341
1341
|
* instant — "what did we believe as of commit X?". Omit `asOf` for the full,
|
|
1342
1342
|
* history-inclusive view (backward-compatible default). */
|
|
1343
1343
|
why(target, opts = {}) {
|
|
1344
|
-
// An
|
|
1345
|
-
//
|
|
1344
|
+
// An agent's verbatim edit-payload path is absolute; stored paths are
|
|
1345
|
+
// repo-relative, so rewrite before any matching (issue #299).
|
|
1346
1346
|
target = repoRelativeTarget(target, this.paths.root);
|
|
1347
1347
|
// A record id (every delivered brief line ends in `hunch_why("dec_…")`) resolves
|
|
1348
1348
|
// to that record directly (issue #474). With `asOf`, a record not in force then
|
|
@@ -1350,8 +1350,10 @@ export class HunchStore {
|
|
|
1350
1350
|
const lookup = WHY_RECORD_ID.test(target) ? this.whyRecord(target, opts.canRead, opts.asOf) : undefined;
|
|
1351
1351
|
if (lookup?.record)
|
|
1352
1352
|
return { target, decisions: [], bugs: [], constraints: [], symbols: [], components: [], byId: lookup };
|
|
1353
|
-
// A miss is reported
|
|
1354
|
-
|
|
1353
|
+
// A miss is reported for a generated-shape id, or for any id on file that was not
|
|
1354
|
+
// in force at `asOf` (issue #493); anything else resolves as before.
|
|
1355
|
+
const byId = lookup && (WHY_GENERATED_ID.test(target) || (opts.asOf && this.whyRecord(target, opts.canRead).record))
|
|
1356
|
+
? lookup : undefined;
|
|
1355
1357
|
const decisions = this.recs("decisions").filter(opts.canRead ?? (() => true));
|
|
1356
1358
|
const bugs = this.recs("bugs").filter(opts.canRead ?? (() => true));
|
|
1357
1359
|
const constraints = this.recs("constraints").filter(opts.canRead ?? (() => true));
|
|
@@ -1362,11 +1364,11 @@ export class HunchStore {
|
|
|
1362
1364
|
// so an unanchored suffix pulled unrelated files' records into why()/the
|
|
1363
1365
|
// pre-edit grounding block (issue #32). Segment-anchored matching only.
|
|
1364
1366
|
//
|
|
1365
|
-
// A target that
|
|
1366
|
-
// vscode-extension/index.ts
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1367
|
+
// A target that is a path the index already knows (README.md,
|
|
1368
|
+
// `vscode-extension/index.ts`) names exactly ONE file. Suffix-matching it too
|
|
1369
|
+
// would pull in every other file sharing its basename (issue #299: root
|
|
1370
|
+
// `index.ts` received `vscode-extension/index.ts`'s rules and vice versa). "Known
|
|
1371
|
+
// to the index" is answered from already-loaded graph data, looked up
|
|
1370
1372
|
// first (a symbol's exact file, or an indexed component's path glob — so a
|
|
1371
1373
|
// symbol-less indexed file still counts as real), and only then from the
|
|
1372
1374
|
// working tree, for a real file the index cannot see at all (issue #334).
|
|
@@ -1446,7 +1448,8 @@ export class HunchStore {
|
|
|
1446
1448
|
}
|
|
1447
1449
|
/** Transitive blast radius: every symbol/component that (in)directly depends on
|
|
1448
1450
|
* `id`, via a recursive CTE over the edges graph (hunch_get_dependents). We
|
|
1449
|
-
* walk edges BACKWARD (edges.to = current) following call/dep/import/contains
|
|
1451
|
+
* walk edges BACKWARD (edges.to = current) following call/dep/import/contains,
|
|
1452
|
+
* plus `references` — the only edge YAML/Helm/Kustomize emit (issue #486). */
|
|
1450
1453
|
getDependents(id, maxDepth = 6) {
|
|
1451
1454
|
return this.db.prepare(
|
|
1452
1455
|
/* sql */ `
|
|
@@ -1455,7 +1458,7 @@ export class HunchStore {
|
|
|
1455
1458
|
UNION
|
|
1456
1459
|
SELECT e."from", up.depth + 1
|
|
1457
1460
|
FROM edges e JOIN up ON e."to" = up.node
|
|
1458
|
-
WHERE e.type IN ('calls','depends_on','imports','contains','implements') AND up.depth < ?
|
|
1461
|
+
WHERE e.type IN ('calls','depends_on','imports','contains','implements','references') AND up.depth < ?
|
|
1459
1462
|
)
|
|
1460
1463
|
SELECT up.node AS id, MIN(up.depth) AS depth,
|
|
1461
1464
|
COALESCE(s.name || ' @ ' || s.file, c.name, up.node) AS via
|
|
@@ -1497,7 +1500,7 @@ export class HunchStore {
|
|
|
1497
1500
|
UNION
|
|
1498
1501
|
SELECT e."from", up.depth + 1
|
|
1499
1502
|
FROM edges e JOIN up ON e."to" = up.node
|
|
1500
|
-
WHERE e.type IN ('calls','depends_on','imports','contains','implements') AND up.depth < ?
|
|
1503
|
+
WHERE e.type IN ('calls','depends_on','imports','contains','implements','references') AND up.depth < ?
|
|
1501
1504
|
)
|
|
1502
1505
|
SELECT s.file AS file, s.name AS via, MIN(up.depth) AS depth
|
|
1503
1506
|
FROM up JOIN symbols s ON s.id = up.node
|
|
@@ -1693,9 +1696,8 @@ export class HunchStore {
|
|
|
1693
1696
|
* longer enforced. Pass `{ asOf }` to instead return the invariants in force at
|
|
1694
1697
|
* that instant (time-travel: "what must I not have broken as of commit X?"). */
|
|
1695
1698
|
checkConstraints(scope, opts = {}) {
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
// blocking rule plainly applied (issue #296).
|
|
1699
|
+
// Scopes are repo-relative; an absolute target matched nothing even when a
|
|
1700
|
+
// blocking rule applied (issue #296).
|
|
1699
1701
|
scope = repoRelativeTarget(scope, this.paths.root);
|
|
1700
1702
|
const all = this.recs("constraints");
|
|
1701
1703
|
const asOf = opts.asOf;
|
|
@@ -1710,11 +1712,8 @@ export class HunchStore {
|
|
|
1710
1712
|
* glob, and the queried scope may be either too. Advisory only — findings never
|
|
1711
1713
|
* enter any block path. Sorted worst-first, then id for stable output. */
|
|
1712
1714
|
liveFindingsFor(scope) {
|
|
1713
|
-
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1714
|
-
//
|
|
1715
|
-
// below, and an un-guarded suffix fallback could pull in an unrelated file
|
|
1716
|
-
// that merely shares a basename. Normalize once and reuse the same "is this
|
|
1717
|
-
// path known to the index" guard.
|
|
1715
|
+
// Same absolute-target and same-basename-leak gaps as why() (issue #299):
|
|
1716
|
+
// normalize once and reuse the known-path guard.
|
|
1718
1717
|
const t = repoRelativeTarget(scope, this.paths.root);
|
|
1719
1718
|
const indexed = this.isKnownPath(t);
|
|
1720
1719
|
const live = (f) => f.triage === "open" || f.triage === "accepted-risk" || f.triage === "scheduled";
|
|
@@ -2337,9 +2336,7 @@ export class HunchStore {
|
|
|
2337
2336
|
* a task on `target`, ordered by what matters most — invariants first, then the
|
|
2338
2337
|
* why, then blast radius and bug history — trimmed to a rough token budget. */
|
|
2339
2338
|
assembleContext(target, budget = 1500, opts = {}) {
|
|
2340
|
-
//
|
|
2341
|
-
// repo-relative BEFORE it reaches why()/liveFindingsFor() below, or an
|
|
2342
|
-
// absolute path never matches the repo-relative stored data (issue #299).
|
|
2339
|
+
// Normalize before it reaches why()/liveFindingsFor() below (issue #299).
|
|
2343
2340
|
target = repoRelativeTarget(target, this.paths.root);
|
|
2344
2341
|
const w = this.why(target, opts);
|
|
2345
2342
|
const symIds = w.symbols.map((s) => s.id);
|
|
@@ -126,12 +126,22 @@ export declare class JsonStore {
|
|
|
126
126
|
* overlay. Returns the number of files removed. Invalidates the memoized load. */
|
|
127
127
|
dropAll(kind: EntityKind): number;
|
|
128
128
|
/** Persist a schema migration: rewrite every LOADABLE record in its current shape.
|
|
129
|
-
* A record that still fails validation after migration
|
|
130
|
-
*
|
|
131
|
-
*
|
|
129
|
+
* A record that still fails validation after migration (or whose migration
|
|
130
|
+
* throws) is kept in its ORIGINAL shape (never deleted, never half-migrated) and
|
|
131
|
+
* counted as `skipped`, so migration can't lose data. The caller stamps the
|
|
132
|
+
* manifest at the returned `version`: SCHEMA_VERSION only when nothing was
|
|
133
|
+
* skipped, else the FROM version, so a manual fix plus a rerun re-applies the
|
|
134
|
+
* (idempotent) migrations to the skipped records instead of skipping them.
|
|
135
|
+
* A bad record of a kind NO pending migration touches can't be fixed by a
|
|
136
|
+
* rerun, so it is counted as `invalid` instead and doesn't hold the version. */
|
|
132
137
|
persistMigration(): {
|
|
133
138
|
migrated: number;
|
|
134
139
|
skipped: number;
|
|
140
|
+
invalid: number;
|
|
141
|
+
version: number;
|
|
135
142
|
};
|
|
143
|
+
/** Migrate + validate one raw record for persistMigration; a throwing migration
|
|
144
|
+
* step counts as a failure, so the record stays in its original shape. */
|
|
145
|
+
private tryMigrate;
|
|
136
146
|
}
|
|
137
147
|
export {};
|
package/dist/store/jsonStore.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomBytes } from "node:crypto";
|
|
|
8
8
|
import { hostname } from "node:os";
|
|
9
9
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { ENTITY_KINDS, SCHEMAS } from "../core/types.js";
|
|
11
|
-
import { BASELINE_VERSION, migrateRaw, SCHEMA_VERSION } from "../core/migrate.js";
|
|
11
|
+
import { BASELINE_VERSION, hasPendingMigration, migrateRaw, SCHEMA_VERSION } from "../core/migrate.js";
|
|
12
12
|
import { writeFileAtomic } from "../core/io.js";
|
|
13
13
|
import { readStoreArtifact } from "../core/storeArtifact.js";
|
|
14
14
|
import { createdAfterLock, heldLockNonces, judgeSameHostOwner, processStartedAt, selfStartToken, startTokenOf } from "../core/procstart.js";
|
|
@@ -942,19 +942,29 @@ export class JsonStore {
|
|
|
942
942
|
return names.length;
|
|
943
943
|
}
|
|
944
944
|
/** Persist a schema migration: rewrite every LOADABLE record in its current shape.
|
|
945
|
-
* A record that still fails validation after migration
|
|
946
|
-
*
|
|
947
|
-
*
|
|
945
|
+
* A record that still fails validation after migration (or whose migration
|
|
946
|
+
* throws) is kept in its ORIGINAL shape (never deleted, never half-migrated) and
|
|
947
|
+
* counted as `skipped`, so migration can't lose data. The caller stamps the
|
|
948
|
+
* manifest at the returned `version`: SCHEMA_VERSION only when nothing was
|
|
949
|
+
* skipped, else the FROM version, so a manual fix plus a rerun re-applies the
|
|
950
|
+
* (idempotent) migrations to the skipped records instead of skipping them.
|
|
951
|
+
* A bad record of a kind NO pending migration touches can't be fixed by a
|
|
952
|
+
* rerun, so it is counted as `invalid` instead and doesn't hold the version. */
|
|
948
953
|
persistMigration() {
|
|
949
954
|
let migrated = 0;
|
|
950
955
|
let skipped = 0;
|
|
956
|
+
let invalid = 0;
|
|
951
957
|
this.cache.clear(); // every record is rewritten; drop all memoized loads
|
|
952
958
|
const version = this.schemaVersion(); // read once; we're migrating FROM this
|
|
953
959
|
for (const kind of ENTITY_KINDS) {
|
|
954
960
|
const directory = this.safeKindDirectory(kind, false);
|
|
955
961
|
if (!directory)
|
|
956
962
|
continue;
|
|
957
|
-
const
|
|
963
|
+
const pending = hasPendingMigration(kind, version);
|
|
964
|
+
const fail = () => { if (pending)
|
|
965
|
+
skipped++;
|
|
966
|
+
else
|
|
967
|
+
invalid++; };
|
|
958
968
|
const single = SINGLE_FILE[kind];
|
|
959
969
|
if (single) {
|
|
960
970
|
const f = join(directory.lexical, single);
|
|
@@ -966,23 +976,23 @@ export class JsonStore {
|
|
|
966
976
|
arr = JSON.parse(text);
|
|
967
977
|
}
|
|
968
978
|
catch {
|
|
969
|
-
|
|
979
|
+
fail();
|
|
970
980
|
continue;
|
|
971
981
|
}
|
|
972
982
|
if (!Array.isArray(arr)) {
|
|
973
|
-
|
|
983
|
+
fail();
|
|
974
984
|
continue;
|
|
975
985
|
}
|
|
976
986
|
const kept = [];
|
|
977
987
|
for (const raw of arr) {
|
|
978
|
-
const r =
|
|
988
|
+
const r = this.tryMigrate(kind, raw, version);
|
|
979
989
|
if (r.success) {
|
|
980
990
|
kept.push(r.data);
|
|
981
991
|
migrated++;
|
|
982
992
|
}
|
|
983
993
|
else {
|
|
984
994
|
kept.push(raw); // preserve unmigratable records rather than drop them
|
|
985
|
-
|
|
995
|
+
fail();
|
|
986
996
|
}
|
|
987
997
|
}
|
|
988
998
|
this.writeContainedFile(directory, f, encode(kept), this.maxBytes(kind));
|
|
@@ -995,24 +1005,38 @@ export class JsonStore {
|
|
|
995
1005
|
const text = this.readContainedFile(directory, p, this.maxBytes(kind));
|
|
996
1006
|
if (text === null)
|
|
997
1007
|
continue;
|
|
1008
|
+
// A 0-byte merge-driver tombstone holds no record (issue #37) — the
|
|
1009
|
+
// loader's rule; counting it would pin the manifest version forever.
|
|
1010
|
+
if (text.trim() === "")
|
|
1011
|
+
continue;
|
|
998
1012
|
raw = JSON.parse(text);
|
|
999
1013
|
}
|
|
1000
1014
|
catch {
|
|
1001
|
-
|
|
1015
|
+
fail();
|
|
1002
1016
|
continue;
|
|
1003
1017
|
}
|
|
1004
|
-
const r =
|
|
1018
|
+
const r = this.tryMigrate(kind, raw, version);
|
|
1005
1019
|
if (r.success) {
|
|
1006
1020
|
this.writeContainedFile(directory, p, encode(r.data), this.maxBytes(kind));
|
|
1007
1021
|
migrated++;
|
|
1008
1022
|
}
|
|
1009
1023
|
else {
|
|
1010
|
-
|
|
1024
|
+
fail(); // leave the file as-is
|
|
1011
1025
|
}
|
|
1012
1026
|
}
|
|
1013
1027
|
}
|
|
1014
1028
|
}
|
|
1015
|
-
return { migrated, skipped };
|
|
1029
|
+
return { migrated, skipped, invalid, version: skipped ? version : SCHEMA_VERSION };
|
|
1030
|
+
}
|
|
1031
|
+
/** Migrate + validate one raw record for persistMigration; a throwing migration
|
|
1032
|
+
* step counts as a failure, so the record stays in its original shape. */
|
|
1033
|
+
tryMigrate(kind, raw, version) {
|
|
1034
|
+
try {
|
|
1035
|
+
return SCHEMAS[kind].safeParse(this.migrate(kind, raw, version));
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
return { success: false };
|
|
1039
|
+
}
|
|
1016
1040
|
}
|
|
1017
1041
|
}
|
|
1018
1042
|
//# sourceMappingURL=jsonStore.js.map
|
|
@@ -795,15 +795,21 @@ function writeStateAuthorized(store, input, opts, access) {
|
|
|
795
795
|
// land its `superseded` event in THIS ledger, invisible to that partition's subscribers.
|
|
796
796
|
if (!inScope(supersededRecord))
|
|
797
797
|
throw new StateRefusal("conflict", `supersedes ${request.supersedes} is on record in ${scopePath(recordScope(supersededRecord, repo))}, not ${scopePath(request.scope)}: a write supersedes only a record in its own partition`, { incumbent_id: String(supersededRecord.id), reason: "supersede target in another partition" });
|
|
798
|
+
const matches = typeof request.expected_version === 'number'
|
|
799
|
+
? latestSeqFor(ledger, String(supersededRecord.id)) === request.expected_version
|
|
800
|
+
: request.expected_version === stateHash(supersededRecord);
|
|
798
801
|
if (stateHash(supersededVisibility ?? null) !== stateHash(nextVisibility ?? null)) {
|
|
799
802
|
if (supersededVisibility ? supersededVisibility.owner !== request.principal.id : request.principal.kind !== 'human')
|
|
800
803
|
deny();
|
|
801
|
-
|
|
802
|
-
? latestSeqFor(readLedger(hunchDir, request.scope), String(supersededRecord.id)) === request.expected_version
|
|
803
|
-
: request.expected_version === stateHash(supersededRecord);
|
|
804
|
-
if (!matches)
|
|
804
|
+
if (request.expected_version === null)
|
|
805
805
|
throw new StateRefusal('malformed', 'changing visibility during supersession requires the predecessor expected_version');
|
|
806
806
|
}
|
|
807
|
+
// A supersession's expected_version names the predecessor (#510): a stale one is a conflict.
|
|
808
|
+
if (request.expected_version !== null && !matches)
|
|
809
|
+
throw new StateRefusal("conflict", `expected_version does not match the superseded ${supersededRecord.id}`, { incumbent_id: String(supersededRecord.id), reason: "expected_version mismatch" });
|
|
810
|
+
}
|
|
811
|
+
else if (request.supersedes && request.expected_version !== null) {
|
|
812
|
+
throw new StateRefusal("conflict", `expected_version names ${request.supersedes}, which is not on record`, { incumbent_id: request.supersedes, reason: "expected_version without incumbent" });
|
|
807
813
|
}
|
|
808
814
|
if (!incumbent && !supersededRecord && nextVisibility?.owner !== undefined && nextVisibility.owner !== request.principal.id)
|
|
809
815
|
deny();
|
|
@@ -873,6 +879,13 @@ function writeStateAuthorized(store, input, opts, access) {
|
|
|
873
879
|
if (!ok)
|
|
874
880
|
throw new StateRefusal("conflict", `expected_version does not match the incumbent ${id}`, { incumbent_id: id, reason: "expected_version mismatch" });
|
|
875
881
|
}
|
|
882
|
+
// Compare-and-swap with no incumbent (#510): a writer naming a version believes a record exists,
|
|
883
|
+
// so a fresh create under it is a stale read, never silently accepted. Numeric 0 is the ledger's
|
|
884
|
+
// "never seen" and asserts absence; a supersession's expected_version names its predecessor.
|
|
885
|
+
if (!existing && request.expected_version !== null && !request.supersedes
|
|
886
|
+
&& !(typeof request.expected_version === "number" && latestSeqFor(ledger, id) === request.expected_version)) {
|
|
887
|
+
throw new StateRefusal("conflict", `expected_version ${JSON.stringify(request.expected_version)} names a version, but ${id} has none on record: pass null to create it (0 asserts the id was never written)`, { incumbent_id: id, reason: "expected_version without incumbent" });
|
|
888
|
+
}
|
|
876
889
|
if (replayLink) {
|
|
877
890
|
const recordHash = stateHash(replayLink);
|
|
878
891
|
appendChanges(hunchDir, request.scope, missingEventFor(ledger, facet, replayLink, request.principal.id), { key: request.idempotency_key, entry: { record_id: replayLink.id, record_hash: recordHash, payload_hash: hash, facet } }, now, opts.ledgerCache?.ledger);
|
|
@@ -111,8 +111,9 @@ export interface SynthProvider {
|
|
|
111
111
|
}
|
|
112
112
|
/** Every selectable synthesis mode. `auto` is a preference value rather than a
|
|
113
113
|
* provider: it resolves the invocation's origin without choosing by availability.
|
|
114
|
-
* "openai-compat" is the opt-in local
|
|
115
|
-
* LM Studio
|
|
114
|
+
* "openai-compat" is the opt-in local or self-hosted HTTP provider (Ollama, vLLM,
|
|
115
|
+
* LM Studio and so on). It is not a subscription, but it can be selected
|
|
116
|
+
* explicitly like one. */
|
|
116
117
|
export declare const SYNTH_PROVIDER_NAMES: readonly ["claude-cli", "codex-cli", "cursor-agent", "kimi-cli", "openai-compat", "deterministic"];
|
|
117
118
|
export declare const SYNTH_PREFERENCES: readonly ["auto", "claude-cli", "codex-cli", "cursor-agent", "kimi-cli", "openai-compat", "deterministic"];
|
|
118
119
|
export type SynthProviderName = (typeof SYNTH_PROVIDER_NAMES)[number];
|
|
@@ -172,8 +173,11 @@ export declare function safeModel(v: string | undefined, fallback: undefined): s
|
|
|
172
173
|
* the explicit skip flag preserves that neutral-cwd isolation without loading a
|
|
173
174
|
* target repo's agent rules or MCP configuration. */
|
|
174
175
|
export declare function codexExecArgs(model?: string): string[];
|
|
176
|
+
/** Sanitize HUNCH_SYNTH_TIMEOUT_MS: a finite number > 0, else `fallback`. */
|
|
175
177
|
export declare function safeTimeout(v: string | undefined, fallback: number): number;
|
|
178
|
+
/** Sanitize HUNCH_SYNTH_MAX_TOKENS: a finite number > 0, else `fallback`. */
|
|
176
179
|
export declare function safeMaxTokens(v: string | undefined, fallback: number): number;
|
|
180
|
+
/** True only when the user explicitly opted in to public (possibly metered) hosts. */
|
|
177
181
|
export declare function meteredHostsAllowed(env?: NodeJS.ProcessEnv): boolean;
|
|
178
182
|
export declare class OpenAICompatProvider extends PromptSynthProvider {
|
|
179
183
|
readonly name = "openai-compat";
|
|
@@ -182,9 +186,10 @@ export declare class OpenAICompatProvider extends PromptSynthProvider {
|
|
|
182
186
|
}
|
|
183
187
|
/** Best-effort: does the configured openai-compat endpoint look like Ollama with
|
|
184
188
|
* an UNSET num_ctx? Returns
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
189
|
+
* a warning string when the model does not pin num_ctx, or null when the
|
|
190
|
+
* endpoint is unreachable, does not look like Ollama's /api/show response shape,
|
|
191
|
+
* or already has num_ctx set. Diagnostics only: it never throws and never blocks.
|
|
192
|
+
* Deliberately does NOT try to
|
|
188
193
|
* report the model's effective context length: modern Ollama defaults may come
|
|
189
194
|
* from server configuration or VRAM tiers, and model_info keys are not a stable
|
|
190
195
|
* parse target. We therefore report only the observed fact — whether num_ctx is
|
|
@@ -203,19 +208,12 @@ export declare class DeterministicProvider implements SynthProvider {
|
|
|
203
208
|
* tagged. If nothing parses, hand the raw output to the mapper (→ it finds the JSON
|
|
204
209
|
* draft or throws → deterministic fallback). Tolerant by design: drift degrades, never crashes. */
|
|
205
210
|
export declare function extractCodexText(out: string): string;
|
|
206
|
-
/** Test-only:
|
|
207
|
-
*
|
|
208
|
-
* cached by an earlier call in the same process. Never call from production code. */
|
|
211
|
+
/** Test-only: empty the availability cache so a test that changes env vars
|
|
212
|
+
* mid-process is not served a stale cached result. */
|
|
209
213
|
export declare function __resetAvailabilityCacheForTests(): void;
|
|
210
|
-
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* users reach for that name first. Applied to the HUNCH_SYNTH_PROVIDER env var in
|
|
214
|
-
* resolveSynthesisProvider below, and exported so the `hunch provider <name>` CLI
|
|
215
|
-
* command (index.ts) normalizes it the same way before validating/persisting a
|
|
216
|
-
* local preference — the two paths must agree, or a user who sets one and reads
|
|
217
|
-
* the other back gets a confusing "unknown provider" message for a name that
|
|
218
|
-
* actually works. */
|
|
214
|
+
/** The provider is not Ollama-specific, but Ollama is the name users try first, so
|
|
215
|
+
* "ollama" is an alias for "openai-compat". The env var and `hunch provider <name>`
|
|
216
|
+
* both normalize through here so the two paths agree. */
|
|
219
217
|
export declare function normalizeProviderName(v: string | undefined): string | undefined;
|
|
220
218
|
/** Read a per-user, gitignored choice. Invalid/missing local state is treated as auto;
|
|
221
219
|
* `writeSynthesisPreference` refuses to overwrite malformed data so this forgiveness
|
|
@@ -258,10 +256,10 @@ export declare class EnsembleProvider implements SynthProvider {
|
|
|
258
256
|
export declare function selectEnsemble(opts?: ProviderSelectionOptions & {
|
|
259
257
|
samples?: number;
|
|
260
258
|
}): Promise<EnsembleProvider | null>;
|
|
261
|
-
/** Pick
|
|
262
|
-
* synthesis would use (subscription CLI or the opt-in openai-compat endpoint),
|
|
263
|
-
*
|
|
264
|
-
* resolves to the deterministic fallback
|
|
259
|
+
/** Pick the provider for the Critic pass: the same resolved provider normal
|
|
260
|
+
* synthesis would use (a subscription CLI or the opt-in openai-compat endpoint),
|
|
261
|
+
* under the same env / local-preference / auto policy. Returns null when that
|
|
262
|
+
* resolves to the deterministic fallback, in which case verification does nothing. The
|
|
265
263
|
* un-audited draft stands (graceful degradation; dec_18a81c8291). */
|
|
266
264
|
export declare function selectVerifier(opts?: ProviderSelectionOptions): Promise<SynthProvider | null>;
|
|
267
265
|
/** Map model text → RelevanceVerdict, or null when nothing usable parses (→ the
|