empirical-sdd 0.26.1 → 0.27.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/CHANGELOG.md +17 -1
- package/README.md +1 -1
- package/dist/cli.js +401 -53
- package/dist/demo-integration-repair.js +398 -50
- package/dist/demo-ticket-policy.js +398 -50
- package/dist/index.js +398 -50
- package/dist/integrations.js +2 -2
- package/dist/mcp.js +401 -53
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.js +1 -1
- package/dist/tracker-comments.d.ts +47 -0
- package/docs/demo.md +1 -1
- package/docs/protocol.md +1 -1
- package/docs/versioning.md +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ under the alpha rules in [docs/versioning.md](docs/versioning.md).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.27.0] - 2026-08-21
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Made Tracker Policy v2 lifecycle comments human-first across GitHub, Linear,
|
|
16
|
+
and Jira, with plain-language status, focused action requests, friendly
|
|
17
|
+
evidence links, native Jira formatting, and provider-safe exact recovery
|
|
18
|
+
markers that remain compatible with existing comments.
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
- Existing Schema 5 and Tracker Policy v1/v2 repositories require no state
|
|
23
|
+
migration. Visibility cadence, ticket bindings, issue descriptions, and
|
|
24
|
+
historical comments remain compatible.
|
|
25
|
+
|
|
11
26
|
## [0.26.1] - 2026-08-21
|
|
12
27
|
|
|
13
28
|
### Fixed
|
|
@@ -193,7 +208,8 @@ Published through GitHub Actions trusted publishing with npm provenance.
|
|
|
193
208
|
|
|
194
209
|
- Prepared and released package version `0.20.2`.
|
|
195
210
|
|
|
196
|
-
[Unreleased]: https://github.com/goempirical/empirical-sdd/compare/v0.
|
|
211
|
+
[Unreleased]: https://github.com/goempirical/empirical-sdd/compare/v0.27.0...HEAD
|
|
212
|
+
[0.27.0]: https://github.com/goempirical/empirical-sdd/compare/v0.26.1...v0.27.0
|
|
197
213
|
[0.26.1]: https://github.com/goempirical/empirical-sdd/compare/v0.26.0...v0.26.1
|
|
198
214
|
[0.26.0]: https://github.com/goempirical/empirical-sdd/compare/v0.25.0...v0.26.0
|
|
199
215
|
[0.25.0]: https://github.com/goempirical/empirical-sdd/compare/v0.24.1...v0.25.0
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Agent-neutral, resumable spec-driven development for coding agents. Empirical
|
|
|
4
4
|
turns an ordinary change request into a deterministic workflow with durable
|
|
5
5
|
state, reviewable evidence, and safe Git integration.
|
|
6
6
|
|
|
7
|
-
> Empirical 0.
|
|
7
|
+
> Empirical 0.27 is alpha software. It requires Node.js 22 or newer.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
package/dist/cli.js
CHANGED
|
@@ -14862,7 +14862,7 @@ function verifyReceiptDigest(receipt) {
|
|
|
14862
14862
|
throw new Error(`Evidence receipt ${parsed.id} has an asserted result inconsistent with its provenance.`);
|
|
14863
14863
|
}
|
|
14864
14864
|
}
|
|
14865
|
-
var SCHEMA_VERSION = 5, POLICY_SCHEMA_VERSION = 2, MANIFEST_SCHEMA_VERSION = 2, RECEIPT_SCHEMA_VERSION = 1, PRODUCT_VERSION = "0.
|
|
14865
|
+
var SCHEMA_VERSION = 5, POLICY_SCHEMA_VERSION = 2, MANIFEST_SCHEMA_VERSION = 2, RECEIPT_SCHEMA_VERSION = 1, PRODUCT_VERSION = "0.27.0", workflowSchema, executionModeSchema, riskFloorSchema, completionLevelSchema, phaseSchema, workflowStatusSchema, criterionSchema, evidenceKindSchema, commandPolicySchema, evidencePolicySchema, projectPolicySchema, impactManifestSchema, authorizationSchema, COMPLETION_ORDER, receiptProvenanceSchema, receiptBaseSchema, executedReceiptSchema, artifactRecordSchema, collectedReceiptSchema, evidenceReceiptSchema;
|
|
14866
14866
|
var init_protocol = __esm(() => {
|
|
14867
14867
|
init_zod();
|
|
14868
14868
|
workflowSchema = exports_external.enum(["fast", "complex"]);
|
|
@@ -24050,7 +24050,7 @@ class ProjectStore {
|
|
|
24050
24050
|
return { ...await migrateSchema4To5(this.root) };
|
|
24051
24051
|
}
|
|
24052
24052
|
if (rawVersion !== SCHEMA_VERSION) {
|
|
24053
|
-
throw new EmpiricalError("MIGRATION_REQUIRED", `Schema ${rawVersion} is unsupported; Empirical 0.
|
|
24053
|
+
throw new EmpiricalError("MIGRATION_REQUIRED", `Schema ${rawVersion} is unsupported; Empirical 0.27 migrates only Schema 4 to Schema 5`);
|
|
24054
24054
|
}
|
|
24055
24055
|
await project.ensureProjectMetadata();
|
|
24056
24056
|
return project.withResourceLock("specs", async () => {
|
|
@@ -37258,6 +37258,341 @@ function isMissingFile(error51) {
|
|
|
37258
37258
|
return isRecord4(error51) && error51.code === "ENOENT";
|
|
37259
37259
|
}
|
|
37260
37260
|
|
|
37261
|
+
// src/tracker-comments.ts
|
|
37262
|
+
var TRACKER_MARKER_ORIGIN = "https://github.com/goempirical/empirical-sdd";
|
|
37263
|
+
var EFFECT_KEY = /^sha256:[a-f0-9]{64}$/;
|
|
37264
|
+
var HUMAN_TEXT_LIMIT = 320;
|
|
37265
|
+
var WORK_LABEL_LIMIT = 96;
|
|
37266
|
+
var EVIDENCE_LIMIT = 10;
|
|
37267
|
+
var JIRA_SCAN_LIMIT = 1e4;
|
|
37268
|
+
var COMMENT_PROPERTY = "empirical-sdd-effect";
|
|
37269
|
+
function renderTrackerMilestone(provider, projection, effectKey) {
|
|
37270
|
+
const identity = trackerMilestoneIdentity(effectKey);
|
|
37271
|
+
const view = createTrackerMilestoneView(projection);
|
|
37272
|
+
if (provider === "github") {
|
|
37273
|
+
return {
|
|
37274
|
+
provider,
|
|
37275
|
+
body: [...renderMarkdownSections(view), "", identity.github].join(`
|
|
37276
|
+
`)
|
|
37277
|
+
};
|
|
37278
|
+
}
|
|
37279
|
+
if (provider === "linear") {
|
|
37280
|
+
return {
|
|
37281
|
+
provider,
|
|
37282
|
+
body: [...renderMarkdownSections(view), "", identity.linear].join(`
|
|
37283
|
+
`)
|
|
37284
|
+
};
|
|
37285
|
+
}
|
|
37286
|
+
return {
|
|
37287
|
+
provider,
|
|
37288
|
+
body: renderJiraMilestone(view, identity),
|
|
37289
|
+
property: { key: COMMENT_PROPERTY, value: identity.effectKey }
|
|
37290
|
+
};
|
|
37291
|
+
}
|
|
37292
|
+
function createTrackerMilestoneView(projection) {
|
|
37293
|
+
const summary = humanText(projection.summary, HUMAN_TEXT_LIMIT);
|
|
37294
|
+
const stopped = projection.status === "blocked" || projection.status === "awaiting_human";
|
|
37295
|
+
const actionText = stopped ? humanText(projection.blocker ?? projection.summary, HUMAN_TEXT_LIMIT) ?? (projection.status === "awaiting_human" ? "A decision or input is required before work can continue." : "Work cannot continue until this blocker is resolved.") : null;
|
|
37296
|
+
return {
|
|
37297
|
+
headline: milestoneHeadline(projection),
|
|
37298
|
+
work: humanizeFeature(projection.feature),
|
|
37299
|
+
summary: summary === actionText ? null : summary,
|
|
37300
|
+
action: actionText ? {
|
|
37301
|
+
label: projection.status === "awaiting_human" ? "Action needed" : "Blocker",
|
|
37302
|
+
text: actionText
|
|
37303
|
+
} : null,
|
|
37304
|
+
evidence: milestoneEvidence(projection.artifacts ?? [])
|
|
37305
|
+
};
|
|
37306
|
+
}
|
|
37307
|
+
function inspectTrackerMilestoneMarker(provider, comment, effectKey) {
|
|
37308
|
+
const identity = trackerMilestoneIdentity(effectKey);
|
|
37309
|
+
if (provider === "github" || provider === "linear") {
|
|
37310
|
+
if (typeof comment.body !== "string")
|
|
37311
|
+
return "absent";
|
|
37312
|
+
return inspectMarkdownMarker(comment.body, provider === "github" ? identity.github : identity.linear, identity);
|
|
37313
|
+
}
|
|
37314
|
+
const body = inspectJiraBodyMarker(comment.body, identity);
|
|
37315
|
+
const property = inspectJiraPropertyMarker(comment.properties, identity);
|
|
37316
|
+
if (body === "malformed" || property === "malformed")
|
|
37317
|
+
return "malformed";
|
|
37318
|
+
return body === "match" || property === "match" ? "match" : "absent";
|
|
37319
|
+
}
|
|
37320
|
+
function trackerMilestoneIdentity(effectKey) {
|
|
37321
|
+
if (!EFFECT_KEY.test(effectKey)) {
|
|
37322
|
+
throw new EmpiricalError("TRACKER_MARKER_INVALID", "Tracker milestone identity requires one exact effect digest");
|
|
37323
|
+
}
|
|
37324
|
+
const url2 = `${TRACKER_MARKER_ORIGIN}#empirical-milestone:${effectKey}`;
|
|
37325
|
+
return {
|
|
37326
|
+
effectKey,
|
|
37327
|
+
url: url2,
|
|
37328
|
+
legacyMarkdown: `[Empirical milestone](<${url2}>)`,
|
|
37329
|
+
github: `<!-- empirical-sdd-effect:${effectKey} -->`,
|
|
37330
|
+
linear: `_[Managed by Empirical](<${url2}>)_`
|
|
37331
|
+
};
|
|
37332
|
+
}
|
|
37333
|
+
function milestoneHeadline(projection) {
|
|
37334
|
+
if (projection.status === "awaiting_human")
|
|
37335
|
+
return "Input needed";
|
|
37336
|
+
if (projection.status === "blocked")
|
|
37337
|
+
return "Work is blocked";
|
|
37338
|
+
if (projection.status === "done" || projection.phase === "done") {
|
|
37339
|
+
switch (projection.completionLevel) {
|
|
37340
|
+
case "implemented":
|
|
37341
|
+
return "Implementation complete";
|
|
37342
|
+
case "verified":
|
|
37343
|
+
return "Verification complete";
|
|
37344
|
+
case "integrated":
|
|
37345
|
+
return "Integration complete";
|
|
37346
|
+
case "delivered":
|
|
37347
|
+
return "Delivery complete";
|
|
37348
|
+
case "published":
|
|
37349
|
+
return "Publication complete";
|
|
37350
|
+
default:
|
|
37351
|
+
return "Workflow complete";
|
|
37352
|
+
}
|
|
37353
|
+
}
|
|
37354
|
+
switch (projection.phase) {
|
|
37355
|
+
case "idle":
|
|
37356
|
+
case "shape":
|
|
37357
|
+
case "specify":
|
|
37358
|
+
case "design":
|
|
37359
|
+
return "Defining the work";
|
|
37360
|
+
case "plan":
|
|
37361
|
+
return "Plan ready";
|
|
37362
|
+
case "implement":
|
|
37363
|
+
case "context":
|
|
37364
|
+
return "Implementation in progress";
|
|
37365
|
+
case "verify":
|
|
37366
|
+
return "Verification in progress";
|
|
37367
|
+
case "review":
|
|
37368
|
+
return "Ready for review";
|
|
37369
|
+
case "integrate":
|
|
37370
|
+
return "Integration in progress";
|
|
37371
|
+
case "deliver":
|
|
37372
|
+
return "Delivery in progress";
|
|
37373
|
+
case "publish":
|
|
37374
|
+
return "Publication in progress";
|
|
37375
|
+
case "archive":
|
|
37376
|
+
return "Wrapping up";
|
|
37377
|
+
default:
|
|
37378
|
+
return "Work in progress";
|
|
37379
|
+
}
|
|
37380
|
+
}
|
|
37381
|
+
function humanizeFeature(feature) {
|
|
37382
|
+
const value = feature.split("-").filter(Boolean).join(" ");
|
|
37383
|
+
const sentence = `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
37384
|
+
return boundText(sentence, WORK_LABEL_LIMIT) ?? "Empirical work";
|
|
37385
|
+
}
|
|
37386
|
+
function humanText(value, limit) {
|
|
37387
|
+
if (!value?.trim())
|
|
37388
|
+
return null;
|
|
37389
|
+
const normalized = value.normalize("NFKC").replace(/\[([^\]\r\n]{0,128})\]\([^\)\r\n]{1,2048}\)/g, "$1").replace(/\bhttps?:\/\/\S+/gi, "[link omitted]").replace(/\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]").replace(/\b(?:gh[pousr]_|github_pat_|lin_api_)[A-Za-z0-9_]{16,}\b/gi, "[REDACTED]").replace(/((?:authorization|password|secret|api[_ -]?key|token)\s*[:=]\s*)\S+/gi, "$1[REDACTED]").replace(/\b(?:executed|collected)-[a-z0-9-]+\b/gi, "[REDACTED]").replace(/\b(?:sha256:)?[a-f0-9]{40,64}\b/gi, "[REDACTED]").replace(/empirical-(?:milestone|sdd-effect):/gi, "Empirical update ").replace(/<!--|-->/g, " ").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").replace(/@/g, "@").replace(/\s+/g, " ").trim();
|
|
37390
|
+
return boundText(normalized, limit);
|
|
37391
|
+
}
|
|
37392
|
+
function boundText(value, limit) {
|
|
37393
|
+
const points = [...value.trim()];
|
|
37394
|
+
if (points.length === 0)
|
|
37395
|
+
return null;
|
|
37396
|
+
if (points.length <= limit)
|
|
37397
|
+
return points.join("");
|
|
37398
|
+
return `${points.slice(0, Math.max(1, limit - 1)).join("").trimEnd()}…`;
|
|
37399
|
+
}
|
|
37400
|
+
function milestoneEvidence(artifacts) {
|
|
37401
|
+
const candidates = artifacts.flatMap((artifact) => {
|
|
37402
|
+
const url2 = safeEvidenceUrl(artifact.url);
|
|
37403
|
+
return url2 ? [{ base: evidenceLabel(artifact.mediaType), url: url2 }] : [];
|
|
37404
|
+
}).slice(0, EVIDENCE_LIMIT);
|
|
37405
|
+
const totals = new Map;
|
|
37406
|
+
for (const candidate of candidates) {
|
|
37407
|
+
totals.set(candidate.base, (totals.get(candidate.base) ?? 0) + 1);
|
|
37408
|
+
}
|
|
37409
|
+
const seen = new Map;
|
|
37410
|
+
return candidates.map((candidate) => {
|
|
37411
|
+
const index = (seen.get(candidate.base) ?? 0) + 1;
|
|
37412
|
+
seen.set(candidate.base, index);
|
|
37413
|
+
return {
|
|
37414
|
+
label: (totals.get(candidate.base) ?? 0) > 1 ? `${candidate.base} ${index}` : candidate.base,
|
|
37415
|
+
url: candidate.url
|
|
37416
|
+
};
|
|
37417
|
+
});
|
|
37418
|
+
}
|
|
37419
|
+
function evidenceLabel(mediaType) {
|
|
37420
|
+
if (mediaType.startsWith("image/"))
|
|
37421
|
+
return "Screenshot";
|
|
37422
|
+
if (mediaType === "application/pdf")
|
|
37423
|
+
return "Document";
|
|
37424
|
+
if (mediaType === "application/json")
|
|
37425
|
+
return "Report";
|
|
37426
|
+
if (mediaType === "text/plain" || mediaType === "text/markdown")
|
|
37427
|
+
return "Notes";
|
|
37428
|
+
return "Evidence";
|
|
37429
|
+
}
|
|
37430
|
+
function safeEvidenceUrl(value) {
|
|
37431
|
+
if (!value || value !== value.trim() || value.length > 2048 || /[\u0000-\u001f\u007f<>]/.test(value)) {
|
|
37432
|
+
return null;
|
|
37433
|
+
}
|
|
37434
|
+
if (hasUnsafeLinkMaterial(value))
|
|
37435
|
+
return null;
|
|
37436
|
+
try {
|
|
37437
|
+
const url2 = new URL(value);
|
|
37438
|
+
if (url2.protocol !== "https:" || url2.username || url2.password)
|
|
37439
|
+
return null;
|
|
37440
|
+
let decoded;
|
|
37441
|
+
try {
|
|
37442
|
+
decoded = decodeURIComponent(`${url2.pathname}${url2.search}${url2.hash}`);
|
|
37443
|
+
} catch {
|
|
37444
|
+
return null;
|
|
37445
|
+
}
|
|
37446
|
+
if (hasUnsafeLinkMaterial(decoded))
|
|
37447
|
+
return null;
|
|
37448
|
+
return url2.toString();
|
|
37449
|
+
} catch {
|
|
37450
|
+
return null;
|
|
37451
|
+
}
|
|
37452
|
+
}
|
|
37453
|
+
function hasUnsafeLinkMaterial(value) {
|
|
37454
|
+
return /\b(?:bearer|basic)\s+/i.test(value) || /\b(?:gh[pousr]_|github_pat_|lin_api_)[A-Za-z0-9_]{16,}\b/i.test(value) || /(?:authorization|password|secret|api[_ -]?key|token)\s*[:=]/i.test(value) || /(?:empirical-(?:milestone|sdd-effect):|sha256:)/i.test(value);
|
|
37455
|
+
}
|
|
37456
|
+
function renderMarkdownSections(view) {
|
|
37457
|
+
const lines = [
|
|
37458
|
+
`## ${escapeMarkdown(view.headline)}`,
|
|
37459
|
+
`**${escapeMarkdown(view.work)}**`
|
|
37460
|
+
];
|
|
37461
|
+
if (view.summary)
|
|
37462
|
+
lines.push("", escapeMarkdown(view.summary));
|
|
37463
|
+
if (view.action) {
|
|
37464
|
+
lines.push("", `### ${view.action.label}`, escapeMarkdown(view.action.text));
|
|
37465
|
+
}
|
|
37466
|
+
if (view.evidence.length > 0) {
|
|
37467
|
+
lines.push("", "### Evidence", ...view.evidence.map((entry) => `- [${entry.label}](<${entry.url}>)`));
|
|
37468
|
+
}
|
|
37469
|
+
return lines;
|
|
37470
|
+
}
|
|
37471
|
+
function escapeMarkdown(value) {
|
|
37472
|
+
return value.replace(/([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g, "\\$1");
|
|
37473
|
+
}
|
|
37474
|
+
function renderJiraMilestone(view, identity) {
|
|
37475
|
+
const content = [
|
|
37476
|
+
jiraHeading(view.headline, 2),
|
|
37477
|
+
jiraParagraph(view.work, [{ type: "strong" }])
|
|
37478
|
+
];
|
|
37479
|
+
if (view.summary)
|
|
37480
|
+
content.push(jiraParagraph(view.summary));
|
|
37481
|
+
if (view.action) {
|
|
37482
|
+
content.push(jiraHeading(view.action.label, 3), jiraParagraph(view.action.text));
|
|
37483
|
+
}
|
|
37484
|
+
if (view.evidence.length > 0) {
|
|
37485
|
+
content.push(jiraHeading("Evidence", 3), {
|
|
37486
|
+
type: "bulletList",
|
|
37487
|
+
content: view.evidence.map((entry) => ({
|
|
37488
|
+
type: "listItem",
|
|
37489
|
+
content: [jiraParagraph(entry.label, [{ type: "link", attrs: { href: entry.url } }])]
|
|
37490
|
+
}))
|
|
37491
|
+
});
|
|
37492
|
+
}
|
|
37493
|
+
content.push(jiraParagraph("Managed by Empirical", [
|
|
37494
|
+
{ type: "em" },
|
|
37495
|
+
{ type: "link", attrs: { href: identity.url } }
|
|
37496
|
+
]));
|
|
37497
|
+
return { version: 1, type: "doc", content };
|
|
37498
|
+
}
|
|
37499
|
+
function jiraHeading(text, level) {
|
|
37500
|
+
return {
|
|
37501
|
+
type: "heading",
|
|
37502
|
+
attrs: { level },
|
|
37503
|
+
content: [{ type: "text", text }]
|
|
37504
|
+
};
|
|
37505
|
+
}
|
|
37506
|
+
function jiraParagraph(text, marks = []) {
|
|
37507
|
+
return {
|
|
37508
|
+
type: "paragraph",
|
|
37509
|
+
content: [{ type: "text", text, ...marks.length > 0 ? { marks } : {} }]
|
|
37510
|
+
};
|
|
37511
|
+
}
|
|
37512
|
+
function inspectMarkdownMarker(body, current, identity) {
|
|
37513
|
+
const related = countOccurrences(body, identity.effectKey);
|
|
37514
|
+
if (related === 0)
|
|
37515
|
+
return "absent";
|
|
37516
|
+
const lines = body.split(/\r?\n/);
|
|
37517
|
+
const recognized = lines.filter((line) => line === current || line === identity.legacyMarkdown).length;
|
|
37518
|
+
return related === 1 && recognized === 1 ? "match" : "malformed";
|
|
37519
|
+
}
|
|
37520
|
+
function inspectJiraBodyMarker(body, identity) {
|
|
37521
|
+
const scan = { related: 0, recognized: 0 };
|
|
37522
|
+
let visited = 0;
|
|
37523
|
+
const walk = (value) => {
|
|
37524
|
+
visited += 1;
|
|
37525
|
+
if (visited > JIRA_SCAN_LIMIT) {
|
|
37526
|
+
scan.related += 1;
|
|
37527
|
+
return;
|
|
37528
|
+
}
|
|
37529
|
+
if (typeof value === "string") {
|
|
37530
|
+
scan.related += countOccurrences(value, identity.effectKey);
|
|
37531
|
+
if (value === identity.legacyMarkdown)
|
|
37532
|
+
scan.recognized += 1;
|
|
37533
|
+
return;
|
|
37534
|
+
}
|
|
37535
|
+
if (Array.isArray(value)) {
|
|
37536
|
+
for (const entry of value)
|
|
37537
|
+
walk(entry);
|
|
37538
|
+
return;
|
|
37539
|
+
}
|
|
37540
|
+
if (!isRecord5(value))
|
|
37541
|
+
return;
|
|
37542
|
+
if (value.type === "text" && value.text === "Managed by Empirical" && Array.isArray(value.marks)) {
|
|
37543
|
+
for (const rawMark of value.marks) {
|
|
37544
|
+
if (!isRecord5(rawMark) || rawMark.type !== "link" || !isRecord5(rawMark.attrs))
|
|
37545
|
+
continue;
|
|
37546
|
+
if (rawMark.attrs.href === identity.url)
|
|
37547
|
+
scan.recognized += 1;
|
|
37548
|
+
}
|
|
37549
|
+
}
|
|
37550
|
+
for (const entry of Object.values(value))
|
|
37551
|
+
walk(entry);
|
|
37552
|
+
};
|
|
37553
|
+
walk(body);
|
|
37554
|
+
if (scan.related === 0)
|
|
37555
|
+
return "absent";
|
|
37556
|
+
return scan.related === 1 && scan.recognized === 1 ? "match" : "malformed";
|
|
37557
|
+
}
|
|
37558
|
+
function inspectJiraPropertyMarker(properties, identity) {
|
|
37559
|
+
const values = [];
|
|
37560
|
+
if (Array.isArray(properties)) {
|
|
37561
|
+
for (const rawProperty of properties) {
|
|
37562
|
+
if (isRecord5(rawProperty) && rawProperty.key === COMMENT_PROPERTY)
|
|
37563
|
+
values.push(rawProperty.value);
|
|
37564
|
+
}
|
|
37565
|
+
} else if (isRecord5(properties) && Object.hasOwn(properties, COMMENT_PROPERTY)) {
|
|
37566
|
+
values.push(properties[COMMENT_PROPERTY]);
|
|
37567
|
+
}
|
|
37568
|
+
if (values.length === 0)
|
|
37569
|
+
return "absent";
|
|
37570
|
+
let related = 0;
|
|
37571
|
+
let recognized = 0;
|
|
37572
|
+
for (const value of values) {
|
|
37573
|
+
if (typeof value !== "string")
|
|
37574
|
+
continue;
|
|
37575
|
+
related += countOccurrences(value, identity.effectKey);
|
|
37576
|
+
if (value === identity.effectKey || value === identity.legacyMarkdown)
|
|
37577
|
+
recognized += 1;
|
|
37578
|
+
}
|
|
37579
|
+
if (related === 0)
|
|
37580
|
+
return "absent";
|
|
37581
|
+
return related === 1 && recognized === 1 ? "match" : "malformed";
|
|
37582
|
+
}
|
|
37583
|
+
function countOccurrences(value, token) {
|
|
37584
|
+
let count = 0;
|
|
37585
|
+
let offset = 0;
|
|
37586
|
+
while ((offset = value.indexOf(token, offset)) !== -1) {
|
|
37587
|
+
count += 1;
|
|
37588
|
+
offset += token.length;
|
|
37589
|
+
}
|
|
37590
|
+
return count;
|
|
37591
|
+
}
|
|
37592
|
+
function isRecord5(value) {
|
|
37593
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37594
|
+
}
|
|
37595
|
+
|
|
37261
37596
|
// src/tracking.ts
|
|
37262
37597
|
init_protocol();
|
|
37263
37598
|
var TRACKER_SCHEMA_VERSION = 2;
|
|
@@ -38535,8 +38870,7 @@ async function projectRemoteTicketV2(root, policy, binding, pending, credentials
|
|
|
38535
38870
|
if (shouldPublishMilestone(policy, binding, pending.projection)) {
|
|
38536
38871
|
const commentKey = trackerEffectKey(policy, pending.projection, "comment");
|
|
38537
38872
|
if (!hasTrackerEffect(activePending, commentKey)) {
|
|
38538
|
-
const
|
|
38539
|
-
const commentId = await publishRemoteMilestone(policy, binding, renderMilestone(pending.projection, marker2), marker2, credentials, dependencies);
|
|
38873
|
+
const commentId = await publishRemoteMilestone(policy, binding, pending.projection, commentKey, credentials, dependencies);
|
|
38540
38874
|
activePending = await acknowledgeTrackerEffect(root, activePending, { key: commentKey, kind: "comment", remoteId: commentId, at: now(dependencies) });
|
|
38541
38875
|
}
|
|
38542
38876
|
for (const artifact of pending.projection.artifacts ?? []) {
|
|
@@ -38626,8 +38960,12 @@ async function transitionRemoteTicketV2(policy, binding, projection, credentials
|
|
|
38626
38960
|
}
|
|
38627
38961
|
return { ...binding, url: `${jiraOrigin(policy)}/browse/${encodeURIComponent(binding.remoteKey)}` };
|
|
38628
38962
|
}
|
|
38629
|
-
async function publishRemoteMilestone(policy, binding,
|
|
38963
|
+
async function publishRemoteMilestone(policy, binding, projection, effectKey, credentials, dependencies) {
|
|
38630
38964
|
if (policy.provider === "github") {
|
|
38965
|
+
const milestone2 = renderTrackerMilestone("github", projection, effectKey);
|
|
38966
|
+
if (milestone2.provider !== "github") {
|
|
38967
|
+
throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "GitHub milestone renderer returned another provider payload");
|
|
38968
|
+
}
|
|
38631
38969
|
const token = accessTokenFor(credentials, "github");
|
|
38632
38970
|
const found2 = [];
|
|
38633
38971
|
let complete2 = false;
|
|
@@ -38639,8 +38977,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38639
38977
|
}, [200], [token]);
|
|
38640
38978
|
if (!Array.isArray(comments))
|
|
38641
38979
|
throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "GitHub milestone comments are malformed");
|
|
38642
|
-
|
|
38643
|
-
|
|
38980
|
+
for (const entry of comments) {
|
|
38981
|
+
const comment = record4(entry, "GitHub milestone comment");
|
|
38982
|
+
const inspection = inspectTrackerMilestoneMarker("github", comment, effectKey);
|
|
38983
|
+
if (inspection === "malformed") {
|
|
38984
|
+
throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A GitHub comment contains malformed milestone ownership evidence");
|
|
38985
|
+
}
|
|
38986
|
+
if (inspection === "match") {
|
|
38987
|
+
found2.push(String(requiredNumber(comment, "id", "GitHub milestone comment id")));
|
|
38988
|
+
}
|
|
38989
|
+
}
|
|
38644
38990
|
if (comments.length < 100) {
|
|
38645
38991
|
complete2 = true;
|
|
38646
38992
|
break;
|
|
@@ -38656,11 +39002,15 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38656
39002
|
method: "POST",
|
|
38657
39003
|
url: `https://api.github.com/repos/${encodeURIComponent(policy.target.owner)}/${encodeURIComponent(policy.target.repository)}/issues/${encodeURIComponent(binding.remoteKey)}/comments`,
|
|
38658
39004
|
headers: githubHeaders(token),
|
|
38659
|
-
body: JSON.stringify({ body })
|
|
39005
|
+
body: JSON.stringify({ body: milestone2.body })
|
|
38660
39006
|
}, [201], [token]);
|
|
38661
39007
|
return String(requiredNumber(created2, "id", "GitHub milestone comment id"));
|
|
38662
39008
|
}
|
|
38663
39009
|
if (policy.provider === "linear") {
|
|
39010
|
+
const milestone2 = renderTrackerMilestone("linear", projection, effectKey);
|
|
39011
|
+
if (milestone2.provider !== "linear") {
|
|
39012
|
+
throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "Linear milestone renderer returned another provider payload");
|
|
39013
|
+
}
|
|
38664
39014
|
const accessToken = linearAuthorizationFor(credentials);
|
|
38665
39015
|
let after = null;
|
|
38666
39016
|
const cursors = new Set;
|
|
@@ -38672,8 +39022,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38672
39022
|
}`, { id: binding.remoteId, after }, accessToken, dependencies);
|
|
38673
39023
|
const issue2 = record4(data2.issue, "Linear milestone issue");
|
|
38674
39024
|
const comments = connection(issue2.comments, "Linear milestone comments");
|
|
38675
|
-
const
|
|
38676
|
-
|
|
39025
|
+
for (const entry of comments.nodes) {
|
|
39026
|
+
const comment = record4(entry, "Linear milestone comment");
|
|
39027
|
+
const inspection = inspectTrackerMilestoneMarker("linear", comment, effectKey);
|
|
39028
|
+
if (inspection === "malformed") {
|
|
39029
|
+
throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A Linear comment contains malformed milestone ownership evidence");
|
|
39030
|
+
}
|
|
39031
|
+
if (inspection === "match") {
|
|
39032
|
+
found2.push(requiredString(comment, "id", "Linear milestone comment id"));
|
|
39033
|
+
}
|
|
39034
|
+
}
|
|
38677
39035
|
if (!comments.hasNextPage) {
|
|
38678
39036
|
complete2 = true;
|
|
38679
39037
|
break;
|
|
@@ -38688,12 +39046,16 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38688
39046
|
return found2[0];
|
|
38689
39047
|
const data = await linearGraphql(`mutation Milestone($input: CommentCreateInput!) {
|
|
38690
39048
|
commentCreate(input: $input) { success comment { id body } }
|
|
38691
|
-
}`, { input: { issueId: binding.remoteId, body } }, accessToken, dependencies);
|
|
39049
|
+
}`, { input: { issueId: binding.remoteId, body: milestone2.body } }, accessToken, dependencies);
|
|
38692
39050
|
const create = record4(data.commentCreate, "Linear commentCreate");
|
|
38693
39051
|
if (create.success !== true)
|
|
38694
39052
|
throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "Linear did not confirm milestone comment creation");
|
|
38695
39053
|
return requiredString(record4(create.comment, "Linear milestone comment"), "id", "Linear milestone comment id");
|
|
38696
39054
|
}
|
|
39055
|
+
const milestone = renderTrackerMilestone("jira", projection, effectKey);
|
|
39056
|
+
if (milestone.provider !== "jira") {
|
|
39057
|
+
throw new EmpiricalError("TRACKER_RENDERER_MISMATCH", "Jira milestone renderer returned another provider payload");
|
|
39058
|
+
}
|
|
38697
39059
|
const jira = jiraRequestContext(policy.target.siteUrl, credentials);
|
|
38698
39060
|
let startAt = 0;
|
|
38699
39061
|
const found = [];
|
|
@@ -38701,13 +39063,21 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38701
39063
|
for (let page = 0;page < TRACKER_DISCOVERY_LIMIT; page += 1) {
|
|
38702
39064
|
const response = record4(await requestJson(dependencies, {
|
|
38703
39065
|
method: "GET",
|
|
38704
|
-
url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment?startAt=${startAt}&maxResults=100`,
|
|
39066
|
+
url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment?startAt=${startAt}&maxResults=100&expand=properties`,
|
|
38705
39067
|
headers: jira.headers
|
|
38706
39068
|
}, [200], jira.secrets), "Jira milestone comments");
|
|
38707
39069
|
if (!Array.isArray(response.comments))
|
|
38708
39070
|
throw new EmpiricalError("TRACKER_MALFORMED_RESPONSE", "Jira milestone comments are malformed");
|
|
38709
|
-
const
|
|
38710
|
-
|
|
39071
|
+
for (const entry of response.comments) {
|
|
39072
|
+
const comment = record4(entry, "Jira milestone comment");
|
|
39073
|
+
const inspection = inspectTrackerMilestoneMarker("jira", comment, effectKey);
|
|
39074
|
+
if (inspection === "malformed") {
|
|
39075
|
+
throw new EmpiricalError("TRACKER_MARKER_MALFORMED", "A Jira comment contains malformed milestone ownership evidence");
|
|
39076
|
+
}
|
|
39077
|
+
if (inspection === "match") {
|
|
39078
|
+
found.push(requiredString(comment, "id", "Jira milestone comment id"));
|
|
39079
|
+
}
|
|
39080
|
+
}
|
|
38711
39081
|
const total = optionalFiniteNumber(response.total) ?? response.comments.length;
|
|
38712
39082
|
if (startAt + response.comments.length >= total) {
|
|
38713
39083
|
complete = true;
|
|
@@ -38727,7 +39097,7 @@ async function publishRemoteMilestone(policy, binding, body, marker2, credential
|
|
|
38727
39097
|
method: "POST",
|
|
38728
39098
|
url: `${jira.apiOrigin}/rest/api/3/issue/${encodeURIComponent(binding.remoteKey)}/comment`,
|
|
38729
39099
|
headers: jira.headers,
|
|
38730
|
-
body: JSON.stringify({ body:
|
|
39100
|
+
body: JSON.stringify({ body: milestone.body, properties: [milestone.property] })
|
|
38731
39101
|
}, [201], jira.secrets), "Jira milestone comment");
|
|
38732
39102
|
return requiredString(created, "id", "Jira milestone comment id");
|
|
38733
39103
|
}
|
|
@@ -38849,27 +39219,6 @@ function trackerEffectKey(policy, projection, kind, artifactDigest = null) {
|
|
|
38849
39219
|
artifactDigest
|
|
38850
39220
|
});
|
|
38851
39221
|
}
|
|
38852
|
-
function milestoneMarker(key) {
|
|
38853
|
-
return `[Empirical milestone](<${LINEAR_MARKER_ORIGIN}#empirical-milestone:${key}>)`;
|
|
38854
|
-
}
|
|
38855
|
-
function renderMilestone(projection, marker2) {
|
|
38856
|
-
const clean = (value) => value?.replace(/<!--|-->/g, "").replace(/\s+/g, " ").trim();
|
|
38857
|
-
const summary = clean(projection.summary);
|
|
38858
|
-
const blocker = clean(projection.blocker);
|
|
38859
|
-
const artifacts = projection.artifacts ?? [];
|
|
38860
|
-
return [
|
|
38861
|
-
`## Empirical milestone · ${readableTrackerToken(projection.phase)}`,
|
|
38862
|
-
marker2,
|
|
38863
|
-
`- Feature: ${projection.feature}`,
|
|
38864
|
-
`- Revision: ${projection.revision}`,
|
|
38865
|
-
`- Progress: ${readableTrackerToken(projection.progress)}`,
|
|
38866
|
-
`- Completion: ${readableCompletionLevel(projection.completionLevel)}`,
|
|
38867
|
-
...summary ? [`- Summary: ${summary}`] : [],
|
|
38868
|
-
...blocker ? [`- Blocker: ${blocker}`] : [],
|
|
38869
|
-
...artifacts.length ? ["- Reviewable artifacts:", ...artifacts.map((artifact) => artifact.url ? ` - ${artifact.path} · ${artifact.receiptId} · ${artifact.url}` : ` - ${artifact.path} · ${artifact.receiptId} · provider upload/link pending or unsupported`)] : []
|
|
38870
|
-
].join(`
|
|
38871
|
-
`);
|
|
38872
|
-
}
|
|
38873
39222
|
function hasTrackerEffect(pending, key) {
|
|
38874
39223
|
return (pending.effects ?? []).some((effect) => effect.key === key);
|
|
38875
39224
|
}
|
|
@@ -40301,14 +40650,13 @@ Gate: ${summary}` : "";
|
|
|
40301
40650
|
].join(`
|
|
40302
40651
|
`);
|
|
40303
40652
|
}
|
|
40304
|
-
var LINEAR_MARKER_ORIGIN = "https://github.com/goempirical/empirical-sdd";
|
|
40305
40653
|
function linearProjectionBoundary(feature, boundary) {
|
|
40306
40654
|
const label = boundary === "start" ? "Delivery status" : "Managed by Empirical SDD · local workflow is authoritative";
|
|
40307
40655
|
const heading = boundary === "start" ? "## " : "";
|
|
40308
|
-
return `${heading}[${label}](<${
|
|
40656
|
+
return `${heading}[${label}](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${feature}:${boundary}>)`;
|
|
40309
40657
|
}
|
|
40310
40658
|
function linearCreateMarkerLine(intent, idempotencyKey) {
|
|
40311
|
-
return `[Crash-safe synchronization enabled](<${
|
|
40659
|
+
return `[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${intent.marker}:${idempotencyKey}>)`;
|
|
40312
40660
|
}
|
|
40313
40661
|
function readableTrackerToken(value) {
|
|
40314
40662
|
return value.split(/[-_]/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
@@ -40320,7 +40668,7 @@ function renderLinearProjection(projection) {
|
|
|
40320
40668
|
const summary = projection.summary?.replace(/<!--|-->/g, "").replace(/\s+/g, " ").trim();
|
|
40321
40669
|
return [
|
|
40322
40670
|
linearProjectionBoundary(projection.feature, "start"),
|
|
40323
|
-
`[${readableTrackerToken(projection.progress)}](<${
|
|
40671
|
+
`[${readableTrackerToken(projection.progress)}](<${TRACKER_MARKER_ORIGIN}#${projection.marker}>)`,
|
|
40324
40672
|
`- Phase: ${readableTrackerToken(projection.phase)}`,
|
|
40325
40673
|
`- Workflow: ${readableTrackerToken(projection.status)}`,
|
|
40326
40674
|
`- Revision: ${projection.revision}`,
|
|
@@ -40337,11 +40685,11 @@ function upsertLinearMarkerBlock(existing, projection) {
|
|
|
40337
40685
|
const legacyEnd = `<!-- empirical-sdd:${projection.feature}:end -->`;
|
|
40338
40686
|
const readableStart = linearProjectionBoundary(projection.feature, "start");
|
|
40339
40687
|
const readableEnd = linearProjectionBoundary(projection.feature, "end");
|
|
40340
|
-
const priorReadableStart = `[**Empirical SDD**](${
|
|
40341
|
-
const priorCanonicalStart = `**[Empirical SDD](<${
|
|
40342
|
-
const priorReadableEnd = `[Managed projection](${
|
|
40343
|
-
const priorCanonicalEnd = `[Managed projection](<${
|
|
40344
|
-
const priorHeadingStart = `## [Empirical SDD](<${
|
|
40688
|
+
const priorReadableStart = `[**Empirical SDD**](${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start)`;
|
|
40689
|
+
const priorCanonicalStart = `**[Empirical SDD](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
|
|
40690
|
+
const priorReadableEnd = `[Managed projection](${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end)`;
|
|
40691
|
+
const priorCanonicalEnd = `[Managed projection](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:end>)`;
|
|
40692
|
+
const priorHeadingStart = `## [Empirical SDD](<${TRACKER_MARKER_ORIGIN}#empirical-sdd:${projection.feature}:start>)`;
|
|
40345
40693
|
const priorIndentedReadableEnd = ` ${readableEnd}`;
|
|
40346
40694
|
const markerFragments = [
|
|
40347
40695
|
`empirical-sdd:${projection.feature}:start`,
|
|
@@ -40382,12 +40730,12 @@ function linearCreateMarkerMatches(value, marker2) {
|
|
|
40382
40730
|
const markerPrefix2 = `${marker2}:`;
|
|
40383
40731
|
const legacyStartPrefix = `<!-- ${markerPrefix2}`;
|
|
40384
40732
|
const readablePrefixes = [
|
|
40385
|
-
`[Recovery reference](${
|
|
40386
|
-
`[Crash-safe synchronization enabled](${
|
|
40733
|
+
`[Recovery reference](${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`,
|
|
40734
|
+
`[Crash-safe synchronization enabled](${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`
|
|
40387
40735
|
];
|
|
40388
40736
|
const canonicalReadablePrefixes = [
|
|
40389
|
-
`[Recovery reference](<${
|
|
40390
|
-
`[Crash-safe synchronization enabled](<${
|
|
40737
|
+
`[Recovery reference](<${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`,
|
|
40738
|
+
`[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${markerPrefix2}`
|
|
40391
40739
|
];
|
|
40392
40740
|
const matches = [];
|
|
40393
40741
|
const consumed = new Set;
|
|
@@ -40445,7 +40793,7 @@ function migrateLinearCreateMarker(existing, feature) {
|
|
|
40445
40793
|
const lines = existing.replace(/\r\n/g, `
|
|
40446
40794
|
`).split(`
|
|
40447
40795
|
`);
|
|
40448
|
-
const replacement = `[Crash-safe synchronization enabled](<${
|
|
40796
|
+
const replacement = `[Crash-safe synchronization enabled](<${TRACKER_MARKER_ORIGIN}#${marker2}:${match.idempotencyKey}>)`;
|
|
40449
40797
|
return [
|
|
40450
40798
|
...lines.slice(0, match.start),
|
|
40451
40799
|
replacement,
|
|
@@ -44091,7 +44439,7 @@ function mcpDefaultRoot() {
|
|
|
44091
44439
|
}
|
|
44092
44440
|
async function prepareTrackerOAuthHandoff(server, subject, dependencies) {
|
|
44093
44441
|
const elicitation = server.server.getClientCapabilities()?.elicitation;
|
|
44094
|
-
if (!
|
|
44442
|
+
if (!isRecord6(elicitation) || !isRecord6(elicitation.url))
|
|
44095
44443
|
return;
|
|
44096
44444
|
const authorization = await trackerOAuthAuthorization(subject, dependencies);
|
|
44097
44445
|
if (!authorization)
|
|
@@ -44113,13 +44461,13 @@ async function toolResult(operation2) {
|
|
|
44113
44461
|
const value = await operation2();
|
|
44114
44462
|
return {
|
|
44115
44463
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
44116
|
-
structuredContent:
|
|
44464
|
+
structuredContent: isRecord6(value) ? value : { value }
|
|
44117
44465
|
};
|
|
44118
44466
|
} catch (error51) {
|
|
44119
44467
|
return { isError: true, content: [{ type: "text", text: asErrorMessage(error51) }] };
|
|
44120
44468
|
}
|
|
44121
44469
|
}
|
|
44122
|
-
function
|
|
44470
|
+
function isRecord6(value) {
|
|
44123
44471
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44124
44472
|
}
|
|
44125
44473
|
|