dxcomplete 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -22
- package/dist/init.js +19 -0
- package/dist/mcp/docs.d.ts +18 -2
- package/dist/mcp/docs.js +201 -13
- package/dist/mcp/server.js +761 -47
- package/dist/runtime/check.d.ts +1 -1
- package/dist/runtime/records.d.ts +95 -4
- package/dist/runtime/records.js +640 -11
- package/dist/validate.js +3 -1
- package/docs/codex-integration.md +45 -18
- package/docs/glossary.md +39 -7
- package/docs/index.md +2 -1
- package/docs/model.md +3 -3
- package/docs/operating-guide.md +116 -0
- package/docs/taxonomy.md +12 -6
- package/docs/workflows.md +5 -3
- package/package.json +21 -3
- package/scripts/{dogfood-work-order.mjs → runtime-work-order.mjs} +4 -4
- package/scripts/smoke-mcp-http.mjs +460 -6
- package/src/init.ts +35 -0
- package/src/mcp/docs.ts +234 -14
- package/src/mcp/server.ts +1138 -83
- package/src/runtime/records.ts +914 -12
- package/src/validate.ts +3 -1
- package/templates/AGENTS.md +30 -0
- package/templates/process/controls.yml +10 -6
- package/templates/process/diagrams/01-intake-triage.mmd +5 -5
- package/templates/process/diagrams/02-product-definition.mmd +1 -1
- package/templates/process/diagrams/06-change-release-control.mmd +5 -7
- package/templates/process/diagrams/07-deployment-operations.mmd +2 -2
- package/templates/process/diagrams/08-support-incident-management.mmd +5 -4
- package/templates/process/diagrams/09-problem-improvement.mmd +4 -3
- package/templates/process/diagrams/10-risk-control-management.mmd +6 -4
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
- package/templates/process/taxonomy.yml +91 -17
- package/templates/process/workflows.yml +10 -9
- package/website/flow.html +1 -0
- package/website/glossary.html +37 -8
- package/website/index.html +2 -1
- package/website/objects.html +68 -11
- package/website/operating-guide.html +165 -0
- package/website/outcomes.html +1 -0
- package/website/phase-build.html +1 -0
- package/website/phase-elicit.html +1 -0
- package/website/phase-go-live.html +2 -1
- package/website/phase-measure.html +1 -0
- package/website/phase-operate.html +1 -0
- package/website/phase-orient.html +1 -0
- package/website/phase-weigh.html +1 -0
- package/website/roles.html +1 -0
|
@@ -25,7 +25,7 @@ import { loadWorkspaceConfig } from "../dist/runtime/workspace.js";
|
|
|
25
25
|
|
|
26
26
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
27
|
const runId = `smoke-http-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
28
|
-
const expectedHostedToolCount =
|
|
28
|
+
const expectedHostedToolCount = 60;
|
|
29
29
|
const smokeTimeoutMs = 60_000;
|
|
30
30
|
const smokeCleanupActorId = "dxcomplete-http-smoke";
|
|
31
31
|
const smokeStartedAt = Date.now();
|
|
@@ -44,6 +44,11 @@ const smokeRecordTypes = [
|
|
|
44
44
|
"commitments",
|
|
45
45
|
"deferrals",
|
|
46
46
|
"changes",
|
|
47
|
+
"incidents",
|
|
48
|
+
"problems",
|
|
49
|
+
"maintenance_schedules",
|
|
50
|
+
"support_requests",
|
|
51
|
+
"value_realizations",
|
|
47
52
|
"decisions",
|
|
48
53
|
"risks"
|
|
49
54
|
];
|
|
@@ -69,6 +74,11 @@ let createdBenefitsId;
|
|
|
69
74
|
let createdCommitmentId;
|
|
70
75
|
let createdDeferralId;
|
|
71
76
|
let createdChangeId;
|
|
77
|
+
let createdChangeEnvironmentId;
|
|
78
|
+
let createdChangeComponentId;
|
|
79
|
+
let createdIncidentId;
|
|
80
|
+
let createdProblemId;
|
|
81
|
+
let createdRiskId;
|
|
72
82
|
let createdDecisionId;
|
|
73
83
|
let createdServiceClientId;
|
|
74
84
|
|
|
@@ -346,6 +356,16 @@ try {
|
|
|
346
356
|
assert(status.server.tools.includes("append_deferral_event"), "Hosted surface is missing append_deferral_event.");
|
|
347
357
|
assert(status.server.tools.includes("create_change"), "Hosted surface is missing create_change.");
|
|
348
358
|
assert(status.server.tools.includes("append_change_event"), "Hosted surface is missing append_change_event.");
|
|
359
|
+
for (const itsmToolName of [
|
|
360
|
+
"create_risk",
|
|
361
|
+
"append_risk_entry",
|
|
362
|
+
"create_incident",
|
|
363
|
+
"append_incident_entry",
|
|
364
|
+
"create_problem",
|
|
365
|
+
"append_problem_entry"
|
|
366
|
+
]) {
|
|
367
|
+
assert(status.server.tools.includes(itsmToolName), `Hosted surface is missing ${itsmToolName}.`);
|
|
368
|
+
}
|
|
349
369
|
assert(status.server.tools.includes("link_decision_input"), "Hosted surface is missing link_decision_input.");
|
|
350
370
|
assert(status.server.tools.includes("unlink_records"), "Hosted surface is missing unlink_records.");
|
|
351
371
|
assert(status.server.tools.includes("append_decision_entry"), "Hosted surface is missing append_decision_entry.");
|
|
@@ -605,6 +625,11 @@ try {
|
|
|
605
625
|
processGuide.runtimeRecordTypes?.includes("components"),
|
|
606
626
|
"environments and components should appear as current runtime record types."
|
|
607
627
|
);
|
|
628
|
+
assert(
|
|
629
|
+
processGuide.runtimeRecordTypes?.includes("incidents") &&
|
|
630
|
+
processGuide.runtimeRecordTypes?.includes("problems"),
|
|
631
|
+
"incidents and problems should appear as current runtime record types."
|
|
632
|
+
);
|
|
608
633
|
for (const removedRuntimeRecordType of [
|
|
609
634
|
"service_charters",
|
|
610
635
|
"business_cases",
|
|
@@ -644,6 +669,20 @@ try {
|
|
|
644
669
|
processGuideText.includes("component"),
|
|
645
670
|
"Hosted process guide should describe Operational Registry routing."
|
|
646
671
|
);
|
|
672
|
+
const roleOperatingGuidanceText = JSON.stringify(processGuide.roleOperatingGuidance ?? []);
|
|
673
|
+
assert(
|
|
674
|
+
roleOperatingGuidanceText.includes("Default to Requirement -> Task") &&
|
|
675
|
+
roleOperatingGuidanceText.includes("Use Change for discrete run-side alterations") &&
|
|
676
|
+
roleOperatingGuidanceText.includes("DX Complete Ticket"),
|
|
677
|
+
"Hosted process guide should include compact role operating guidance."
|
|
678
|
+
);
|
|
679
|
+
const itsmGuidanceText = JSON.stringify(processGuide.itsmGuidance ?? []);
|
|
680
|
+
assert(
|
|
681
|
+
itsmGuidanceText.includes("Incident is the current first-class record") &&
|
|
682
|
+
itsmGuidanceText.includes("Problem is the current first-class record") &&
|
|
683
|
+
itsmGuidanceText.includes("Do not create ITSM-style records merely because work is happening"),
|
|
684
|
+
"Hosted process guide should describe current Incident/Problem records with restraint guidance."
|
|
685
|
+
);
|
|
647
686
|
|
|
648
687
|
const rolesDoc = await callJsonTool("get_doc", { page: "roles" });
|
|
649
688
|
assert(rolesDoc.surfaceVersion === status.server.surfaceVersion, "Hosted get_doc surface version did not match runtime_status.");
|
|
@@ -673,6 +712,21 @@ try {
|
|
|
673
712
|
for (const oldRoleName of ["Director", "Product Lead", "Product", "Engineering", "Coder", "QA", "Operations", "Support", "Admin", "Client/User"]) {
|
|
674
713
|
assert(!actualRoleNames.includes(oldRoleName), `Hosted get_doc roles should not include old role ${oldRoleName}.`);
|
|
675
714
|
}
|
|
715
|
+
const operatingGuideDoc = await callJsonTool("get_doc", { page: "operating_guide" });
|
|
716
|
+
assert(
|
|
717
|
+
operatingGuideDoc.surfaceFingerprint === status.server.surfaceFingerprint,
|
|
718
|
+
"Hosted get_doc operating guide fingerprint did not match runtime_status."
|
|
719
|
+
);
|
|
720
|
+
const operatingGuideText = JSON.stringify(operatingGuideDoc);
|
|
721
|
+
assert(
|
|
722
|
+
operatingGuideDoc.roles?.some(
|
|
723
|
+
(role) => role.name === "Engineer and Codex assistance" && role.use.includes("Requirement to Task")
|
|
724
|
+
) &&
|
|
725
|
+
operatingGuideDoc.roles?.some((role) => role.name === "Operator and administration" && role.defaultRecords.includes("Incident") && role.defaultRecords.includes("Problem")) &&
|
|
726
|
+
operatingGuideText.includes("specific service-impacting") &&
|
|
727
|
+
operatingGuideText.includes("DX Complete Ticket"),
|
|
728
|
+
"Hosted get_doc operating guide should include role routing, Change boundary, tickets, and Incident/Problem guidance."
|
|
729
|
+
);
|
|
676
730
|
|
|
677
731
|
const glossaryDoc = await callJsonTool("get_doc", { page: "glossary" });
|
|
678
732
|
assert(
|
|
@@ -1757,9 +1811,71 @@ try {
|
|
|
1757
1811
|
"append_deferral_event"
|
|
1758
1812
|
);
|
|
1759
1813
|
|
|
1814
|
+
const risk = await callJsonTool("create_risk", {
|
|
1815
|
+
title: `DX Complete hosted MCP smoke risk ${runId}`,
|
|
1816
|
+
topic: "Smoke uncertainty.",
|
|
1817
|
+
likelihood: "low",
|
|
1818
|
+
impact: "low",
|
|
1819
|
+
treatment: "mitigate",
|
|
1820
|
+
treatmentRationale: "Smoke risk is mitigated by cleanup."
|
|
1821
|
+
});
|
|
1822
|
+
createdRiskId = risk._id;
|
|
1823
|
+
assert(risk.recordType === "risks", "Hosted create_risk created the wrong record type.");
|
|
1824
|
+
assert(risk.readableId?.startsWith("RSK-"), "Hosted Risk did not receive an RSK readable ID.");
|
|
1825
|
+
assert(risk.fields.currentStatus?.status === "open", "Hosted Risk did not derive current status.");
|
|
1826
|
+
assert(risk.fields.currentAssessment?.likelihood === "low", "Hosted Risk did not derive current assessment.");
|
|
1827
|
+
assert(risk.fields.currentTreatment?.treatment === "mitigate", "Hosted Risk did not derive current treatment.");
|
|
1828
|
+
const reassessedRisk = await callJsonTool("append_risk_entry", {
|
|
1829
|
+
riskId: risk._id,
|
|
1830
|
+
entryType: "assessment",
|
|
1831
|
+
body: "Smoke reassessment.",
|
|
1832
|
+
likelihood: "medium",
|
|
1833
|
+
impact: "high"
|
|
1834
|
+
});
|
|
1835
|
+
assert(reassessedRisk.fields.currentAssessment?.impact === "high", "Hosted Risk assessment did not derive from latest assessment entry.");
|
|
1836
|
+
const closedRisk = await callJsonTool("append_risk_entry", {
|
|
1837
|
+
riskId: risk._id,
|
|
1838
|
+
entryType: "closed",
|
|
1839
|
+
body: "Smoke risk closed."
|
|
1840
|
+
});
|
|
1841
|
+
assert(closedRisk.fields.currentStatus?.status === "closed", "Hosted Risk status did not derive from closed entry.");
|
|
1842
|
+
await expectToolError(
|
|
1843
|
+
"create_record",
|
|
1844
|
+
{
|
|
1845
|
+
recordType: "risks",
|
|
1846
|
+
title: `DX Complete hosted MCP direct risk ${runId}`,
|
|
1847
|
+
fields: { entries: [] }
|
|
1848
|
+
},
|
|
1849
|
+
"append_risk_entry"
|
|
1850
|
+
);
|
|
1851
|
+
await expectToolError(
|
|
1852
|
+
"update_record",
|
|
1853
|
+
{
|
|
1854
|
+
recordType: "risks",
|
|
1855
|
+
id: risk._id,
|
|
1856
|
+
unsetFields: ["currentStatus"]
|
|
1857
|
+
},
|
|
1858
|
+
"append_risk_entry"
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
const changeEnvironment = await callJsonTool("create_environment", {
|
|
1862
|
+
name: `DX Complete hosted MCP smoke change environment ${runId}`,
|
|
1863
|
+
summary: "Change smoke environment."
|
|
1864
|
+
});
|
|
1865
|
+
createdChangeEnvironmentId = changeEnvironment._id;
|
|
1866
|
+
const changeComponent = await callJsonTool("create_component", {
|
|
1867
|
+
name: `DX Complete hosted MCP smoke change component ${runId}`,
|
|
1868
|
+
environmentId: changeEnvironment._id,
|
|
1869
|
+
kind: "smoke component",
|
|
1870
|
+
locator: { route: "/smoke-change" }
|
|
1871
|
+
});
|
|
1872
|
+
createdChangeComponentId = changeComponent._id;
|
|
1873
|
+
|
|
1760
1874
|
const change = await callJsonTool("create_change", {
|
|
1761
1875
|
title: `DX Complete hosted MCP smoke change ${runId}`,
|
|
1762
1876
|
summary: "Change used by the hosted MCP smoke test.",
|
|
1877
|
+
changeType: "emergency",
|
|
1878
|
+
emergencyImportance: "Smoke change importance is only test coverage.",
|
|
1763
1879
|
changePlan: "Record a harmless smoke-test service change.",
|
|
1764
1880
|
executionSteps: ["Create the Change record.", "Append the smoke-test events."],
|
|
1765
1881
|
rollbackPlan: "Archive the smoke-test records during cleanup.",
|
|
@@ -1774,6 +1890,8 @@ try {
|
|
|
1774
1890
|
assert(Array.isArray(change.fields.executionSteps) && change.fields.executionSteps.length === 2, "Hosted Change did not store executionSteps.");
|
|
1775
1891
|
assert(change.fields.rollbackPlan?.includes("Archive"), "Hosted Change did not store rollbackPlan.");
|
|
1776
1892
|
assert(change.fields.riskImpact?.includes("No service impact"), "Hosted Change did not store riskImpact.");
|
|
1893
|
+
assert(change.fields.changeType === "emergency", "Hosted Change did not store changeType.");
|
|
1894
|
+
assert(change.fields.emergencyRationaleGaps?.includes("emergencyImmediacy"), "Hosted emergency Change did not expose missing immediacy rationale.");
|
|
1777
1895
|
assert(
|
|
1778
1896
|
change.links.some((link) => link.toType === "requirements" && link.toId === requirement._id && link.relationship === "for_requirement"),
|
|
1779
1897
|
"Hosted create_change did not link to the requirement."
|
|
@@ -1802,14 +1920,53 @@ try {
|
|
|
1802
1920
|
"Hosted Change veto event did not store the veto role."
|
|
1803
1921
|
);
|
|
1804
1922
|
|
|
1923
|
+
const changeWithResultCommit = await callJsonTool("append_change_event", {
|
|
1924
|
+
changeId: change._id,
|
|
1925
|
+
eventType: "result_reported",
|
|
1926
|
+
result: "completed",
|
|
1927
|
+
summary: "Smoke result with Git reference.",
|
|
1928
|
+
gitCommitReferences: [
|
|
1929
|
+
{
|
|
1930
|
+
commit: "abc1234",
|
|
1931
|
+
repository: "smoke-repo",
|
|
1932
|
+
url: "https://example.test/smoke-repo/commit/abc1234"
|
|
1933
|
+
}
|
|
1934
|
+
]
|
|
1935
|
+
});
|
|
1936
|
+
const resultEvent = changeWithResultCommit.fields.events?.at(-1);
|
|
1937
|
+
assert(resultEvent?.gitCommitReferences?.[0]?.commit === "abc1234", "Hosted Change result event did not store Git commit reference.");
|
|
1938
|
+
assert(resultEvent?.gitCommitReferences?.[0]?.repository === "smoke-repo", "Hosted Change result event did not store Git repository reference.");
|
|
1939
|
+
|
|
1940
|
+
const changeWithRecoveryCommit = await callJsonTool("append_change_event", {
|
|
1941
|
+
changeId: change._id,
|
|
1942
|
+
eventType: "recovery_recorded",
|
|
1943
|
+
summary: "Smoke recovery with rollback commit reference.",
|
|
1944
|
+
gitCommitReferences: [{ commit: "def5678" }]
|
|
1945
|
+
});
|
|
1946
|
+
assert(
|
|
1947
|
+
changeWithRecoveryCommit.fields.events?.at(-1)?.gitCommitReferences?.[0]?.commit === "def5678",
|
|
1948
|
+
"Hosted Change recovery event did not store Git commit reference."
|
|
1949
|
+
);
|
|
1950
|
+
|
|
1805
1951
|
await expectToolError(
|
|
1806
1952
|
"append_change_event",
|
|
1807
1953
|
{
|
|
1808
1954
|
changeId: change._id,
|
|
1809
1955
|
eventType: "emergency_declared",
|
|
1810
|
-
|
|
1956
|
+
note: "Emergency is modeled through changeType, not an event."
|
|
1957
|
+
},
|
|
1958
|
+
"Invalid option"
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
await expectToolError(
|
|
1962
|
+
"append_change_event",
|
|
1963
|
+
{
|
|
1964
|
+
changeId: change._id,
|
|
1965
|
+
eventType: "notice_given",
|
|
1966
|
+
notice: "Smoke notice with invalid Git reference.",
|
|
1967
|
+
gitCommitReferences: [{ commit: "abc1234" }]
|
|
1811
1968
|
},
|
|
1812
|
-
"
|
|
1969
|
+
"gitCommitReferences"
|
|
1813
1970
|
);
|
|
1814
1971
|
|
|
1815
1972
|
await expectToolError(
|
|
@@ -1853,6 +2010,111 @@ try {
|
|
|
1853
2010
|
"Hosted list_linked_records did not return the linked requirement for Change."
|
|
1854
2011
|
);
|
|
1855
2012
|
|
|
2013
|
+
const incident = await callJsonTool("create_incident", {
|
|
2014
|
+
title: `DX Complete hosted MCP smoke incident ${runId}`,
|
|
2015
|
+
description: "Smoke Incident for Change linkage.",
|
|
2016
|
+
severity: "low",
|
|
2017
|
+
componentIds: [changeComponent._id]
|
|
2018
|
+
});
|
|
2019
|
+
createdIncidentId = incident._id;
|
|
2020
|
+
assert(incident.recordType === "incidents", "Hosted create_incident created the wrong record type.");
|
|
2021
|
+
assert(incident.readableId?.startsWith("INC-"), "Hosted Incident did not receive an INC readable ID.");
|
|
2022
|
+
assert(incident.fields.currentStatus?.status === "open", "Hosted Incident did not derive current status.");
|
|
2023
|
+
assert(incident.fields.currentSeverity?.severity === "low", "Hosted Incident did not derive current severity.");
|
|
2024
|
+
assert(
|
|
2025
|
+
incident.links.some((link) => link.toType === "components" && link.toId === changeComponent._id && link.relationship === "affects"),
|
|
2026
|
+
"Hosted Incident did not link affected Component."
|
|
2027
|
+
);
|
|
2028
|
+
|
|
2029
|
+
const resolvedIncident = await callJsonTool("append_incident_entry", {
|
|
2030
|
+
incidentId: incident._id,
|
|
2031
|
+
entryType: "resolved",
|
|
2032
|
+
body: "Smoke Incident resolved."
|
|
2033
|
+
});
|
|
2034
|
+
assert(resolvedIncident.fields.currentStatus?.status === "resolved", "Hosted Incident status did not derive from resolved entry.");
|
|
2035
|
+
await expectToolError(
|
|
2036
|
+
"create_record",
|
|
2037
|
+
{
|
|
2038
|
+
recordType: "incidents",
|
|
2039
|
+
title: `DX Complete hosted MCP direct incident ${runId}`,
|
|
2040
|
+
fields: { entries: [] }
|
|
2041
|
+
},
|
|
2042
|
+
"append_incident_entry"
|
|
2043
|
+
);
|
|
2044
|
+
|
|
2045
|
+
const problem = await callJsonTool("create_problem", {
|
|
2046
|
+
title: `DX Complete hosted MCP smoke problem ${runId}`,
|
|
2047
|
+
description: "Smoke Problem evidenced by the smoke Incident.",
|
|
2048
|
+
incidentIds: [incident._id],
|
|
2049
|
+
componentIds: [changeComponent._id]
|
|
2050
|
+
});
|
|
2051
|
+
createdProblemId = problem._id;
|
|
2052
|
+
assert(problem.recordType === "problems", "Hosted create_problem created the wrong record type.");
|
|
2053
|
+
assert(problem.readableId?.startsWith("PRB-"), "Hosted Problem did not receive a PRB readable ID.");
|
|
2054
|
+
assert(problem.fields.currentStatus?.status === "open", "Hosted Problem did not derive current status.");
|
|
2055
|
+
assert(
|
|
2056
|
+
problem.links.some((link) => link.toType === "incidents" && link.toId === incident._id && link.relationship === "evidenced_by"),
|
|
2057
|
+
"Hosted Problem did not link to the evidencing Incident."
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
const problemWithRootCause = await callJsonTool("append_problem_entry", {
|
|
2061
|
+
problemId: problem._id,
|
|
2062
|
+
entryType: "root_cause",
|
|
2063
|
+
body: "Smoke root cause.",
|
|
2064
|
+
rootCause: "Smoke root cause."
|
|
2065
|
+
});
|
|
2066
|
+
assert(problemWithRootCause.fields.currentRootCause?.rootCause === "Smoke root cause.", "Hosted Problem root cause did not derive from entry.");
|
|
2067
|
+
await expectToolError(
|
|
2068
|
+
"update_record",
|
|
2069
|
+
{
|
|
2070
|
+
recordType: "problems",
|
|
2071
|
+
id: problem._id,
|
|
2072
|
+
unsetFields: ["entries"]
|
|
2073
|
+
},
|
|
2074
|
+
"append_problem_entry"
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
await callJsonTool("link_records", {
|
|
2078
|
+
fromType: "changes",
|
|
2079
|
+
fromId: change._id,
|
|
2080
|
+
toType: "components",
|
|
2081
|
+
toId: changeComponent._id,
|
|
2082
|
+
relationship: "affects"
|
|
2083
|
+
});
|
|
2084
|
+
await callJsonTool("link_records", {
|
|
2085
|
+
fromType: "changes",
|
|
2086
|
+
fromId: change._id,
|
|
2087
|
+
toType: "incidents",
|
|
2088
|
+
toId: incident._id,
|
|
2089
|
+
relationship: "caused"
|
|
2090
|
+
});
|
|
2091
|
+
await callJsonTool("link_records", {
|
|
2092
|
+
fromType: "changes",
|
|
2093
|
+
fromId: change._id,
|
|
2094
|
+
toType: "incidents",
|
|
2095
|
+
toId: incident._id,
|
|
2096
|
+
relationship: "resolved"
|
|
2097
|
+
});
|
|
2098
|
+
const changeComponentLinks = await callJsonTool("list_linked_records", {
|
|
2099
|
+
recordType: "changes",
|
|
2100
|
+
id: change._id,
|
|
2101
|
+
relationship: "affects"
|
|
2102
|
+
});
|
|
2103
|
+
assert(
|
|
2104
|
+
changeComponentLinks.outbound.some((entry) => entry.record._id === changeComponent._id),
|
|
2105
|
+
"Hosted Change did not link to affected Component."
|
|
2106
|
+
);
|
|
2107
|
+
const changeIncidentLinks = await callJsonTool("list_linked_records", {
|
|
2108
|
+
recordType: "incidents",
|
|
2109
|
+
id: incident._id,
|
|
2110
|
+
direction: "inbound",
|
|
2111
|
+
relationship: "resolved"
|
|
2112
|
+
});
|
|
2113
|
+
assert(
|
|
2114
|
+
changeIncidentLinks.inbound.some((entry) => entry.record._id === change._id),
|
|
2115
|
+
"Hosted inbound traversal did not find Change resolved Incident link."
|
|
2116
|
+
);
|
|
2117
|
+
|
|
1856
2118
|
const referencedRequirement = await callJsonTool("link_records", {
|
|
1857
2119
|
fromType: "requirements",
|
|
1858
2120
|
fromId: requirement._id,
|
|
@@ -2243,7 +2505,12 @@ try {
|
|
|
2243
2505
|
await archiveCreatedRecord("requirements", createdRequirementId);
|
|
2244
2506
|
await archiveCreatedRecord("estimates", createdEstimateId);
|
|
2245
2507
|
await archiveCreatedRecord("benefits", createdBenefitsId);
|
|
2508
|
+
await archiveCreatedRecord("problems", createdProblemId);
|
|
2509
|
+
await archiveCreatedRecord("incidents", createdIncidentId);
|
|
2246
2510
|
await archiveCreatedRecord("changes", createdChangeId);
|
|
2511
|
+
await archiveCreatedRecord("components", createdChangeComponentId);
|
|
2512
|
+
await archiveCreatedRecord("environments", createdChangeEnvironmentId);
|
|
2513
|
+
await archiveCreatedRecord("risks", createdRiskId);
|
|
2247
2514
|
await archiveCreatedRecord("commitments", createdCommitmentId);
|
|
2248
2515
|
await archiveCreatedRecord("deferrals", createdDeferralId);
|
|
2249
2516
|
await archiveCreatedRecord("expectations", createdExpectationId);
|
|
@@ -2568,6 +2835,12 @@ async function runSurfaceSection({ mcpUrl, actor }) {
|
|
|
2568
2835
|
"create_estimate",
|
|
2569
2836
|
"create_benefits",
|
|
2570
2837
|
"create_change",
|
|
2838
|
+
"create_incident",
|
|
2839
|
+
"append_incident_entry",
|
|
2840
|
+
"create_problem",
|
|
2841
|
+
"append_problem_entry",
|
|
2842
|
+
"create_risk",
|
|
2843
|
+
"append_risk_entry",
|
|
2571
2844
|
"create_decision",
|
|
2572
2845
|
"append_decision_entry",
|
|
2573
2846
|
"link_decision_input",
|
|
@@ -2598,7 +2871,7 @@ async function runSurfaceSection({ mcpUrl, actor }) {
|
|
|
2598
2871
|
assert(!status.server.tools.includes(removedToolName), `Hosted runtime should not expose removed tool ${removedToolName}.`);
|
|
2599
2872
|
}
|
|
2600
2873
|
assert(manifestByName.get("get_doc")?.inputFields.join(",") === "page,term", "Hosted get_doc should expose page and optional term inputs.");
|
|
2601
|
-
for (const toolName of ["get_doc", "create_statement", "update_statement", "create_expectation", "create_task", "append_task_entry", "create_environment", "update_environment", "list_environments", "create_component", "update_component", "list_components", "create_estimate", "create_benefits", "create_change", "create_decision", "append_decision_entry", "link_decision_input", "append_journal_note", "read_journal", "get_journal_entry", "append_journal_summary"]) {
|
|
2874
|
+
for (const toolName of ["get_doc", "create_statement", "update_statement", "create_expectation", "create_task", "append_task_entry", "create_environment", "update_environment", "list_environments", "create_component", "update_component", "list_components", "create_estimate", "create_benefits", "create_change", "create_incident", "append_incident_entry", "create_problem", "append_problem_entry", "create_risk", "append_risk_entry", "create_decision", "append_decision_entry", "link_decision_input", "append_journal_note", "read_journal", "get_journal_entry", "append_journal_summary"]) {
|
|
2602
2875
|
assert(!manifestByName.get(toolName)?.inputFields.includes("workspaceId"), `${toolName} should not expose workspaceId on the hosted surface.`);
|
|
2603
2876
|
}
|
|
2604
2877
|
|
|
@@ -2668,12 +2941,31 @@ async function runDocsSection() {
|
|
|
2668
2941
|
processGuide.recordRoutingGuidance?.sharpTest?.includes("Will anything reference or depend on this"),
|
|
2669
2942
|
"Process guide should include compact record-routing guidance."
|
|
2670
2943
|
);
|
|
2944
|
+
const roleOperatingGuidanceText = JSON.stringify(processGuide.roleOperatingGuidance ?? []);
|
|
2945
|
+
assert(
|
|
2946
|
+
roleOperatingGuidanceText.includes("Default to Requirement -> Task") &&
|
|
2947
|
+
roleOperatingGuidanceText.includes("Use Change for discrete run-side alterations") &&
|
|
2948
|
+
roleOperatingGuidanceText.includes("DX Complete Ticket"),
|
|
2949
|
+
"Process guide should include compact role operating guidance."
|
|
2950
|
+
);
|
|
2951
|
+
const itsmGuidanceText = JSON.stringify(processGuide.itsmGuidance ?? []);
|
|
2952
|
+
assert(
|
|
2953
|
+
itsmGuidanceText.includes("Incident is the current first-class record") &&
|
|
2954
|
+
itsmGuidanceText.includes("Problem is the current first-class record") &&
|
|
2955
|
+
itsmGuidanceText.includes("Do not create ITSM-style records merely because work is happening"),
|
|
2956
|
+
"Process guide should describe current Incident/Problem records with restraint guidance."
|
|
2957
|
+
);
|
|
2671
2958
|
assert(processGuide.phases.map((phase) => phase.id).join(",") === "orient,elicit,weigh,build,go_live,operate,measure", "Process guide returned an unexpected phase order.");
|
|
2672
2959
|
assert(processGuide.runtimeRecordTypes?.includes("statements"), "Process guide should include statements.");
|
|
2673
2960
|
assert(processGuide.runtimeRecordTypes?.includes("journal_entries"), "Process guide should include journal_entries.");
|
|
2674
2961
|
assert(processGuide.runtimeRecordTypes?.includes("environments"), "Process guide should include environments.");
|
|
2675
2962
|
assert(processGuide.runtimeRecordTypes?.includes("components"), "Process guide should include components.");
|
|
2963
|
+
assert(processGuide.runtimeRecordTypes?.includes("maintenance_schedules"), "Process guide should include maintenance_schedules.");
|
|
2676
2964
|
assert(processGuide.runtimeRecordTypes?.includes("benefits"), "Process guide should include benefits.");
|
|
2965
|
+
assert(processGuide.runtimeRecordTypes?.includes("value_realizations"), "Process guide should include value_realizations.");
|
|
2966
|
+
assert(processGuide.runtimeRecordTypes?.includes("incidents"), "Process guide should include incidents.");
|
|
2967
|
+
assert(processGuide.runtimeRecordTypes?.includes("problems"), "Process guide should include problems.");
|
|
2968
|
+
assert(processGuide.runtimeRecordTypes?.includes("support_requests"), "Process guide should include support_requests.");
|
|
2677
2969
|
assert(!processGuide.runtimeRecordTypes?.includes("initiatives"), "Process guide should not include initiatives.");
|
|
2678
2970
|
for (const removedRuntimeRecordType of ["service_charters", "cost_baselines", "cost_actuals", "benefit_measurements"]) {
|
|
2679
2971
|
assert(!processGuide.runtimeRecordTypes?.includes(removedRuntimeRecordType), `Process guide should not include ${removedRuntimeRecordType}.`);
|
|
@@ -2682,6 +2974,19 @@ async function runDocsSection() {
|
|
|
2682
2974
|
const rolesDoc = await callJsonTool("get_doc", { page: "roles" });
|
|
2683
2975
|
assert(rolesDoc.surfaceFingerprint === status.server.surfaceFingerprint, "Roles doc fingerprint did not match runtime_status.");
|
|
2684
2976
|
assert(rolesDoc.roles?.map((role) => role.name).join(",") === "Owner,Engineer,Tester,Operator,Support Agent,End User", "Roles doc did not return the locked six-role model.");
|
|
2977
|
+
const operatingGuideDoc = await callJsonTool("get_doc", { page: "operating_guide" });
|
|
2978
|
+
assert(operatingGuideDoc.surfaceFingerprint === status.server.surfaceFingerprint, "Operating guide doc fingerprint did not match runtime_status.");
|
|
2979
|
+
const operatingGuideText = JSON.stringify(operatingGuideDoc);
|
|
2980
|
+
assert(
|
|
2981
|
+
operatingGuideDoc.roles?.some(
|
|
2982
|
+
(role) => role.name === "Engineer and Codex assistance" && role.use.includes("Requirement to Task")
|
|
2983
|
+
) &&
|
|
2984
|
+
operatingGuideDoc.roles?.some((role) => role.name === "Operator and administration" && role.defaultRecords.includes("Incident") && role.defaultRecords.includes("Problem") && role.defaultRecords.includes("Maintenance Schedule")) &&
|
|
2985
|
+
operatingGuideDoc.roles?.some((role) => role.name === "Support Agent" && role.defaultRecords.includes("Support Request")) &&
|
|
2986
|
+
operatingGuideText.includes("specific service-impacting") &&
|
|
2987
|
+
operatingGuideText.includes("DX Complete Ticket"),
|
|
2988
|
+
"Operating guide doc should include role routing, Change boundary, tickets, and Incident/Problem guidance."
|
|
2989
|
+
);
|
|
2685
2990
|
|
|
2686
2991
|
const recordsDoc = await callJsonTool("get_doc", { page: "records" });
|
|
2687
2992
|
const recordNames = recordsDoc.groups?.flatMap((group) => group.records.map((record) => record.name)) ?? [];
|
|
@@ -2690,9 +2995,14 @@ async function runDocsSection() {
|
|
|
2690
2995
|
recordNames.includes("Journal") &&
|
|
2691
2996
|
recordNames.includes("Environment") &&
|
|
2692
2997
|
recordNames.includes("Component") &&
|
|
2998
|
+
recordNames.includes("Maintenance Schedule") &&
|
|
2693
2999
|
recordNames.includes("Estimate") &&
|
|
2694
|
-
recordNames.includes("Benefits")
|
|
2695
|
-
|
|
3000
|
+
recordNames.includes("Benefits") &&
|
|
3001
|
+
recordNames.includes("Value Realization") &&
|
|
3002
|
+
recordNames.includes("Incident") &&
|
|
3003
|
+
recordNames.includes("Problem") &&
|
|
3004
|
+
recordNames.includes("Support Request"),
|
|
3005
|
+
"Records doc should include Statement, Journal, Environment, Component, Maintenance Schedule, Estimate, Benefits, Value Realization, Incident, Problem, and Support Request."
|
|
2696
3006
|
);
|
|
2697
3007
|
assert(
|
|
2698
3008
|
recordsDoc.routingGuidance?.sharpTest?.includes("Will anything reference or depend on this") &&
|
|
@@ -2719,8 +3029,26 @@ async function runDocsSection() {
|
|
|
2719
3029
|
registryDoc.term?.definition.includes("not monitoring"),
|
|
2720
3030
|
"Operational Registry glossary term should describe inventory-only boundaries."
|
|
2721
3031
|
);
|
|
3032
|
+
const incidentDoc = await callJsonTool("get_doc", { page: "glossary", term: "incident" });
|
|
3033
|
+
assert(
|
|
3034
|
+
incidentDoc.term?.definition.includes("specific service-impacting") &&
|
|
3035
|
+
incidentDoc.term?.definition.includes("ordered entries"),
|
|
3036
|
+
"Incident glossary term should describe the current Incident ledger record."
|
|
3037
|
+
);
|
|
3038
|
+
const problemDoc = await callJsonTool("get_doc", { page: "glossary", term: "problem" });
|
|
3039
|
+
assert(
|
|
3040
|
+
problemDoc.term?.definition.includes("underlying or recurring cause") &&
|
|
3041
|
+
problemDoc.term?.definition.includes("Incidents"),
|
|
3042
|
+
"Problem glossary term should describe the current Problem ledger record."
|
|
3043
|
+
);
|
|
2722
3044
|
const componentDoc = await callJsonTool("get_doc", { page: "glossary", term: "component" });
|
|
2723
3045
|
assert(componentDoc.term?.definition.includes("one Environment"), "Component glossary term should describe one-environment scope.");
|
|
3046
|
+
const maintenanceDoc = await callJsonTool("get_doc", { page: "glossary", term: "maintenance schedule" });
|
|
3047
|
+
assert(maintenanceDoc.term?.definition.includes("cadence"), "Maintenance Schedule glossary term should describe cadence.");
|
|
3048
|
+
const supportRequestDoc = await callJsonTool("get_doc", { page: "glossary", term: "support request" });
|
|
3049
|
+
assert(supportRequestDoc.term?.definition.includes("workspace-visible follow-up"), "Support Request glossary term should describe shared follow-up.");
|
|
3050
|
+
const valueRealizationDoc = await callJsonTool("get_doc", { page: "glossary", term: "value realization" });
|
|
3051
|
+
assert(valueRealizationDoc.term?.definition.includes("baseline") && valueRealizationDoc.term?.definition.includes("actual"), "Value Realization glossary term should describe baseline and actual metrics.");
|
|
2724
3052
|
const secretPointerDoc = await callJsonTool("get_doc", { page: "glossary", term: "secret pointer" });
|
|
2725
3053
|
assert(secretPointerDoc.term?.definition.includes("should not contain the secret value"), "Secret Pointer glossary term should warn against secret values.");
|
|
2726
3054
|
await expectToolError("get_doc", { page: "glossary", term: "voice" }, "Glossary term not found");
|
|
@@ -2841,6 +3169,50 @@ async function runRecordsSection(cleanupRecords) {
|
|
|
2841
3169
|
body: "Invalid task comment.",
|
|
2842
3170
|
status: "done"
|
|
2843
3171
|
}, "status is only valid");
|
|
3172
|
+
const maintenanceSchedule = await callJsonTool("create_maintenance_schedule", {
|
|
3173
|
+
name: `DX Complete hosted MCP smoke maintenance ${runId}`,
|
|
3174
|
+
kind: "temporary smoke maintenance",
|
|
3175
|
+
cadence: { count: 1, unit: "day" },
|
|
3176
|
+
startDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
3177
|
+
rationale: "Exercise recurring operational hygiene tracking."
|
|
3178
|
+
});
|
|
3179
|
+
cleanupRecords.push(["maintenance_schedules", maintenanceSchedule._id]);
|
|
3180
|
+
assert(maintenanceSchedule.recordType === "maintenance_schedules", "Records smoke Maintenance Schedule had the wrong record type.");
|
|
3181
|
+
assert(/^MNT-\d{4,}$/.test(maintenanceSchedule.readableId ?? ""), "Maintenance Schedule did not receive an MNT readable ID.");
|
|
3182
|
+
assert(maintenanceSchedule.derived?.overdue === true, "Maintenance Schedule should be overdue before a linked completion.");
|
|
3183
|
+
await callJsonTool("link_records", {
|
|
3184
|
+
fromType: "tasks",
|
|
3185
|
+
fromId: task._id,
|
|
3186
|
+
toType: "maintenance_schedules",
|
|
3187
|
+
toId: maintenanceSchedule._id,
|
|
3188
|
+
relationship: "assigned_to"
|
|
3189
|
+
});
|
|
3190
|
+
await callJsonTool("append_task_entry", {
|
|
3191
|
+
taskId: task._id,
|
|
3192
|
+
entryType: "status_change",
|
|
3193
|
+
body: "Maintenance task completed.",
|
|
3194
|
+
status: "done"
|
|
3195
|
+
});
|
|
3196
|
+
const maintenanceAfterCompletion = await callJsonTool("get_record", {
|
|
3197
|
+
recordType: "maintenance_schedules",
|
|
3198
|
+
id: maintenanceSchedule._id
|
|
3199
|
+
});
|
|
3200
|
+
assert(
|
|
3201
|
+
maintenanceAfterCompletion.derived?.occurrenceCount >= 1 &&
|
|
3202
|
+
maintenanceAfterCompletion.derived?.latestOccurrence?.recordType === "tasks",
|
|
3203
|
+
"Maintenance Schedule did not derive occurrence state from a linked completed Task."
|
|
3204
|
+
);
|
|
3205
|
+
const updatedMaintenanceSchedule = await callJsonTool("update_maintenance_schedule", {
|
|
3206
|
+
id: maintenanceSchedule._id,
|
|
3207
|
+
notes: "Updated selected records smoke maintenance schedule.",
|
|
3208
|
+
revisionNote: "Selected records smoke maintenance revision."
|
|
3209
|
+
});
|
|
3210
|
+
assert(updatedMaintenanceSchedule.fields.versionHistory?.length === 1, "Maintenance Schedule update did not preserve version history.");
|
|
3211
|
+
await expectToolError("update_record", {
|
|
3212
|
+
recordType: "maintenance_schedules",
|
|
3213
|
+
id: maintenanceSchedule._id,
|
|
3214
|
+
fields: { cadence: { count: 1, unit: "week" } }
|
|
3215
|
+
}, "update_maintenance_schedule");
|
|
2844
3216
|
await expectToolError("update_record", {
|
|
2845
3217
|
recordType: "tasks",
|
|
2846
3218
|
id: task._id,
|
|
@@ -3016,6 +3388,38 @@ async function runRecordsSection(cleanupRecords) {
|
|
|
3016
3388
|
id: component._id,
|
|
3017
3389
|
unsetFields: ["versionHistory"]
|
|
3018
3390
|
}, "versionHistory");
|
|
3391
|
+
const supportRequest = await callJsonTool("create_support_request", {
|
|
3392
|
+
title: `DX Complete hosted MCP smoke support request ${runId}`,
|
|
3393
|
+
reporter: "Smoke End User",
|
|
3394
|
+
kind: "question",
|
|
3395
|
+
reportedExperience: "The smoke user asked for help with a temporary workflow."
|
|
3396
|
+
});
|
|
3397
|
+
cleanupRecords.push(["support_requests", supportRequest._id]);
|
|
3398
|
+
assert(supportRequest.recordType === "support_requests", "Records smoke Support Request had the wrong record type.");
|
|
3399
|
+
assert(/^SUP-\d{4,}$/.test(supportRequest.readableId ?? ""), "Support Request did not receive an SUP readable ID.");
|
|
3400
|
+
assert(supportRequest.fields.currentStatus?.status === "open", "Support Request did not derive initial open status.");
|
|
3401
|
+
const triagedSupportRequest = await callJsonTool("append_support_request_entry", {
|
|
3402
|
+
supportRequestId: supportRequest._id,
|
|
3403
|
+
entryType: "triage",
|
|
3404
|
+
body: "Support request triaged."
|
|
3405
|
+
});
|
|
3406
|
+
assert(triagedSupportRequest.fields.currentStatus?.status === "triaged", "Support Request triage did not derive current status.");
|
|
3407
|
+
const resolvedSupportRequest = await callJsonTool("append_support_request_entry", {
|
|
3408
|
+
supportRequestId: supportRequest._id,
|
|
3409
|
+
entryType: "resolved",
|
|
3410
|
+
body: "Support request resolved."
|
|
3411
|
+
});
|
|
3412
|
+
assert(resolvedSupportRequest.fields.currentStatus?.status === "resolved", "Support Request resolution did not derive current status.");
|
|
3413
|
+
await expectToolError("create_record", {
|
|
3414
|
+
recordType: "support_requests",
|
|
3415
|
+
title: `DX Complete hosted MCP direct support request entries ${runId}`,
|
|
3416
|
+
fields: { entries: [] }
|
|
3417
|
+
}, "append_support_request_entry");
|
|
3418
|
+
await expectToolError("append_support_request_entry", {
|
|
3419
|
+
supportRequestId: supportRequest._id,
|
|
3420
|
+
entryType: "escalated",
|
|
3421
|
+
body: "Missing incident."
|
|
3422
|
+
}, "incidentId");
|
|
3019
3423
|
await assertReadableIdsContiguous();
|
|
3020
3424
|
await expectToolError("update_record", {
|
|
3021
3425
|
recordType: "requirements",
|
|
@@ -3085,6 +3489,54 @@ async function runWeighSection(cleanupRecords) {
|
|
|
3085
3489
|
relationship: "informed_by"
|
|
3086
3490
|
});
|
|
3087
3491
|
assert(commitmentWithBenefits.links.some((link) => link.toType === "benefits" && link.relationship === "informed_by"), "Weigh smoke did not link Commitment to Benefits.");
|
|
3492
|
+
const valueRealization = await callJsonTool("create_value_realization", {
|
|
3493
|
+
title: `DX Complete hosted MCP smoke value realization ${runId}`,
|
|
3494
|
+
requirementIds: [requirement._id],
|
|
3495
|
+
commitmentIds: [commitment._id],
|
|
3496
|
+
metrics: [
|
|
3497
|
+
{
|
|
3498
|
+
name: "Cycle time",
|
|
3499
|
+
unit: "hours",
|
|
3500
|
+
direction: "lower_is_better",
|
|
3501
|
+
baseline: { value: 10, measuredAt: "2026-06-01T00:00:00.000Z" },
|
|
3502
|
+
actual: { value: 7, measuredAt: "2026-06-02T00:00:00.000Z" }
|
|
3503
|
+
},
|
|
3504
|
+
{
|
|
3505
|
+
name: "Satisfaction signal",
|
|
3506
|
+
unit: "score",
|
|
3507
|
+
direction: "higher_is_better",
|
|
3508
|
+
baseline: { value: 3, measuredAt: "2026-06-01T00:00:00.000Z" }
|
|
3509
|
+
}
|
|
3510
|
+
]
|
|
3511
|
+
});
|
|
3512
|
+
cleanupRecords.push(["value_realizations", valueRealization._id]);
|
|
3513
|
+
assert(valueRealization.recordType === "value_realizations", "Weigh smoke Value Realization had the wrong record type.");
|
|
3514
|
+
assert(/^VAL-\d{4,}$/.test(valueRealization.readableId ?? ""), "Value Realization did not receive a VAL readable ID.");
|
|
3515
|
+
assert(
|
|
3516
|
+
valueRealization.derived?.comparisons?.some((comparison) => comparison.name === "Cycle time" && comparison.outcome === "improved") &&
|
|
3517
|
+
valueRealization.derived?.comparisons?.some((comparison) => comparison.name === "Satisfaction signal" && comparison.status === "open"),
|
|
3518
|
+
"Value Realization did not derive measured/open comparisons."
|
|
3519
|
+
);
|
|
3520
|
+
const updatedValueRealization = await callJsonTool("update_value_realization", {
|
|
3521
|
+
id: valueRealization._id,
|
|
3522
|
+
metrics: [
|
|
3523
|
+
{
|
|
3524
|
+
id: valueRealization.fields.metrics[0].id,
|
|
3525
|
+
name: "Cycle time",
|
|
3526
|
+
unit: "hours",
|
|
3527
|
+
direction: "lower_is_better",
|
|
3528
|
+
baseline: { value: 10, measuredAt: "2026-06-01T00:00:00.000Z" },
|
|
3529
|
+
actual: { value: 6, measuredAt: "2026-06-03T00:00:00.000Z" }
|
|
3530
|
+
}
|
|
3531
|
+
],
|
|
3532
|
+
revisionNote: "Weigh smoke value realization revision."
|
|
3533
|
+
});
|
|
3534
|
+
assert(updatedValueRealization.fields.versionHistory?.length === 1, "Value Realization update did not preserve version history.");
|
|
3535
|
+
await expectToolError("update_record", {
|
|
3536
|
+
recordType: "value_realizations",
|
|
3537
|
+
id: valueRealization._id,
|
|
3538
|
+
fields: { metrics: [] }
|
|
3539
|
+
}, "update_value_realization");
|
|
3088
3540
|
}
|
|
3089
3541
|
|
|
3090
3542
|
async function runChangeSection(cleanupRecords) {
|
|
@@ -3342,6 +3794,8 @@ async function assertReadableIdsContiguous() {
|
|
|
3342
3794
|
deferrals: "DFR",
|
|
3343
3795
|
decisions: "DEC",
|
|
3344
3796
|
changes: "CHG",
|
|
3797
|
+
incidents: "INC",
|
|
3798
|
+
problems: "PRB",
|
|
3345
3799
|
risks: "RSK",
|
|
3346
3800
|
estimates: "EST",
|
|
3347
3801
|
benefits: "BFT"
|