instar 1.3.877 → 1.3.879
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/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/core/ApprenticeshipMatrixAcceptance.d.ts +124 -0
- package/dist/core/ApprenticeshipMatrixAcceptance.d.ts.map +1 -0
- package/dist/core/ApprenticeshipMatrixAcceptance.js +240 -0
- package/dist/core/ApprenticeshipMatrixAcceptance.js.map +1 -0
- package/dist/core/ApprenticeshipProgram.d.ts +25 -3
- package/dist/core/ApprenticeshipProgram.d.ts.map +1 -1
- package/dist/core/ApprenticeshipProgram.js +72 -25
- package/dist/core/ApprenticeshipProgram.js.map +1 -1
- package/dist/core/ApprenticeshipStallGate.d.ts +297 -0
- package/dist/core/ApprenticeshipStallGate.d.ts.map +1 -0
- package/dist/core/ApprenticeshipStallGate.js +935 -0
- package/dist/core/ApprenticeshipStallGate.js.map +1 -0
- package/dist/core/MessagingToneGate.d.ts +33 -1
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +47 -9
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +8 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +75 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/apprenticeshipStallGate.worker.d.ts +13 -0
- package/dist/core/apprenticeshipStallGate.worker.d.ts.map +1 -0
- package/dist/core/apprenticeshipStallGate.worker.js +23 -0
- package/dist/core/apprenticeshipStallGate.worker.js.map +1 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/SelfUnblockChecklist.d.ts +1 -1
- package/dist/monitoring/SelfUnblockChecklist.d.ts.map +1 -1
- package/dist/monitoring/SelfUnblockChecklist.js +2 -0
- package/dist/monitoring/SelfUnblockChecklist.js.map +1 -1
- package/dist/monitoring/SelfUnblockProbeProviders.d.ts +19 -1
- package/dist/monitoring/SelfUnblockProbeProviders.d.ts.map +1 -1
- package/dist/monitoring/SelfUnblockProbeProviders.js +187 -2
- package/dist/monitoring/SelfUnblockProbeProviders.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +22 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -2
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +22 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +2 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +281 -5
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/stall-matrix-live-check.mjs +309 -0
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates/jobs/instar/stall-matrix-live-check.md +32 -0
- package/src/scaffold/templates.ts +4 -2
- package/upgrades/1.3.878.md +67 -0
- package/upgrades/1.3.879.md +62 -0
- package/upgrades/side-effects/correction-derived-hardening.md +188 -0
- package/upgrades/side-effects/stall-coverage-matrix-pr-b.md +154 -0
- package/upgrades/1.3.877.md +0 -53
package/dist/server/routes.js
CHANGED
|
@@ -158,6 +158,7 @@ import { withSyncOp } from '../core/InFlightSyncOpMarker.js';
|
|
|
158
158
|
import { runAccretionSweep, computeUnbuiltSet, parseDeclaredDeliverables, } from '../core/ScopeAccretionSweep.js';
|
|
159
159
|
import { resolveCorroboration } from '../core/ScopeAccretionCorroboration.js';
|
|
160
160
|
import { ScopeAccretionRatifier } from '../core/ScopeAccretionRatifier.js';
|
|
161
|
+
import { runStallGateValidation, readInstallProvenance, degradedAcceptanceHash, canonicalRowHash, canonicalRowSetHash, } from '../core/ApprenticeshipStallGate.js';
|
|
161
162
|
import { CommitmentTracker } from '../monitoring/CommitmentTracker.js';
|
|
162
163
|
import { ProcessIntegrity } from '../core/ProcessIntegrity.js';
|
|
163
164
|
import { decideIngress, commitInboundReply, dedupeKeyFor } from '../messaging/ingressDedup.js';
|
|
@@ -1272,6 +1273,40 @@ export function createRoutes(ctx) {
|
|
|
1272
1273
|
result,
|
|
1273
1274
|
});
|
|
1274
1275
|
if (!result.pass) {
|
|
1276
|
+
// ── Advisory disposition (operator directive 2026-07-18, topic 29723) ──
|
|
1277
|
+
// An advisory-rule citation is a NUDGE, never a terminal block: the
|
|
1278
|
+
// message is returned to the AGENT (not sent, not dropped) with the
|
|
1279
|
+
// pitfall named, and the agent holds the ultimate decision — revise, or
|
|
1280
|
+
// resend unchanged with metadata.toneAdvisoryAck = "<rule>" to
|
|
1281
|
+
// acknowledge; the override is RECORDED (a decision-quality signal,
|
|
1282
|
+
// never authority). A blocking rule can NEVER be overridden this way.
|
|
1283
|
+
if (result.advisory === true) {
|
|
1284
|
+
if (options.toneAdvisoryAck === result.rule) {
|
|
1285
|
+
const overridden = { ...result, advisoryOverridden: true };
|
|
1286
|
+
logToneGateDecision({
|
|
1287
|
+
text,
|
|
1288
|
+
channel,
|
|
1289
|
+
topicId: options.topicId,
|
|
1290
|
+
signals,
|
|
1291
|
+
result: overridden,
|
|
1292
|
+
});
|
|
1293
|
+
return { ok: true };
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
ok: false,
|
|
1297
|
+
status: 422,
|
|
1298
|
+
reason: 'tone-gate-advisory',
|
|
1299
|
+
body: {
|
|
1300
|
+
error: 'tone-gate-advisory',
|
|
1301
|
+
notSent: true,
|
|
1302
|
+
rule: result.rule,
|
|
1303
|
+
issue: result.issue,
|
|
1304
|
+
suggestion: result.suggestion,
|
|
1305
|
+
howToProceed: `ADVISORY — the message was NOT sent, and the decision is yours: revise and re-send, or re-send unchanged with metadata.toneAdvisoryAck set to "${result.rule}" to acknowledge and deliver (the override is recorded).`,
|
|
1306
|
+
latencyMs: result.latencyMs,
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1275
1310
|
return {
|
|
1276
1311
|
ok: false,
|
|
1277
1312
|
status: 422,
|
|
@@ -3829,12 +3864,86 @@ export function createRoutes(ctx) {
|
|
|
3829
3864
|
recordMetric: (o) => scopeAccretionMetric(o, 'ratification'),
|
|
3830
3865
|
log: (m) => console.warn(m),
|
|
3831
3866
|
});
|
|
3867
|
+
const resolveMatrixState = (instanceId, framework) => {
|
|
3868
|
+
try {
|
|
3869
|
+
const logPath = path.join(ctx.config.stateDir, 'logs', 'apprenticeship-decisions.jsonl');
|
|
3870
|
+
const prov = readInstallProvenance(logPath);
|
|
3871
|
+
if (prov.ok && prov.installClass === 'fleet') {
|
|
3872
|
+
return { kind: 'degraded', contentHash: degradedAcceptanceHash(instanceId, framework) };
|
|
3873
|
+
}
|
|
3874
|
+
const out = runStallGateValidation({
|
|
3875
|
+
repoRoot: ctx.config.projectDir,
|
|
3876
|
+
framework,
|
|
3877
|
+
nowIso: new Date().toISOString(),
|
|
3878
|
+
});
|
|
3879
|
+
if (out.fileMissing || !out.result)
|
|
3880
|
+
return null;
|
|
3881
|
+
const rowsById = new Map();
|
|
3882
|
+
for (const r of out.rawRows ?? []) {
|
|
3883
|
+
if (typeof r.class === 'string')
|
|
3884
|
+
rowsById.set(`${framework}:${r.class}`, r);
|
|
3885
|
+
}
|
|
3886
|
+
return { kind: 'source', contentHash: out.result.contentHash, rowsById };
|
|
3887
|
+
}
|
|
3888
|
+
catch {
|
|
3889
|
+
// @silent-fallback-ok — null = "nothing enumerable"; the routes answer an
|
|
3890
|
+
// explicit 409 / hash-mismatch refusal on it, never a silent success.
|
|
3891
|
+
return null;
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
const ROW_ID_RE = /^[a-z0-9-]+:[a-z0-9-]+$/;
|
|
3895
|
+
const RULE_RE = /^[a-z0-9-]{1,64}$/;
|
|
3896
|
+
/** The CURRENT content hash for a challenge's SCOPE (Decision 20 binding
|
|
3897
|
+
* granularity) — recomputed at bind time so accept-then-edit voids. */
|
|
3898
|
+
const resolveChallengeCurrentHash = (challenge) => {
|
|
3899
|
+
const state = resolveMatrixState(challenge.instanceId, challenge.framework);
|
|
3900
|
+
if (!state)
|
|
3901
|
+
return null;
|
|
3902
|
+
if (challenge.scope === 'degraded')
|
|
3903
|
+
return state.kind === 'degraded' ? state.contentHash : null;
|
|
3904
|
+
if (state.kind !== 'source')
|
|
3905
|
+
return null;
|
|
3906
|
+
if (challenge.scope === 'whole-set')
|
|
3907
|
+
return state.contentHash;
|
|
3908
|
+
if (challenge.scope === 'rows') {
|
|
3909
|
+
const entries = [];
|
|
3910
|
+
for (const id of challenge.rowIds) {
|
|
3911
|
+
const row = state.rowsById.get(id);
|
|
3912
|
+
if (!row)
|
|
3913
|
+
return null;
|
|
3914
|
+
entries.push({ rowId: id, row });
|
|
3915
|
+
}
|
|
3916
|
+
return canonicalRowSetHash(entries);
|
|
3917
|
+
}
|
|
3918
|
+
if (challenge.scope === 'override') {
|
|
3919
|
+
const row = challenge.rowIds.length === 1 ? state.rowsById.get(challenge.rowIds[0]) : undefined;
|
|
3920
|
+
return row ? canonicalRowHash(row) : null;
|
|
3921
|
+
}
|
|
3922
|
+
return null;
|
|
3923
|
+
};
|
|
3924
|
+
// Conversational bind (mirrors the ratifier's reply-anchor mechanic): a
|
|
3925
|
+
// verified-operator reply onto a recorded enumeration message binds the
|
|
3926
|
+
// challenge. Signal-only — fire-and-forget on both ingress paths.
|
|
3927
|
+
const observeMatrixAcceptanceInbound = (evt) => {
|
|
3928
|
+
const store = ctx.apprenticeshipMatrixAcceptance;
|
|
3929
|
+
if (!store)
|
|
3930
|
+
return;
|
|
3931
|
+
void store.observeInbound(evt, {
|
|
3932
|
+
getOperatorUid: (topicId) => ctx.topicOperatorStore?.getOperator(topicId)?.uid ?? null,
|
|
3933
|
+
resolveCurrentContentHash: (challenge) => resolveChallengeCurrentHash(challenge),
|
|
3934
|
+
ack: async (topicId, text) => {
|
|
3935
|
+
if (ctx.telegram)
|
|
3936
|
+
await ctx.telegram.sendToTopic(topicId, text);
|
|
3937
|
+
},
|
|
3938
|
+
});
|
|
3939
|
+
};
|
|
3832
3940
|
if (ctx.telegram) {
|
|
3833
3941
|
// Direct-poll ingress (the server long-polls Telegram itself, R45). The
|
|
3834
|
-
// lifeline-forward ingress fires the same
|
|
3942
|
+
// lifeline-forward ingress fires the same observers inside
|
|
3835
3943
|
// POST /internal/telegram-forward below.
|
|
3836
3944
|
ctx.telegram.onScopeAccretionInbound = (evt) => {
|
|
3837
3945
|
void scopeAccretionRatifier.observeInbound(evt);
|
|
3946
|
+
observeMatrixAcceptanceInbound(evt);
|
|
3838
3947
|
};
|
|
3839
3948
|
}
|
|
3840
3949
|
// ── POST /autonomous/register (R30/R43/R49) — the server-side start snapshot ──
|
|
@@ -11360,6 +11469,12 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
11360
11469
|
const senderClass = coerceSenderClass(metadata?.senderClass);
|
|
11361
11470
|
const advisoryAck = metadata?.advisoryAck === true;
|
|
11362
11471
|
const advisoryCodes = coerceAdvisoryCodes(metadata?.advisoryCodes);
|
|
11472
|
+
// Advisory-rule override (operator directive 2026-07-18): the FULL rule id
|
|
11473
|
+
// the sender explicitly acknowledges. Length-clamped; validated against the
|
|
11474
|
+
// cited rule at the seam (a mismatch or a blocking rule is never overridden).
|
|
11475
|
+
const toneAdvisoryAck = typeof metadata?.toneAdvisoryAck === 'string'
|
|
11476
|
+
? metadata.toneAdvisoryAck.slice(0, 64)
|
|
11477
|
+
: undefined;
|
|
11363
11478
|
const metadataJobSlug = typeof metadata?.jobSlug === 'string' ? metadata.jobSlug.slice(0, 128) : '';
|
|
11364
11479
|
// ── Observability breadcrumbs (§2.1 — visibility on the named dodge
|
|
11365
11480
|
// classes; sovereignty over the send is accepted, nothing is gated). ──
|
|
@@ -11435,6 +11550,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
11435
11550
|
allowDuplicate,
|
|
11436
11551
|
allowLocalhostLink,
|
|
11437
11552
|
messageKind,
|
|
11553
|
+
toneAdvisoryAck,
|
|
11438
11554
|
})))
|
|
11439
11555
|
return;
|
|
11440
11556
|
// In-flight reservation (2026-07-03): the isDuplicate pre-check above and the
|
|
@@ -12296,6 +12412,9 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
12296
12412
|
if (await checkOutboundMessage(text, 'slack', res, {
|
|
12297
12413
|
allowDebugText: metadata?.allowDebugText === true,
|
|
12298
12414
|
allowDuplicate: metadata?.allowDuplicate === true,
|
|
12415
|
+
toneAdvisoryAck: typeof metadata?.toneAdvisoryAck === 'string'
|
|
12416
|
+
? metadata.toneAdvisoryAck.slice(0, 64)
|
|
12417
|
+
: undefined,
|
|
12299
12418
|
// Kind threading mirrors /telegram/reply — the jargon/filePath signal
|
|
12300
12419
|
// computation is single-sourced inside evaluateOutbound, so every
|
|
12301
12420
|
// channel gets it uniformly (spec outbound-jargon-filepath-gap §2.2).
|
|
@@ -17614,6 +17733,13 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
17614
17733
|
replyToMessageId: fwdReplyTo,
|
|
17615
17734
|
at: Date.now(),
|
|
17616
17735
|
});
|
|
17736
|
+
observeMatrixAcceptanceInbound({
|
|
17737
|
+
topicId: Number(topicId),
|
|
17738
|
+
text: String(text),
|
|
17739
|
+
senderUid: String(fromUserId ?? ''),
|
|
17740
|
+
messageId: Number(messageId) || 0,
|
|
17741
|
+
replyToMessageId: fwdReplyTo,
|
|
17742
|
+
});
|
|
17617
17743
|
}
|
|
17618
17744
|
// ── Exactly-once ingress gate (spec §8 G3a) ──────────────────────
|
|
17619
17745
|
// Default-DARK: only active when multiMachine.exactlyOnceIngress wired the
|
|
@@ -20250,7 +20376,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20250
20376
|
res.status(400).json({ error: err instanceof Error ? err.message : String(err) });
|
|
20251
20377
|
}
|
|
20252
20378
|
});
|
|
20253
|
-
router.post('/apprenticeship/instances/:id/transition', (req, res) => {
|
|
20379
|
+
router.post('/apprenticeship/instances/:id/transition', async (req, res) => {
|
|
20254
20380
|
if (!ctx.apprenticeshipProgram) {
|
|
20255
20381
|
res.status(503).json({ error: 'apprenticeship program disabled' });
|
|
20256
20382
|
return;
|
|
@@ -20260,7 +20386,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20260
20386
|
res.status(400).json({ error: 'to must be one of pending | active | complete | blocked | abandoned' });
|
|
20261
20387
|
return;
|
|
20262
20388
|
}
|
|
20263
|
-
const result = ctx.apprenticeshipProgram.transition(req.params.id, to);
|
|
20389
|
+
const result = await ctx.apprenticeshipProgram.transition(req.params.id, to);
|
|
20264
20390
|
if (!result.ok) {
|
|
20265
20391
|
// 404 for a missing instance; 409 for a refused/illegal transition.
|
|
20266
20392
|
const code = result.reason.includes('not found') ? 404 : 409;
|
|
@@ -20294,7 +20420,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20294
20420
|
}
|
|
20295
20421
|
res.json(ctx.apprenticeshipProgram.evaluateStartGate(inst));
|
|
20296
20422
|
});
|
|
20297
|
-
router.post('/apprenticeship/instances/:id/can-complete', (req, res) => {
|
|
20423
|
+
router.post('/apprenticeship/instances/:id/can-complete', async (req, res) => {
|
|
20298
20424
|
if (!ctx.apprenticeshipProgram) {
|
|
20299
20425
|
res.status(503).json({ error: 'apprenticeship program disabled' });
|
|
20300
20426
|
return;
|
|
@@ -20304,7 +20430,157 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
|
|
|
20304
20430
|
res.status(404).json({ error: 'not found' });
|
|
20305
20431
|
return;
|
|
20306
20432
|
}
|
|
20307
|
-
res.json(ctx.apprenticeshipProgram.evaluateCompletionGate(inst));
|
|
20433
|
+
res.json(await ctx.apprenticeshipProgram.evaluateCompletionGate(inst));
|
|
20434
|
+
});
|
|
20435
|
+
// ── Stall-coverage matrix acceptance (framework-stall-coverage-matrix §2.2) ──
|
|
20436
|
+
//
|
|
20437
|
+
// POST /apprenticeship/instances/:id/matrix-acceptance/enumerate — Bearer;
|
|
20438
|
+
// the SERVER renders + records the exact enumerated set (a challenge).
|
|
20439
|
+
// Optional { topicId } posts the enumeration to the topic so the
|
|
20440
|
+
// verified operator can bind it with a reply-anchored yes/approve (the
|
|
20441
|
+
// ScopeAccretionRatifier mechanic).
|
|
20442
|
+
// POST /apprenticeship/instances/:id/matrix-acceptance — dashboard-PIN
|
|
20443
|
+
// bind: { pin, challengeId }. Requester ≠ acceptor is structural — the
|
|
20444
|
+
// Bearer token alone can NEVER record an acceptance (Decision 14).
|
|
20445
|
+
router.post('/apprenticeship/instances/:id/matrix-acceptance/enumerate', async (req, res) => {
|
|
20446
|
+
if (!ctx.apprenticeshipProgram || !ctx.apprenticeshipMatrixAcceptance) {
|
|
20447
|
+
res.status(503).json({ error: 'apprenticeship program disabled' });
|
|
20448
|
+
return;
|
|
20449
|
+
}
|
|
20450
|
+
const inst = ctx.apprenticeshipProgram.get(req.params.id);
|
|
20451
|
+
if (!inst) {
|
|
20452
|
+
res.status(404).json({ error: 'not found' });
|
|
20453
|
+
return;
|
|
20454
|
+
}
|
|
20455
|
+
const state = resolveMatrixState(inst.id, inst.framework);
|
|
20456
|
+
if (!state) {
|
|
20457
|
+
res.status(409).json({ error: `no stall-coverage matrix content to enumerate for framework '${inst.framework}'` });
|
|
20458
|
+
return;
|
|
20459
|
+
}
|
|
20460
|
+
// Scope-aware mint (Decision 20): whole-set (default) / rows / override.
|
|
20461
|
+
// Rows + override are the production path for `acceptanceRef:` clears and
|
|
20462
|
+
// the §3.4 per-instance relief — hashed via the SAME canonical-row
|
|
20463
|
+
// serialization the gate re-derives at verify time.
|
|
20464
|
+
const requestedScope = typeof req.body?.scope === 'string' ? req.body.scope : 'whole-set';
|
|
20465
|
+
let enumerated;
|
|
20466
|
+
if (state.kind === 'degraded') {
|
|
20467
|
+
if (requestedScope !== 'whole-set' && requestedScope !== 'degraded') {
|
|
20468
|
+
res.status(409).json({ error: 'row/override-scoped acceptance is not available on a no-source install (degraded verdict only)' });
|
|
20469
|
+
return;
|
|
20470
|
+
}
|
|
20471
|
+
enumerated = { scope: 'degraded', contentHash: state.contentHash, rowIds: [] };
|
|
20472
|
+
}
|
|
20473
|
+
else if (requestedScope === 'whole-set') {
|
|
20474
|
+
enumerated = { scope: 'whole-set', contentHash: state.contentHash, rowIds: [...state.rowsById.keys()] };
|
|
20475
|
+
}
|
|
20476
|
+
else if (requestedScope === 'rows') {
|
|
20477
|
+
const rowIds = Array.isArray(req.body?.rowIds)
|
|
20478
|
+
? req.body.rowIds.filter((x) => typeof x === 'string')
|
|
20479
|
+
: [];
|
|
20480
|
+
if (rowIds.length === 0 || rowIds.length > 64 || !rowIds.every((id) => ROW_ID_RE.test(id))) {
|
|
20481
|
+
res.status(400).json({ error: '"rowIds" (non-empty array of "<framework>:<classId>" strings) required for scope "rows"' });
|
|
20482
|
+
return;
|
|
20483
|
+
}
|
|
20484
|
+
const entries = [];
|
|
20485
|
+
for (const id of rowIds) {
|
|
20486
|
+
const row = state.rowsById.get(id);
|
|
20487
|
+
if (!row) {
|
|
20488
|
+
res.status(409).json({ error: `rowId '${id}' does not resolve to a current matrix row` });
|
|
20489
|
+
return;
|
|
20490
|
+
}
|
|
20491
|
+
entries.push({ rowId: id, row });
|
|
20492
|
+
}
|
|
20493
|
+
enumerated = { scope: 'rows', contentHash: canonicalRowSetHash(entries), rowIds: [...rowIds].sort() };
|
|
20494
|
+
}
|
|
20495
|
+
else if (requestedScope === 'override') {
|
|
20496
|
+
const rule = typeof req.body?.rule === 'string' ? req.body.rule : '';
|
|
20497
|
+
const classId = typeof req.body?.classId === 'string' ? req.body.classId : '';
|
|
20498
|
+
if (!RULE_RE.test(rule) || !/^[a-z0-9-]+$/.test(classId)) {
|
|
20499
|
+
res.status(400).json({ error: '"rule" (kebab rule name) and "classId" required for scope "override"' });
|
|
20500
|
+
return;
|
|
20501
|
+
}
|
|
20502
|
+
const rowId = `${inst.framework}:${classId}`;
|
|
20503
|
+
const row = state.rowsById.get(rowId);
|
|
20504
|
+
if (!row) {
|
|
20505
|
+
res.status(409).json({ error: `classId '${classId}' does not resolve to a current matrix row` });
|
|
20506
|
+
return;
|
|
20507
|
+
}
|
|
20508
|
+
enumerated = { scope: 'override', contentHash: canonicalRowHash(row), rowIds: [rowId], rule };
|
|
20509
|
+
}
|
|
20510
|
+
else {
|
|
20511
|
+
res.status(400).json({ error: '"scope" must be one of whole-set | rows | override' });
|
|
20512
|
+
return;
|
|
20513
|
+
}
|
|
20514
|
+
const challenge = ctx.apprenticeshipMatrixAcceptance.mintChallenge({
|
|
20515
|
+
instanceId: inst.id,
|
|
20516
|
+
framework: inst.framework,
|
|
20517
|
+
scope: enumerated.scope,
|
|
20518
|
+
contentHash: enumerated.contentHash,
|
|
20519
|
+
rowIds: enumerated.rowIds,
|
|
20520
|
+
...(enumerated.rule ? { rule: enumerated.rule } : {}),
|
|
20521
|
+
});
|
|
20522
|
+
const renderedText = ctx.apprenticeshipMatrixAcceptance.renderEnumeration(challenge);
|
|
20523
|
+
// Conversational arm: post the server-authored enumeration to the topic so
|
|
20524
|
+
// a reply-anchored verified-operator confirmation can bind it.
|
|
20525
|
+
let posted = null;
|
|
20526
|
+
const topicId = typeof req.body?.topicId === 'number' ? req.body.topicId : null;
|
|
20527
|
+
if (topicId !== null && ctx.telegram) {
|
|
20528
|
+
try {
|
|
20529
|
+
const sent = await ctx.telegram.sendToTopic(topicId, renderedText);
|
|
20530
|
+
posted = { topicId, messageId: sent.messageId };
|
|
20531
|
+
ctx.apprenticeshipMatrixAcceptance.attachMessage(challenge.challengeId, posted);
|
|
20532
|
+
}
|
|
20533
|
+
catch {
|
|
20534
|
+
// @silent-fallback-ok — a failed Telegram post only loses the
|
|
20535
|
+
// conversational anchor; the challenge is recorded and the PIN path
|
|
20536
|
+
// remains fully available (the response reports posted: null).
|
|
20537
|
+
posted = null;
|
|
20538
|
+
}
|
|
20539
|
+
}
|
|
20540
|
+
res.json({
|
|
20541
|
+
challengeId: challenge.challengeId,
|
|
20542
|
+
scope: challenge.scope,
|
|
20543
|
+
contentHash: challenge.contentHash,
|
|
20544
|
+
rowIds: challenge.rowIds,
|
|
20545
|
+
...(challenge.rule ? { rule: challenge.rule } : {}),
|
|
20546
|
+
renderedText,
|
|
20547
|
+
posted,
|
|
20548
|
+
});
|
|
20549
|
+
});
|
|
20550
|
+
router.post('/apprenticeship/instances/:id/matrix-acceptance', (req, res) => {
|
|
20551
|
+
if (!ctx.apprenticeshipProgram || !ctx.apprenticeshipMatrixAcceptance) {
|
|
20552
|
+
res.status(503).json({ error: 'apprenticeship program disabled' });
|
|
20553
|
+
return;
|
|
20554
|
+
}
|
|
20555
|
+
if (!checkMandatePin(req, res))
|
|
20556
|
+
return; // PIN-gated: a Bearer token is structurally insufficient
|
|
20557
|
+
const inst = ctx.apprenticeshipProgram.get(req.params.id);
|
|
20558
|
+
if (!inst) {
|
|
20559
|
+
res.status(404).json({ error: 'not found' });
|
|
20560
|
+
return;
|
|
20561
|
+
}
|
|
20562
|
+
const challengeId = typeof req.body?.challengeId === 'string' ? req.body.challengeId : '';
|
|
20563
|
+
if (!challengeId) {
|
|
20564
|
+
res.status(400).json({ error: '"challengeId" (string) required' });
|
|
20565
|
+
return;
|
|
20566
|
+
}
|
|
20567
|
+
const challenge = ctx.apprenticeshipMatrixAcceptance.getChallenge(challengeId);
|
|
20568
|
+
if (!challenge || challenge.instanceId !== inst.id) {
|
|
20569
|
+
res.status(404).json({ error: 'unknown challenge for this instance' });
|
|
20570
|
+
return;
|
|
20571
|
+
}
|
|
20572
|
+
// Re-resolve the CURRENT content for the challenge's SCOPE at bind time —
|
|
20573
|
+
// accept-then-edit voids the challenge (§2.2 / Decision 20).
|
|
20574
|
+
const bound = ctx.apprenticeshipMatrixAcceptance.bind({
|
|
20575
|
+
challengeId,
|
|
20576
|
+
principal: { kind: 'operator-pin', id: 'dashboard-pin' },
|
|
20577
|
+
currentContentHash: resolveChallengeCurrentHash(challenge),
|
|
20578
|
+
});
|
|
20579
|
+
if (!bound.ok) {
|
|
20580
|
+
res.status(409).json({ ok: false, reason: bound.reason });
|
|
20581
|
+
return;
|
|
20582
|
+
}
|
|
20583
|
+
res.json({ ok: true, reason: bound.reason, challengeRef: challengeId });
|
|
20308
20584
|
});
|
|
20309
20585
|
// ORG-INTENT.md tradeoff resolution (Phase 3 of the ORG-INTENT runtime
|
|
20310
20586
|
// project). Given two contending values, consults the organization's
|