klypix-mcp 1.44.0 β 1.45.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/package.json +2 -2
- package/src/global-brain-hook.mjs +9 -0
- package/src/klypix-format.mjs +300 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"node": ">=18"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
|
-
"test": "node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/presence-visibility.mjs && node test/cli-args.mjs && node test/uninstall.mjs"
|
|
68
|
+
"test": "node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/presence-visibility.mjs && node test/cli-args.mjs && node test/uninstall.mjs"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
@@ -1667,6 +1667,15 @@ async function capture(lib) {
|
|
|
1667
1667
|
process.stderr.write(`[brain] β³ likely fulfilled (${f.cov}): "${f.item.slice(0, 70)}"${rest} ${act}\n`);
|
|
1668
1668
|
}
|
|
1669
1669
|
}
|
|
1670
|
+
// π οΈ staleness receipts (2026-08-01): a captured π that appears to REMOVE a
|
|
1671
|
+
// limitation a standing skill still asserts. The skill is NEVER archived β
|
|
1672
|
+
// the receipt hands the session a ready-to-fill ~ amendment, because a rule
|
|
1673
|
+
// whose limitation half died usually keeps a trap half worth saving.
|
|
1674
|
+
if (Array.isArray(stats.skillStale) && stats.skillStale.length) {
|
|
1675
|
+
for (const f of stats.skillStale.slice(0, 3)) {
|
|
1676
|
+
process.stderr.write(`[brain] β οΈ rule may be obsolete (${f.cov}): skill "${f.skill.slice(0, 70)}" asserts "${String(f.clause || '').slice(0, 60)}" β this ship appears to remove it. If so, amend: ${f.marker} (retire by naming it in closes:, or dismiss via brain_connect relationship:"not_fulfilled")\n`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1670
1679
|
appendJsonl(LEDGER, { ts: nowIso(), mode: 'capture', stats, decisions: ledger }, 1000);
|
|
1671
1680
|
appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'capture', ok: true, brainBytes: brainBytes(), added: stats.added, skipped: ledger.filter(d => d.action.startsWith('skipped')).length }, 500);
|
|
1672
1681
|
}
|
package/src/klypix-format.mjs
CHANGED
|
@@ -1343,10 +1343,57 @@ export function structToBrief(struct, { recentDays = 14, maxRecent = 40, maxMile
|
|
|
1343
1343
|
}
|
|
1344
1344
|
if (skills.length) {
|
|
1345
1345
|
push('', '## π οΈ Skills β how we do things here (reusable; applies every session)');
|
|
1346
|
+
// π οΈβπ staleness suffix (2026-08-01 incident): the brief is the ONE
|
|
1347
|
+
// surface every session reads, and skills never age out of it β so a
|
|
1348
|
+
// rule encoding a since-removed limitation lies here every morning
|
|
1349
|
+
// with a freshness badge next to it. Persisted 'may obsolete' edges
|
|
1350
|
+
// apply first (capture-time detection); the fresh scan is bounded to
|
|
1351
|
+
// the SHOWN clause-bearing skills Γ milestones newer than the oldest
|
|
1352
|
+
// of them (milestone token sets built once, lazily β most briefs have
|
|
1353
|
+
// zero clause-bearing skills and pay nothing).
|
|
1354
|
+
const staleBySkill = new Map();
|
|
1355
|
+
try {
|
|
1356
|
+
const shownSkills = skills.slice(0, maxSkills);
|
|
1357
|
+
const persisted = obsolescenceOverlaysFor(struct, shownSkills);
|
|
1358
|
+
for (const [id, o] of persisted) staleBySkill.set(id, o.by);
|
|
1359
|
+
const clauseSkills = shownSkills
|
|
1360
|
+
.filter(c => !staleBySkill.has(c.id))
|
|
1361
|
+
.map(c => ({ c, claims: extractLimitationClaims(c.text) }))
|
|
1362
|
+
.filter(x => x.claims.length);
|
|
1363
|
+
if (clauseSkills.length) {
|
|
1364
|
+
const oldest = Math.min(...clauseSkills.map(x => x.c.createdAt || 0));
|
|
1365
|
+
const newerMiles = miles.filter(m => (m.createdAt || 0) > oldest);
|
|
1366
|
+
if (newerMiles.length) {
|
|
1367
|
+
const settled = new Set();
|
|
1368
|
+
for (const cn of struct.connections || []) {
|
|
1369
|
+
if (cn.label === 'may obsolete' || DISMISSAL_RELS.has(cn.relationship)) settled.add(`${cn.fromId}|${cn.toId}`);
|
|
1370
|
+
}
|
|
1371
|
+
const mPre = newerMiles.map(m => ({ m, idx: stemIndex(tokenSet(m.text)) }));
|
|
1372
|
+
let df = null;
|
|
1373
|
+
const dfMap = () => (df ??= buildStemDf(struct));
|
|
1374
|
+
for (const { c, claims } of clauseSkills) {
|
|
1375
|
+
let best = null, bestScore = 0;
|
|
1376
|
+
for (const { m, idx: mIdx } of mPre) {
|
|
1377
|
+
if ((m.createdAt || 0) <= (c.createdAt || 0) || settled.has(`${c.id}|${m.id}`)) continue;
|
|
1378
|
+
for (const cl of claims) {
|
|
1379
|
+
const clIdx = stemIndex(cl.tokens);
|
|
1380
|
+
const cov = coverageOf(new Set(clIdx.keys()), new Set(mIdx.keys()));
|
|
1381
|
+
const anchors = sharedAnchors(clIdx, mIdx, dfMap(), { exclude: structuralStems(c.area, m.area) });
|
|
1382
|
+
if (!serveTimeAccepts({ cov, anchors, size: clIdx.size }, (c.area || '') === (m.area || ''))) continue;
|
|
1383
|
+
const score = cov + anchors.length * 0.2;
|
|
1384
|
+
if (score > bestScore) { bestScore = score; best = m; }
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (best) staleBySkill.set(c.id, flat(best.text).slice(0, 70));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
} catch { /* staleness suffix is best-effort β never break the brief */ }
|
|
1346
1392
|
let shown = 0;
|
|
1347
1393
|
for (const c of skills.slice(0, maxSkills)) {
|
|
1348
1394
|
if (used > BUDGET_CHARS * 0.85) break;
|
|
1349
|
-
|
|
1395
|
+
const ob = staleBySkill.get(c.id);
|
|
1396
|
+
push(`- ${fr(c)}${flat(c.text)}${ob ? ` β οΈ a newer π may have removed this limitation (β${ob}β¦β) β verify before applying; ~ amend, closes:<its title> to retire, or dismiss via brain_connect not_fulfilled` : ''}`);
|
|
1350
1397
|
shown++;
|
|
1351
1398
|
}
|
|
1352
1399
|
if (shown < skills.length) push(`- β¦and ${skills.length - shown} more skill(s) β search the brain.`);
|
|
@@ -1725,6 +1772,64 @@ export function rankForQuestion(struct, question, { semantic = null, k = 10, as_
|
|
|
1725
1772
|
}
|
|
1726
1773
|
} catch { /* pairing is a best-effort overlay β never fail the answer */ }
|
|
1727
1774
|
}
|
|
1775
|
+
// SERVE-TIME π οΈβπ OBSOLESCENCE (2026-08-01 incident): the β pass above
|
|
1776
|
+
// cannot see the class where a SKILL encodes a since-removed limitation β
|
|
1777
|
+
// skills are excluded from claim sources by design, get a +1 ranking boost,
|
|
1778
|
+
// and never age out, so "Chat has no tools" kept outranking the same-day
|
|
1779
|
+
// milestone that shipped chat tools, and an agent asserted the limitation
|
|
1780
|
+
// to the founder as current fact. Persisted 'may obsolete' edges apply
|
|
1781
|
+
// first; then the same in-answer pairing discipline as the β pass, but
|
|
1782
|
+
// coverage runs against the skill's LIMITATION CLAUSES (a skill is long;
|
|
1783
|
+
// its whole text would drown the claim). Negative capability claims are
|
|
1784
|
+
// the highest-risk kind β the fix that falsifies one is exactly the card
|
|
1785
|
+
// that will not match a question phrased around the limitation β so the
|
|
1786
|
+
// hedge renders even when the milestone barely made the hit set.
|
|
1787
|
+
if (!timeTravel) {
|
|
1788
|
+
try {
|
|
1789
|
+
const skillHits = hits.filter(h => !h.correction && !h.archived && isSkillCard(h.card));
|
|
1790
|
+
if (skillHits.length) {
|
|
1791
|
+
const persisted = obsolescenceOverlaysFor(struct, skillHits.map(h => h.card));
|
|
1792
|
+
for (const sh of skillHits) if (persisted.has(sh.card.id)) sh.obsolescence = persisted.get(sh.card.id);
|
|
1793
|
+
const fresh = skillHits.filter(h => !h.obsolescence);
|
|
1794
|
+
const mileHits = hits.filter(h => isMilestoneCard(h.card) && !/^archive$/i.test(h.card.area || ''));
|
|
1795
|
+
if (fresh.length && mileHits.length) {
|
|
1796
|
+
const settled = new Set();
|
|
1797
|
+
for (const cn of struct.connections || []) {
|
|
1798
|
+
if (cn.label === 'may obsolete' || DISMISSAL_RELS.has(cn.relationship)) settled.add(`${cn.fromId}|${cn.toId}`);
|
|
1799
|
+
}
|
|
1800
|
+
let df = null;
|
|
1801
|
+
const dfMap = () => (df ??= buildStemDf(struct));
|
|
1802
|
+
const mPre = mileHits.map(mh => ({ mh, idx: stemIndex(tokenSet(mh.card.text)) }));
|
|
1803
|
+
for (const sh of fresh) {
|
|
1804
|
+
const claims = extractLimitationClaims(sh.card.text);
|
|
1805
|
+
if (!claims.length) continue; // no state claim β advice; never flagged
|
|
1806
|
+
let best = null, bestScore = 0, bestClause = null;
|
|
1807
|
+
for (const { mh, idx: mIdx } of mPre) {
|
|
1808
|
+
const s = sh.card, m = mh.card;
|
|
1809
|
+
if (m.id === s.id || (m.createdAt || 0) <= (s.createdAt || 0)) continue;
|
|
1810
|
+
if (settled.has(`${s.id}|${m.id}`)) continue;
|
|
1811
|
+
const sim = typeof pairSim === 'function' ? pairSim(s.id, m.id) : null;
|
|
1812
|
+
for (const cl of claims) {
|
|
1813
|
+
const clIdx = stemIndex(cl.tokens);
|
|
1814
|
+
const cov = coverageOf(new Set(clIdx.keys()), new Set(mIdx.keys()));
|
|
1815
|
+
const anchors = sharedAnchors(clIdx, mIdx, dfMap(), { exclude: structuralStems(s.area, m.area) });
|
|
1816
|
+
const lex = { cov, anchors, size: clIdx.size };
|
|
1817
|
+
const accept = (sim != null && sim >= 0.55 && lex.size >= SERVE_MIN_STEMS && (anchors.length >= 1 || cov >= 0.2))
|
|
1818
|
+
|| serveTimeAccepts(lex, (s.area || '') === (m.area || ''));
|
|
1819
|
+
if (!accept) continue;
|
|
1820
|
+
const score = (sim ?? 0) + cov + anchors.length * 0.2;
|
|
1821
|
+
if (score > bestScore) { bestScore = score; best = m; bestClause = cl.clause; }
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (best) {
|
|
1825
|
+
const head = String(best.text || '').replace(/\s+/g, ' ').trim().slice(0, 100);
|
|
1826
|
+
sh.obsolescence = { by: head, byId: best.id, clause: bestClause, unconfirmed: true };
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
} catch { /* best-effort overlay β never fail the answer */ }
|
|
1832
|
+
}
|
|
1728
1833
|
return { hits, total: scored.length, tokens, statusShaped, statusStrong };
|
|
1729
1834
|
}
|
|
1730
1835
|
|
|
@@ -1761,6 +1866,12 @@ export function questionContextToMarkdown(question, result, { mode = 'lexical',
|
|
|
1761
1866
|
block += h.fulfillment.unconfirmed
|
|
1762
1867
|
? `\n\n β³ POSSIBLY FULFILLED (serve-time hint β detected within this answer, no confirmed link): a newer milestone in this same answer may cover this open item: β${flat(h.fulfillment.by).slice(0, 200)}β. VERIFY before treating it as still-to-do; confirm with a β marker if done, or dismiss via brain_connect relationship:"not_fulfilled".`
|
|
1763
1868
|
: `\n\n β³ LIKELY FULFILLED β a newer milestone appears to cover this open item: β${flat(h.fulfillment.by).slice(0, 200)}β. Verify before treating it as still-to-do; confirm with a β marker if done.`;
|
|
1869
|
+
} else if (h.obsolescence) {
|
|
1870
|
+
// A π οΈ rule asserting a since-removed limitation (2026-08-01
|
|
1871
|
+
// incident). Same hedging discipline as β³: a hint instructs the
|
|
1872
|
+
// reader to verify, never to inherit the machine's guess β but the
|
|
1873
|
+
// DIRECTION flips: for current capability, trust the NEWER card.
|
|
1874
|
+
block += `\n\n β οΈ RULE MAY BE OBSOLETE${h.obsolescence.unconfirmed ? ' (serve-time hint β no confirmed link)' : ''}: this skill asserts a limitation${h.obsolescence.clause ? ` β β${flat(h.obsolescence.clause).slice(0, 140)}β` : ''} that a newer milestone appears to REMOVE: β${flat(h.obsolescence.by).slice(0, 200)}β. For current capability trust the newer card; VERIFY before citing this limitation as still true. If obsolete, amend the skill (~ marker), or retire it deliberately by NAMING it in a closes: (fuzzy β/supersede never touch a π οΈ); if it still holds, dismiss via brain_connect relationship:"not_fulfilled".`;
|
|
1764
1875
|
}
|
|
1765
1876
|
if (used + block.length + 2 > budgetChars && shown > 0) { out.push(`\n_β¦and ${hits.length - shown} more matched card(s) omitted for length β narrow the question or use search for the rest._`); break; }
|
|
1766
1877
|
out.push(block, '');
|
|
@@ -3040,7 +3151,7 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
|
|
|
3040
3151
|
// corrections[] lists cross-area/low-bar supersedes driven by a correction
|
|
3041
3152
|
// cue, so the surface (brain_note result / capture stderr) can say WHAT was
|
|
3042
3153
|
// archived and how to undo β the confirmation channel for the widened match.
|
|
3043
|
-
const stats = { added: 0, superseded: 0, resolved: 0, linked: 0, updated: 0, closed: 0, merged: 0, corrections: [], fulfillCandidates: [] };
|
|
3154
|
+
const stats = { added: 0, superseded: 0, resolved: 0, linked: 0, updated: 0, closed: 0, merged: 0, corrections: [], fulfillCandidates: [], skillStale: [] };
|
|
3044
3155
|
|
|
3045
3156
|
// Pass 1 β resolutions + supersede marking operate on EXISTING cards.
|
|
3046
3157
|
if (resolutions.length || cards.length || updates.length) {
|
|
@@ -3340,8 +3451,18 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
|
|
|
3340
3451
|
// generic to trust beyond the strongest few.
|
|
3341
3452
|
const matches = [];
|
|
3342
3453
|
for (const c of liveTextCards()) {
|
|
3343
|
-
if (/π /.test(c.text)) continue; // skills are standing reference β a closes: must never archive one (mirror the supersede guard)
|
|
3344
3454
|
const ct = (c.title || '').trim().toLowerCase();
|
|
3455
|
+
// A π οΈ retires ONLY by being NAMED: exact/prefix match of its
|
|
3456
|
+
// glyph-and-area-stripped title. Never the contains path (titles
|
|
3457
|
+
// are derived from prose, so a skill that merely MENTIONS the
|
|
3458
|
+
// target span mid-sentence would be swept β review-B's exact
|
|
3459
|
+
// trap), and never token coverage (2026-08-01: naming is a
|
|
3460
|
+
// deliberate human act; overlap is not).
|
|
3461
|
+
if (/π /.test(c.text)) {
|
|
3462
|
+
const core = ct.replace(/^[^:\n]{1,40}:\s*/, '').replace(/^[π οΈβπ―πβ
\s]+/u, '');
|
|
3463
|
+
if (core && wantTitle.length >= 6 && (core === wantTitle || core.startsWith(wantTitle) || wantTitle.startsWith(core))) matches.push({ c, cov: 1 });
|
|
3464
|
+
continue;
|
|
3465
|
+
}
|
|
3345
3466
|
// Title fast-path: exact / prefix (β₯6 chars), or the card title
|
|
3346
3467
|
// CONTAINS the target β the contains variant needs a LONGER target
|
|
3347
3468
|
// (β₯10) because a short generic word ("sandbox") appears in many
|
|
@@ -3409,6 +3530,35 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
|
|
|
3409
3530
|
});
|
|
3410
3531
|
}
|
|
3411
3532
|
}
|
|
3533
|
+
|
|
3534
|
+
// π οΈβπ OBSOLESCENCE CROSS-CHECK (2026-08-01 incident) β the write-side
|
|
3535
|
+
// twin of the serve-time skill pass, mirroring T6 exactly one class
|
|
3536
|
+
// over: a new π is reconciled against live SKILLS' limitation clauses
|
|
3537
|
+
// ("chat has no tools") the way T6 reconciles it against open claims.
|
|
3538
|
+
// Output is a receipt (stats.skillStale, printed by the hook with a
|
|
3539
|
+
// ready-to-emit ~ amendment) plus a dashed amber 'may obsolete' edge
|
|
3540
|
+
// for coverage-grade pairs. NEVER archives β the supersede and closes:
|
|
3541
|
+
// passes deliberately refuse to retire a π οΈ, and this pass inherits
|
|
3542
|
+
// that: a rule leaves the brain by human hand or not at all.
|
|
3543
|
+
for (const card of cards) {
|
|
3544
|
+
if (!/π/.test(card.text) || card.__fromResolve) continue;
|
|
3545
|
+
const cands = findSkillObsolescenceCandidates(struct, [{ text: card.text, createdAt: now }], { maxPerMilestone: 2 });
|
|
3546
|
+
if (!cands.length) continue;
|
|
3547
|
+
card.__obsoletes = cands.filter(c => c.via !== 'anchor').map(c => c.skill.id);
|
|
3548
|
+
for (const c of cands) {
|
|
3549
|
+
stats.skillStale.push({
|
|
3550
|
+
skill: String(c.skill.text || '').replace(/\s+/g, ' ').slice(0, 90),
|
|
3551
|
+
area: c.skill.area || null,
|
|
3552
|
+
clause: c.clause,
|
|
3553
|
+
cov: c.cov,
|
|
3554
|
+
...(c.via ? { via: c.via } : {}),
|
|
3555
|
+
// The suggested act is an AMENDMENT (~), not an archive: most
|
|
3556
|
+
// stale rules want the correction written into them β the
|
|
3557
|
+
// trap half often survives even when the limitation half died.
|
|
3558
|
+
marker: `π§ BRAIN [${c.skill.area || 'Notes'}] ~: ${String(c.skill.title || c.skill.text).replace(/\s+/g, ' ').replace(/^π οΈ?\s*/, '').slice(0, 60)} β CORRECTION: <what this ship changed>`,
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3412
3562
|
stampBrainKind(manifest);
|
|
3413
3563
|
work = await finalizeBrainZip(zip, canvas, manifest, now);
|
|
3414
3564
|
}
|
|
@@ -3450,6 +3600,10 @@ export async function captureIntoBrain(buffer, { cards = [], resolutions = [], u
|
|
|
3450
3600
|
// Dashed, muted hint β a suggestion must never render pixel-identical
|
|
3451
3601
|
// to a confirmed 'closed by' verdict edge on the human's canvas.
|
|
3452
3602
|
for (const cid of (card.__fulfills || [])) addConn(cid, created.id, 'likely closed by', undefined, { style: 'dashed', width: 1.5, color: 'rgba(16,185,129,0.55)' });
|
|
3603
|
+
// π οΈ staleness hints are AMBER, never the fulfilment emerald β "this
|
|
3604
|
+
// ship may retire that rule" and "this ship may close that task" must
|
|
3605
|
+
// stay visually distinct on the human's canvas.
|
|
3606
|
+
for (const sid of (card.__obsoletes || [])) addConn(sid, created.id, 'may obsolete', undefined, { style: 'dashed', width: 1.5, color: 'rgba(245,158,11,0.6)' });
|
|
3453
3607
|
for (const link of (created.links || [])) {
|
|
3454
3608
|
const want = String(link).trim().toLowerCase();
|
|
3455
3609
|
if (!want) continue;
|
|
@@ -4047,6 +4201,149 @@ export function fulfillmentOverlaysFor(struct, cards) {
|
|
|
4047
4201
|
return map;
|
|
4048
4202
|
}
|
|
4049
4203
|
|
|
4204
|
+
// ββ π οΈβπ obsolescence (2026-08-01 field incident) ββββββββββββββββββββββββββ
|
|
4205
|
+
// A π οΈ skill that encodes a TEMPORARY limitation is more dangerous stale than
|
|
4206
|
+
// an open β: skills are BUILT to resurface every session, never age out, and
|
|
4207
|
+
// carry a ranking boost β so when a later π removes the limitation, the wrong
|
|
4208
|
+
// half keeps outranking the fix for any question phrased around the limitation.
|
|
4209
|
+
// Field case: an agent told the founder "Chat can talk but not act" off skill
|
|
4210
|
+
// 32d1c2a ("Chat (Gemini Flash) has no toolsβ¦") while milestone 2440361 β SAME
|
|
4211
|
+
// DAY β had shipped native tool-use in chat. An open β at least reads as
|
|
4212
|
+
// unsettled; a π οΈ reads as settled law. The ββπ machinery cannot see this
|
|
4213
|
+
// class (skills are excluded from claim sources BY DESIGN, and must stay
|
|
4214
|
+
// excluded β a milestone must never archive advice), so this is its sibling:
|
|
4215
|
+
// detect and HEDGE, never retire. Retiring a rule stays a human act.
|
|
4216
|
+
//
|
|
4217
|
+
// The extractor is the precision gate. It matches STATE claims β sentences
|
|
4218
|
+
// asserting what the system currently does or lacks ("has no tools", "returns
|
|
4219
|
+
// mock streams", "not implemented") β and refuses IMPERATIVE advice ("never
|
|
4220
|
+
// set backgroundThrottling:false", "always dedup zKeys"), which is the
|
|
4221
|
+
// evergreen content skills exist for. A skill with no state claim can never be
|
|
4222
|
+
// flagged, so the failure mode of an over-eager cue is a missed hint, not a
|
|
4223
|
+
// slandered rule.
|
|
4224
|
+
const LIMITATION_CUE_RE = new RegExp([
|
|
4225
|
+
/\b(?:has|have|had) no \p{L}/u.source,
|
|
4226
|
+
/\bno \p{L}+ (?:yet|at all|anywhere)\b/u.source,
|
|
4227
|
+
/\bcan(?:not|['β]t) \p{L}/u.source,
|
|
4228
|
+
/\b(?:does|do)(?:es)?(?: not|n['β]t) (?:support|have|act|work|exist|persist|stream|fire|write|read|speak|run|apply|reach|see|know|check|enforce)\b/u.source,
|
|
4229
|
+
/\bnot (?:implemented|wired|supported|available|built|shipped|published|deployed|enforced|persisted|possible|reachable|exposed|functional)\b/u.source,
|
|
4230
|
+
/\breturns? (?:a )?(?:mock|stub|null|nothing)\b/u.source,
|
|
4231
|
+
/\bis (?:dead|unused|missing|absent|unreachable|unwired|talk[- ]?only)\b/u.source,
|
|
4232
|
+
/\bonly (?:works|runs|streams|answers|covers|supports|reads)\b/u.source,
|
|
4233
|
+
/\bstill (?:missing|absent|manual|unwired|blocked|mocked?)\b/u.source,
|
|
4234
|
+
].join('|'), 'iu');
|
|
4235
|
+
// Imperative openers = advice, not state. Anchored to the CLAUSE start so
|
|
4236
|
+
// "Chat has no tools" (subject-first) passes while "never set X" is refused.
|
|
4237
|
+
const ADVICE_OPENER_RE = /^(?:never|always|do not|don['β]t|avoid|prefer|remember|must|treat|use|keep|before|when|if)\b/i;
|
|
4238
|
+
export function extractLimitationClaims(text) {
|
|
4239
|
+
// Same unwrap discipline as extractOpenClauses: stored hard-wraps would
|
|
4240
|
+
// otherwise truncate every sentence at the first wrap point. Same
|
|
4241
|
+
// version-literal-safe sentence split ('1.3.28' survives).
|
|
4242
|
+
const t = normalizeWrappedProse(String(text || ''));
|
|
4243
|
+
const out = [];
|
|
4244
|
+
for (const raw of t.split(/[!?]\s+|\.(?=\s|$)/)) {
|
|
4245
|
+
let clause = raw.replace(/^[\s\-βββ’Β·"ββ'ββ()[\]]+/, '').trim();
|
|
4246
|
+
if (!clause || clause.length < 12) continue;
|
|
4247
|
+
if (ADVICE_OPENER_RE.test(clause)) continue;
|
|
4248
|
+
if (!LIMITATION_CUE_RE.test(clause)) continue;
|
|
4249
|
+
// CONTRAST CUT: the limitation is the pre-contrast span β "Chat has no
|
|
4250
|
+
// tools, but the system prompt claimedβ¦" claims only "Chat has no
|
|
4251
|
+
// tools"; the tail is elaboration whose tokens drown the coverage
|
|
4252
|
+
// denominator (measured on the incident card: whole-sentence cov 0.31
|
|
4253
|
+
// β cut cov 0.75). Keep the cut only if it still carries the cue.
|
|
4254
|
+
const cut = clause.split(/,?\s+(?:but|however|yet|whereas|--+|β)\s+/i)[0].trim();
|
|
4255
|
+
if (cut.length >= 12 && LIMITATION_CUE_RE.test(cut) && claimTokens(cut).size >= 3) clause = cut;
|
|
4256
|
+
const tokens = claimTokens(clause);
|
|
4257
|
+
if (tokens.size < 3) continue; // tiny claims match everything
|
|
4258
|
+
out.push({ clause: clause.slice(0, 160), tokens });
|
|
4259
|
+
if (out.length >= 4) break; // a skill is one lesson, not a spec sheet
|
|
4260
|
+
}
|
|
4261
|
+
return out;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
// One-sided scan: which live π οΈ limitation claims do these milestones appear to
|
|
4265
|
+
// REMOVE? Mirrors findFulfillmentCandidates (bounded, receipt-bearing, archives
|
|
4266
|
+
// nothing): a milestone must post-date the skill, acceptance is clause coverage
|
|
4267
|
+
// OR rare-anchor corroboration, dismissals (`not_fulfilled`) and existing hint
|
|
4268
|
+
// edges are never re-suggested, and one generic ship is capped so it can never
|
|
4269
|
+
// spray edges across the rulebook.
|
|
4270
|
+
export function findSkillObsolescenceCandidates(struct, milestones, { coverAt = 0.6, maxPerMilestone = 3 } = {}) {
|
|
4271
|
+
if (!struct || !Array.isArray(struct.cards) || !Array.isArray(milestones) || !milestones.length) return [];
|
|
4272
|
+
const isArchived = (c) => /^archive$/i.test(c.area || '');
|
|
4273
|
+
const liveSkills = struct.cards.filter(c => c.type !== 'container' && (c.text || '').trim()
|
|
4274
|
+
&& !isArchived(c) && isSkillCard(c) && !/β©|‡/.test(c.text));
|
|
4275
|
+
if (!liveSkills.length) return [];
|
|
4276
|
+
const settled = new Set();
|
|
4277
|
+
for (const cn of struct.connections || []) {
|
|
4278
|
+
if (cn.label === 'may obsolete' || DISMISSAL_RELS.has(cn.relationship)) settled.add(`${cn.fromId}|${cn.toId}`);
|
|
4279
|
+
}
|
|
4280
|
+
const mPre = milestones.map(m => ({ m, idx: stemIndex(tokenSet(m.text)) }));
|
|
4281
|
+
let df = null;
|
|
4282
|
+
const dfMap = () => (df ??= buildStemDf(struct));
|
|
4283
|
+
const out = [];
|
|
4284
|
+
for (const s of liveSkills) {
|
|
4285
|
+
const claims = extractLimitationClaims(s.text);
|
|
4286
|
+
for (const cl of claims) {
|
|
4287
|
+
const clIdx = stemIndex(cl.tokens);
|
|
4288
|
+
const clStems = new Set(clIdx.keys());
|
|
4289
|
+
for (const { m, idx: mIdx } of mPre) {
|
|
4290
|
+
if (m.id && m.id === s.id) continue;
|
|
4291
|
+
if (m.id && settled.has(`${s.id}|${m.id}`)) continue;
|
|
4292
|
+
if (Number.isFinite(m.createdAt) && m.createdAt <= (s.createdAt || 0)) continue; // a rule written AFTER the ship is post-fix knowledge, not stale
|
|
4293
|
+
const cov = coverageOf(clStems, new Set(mIdx.keys()));
|
|
4294
|
+
let viaAnchor = false;
|
|
4295
|
+
if (cov < coverAt) {
|
|
4296
|
+
const anchors = sharedAnchors(clIdx, mIdx, dfMap(), { exclude: structuralStems(s.area, m.area) });
|
|
4297
|
+
if (!anchorsSufficient(anchors, (s.area || '') === (m.area || ''), cov)) continue;
|
|
4298
|
+
viaAnchor = true;
|
|
4299
|
+
}
|
|
4300
|
+
out.push({ skill: s, clause: cl.clause, milestone: m, cov: Math.round(cov * 100) / 100, ...(viaAnchor ? { via: 'anchor' } : {}) });
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
// Best candidate per (skill, milestone) pair, then per-milestone cap.
|
|
4305
|
+
const byPair = new Map();
|
|
4306
|
+
for (const c of out) {
|
|
4307
|
+
const key = `${c.skill.id}|${c.milestone.id || milestones.indexOf(c.milestone)}`;
|
|
4308
|
+
if (!byPair.has(key) || byPair.get(key).cov < c.cov) byPair.set(key, c);
|
|
4309
|
+
}
|
|
4310
|
+
const byMile = new Map();
|
|
4311
|
+
for (const c of byPair.values()) {
|
|
4312
|
+
const k = c.milestone.id || String(milestones.indexOf(c.milestone));
|
|
4313
|
+
if (!byMile.has(k)) byMile.set(k, []);
|
|
4314
|
+
byMile.get(k).push(c);
|
|
4315
|
+
}
|
|
4316
|
+
const capped = [];
|
|
4317
|
+
for (const list of byMile.values()) {
|
|
4318
|
+
list.sort((a, b) => b.cov - a.cov);
|
|
4319
|
+
capped.push(...list.slice(0, maxPerMilestone));
|
|
4320
|
+
}
|
|
4321
|
+
return capped;
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// Persisted-edge overlay reader for 'may obsolete' hints β the π οΈ twin of
|
|
4325
|
+
// fulfillmentOverlaysFor, same rules: dismissals win, a since-archived
|
|
4326
|
+
// milestone no longer vouches, machine-written hints stay hedged forever.
|
|
4327
|
+
export function obsolescenceOverlaysFor(struct, cards) {
|
|
4328
|
+
const map = new Map();
|
|
4329
|
+
if (!struct || !Array.isArray(struct.connections)) return map;
|
|
4330
|
+
const byId = new Map(struct.cards.map(c => [c.id, c]));
|
|
4331
|
+
const want = new Set((cards || []).map(c => c.id));
|
|
4332
|
+
const dismissed = new Set();
|
|
4333
|
+
for (const cn of struct.connections) {
|
|
4334
|
+
if (DISMISSAL_RELS.has(cn.relationship)) { dismissed.add(`${cn.fromId}|${cn.toId}`); dismissed.add(`${cn.toId}|${cn.fromId}`); }
|
|
4335
|
+
}
|
|
4336
|
+
for (const cn of struct.connections) {
|
|
4337
|
+
if (cn.label !== 'may obsolete' || !want.has(cn.fromId)) continue;
|
|
4338
|
+
if (dismissed.has(`${cn.fromId}|${cn.toId}`)) continue;
|
|
4339
|
+
const m = byId.get(cn.toId);
|
|
4340
|
+
if (!m || /^archive$/i.test(m.area || '') || /β©|‡/.test(m.text || '')) continue;
|
|
4341
|
+
const head = String(m.text || '').replace(/\s+/g, ' ').trim().slice(0, 100);
|
|
4342
|
+
if (!map.has(cn.fromId)) map.set(cn.fromId, { by: head, byId: m.id, unconfirmed: cn.hintVia !== 'human' });
|
|
4343
|
+
}
|
|
4344
|
+
return map;
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4050
4347
|
// Corpse-rate (T10): the release-gated staleness metric. Fixtures are mined
|
|
4051
4348
|
// from the brain's OWN supersede/close edges: for each archived card with a
|
|
4052
4349
|
// live successor, build the ambiguous shared-subject question both could
|